Make the about mopdal popoutable
parent
d4179db329
commit
943c33ff4b
|
@ -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"
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
:global {
|
||||
.modal-about {
|
||||
display: flex!important;
|
||||
flex-direction: row!important;
|
||||
|
||||
text-align: center;
|
||||
color: #999999;
|
||||
|
||||
.container-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container-right {
|
||||
text-align: left;
|
||||
padding-left: 2em;
|
||||
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
margin-block-start: 0.35em;
|
||||
margin-block-end: 0.35em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25em;
|
||||
margin-block-start: 0.10em;
|
||||
margin-block-end: 0.10em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-block-start: .25em;
|
||||
margin-block-end: .25em;
|
||||
}
|
||||
}
|
||||
|
||||
.version {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
a {
|
||||
width: 50%;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
text-align: right;
|
||||
}
|
||||
.value {
|
||||
padding-left: .25em;
|
||||
|
||||
text-align: left;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(.03125 0 0 .03125 -3.5723e-5 0)" fill="#7289da">
|
||||
<circle cx="48" cy="304" r="24"/>
|
||||
<circle cx="192" cy="448" r="24"/>
|
||||
<circle cx="101" cy="395" r="24"/>
|
||||
<circle cx="48" cy="304" r="24"/>
|
||||
<path d="m216 448a24 24 0 10-24 24 24 24 0 0024-24"/>
|
||||
<circle cx="101" cy="395" r="24"/>
|
||||
<circle cx="48" cy="192" r="24"/>
|
||||
<circle cx="192" cy="48" r="24"/>
|
||||
<circle cx="101" cy="101" r="24"/>
|
||||
<circle cx="48" cy="192" r="24"/>
|
||||
<path d="m216 48a24 24 0 11-24-24 24 24 0 0124 24"/>
|
||||
<circle cx="101" cy="101" r="24"/>
|
||||
<path d="m311.99 472a24 24 0 01-6.433-47.123 185.51 185.51 0 0096.328-64.917 181.56 181.56 0 0038.113-111.96c0-81.5-55.349-154.25-134.6-176.92a24 24 0 0113.2-46.147 236.54 236.54 0 01121 82.086 230.51 230.51 0 01.276 282.28 233.82 233.82 0 01-121.42 81.812 24.04 24.04 0 01-6.463.888z"/>
|
||||
<path d="m456.03 488a24.512 24.512 0 01-2.679-.148l-144-16a24 24 0 01-20.474-30.278l40-144a24 24 0 1146.248 12.848l-32.454 116.84 115.98 12.886a24 24 0 01-2.621 47.854z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -10,7 +10,6 @@ import {hashPassword} from "./utils/helpers";
|
|||
import {HandshakeHandler} from "./connection/HandshakeHandler";
|
||||
import {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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
//TODO: Test if we could render this image and not only the browser by knowing the type.
|
||||
import {createErrorModal, createModal} from "../../ui/elements/Modal";
|
||||
import {tr, tra} from "../../i18n/localize";
|
||||
import {arrayBufferBase64} from "../../utils/buffers";
|
||||
import {LogCategory, logError, logTrace} from "tc-shared/log";
|
||||
|
||||
export function spawnAvatarUpload(callback_data: (data: ArrayBuffer | undefined | null) => any) {
|
||||
const modal = createModal({
|
||||
header: tr("Avatar Upload"),
|
||||
footer: undefined,
|
||||
body: () => {
|
||||
return $("#tmpl_avatar_upload").renderTag({});
|
||||
}
|
||||
});
|
||||
|
||||
let _data_submitted = false;
|
||||
let _current_avatar;
|
||||
|
||||
modal.htmlTag.find(".button-select").on('click', event => {
|
||||
modal.htmlTag.find(".file-inputs").trigger('click');
|
||||
});
|
||||
|
||||
modal.htmlTag.find(".button-delete").on('click', () => {
|
||||
if (_data_submitted)
|
||||
return;
|
||||
_data_submitted = true;
|
||||
modal.close();
|
||||
callback_data(null);
|
||||
});
|
||||
|
||||
modal.htmlTag.find(".button-cancel").on('click', () => modal.close());
|
||||
const button_upload = modal.htmlTag.find(".button-upload");
|
||||
button_upload.on('click', event => (!_data_submitted) && (_data_submitted = true, modal.close(), true) && callback_data(_current_avatar));
|
||||
|
||||
const set_avatar = (data: string | undefined, type?: string) => {
|
||||
_current_avatar = data ? arrayBufferBase64(data) : undefined;
|
||||
button_upload.prop("disabled", !_current_avatar);
|
||||
modal.htmlTag.find(".preview img").attr("src", data ? ("data:image/" + type + ";base64," + data) : "img/style/avatar.png");
|
||||
};
|
||||
|
||||
const input_node = modal.htmlTag.find(".file-inputs")[0] as HTMLInputElement;
|
||||
input_node.multiple = false;
|
||||
|
||||
modal.htmlTag.find(".file-inputs").on('change', event => {
|
||||
logTrace(LogCategory.CLIENT, "Files: %o", input_node.files);
|
||||
|
||||
const read_file = (file: File) => new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = error => reject(error);
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const data = await read_file(input_node.files[0]);
|
||||
|
||||
if (!data.startsWith("data:image/")) {
|
||||
logError(LogCategory.FILE_TRANSFER, tr("Failed to load file %s: Invalid data media type (%o)"), input_node.files[0].name, data);
|
||||
createErrorModal(tr("Icon upload failed"), tra("Failed to select avatar {}.<br>File is not an image", input_node.files[0].name)).open();
|
||||
return;
|
||||
}
|
||||
const semi = data.indexOf(';');
|
||||
const type = data.substring(11, semi);
|
||||
logTrace(LogCategory.CLIENT, tr("Given image has type %s"), type);
|
||||
|
||||
set_avatar(data.substr(semi + 8 /* 8 bytes := base64, */), type);
|
||||
})();
|
||||
});
|
||||
set_avatar(undefined);
|
||||
modal.close_listener.push(() => !_data_submitted && callback_data(undefined));
|
||||
modal.open();
|
||||
}
|
|
@ -29,9 +29,7 @@ export function spawnAvatarList(client: ConnectionHandler) {
|
|||
header: tr("Avatars"),
|
||||
footer: undefined,
|
||||
body: () => {
|
||||
const template = $("#tmpl_avatar_list").renderTag({});
|
||||
|
||||
return template;
|
||||
return $("#tmpl_avatar_list").renderTag({});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
export const Regex = {
|
||||
//DOMAIN<:port>
|
||||
DOMAIN: /^(localhost|((([a-zA-Z0-9_-]{0,63}\.){0,253})?[a-zA-Z0-9_-]{0,63}\.[a-zA-Z]{2,64}))(|:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[0-5]?[0-9]{1,46}))$/,
|
||||
//IP<:port>
|
||||
IP_V4: /(^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(|:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[0-5]?[0-9]{1,4}))$/,
|
||||
IP_V6: /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/,
|
||||
IP: /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$|^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/,
|
||||
};
|
|
@ -1,85 +0,0 @@
|
|||
import {LogCategory, logWarn} from "../../log";
|
||||
import {createModal, Modal} from "../../ui/elements/Modal";
|
||||
import {ClientEntry} from "../../tree/Client";
|
||||
import {GroupManager, GroupType} from "../../permission/GroupManager";
|
||||
import PermissionType from "../../permission/PermissionType";
|
||||
import {generateIconJQueryTag, getIconManager} from "tc-shared/file/Icons";
|
||||
|
||||
let current_modal: Modal;
|
||||
export function createServerGroupAssignmentModal(client: ClientEntry, callback: (groups: number[], flag: boolean) => Promise<boolean>) {
|
||||
if(current_modal)
|
||||
current_modal.close();
|
||||
|
||||
current_modal = createModal({
|
||||
header: tr("Server Groups"),
|
||||
body: () => {
|
||||
let tag: any = {};
|
||||
let groups = tag["groups"] = [];
|
||||
|
||||
tag["client"] = client.createChatTag();
|
||||
|
||||
const _groups = client.channelTree.client.groups.serverGroups.sort(GroupManager.sorter());
|
||||
for(let group of _groups) {
|
||||
if(group.type != GroupType.NORMAL) continue;
|
||||
|
||||
let entry = {} as any;
|
||||
entry["id"] = group.id;
|
||||
entry["name"] = group.name;
|
||||
entry["disabled"] = !client.channelTree.client.permissions.neededPermission(PermissionType.I_GROUP_MEMBER_ADD_POWER).granted(group.requiredMemberRemovePower);
|
||||
entry["default"] = client.channelTree.server.properties.virtualserver_default_server_group == group.id;
|
||||
tag["icon_" + group.id] = generateIconJQueryTag(getIconManager().resolveIcon(group.properties.iconid, client.channelTree.server.properties.virtualserver_unique_identifier, client.channelTree.client.handlerId));
|
||||
groups.push(entry);
|
||||
}
|
||||
|
||||
let template = $("#tmpl_server_group_assignment").renderTag(tag);
|
||||
|
||||
const update_groups = () => {
|
||||
for(let group of _groups) {
|
||||
template.find("input[group-id='" + group.id + "']").prop("checked", client.groupAssigned(group));
|
||||
}
|
||||
};
|
||||
|
||||
template.find(".group-entry input").each((_idx, _entry) => {
|
||||
let entry = $(_entry);
|
||||
|
||||
entry.on('change', event => {
|
||||
let group_id = parseInt(entry.attr("group-id"));
|
||||
let group = client.channelTree.client.groups.findServerGroup(group_id);
|
||||
if(!group) {
|
||||
logWarn(LogCategory.GENERAL, tr("Could not resolve target group!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
let target = entry.prop("checked");
|
||||
callback([group.id], target).catch(e => { logWarn(LogCategory.GENERAL, tr("Failed to change group assignment: %o"), e)}).then(update_groups);
|
||||
});
|
||||
});
|
||||
|
||||
template.find(".button-close").on('click', () => current_modal.close());
|
||||
template.find(".button-remove-all").on('click', () => {
|
||||
const group_ids = [];
|
||||
|
||||
template.find(".group-entry input").each((_idx, _entry) => {
|
||||
let entry = $(_entry);
|
||||
if(entry.attr("default") !== undefined || !entry.prop("checked"))
|
||||
return;
|
||||
|
||||
group_ids.push(parseInt(entry.attr("group-id")));
|
||||
});
|
||||
|
||||
callback(group_ids, false).catch(e => { logWarn(LogCategory.GENERAL, tr("Failed to remove all group assignments: %o"), e)}).then(update_groups);
|
||||
|
||||
});
|
||||
|
||||
update_groups();
|
||||
return template;
|
||||
},
|
||||
footer: null,
|
||||
min_width: "10em"
|
||||
|
||||
});
|
||||
|
||||
current_modal.htmlTag.find(".modal-body").addClass("modal-server-group-assignments");
|
||||
current_modal.close_listener.push(() => current_modal = undefined);
|
||||
current_modal.open();
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import {Registry} from "tc-events";
|
||||
import {ModalAboutEvents, ModalAboutVariables} from "tc-shared/ui/modal/about/Definitions";
|
||||
import {IpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable";
|
||||
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
||||
import {CallOnce, ignorePromise} from "tc-shared/proto";
|
||||
import {getBackend} from "tc-shared/backend";
|
||||
|
||||
class Controller {
|
||||
readonly events: Registry<ModalAboutEvents>;
|
||||
readonly variables: IpcUiVariableProvider<ModalAboutVariables>;
|
||||
|
||||
private eggShown: boolean;
|
||||
|
||||
constructor() {
|
||||
this.events = new Registry<ModalAboutEvents>();
|
||||
this.variables = new IpcUiVariableProvider<ModalAboutVariables>();
|
||||
|
||||
this.eggShown = false;
|
||||
|
||||
this.variables.setVariableProvider("nativeVersion", () => {
|
||||
if(__build.target === "client") {
|
||||
const backend = getBackend("native");
|
||||
return backend.getVersionInfo().version;
|
||||
} else {
|
||||
return "unknown";
|
||||
}
|
||||
});
|
||||
|
||||
this.variables.setVariableProvider("uiVersion", () => __build.version);
|
||||
this.variables.setVariableProvider("uiVersionTimestamp", () => __build.timestamp);
|
||||
|
||||
this.variables.setVariableProvider("eggShown", () => this.eggShown);
|
||||
this.variables.setVariableEditor("eggShown", newValue => { this.eggShown = newValue; });
|
||||
|
||||
this.events.on("action_update_high_score", event => {
|
||||
let highScore = parseInt(localStorage.getItem("ee-snake-high-score"));
|
||||
if(!isNaN(highScore) && highScore >= event.score) {
|
||||
/* No change */
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem("ee-snake-high-score", event.score.toString());
|
||||
});
|
||||
|
||||
this.events.on("query_high_score", () => {
|
||||
let highScore = parseInt(localStorage.getItem("ee-snake-high-score"));
|
||||
if(isNaN(highScore)) {
|
||||
highScore = 0;
|
||||
}
|
||||
|
||||
this.events.fire("notify_high_score", { score: highScore });
|
||||
});
|
||||
}
|
||||
|
||||
@CallOnce
|
||||
destroy() {
|
||||
this.events.destroy();
|
||||
this.variables.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export function spawnAboutModal() {
|
||||
const controller = new Controller();
|
||||
const modal = spawnModal("modal-about", [
|
||||
controller.events.generateIpcDescription(),
|
||||
controller.variables.generateConsumerDescription()
|
||||
], {
|
||||
popoutable: true
|
||||
});
|
||||
|
||||
modal.getEvents().on("destroy", () => controller.destroy());
|
||||
ignorePromise(modal.show());
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
export interface ModalAboutVariables {
|
||||
readonly uiVersion: string,
|
||||
readonly uiVersionTimestamp: number,
|
||||
readonly nativeVersion: string,
|
||||
|
||||
eggShown: boolean
|
||||
}
|
||||
|
||||
export interface ModalAboutEvents {
|
||||
action_update_high_score: { score: number },
|
||||
query_high_score: {},
|
||||
notify_high_score: { score: number },
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
|
||||
position: relative;
|
||||
|
||||
padding: 1em;
|
||||
user-select: none;
|
||||
color: #999999;
|
||||
|
||||
&.windowed {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.containerLeft {
|
||||
align-self: center;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.containerRight {
|
||||
align-self: center;
|
||||
|
||||
text-align: left;
|
||||
padding-left: 2em;
|
||||
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
margin-block-start: 0.35em;
|
||||
margin-block-end: 0.35em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25em;
|
||||
margin-block-start: 0.10em;
|
||||
margin-block-end: 0.10em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-block-start: .25em;
|
||||
margin-block-end: .25em;
|
||||
}
|
||||
|
||||
a {
|
||||
user-select: all;
|
||||
}
|
||||
}
|
||||
|
||||
.version {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.key {
|
||||
width: 50%;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.value {
|
||||
width: 50%;
|
||||
padding-left: .25em;
|
||||
|
||||
text-align: left;
|
||||
user-select: all;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gameContainer {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,752 @@
|
|||
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
import React, {useContext, useEffect} from "react";
|
||||
import {IpcRegistryDescription, Registry} from "tc-events";
|
||||
import {ModalAboutEvents, ModalAboutVariables} from "tc-shared/ui/modal/about/Definitions";
|
||||
import {UiVariableConsumer} from "tc-shared/ui/utils/Variable";
|
||||
import {createIpcUiVariableConsumer, IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {joinClassList, useTr} from "tc-shared/ui/react-elements/Helper";
|
||||
import TeaCupAnimatedImage from "./TeaSpeakCupAnimated.png";
|
||||
import {LogCategory, logError} from "tc-shared/log";
|
||||
import {CallOnce} from "tc-shared/proto";
|
||||
import {EventType, getKeyBoard, KeyEvent} from "tc-shared/PPTListener";
|
||||
|
||||
const cssStyle = require("./Renderer.scss");
|
||||
const VariablesContext = React.createContext<UiVariableConsumer<ModalAboutVariables>>(undefined);
|
||||
const EventsContext = React.createContext<Registry<ModalAboutEvents>>(undefined);
|
||||
|
||||
interface CanvasProperties {
|
||||
with: number,
|
||||
height: number,
|
||||
|
||||
timestamp: number,
|
||||
}
|
||||
|
||||
interface TickProperties {
|
||||
timestamp: number,
|
||||
}
|
||||
|
||||
interface GameState {
|
||||
name() : string;
|
||||
|
||||
initialize(game: SnakeGame);
|
||||
finalize();
|
||||
|
||||
handleKeyEvent(event: KeyEvent);
|
||||
gameTick(game: SnakeGame, properties: TickProperties);
|
||||
render(context: CanvasRenderingContext2D, properties: CanvasProperties);
|
||||
}
|
||||
|
||||
class GameStateCriticalError implements GameState {
|
||||
constructor(readonly errorMessage: string) {}
|
||||
|
||||
name(): string {
|
||||
return "Critical Error";
|
||||
}
|
||||
|
||||
initialize() { }
|
||||
finalize() { }
|
||||
|
||||
handleKeyEvent(event: KeyEvent) {}
|
||||
gameTick(game: SnakeGame, properties: TickProperties) { }
|
||||
render(context: CanvasRenderingContext2D, properties: CanvasProperties) {
|
||||
context.fillStyle = "black";
|
||||
context.fillRect(0, 0, properties.with, properties.height);
|
||||
|
||||
const fontPixelSize = Math.max(10, Math.floor(properties.height * 0.025));
|
||||
context.fillStyle = "red";
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
context.font = fontPixelSize + "px Lucida Console, monospace";
|
||||
context.fillText(tr("A critical error happened") + ":", properties.with / 2, (properties.height - fontPixelSize - 2) / 2);
|
||||
context.fillText(this.errorMessage, properties.with / 2, (properties.height + fontPixelSize + 2) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
class GameStateStart implements GameState {
|
||||
private highScore: number;
|
||||
private spacePressed: boolean;
|
||||
|
||||
name(): string {
|
||||
return "Start";
|
||||
}
|
||||
|
||||
initialize(game: SnakeGame) {
|
||||
this.highScore = game.getHighScore();
|
||||
this.spacePressed = false;
|
||||
}
|
||||
|
||||
finalize() { }
|
||||
|
||||
handleKeyEvent(event: KeyEvent) {
|
||||
if(event.keyCode === "Space") {
|
||||
this.spacePressed = event.type !== EventType.KEY_RELEASE;
|
||||
}
|
||||
}
|
||||
|
||||
gameTick(game: SnakeGame, properties: TickProperties) {
|
||||
if(this.spacePressed) {
|
||||
game.setState(new GameStateInGame());
|
||||
return;
|
||||
}
|
||||
|
||||
this.highScore = game.getHighScore();
|
||||
}
|
||||
|
||||
render(context: CanvasRenderingContext2D, properties: CanvasProperties) {
|
||||
const fontPixelSize = Math.max(10, Math.floor(properties.height * 0.05));
|
||||
const dynamicFontPixelSize = fontPixelSize + Math.sin(properties.timestamp / 750) / 2;
|
||||
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
|
||||
context.font = fontPixelSize + "px Lucida Console, monospace";
|
||||
context.fillStyle = "white";
|
||||
context.fillText(tr("Welcome to the snake game."), properties.with / 2, properties.height / 2 - fontPixelSize);
|
||||
if(this.highScore > 0) {
|
||||
context.fillText(tr("High score: ") + this.highScore, properties.with / 2, properties.height / 2);
|
||||
}
|
||||
|
||||
context.font = dynamicFontPixelSize + "px Lucida Console, monospace";
|
||||
context.fillStyle = "lightblue";
|
||||
context.fillText(tr("Press 'Space' to start!"), properties.with / 2, properties.height / 2 + fontPixelSize * 2);
|
||||
}
|
||||
}
|
||||
|
||||
class GameStateGameOver implements GameState {
|
||||
private spacePressed: boolean;
|
||||
|
||||
constructor(readonly gameScore: number) { }
|
||||
|
||||
name(): string {
|
||||
return "Start";
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.spacePressed = false;
|
||||
}
|
||||
|
||||
finalize() { }
|
||||
|
||||
handleKeyEvent(event: KeyEvent) {
|
||||
if(event.keyCode === "Space") {
|
||||
this.spacePressed = event.type !== EventType.KEY_RELEASE;
|
||||
}
|
||||
}
|
||||
|
||||
gameTick(game: SnakeGame, properties: TickProperties) {
|
||||
if(this.spacePressed) {
|
||||
game.setState(new GameStateStart());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
render(context: CanvasRenderingContext2D, properties: CanvasProperties) {
|
||||
const fontPixelSize = Math.max(10, Math.floor(properties.height * 0.04));
|
||||
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
|
||||
context.font = (fontPixelSize * 2) + "px Lucida Console, monospace";
|
||||
context.fillStyle = "red";
|
||||
context.fillText(tr("Game over!"), properties.with / 2, properties.height / 2 - fontPixelSize * 2);
|
||||
|
||||
context.font = fontPixelSize + "px Lucida Console, monospace";
|
||||
context.fillStyle = "white";
|
||||
context.fillText(tr("Current score: ") + this.gameScore, properties.with / 2, properties.height / 2);
|
||||
context.fillText(tr("Press 'Space' to continue."), properties.with / 2, properties.height / 2 + fontPixelSize * 2);
|
||||
}
|
||||
}
|
||||
|
||||
type GameDirection = "north" | "south" | "west" | "east";
|
||||
|
||||
class GameStateInGame implements GameState {
|
||||
private static readonly kGridWidth = 20;
|
||||
private static readonly kGridHeight = 20;
|
||||
|
||||
private static readonly kSnakeSpeed = 300;
|
||||
|
||||
private lastTileMove: number;
|
||||
private snake: GameDirection[];
|
||||
private snakePosition: { x: number, y: number };
|
||||
private snakeDirection: GameDirection;
|
||||
|
||||
private applePosition: { x: number, y: number };
|
||||
|
||||
name(): string {
|
||||
return "InGame";
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.snake = [];
|
||||
this.snakePosition = { x: Math.floor(GameStateInGame.kGridWidth / 2), y: Math.floor(GameStateInGame.kGridHeight / 2) };
|
||||
this.snakeDirection = "north";
|
||||
|
||||
this.generateApple([[this.snakePosition.x, this.snakePosition.y]]);
|
||||
}
|
||||
|
||||
finalize() {}
|
||||
|
||||
handleKeyEvent(event: KeyEvent) {
|
||||
if(event.type !== EventType.KEY_RELEASE) {
|
||||
switch (event.key) {
|
||||
case "ArrowRight":
|
||||
this.snakeDirection = "east";
|
||||
break;
|
||||
|
||||
case "ArrowLeft":
|
||||
this.snakeDirection = "west";
|
||||
break;
|
||||
|
||||
case "ArrowUp":
|
||||
this.snakeDirection = "north";
|
||||
break;
|
||||
|
||||
case "ArrowDown":
|
||||
this.snakeDirection = "south";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gameTick(game: SnakeGame, properties: TickProperties) {
|
||||
if(typeof this.lastTileMove === "undefined") {
|
||||
this.lastTileMove = properties.timestamp;
|
||||
} else {
|
||||
let moveSteps = Math.floor((properties.timestamp - this.lastTileMove) / GameStateInGame.kSnakeSpeed);
|
||||
this.lastTileMove += moveSteps * GameStateInGame.kSnakeSpeed;
|
||||
|
||||
let blockedTiles: [number, number][];
|
||||
while(moveSteps-- > 0) {
|
||||
this.snake.unshift(this.snakeDirection);
|
||||
|
||||
switch (this.snakeDirection) {
|
||||
case "north":
|
||||
this.snakePosition.y -= 1;
|
||||
break;
|
||||
|
||||
case "east":
|
||||
this.snakePosition.x += 1;
|
||||
break;
|
||||
|
||||
case "south":
|
||||
this.snakePosition.y += 1;
|
||||
break;
|
||||
|
||||
case "west":
|
||||
this.snakePosition.x -= 1;
|
||||
break;
|
||||
}
|
||||
|
||||
let generateApple = false;
|
||||
if(this.snakePosition.x === this.applePosition.x && this.snakePosition.y === this.applePosition.y) {
|
||||
generateApple = true;
|
||||
} else {
|
||||
this.snake.pop();
|
||||
}
|
||||
|
||||
blockedTiles = [];
|
||||
if(!this.validateSnake(blockedTiles)) {
|
||||
game.updateHighScore(this.snake.length + 1);
|
||||
game.setState(new GameStateGameOver(this.snake.length + 1));
|
||||
return;
|
||||
}
|
||||
|
||||
if(generateApple) {
|
||||
if(!this.generateApple(blockedTiles)) {
|
||||
game.updateHighScore(this.snake.length + 1);
|
||||
game.setState(new GameStateGameOver(this.snake.length + 1));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render(context: CanvasRenderingContext2D, properties: CanvasProperties) {
|
||||
const fontPixelSize = Math.max(10, Math.floor(properties.height * 0.025));
|
||||
|
||||
const borderSize = 2;
|
||||
const paddingTop = 5;
|
||||
const paddingLeft = 5;
|
||||
const tileSize = Math.min(
|
||||
(properties.height - borderSize * 2 - paddingTop * 2) / GameStateInGame.kGridHeight,
|
||||
(properties.with - borderSize * 2 - paddingLeft * 2) / GameStateInGame.kGridWidth
|
||||
);
|
||||
|
||||
context.strokeStyle = "green";
|
||||
context.lineWidth = borderSize;
|
||||
|
||||
const gridWidth = tileSize * GameStateInGame.kGridWidth;
|
||||
const gridHeight = tileSize * GameStateInGame.kGridHeight;
|
||||
const gridOffsetX = (properties.with - gridWidth) / 2;
|
||||
const gridOffsetY = (properties.height - gridHeight) / 2;
|
||||
|
||||
context.strokeRect(gridOffsetX - borderSize / 2, gridOffsetY - borderSize / 2, gridWidth + borderSize, gridHeight + borderSize);
|
||||
|
||||
{
|
||||
context.save();
|
||||
|
||||
context.translate(gridOffsetX, gridOffsetY);
|
||||
context.scale(tileSize, tileSize);
|
||||
this.renderSnake(context);
|
||||
|
||||
context.restore();
|
||||
}
|
||||
|
||||
{
|
||||
context.font = fontPixelSize + "px Lucida Console, monospace";
|
||||
context.fillStyle = "green";
|
||||
context.textAlign = "left";
|
||||
context.textBaseline = "top";
|
||||
|
||||
const textScore = tr("Score") + ":";
|
||||
const textScoreBounds = context.measureText(textScore)
|
||||
context.fillText(textScore, gridOffsetX + tileSize / 2, gridOffsetY + tileSize / 2);
|
||||
context.fillText(this.snake.length.toString(), gridOffsetX + tileSize / 2 + textScoreBounds.width + fontPixelSize * .25, gridOffsetY + tileSize / 2);
|
||||
}
|
||||
}
|
||||
|
||||
renderSnake(context: CanvasRenderingContext2D) {
|
||||
let positionX = this.snakePosition.x, positionY = this.snakePosition.y;
|
||||
|
||||
for(let tileIndex = 0; tileIndex <= this.snake.length; tileIndex++) {
|
||||
if(tileIndex === 0) {
|
||||
context.fillStyle = "lightgreen";
|
||||
} else if(tileIndex === 1) {
|
||||
context.fillStyle = "green";
|
||||
}
|
||||
context.fillRect(positionX, positionY, 1, 1);
|
||||
|
||||
const tileDirection = this.snake[tileIndex];
|
||||
switch (tileDirection) {
|
||||
case "north":
|
||||
positionY += 1;
|
||||
break;
|
||||
|
||||
case "east":
|
||||
positionX -= 1;
|
||||
break;
|
||||
|
||||
case "south":
|
||||
positionY -= 1;
|
||||
break;
|
||||
|
||||
case "west":
|
||||
positionX += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
context.fillStyle = "red";
|
||||
context.fillRect(this.applePosition.x, this.applePosition.y, 1, 1);
|
||||
}
|
||||
|
||||
private generateApple(blockedTiles: [number, number][]): boolean {
|
||||
const freeTiles = [];
|
||||
|
||||
for(let posX = 0; posX < GameStateInGame.kGridWidth; posX++) {
|
||||
for(let posY = 0; posY < GameStateInGame.kGridHeight; posY++) {
|
||||
if(blockedTiles.findIndex(tile => posX === tile[0] && posY === tile[1]) === -1) {
|
||||
freeTiles.push([posX, posY]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(freeTiles.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tile = freeTiles[Math.floor(Math.random() * freeTiles.length)];
|
||||
this.applePosition = {
|
||||
x: tile[0],
|
||||
y: tile[1]
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private validateSnake(blockedTiles: [number, number][]) : boolean {
|
||||
let positionX = this.snakePosition.x, positionY = this.snakePosition.y;
|
||||
|
||||
for(let tileIndex = 0; tileIndex <= this.snake.length; tileIndex++) {
|
||||
if(positionY < 0 || positionY >= GameStateInGame.kGridHeight) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(positionX < 0 || positionX >= GameStateInGame.kGridWidth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(blockedTiles.findIndex(tile => tile[0] === positionX && tile[1] === positionY) !== -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
blockedTiles.push([positionX, positionY]);
|
||||
switch (this.snake[tileIndex]) {
|
||||
case "north":
|
||||
positionY += 1;
|
||||
break;
|
||||
|
||||
case "east":
|
||||
positionX -= 1;
|
||||
break;
|
||||
|
||||
case "south":
|
||||
positionY -= 1;
|
||||
break;
|
||||
|
||||
case "west":
|
||||
positionX += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class SnakeGame {
|
||||
private static readonly kDebugInfo = false;
|
||||
private readonly keyListener: () => void;
|
||||
private readonly canvasElement: HTMLCanvasElement;
|
||||
private readonly canvasContext: CanvasRenderingContext2D;
|
||||
private readonly renderTimings: number[];
|
||||
private currentFps: number;
|
||||
private currentFrameTime: number;
|
||||
|
||||
private animationId: number;
|
||||
private tickId: number;
|
||||
|
||||
private highScore: number;
|
||||
private highScoreListener: (newValue: number) => void;
|
||||
|
||||
|
||||
private currentState: GameState;
|
||||
|
||||
constructor(canvasElement: HTMLCanvasElement) {
|
||||
this.canvasElement = canvasElement;
|
||||
this.canvasContext = this.canvasElement.getContext("2d");
|
||||
this.setState(new GameStateCriticalError("Missing initial state"));
|
||||
|
||||
this.currentFps = 0;
|
||||
this.currentFrameTime = 0;
|
||||
|
||||
this.renderTimings = [];
|
||||
this.canvasContext.imageSmoothingEnabled = false;
|
||||
//this.canvasContext.imageSmoothingQuality = "high";
|
||||
|
||||
this.animationId = requestAnimationFrame(() => {
|
||||
this.invokeRender();
|
||||
});
|
||||
|
||||
this.tickId = setInterval(() => {
|
||||
try {
|
||||
this.currentState.gameTick(this, {
|
||||
timestamp: Date.now()
|
||||
});
|
||||
} catch (error) {
|
||||
logError(LogCategory.GENERAL, tr("Failed to tick current game state: %o"), error);
|
||||
this.setState(new GameStateCriticalError(tr("game tick caused an error")))
|
||||
}
|
||||
}, 50);
|
||||
|
||||
{
|
||||
const keyboard = getKeyBoard();
|
||||
const listener = event => {
|
||||
this.currentState.handleKeyEvent(event);
|
||||
};
|
||||
keyboard.registerListener(listener);
|
||||
this.keyListener = () => keyboard.unregisterListener(listener);
|
||||
}
|
||||
|
||||
this.highScore = 0;
|
||||
this.setState(new GameStateStart());
|
||||
}
|
||||
|
||||
getHighScore() : number { return this.highScore; }
|
||||
updateHighScore(gameScore: number) : boolean {
|
||||
if(gameScore <= this.highScore) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.highScore = gameScore;
|
||||
if(this.highScoreListener) {
|
||||
this.highScoreListener(this.highScore);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
setHighScoreListener(listener: (newValue: number) => void) {
|
||||
this.highScoreListener = listener;
|
||||
}
|
||||
|
||||
@CallOnce
|
||||
destroy() {
|
||||
this.keyListener();
|
||||
|
||||
cancelAnimationFrame(this.animationId);
|
||||
clearInterval(this.tickId);
|
||||
|
||||
this.tickId = 0;
|
||||
this.animationId = 0;
|
||||
}
|
||||
|
||||
setState(state: GameState) {
|
||||
this.currentState?.finalize();
|
||||
|
||||
this.currentState = state;
|
||||
this.currentState.initialize(this);
|
||||
}
|
||||
|
||||
private invokeRender() {
|
||||
const frameStart = performance.now();
|
||||
while(this.renderTimings[0] + 1000 <= frameStart) { this.renderTimings.shift(); }
|
||||
this.renderTimings.push(frameStart);
|
||||
this.currentFps = this.currentFps * .8 + this.renderTimings.length * .2;
|
||||
|
||||
try {
|
||||
this.render();
|
||||
} catch (error) {
|
||||
logError(LogCategory.GENERAL, tr("Failed to render game: %o"), error);
|
||||
}
|
||||
|
||||
const frameEnd = performance.now();
|
||||
this.currentFrameTime = this.currentFrameTime * .8 + (frameEnd - frameStart) * .2;
|
||||
|
||||
this.animationId = requestAnimationFrame(() => {
|
||||
this.invokeRender();
|
||||
});
|
||||
}
|
||||
|
||||
private render() {
|
||||
this.canvasElement.width = this.canvasElement.clientWidth;
|
||||
this.canvasElement.height = this.canvasElement.clientHeight;
|
||||
const properties: CanvasProperties = {
|
||||
with: this.canvasElement.clientWidth,
|
||||
height: this.canvasElement.clientHeight,
|
||||
timestamp: performance.now()
|
||||
};
|
||||
|
||||
const ctx = this.canvasContext;
|
||||
ctx.fillStyle = "black";
|
||||
ctx.fillRect(0, 0, properties.with, properties.height);
|
||||
|
||||
this.currentState.render(this.canvasContext, properties);
|
||||
|
||||
/* Debug Info */
|
||||
if(SnakeGame.kDebugInfo) {
|
||||
const fontPixelSize = Math.max(10, Math.floor(properties.height * 0.025));
|
||||
const keyWidth = 12 * fontPixelSize;
|
||||
|
||||
ctx.fillStyle = "white";
|
||||
ctx.textAlign = "left";
|
||||
ctx.textBaseline = "top";
|
||||
ctx.font = fontPixelSize + "px Lucida Console, monospace";
|
||||
ctx.fillText("FPS:", 10, 10);
|
||||
ctx.fillText(this.currentFps.toFixed(2), keyWidth, 10);
|
||||
|
||||
ctx.fillText("Frame Time (ms):", 10, 10 + fontPixelSize * 1.2);
|
||||
ctx.fillText(this.currentFrameTime.toFixed(2), keyWidth, 10 + fontPixelSize * 1.2);
|
||||
|
||||
ctx.fillText("Stage Name:", 10, 10 + fontPixelSize * 1.2 * 2);
|
||||
ctx.fillText(this.currentState.name(), keyWidth, 10 + fontPixelSize * 1.2 * 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SnakeGameRenderer = React.memo(() => {
|
||||
const events = useContext(EventsContext);
|
||||
const refCanvas = React.createRef<HTMLCanvasElement>();
|
||||
|
||||
useEffect(() => {
|
||||
const game = new SnakeGame(refCanvas.current);
|
||||
const listenerHighScore = events.on("notify_high_score", event => game.updateHighScore(event.score));
|
||||
|
||||
game.setHighScoreListener(newValue => events.fire("action_update_high_score", { score: newValue }));
|
||||
events.fire("query_high_score");
|
||||
|
||||
return () => {
|
||||
listenerHighScore();
|
||||
game.destroy();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={cssStyle.gameContainer}>
|
||||
<canvas ref={refCanvas} />
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
const SnakeEasterEgg = React.memo(() => {
|
||||
const variables = useContext(VariablesContext);
|
||||
const eggShown = variables.useReadOnly("eggShown", undefined, false);
|
||||
|
||||
if(eggShown) {
|
||||
return <SnakeGameRenderer />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
|
||||
const InfoTitle = React.memo(() => {
|
||||
const variables = useContext(VariablesContext);
|
||||
const uiVersion = variables.useReadOnly("uiVersion", undefined, useTr("loading"));
|
||||
|
||||
return (
|
||||
<h1>
|
||||
TeaSpeak-Client build {uiVersion}
|
||||
</h1>
|
||||
);
|
||||
});
|
||||
|
||||
const ModalTitle = React.memo(() => {
|
||||
const variables = useContext(VariablesContext);
|
||||
const eggShown = variables.useReadOnly("eggShown", undefined, false);
|
||||
|
||||
if(eggShown) {
|
||||
return <Translatable key={"snake"}>The Snake Game</Translatable>;
|
||||
} else if(__build.target === "web") {
|
||||
return <Translatable key={"web"}>About TeaWeb</Translatable>;
|
||||
} else {
|
||||
return <Translatable key={"client"}>About TeaClient</Translatable>;
|
||||
}
|
||||
});
|
||||
|
||||
const SupportEmail = React.memo(() => {
|
||||
let targetMail;
|
||||
if(__build.target === "web") {
|
||||
targetMail = "web.support@teaspeak.de";
|
||||
} else {
|
||||
targetMail = "client.support@teaspeak.de";
|
||||
}
|
||||
|
||||
return (
|
||||
<a href={"mailto:" + targetMail}>{targetMail}</a>
|
||||
);
|
||||
});
|
||||
|
||||
const VersionInfo = React.memo(() => {
|
||||
const variables = useContext(VariablesContext);
|
||||
const result = [];
|
||||
|
||||
const uiVersion = variables.useReadOnly("uiVersion", undefined, useTr("loading"));
|
||||
if(__build.target === "web") {
|
||||
result.push(
|
||||
<div className={cssStyle.version} key={"web"}>
|
||||
<div className={cssStyle.key}><Translatable>TeaWeb</Translatable></div>:
|
||||
<div className={cssStyle.value}>{uiVersion}</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const nativeVersion = variables.useReadOnly("nativeVersion", undefined, useTr("loading"));
|
||||
result.push(
|
||||
<div className={cssStyle.version} key={"native"}>
|
||||
<div className={cssStyle.key}><Translatable>TeaClient</Translatable></div>:
|
||||
<div className={cssStyle.value}>{nativeVersion}</div>
|
||||
</div>
|
||||
);
|
||||
result.push(
|
||||
<div className={cssStyle.version} key={"ui"}>
|
||||
<div className={cssStyle.key}><Translatable>User Interface</Translatable></div>:
|
||||
<div className={cssStyle.value}>{uiVersion}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{result}
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
const LicenseInfo = React.memo(() => {
|
||||
let applicationName;
|
||||
if(__build.target === "web") {
|
||||
applicationName = "TeaWeb";
|
||||
} else {
|
||||
applicationName = "TeaClient";
|
||||
}
|
||||
return (
|
||||
<p>
|
||||
The {applicationName} application is licensed by MPL-2.0<br />
|
||||
More information here: <a href="https://github.com/TeaSpeak/TeaWeb/blob/master/LICENSE.TXT" target="_blank">https://github.com/TeaSpeak/TeaWeb/blob/master/LICENSE.TXT</a>
|
||||
</p>
|
||||
)
|
||||
});
|
||||
|
||||
const MarkusHadenfeldt = React.memo(() => {
|
||||
const variables = useContext(VariablesContext);
|
||||
const variable = variables.useVariable("eggShown");
|
||||
|
||||
return (
|
||||
<span onDoubleClick={() => variable.setValue(true)}>
|
||||
(Markus Hadenfeldt)
|
||||
</span>
|
||||
);
|
||||
})
|
||||
|
||||
class Modal extends AbstractModal {
|
||||
private readonly events: Registry<ModalAboutEvents>;
|
||||
private readonly variables: UiVariableConsumer<ModalAboutVariables>;
|
||||
|
||||
constructor(events: IpcRegistryDescription<ModalAboutEvents>, variables: IpcVariableDescriptor<ModalAboutVariables>) {
|
||||
super();
|
||||
|
||||
this.events = Registry.fromIpcDescription(events);
|
||||
this.variables = createIpcUiVariableConsumer(variables);
|
||||
}
|
||||
|
||||
renderBody(): React.ReactElement {
|
||||
return (
|
||||
<EventsContext.Provider value={this.events}>
|
||||
<VariablesContext.Provider value={this.variables}>
|
||||
<div className={joinClassList(cssStyle.container, this.properties.windowed && cssStyle.windowed)}>
|
||||
<div className={cssStyle.containerLeft}>
|
||||
<div>
|
||||
<img src={TeaCupAnimatedImage} alt={useTr("TeaSpeak - Logo")} draggable={false} />
|
||||
</div>
|
||||
<div>
|
||||
Copyright (c) 2017-2021 TeaSpeak <br/>
|
||||
<MarkusHadenfeldt />
|
||||
</div>
|
||||
<div>
|
||||
<VersionInfo/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cssStyle.containerRight}>
|
||||
<InfoTitle/>
|
||||
<h2><Translatable>Special thanks</Translatable></h2>
|
||||
<p>
|
||||
"Яedeemer" (Janni K.)<br />
|
||||
Chromatic-Solutions (Sofian) for the lovely dark design
|
||||
</p>
|
||||
<h2><Translatable>Contact</Translatable></h2>
|
||||
<p>
|
||||
<Translatable>E-Mail</Translatable>: <SupportEmail /><br />
|
||||
<Translatable>WWW</Translatable>: <a href="https://teaspeak.de" target="_blank">https://teaspeak.de</a><br/>
|
||||
<Translatable>Community</Translatable>: <a href="https://forum.teaspeak.de" target="_blank">https://forum.teaspeak.de</a>
|
||||
</p>
|
||||
<h2><Translatable>License</Translatable></h2>
|
||||
<LicenseInfo />
|
||||
</div>
|
||||
<SnakeEasterEgg />
|
||||
</div>
|
||||
</VariablesContext.Provider>
|
||||
</EventsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
renderTitle(): string | React.ReactElement {
|
||||
return (
|
||||
<VariablesContext.Provider value={this.variables}>
|
||||
<ModalTitle />
|
||||
</VariablesContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Modal;
|
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
|
@ -17,10 +17,10 @@ import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
|||
import {server_connections} from "tc-shared/ConnectionManager";
|
||||
import {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;
|
||||
|
||||
|
|
|
@ -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>
|
||||
]
|
||||
}
|
|
@ -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
|
||||
});
|
|
@ -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];
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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";
|
|
@ -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()),
|
||||
|
|
Loading…
Reference in New Issue