diff --git a/shared/css/load-css.tsx b/shared/css/load-css.tsx index 173bcfa7..c520ea68 100644 --- a/shared/css/load-css.tsx +++ b/shared/css/load-css.tsx @@ -6,7 +6,6 @@ import "./static/htmltags.scss" import "./static/mixin.scss" import "./static/modal.scss" import "./static/modals.scss" -import "./static/modal-about.scss" import "./static/modal-avatar.scss" import "./static/modal-banclient.scss" import "./static/modal-banlist.scss" diff --git a/shared/css/static/modal-about.scss b/shared/css/static/modal-about.scss deleted file mode 100644 index 76395463..00000000 --- a/shared/css/static/modal-about.scss +++ /dev/null @@ -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; - } - } - } -} \ No newline at end of file diff --git a/shared/img/client-icons/settings_loading.svg b/shared/img/client-icons/settings_loading.svg new file mode 100644 index 00000000..dc1aa0db --- /dev/null +++ b/shared/img/client-icons/settings_loading.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index e8f21d65..b83ff81f 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -10,7 +10,6 @@ import {hashPassword} from "./utils/helpers"; import {HandshakeHandler} from "./connection/HandshakeHandler"; import {FilterMode, InputStartError, InputState} from "./voice/RecorderBase"; import {defaultRecorder, RecorderProfile} from "./voice/RecorderProfile"; -import {Regex} from "./ui/modal/ModalConnect"; import {formatMessage} from "./ui/frames/chat"; import {EventHandler, Registry} from "./events"; 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 {getDNSProvider} from "tc-shared/dns"; import {W2GPluginCmdHandler} from "tc-shared/ui/modal/video-viewer/W2GPlugin"; +import ipRegex from "ip-regex"; import * as htmltags from "./ui/htmltags"; 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 */ } else { this.log.log("connection.hostname.resolve", {}); @@ -640,7 +640,7 @@ export class ConnectionHandler { case DisconnectReason.HANDSHAKE_TEAMSPEAK_REQUIRED: createErrorModal( 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(); this.sound.play(Sound.CONNECTION_DISCONNECTED); autoReconnect = false; diff --git a/shared/js/PPTListener.ts b/shared/js/PPTListener.ts index 364b952c..2c61d017 100644 --- a/shared/js/PPTListener.ts +++ b/shared/js/PPTListener.ts @@ -1,125 +1,4 @@ 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 { KEY_PRESS, diff --git a/shared/js/main.tsx b/shared/js/main.tsx index 36b17dd1..b60ebc05 100644 --- a/shared/js/main.tsx +++ b/shared/js/main.tsx @@ -442,7 +442,7 @@ const task_connect_handler: loader.Task = { if(chandler && AppParameters.getValue(AppParameters.KEY_CONNECT_NO_SINGLE_INSTANCE)) { try { await chandler.post_connect_request(connectData, () => new Promise(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); }, { 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")); - const message = - "You're connecting to {0} within the other TeaWeb instance.{:br:}" + - "You could now close this page."; createInfoModal( 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, footer: undefined diff --git a/shared/js/ui/AppController.ts b/shared/js/ui/AppController.ts index 1b1b68ff..4a4a685f 100644 --- a/shared/js/ui/AppController.ts +++ b/shared/js/ui/AppController.ts @@ -81,12 +81,12 @@ export class AppController { this.connectionListEvents = new Registry(); 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.serverLogController = new ServerEventLogController(); 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) { diff --git a/shared/js/ui/elements/Modal.ts b/shared/js/ui/elements/Modal.ts index ac2911ba..4c04cb01 100644 --- a/shared/js/ui/elements/Modal.ts +++ b/shared/js/ui/elements/Modal.ts @@ -1,6 +1,5 @@ import * as loader from "tc-loader"; import {Stage} from "tc-loader"; -import {KeyCode} from "../../PPTListener"; import $ from "jquery"; import {LogCategory, logError} from "tc-shared/log"; @@ -341,7 +340,7 @@ export function createInputModal(headMessage: BodyCreator, question: BodyCreator button_submit.prop("disabled", !valid); }); input.on('keydown', event => { - if(event.keyCode !== KeyCode.KEY_RETURN || event.shiftKey) + if(event.key !== "Enter" || event.shiftKey) return; if(button_submit.prop("disabled")) return; diff --git a/shared/js/ui/modal/ModalAbout.ts b/shared/js/ui/modal/ModalAbout.ts index 4514cfd0..8548d714 100644 --- a/shared/js/ui/modal/ModalAbout.ts +++ b/shared/js/ui/modal/ModalAbout.ts @@ -1,6 +1,7 @@ import {createModal} from "../../ui/elements/Modal"; import {getBackend} from "tc-shared/backend"; import {tr} from "tc-shared/i18n/localize"; +import {spawnAboutModal} from "tc-shared/ui/modal/about/Controller"; function format_date(date: number) { const d = new Date(date); @@ -9,10 +10,13 @@ function format_date(date: number) { } export function spawnAbout() { + spawnAboutModal(); + return; + const connectModal = createModal({ header: tr("About"), body: () => { - let tag = $("#tmpl_about").renderTag({ + return $("#tmpl_about").renderTag({ client: __build.target !== "web", version_client: __build.target === "web" ? __build.version || "in-dev" : "loading...", @@ -20,7 +24,6 @@ export function spawnAbout() { version_timestamp: format_date(__build.timestamp) }); - return tag; }, footer: null, diff --git a/shared/js/ui/modal/ModalAvatar.ts b/shared/js/ui/modal/ModalAvatar.ts deleted file mode 100644 index 897d439e..00000000 --- a/shared/js/ui/modal/ModalAvatar.ts +++ /dev/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((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 {}.
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(); -} \ No newline at end of file diff --git a/shared/js/ui/modal/ModalAvatarList.ts b/shared/js/ui/modal/ModalAvatarList.ts index 897bd27a..a091d98c 100644 --- a/shared/js/ui/modal/ModalAvatarList.ts +++ b/shared/js/ui/modal/ModalAvatarList.ts @@ -29,9 +29,7 @@ export function spawnAvatarList(client: ConnectionHandler) { header: tr("Avatars"), footer: undefined, body: () => { - const template = $("#tmpl_avatar_list").renderTag({}); - - return template; + return $("#tmpl_avatar_list").renderTag({}); } }); diff --git a/shared/js/ui/modal/ModalConnect.ts b/shared/js/ui/modal/ModalConnect.ts deleted file mode 100644 index 1f24d304..00000000 --- a/shared/js/ui/modal/ModalConnect.ts +++ /dev/null @@ -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*$/, -}; \ No newline at end of file diff --git a/shared/js/ui/modal/ModalGroupAssignment.ts b/shared/js/ui/modal/ModalGroupAssignment.ts deleted file mode 100644 index 06d9a214..00000000 --- a/shared/js/ui/modal/ModalGroupAssignment.ts +++ /dev/null @@ -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) { - 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(); -} \ No newline at end of file diff --git a/shared/js/ui/modal/about/Controller.ts b/shared/js/ui/modal/about/Controller.ts new file mode 100644 index 00000000..6b666176 --- /dev/null +++ b/shared/js/ui/modal/about/Controller.ts @@ -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; + readonly variables: IpcUiVariableProvider; + + private eggShown: boolean; + + constructor() { + this.events = new Registry(); + this.variables = new IpcUiVariableProvider(); + + 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()); +} \ No newline at end of file diff --git a/shared/js/ui/modal/about/Definitions.ts b/shared/js/ui/modal/about/Definitions.ts new file mode 100644 index 00000000..829689a4 --- /dev/null +++ b/shared/js/ui/modal/about/Definitions.ts @@ -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 }, +} \ No newline at end of file diff --git a/shared/js/ui/modal/about/Renderer.scss b/shared/js/ui/modal/about/Renderer.scss new file mode 100644 index 00000000..70f8fc35 --- /dev/null +++ b/shared/js/ui/modal/about/Renderer.scss @@ -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; + } +} \ No newline at end of file diff --git a/shared/js/ui/modal/about/Renderer.tsx b/shared/js/ui/modal/about/Renderer.tsx new file mode 100644 index 00000000..0b26be37 --- /dev/null +++ b/shared/js/ui/modal/about/Renderer.tsx @@ -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>(undefined); +const EventsContext = React.createContext>(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(); + + 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 ( +
+ +
+ ) +}); + +const SnakeEasterEgg = React.memo(() => { + const variables = useContext(VariablesContext); + const eggShown = variables.useReadOnly("eggShown", undefined, false); + + if(eggShown) { + return ; + } else { + return null; + } +}) + +const InfoTitle = React.memo(() => { + const variables = useContext(VariablesContext); + const uiVersion = variables.useReadOnly("uiVersion", undefined, useTr("loading")); + + return ( +

+ TeaSpeak-Client build {uiVersion} +

+ ); +}); + +const ModalTitle = React.memo(() => { + const variables = useContext(VariablesContext); + const eggShown = variables.useReadOnly("eggShown", undefined, false); + + if(eggShown) { + return The Snake Game; + } else if(__build.target === "web") { + return About TeaWeb; + } else { + return About TeaClient; + } +}); + +const SupportEmail = React.memo(() => { + let targetMail; + if(__build.target === "web") { + targetMail = "web.support@teaspeak.de"; + } else { + targetMail = "client.support@teaspeak.de"; + } + + return ( + {targetMail} + ); +}); + +const VersionInfo = React.memo(() => { + const variables = useContext(VariablesContext); + const result = []; + + const uiVersion = variables.useReadOnly("uiVersion", undefined, useTr("loading")); + if(__build.target === "web") { + result.push( +
+
TeaWeb
: +
{uiVersion}
+
+ ); + } else { + const nativeVersion = variables.useReadOnly("nativeVersion", undefined, useTr("loading")); + result.push( +
+
TeaClient
: +
{nativeVersion}
+
+ ); + result.push( +
+
User Interface
: +
{uiVersion}
+
+ ); + } + + return ( + + {result} + + ); +}); + +const LicenseInfo = React.memo(() => { + let applicationName; + if(__build.target === "web") { + applicationName = "TeaWeb"; + } else { + applicationName = "TeaClient"; + } + return ( +

+ The {applicationName} application is licensed by MPL-2.0
+ More information here: https://github.com/TeaSpeak/TeaWeb/blob/master/LICENSE.TXT +

+ ) +}); + +const MarkusHadenfeldt = React.memo(() => { + const variables = useContext(VariablesContext); + const variable = variables.useVariable("eggShown"); + + return ( + variable.setValue(true)}> + (Markus Hadenfeldt) + + ); +}) + +class Modal extends AbstractModal { + private readonly events: Registry; + private readonly variables: UiVariableConsumer; + + constructor(events: IpcRegistryDescription, variables: IpcVariableDescriptor) { + super(); + + this.events = Registry.fromIpcDescription(events); + this.variables = createIpcUiVariableConsumer(variables); + } + + renderBody(): React.ReactElement { + return ( + + +
+
+
+ {useTr("TeaSpeak +
+
+ Copyright (c) 2017-2021 TeaSpeak
+ +
+
+ +
+
+
+ +

Special thanks

+

+ "Яedeemer" (Janni K.)
+ Chromatic-Solutions (Sofian) for the lovely dark design +

+

Contact

+

+ E-Mail:
+ WWW: https://teaspeak.de
+ Community: https://forum.teaspeak.de +

+

License

+ +
+ +
+
+
+ ); + } + + renderTitle(): string | React.ReactElement { + return ( + + + + ); + } +} + +export default Modal; \ No newline at end of file diff --git a/shared/js/ui/modal/about/TeaSpeakCupAnimated.png b/shared/js/ui/modal/about/TeaSpeakCupAnimated.png new file mode 100644 index 00000000..3eda87e5 Binary files /dev/null and b/shared/js/ui/modal/about/TeaSpeakCupAnimated.png differ diff --git a/shared/js/ui/modal/connect/Controller.ts b/shared/js/ui/modal/connect/Controller.ts index e9c24640..50a99a7a 100644 --- a/shared/js/ui/modal/connect/Controller.ts +++ b/shared/js/ui/modal/connect/Controller.ts @@ -17,10 +17,10 @@ import {ConnectionHandler} from "tc-shared/ConnectionHandler"; import {server_connections} from "tc-shared/ConnectionManager"; import {parseServerAddress} from "tc-shared/tree/Server"; import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings"; -import ipRegex from "ip-regex"; import {UiVariableProvider} from "tc-shared/ui/utils/Variable"; import {createIpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable"; 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; diff --git a/shared/js/ui/react-elements/modal/Definitions.ts b/shared/js/ui/react-elements/modal/Definitions.ts index f8ec60d6..b71057fd 100644 --- a/shared/js/ui/react-elements/modal/Definitions.ts +++ b/shared/js/ui/react-elements/modal/Definitions.ts @@ -21,6 +21,7 @@ import {PermissionEditorEvents} from "tc-shared/ui/modal/permission/EditorDefini import {PermissionEditorServerInfo} from "tc-shared/ui/modal/permission/ModalRenderer"; import {ModalAvatarUploadEvents, ModalAvatarUploadVariables} from "tc-shared/ui/modal/avatar-upload/Definitions"; 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 ModalRenderType = "page" | "dialog"; @@ -215,5 +216,9 @@ export interface ModalConstructorArguments { "modal-input-processor": [ /* events */ IpcRegistryDescription, /* variables */ IpcVariableDescriptor, + ], + "modal-about": [ + /* events */ IpcRegistryDescription, + /* variables */ IpcVariableDescriptor ] } \ No newline at end of file diff --git a/shared/js/ui/react-elements/modal/Registry.ts b/shared/js/ui/react-elements/modal/Registry.ts index 96fda81e..9aa865e8 100644 --- a/shared/js/ui/react-elements/modal/Registry.ts +++ b/shared/js/ui/react-elements/modal/Registry.ts @@ -113,4 +113,10 @@ registerModal({ modalId: "modal-input-processor", classLoader: async () => await import("tc-shared/ui/modal/input-processor/Renderer"), popoutSupported: true +}); + +registerModal({ + modalId: "modal-about", + classLoader: async () => await import("tc-shared/ui/modal/about/Renderer"), + popoutSupported: true }); \ No newline at end of file diff --git a/shared/js/ui/utils/LocalVariable.ts b/shared/js/ui/utils/LocalVariable.ts index c7a5e95c..5e892dc2 100644 --- a/shared/js/ui/utils/LocalVariable.ts +++ b/shared/js/ui/utils/LocalVariable.ts @@ -53,8 +53,8 @@ class LocalUiVariableConsumer extends UiVariabl } export function createLocalUiVariables() : [UiVariableProvider, UiVariableConsumer] { - const provider = new LocalUiVariableProvider(); - const consumer = new LocalUiVariableConsumer(provider); + const provider = new LocalUiVariableProvider(); + const consumer = new LocalUiVariableConsumer(provider); provider.setConsumer(consumer); - return [provider as any, consumer as any]; + return [provider, consumer]; } \ No newline at end of file diff --git a/web/app/connection/ServerConnection.ts b/web/app/connection/ServerConnection.ts index e5068a68..a12ee2d4 100644 --- a/web/app/connection/ServerConnection.ts +++ b/web/app/connection/ServerConnection.ts @@ -11,7 +11,6 @@ import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; import {settings, Settings} from "tc-shared/settings"; import * as log 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 {WrappedWebSocket} from "./WrappedWebSocket"; 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 { tr } from "tc-shared/i18n/localize"; import {createErrorModal} from "tc-shared/ui/elements/Modal"; +import ipRegex from "ip-regex"; class ReturnListener { resolve: (value?: T | PromiseLike) => void; @@ -137,10 +137,12 @@ export class ServerConnection extends AbstractServerConnection { proxySocket: if(!settings.getValue(Settings.KEY_CONNECT_NO_DNSPROXY)) { let host; - if(Regex.IP_V4.test(address.host)) { - host = address.host.replace(/\./g, "-") + ".con-gate.work"; - } else if(Regex.IP_V6.test(address.host)) { - host = address.host.replace(/\[(.*)]/, "$1").replace(/:/g, "_") + ".con-gate.work"; + + if(ipRegex({ exact: true }).test(address.host)) { + host = address.host; + host = host.replace(/\./g, "-"); + host = host.replace(/:/g, "_"); + host = host + ".con-gate.work"; } else { break proxySocket; } diff --git a/web/app/entry-points/ModalWindow.ts b/web/app/entry-points/ModalWindow.ts index f55e24d4..506d5c1d 100644 --- a/web/app/entry-points/ModalWindow.ts +++ b/web/app/entry-points/ModalWindow.ts @@ -1,4 +1,5 @@ /* This is the entry point file for the external modals */ import "../ui/context-menu"; +import "../hooks/KeyBoard"; import "tc-shared/entry-points/ModalWindow"; \ No newline at end of file diff --git a/web/app/hooks/KeyBoard.ts b/web/app/hooks/KeyBoard.ts index 9cce889d..625305a3 100644 --- a/web/app/hooks/KeyBoard.ts +++ b/web/app/hooks/KeyBoard.ts @@ -3,6 +3,10 @@ import {Stage} from "tc-loader"; import {setKeyBoardBackend} from "tc-shared/PPTListener"; import {WebKeyBoard} from "../KeyBoard"; +/* + * Will be loaded within the renderer and the main application. + */ + loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { name: "audio backend init", function: async () => setKeyBoardBackend(new WebKeyBoard()),