Make the about mopdal popoutable

master
WolverinDEV 2021-04-19 13:27:09 +02:00
parent d4179db329
commit 943c33ff4b
25 changed files with 995 additions and 376 deletions

View File

@ -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"

View File

@ -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;
}
}
}
}

View File

@ -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

View File

@ -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;

View File

@ -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,

View File

@ -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<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);
}, {
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

View File

@ -81,12 +81,12 @@ export class AppController {
this.connectionListEvents = new Registry<ConnectionListUIEvents>();
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) {

View File

@ -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;

View File

@ -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,

View File

@ -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();
}

View File

@ -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({});
}
});

View File

@ -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*$/,
};

View File

@ -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();
}

View File

@ -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());
}

View File

@ -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 },
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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;

View File

@ -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<ModalInputProcessorEvents>,
/* variables */ IpcVariableDescriptor<ModalInputProcessorVariables>,
],
"modal-about": [
/* events */ IpcRegistryDescription,
/* variables */ IpcVariableDescriptor<ModalAboutVariables>
]
}

View File

@ -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
});

View File

@ -53,8 +53,8 @@ class LocalUiVariableConsumer<Variables extends UiVariableMap> extends UiVariabl
}
export function createLocalUiVariables<Variables extends UiVariableMap>() : [UiVariableProvider<Variables>, UiVariableConsumer<Variables>] {
const provider = new LocalUiVariableProvider();
const consumer = new LocalUiVariableConsumer(provider);
const provider = new LocalUiVariableProvider<Variables>();
const consumer = new LocalUiVariableConsumer<Variables>(provider);
provider.setConsumer(consumer);
return [provider as any, consumer as any];
return [provider, consumer];
}

View File

@ -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<T> {
resolve: (value?: T | PromiseLike<T>) => 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;
}

View File

@ -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";

View File

@ -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()),