Make the about mopdal popoutable
parent
d4179db329
commit
943c33ff4b
|
@ -6,7 +6,6 @@ import "./static/htmltags.scss"
|
||||||
import "./static/mixin.scss"
|
import "./static/mixin.scss"
|
||||||
import "./static/modal.scss"
|
import "./static/modal.scss"
|
||||||
import "./static/modals.scss"
|
import "./static/modals.scss"
|
||||||
import "./static/modal-about.scss"
|
|
||||||
import "./static/modal-avatar.scss"
|
import "./static/modal-avatar.scss"
|
||||||
import "./static/modal-banclient.scss"
|
import "./static/modal-banclient.scss"
|
||||||
import "./static/modal-banlist.scss"
|
import "./static/modal-banlist.scss"
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
:global {
|
|
||||||
.modal-about {
|
|
||||||
display: flex!important;
|
|
||||||
flex-direction: row!important;
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
color: #999999;
|
|
||||||
|
|
||||||
.container-left {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-right {
|
|
||||||
text-align: left;
|
|
||||||
padding-left: 2em;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.5em;
|
|
||||||
margin-block-start: 0.35em;
|
|
||||||
margin-block-end: 0.35em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.25em;
|
|
||||||
margin-block-start: 0.10em;
|
|
||||||
margin-block-end: 0.10em;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-block-start: .25em;
|
|
||||||
margin-block-end: .25em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.version {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: stretch;
|
|
||||||
|
|
||||||
a {
|
|
||||||
width: 50%;
|
|
||||||
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
.value {
|
|
||||||
padding-left: .25em;
|
|
||||||
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="matrix(.03125 0 0 .03125 -3.5723e-5 0)" fill="#7289da">
|
||||||
|
<circle cx="48" cy="304" r="24"/>
|
||||||
|
<circle cx="192" cy="448" r="24"/>
|
||||||
|
<circle cx="101" cy="395" r="24"/>
|
||||||
|
<circle cx="48" cy="304" r="24"/>
|
||||||
|
<path d="m216 448a24 24 0 10-24 24 24 24 0 0024-24"/>
|
||||||
|
<circle cx="101" cy="395" r="24"/>
|
||||||
|
<circle cx="48" cy="192" r="24"/>
|
||||||
|
<circle cx="192" cy="48" r="24"/>
|
||||||
|
<circle cx="101" cy="101" r="24"/>
|
||||||
|
<circle cx="48" cy="192" r="24"/>
|
||||||
|
<path d="m216 48a24 24 0 11-24-24 24 24 0 0124 24"/>
|
||||||
|
<circle cx="101" cy="101" r="24"/>
|
||||||
|
<path d="m311.99 472a24 24 0 01-6.433-47.123 185.51 185.51 0 0096.328-64.917 181.56 181.56 0 0038.113-111.96c0-81.5-55.349-154.25-134.6-176.92a24 24 0 0113.2-46.147 236.54 236.54 0 01121 82.086 230.51 230.51 0 01.276 282.28 233.82 233.82 0 01-121.42 81.812 24.04 24.04 0 01-6.463.888z"/>
|
||||||
|
<path d="m456.03 488a24.512 24.512 0 01-2.679-.148l-144-16a24 24 0 01-20.474-30.278l40-144a24 24 0 1146.248 12.848l-32.454 116.84 115.98 12.886a24 24 0 01-2.621 47.854z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -10,7 +10,6 @@ import {hashPassword} from "./utils/helpers";
|
||||||
import {HandshakeHandler} from "./connection/HandshakeHandler";
|
import {HandshakeHandler} from "./connection/HandshakeHandler";
|
||||||
import {FilterMode, InputStartError, InputState} from "./voice/RecorderBase";
|
import {FilterMode, InputStartError, InputState} from "./voice/RecorderBase";
|
||||||
import {defaultRecorder, RecorderProfile} from "./voice/RecorderProfile";
|
import {defaultRecorder, RecorderProfile} from "./voice/RecorderProfile";
|
||||||
import {Regex} from "./ui/modal/ModalConnect";
|
|
||||||
import {formatMessage} from "./ui/frames/chat";
|
import {formatMessage} from "./ui/frames/chat";
|
||||||
import {EventHandler, Registry} from "./events";
|
import {EventHandler, Registry} from "./events";
|
||||||
import {FileManager} from "./file/FileManager";
|
import {FileManager} from "./file/FileManager";
|
||||||
|
@ -37,6 +36,7 @@ import {ConnectParameters} from "tc-shared/ui/modal/connect/Controller";
|
||||||
import {assertMainApplication} from "tc-shared/ui/utils";
|
import {assertMainApplication} from "tc-shared/ui/utils";
|
||||||
import {getDNSProvider} from "tc-shared/dns";
|
import {getDNSProvider} from "tc-shared/dns";
|
||||||
import {W2GPluginCmdHandler} from "tc-shared/ui/modal/video-viewer/W2GPlugin";
|
import {W2GPluginCmdHandler} from "tc-shared/ui/modal/video-viewer/W2GPlugin";
|
||||||
|
import ipRegex from "ip-regex";
|
||||||
import * as htmltags from "./ui/htmltags";
|
import * as htmltags from "./ui/htmltags";
|
||||||
|
|
||||||
assertMainApplication();
|
assertMainApplication();
|
||||||
|
@ -328,7 +328,7 @@ export class ConnectionHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(resolvedAddress.host.match(Regex.IP_V4) || resolvedAddress.host.match(Regex.IP_V6)) {
|
if(ipRegex({ exact: true }).test(resolvedAddress.host)) {
|
||||||
/* We don't have to resolve the target host */
|
/* We don't have to resolve the target host */
|
||||||
} else {
|
} else {
|
||||||
this.log.log("connection.hostname.resolve", {});
|
this.log.log("connection.hostname.resolve", {});
|
||||||
|
@ -640,7 +640,7 @@ export class ConnectionHandler {
|
||||||
case DisconnectReason.HANDSHAKE_TEAMSPEAK_REQUIRED:
|
case DisconnectReason.HANDSHAKE_TEAMSPEAK_REQUIRED:
|
||||||
createErrorModal(
|
createErrorModal(
|
||||||
tr("Target server is a TeamSpeak server"),
|
tr("Target server is a TeamSpeak server"),
|
||||||
formatMessage(tr("The target server is a TeamSpeak 3 server!{:br:}Only TeamSpeak 3 based identities are able to connect.{:br:}Please select another profile or change the identify type."))
|
tr("The target server is a TeamSpeak 3 server!\nOnly TeamSpeak 3 based identities are able to connect.\nPlease select another profile or change the identify type.")
|
||||||
).open();
|
).open();
|
||||||
this.sound.play(Sound.CONNECTION_DISCONNECTED);
|
this.sound.play(Sound.CONNECTION_DISCONNECTED);
|
||||||
autoReconnect = false;
|
autoReconnect = false;
|
||||||
|
|
|
@ -1,125 +1,4 @@
|
||||||
import { tr } from "./i18n/localize";
|
import { tr } from "./i18n/localize";
|
||||||
import {LogCategory, logTrace} from "tc-shared/log";
|
|
||||||
|
|
||||||
export enum KeyCode {
|
|
||||||
KEY_CANCEL = 3,
|
|
||||||
KEY_HELP = 6,
|
|
||||||
KEY_BACK_SPACE = 8,
|
|
||||||
KEY_TAB = 9,
|
|
||||||
KEY_CLEAR = 12,
|
|
||||||
KEY_RETURN = 13,
|
|
||||||
KEY_ENTER = 14,
|
|
||||||
KEY_SHIFT = 16,
|
|
||||||
KEY_CONTROL = 17,
|
|
||||||
KEY_ALT = 18,
|
|
||||||
KEY_PAUSE = 19,
|
|
||||||
KEY_CAPS_LOCK = 20,
|
|
||||||
KEY_ESCAPE = 27,
|
|
||||||
KEY_SPACE = 32,
|
|
||||||
KEY_PAGE_UP = 33,
|
|
||||||
KEY_PAGE_DOWN = 34,
|
|
||||||
KEY_END = 35,
|
|
||||||
KEY_HOME = 36,
|
|
||||||
KEY_LEFT = 37,
|
|
||||||
KEY_UP = 38,
|
|
||||||
KEY_RIGHT = 39,
|
|
||||||
KEY_DOWN = 40,
|
|
||||||
KEY_PRINTSCREEN = 44,
|
|
||||||
KEY_INSERT = 45,
|
|
||||||
KEY_DELETE = 46,
|
|
||||||
KEY_0 = 48,
|
|
||||||
KEY_1 = 49,
|
|
||||||
KEY_2 = 50,
|
|
||||||
KEY_3 = 51,
|
|
||||||
KEY_4 = 52,
|
|
||||||
KEY_5 = 53,
|
|
||||||
KEY_6 = 54,
|
|
||||||
KEY_7 = 55,
|
|
||||||
KEY_8 = 56,
|
|
||||||
KEY_9 = 57,
|
|
||||||
KEY_SEMICOLON = 59,
|
|
||||||
KEY_EQUALS = 61,
|
|
||||||
KEY_A = 65,
|
|
||||||
KEY_B = 66,
|
|
||||||
KEY_C = 67,
|
|
||||||
KEY_D = 68,
|
|
||||||
KEY_E = 69,
|
|
||||||
KEY_F = 70,
|
|
||||||
KEY_G = 71,
|
|
||||||
KEY_H = 72,
|
|
||||||
KEY_I = 73,
|
|
||||||
KEY_J = 74,
|
|
||||||
KEY_K = 75,
|
|
||||||
KEY_L = 76,
|
|
||||||
KEY_M = 77,
|
|
||||||
KEY_N = 78,
|
|
||||||
KEY_O = 79,
|
|
||||||
KEY_P = 80,
|
|
||||||
KEY_Q = 81,
|
|
||||||
KEY_R = 82,
|
|
||||||
KEY_S = 83,
|
|
||||||
KEY_T = 84,
|
|
||||||
KEY_U = 85,
|
|
||||||
KEY_V = 86,
|
|
||||||
KEY_W = 87,
|
|
||||||
KEY_X = 88,
|
|
||||||
KEY_Y = 89,
|
|
||||||
KEY_Z = 90,
|
|
||||||
KEY_LEFT_CMD = 91,
|
|
||||||
KEY_RIGHT_CMD = 93,
|
|
||||||
KEY_CONTEXT_MENU = 93,
|
|
||||||
KEY_NUMPAD0 = 96,
|
|
||||||
KEY_NUMPAD1 = 97,
|
|
||||||
KEY_NUMPAD2 = 98,
|
|
||||||
KEY_NUMPAD3 = 99,
|
|
||||||
KEY_NUMPAD4 = 100,
|
|
||||||
KEY_NUMPAD5 = 101,
|
|
||||||
KEY_NUMPAD6 = 102,
|
|
||||||
KEY_NUMPAD7 = 103,
|
|
||||||
KEY_NUMPAD8 = 104,
|
|
||||||
KEY_NUMPAD9 = 105,
|
|
||||||
KEY_MULTIPLY = 106,
|
|
||||||
KEY_ADD = 107,
|
|
||||||
KEY_SEPARATOR = 108,
|
|
||||||
KEY_SUBTRACT = 109,
|
|
||||||
KEY_DECIMAL = 110,
|
|
||||||
KEY_DIVIDE = 111,
|
|
||||||
KEY_F1 = 112,
|
|
||||||
KEY_F2 = 113,
|
|
||||||
KEY_F3 = 114,
|
|
||||||
KEY_F4 = 115,
|
|
||||||
KEY_F5 = 116,
|
|
||||||
KEY_F6 = 117,
|
|
||||||
KEY_F7 = 118,
|
|
||||||
KEY_F8 = 119,
|
|
||||||
KEY_F9 = 120,
|
|
||||||
KEY_F10 = 121,
|
|
||||||
KEY_F11 = 122,
|
|
||||||
KEY_F12 = 123,
|
|
||||||
KEY_F13 = 124,
|
|
||||||
KEY_F14 = 125,
|
|
||||||
KEY_F15 = 126,
|
|
||||||
KEY_F16 = 127,
|
|
||||||
KEY_F17 = 128,
|
|
||||||
KEY_F18 = 129,
|
|
||||||
KEY_F19 = 130,
|
|
||||||
KEY_F20 = 131,
|
|
||||||
KEY_F21 = 132,
|
|
||||||
KEY_F22 = 133,
|
|
||||||
KEY_F23 = 134,
|
|
||||||
KEY_F24 = 135,
|
|
||||||
KEY_NUM_LOCK = 144,
|
|
||||||
KEY_SCROLL_LOCK = 145,
|
|
||||||
KEY_COMMA = 188,
|
|
||||||
KEY_PERIOD = 190,
|
|
||||||
KEY_SLASH = 191,
|
|
||||||
KEY_BACK_QUOTE = 192,
|
|
||||||
KEY_OPEN_BRACKET = 219,
|
|
||||||
KEY_BACK_SLASH = 220,
|
|
||||||
KEY_CLOSE_BRACKET = 221,
|
|
||||||
KEY_QUOTE = 222,
|
|
||||||
KEY_META = 224
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum EventType {
|
export enum EventType {
|
||||||
KEY_PRESS,
|
KEY_PRESS,
|
||||||
|
|
|
@ -442,7 +442,7 @@ const task_connect_handler: loader.Task = {
|
||||||
if(chandler && AppParameters.getValue(AppParameters.KEY_CONNECT_NO_SINGLE_INSTANCE)) {
|
if(chandler && AppParameters.getValue(AppParameters.KEY_CONNECT_NO_SINGLE_INSTANCE)) {
|
||||||
try {
|
try {
|
||||||
await chandler.post_connect_request(connectData, () => new Promise<boolean>(resolve => {
|
await chandler.post_connect_request(connectData, () => new Promise<boolean>(resolve => {
|
||||||
spawnYesNo(tr("Another TeaWeb instance is already running"), tra("Another TeaWeb instance is already running.{:br:}Would you like to connect there?"), response => {
|
spawnYesNo(tr("Another TeaWeb instance is already running"), tra("Another TeaWeb instance is already running.\nWould you like to connect there?"), response => {
|
||||||
resolve(response);
|
resolve(response);
|
||||||
}, {
|
}, {
|
||||||
closeable: false
|
closeable: false
|
||||||
|
@ -450,12 +450,9 @@ const task_connect_handler: loader.Task = {
|
||||||
}));
|
}));
|
||||||
logInfo(LogCategory.CLIENT, tr("Executed connect successfully in another browser window. Closing this window"));
|
logInfo(LogCategory.CLIENT, tr("Executed connect successfully in another browser window. Closing this window"));
|
||||||
|
|
||||||
const message =
|
|
||||||
"You're connecting to {0} within the other TeaWeb instance.{:br:}" +
|
|
||||||
"You could now close this page.";
|
|
||||||
createInfoModal(
|
createInfoModal(
|
||||||
tr("Connecting successfully within other instance"),
|
tr("Connecting successfully within other instance"),
|
||||||
formatMessage(/* @tr-ignore */ tr(message), connectData.address),
|
formatMessage(tr("You're connecting to {0} within the other TeaWeb instance.\nYou could now close this page."), connectData.address),
|
||||||
{
|
{
|
||||||
closeable: false,
|
closeable: false,
|
||||||
footer: undefined
|
footer: undefined
|
||||||
|
|
|
@ -81,12 +81,12 @@ export class AppController {
|
||||||
this.connectionListEvents = new Registry<ConnectionListUIEvents>();
|
this.connectionListEvents = new Registry<ConnectionListUIEvents>();
|
||||||
initializeConnectionListController(this.connectionListEvents);
|
initializeConnectionListController(this.connectionListEvents);
|
||||||
|
|
||||||
this.listener.push(server_connections.events().on("notify_active_handler_changed", event => this.setConnectionHandler(event.newHandler)));
|
|
||||||
this.setConnectionHandler(server_connections.getActiveConnectionHandler());
|
|
||||||
|
|
||||||
this.sideBarController = new SideBarController();
|
this.sideBarController = new SideBarController();
|
||||||
this.serverLogController = new ServerEventLogController();
|
this.serverLogController = new ServerEventLogController();
|
||||||
this.hostBannerController = new HostBannerController();
|
this.hostBannerController = new HostBannerController();
|
||||||
|
|
||||||
|
this.listener.push(server_connections.events().on("notify_active_handler_changed", event => this.setConnectionHandler(event.newHandler)));
|
||||||
|
this.setConnectionHandler(server_connections.getActiveConnectionHandler());
|
||||||
}
|
}
|
||||||
|
|
||||||
setConnectionHandler(connection: ConnectionHandler) {
|
setConnectionHandler(connection: ConnectionHandler) {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import * as loader from "tc-loader";
|
import * as loader from "tc-loader";
|
||||||
import {Stage} from "tc-loader";
|
import {Stage} from "tc-loader";
|
||||||
import {KeyCode} from "../../PPTListener";
|
|
||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
import {LogCategory, logError} from "tc-shared/log";
|
import {LogCategory, logError} from "tc-shared/log";
|
||||||
|
|
||||||
|
@ -341,7 +340,7 @@ export function createInputModal(headMessage: BodyCreator, question: BodyCreator
|
||||||
button_submit.prop("disabled", !valid);
|
button_submit.prop("disabled", !valid);
|
||||||
});
|
});
|
||||||
input.on('keydown', event => {
|
input.on('keydown', event => {
|
||||||
if(event.keyCode !== KeyCode.KEY_RETURN || event.shiftKey)
|
if(event.key !== "Enter" || event.shiftKey)
|
||||||
return;
|
return;
|
||||||
if(button_submit.prop("disabled"))
|
if(button_submit.prop("disabled"))
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {createModal} from "../../ui/elements/Modal";
|
import {createModal} from "../../ui/elements/Modal";
|
||||||
import {getBackend} from "tc-shared/backend";
|
import {getBackend} from "tc-shared/backend";
|
||||||
import {tr} from "tc-shared/i18n/localize";
|
import {tr} from "tc-shared/i18n/localize";
|
||||||
|
import {spawnAboutModal} from "tc-shared/ui/modal/about/Controller";
|
||||||
|
|
||||||
function format_date(date: number) {
|
function format_date(date: number) {
|
||||||
const d = new Date(date);
|
const d = new Date(date);
|
||||||
|
@ -9,10 +10,13 @@ function format_date(date: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function spawnAbout() {
|
export function spawnAbout() {
|
||||||
|
spawnAboutModal();
|
||||||
|
return;
|
||||||
|
|
||||||
const connectModal = createModal({
|
const connectModal = createModal({
|
||||||
header: tr("About"),
|
header: tr("About"),
|
||||||
body: () => {
|
body: () => {
|
||||||
let tag = $("#tmpl_about").renderTag({
|
return $("#tmpl_about").renderTag({
|
||||||
client: __build.target !== "web",
|
client: __build.target !== "web",
|
||||||
|
|
||||||
version_client: __build.target === "web" ? __build.version || "in-dev" : "loading...",
|
version_client: __build.target === "web" ? __build.version || "in-dev" : "loading...",
|
||||||
|
@ -20,7 +24,6 @@ export function spawnAbout() {
|
||||||
|
|
||||||
version_timestamp: format_date(__build.timestamp)
|
version_timestamp: format_date(__build.timestamp)
|
||||||
});
|
});
|
||||||
return tag;
|
|
||||||
},
|
},
|
||||||
footer: null,
|
footer: null,
|
||||||
|
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
//TODO: Test if we could render this image and not only the browser by knowing the type.
|
|
||||||
import {createErrorModal, createModal} from "../../ui/elements/Modal";
|
|
||||||
import {tr, tra} from "../../i18n/localize";
|
|
||||||
import {arrayBufferBase64} from "../../utils/buffers";
|
|
||||||
import {LogCategory, logError, logTrace} from "tc-shared/log";
|
|
||||||
|
|
||||||
export function spawnAvatarUpload(callback_data: (data: ArrayBuffer | undefined | null) => any) {
|
|
||||||
const modal = createModal({
|
|
||||||
header: tr("Avatar Upload"),
|
|
||||||
footer: undefined,
|
|
||||||
body: () => {
|
|
||||||
return $("#tmpl_avatar_upload").renderTag({});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let _data_submitted = false;
|
|
||||||
let _current_avatar;
|
|
||||||
|
|
||||||
modal.htmlTag.find(".button-select").on('click', event => {
|
|
||||||
modal.htmlTag.find(".file-inputs").trigger('click');
|
|
||||||
});
|
|
||||||
|
|
||||||
modal.htmlTag.find(".button-delete").on('click', () => {
|
|
||||||
if (_data_submitted)
|
|
||||||
return;
|
|
||||||
_data_submitted = true;
|
|
||||||
modal.close();
|
|
||||||
callback_data(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modal.htmlTag.find(".button-cancel").on('click', () => modal.close());
|
|
||||||
const button_upload = modal.htmlTag.find(".button-upload");
|
|
||||||
button_upload.on('click', event => (!_data_submitted) && (_data_submitted = true, modal.close(), true) && callback_data(_current_avatar));
|
|
||||||
|
|
||||||
const set_avatar = (data: string | undefined, type?: string) => {
|
|
||||||
_current_avatar = data ? arrayBufferBase64(data) : undefined;
|
|
||||||
button_upload.prop("disabled", !_current_avatar);
|
|
||||||
modal.htmlTag.find(".preview img").attr("src", data ? ("data:image/" + type + ";base64," + data) : "img/style/avatar.png");
|
|
||||||
};
|
|
||||||
|
|
||||||
const input_node = modal.htmlTag.find(".file-inputs")[0] as HTMLInputElement;
|
|
||||||
input_node.multiple = false;
|
|
||||||
|
|
||||||
modal.htmlTag.find(".file-inputs").on('change', event => {
|
|
||||||
logTrace(LogCategory.CLIENT, "Files: %o", input_node.files);
|
|
||||||
|
|
||||||
const read_file = (file: File) => new Promise<string>((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => resolve(reader.result as string);
|
|
||||||
reader.onerror = error => reject(error);
|
|
||||||
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
});
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const data = await read_file(input_node.files[0]);
|
|
||||||
|
|
||||||
if (!data.startsWith("data:image/")) {
|
|
||||||
logError(LogCategory.FILE_TRANSFER, tr("Failed to load file %s: Invalid data media type (%o)"), input_node.files[0].name, data);
|
|
||||||
createErrorModal(tr("Icon upload failed"), tra("Failed to select avatar {}.<br>File is not an image", input_node.files[0].name)).open();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const semi = data.indexOf(';');
|
|
||||||
const type = data.substring(11, semi);
|
|
||||||
logTrace(LogCategory.CLIENT, tr("Given image has type %s"), type);
|
|
||||||
|
|
||||||
set_avatar(data.substr(semi + 8 /* 8 bytes := base64, */), type);
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
set_avatar(undefined);
|
|
||||||
modal.close_listener.push(() => !_data_submitted && callback_data(undefined));
|
|
||||||
modal.open();
|
|
||||||
}
|
|
|
@ -29,9 +29,7 @@ export function spawnAvatarList(client: ConnectionHandler) {
|
||||||
header: tr("Avatars"),
|
header: tr("Avatars"),
|
||||||
footer: undefined,
|
footer: undefined,
|
||||||
body: () => {
|
body: () => {
|
||||||
const template = $("#tmpl_avatar_list").renderTag({});
|
return $("#tmpl_avatar_list").renderTag({});
|
||||||
|
|
||||||
return template;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
export const Regex = {
|
|
||||||
//DOMAIN<:port>
|
|
||||||
DOMAIN: /^(localhost|((([a-zA-Z0-9_-]{0,63}\.){0,253})?[a-zA-Z0-9_-]{0,63}\.[a-zA-Z]{2,64}))(|:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[0-5]?[0-9]{1,46}))$/,
|
|
||||||
//IP<:port>
|
|
||||||
IP_V4: /(^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(|:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[0-5]?[0-9]{1,4}))$/,
|
|
||||||
IP_V6: /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/,
|
|
||||||
IP: /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$|^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/,
|
|
||||||
};
|
|
|
@ -1,85 +0,0 @@
|
||||||
import {LogCategory, logWarn} from "../../log";
|
|
||||||
import {createModal, Modal} from "../../ui/elements/Modal";
|
|
||||||
import {ClientEntry} from "../../tree/Client";
|
|
||||||
import {GroupManager, GroupType} from "../../permission/GroupManager";
|
|
||||||
import PermissionType from "../../permission/PermissionType";
|
|
||||||
import {generateIconJQueryTag, getIconManager} from "tc-shared/file/Icons";
|
|
||||||
|
|
||||||
let current_modal: Modal;
|
|
||||||
export function createServerGroupAssignmentModal(client: ClientEntry, callback: (groups: number[], flag: boolean) => Promise<boolean>) {
|
|
||||||
if(current_modal)
|
|
||||||
current_modal.close();
|
|
||||||
|
|
||||||
current_modal = createModal({
|
|
||||||
header: tr("Server Groups"),
|
|
||||||
body: () => {
|
|
||||||
let tag: any = {};
|
|
||||||
let groups = tag["groups"] = [];
|
|
||||||
|
|
||||||
tag["client"] = client.createChatTag();
|
|
||||||
|
|
||||||
const _groups = client.channelTree.client.groups.serverGroups.sort(GroupManager.sorter());
|
|
||||||
for(let group of _groups) {
|
|
||||||
if(group.type != GroupType.NORMAL) continue;
|
|
||||||
|
|
||||||
let entry = {} as any;
|
|
||||||
entry["id"] = group.id;
|
|
||||||
entry["name"] = group.name;
|
|
||||||
entry["disabled"] = !client.channelTree.client.permissions.neededPermission(PermissionType.I_GROUP_MEMBER_ADD_POWER).granted(group.requiredMemberRemovePower);
|
|
||||||
entry["default"] = client.channelTree.server.properties.virtualserver_default_server_group == group.id;
|
|
||||||
tag["icon_" + group.id] = generateIconJQueryTag(getIconManager().resolveIcon(group.properties.iconid, client.channelTree.server.properties.virtualserver_unique_identifier, client.channelTree.client.handlerId));
|
|
||||||
groups.push(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
let template = $("#tmpl_server_group_assignment").renderTag(tag);
|
|
||||||
|
|
||||||
const update_groups = () => {
|
|
||||||
for(let group of _groups) {
|
|
||||||
template.find("input[group-id='" + group.id + "']").prop("checked", client.groupAssigned(group));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
template.find(".group-entry input").each((_idx, _entry) => {
|
|
||||||
let entry = $(_entry);
|
|
||||||
|
|
||||||
entry.on('change', event => {
|
|
||||||
let group_id = parseInt(entry.attr("group-id"));
|
|
||||||
let group = client.channelTree.client.groups.findServerGroup(group_id);
|
|
||||||
if(!group) {
|
|
||||||
logWarn(LogCategory.GENERAL, tr("Could not resolve target group!"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let target = entry.prop("checked");
|
|
||||||
callback([group.id], target).catch(e => { logWarn(LogCategory.GENERAL, tr("Failed to change group assignment: %o"), e)}).then(update_groups);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
template.find(".button-close").on('click', () => current_modal.close());
|
|
||||||
template.find(".button-remove-all").on('click', () => {
|
|
||||||
const group_ids = [];
|
|
||||||
|
|
||||||
template.find(".group-entry input").each((_idx, _entry) => {
|
|
||||||
let entry = $(_entry);
|
|
||||||
if(entry.attr("default") !== undefined || !entry.prop("checked"))
|
|
||||||
return;
|
|
||||||
|
|
||||||
group_ids.push(parseInt(entry.attr("group-id")));
|
|
||||||
});
|
|
||||||
|
|
||||||
callback(group_ids, false).catch(e => { logWarn(LogCategory.GENERAL, tr("Failed to remove all group assignments: %o"), e)}).then(update_groups);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
update_groups();
|
|
||||||
return template;
|
|
||||||
},
|
|
||||||
footer: null,
|
|
||||||
min_width: "10em"
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
current_modal.htmlTag.find(".modal-body").addClass("modal-server-group-assignments");
|
|
||||||
current_modal.close_listener.push(() => current_modal = undefined);
|
|
||||||
current_modal.open();
|
|
||||||
}
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
import {Registry} from "tc-events";
|
||||||
|
import {ModalAboutEvents, ModalAboutVariables} from "tc-shared/ui/modal/about/Definitions";
|
||||||
|
import {IpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable";
|
||||||
|
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
||||||
|
import {CallOnce, ignorePromise} from "tc-shared/proto";
|
||||||
|
import {getBackend} from "tc-shared/backend";
|
||||||
|
|
||||||
|
class Controller {
|
||||||
|
readonly events: Registry<ModalAboutEvents>;
|
||||||
|
readonly variables: IpcUiVariableProvider<ModalAboutVariables>;
|
||||||
|
|
||||||
|
private eggShown: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.events = new Registry<ModalAboutEvents>();
|
||||||
|
this.variables = new IpcUiVariableProvider<ModalAboutVariables>();
|
||||||
|
|
||||||
|
this.eggShown = false;
|
||||||
|
|
||||||
|
this.variables.setVariableProvider("nativeVersion", () => {
|
||||||
|
if(__build.target === "client") {
|
||||||
|
const backend = getBackend("native");
|
||||||
|
return backend.getVersionInfo().version;
|
||||||
|
} else {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.variables.setVariableProvider("uiVersion", () => __build.version);
|
||||||
|
this.variables.setVariableProvider("uiVersionTimestamp", () => __build.timestamp);
|
||||||
|
|
||||||
|
this.variables.setVariableProvider("eggShown", () => this.eggShown);
|
||||||
|
this.variables.setVariableEditor("eggShown", newValue => { this.eggShown = newValue; });
|
||||||
|
|
||||||
|
this.events.on("action_update_high_score", event => {
|
||||||
|
let highScore = parseInt(localStorage.getItem("ee-snake-high-score"));
|
||||||
|
if(!isNaN(highScore) && highScore >= event.score) {
|
||||||
|
/* No change */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem("ee-snake-high-score", event.score.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
this.events.on("query_high_score", () => {
|
||||||
|
let highScore = parseInt(localStorage.getItem("ee-snake-high-score"));
|
||||||
|
if(isNaN(highScore)) {
|
||||||
|
highScore = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.events.fire("notify_high_score", { score: highScore });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallOnce
|
||||||
|
destroy() {
|
||||||
|
this.events.destroy();
|
||||||
|
this.variables.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function spawnAboutModal() {
|
||||||
|
const controller = new Controller();
|
||||||
|
const modal = spawnModal("modal-about", [
|
||||||
|
controller.events.generateIpcDescription(),
|
||||||
|
controller.variables.generateConsumerDescription()
|
||||||
|
], {
|
||||||
|
popoutable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.getEvents().on("destroy", () => controller.destroy());
|
||||||
|
ignorePromise(modal.show());
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
export interface ModalAboutVariables {
|
||||||
|
readonly uiVersion: string,
|
||||||
|
readonly uiVersionTimestamp: number,
|
||||||
|
readonly nativeVersion: string,
|
||||||
|
|
||||||
|
eggShown: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModalAboutEvents {
|
||||||
|
action_update_high_score: { score: number },
|
||||||
|
query_high_score: {},
|
||||||
|
notify_high_score: { score: number },
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
padding: 1em;
|
||||||
|
user-select: none;
|
||||||
|
color: #999999;
|
||||||
|
|
||||||
|
&.windowed {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.containerLeft {
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.containerRight {
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 2em;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin-block-start: 0.35em;
|
||||||
|
margin-block-end: 0.35em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.25em;
|
||||||
|
margin-block-start: 0.10em;
|
||||||
|
margin-block-end: 0.10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-block-start: .25em;
|
||||||
|
margin-block-end: .25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.version {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
.key {
|
||||||
|
width: 50%;
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
width: 50%;
|
||||||
|
padding-left: .25em;
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
|
user-select: all;
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gameContainer {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,752 @@
|
||||||
|
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
|
import React, {useContext, useEffect} from "react";
|
||||||
|
import {IpcRegistryDescription, Registry} from "tc-events";
|
||||||
|
import {ModalAboutEvents, ModalAboutVariables} from "tc-shared/ui/modal/about/Definitions";
|
||||||
|
import {UiVariableConsumer} from "tc-shared/ui/utils/Variable";
|
||||||
|
import {createIpcUiVariableConsumer, IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable";
|
||||||
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
|
import {joinClassList, useTr} from "tc-shared/ui/react-elements/Helper";
|
||||||
|
import TeaCupAnimatedImage from "./TeaSpeakCupAnimated.png";
|
||||||
|
import {LogCategory, logError} from "tc-shared/log";
|
||||||
|
import {CallOnce} from "tc-shared/proto";
|
||||||
|
import {EventType, getKeyBoard, KeyEvent} from "tc-shared/PPTListener";
|
||||||
|
|
||||||
|
const cssStyle = require("./Renderer.scss");
|
||||||
|
const VariablesContext = React.createContext<UiVariableConsumer<ModalAboutVariables>>(undefined);
|
||||||
|
const EventsContext = React.createContext<Registry<ModalAboutEvents>>(undefined);
|
||||||
|
|
||||||
|
interface CanvasProperties {
|
||||||
|
with: number,
|
||||||
|
height: number,
|
||||||
|
|
||||||
|
timestamp: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TickProperties {
|
||||||
|
timestamp: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GameState {
|
||||||
|
name() : string;
|
||||||
|
|
||||||
|
initialize(game: SnakeGame);
|
||||||
|
finalize();
|
||||||
|
|
||||||
|
handleKeyEvent(event: KeyEvent);
|
||||||
|
gameTick(game: SnakeGame, properties: TickProperties);
|
||||||
|
render(context: CanvasRenderingContext2D, properties: CanvasProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
class GameStateCriticalError implements GameState {
|
||||||
|
constructor(readonly errorMessage: string) {}
|
||||||
|
|
||||||
|
name(): string {
|
||||||
|
return "Critical Error";
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() { }
|
||||||
|
finalize() { }
|
||||||
|
|
||||||
|
handleKeyEvent(event: KeyEvent) {}
|
||||||
|
gameTick(game: SnakeGame, properties: TickProperties) { }
|
||||||
|
render(context: CanvasRenderingContext2D, properties: CanvasProperties) {
|
||||||
|
context.fillStyle = "black";
|
||||||
|
context.fillRect(0, 0, properties.with, properties.height);
|
||||||
|
|
||||||
|
const fontPixelSize = Math.max(10, Math.floor(properties.height * 0.025));
|
||||||
|
context.fillStyle = "red";
|
||||||
|
context.textAlign = "center";
|
||||||
|
context.textBaseline = "middle";
|
||||||
|
context.font = fontPixelSize + "px Lucida Console, monospace";
|
||||||
|
context.fillText(tr("A critical error happened") + ":", properties.with / 2, (properties.height - fontPixelSize - 2) / 2);
|
||||||
|
context.fillText(this.errorMessage, properties.with / 2, (properties.height + fontPixelSize + 2) / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GameStateStart implements GameState {
|
||||||
|
private highScore: number;
|
||||||
|
private spacePressed: boolean;
|
||||||
|
|
||||||
|
name(): string {
|
||||||
|
return "Start";
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(game: SnakeGame) {
|
||||||
|
this.highScore = game.getHighScore();
|
||||||
|
this.spacePressed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
finalize() { }
|
||||||
|
|
||||||
|
handleKeyEvent(event: KeyEvent) {
|
||||||
|
if(event.keyCode === "Space") {
|
||||||
|
this.spacePressed = event.type !== EventType.KEY_RELEASE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gameTick(game: SnakeGame, properties: TickProperties) {
|
||||||
|
if(this.spacePressed) {
|
||||||
|
game.setState(new GameStateInGame());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.highScore = game.getHighScore();
|
||||||
|
}
|
||||||
|
|
||||||
|
render(context: CanvasRenderingContext2D, properties: CanvasProperties) {
|
||||||
|
const fontPixelSize = Math.max(10, Math.floor(properties.height * 0.05));
|
||||||
|
const dynamicFontPixelSize = fontPixelSize + Math.sin(properties.timestamp / 750) / 2;
|
||||||
|
|
||||||
|
context.textAlign = "center";
|
||||||
|
context.textBaseline = "middle";
|
||||||
|
|
||||||
|
context.font = fontPixelSize + "px Lucida Console, monospace";
|
||||||
|
context.fillStyle = "white";
|
||||||
|
context.fillText(tr("Welcome to the snake game."), properties.with / 2, properties.height / 2 - fontPixelSize);
|
||||||
|
if(this.highScore > 0) {
|
||||||
|
context.fillText(tr("High score: ") + this.highScore, properties.with / 2, properties.height / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.font = dynamicFontPixelSize + "px Lucida Console, monospace";
|
||||||
|
context.fillStyle = "lightblue";
|
||||||
|
context.fillText(tr("Press 'Space' to start!"), properties.with / 2, properties.height / 2 + fontPixelSize * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GameStateGameOver implements GameState {
|
||||||
|
private spacePressed: boolean;
|
||||||
|
|
||||||
|
constructor(readonly gameScore: number) { }
|
||||||
|
|
||||||
|
name(): string {
|
||||||
|
return "Start";
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
this.spacePressed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
finalize() { }
|
||||||
|
|
||||||
|
handleKeyEvent(event: KeyEvent) {
|
||||||
|
if(event.keyCode === "Space") {
|
||||||
|
this.spacePressed = event.type !== EventType.KEY_RELEASE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gameTick(game: SnakeGame, properties: TickProperties) {
|
||||||
|
if(this.spacePressed) {
|
||||||
|
game.setState(new GameStateStart());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(context: CanvasRenderingContext2D, properties: CanvasProperties) {
|
||||||
|
const fontPixelSize = Math.max(10, Math.floor(properties.height * 0.04));
|
||||||
|
|
||||||
|
context.textAlign = "center";
|
||||||
|
context.textBaseline = "middle";
|
||||||
|
|
||||||
|
context.font = (fontPixelSize * 2) + "px Lucida Console, monospace";
|
||||||
|
context.fillStyle = "red";
|
||||||
|
context.fillText(tr("Game over!"), properties.with / 2, properties.height / 2 - fontPixelSize * 2);
|
||||||
|
|
||||||
|
context.font = fontPixelSize + "px Lucida Console, monospace";
|
||||||
|
context.fillStyle = "white";
|
||||||
|
context.fillText(tr("Current score: ") + this.gameScore, properties.with / 2, properties.height / 2);
|
||||||
|
context.fillText(tr("Press 'Space' to continue."), properties.with / 2, properties.height / 2 + fontPixelSize * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type GameDirection = "north" | "south" | "west" | "east";
|
||||||
|
|
||||||
|
class GameStateInGame implements GameState {
|
||||||
|
private static readonly kGridWidth = 20;
|
||||||
|
private static readonly kGridHeight = 20;
|
||||||
|
|
||||||
|
private static readonly kSnakeSpeed = 300;
|
||||||
|
|
||||||
|
private lastTileMove: number;
|
||||||
|
private snake: GameDirection[];
|
||||||
|
private snakePosition: { x: number, y: number };
|
||||||
|
private snakeDirection: GameDirection;
|
||||||
|
|
||||||
|
private applePosition: { x: number, y: number };
|
||||||
|
|
||||||
|
name(): string {
|
||||||
|
return "InGame";
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
this.snake = [];
|
||||||
|
this.snakePosition = { x: Math.floor(GameStateInGame.kGridWidth / 2), y: Math.floor(GameStateInGame.kGridHeight / 2) };
|
||||||
|
this.snakeDirection = "north";
|
||||||
|
|
||||||
|
this.generateApple([[this.snakePosition.x, this.snakePosition.y]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
finalize() {}
|
||||||
|
|
||||||
|
handleKeyEvent(event: KeyEvent) {
|
||||||
|
if(event.type !== EventType.KEY_RELEASE) {
|
||||||
|
switch (event.key) {
|
||||||
|
case "ArrowRight":
|
||||||
|
this.snakeDirection = "east";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ArrowLeft":
|
||||||
|
this.snakeDirection = "west";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ArrowUp":
|
||||||
|
this.snakeDirection = "north";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ArrowDown":
|
||||||
|
this.snakeDirection = "south";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gameTick(game: SnakeGame, properties: TickProperties) {
|
||||||
|
if(typeof this.lastTileMove === "undefined") {
|
||||||
|
this.lastTileMove = properties.timestamp;
|
||||||
|
} else {
|
||||||
|
let moveSteps = Math.floor((properties.timestamp - this.lastTileMove) / GameStateInGame.kSnakeSpeed);
|
||||||
|
this.lastTileMove += moveSteps * GameStateInGame.kSnakeSpeed;
|
||||||
|
|
||||||
|
let blockedTiles: [number, number][];
|
||||||
|
while(moveSteps-- > 0) {
|
||||||
|
this.snake.unshift(this.snakeDirection);
|
||||||
|
|
||||||
|
switch (this.snakeDirection) {
|
||||||
|
case "north":
|
||||||
|
this.snakePosition.y -= 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "east":
|
||||||
|
this.snakePosition.x += 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "south":
|
||||||
|
this.snakePosition.y += 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "west":
|
||||||
|
this.snakePosition.x -= 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let generateApple = false;
|
||||||
|
if(this.snakePosition.x === this.applePosition.x && this.snakePosition.y === this.applePosition.y) {
|
||||||
|
generateApple = true;
|
||||||
|
} else {
|
||||||
|
this.snake.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
blockedTiles = [];
|
||||||
|
if(!this.validateSnake(blockedTiles)) {
|
||||||
|
game.updateHighScore(this.snake.length + 1);
|
||||||
|
game.setState(new GameStateGameOver(this.snake.length + 1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(generateApple) {
|
||||||
|
if(!this.generateApple(blockedTiles)) {
|
||||||
|
game.updateHighScore(this.snake.length + 1);
|
||||||
|
game.setState(new GameStateGameOver(this.snake.length + 1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(context: CanvasRenderingContext2D, properties: CanvasProperties) {
|
||||||
|
const fontPixelSize = Math.max(10, Math.floor(properties.height * 0.025));
|
||||||
|
|
||||||
|
const borderSize = 2;
|
||||||
|
const paddingTop = 5;
|
||||||
|
const paddingLeft = 5;
|
||||||
|
const tileSize = Math.min(
|
||||||
|
(properties.height - borderSize * 2 - paddingTop * 2) / GameStateInGame.kGridHeight,
|
||||||
|
(properties.with - borderSize * 2 - paddingLeft * 2) / GameStateInGame.kGridWidth
|
||||||
|
);
|
||||||
|
|
||||||
|
context.strokeStyle = "green";
|
||||||
|
context.lineWidth = borderSize;
|
||||||
|
|
||||||
|
const gridWidth = tileSize * GameStateInGame.kGridWidth;
|
||||||
|
const gridHeight = tileSize * GameStateInGame.kGridHeight;
|
||||||
|
const gridOffsetX = (properties.with - gridWidth) / 2;
|
||||||
|
const gridOffsetY = (properties.height - gridHeight) / 2;
|
||||||
|
|
||||||
|
context.strokeRect(gridOffsetX - borderSize / 2, gridOffsetY - borderSize / 2, gridWidth + borderSize, gridHeight + borderSize);
|
||||||
|
|
||||||
|
{
|
||||||
|
context.save();
|
||||||
|
|
||||||
|
context.translate(gridOffsetX, gridOffsetY);
|
||||||
|
context.scale(tileSize, tileSize);
|
||||||
|
this.renderSnake(context);
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
context.font = fontPixelSize + "px Lucida Console, monospace";
|
||||||
|
context.fillStyle = "green";
|
||||||
|
context.textAlign = "left";
|
||||||
|
context.textBaseline = "top";
|
||||||
|
|
||||||
|
const textScore = tr("Score") + ":";
|
||||||
|
const textScoreBounds = context.measureText(textScore)
|
||||||
|
context.fillText(textScore, gridOffsetX + tileSize / 2, gridOffsetY + tileSize / 2);
|
||||||
|
context.fillText(this.snake.length.toString(), gridOffsetX + tileSize / 2 + textScoreBounds.width + fontPixelSize * .25, gridOffsetY + tileSize / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSnake(context: CanvasRenderingContext2D) {
|
||||||
|
let positionX = this.snakePosition.x, positionY = this.snakePosition.y;
|
||||||
|
|
||||||
|
for(let tileIndex = 0; tileIndex <= this.snake.length; tileIndex++) {
|
||||||
|
if(tileIndex === 0) {
|
||||||
|
context.fillStyle = "lightgreen";
|
||||||
|
} else if(tileIndex === 1) {
|
||||||
|
context.fillStyle = "green";
|
||||||
|
}
|
||||||
|
context.fillRect(positionX, positionY, 1, 1);
|
||||||
|
|
||||||
|
const tileDirection = this.snake[tileIndex];
|
||||||
|
switch (tileDirection) {
|
||||||
|
case "north":
|
||||||
|
positionY += 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "east":
|
||||||
|
positionX -= 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "south":
|
||||||
|
positionY -= 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "west":
|
||||||
|
positionX += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.fillStyle = "red";
|
||||||
|
context.fillRect(this.applePosition.x, this.applePosition.y, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateApple(blockedTiles: [number, number][]): boolean {
|
||||||
|
const freeTiles = [];
|
||||||
|
|
||||||
|
for(let posX = 0; posX < GameStateInGame.kGridWidth; posX++) {
|
||||||
|
for(let posY = 0; posY < GameStateInGame.kGridHeight; posY++) {
|
||||||
|
if(blockedTiles.findIndex(tile => posX === tile[0] && posY === tile[1]) === -1) {
|
||||||
|
freeTiles.push([posX, posY]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(freeTiles.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tile = freeTiles[Math.floor(Math.random() * freeTiles.length)];
|
||||||
|
this.applePosition = {
|
||||||
|
x: tile[0],
|
||||||
|
y: tile[1]
|
||||||
|
};
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateSnake(blockedTiles: [number, number][]) : boolean {
|
||||||
|
let positionX = this.snakePosition.x, positionY = this.snakePosition.y;
|
||||||
|
|
||||||
|
for(let tileIndex = 0; tileIndex <= this.snake.length; tileIndex++) {
|
||||||
|
if(positionY < 0 || positionY >= GameStateInGame.kGridHeight) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(positionX < 0 || positionX >= GameStateInGame.kGridWidth) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(blockedTiles.findIndex(tile => tile[0] === positionX && tile[1] === positionY) !== -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockedTiles.push([positionX, positionY]);
|
||||||
|
switch (this.snake[tileIndex]) {
|
||||||
|
case "north":
|
||||||
|
positionY += 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "east":
|
||||||
|
positionX -= 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "south":
|
||||||
|
positionY -= 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "west":
|
||||||
|
positionX += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SnakeGame {
|
||||||
|
private static readonly kDebugInfo = false;
|
||||||
|
private readonly keyListener: () => void;
|
||||||
|
private readonly canvasElement: HTMLCanvasElement;
|
||||||
|
private readonly canvasContext: CanvasRenderingContext2D;
|
||||||
|
private readonly renderTimings: number[];
|
||||||
|
private currentFps: number;
|
||||||
|
private currentFrameTime: number;
|
||||||
|
|
||||||
|
private animationId: number;
|
||||||
|
private tickId: number;
|
||||||
|
|
||||||
|
private highScore: number;
|
||||||
|
private highScoreListener: (newValue: number) => void;
|
||||||
|
|
||||||
|
|
||||||
|
private currentState: GameState;
|
||||||
|
|
||||||
|
constructor(canvasElement: HTMLCanvasElement) {
|
||||||
|
this.canvasElement = canvasElement;
|
||||||
|
this.canvasContext = this.canvasElement.getContext("2d");
|
||||||
|
this.setState(new GameStateCriticalError("Missing initial state"));
|
||||||
|
|
||||||
|
this.currentFps = 0;
|
||||||
|
this.currentFrameTime = 0;
|
||||||
|
|
||||||
|
this.renderTimings = [];
|
||||||
|
this.canvasContext.imageSmoothingEnabled = false;
|
||||||
|
//this.canvasContext.imageSmoothingQuality = "high";
|
||||||
|
|
||||||
|
this.animationId = requestAnimationFrame(() => {
|
||||||
|
this.invokeRender();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.tickId = setInterval(() => {
|
||||||
|
try {
|
||||||
|
this.currentState.gameTick(this, {
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logError(LogCategory.GENERAL, tr("Failed to tick current game state: %o"), error);
|
||||||
|
this.setState(new GameStateCriticalError(tr("game tick caused an error")))
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
{
|
||||||
|
const keyboard = getKeyBoard();
|
||||||
|
const listener = event => {
|
||||||
|
this.currentState.handleKeyEvent(event);
|
||||||
|
};
|
||||||
|
keyboard.registerListener(listener);
|
||||||
|
this.keyListener = () => keyboard.unregisterListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.highScore = 0;
|
||||||
|
this.setState(new GameStateStart());
|
||||||
|
}
|
||||||
|
|
||||||
|
getHighScore() : number { return this.highScore; }
|
||||||
|
updateHighScore(gameScore: number) : boolean {
|
||||||
|
if(gameScore <= this.highScore) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.highScore = gameScore;
|
||||||
|
if(this.highScoreListener) {
|
||||||
|
this.highScoreListener(this.highScore);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHighScoreListener(listener: (newValue: number) => void) {
|
||||||
|
this.highScoreListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallOnce
|
||||||
|
destroy() {
|
||||||
|
this.keyListener();
|
||||||
|
|
||||||
|
cancelAnimationFrame(this.animationId);
|
||||||
|
clearInterval(this.tickId);
|
||||||
|
|
||||||
|
this.tickId = 0;
|
||||||
|
this.animationId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(state: GameState) {
|
||||||
|
this.currentState?.finalize();
|
||||||
|
|
||||||
|
this.currentState = state;
|
||||||
|
this.currentState.initialize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private invokeRender() {
|
||||||
|
const frameStart = performance.now();
|
||||||
|
while(this.renderTimings[0] + 1000 <= frameStart) { this.renderTimings.shift(); }
|
||||||
|
this.renderTimings.push(frameStart);
|
||||||
|
this.currentFps = this.currentFps * .8 + this.renderTimings.length * .2;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.render();
|
||||||
|
} catch (error) {
|
||||||
|
logError(LogCategory.GENERAL, tr("Failed to render game: %o"), error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const frameEnd = performance.now();
|
||||||
|
this.currentFrameTime = this.currentFrameTime * .8 + (frameEnd - frameStart) * .2;
|
||||||
|
|
||||||
|
this.animationId = requestAnimationFrame(() => {
|
||||||
|
this.invokeRender();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private render() {
|
||||||
|
this.canvasElement.width = this.canvasElement.clientWidth;
|
||||||
|
this.canvasElement.height = this.canvasElement.clientHeight;
|
||||||
|
const properties: CanvasProperties = {
|
||||||
|
with: this.canvasElement.clientWidth,
|
||||||
|
height: this.canvasElement.clientHeight,
|
||||||
|
timestamp: performance.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
const ctx = this.canvasContext;
|
||||||
|
ctx.fillStyle = "black";
|
||||||
|
ctx.fillRect(0, 0, properties.with, properties.height);
|
||||||
|
|
||||||
|
this.currentState.render(this.canvasContext, properties);
|
||||||
|
|
||||||
|
/* Debug Info */
|
||||||
|
if(SnakeGame.kDebugInfo) {
|
||||||
|
const fontPixelSize = Math.max(10, Math.floor(properties.height * 0.025));
|
||||||
|
const keyWidth = 12 * fontPixelSize;
|
||||||
|
|
||||||
|
ctx.fillStyle = "white";
|
||||||
|
ctx.textAlign = "left";
|
||||||
|
ctx.textBaseline = "top";
|
||||||
|
ctx.font = fontPixelSize + "px Lucida Console, monospace";
|
||||||
|
ctx.fillText("FPS:", 10, 10);
|
||||||
|
ctx.fillText(this.currentFps.toFixed(2), keyWidth, 10);
|
||||||
|
|
||||||
|
ctx.fillText("Frame Time (ms):", 10, 10 + fontPixelSize * 1.2);
|
||||||
|
ctx.fillText(this.currentFrameTime.toFixed(2), keyWidth, 10 + fontPixelSize * 1.2);
|
||||||
|
|
||||||
|
ctx.fillText("Stage Name:", 10, 10 + fontPixelSize * 1.2 * 2);
|
||||||
|
ctx.fillText(this.currentState.name(), keyWidth, 10 + fontPixelSize * 1.2 * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SnakeGameRenderer = React.memo(() => {
|
||||||
|
const events = useContext(EventsContext);
|
||||||
|
const refCanvas = React.createRef<HTMLCanvasElement>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const game = new SnakeGame(refCanvas.current);
|
||||||
|
const listenerHighScore = events.on("notify_high_score", event => game.updateHighScore(event.score));
|
||||||
|
|
||||||
|
game.setHighScoreListener(newValue => events.fire("action_update_high_score", { score: newValue }));
|
||||||
|
events.fire("query_high_score");
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
listenerHighScore();
|
||||||
|
game.destroy();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.gameContainer}>
|
||||||
|
<canvas ref={refCanvas} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
const SnakeEasterEgg = React.memo(() => {
|
||||||
|
const variables = useContext(VariablesContext);
|
||||||
|
const eggShown = variables.useReadOnly("eggShown", undefined, false);
|
||||||
|
|
||||||
|
if(eggShown) {
|
||||||
|
return <SnakeGameRenderer />;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const InfoTitle = React.memo(() => {
|
||||||
|
const variables = useContext(VariablesContext);
|
||||||
|
const uiVersion = variables.useReadOnly("uiVersion", undefined, useTr("loading"));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<h1>
|
||||||
|
TeaSpeak-Client build {uiVersion}
|
||||||
|
</h1>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ModalTitle = React.memo(() => {
|
||||||
|
const variables = useContext(VariablesContext);
|
||||||
|
const eggShown = variables.useReadOnly("eggShown", undefined, false);
|
||||||
|
|
||||||
|
if(eggShown) {
|
||||||
|
return <Translatable key={"snake"}>The Snake Game</Translatable>;
|
||||||
|
} else if(__build.target === "web") {
|
||||||
|
return <Translatable key={"web"}>About TeaWeb</Translatable>;
|
||||||
|
} else {
|
||||||
|
return <Translatable key={"client"}>About TeaClient</Translatable>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const SupportEmail = React.memo(() => {
|
||||||
|
let targetMail;
|
||||||
|
if(__build.target === "web") {
|
||||||
|
targetMail = "web.support@teaspeak.de";
|
||||||
|
} else {
|
||||||
|
targetMail = "client.support@teaspeak.de";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a href={"mailto:" + targetMail}>{targetMail}</a>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const VersionInfo = React.memo(() => {
|
||||||
|
const variables = useContext(VariablesContext);
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
const uiVersion = variables.useReadOnly("uiVersion", undefined, useTr("loading"));
|
||||||
|
if(__build.target === "web") {
|
||||||
|
result.push(
|
||||||
|
<div className={cssStyle.version} key={"web"}>
|
||||||
|
<div className={cssStyle.key}><Translatable>TeaWeb</Translatable></div>:
|
||||||
|
<div className={cssStyle.value}>{uiVersion}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const nativeVersion = variables.useReadOnly("nativeVersion", undefined, useTr("loading"));
|
||||||
|
result.push(
|
||||||
|
<div className={cssStyle.version} key={"native"}>
|
||||||
|
<div className={cssStyle.key}><Translatable>TeaClient</Translatable></div>:
|
||||||
|
<div className={cssStyle.value}>{nativeVersion}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
result.push(
|
||||||
|
<div className={cssStyle.version} key={"ui"}>
|
||||||
|
<div className={cssStyle.key}><Translatable>User Interface</Translatable></div>:
|
||||||
|
<div className={cssStyle.value}>{uiVersion}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{result}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const LicenseInfo = React.memo(() => {
|
||||||
|
let applicationName;
|
||||||
|
if(__build.target === "web") {
|
||||||
|
applicationName = "TeaWeb";
|
||||||
|
} else {
|
||||||
|
applicationName = "TeaClient";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
The {applicationName} application is licensed by MPL-2.0<br />
|
||||||
|
More information here: <a href="https://github.com/TeaSpeak/TeaWeb/blob/master/LICENSE.TXT" target="_blank">https://github.com/TeaSpeak/TeaWeb/blob/master/LICENSE.TXT</a>
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
const MarkusHadenfeldt = React.memo(() => {
|
||||||
|
const variables = useContext(VariablesContext);
|
||||||
|
const variable = variables.useVariable("eggShown");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span onDoubleClick={() => variable.setValue(true)}>
|
||||||
|
(Markus Hadenfeldt)
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
|
class Modal extends AbstractModal {
|
||||||
|
private readonly events: Registry<ModalAboutEvents>;
|
||||||
|
private readonly variables: UiVariableConsumer<ModalAboutVariables>;
|
||||||
|
|
||||||
|
constructor(events: IpcRegistryDescription<ModalAboutEvents>, variables: IpcVariableDescriptor<ModalAboutVariables>) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.events = Registry.fromIpcDescription(events);
|
||||||
|
this.variables = createIpcUiVariableConsumer(variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBody(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<EventsContext.Provider value={this.events}>
|
||||||
|
<VariablesContext.Provider value={this.variables}>
|
||||||
|
<div className={joinClassList(cssStyle.container, this.properties.windowed && cssStyle.windowed)}>
|
||||||
|
<div className={cssStyle.containerLeft}>
|
||||||
|
<div>
|
||||||
|
<img src={TeaCupAnimatedImage} alt={useTr("TeaSpeak - Logo")} draggable={false} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Copyright (c) 2017-2021 TeaSpeak <br/>
|
||||||
|
<MarkusHadenfeldt />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<VersionInfo/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.containerRight}>
|
||||||
|
<InfoTitle/>
|
||||||
|
<h2><Translatable>Special thanks</Translatable></h2>
|
||||||
|
<p>
|
||||||
|
"Яedeemer" (Janni K.)<br />
|
||||||
|
Chromatic-Solutions (Sofian) for the lovely dark design
|
||||||
|
</p>
|
||||||
|
<h2><Translatable>Contact</Translatable></h2>
|
||||||
|
<p>
|
||||||
|
<Translatable>E-Mail</Translatable>: <SupportEmail /><br />
|
||||||
|
<Translatable>WWW</Translatable>: <a href="https://teaspeak.de" target="_blank">https://teaspeak.de</a><br/>
|
||||||
|
<Translatable>Community</Translatable>: <a href="https://forum.teaspeak.de" target="_blank">https://forum.teaspeak.de</a>
|
||||||
|
</p>
|
||||||
|
<h2><Translatable>License</Translatable></h2>
|
||||||
|
<LicenseInfo />
|
||||||
|
</div>
|
||||||
|
<SnakeEasterEgg />
|
||||||
|
</div>
|
||||||
|
</VariablesContext.Provider>
|
||||||
|
</EventsContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTitle(): string | React.ReactElement {
|
||||||
|
return (
|
||||||
|
<VariablesContext.Provider value={this.variables}>
|
||||||
|
<ModalTitle />
|
||||||
|
</VariablesContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Modal;
|
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
|
@ -17,10 +17,10 @@ import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||||
import {server_connections} from "tc-shared/ConnectionManager";
|
import {server_connections} from "tc-shared/ConnectionManager";
|
||||||
import {parseServerAddress} from "tc-shared/tree/Server";
|
import {parseServerAddress} from "tc-shared/tree/Server";
|
||||||
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
|
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
|
||||||
import ipRegex from "ip-regex";
|
|
||||||
import {UiVariableProvider} from "tc-shared/ui/utils/Variable";
|
import {UiVariableProvider} from "tc-shared/ui/utils/Variable";
|
||||||
import {createIpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable";
|
import {createIpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable";
|
||||||
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
||||||
|
import ipRegex from "ip-regex";
|
||||||
|
|
||||||
const kRegexDomain = /^(localhost|((([a-zA-Z0-9_-]{0,63}\.){0,253})?[a-zA-Z0-9_-]{0,63}\.[a-zA-Z]{2,64}))$/i;
|
const kRegexDomain = /^(localhost|((([a-zA-Z0-9_-]{0,63}\.){0,253})?[a-zA-Z0-9_-]{0,63}\.[a-zA-Z]{2,64}))$/i;
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {PermissionEditorEvents} from "tc-shared/ui/modal/permission/EditorDefini
|
||||||
import {PermissionEditorServerInfo} from "tc-shared/ui/modal/permission/ModalRenderer";
|
import {PermissionEditorServerInfo} from "tc-shared/ui/modal/permission/ModalRenderer";
|
||||||
import {ModalAvatarUploadEvents, ModalAvatarUploadVariables} from "tc-shared/ui/modal/avatar-upload/Definitions";
|
import {ModalAvatarUploadEvents, ModalAvatarUploadVariables} from "tc-shared/ui/modal/avatar-upload/Definitions";
|
||||||
import {ModalInputProcessorEvents, ModalInputProcessorVariables} from "tc-shared/ui/modal/input-processor/Definitios";
|
import {ModalInputProcessorEvents, ModalInputProcessorVariables} from "tc-shared/ui/modal/input-processor/Definitios";
|
||||||
|
import {ModalAboutVariables} from "tc-shared/ui/modal/about/Definitions";
|
||||||
|
|
||||||
export type ModalType = "error" | "warning" | "info" | "none";
|
export type ModalType = "error" | "warning" | "info" | "none";
|
||||||
export type ModalRenderType = "page" | "dialog";
|
export type ModalRenderType = "page" | "dialog";
|
||||||
|
@ -215,5 +216,9 @@ export interface ModalConstructorArguments {
|
||||||
"modal-input-processor": [
|
"modal-input-processor": [
|
||||||
/* events */ IpcRegistryDescription<ModalInputProcessorEvents>,
|
/* events */ IpcRegistryDescription<ModalInputProcessorEvents>,
|
||||||
/* variables */ IpcVariableDescriptor<ModalInputProcessorVariables>,
|
/* variables */ IpcVariableDescriptor<ModalInputProcessorVariables>,
|
||||||
|
],
|
||||||
|
"modal-about": [
|
||||||
|
/* events */ IpcRegistryDescription,
|
||||||
|
/* variables */ IpcVariableDescriptor<ModalAboutVariables>
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -113,4 +113,10 @@ registerModal({
|
||||||
modalId: "modal-input-processor",
|
modalId: "modal-input-processor",
|
||||||
classLoader: async () => await import("tc-shared/ui/modal/input-processor/Renderer"),
|
classLoader: async () => await import("tc-shared/ui/modal/input-processor/Renderer"),
|
||||||
popoutSupported: true
|
popoutSupported: true
|
||||||
|
});
|
||||||
|
|
||||||
|
registerModal({
|
||||||
|
modalId: "modal-about",
|
||||||
|
classLoader: async () => await import("tc-shared/ui/modal/about/Renderer"),
|
||||||
|
popoutSupported: true
|
||||||
});
|
});
|
|
@ -53,8 +53,8 @@ class LocalUiVariableConsumer<Variables extends UiVariableMap> extends UiVariabl
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createLocalUiVariables<Variables extends UiVariableMap>() : [UiVariableProvider<Variables>, UiVariableConsumer<Variables>] {
|
export function createLocalUiVariables<Variables extends UiVariableMap>() : [UiVariableProvider<Variables>, UiVariableConsumer<Variables>] {
|
||||||
const provider = new LocalUiVariableProvider();
|
const provider = new LocalUiVariableProvider<Variables>();
|
||||||
const consumer = new LocalUiVariableConsumer(provider);
|
const consumer = new LocalUiVariableConsumer<Variables>(provider);
|
||||||
provider.setConsumer(consumer);
|
provider.setConsumer(consumer);
|
||||||
return [provider as any, consumer as any];
|
return [provider, consumer];
|
||||||
}
|
}
|
|
@ -11,7 +11,6 @@ import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||||
import {settings, Settings} from "tc-shared/settings";
|
import {settings, Settings} from "tc-shared/settings";
|
||||||
import * as log from "tc-shared/log";
|
import * as log from "tc-shared/log";
|
||||||
import {LogCategory, logDebug, logError, logInfo, logTrace, logWarn} from "tc-shared/log";
|
import {LogCategory, logDebug, logError, logInfo, logTrace, logWarn} from "tc-shared/log";
|
||||||
import {Regex} from "tc-shared/ui/modal/ModalConnect";
|
|
||||||
import {AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler";
|
import {AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler";
|
||||||
import {WrappedWebSocket} from "./WrappedWebSocket";
|
import {WrappedWebSocket} from "./WrappedWebSocket";
|
||||||
import {AbstractVoiceConnection} from "tc-shared/connection/VoiceConnection";
|
import {AbstractVoiceConnection} from "tc-shared/connection/VoiceConnection";
|
||||||
|
@ -24,6 +23,7 @@ import {RTCConnection} from "tc-shared/connection/rtc/Connection";
|
||||||
import {RtpVideoConnection} from "tc-shared/connection/rtc/video/Connection";
|
import {RtpVideoConnection} from "tc-shared/connection/rtc/video/Connection";
|
||||||
import { tr } from "tc-shared/i18n/localize";
|
import { tr } from "tc-shared/i18n/localize";
|
||||||
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||||
|
import ipRegex from "ip-regex";
|
||||||
|
|
||||||
class ReturnListener<T> {
|
class ReturnListener<T> {
|
||||||
resolve: (value?: T | PromiseLike<T>) => void;
|
resolve: (value?: T | PromiseLike<T>) => void;
|
||||||
|
@ -137,10 +137,12 @@ export class ServerConnection extends AbstractServerConnection {
|
||||||
proxySocket:
|
proxySocket:
|
||||||
if(!settings.getValue(Settings.KEY_CONNECT_NO_DNSPROXY)) {
|
if(!settings.getValue(Settings.KEY_CONNECT_NO_DNSPROXY)) {
|
||||||
let host;
|
let host;
|
||||||
if(Regex.IP_V4.test(address.host)) {
|
|
||||||
host = address.host.replace(/\./g, "-") + ".con-gate.work";
|
if(ipRegex({ exact: true }).test(address.host)) {
|
||||||
} else if(Regex.IP_V6.test(address.host)) {
|
host = address.host;
|
||||||
host = address.host.replace(/\[(.*)]/, "$1").replace(/:/g, "_") + ".con-gate.work";
|
host = host.replace(/\./g, "-");
|
||||||
|
host = host.replace(/:/g, "_");
|
||||||
|
host = host + ".con-gate.work";
|
||||||
} else {
|
} else {
|
||||||
break proxySocket;
|
break proxySocket;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* This is the entry point file for the external modals */
|
/* This is the entry point file for the external modals */
|
||||||
|
|
||||||
import "../ui/context-menu";
|
import "../ui/context-menu";
|
||||||
|
import "../hooks/KeyBoard";
|
||||||
import "tc-shared/entry-points/ModalWindow";
|
import "tc-shared/entry-points/ModalWindow";
|
|
@ -3,6 +3,10 @@ import {Stage} from "tc-loader";
|
||||||
import {setKeyBoardBackend} from "tc-shared/PPTListener";
|
import {setKeyBoardBackend} from "tc-shared/PPTListener";
|
||||||
import {WebKeyBoard} from "../KeyBoard";
|
import {WebKeyBoard} from "../KeyBoard";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Will be loaded within the renderer and the main application.
|
||||||
|
*/
|
||||||
|
|
||||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
name: "audio backend init",
|
name: "audio backend init",
|
||||||
function: async () => setKeyBoardBackend(new WebKeyBoard()),
|
function: async () => setKeyBoardBackend(new WebKeyBoard()),
|
||||||
|
|
Loading…
Reference in New Issue