Added an app updated modal when the app has been updated
This commit is contained in:
parent
b0aa0c4c1a
commit
4557479e09
12 changed files with 665 additions and 8 deletions
|
@ -1,9 +1,6 @@
|
||||||
import {LaterPromise} from "tc-shared/utils/LaterPromise";
|
import {LaterPromise} from "tc-shared/utils/LaterPromise";
|
||||||
import {ErrorCode} from "./ErrorCode";
|
import {ErrorCode} from "./ErrorCode";
|
||||||
|
|
||||||
/* legacy */
|
|
||||||
export const ErrorID = ErrorCode;
|
|
||||||
|
|
||||||
export class CommandResult {
|
export class CommandResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
|
|
|
@ -44,6 +44,9 @@ import "./connection/ConnectionBase";
|
||||||
import {ConnectRequestData} from "tc-shared/ipc/ConnectHandler";
|
import {ConnectRequestData} from "tc-shared/ipc/ConnectHandler";
|
||||||
import "./video-viewer/Controller";
|
import "./video-viewer/Controller";
|
||||||
|
|
||||||
|
import "./update/UpdaterWeb";
|
||||||
|
import {checkForUpdatedApp} from "tc-shared/update";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
open_connected_question: () => Promise<boolean>;
|
open_connected_question: () => Promise<boolean>;
|
||||||
|
@ -299,7 +302,7 @@ function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
server_connections.set_active_connection(server_connections.all_connections()[0]);
|
server_connections.set_active_connection(server_connections.all_connections()[0]);
|
||||||
|
checkForUpdatedApp();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
(window as any).test_upload = (message?: string) => {
|
(window as any).test_upload = (message?: string) => {
|
||||||
|
|
|
@ -347,5 +347,5 @@ loader.register_task(Stage.LOADED, {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
priority: -2
|
priority: -2
|
||||||
})
|
});
|
||||||
*/
|
*/
|
24
shared/js/ui/modal/whats-new/Controller.tsx
Normal file
24
shared/js/ui/modal/whats-new/Controller.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||||
|
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||||
|
import * as React from "react";
|
||||||
|
import {WhatsNew} from "tc-shared/ui/modal/whats-new/Renderer";
|
||||||
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
|
import {ChangeLog} from "tc-shared/update/ChangeLog";
|
||||||
|
|
||||||
|
export function spawnUpdatedModal(changes: { changesUI?: ChangeLog, changesClient?: ChangeLog }) {
|
||||||
|
const modal = spawnReactModal(class extends InternalModal {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBody(): React.ReactElement {
|
||||||
|
return <WhatsNew changesUI={changes.changesUI} changesClient={changes.changesClient} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
title(): string | React.ReactElement<Translatable> {
|
||||||
|
return <Translatable>We've updated the client for you</Translatable>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.show();
|
||||||
|
}
|
268
shared/js/ui/modal/whats-new/Renderer.scss
Normal file
268
shared/js/ui/modal/whats-new/Renderer.scss
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
@import "../../../../css/static/mixin";
|
||||||
|
@import "../../../../css/static/properties";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 57em;
|
||||||
|
min-width: 20em;
|
||||||
|
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
@include user-select(none);
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
width: 15em;
|
||||||
|
height: 15em;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-right: 2em;
|
||||||
|
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
all: unset;
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
all: unset;
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin-bottom: .3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitleShort {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitleLong {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
all: unset;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.changes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
margin-top: 1.5em;
|
||||||
|
|
||||||
|
padding-bottom: .5em;
|
||||||
|
padding-right: 1em;
|
||||||
|
padding-left: 1em;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
height: 2px;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
@include transition($button_hover_animation_time ease-in-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left, .right {
|
||||||
|
font-size: 1.4em;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
@include transition($button_hover_animation_time ease-in-out);
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #b6c4d6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selectedLeft {
|
||||||
|
.left {
|
||||||
|
color: #245184;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
background: linear-gradient(to right, #245184 15em, #999999 60%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selectedRight {
|
||||||
|
.right {
|
||||||
|
color: #245184;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
background: linear-gradient(to left, #245184 15em, #999999 60%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
margin-top: .5em;
|
||||||
|
height: 20em;
|
||||||
|
|
||||||
|
background: #28292b;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
font-family: consolas, monospace;
|
||||||
|
|
||||||
|
.changeList {
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
padding: .4em .8em .8em;
|
||||||
|
|
||||||
|
min-height: 1em;
|
||||||
|
height: 20em;
|
||||||
|
|
||||||
|
overflow-x: hidden;
|
||||||
|
@include chat-scrollbar-vertical();
|
||||||
|
|
||||||
|
@include user-select(text);
|
||||||
|
|
||||||
|
> ul > li {
|
||||||
|
margin-top: 1em;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
all: unset;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
margin-left: 1em;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
|
||||||
|
li:before {
|
||||||
|
content: '-';
|
||||||
|
margin-left: -1em;
|
||||||
|
margin-right: .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: hsl(0, 0%, 11%);
|
||||||
|
border-radius: 2px;
|
||||||
|
color: hsl(0, 0%, 50%);
|
||||||
|
|
||||||
|
padding: 0 .25em;
|
||||||
|
margin-left: -.25em;
|
||||||
|
margin-right: -.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.containerBrowse {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
color: #245184;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
right: .5em;
|
||||||
|
|
||||||
|
padding: 1em .5em .5em;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
background-color: #28292bcc;
|
||||||
|
@include transition($button_hover_animation_time ease-in-out);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #28292b;
|
||||||
|
color: #b6c4d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href]:visited {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 40em) {
|
||||||
|
.container .logo {
|
||||||
|
display: none!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container .text {
|
||||||
|
.subtitleShort {
|
||||||
|
display: block!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitleLong {
|
||||||
|
display: none!important;;
|
||||||
|
}
|
||||||
|
|
||||||
|
p br {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
141
shared/js/ui/modal/whats-new/Renderer.tsx
Normal file
141
shared/js/ui/modal/whats-new/Renderer.tsx
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as dompurify from "dompurify";
|
||||||
|
import {useState} from "react";
|
||||||
|
import {ChangeLog, ChangeLogEntry, ChangeSet} from "tc-shared/update/ChangeLog";
|
||||||
|
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
|
import {guid} from "tc-shared/crypto/uid";
|
||||||
|
|
||||||
|
const { Remarkable } = require("remarkable");
|
||||||
|
const cssStyle = require("./Renderer.scss");
|
||||||
|
|
||||||
|
const mdRenderer = new Remarkable();
|
||||||
|
const brElementUuid = guid();
|
||||||
|
|
||||||
|
export interface DisplayableChangeList extends ChangeLog {
|
||||||
|
title: React.ReactElement<Translatable>;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
mdRenderer.renderer.rules.link_open = function (tokens, idx, options /*, env */) {
|
||||||
|
const title = tokens[idx].title ? (`title="${tokens[idx].title}"`) : '';
|
||||||
|
const target = options.linkTarget ? (`target="${options.linkTarget}"`) : '';
|
||||||
|
|
||||||
|
let href = `<a href="${tokens[idx].href}" ${title} ${target} ></a>`;
|
||||||
|
href = dompurify.sanitize(href);
|
||||||
|
if(href.substr(-4) !== "</a>")
|
||||||
|
return "<-- invalid link open... -->";
|
||||||
|
return href.substr(0, href.length - 4);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChangeSetRenderer = (props: { set: ChangeSet }) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{props.set.title ? <li key={"title"}>{props.set.title}</li> : undefined}
|
||||||
|
<ul>
|
||||||
|
{props.set.changes.map((change, index) => typeof change === "string" ? <li key={index} dangerouslySetInnerHTML={{
|
||||||
|
__html: mdRenderer.renderInline(change.replace(/\n/g, brElementUuid)).replace(new RegExp(brElementUuid, "g"), "<br />")
|
||||||
|
}} /> : <ChangeSetRenderer set={change} key={index} />)}
|
||||||
|
</ul>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChangeLogEntryRenderer = React.memo((props: { entry: ChangeLogEntry }) => (
|
||||||
|
<li>
|
||||||
|
<b>{props.entry.timestamp}</b>
|
||||||
|
<ChangeSetRenderer set={props.entry} />
|
||||||
|
</li>
|
||||||
|
));
|
||||||
|
|
||||||
|
const DisplayableChangeListRenderer = (props: { list: DisplayableChangeList | undefined, visible: boolean }) => (
|
||||||
|
<div className={cssStyle.body + " " + (!props.visible ? cssStyle.hidden : "")}>
|
||||||
|
<div className={cssStyle.changeList}>
|
||||||
|
<ul>
|
||||||
|
{props.list?.changes.map((value, index) => <ChangeLogEntryRenderer entry={value} key={index} />)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.containerBrowse}>
|
||||||
|
<a href={props.list?.url} target={"_blank"}><Translatable>Open full Change Log</Translatable></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ChangeListRenderer = (props: { left?: DisplayableChangeList, right?: DisplayableChangeList, defaultSelected: "right" | "left" | "none" }) => {
|
||||||
|
const [ selected, setSelected ] = useState<"left" | "right" | "none">(props.defaultSelected);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.changes}>
|
||||||
|
<div className={cssStyle.header + " " + (selected === "left" ? cssStyle.selectedLeft : selected === "right" ? cssStyle.selectedRight : "")}>
|
||||||
|
<div className={cssStyle.left + " " + (props.left ? "" : cssStyle.hidden)} onClick={() => setSelected("left")}>
|
||||||
|
<a>{props.left?.title}</a>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.right + " " + (props.right ? "" : cssStyle.hidden)} onClick={() => setSelected("right")}>
|
||||||
|
<a>{props.right?.title}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DisplayableChangeListRenderer list={props.left} visible={selected === "left"} />
|
||||||
|
<DisplayableChangeListRenderer list={props.right} visible={selected === "right"} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WhatsNew = (props: { changesUI?: ChangeLog, changesClient?: ChangeLog }) => {
|
||||||
|
let subtitleLong, infoText;
|
||||||
|
|
||||||
|
let changesUI = props.changesUI ? Object.assign({
|
||||||
|
title: <Translatable>UI Change Log</Translatable>,
|
||||||
|
url: "https://github.com/TeaSpeak/TeaWeb/blob/master/ChangeLog.md"
|
||||||
|
}, props.changesUI) : undefined;
|
||||||
|
|
||||||
|
let changesClient = props.changesClient ? Object.assign({
|
||||||
|
title: <Translatable>Client Change Log</Translatable>,
|
||||||
|
url: "https://github.com/TeaSpeak/TeaClient/blob/master/ChangeLog.txt"
|
||||||
|
}, props.changesClient) : undefined;
|
||||||
|
|
||||||
|
let versionUIDate = props.changesUI?.currentVersion, versionNativeDate = props.changesClient?.currentVersion;
|
||||||
|
if(__build.target === "web") {
|
||||||
|
subtitleLong = <Translatable key={"sub-web"}>We've successfully updated the web client for you.</Translatable>;
|
||||||
|
infoText = <VariadicTranslatable key={"info-web"} text={"The web client has been updated to the version from {}."}>{versionUIDate}</VariadicTranslatable>;
|
||||||
|
} else if(props.changesUI && props.changesClient) {
|
||||||
|
subtitleLong = <Translatable key={"sub-native-client-ui"}>We've successfully updated the native client and its UI for you.</Translatable>;
|
||||||
|
infoText = (
|
||||||
|
<React.Fragment key={"info-native-client-ui"}>
|
||||||
|
<VariadicTranslatable text={"The native client has been updated to the version from {}."}>{versionNativeDate}</VariadicTranslatable>
|
||||||
|
<VariadicTranslatable text={"Its UI has been updated to the version {}."}>{versionUIDate}</VariadicTranslatable>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
} else if(props.changesClient) {
|
||||||
|
subtitleLong = <Translatable key={"sub-native-client"}>We've successfully updated the native client for you.</Translatable>;
|
||||||
|
infoText = <VariadicTranslatable key={"info-native-client"} text={"The native client has been updated to the version {}."}>{versionNativeDate}</VariadicTranslatable>;
|
||||||
|
} else if(props.changesUI) {
|
||||||
|
subtitleLong = <Translatable key={"sub-native-ui"}>We've successfully updated the native clients UI for you.</Translatable>;
|
||||||
|
infoText = <VariadicTranslatable key={"info-native-ui"} text={"The native clients UI has been updated to the version from 18.08.2020."}>{versionUIDate}</VariadicTranslatable>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const changes = [ changesUI, changesClient ].filter(e => !!e);
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.container}>
|
||||||
|
<div className={cssStyle.info}>
|
||||||
|
<div className={cssStyle.logo}>
|
||||||
|
<img alt={tr("TeaSpeak logo")} src="img/teaspeak_cup_animated.png" />
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.text}>
|
||||||
|
<h1><Translatable>Welcome back!</Translatable></h1>
|
||||||
|
<h2 className={cssStyle.subtitleLong}>{subtitleLong}</h2>
|
||||||
|
<h2 className={cssStyle.subtitleShort}><Translatable>The client has been updated.</Translatable></h2>
|
||||||
|
<p>
|
||||||
|
<Translatable>While you've been away resting, we did some work.</Translatable> <br />
|
||||||
|
{infoText} <br />
|
||||||
|
<Translatable>A list of changes, bugfixes and new features can be found bellow.</Translatable>
|
||||||
|
</p>
|
||||||
|
<a><Translatable>Enjoy!</Translatable></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChangeListRenderer
|
||||||
|
defaultSelected={"right"}
|
||||||
|
right={changes[0]}
|
||||||
|
left={changes[1]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -50,7 +50,8 @@ export class Translatable extends React.Component<{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VariadicTranslatable = (props: { text: string, __cacheKey?: string, children?: React.ReactElement[] | React.ReactElement }) => {
|
export type VariadicTranslatableChild = React.ReactElement | string;
|
||||||
|
export const VariadicTranslatable = (props: { text: string, __cacheKey?: string, children?: VariadicTranslatableChild[] | VariadicTranslatableChild }) => {
|
||||||
const args = Array.isArray(props.children) ? props.children : [props.children];
|
const args = Array.isArray(props.children) ? props.children : [props.children];
|
||||||
const argsUseCount = [...new Array(args.length)].map(() => 0);
|
const argsUseCount = [...new Array(args.length)].map(() => 0);
|
||||||
|
|
||||||
|
@ -63,8 +64,13 @@ export const VariadicTranslatable = (props: { text: string, __cacheKey?: string,
|
||||||
return e;
|
return e;
|
||||||
|
|
||||||
let element = args[e];
|
let element = args[e];
|
||||||
if(argsUseCount[e])
|
if(argsUseCount[e]) {
|
||||||
element = cloneElement(element);
|
if(typeof element === "string") {
|
||||||
|
/* do nothing */
|
||||||
|
} else {
|
||||||
|
element = cloneElement(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
argsUseCount[e]++;
|
argsUseCount[e]++;
|
||||||
|
|
||||||
return <React.Fragment key={"argument-" + e + "-" + argsUseCount[e]}>{element}</React.Fragment>;
|
return <React.Fragment key={"argument-" + e + "-" + argsUseCount[e]}>{element}</React.Fragment>;
|
||||||
|
|
15
shared/js/update/ChangeLog.ts
Normal file
15
shared/js/update/ChangeLog.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
export type ChangeSetEntry = ChangeSet | string;
|
||||||
|
|
||||||
|
export interface ChangeSet {
|
||||||
|
title?: string;
|
||||||
|
changes: ChangeSetEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangeLogEntry extends ChangeSet {
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangeLog {
|
||||||
|
changes: ChangeLogEntry[],
|
||||||
|
currentVersion: string
|
||||||
|
}
|
11
shared/js/update/Updater.ts
Normal file
11
shared/js/update/Updater.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import {ChangeLog} from "tc-shared/update/ChangeLog";
|
||||||
|
|
||||||
|
export interface Updater {
|
||||||
|
getChangeLog() : ChangeLog;
|
||||||
|
|
||||||
|
getLastUsedVersion() : string;
|
||||||
|
getCurrentVersion() : string;
|
||||||
|
|
||||||
|
/* update the last used version to the current version */
|
||||||
|
updateUsedVersion();
|
||||||
|
}
|
130
shared/js/update/UpdaterWeb.ts
Normal file
130
shared/js/update/UpdaterWeb.ts
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
import {ChangeLog, ChangeSetEntry} from "tc-shared/update/ChangeLog";
|
||||||
|
import * as loader from "tc-loader";
|
||||||
|
import {Stage} from "tc-loader";
|
||||||
|
import {setUIUpdater} from "tc-shared/update/index";
|
||||||
|
import {Updater} from "tc-shared/update/Updater";
|
||||||
|
|
||||||
|
const ChangeLogContents: string = require("../../../ChangeLog.md");
|
||||||
|
const EntryRegex = /^\* \*\*([0-9]{2})\.([0-9]{2})\.([0-9]{2})\*\*$/m;
|
||||||
|
|
||||||
|
function parseChangeLogEntry(lines: string[], index: number) : { entries: ChangeSetEntry[], index: number } {
|
||||||
|
const entryDepth = lines[index].indexOf("-");
|
||||||
|
if(entryDepth === -1) {
|
||||||
|
throw "missing entry depth for line " + index;
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = [] as ChangeSetEntry[];
|
||||||
|
let currentEntry;
|
||||||
|
while(index < lines.length && !lines[index].match(EntryRegex)) {
|
||||||
|
let trimmed = lines[index].trim();
|
||||||
|
if(trimmed.length === 0) {
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(trimmed[0] === '-') {
|
||||||
|
const depth = lines[index].indexOf('-');
|
||||||
|
if(depth > entryDepth) {
|
||||||
|
if(typeof currentEntry === "undefined")
|
||||||
|
throw "missing change child entries parent at line " + index;
|
||||||
|
|
||||||
|
const result = parseChangeLogEntry(lines, index);
|
||||||
|
entries.push({
|
||||||
|
changes: result.entries,
|
||||||
|
title: currentEntry
|
||||||
|
});
|
||||||
|
index = result.index;
|
||||||
|
} else if(depth < entryDepth) {
|
||||||
|
/* we're done with our block */
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
/* new entry */
|
||||||
|
if(typeof currentEntry === "string")
|
||||||
|
entries.push(currentEntry);
|
||||||
|
|
||||||
|
currentEntry = trimmed.substr(1).trim();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(typeof currentEntry === "undefined")
|
||||||
|
throw "this should never happen!";
|
||||||
|
|
||||||
|
currentEntry += "\n" + trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof currentEntry === "string")
|
||||||
|
entries.push(currentEntry);
|
||||||
|
|
||||||
|
return {
|
||||||
|
index: index,
|
||||||
|
entries: entries
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUIChangeLog() : ChangeLog {
|
||||||
|
let result: ChangeLog = {
|
||||||
|
currentVersion: "unknown",
|
||||||
|
changes: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = ChangeLogContents.split("\n");
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
while(index < lines.length && !lines[index].match(EntryRegex))
|
||||||
|
index++;
|
||||||
|
|
||||||
|
while(index < lines.length) {
|
||||||
|
const [ _, day, month, year ] = lines[index].match(EntryRegex);
|
||||||
|
|
||||||
|
const entry = parseChangeLogEntry(lines, index + 1);
|
||||||
|
result.changes.push({
|
||||||
|
timestamp: day + "." + month + "." + year,
|
||||||
|
changes: entry.entries
|
||||||
|
});
|
||||||
|
|
||||||
|
index = entry.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kLastUsedVersionKey = "updater-used-version-web";
|
||||||
|
class WebUpdater implements Updater {
|
||||||
|
private readonly changeLog: ChangeLog;
|
||||||
|
private readonly currentVersion: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.changeLog = parseUIChangeLog();
|
||||||
|
|
||||||
|
const currentBuildTimestamp = new Date(__build.timestamp * 1000);
|
||||||
|
this.currentVersion = ("00" + currentBuildTimestamp.getUTCDate()).substr(-2) + "." +
|
||||||
|
("00" + currentBuildTimestamp.getUTCMonth()).substr(-2) + "." +
|
||||||
|
currentBuildTimestamp.getUTCFullYear().toString().substr(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
getChangeLog(): ChangeLog {
|
||||||
|
return this.changeLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentVersion(): string {
|
||||||
|
return this.currentVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastUsedVersion(): string {
|
||||||
|
return localStorage.getItem(kLastUsedVersionKey) || "08.08.20";
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUsedVersion() {
|
||||||
|
localStorage.setItem(kLastUsedVersionKey, this.getCurrentVersion());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
|
name: "web updater init",
|
||||||
|
function: async () => {
|
||||||
|
setUIUpdater(new WebUpdater());
|
||||||
|
},
|
||||||
|
priority: 50
|
||||||
|
});
|
55
shared/js/update/index.ts
Normal file
55
shared/js/update/index.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import {Updater} from "./Updater";
|
||||||
|
import {ChangeLog} from "tc-shared/update/ChangeLog";
|
||||||
|
import {spawnUpdatedModal} from "tc-shared/ui/modal/whats-new/Controller";
|
||||||
|
|
||||||
|
let updaterUi: Updater;
|
||||||
|
let updaterNative: Updater;
|
||||||
|
|
||||||
|
export function setUIUpdater(updater: Updater) {
|
||||||
|
if(typeof updaterUi !== "undefined") {
|
||||||
|
throw tr("An UI updater has already been registered");
|
||||||
|
}
|
||||||
|
updaterUi = updater;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setNativeUpdater(updater: Updater) {
|
||||||
|
if(typeof updaterNative !== "undefined") {
|
||||||
|
throw tr("An native updater has already been registered");
|
||||||
|
}
|
||||||
|
updaterNative = updater;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChangedChangeLog(updater: Updater) : ChangeLog | undefined {
|
||||||
|
if(updater.getCurrentVersion() === updater.getLastUsedVersion())
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
let changes = {
|
||||||
|
changes: [],
|
||||||
|
currentVersion: updater.getCurrentVersion()
|
||||||
|
} as ChangeLog;
|
||||||
|
|
||||||
|
let usedVersion = updater.getLastUsedVersion();
|
||||||
|
for(const change of updater.getChangeLog().changes) {
|
||||||
|
if(change.timestamp === usedVersion)
|
||||||
|
break;
|
||||||
|
|
||||||
|
changes.changes.push(change);
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes.changes.length > 0 ? changes : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkForUpdatedApp() {
|
||||||
|
let changesUI = updaterUi ? getChangedChangeLog(updaterUi) : undefined;
|
||||||
|
let changesNative = updaterNative ? getChangedChangeLog(updaterNative) : undefined;
|
||||||
|
|
||||||
|
if(changesUI !== undefined || changesNative !== undefined) {
|
||||||
|
spawnUpdatedModal({
|
||||||
|
changesUI: changesUI,
|
||||||
|
changesClient: changesNative
|
||||||
|
});
|
||||||
|
|
||||||
|
updaterUi?.updateUsedVersion();
|
||||||
|
updaterNative?.updateUsedVersion();
|
||||||
|
}
|
||||||
|
}
|
|
@ -226,6 +226,13 @@ export const config = async (target: "web" | "client"): Promise<Configuration> =
|
||||||
{
|
{
|
||||||
test: /\.svg$/,
|
test: /\.svg$/,
|
||||||
loader: 'svg-inline-loader'
|
loader: 'svg-inline-loader'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /ChangeLog\.md$/i,
|
||||||
|
loader: "raw-loader",
|
||||||
|
options: {
|
||||||
|
esModule: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Reference in a new issue