diff --git a/file.ts b/file.ts index 1fa20f8f..249d9c91 100644 --- a/file.ts +++ b/file.ts @@ -444,6 +444,7 @@ const CLIENT_APP_FILE_LIST = [ ...APP_FILE_LIST_CLIENT_SOURCE ]; +/* const WEB_APP_FILE_LIST = [ ...APP_FILE_LIST_SHARED_SOURCE, ...APP_FILE_LIST_SHARED_VENDORS, @@ -451,6 +452,174 @@ const WEB_APP_FILE_LIST = [ ...APP_FILE_LIST_WEB_TEASPEAK, ...CERTACCEPT_FILE_LIST, ]; +*/ +const WEB_APP_FILE_LIST = [ + ...APP_FILE_LIST_SHARED_VENDORS, + { /* shared html and php files */ + "type": "html", + "search-pattern": /^.*([a-zA-Z]+)\.(html|php|json)$/, + "build-target": "dev|rel", + + "path": "./", + "local-path": "./shared/html/" + }, + { /* javascript loader for releases */ + "type": "js", + "search-pattern": /.*$/, + "build-target": "dev|rel", + + "path": "js/", + "local-path": "./dist/" + }, + + { /* shared javascript files (WebRTC adapter) */ + "type": "js", + "search-pattern": /.*\.js$/, + "build-target": "dev|rel", + + "path": "adapter/", + "local-path": "./shared/adapter/" + }, + + { /* shared generated worker codec */ + "type": "js", + "search-pattern": /(WorkerPOW.js)$/, + "build-target": "dev|rel", + + "path": "js/workers/", + "local-path": "./shared/js/workers/" + }, + { /* shared developer single css files */ + "type": "css", + "search-pattern": /.*\.css$/, + "build-target": "dev", + + "path": "css/", + "local-path": "./shared/css/" + }, + { /* shared css mapping files (development mode only) */ + "type": "css", + "search-pattern": /.*\.(css.map|scss)$/, + "build-target": "dev", + + "path": "css/", + "local-path": "./shared/css/", + "req-parm": ["--mappings"] + }, + { /* shared release css files */ + "type": "css", + "search-pattern": /.*\.css$/, + "build-target": "rel", + + "path": "css/", + "local-path": "./shared/generated/" + }, + { /* shared release css files */ + "type": "css", + "search-pattern": /.*\.css$/, + "build-target": "rel", + + "path": "css/loader/", + "local-path": "./shared/css/loader/" + }, + { /* shared release css files */ + "type": "css", + "search-pattern": /.*\.css$/, + "build-target": "dev|rel", + + "path": "css/theme/", + "local-path": "./shared/css/theme/" + }, + { /* shared sound files */ + "type": "wav", + "search-pattern": /.*\.wav$/, + "build-target": "dev|rel", + + "path": "audio/", + "local-path": "./shared/audio/" + }, + { /* shared data sound files */ + "type": "json", + "search-pattern": /.*\.json/, + "build-target": "dev|rel", + + "path": "audio/", + "local-path": "./shared/audio/" + }, + { /* shared image files */ + "type": "img", + "search-pattern": /.*\.(svg|png)/, + "build-target": "dev|rel", + + "path": "img/", + "local-path": "./shared/img/" + }, + { /* own webassembly files */ + "type": "wasm", + "search-pattern": /.*\.(wasm)/, + "build-target": "dev|rel", + + "path": "wat/", + "local-path": "./shared/wat/" + }, + + + /* web specific */ + { /* generated assembly files */ + "web-only": true, + "type": "wasm", + "search-pattern": /.*\.(wasm)/, + "build-target": "dev|rel", + + "path": "wasm/", + "local-path": "./asm/generated/" + }, + { /* generated assembly javascript files */ + "web-only": true, + "type": "js", + "search-pattern": /.*\.(js)/, + "build-target": "dev|rel", + + "path": "wasm/", + "local-path": "./asm/generated/" + }, + { /* web generated worker codec */ + "web-only": true, + "type": "js", + "search-pattern": /(WorkerCodec.js)$/, + "build-target": "dev|rel", + + "path": "js/workers/", + "local-path": "./web/js/workers/" + }, + { /* web css files */ + "web-only": true, + "type": "css", + "search-pattern": /.*\.css$/, + "build-target": "dev|rel", + + "path": "css/", + "local-path": "./web/css/" + }, + { /* web html files */ + "web-only": true, + "type": "html", + "search-pattern": /.*\.(php|html)/, + "build-target": "dev|rel", + + "path": "./", + "local-path": "./web/html/" + }, + { /* translations */ + "web-only": true, /* Only required for the web client */ + "type": "i18n", + "search-pattern": /.*\.(translation|json)/, + "build-target": "dev|rel", + + "path": "i18n/", + "local-path": "./shared/i18n/" + } +] as any; //@ts-ignore declare module "fs-extra" { diff --git a/shared/loader/app.ts b/loader/app/app.ts similarity index 58% rename from shared/loader/app.ts rename to loader/app/app.ts index 4542f478..b1c5d540 100644 --- a/shared/loader/app.ts +++ b/loader/app/app.ts @@ -1,41 +1,39 @@ -/// +import * as loader from "./loader"; -interface Window { - $: JQuery; +declare global { + interface Window { + native_client: boolean; + } } -namespace app { - export enum Type { - UNKNOWN, - CLIENT_RELEASE, - CLIENT_DEBUG, - WEB_DEBUG, - WEB_RELEASE - } - export let type: Type = Type.UNKNOWN; - - export function is_web() { - return type == Type.WEB_RELEASE || type == Type.WEB_DEBUG; - } - - let _ui_version; - export function ui_version() { - if(typeof(_ui_version) !== "string") { - const version_node = document.getElementById("app_version"); - if(!version_node) return undefined; - - const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined; - if(!version) return undefined; - - return (_ui_version = version); - } - return _ui_version; +const node_require: typeof require = window.require; + +let _ui_version; +export function ui_version() { + if(typeof(_ui_version) !== "string") { + const version_node = document.getElementById("app_version"); + if(!version_node) return undefined; + + const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined; + if(!version) return undefined; + + return (_ui_version = version); } + return _ui_version; } /* all javascript loaders */ const loader_javascript = { detect_type: async () => { + //TODO: Detect real version! + loader.set_version({ + backend: "-", + ui: ui_version(), + debug_mode: true, + type: "web" + }); + window.native_client = false; + return; if(window.require) { const request = new Request("js/proto.js"); let file_path = request.url; @@ -43,11 +41,11 @@ const loader_javascript = { throw "Invalid file path (" + file_path + ")"; file_path = file_path.substring(process.platform === "win32" ? 8 : 7); - const fs = require('fs'); + const fs = node_require('fs'); if(fs.existsSync(file_path)) { - app.type = app.Type.CLIENT_DEBUG; + //type = Type.CLIENT_DEBUG; } else { - app.type = app.Type.CLIENT_RELEASE; + //type = Type.CLIENT_RELEASE; } } else { /* test if js/proto.js is available. If so we're in debug mode */ @@ -58,9 +56,9 @@ const loader_javascript = { request.onreadystatechange = () => { if (request.readyState === 4){ if (request.status === 404) { - app.type = app.Type.WEB_RELEASE; + //type = Type.WEB_RELEASE; } else { - app.type = app.Type.WEB_DEBUG; + //type = Type.WEB_DEBUG; } resolve(); } @@ -101,7 +99,7 @@ const loader_javascript = { ["vendor/emoji-picker/src/jquery.lsxemojipicker.js"] ]); - if(app.type == app.Type.WEB_RELEASE || app.type == app.Type.CLIENT_RELEASE) { + if(!loader.version().debug_mode) { loader.register_task(loader.Stage.JAVASCRIPT, { name: "scripts release", priority: 20, @@ -116,176 +114,8 @@ const loader_javascript = { } }, load_scripts_debug: async () => { - /* test if we're loading as TeaClient or WebClient */ - if(!window.require) { - loader.register_task(loader.Stage.JAVASCRIPT, { - name: "javascript web", - priority: 10, - function: loader_javascript.load_scripts_debug_web - }); - } else { - loader.register_task(loader.Stage.JAVASCRIPT, { - name: "javascript client", - priority: 10, - function: loader_javascript.load_scripts_debug_client - }); - } - - /* load some extends classes */ - await loader.load_scripts([ - ["js/connection/ConnectionBase.js"] - ]); - - /* load the main app */ - await loader.load_scripts([ - //Load general API's - "js/proto.js", - "js/i18n/localize.js", - "js/i18n/country.js", - "js/log.js", - "js/events.js", - - "js/sound/Sounds.js", - - "js/utils/helpers.js", - - "js/crypto/sha.js", - "js/crypto/hex.js", - "js/crypto/asn1.js", - "js/crypto/crc32.js", - - //load the profiles - "js/profiles/ConnectionProfile.js", - "js/profiles/Identity.js", - "js/profiles/identities/teaspeak-forum.js", - - //Basic UI elements - "js/ui/elements/context_divider.js", - "js/ui/elements/context_menu.js", - "js/ui/elements/modal.js", - "js/ui/elements/tab.js", - "js/ui/elements/slider.js", - "js/ui/elements/tooltip.js", - "js/ui/elements/net_graph.js", - - //Load permissions - "js/permission/PermissionManager.js", - "js/permission/GroupManager.js", - - //Load UI - "js/ui/modal/ModalAbout.js", - "js/ui/modal/ModalAvatar.js", - "js/ui/modal/ModalAvatarList.js", - "js/ui/modal/ModalClientInfo.js", - "js/ui/modal/ModalChannelInfo.js", - "js/ui/modal/ModalServerInfo.js", - "js/ui/modal/ModalServerInfoBandwidth.js", - "js/ui/modal/ModalQuery.js", - "js/ui/modal/ModalQueryManage.js", - "js/ui/modal/ModalPlaylistList.js", - "js/ui/modal/ModalPlaylistEdit.js", - "js/ui/modal/ModalBookmarks.js", - "js/ui/modal/ModalConnect.js", - "js/ui/modal/ModalSettings.js", - "js/ui/modal/ModalNewcomer.js", - "js/ui/modal/ModalCreateChannel.js", - "js/ui/modal/ModalServerEdit.js", - "js/ui/modal/ModalChangeVolume.js", - "js/ui/modal/ModalChangeLatency.js", - "js/ui/modal/ModalBanClient.js", - "js/ui/modal/ModalIconSelect.js", - "js/ui/modal/ModalInvite.js", - "js/ui/modal/ModalIdentity.js", - "js/ui/modal/ModalBanList.js", - "js/ui/modal/ModalMusicManage.js", - "js/ui/modal/ModalYesNo.js", - "js/ui/modal/ModalPoke.js", - "js/ui/modal/ModalKeySelect.js", - "js/ui/modal/ModalGroupAssignment.js", - "js/ui/modal/permission/ModalPermissionEdit.js", - {url: "js/ui/modal/permission/SenselessPermissions.js", depends: ["js/permission/PermissionManager.js"]}, - {url: "js/ui/modal/permission/CanvasPermissionEditor.js", depends: ["js/ui/modal/permission/ModalPermissionEdit.js"]}, - {url: "js/ui/modal/permission/HTMLPermissionEditor.js", depends: ["js/ui/modal/permission/ModalPermissionEdit.js"]}, - - "js/channel-tree/channel.js", - "js/channel-tree/client.js", - "js/channel-tree/server.js", - "js/channel-tree/view.js", - "js/ui/client_move.js", - "js/ui/htmltags.js", - - - "js/ui/frames/side/chat_helper.js", - "js/ui/frames/side/chat_box.js", - "js/ui/frames/side/client_info.js", - "js/ui/frames/side/music_info.js", - "js/ui/frames/side/conversations.js", - "js/ui/frames/side/private_conversations.js", - - "js/ui/frames/ControlBar.js", - "js/ui/frames/chat.js", - "js/ui/frames/chat_frame.js", - "js/ui/frames/connection_handlers.js", - "js/ui/frames/server_log.js", - "js/ui/frames/hostbanner.js", - "js/ui/frames/MenuBar.js", - "js/ui/frames/image_preview.js", - - //Load audio - "js/voice/RecorderBase.js", - "js/voice/RecorderProfile.js", - - //Load general stuff - "js/settings.js", - "js/bookmarks.js", - "js/FileManager.js", - "js/ConnectionHandler.js", - "js/BrowserIPC.js", - "js/MessageFormatter.js", - - //Connection - "js/connection/CommandHandler.js", - "js/connection/CommandHelper.js", - "js/connection/HandshakeHandler.js", - "js/connection/ServerConnectionDeclaration.js", - - "js/stats.js", - "js/PPTListener.js", - - "js/profiles/identities/NameIdentity.js", //Depends on Identity - "js/profiles/identities/TeaForumIdentity.js", //Depends on Identity - "js/profiles/identities/TeamSpeakIdentity.js", //Depends on Identity - ]); - - await loader.load_script("js/main.js"); + await loader.load_scripts(["js/shared-app.js"]) }, - load_scripts_debug_web: async () => { - await loader.load_scripts([ - "js/audio/AudioPlayer.js", - "js/audio/sounds.js", - "js/audio/WebCodec.js", - "js/WebPPTListener.js", - - "js/voice/AudioResampler.js", - "js/voice/JavascriptRecorder.js", - "js/voice/VoiceHandler.js", - "js/voice/VoiceClient.js", - - //Connection - "js/connection/ServerConnection.js", - "js/dns_impl.js", - - //Load codec - "js/codec/Codec.js", - "js/codec/BasicCodec.js", - {url: "js/codec/CodecWrapperWorker.js", depends: ["js/codec/BasicCodec.js"]}, - ]); - }, - load_scripts_debug_client: async () => { - await loader.load_scripts([ - ]); - }, - load_release: async () => { console.log("Load for release!"); @@ -329,7 +159,7 @@ const loader_style = { ["vendor/highlight/styles/darcula.css", ""], /* empty string means not required */ ]); - if(app.type == app.Type.WEB_DEBUG || app.type == app.Type.CLIENT_DEBUG) { + if(loader.version().debug_mode) { await loader_style.load_style_debug(); } else { await loader_style.load_style_release(); @@ -494,23 +324,10 @@ loader.register_task(loader.Stage.TEMPLATES, { loader.register_task(loader.Stage.LOADED, { name: "loaded handler", - function: async () => { - fadeoutLoader(); - }, + function: async () => loader.hide_overlay(), priority: 5 }); -loader.register_task(loader.Stage.LOADED, { - name: "error task", - function: async () => { - if(Settings.instance.static(Settings.KEY_LOAD_DUMMY_ERROR, false)) { - loader.critical_error("The tea is cold!", "Argh, this is evil! Cold tea dosn't taste good."); - throw "The tea is cold!"; - } - }, - priority: 20 -}); - loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { name: "lsx emoji picker setup", function: async () => await (window as any).setup_lsx_emoji_picker({twemoji: typeof(window.twemoji) !== "undefined"}), @@ -576,31 +393,26 @@ loader.register_task(loader.Stage.SETUP, { priority: 100 }); -loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { - name: "log enabled initialisation", - function: async () => log.initialize(app.type === app.Type.CLIENT_DEBUG || app.type === app.Type.WEB_DEBUG ? log.LogType.TRACE : log.LogType.INFO), - priority: 150 -}); +export function run() { + window["Module"] = (window["Module"] || {}) as any; + /* TeaClient */ + if(node_require) { + const path = node_require("path"); + const remote = node_require('electron').remote; + module.paths.push(path.join(remote.app.getAppPath(), "/modules")); + module.paths.push(path.join(path.dirname(remote.getGlobal("browser-root")), "js")); -window["Module"] = (window["Module"] || {}) as any; -/* TeaClient */ -if(window.require) { - const path = require("path"); - const remote = require('electron').remote; - module.paths.push(path.join(remote.app.getAppPath(), "/modules")); - module.paths.push(path.join(path.dirname(remote.getGlobal("browser-root")), "js")); + //TODO: HERE! + const connector = node_require("renderer"); + loader.register_task(loader.Stage.INITIALIZING, { + name: "teaclient initialize", + function: connector.initialize, + priority: 40 + }); + } - const connector = require("renderer"); - console.log(connector); - - loader.register_task(loader.Stage.INITIALIZING, { - name: "teaclient initialize", - function: connector.initialize, - priority: 40 - }); -} - -if(!loader.running()) { - /* we know that we want to load the app */ - loader.execute_managed(); + if(!loader.running()) { + /* we know that we want to load the app */ + loader.execute_managed(); + } } \ No newline at end of file diff --git a/shared/loader/certaccept.ts b/loader/app/certaccept.ts similarity index 63% rename from shared/loader/certaccept.ts rename to loader/app/certaccept.ts index b95c1a26..7c12bac7 100644 --- a/shared/loader/certaccept.ts +++ b/loader/app/certaccept.ts @@ -1,4 +1,4 @@ -/// +import * as loader from "./loader"; let is_debug = false; @@ -25,34 +25,8 @@ const loader_javascript = { load_scripts: async () => { await loader.load_script(["vendor/jquery/jquery.min.js"]); - - if(!is_debug) { - loader.register_task(loader.Stage.JAVASCRIPT, { - name: "scripts release", - priority: 20, - function: loader_javascript.load_release - }); - } else { - loader.register_task(loader.Stage.JAVASCRIPT, { - name: "scripts debug", - priority: 20, - function: loader_javascript.load_scripts_debug - }); - } - }, - load_scripts_debug: async () => { await loader.load_scripts([ - ["js/proto.js"], - ["js/log.js"], - ["js/BrowserIPC.js"], - ["js/settings.js"], - ["js/main.js"] - ]); - }, - - load_release: async () => { - await loader.load_scripts([ - ["js/certaccept.min.js", "js/certaccept.js"] + ["dist/certificate-popup.js"], ]); } }; @@ -99,9 +73,7 @@ loader.register_task(loader.Stage.STYLE, { loader.register_task(loader.Stage.LOADED, { name: "loaded handler", - function: async () => { - fadeoutLoader(); - }, + function: async () => loader.hide_overlay(), priority: 0 }); @@ -123,25 +95,6 @@ loader.register_task(loader.Stage.INITIALIZING, { priority: 50 }); -loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { - name: "settings initialisation", - function: async () => Settings.initialize(), - priority: 200 -}); - -loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { - name: "bipc initialisation", - function: async () => bipc.setup(), - priority: 100 -}); - - -loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { - name: "log enabled initialisation", - function: async () => log.initialize(is_debug ? log.LogType.TRACE : log.LogType.INFO), - priority: 150 -}); - if(!loader.running()) { /* we know that we want to load the app */ loader.execute_managed(); diff --git a/loader/app/index.ts b/loader/app/index.ts new file mode 100644 index 00000000..536f7ee7 --- /dev/null +++ b/loader/app/index.ts @@ -0,0 +1,5 @@ +import * as loader from "./app"; +import * as loader_base from "./loader"; + +export = loader_base; +loader.run(); \ No newline at end of file diff --git a/loader/app/loader.ts b/loader/app/loader.ts new file mode 100644 index 00000000..2df47c4f --- /dev/null +++ b/loader/app/loader.ts @@ -0,0 +1,712 @@ +import {AppVersion} from "../exports/loader"; +import {type} from "os"; + +declare global { + interface Window { + tr(message: string) : string; + tra(message: string, ...args: any[]); + + log: any; + StaticSettings: any; + } +} + +export interface Config { + loader_groups: boolean; + verbose: boolean; + error: boolean; +} + +export let config: Config = { + loader_groups: false, + verbose: false, + error: true +}; + +export type Task = { + name: string, + priority: number, /* tasks with the same priority will be executed in sync */ + function: () => Promise +}; + +export enum Stage { + /* + loading loader required files (incl this) + */ + INITIALIZING, + /* + setting up the loading process + */ + SETUP, + /* + loading all style sheet files + */ + STYLE, + /* + loading all javascript files + */ + JAVASCRIPT, + /* + loading all template files + */ + TEMPLATES, + /* + initializing static/global stuff + */ + JAVASCRIPT_INITIALIZING, + /* + finalizing load process + */ + FINALIZING, + /* + invoking main task + */ + LOADED, + + DONE +} + +let cache_tag: string | undefined; +let current_stage: Stage = undefined; +const tasks: {[key:number]:Task[]} = {}; + +/* test if all files shall be load from cache or fetch again */ +function loader_cache_tag() { + const app_version = (() => { + const version_node = document.getElementById("app_version"); + if(!version_node) return undefined; + + const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined; + if(!version) return undefined; + + if(!version || version == "unknown" || version.replace(/0+/, "").length == 0) + return undefined; + + return version; + })(); + if(config.verbose) console.log("Found current app version: %o", app_version); + + if(!app_version) { + /* TODO add warning */ + cache_tag = "?_ts=" + Date.now(); + return; + } + const cached_version = localStorage.getItem("cached_version"); + if(!cached_version || cached_version != app_version) { + register_task(Stage.LOADED, { + priority: 0, + name: "cached version updater", + function: async () => { + localStorage.setItem("cached_version", app_version); + } + }); + } + cache_tag = "?_version=" + app_version; +} + +export function get_cache_version() { return cache_tag; } + +export function finished() { + return current_stage == Stage.DONE; +} +export function running() { return typeof(current_stage) !== "undefined"; } + +export function register_task(stage: Stage, task: Task) { + if(current_stage > stage) { + if(config.error) + console.warn("Register loading task, but it had already been finished. Executing task anyways!"); + task.function().catch(error => { + if(config.error) { + console.error("Failed to execute delayed loader task!"); + console.log(" - %s: %o", task.name, error); + } + + critical_error(error); + }); + return; + } + + const task_array = tasks[stage] || []; + task_array.push(task); + tasks[stage] = task_array.sort((a, b) => a.priority - b.priority); +} + +export async function execute() { + document.getElementById("loader-overlay").classList.add("started"); + loader_cache_tag(); + + const load_begin = Date.now(); + + let begin: number = 0; + let end: number = Date.now(); + while(current_stage <= Stage.LOADED || typeof(current_stage) === "undefined") { + + let current_tasks: Task[] = []; + while((tasks[current_stage] || []).length > 0) { + if(current_tasks.length == 0 || current_tasks[0].priority == tasks[current_stage][0].priority) { + current_tasks.push(tasks[current_stage].pop()); + } else break; + } + + const errors: { + error: any, + task: Task + }[] = []; + + const promises: Promise[] = []; + for(const task of current_tasks) { + try { + if(config.verbose) console.debug("Executing loader %s (%d)", task.name, task.priority); + promises.push(task.function().catch(error => { + errors.push({ + task: task, + error: error + }); + return Promise.resolve(); + })); + } catch(error) { + errors.push({ + task: task, + error: error + }); + } + } + + if(promises.length > 0) { + await Promise.all([...promises]); + } + + if(errors.length > 0) { + if(config.loader_groups) console.groupEnd(); + console.error("Failed to execute loader. The following tasks failed (%d):", errors.length); + for(const error of errors) + console.error(" - %s: %o", error.task.name, error.error); + + throw "failed to process step " + Stage[current_stage]; + } + + if(current_tasks.length == 0) { + if(typeof(current_stage) === "undefined") { + current_stage = -1; + if(config.verbose) console.debug("[loader] Booting app"); + } else if(current_stage < Stage.INITIALIZING) { + if(config.loader_groups) console.groupEnd(); + if(config.verbose) console.debug("[loader] Entering next state (%s). Last state took %dms", Stage[current_stage + 1], (end = Date.now()) - begin); + } else { + if(config.loader_groups) console.groupEnd(); + if(config.verbose) console.debug("[loader] Finish invoke took %dms", (end = Date.now()) - begin); + } + + begin = end; + current_stage += 1; + + if(current_stage != Stage.DONE && config.loader_groups) + console.groupCollapsed("Executing loading stage %s", Stage[current_stage]); + } + } + + /* cleanup */ + { + _script_promises = {}; + } + if(config.verbose) console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - load_begin); +} +export function execute_managed() { + execute().then(() => { + if(config.verbose) { + let message; + if(typeof(window.tr) !== "undefined") + message = tr("App loaded successfully!"); + else + message = "App loaded successfully!"; + + if(typeof(window.log) !== "undefined") { + /* We're having our log module */ + window.log.info(window.log.LogCategory.GENERAL, message); + } else { + console.log(message); + } + } + }).catch(error => { + console.error("App loading failed: %o", error); + critical_error("Failed to execute loader", "Lookup the console for more detail"); + }); +} + +export type DependSource = { + url: string; + depends: string[]; +} +export type SourcePath = string | DependSource | string[]; + +function script_name(path: SourcePath, html: boolean) { + if(Array.isArray(path)) { + let buffer = ""; + let _or = " or "; + for(let entry of path) + buffer += _or + script_name(entry, html); + return buffer.slice(_or.length); + } else if(typeof(path) === "string") + return html ? "" + path + "" : path; + else + return html ? "" + path.url + "" : path.url; +} + +class SyntaxError { + source: any; + + constructor(source: any) { + this.source = source; + } +} + +let _script_promises: {[key: string]: Promise} = {}; +export async function load_script(path: SourcePath) : Promise { + if(Array.isArray(path)) { //We have some fallback + return load_script(path[0]).catch(error => { + console.log(typeof error + " - " + (error instanceof SyntaxError)); + if(error instanceof SyntaxError) + return Promise.reject(error); + + if(path.length > 1) + return load_script(path.slice(1)); + + return Promise.reject(error); + }); + } else { + const source = typeof(path) === "string" ? {url: path, depends: []} : path; + if(source.url.length == 0) return Promise.resolve(); + + return _script_promises[source.url] = (async () => { + /* await depends */ + for(const depend of source.depends) { + if(!_script_promises[depend]) + throw "Missing dependency " + depend; + await _script_promises[depend]; + } + + const tag: HTMLScriptElement = document.createElement("script"); + + await new Promise((resolve, reject) => { + let error = false; + const error_handler = (event: ErrorEvent) => { + if(event.filename == tag.src && event.message.indexOf("Illegal constructor") == -1) { //Our tag throw an uncaught error + if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error); + window.removeEventListener('error', error_handler as any); + + reject(new SyntaxError(event.error)); + event.preventDefault(); + error = true; + } + }; + window.addEventListener('error', error_handler as any); + + const cleanup = () => { + tag.onerror = undefined; + tag.onload = undefined; + + clearTimeout(timeout_handle); + window.removeEventListener('error', error_handler as any); + }; + const timeout_handle = setTimeout(() => { + cleanup(); + reject("timeout"); + }, 5000); + tag.type = "application/javascript"; + tag.async = true; + tag.defer = true; + tag.onerror = error => { + cleanup(); + tag.remove(); + reject(error); + }; + tag.onload = () => { + cleanup(); + + if(config.verbose) console.debug("Script %o loaded", path); + setTimeout(resolve, 100); + }; + + document.getElementById("scripts").appendChild(tag); + + tag.src = source.url + (cache_tag || ""); + }); + })(); + } +} + +export async function load_scripts(paths: SourcePath[]) : Promise { + const promises: Promise[] = []; + const errors: { + script: SourcePath, + error: any + }[] = []; + + for(const script of paths) + promises.push(load_script(script).catch(error => { + errors.push({ + script: script, + error: error + }); + return Promise.resolve(); + })); + + await Promise.all([...promises]); + + if(errors.length > 0) { + if(config.error) { + console.error("Failed to load the following scripts:"); + for(const script of errors) { + const sname = script_name(script.script, false); + if(script.error instanceof SyntaxError) { + const source = script.error.source as Error; + if(source.name === "TypeError") { + let prefix = ""; + while(prefix.length < sname.length + 7) prefix += " "; + console.log(" - %s: %s:\n%s", sname, source.message, source.stack.split("\n").map(e => prefix + e.trim()).slice(1).join("\n")); + } else { + console.log(" - %s: %o", sname, source); + } + } else { + console.log(" - %s: %o", sname, script.error); + } + } + } + + critical_error("Failed to load script " + script_name(errors[0].script, true) + "
" + "View the browser console for more information!"); + throw "failed to load script " + script_name(errors[0].script, false); + } +} + +export async function load_style(path: SourcePath) : Promise { + if(Array.isArray(path)) { //We have some fallback + return load_style(path[0]).catch(error => { + if(error instanceof SyntaxError) + return Promise.reject(error.source); + + if(path.length > 1) + return load_script(path.slice(1)); + + return Promise.reject(error); + }); + } else { + if(!path) { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + const tag: HTMLLinkElement = document.createElement("link"); + + let error = false; + const error_handler = (event: ErrorEvent) => { + if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error); + if(event.filename == tag.href) { //FIXME! + window.removeEventListener('error', error_handler as any); + + reject(new SyntaxError(event.error)); + event.preventDefault(); + error = true; + } + }; + window.addEventListener('error', error_handler as any); + + tag.type = "text/css"; + tag.rel = "stylesheet"; + + const cleanup = () => { + tag.onerror = undefined; + tag.onload = undefined; + + clearTimeout(timeout_handle); + window.removeEventListener('error', error_handler as any); + }; + + const timeout_handle = setTimeout(() => { + cleanup(); + reject("timeout"); + }, 5000); + + tag.onerror = error => { + cleanup(); + tag.remove(); + if(config.error) + console.error("File load error for file %s: %o", path, error); + reject("failed to load file " + path); + }; + tag.onload = () => { + cleanup(); + { + const css: CSSStyleSheet = tag.sheet as CSSStyleSheet; + const rules = css.cssRules; + const rules_remove: number[] = []; + const rules_add: string[] = []; + + for(let index = 0; index < rules.length; index++) { + const rule = rules.item(index); + let rule_text = rule.cssText; + + if(rule.cssText.indexOf("%%base_path%%") != -1) { + rules_remove.push(index); + rules_add.push(rule_text.replace("%%base_path%%", document.location.origin + document.location.pathname)); + } + } + + for(const index of rules_remove.sort((a, b) => b > a ? 1 : 0)) { + if(css.removeRule) + css.removeRule(index); + else + css.deleteRule(index); + } + for(const rule of rules_add) + css.insertRule(rule, rules_remove[0]); + } + + if(config.verbose) console.debug("Style sheet %o loaded", path); + setTimeout(resolve, 100); + }; + + document.getElementById("style").appendChild(tag); + tag.href = path + (cache_tag || ""); + }); + } +} + +export async function load_styles(paths: SourcePath[]) : Promise { + const promises: Promise[] = []; + const errors: { + sheet: SourcePath, + error: any + }[] = []; + + for(const sheet of paths) + promises.push(load_style(sheet).catch(error => { + errors.push({ + sheet: sheet, + error: error + }); + return Promise.resolve(); + })); + + await Promise.all([...promises]); + + if(errors.length > 0) { + if(config.error) { + console.error("Failed to load the following style sheet:"); + for(const sheet of errors) + console.log(" - %o: %o", sheet.sheet, sheet.error); + } + + critical_error("Failed to load style sheet " + script_name(errors[0].sheet, true) + "
" + "View the browser console for more information!"); + throw "failed to load style sheet " + script_name(errors[0].sheet, false); + } +} + +export async function load_template(path: SourcePath) : Promise { + try { + const response = await $.ajax(path + (cache_tag || "")); + + let node = document.createElement("html"); + node.innerHTML = response; + let tags: HTMLCollection; + if(node.getElementsByTagName("body").length > 0) + tags = node.getElementsByTagName("body")[0].children; + else + tags = node.children; + + let root = document.getElementById("templates"); + if(!root) { + critical_error("Failed to find template tag!"); + throw "Failed to find template tag"; + } + while(tags.length > 0){ + let tag = tags.item(0); + root.appendChild(tag); + + } + } catch(error) { + let msg; + if('responseText' in error) + msg = error.responseText; + else if(error instanceof Error) + msg = error.message; + + critical_error("failed to load template " + script_name(path, true), msg); + throw "template error"; + } +} + +export async function load_templates(paths: SourcePath[]) : Promise { + const promises: Promise[] = []; + const errors: { + template: SourcePath, + error: any + }[] = []; + + for(const template of paths) + promises.push(load_template(template).catch(error => { + errors.push({ + template: template, + error: error + }); + return Promise.resolve(); + })); + + await Promise.all([...promises]); + + if(errors.length > 0) { + if (config.error) { + console.error("Failed to load the following templates:"); + for (const sheet of errors) + console.log(" - %s: %o", script_name(sheet.template, false), sheet.error); + } + + critical_error("Failed to load template " + script_name(errors[0].template, true) + "
" + "View the browser console for more information!"); + throw "failed to load template " + script_name(errors[0].template, false); + } +} + +let version_: AppVersion; +export function version() : AppVersion { return version_; } +export function set_version(version: AppVersion) { version_ = version; } + +export type ErrorHandler = (message: string, detail: string) => void; + +let _callback_critical_error: ErrorHandler; +let _callback_critical_called: boolean = false; + +export function critical_error(message: string, detail?: string) { + if(_callback_critical_called) { + console.warn("[CRITICAL] %s", message); + if(typeof(detail) === "string") + console.warn("[CRITICAL] %s", detail); + return; + } + + _callback_critical_called = true; + if(_callback_critical_error) { + _callback_critical_error(message, detail); + return; + } + + /* default handling */ + let tag = document.getElementById("critical-load"); + + { + const error_tags = tag.getElementsByClassName("error"); + error_tags[0].innerHTML = message; + } + + if(typeof(detail) === "string") { + let node_detail = tag.getElementsByClassName("detail")[0]; + node_detail.innerHTML = detail; + } + + tag.classList.add("shown"); +} + +export function critical_error_handler(handler?: ErrorHandler, override?: boolean) : ErrorHandler { + if((typeof(handler) === "object" && handler !== _callback_critical_error) || override) + _callback_critical_error = handler; + return _callback_critical_error; +} + +let _fadeout_warned; +export function hide_overlay() { + if(typeof($) === "undefined") { + if(!_fadeout_warned) + console.warn("Could not fadeout loader screen. Missing jquery functions."); + _fadeout_warned = true; + return; + } + const animation_duration = 750; + + $(".loader .bookshelf_wrapper").animate({top: 0, opacity: 0}, animation_duration); + $(".loader .half").animate({width: 0}, animation_duration, () => { + $(".loader").detach(); + }); +} + +{ + + const hello_world = () => { + const clog = console.log; + const print_security = () => { + { + const css = [ + "display: block", + "text-align: center", + "font-size: 42px", + "font-weight: bold", + "-webkit-text-stroke: 2px black", + "color: red" + ].join(";"); + clog("%c ", "font-size: 100px;"); + clog("%cSecurity warning:", css); + } + { + const css = [ + "display: block", + "text-align: center", + "font-size: 18px", + "font-weight: bold" + ].join(";"); + + clog("%cPasting anything in here could give attackers access to your data.", css); + clog("%cUnless you understand exactly what you are doing, close this window and stay safe.", css); + clog("%c ", "font-size: 100px;"); + } + }; + + /* print the hello world */ + { + const css = [ + "display: block", + "text-align: center", + "font-size: 72px", + "font-weight: bold", + "-webkit-text-stroke: 2px black", + "color: #18BC9C" + ].join(";"); + clog("%cHey, hold on!", css); + } + { + const css = [ + "display: block", + "text-align: center", + "font-size: 26px", + "font-weight: bold" + ].join(";"); + + const css_2 = [ + "display: block", + "text-align: center", + "font-size: 26px", + "font-weight: bold", + "color: blue" + ].join(";"); + + const display_detect = /./; + display_detect.toString = function() { print_security(); return ""; }; + + clog("%cLovely to see you using and debugging the TeaSpeak-Web client.", css); + clog("%cIf you have some good ideas or already done some incredible changes,", css); + clog("%cyou'll be may interested to share them here: %chttps://github.com/TeaSpeak/TeaWeb", css, css_2); + clog("%c ", display_detect); + } + }; + + try { /* lets try to print it as VM code :)*/ + let hello_world_code = hello_world.toString(); + hello_world_code = hello_world_code.substr(hello_world_code.indexOf('() => {') + 8); + hello_world_code = hello_world_code.substring(0, hello_world_code.lastIndexOf("}")); + + //Look aheads are not possible with firefox + //hello_world_code = hello_world_code.replace(/(? Promise +}; +export enum Stage { + /* + loading loader required files (incl this) + */ + INITIALIZING, + /* + setting up the loading process + */ + SETUP, + /* + loading all style sheet files + */ + STYLE, + /* + loading all javascript files + */ + JAVASCRIPT, + /* + loading all template files + */ + TEMPLATES, + /* + initializing static/global stuff + */ + JAVASCRIPT_INITIALIZING, + /* + finalizing load process + */ + FINALIZING, + /* + invoking main task + */ + LOADED, + + DONE +} + +export function version() : AppVersion; + +export function finished(); +export function running(); +export function register_task(stage: Stage, task: Task); +export function execute() : Promise; +export function execute_managed(); +export type DependSource = { + url: string; + depends: string[]; +} +export type SourcePath = string | DependSource | string[]; +export function load_script(path: SourcePath) : Promise; +export function load_scripts(paths: SourcePath[]) : Promise; +export function load_style(path: SourcePath) : Promise; +export function load_styles(paths: SourcePath[]) : Promise; +export function load_template(path: SourcePath) : Promise; +export function load_templates(paths: SourcePath[]) : Promise; +export type ErrorHandler = (message: string, detail: string) => void; +export function critical_error(message: string, detail?: string); +export function critical_error_handler(handler?: ErrorHandler, override?: boolean); +export function hide_overlay(); \ No newline at end of file diff --git a/loader/webpack.config.js b/loader/webpack.config.js new file mode 100644 index 00000000..075fb34c --- /dev/null +++ b/loader/webpack.config.js @@ -0,0 +1,62 @@ +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +const isDevelopment = process.env.NODE_ENV === 'development'; +module.exports = { + entry: path.join(__dirname, "app/index.ts"), + devtool: 'inline-source-map', + mode: "development", + plugins: [ + new MiniCssExtractPlugin({ + filename: isDevelopment ? '[name].css' : '[name].[hash].css', + chunkFilename: isDevelopment ? '[id].css' : '[id].[hash].css' + }) + ], + module: { + rules: [ + { + test: /\.s[ac]ss$/, + loader: [ + //isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader, + 'style-loader', + { + loader: 'css-loader', + options: { + modules: true, + sourceMap: isDevelopment + } + }, + { + loader: 'sass-loader', + options: { + sourceMap: isDevelopment + } + } + ] + }, + { + test: /\.tsx?$/, + exclude: /node_modules/, + + loader: [ + { + loader: 'ts-loader', + options: { + transpileOnly: true + } + } + ] + }, + ], + }, + resolve: { + extensions: ['.tsx', '.ts', '.js', ".scss"], + }, + output: { + filename: 'loader.js', + path: path.resolve(__dirname, '../dist'), + library: "loader", + //libraryTarget: "umd" //"var" | "assign" | "this" | "window" | "self" | "global" | "commonjs" | "commonjs2" | "commonjs-module" | "amd" | "amd-require" | "umd" | "umd2" | "jsonp" | "system" + }, + optimization: { } +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e1cc94f0..df6a8f9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1087,6 +1087,11 @@ "safe-buffer": "^5.0.1" } }, + "circular-dependency-plugin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.0.tgz", + "integrity": "sha512-7p4Kn/gffhQaavNfyDFg7LS5S/UT1JAjyGd4UqR2+jzoYF02eDkj0Ec3+48TsIa4zghjLY87nQHIh/ecK9qLdw==" + }, "clap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", diff --git a/package.json b/package.json index 6e2f47f6..b7c8f5ca 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "minify-web-rel-file": "terser --compress --mangle --ecma 6 --keep_classnames --keep_fnames --output", "start": "npm run compile-file-helper && node file.js ndevelop", "build": "webpack --config webpack.config.js", - "watch": "webpack --watch" + "build-loader": "webpack --config loader/webpack.config.js", + "watch": "webpack --watch", + "watch-loader": "webpack --watch --config loader/webpack.config.js" }, "author": "TeaSpeak (WolverinDEV)", "license": "ISC", @@ -63,6 +65,7 @@ "homepage": "https://www.teaspeak.de", "dependencies": { "@types/fs-extra": "^8.0.1", + "circular-dependency-plugin": "^5.2.0", "react": "^16.13.1", "react-dom": "^16.13.1" } diff --git a/shared/backend.d/audio/player.d.ts b/shared/backend.d/audio/player.d.ts new file mode 100644 index 00000000..ef63390d --- /dev/null +++ b/shared/backend.d/audio/player.d.ts @@ -0,0 +1,19 @@ +import {Device} from "tc-shared/audio/player"; + +export function initialize() : boolean; +export function initialized() : boolean; + +export function context() : AudioContext; +export function get_master_volume() : number; +export function set_master_volume(volume: number); + +export function destination() : AudioNode; + +export function on_ready(cb: () => any); + +export function available_devices() : Promise; +export function set_device(device_id: string) : Promise; + +export function current_device() : Device; + +export function initializeFromGesture(); \ No newline at end of file diff --git a/shared/backend.d/audio/recorder.d.ts b/shared/backend.d/audio/recorder.d.ts new file mode 100644 index 00000000..a5fdd45d --- /dev/null +++ b/shared/backend.d/audio/recorder.d.ts @@ -0,0 +1,9 @@ +import {AbstractInput, InputDevice, LevelMeter} from "tc-shared/voice/RecorderBase"; + +export function devices() : InputDevice[]; + +export function device_refresh_available() : boolean; +export function refresh_devices() : Promise; + +export function create_input() : AbstractInput; +export function create_levelmeter(device: InputDevice) : Promise; \ No newline at end of file diff --git a/shared/backend.d/audio/sounds.d.ts b/shared/backend.d/audio/sounds.d.ts new file mode 100644 index 00000000..1140ee9e --- /dev/null +++ b/shared/backend.d/audio/sounds.d.ts @@ -0,0 +1,3 @@ +import {SoundFile} from "tc-shared/sound/Sounds"; + +export function play_sound(file: SoundFile) : Promise; \ No newline at end of file diff --git a/shared/backend.d/connection.d.ts b/shared/backend.d/connection.d.ts new file mode 100644 index 00000000..b362a4eb --- /dev/null +++ b/shared/backend.d/connection.d.ts @@ -0,0 +1,5 @@ +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase"; + +export function spawn_server_connection(handle: ConnectionHandler) : AbstractServerConnection; +export function destroy_server_connection(handle: AbstractServerConnection); \ No newline at end of file diff --git a/shared/backend.d/dns.d.ts b/shared/backend.d/dns.d.ts new file mode 100644 index 00000000..f7be12d6 --- /dev/null +++ b/shared/backend.d/dns.d.ts @@ -0,0 +1,5 @@ +import {AddressTarget, ResolveOptions} from "tc-shared/dns"; +import {ServerAddress} from "tc-shared/ui/server"; + +export function supported(); +export function resolve_address(address: ServerAddress, options?: ResolveOptions) : Promise; \ No newline at end of file diff --git a/shared/backend.d/ppt.d.ts b/shared/backend.d/ppt.d.ts new file mode 100644 index 00000000..06e7a2ef --- /dev/null +++ b/shared/backend.d/ppt.d.ts @@ -0,0 +1,12 @@ +import {KeyEvent, KeyHook, SpecialKey} from "tc-shared/PPTListener"; + +export function initialize() : Promise; +export function finalize(); // most the times not really required + +export function register_key_listener(listener: (_: KeyEvent) => any); +export function unregister_key_listener(listener: (_: KeyEvent) => any); + +export function register_key_hook(hook: KeyHook); +export function unregister_key_hook(hook: KeyHook); + +export function key_pressed(code: string | SpecialKey) : boolean; \ No newline at end of file diff --git a/shared/backend/readme.md b/shared/backend.d/readme.md similarity index 100% rename from shared/backend/readme.md rename to shared/backend.d/readme.md diff --git a/shared/backend/audio.d.ts b/shared/backend/audio.d.ts deleted file mode 100644 index 53a5bdc3..00000000 --- a/shared/backend/audio.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -declare namespace audio { - export namespace player { - export function initialize() : boolean; - export function initialized() : boolean; - - export function context() : AudioContext; - export function get_master_volume() : number; - export function set_master_volume(volume: number); - - export function destination() : AudioNode; - - export function on_ready(cb: () => any); - - export function available_devices() : Promise; - export function set_device(device_id: string) : Promise; - - export function current_device() : Device; - - export function initializeFromGesture(); - } - - export namespace recorder { - export function devices() : InputDevice[]; - - export function device_refresh_available() : boolean; - export function refresh_devices() : Promise; - - export function create_input() : AbstractInput; - export function create_levelmeter(device: InputDevice) : Promise; - } - - export namespace sounds { - export function play_sound(file: sound.SoundFile) : Promise; - } -} \ No newline at end of file diff --git a/shared/backend/connection.d.ts b/shared/backend/connection.d.ts deleted file mode 100644 index e816b7df..00000000 --- a/shared/backend/connection.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare namespace connection { - export function spawn_server_connection(handle: ConnectionHandler) : AbstractServerConnection; - export function destroy_server_connection(handle: AbstractServerConnection); -} \ No newline at end of file diff --git a/shared/backend/dns.d.ts b/shared/backend/dns.d.ts deleted file mode 100644 index 483395c8..00000000 --- a/shared/backend/dns.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare namespace dns { - export function supported(); - export function resolve_address(address: ServerAddress, options?: ResolveOptions) : Promise; -} \ No newline at end of file diff --git a/shared/backend/ppt.d.ts b/shared/backend/ppt.d.ts deleted file mode 100644 index ad9a81e6..00000000 --- a/shared/backend/ppt.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -declare namespace ppt { - export function initialize() : Promise; - export function finalize(); // most the times not really required - - export function register_key_listener(listener: (_: KeyEvent) => any); - export function unregister_key_listener(listener: (_: KeyEvent) => any); - - export function register_key_hook(hook: KeyHook); - export function unregister_key_hook(hook: KeyHook); - - export function key_pressed(code: string | SpecialKey) : boolean; -} \ No newline at end of file diff --git a/shared/css/static/channel-tree.scss b/shared/css/static/channel-tree.scss index 64433d19..14b62dcb 100644 --- a/shared/css/static/channel-tree.scss +++ b/shared/css/static/channel-tree.scss @@ -94,7 +94,96 @@ } &.channel { + display: flex; + flex-direction: column; + .container-channel { + position: relative; + + display: flex; + flex-direction: row; + justify-content: stretch; + + width: 100%; + min-height: 16px; + + align-items: center; + cursor: pointer; + + .channel-type { + flex-grow: 0; + flex-shrink: 0; + + margin-right: 2px; + } + + .container-channel-name { + display: flex; + flex-direction: row; + + flex-grow: 1; + flex-shrink: 1; + + justify-content: left; + + max-width: 100%; /* important for the repetitive channel name! */ + overflow-x: hidden; + height: 16px; + + &.align-right { + justify-content: right; + } + + &.align-center, &.align-repetitive { + justify-content: center; + } + + .channel-name { + align-self: center; + color: $channel_tree_entry_text_color; + + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &.align-repetitive { + .channel-name { + text-overflow: clip; + } + } + } + + .icons { + display: flex; + flex-direction: row; + + flex-grow: 0; + flex-shrink: 0; + } + + &.move-selected { + border-bottom: 1px solid black; + } + + .show-channel-normal-only { + display: none; + + &.channel-normal { + display: block; + } + } + + .icon_no_sound { + display: flex; + } + } + + .container-clients { + display: flex; + flex-direction: column; + } } &.client { @@ -183,6 +272,40 @@ } } + &.channel .container-channel, &.client, &.server { + .marker-text-unread { + position: absolute; + left: 0; + top: 0; + bottom: 0; + + width: 1px; + background-color: #a814147F; + + opacity: 1; + + &:before { + content: ''; + position: absolute; + + left: 0; + top: 0; + bottom: 0; + + width: 24px; + + background: -moz-linear-gradient(left, rgba(168,20,20,.18) 0%, rgba(168,20,20,0) 100%); /* FF3.6-15 */ + background: -webkit-linear-gradient(left, rgba(168,20,20,.18) 0%,rgba(168,20,20,0) 100%); /* Chrome10-25,Safari5.1-6 */ + background: linear-gradient(to right, rgba(168,20,20,.18) 0%,rgba(168,20,20,0) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ + } + + &.hidden { + opacity: 0; + } + + @include transition(opacity $button_hover_animation_time); + } + } } } diff --git a/shared/html/index.php b/shared/html/index.php index a601be7e..f0587239 100644 --- a/shared/html/index.php +++ b/shared/html/index.php @@ -145,9 +145,11 @@
+ +
diff --git a/shared/html/templates.html b/shared/html/templates.html index 017e1d11..b19bb293 100644 --- a/shared/html/templates.html +++ b/shared/html/templates.html @@ -2736,13 +2736,13 @@ {{else type == "default" }}
-
+
{{tr "English (Default / Fallback)" /}}
{{else}}
+ title="{{>country_name}}">
{{> name}}
diff --git a/shared/js/BrowserIPC.ts b/shared/js/BrowserIPC.ts index b7271f6e..45daa140 100644 --- a/shared/js/BrowserIPC.ts +++ b/shared/js/BrowserIPC.ts @@ -1,727 +1,724 @@ -interface Window { - BroadcastChannel: BroadcastChannel; +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; + +export interface BroadcastMessage { + timestamp: number; + receiver: string; + sender: string; + + type: string; + data: any; } -namespace bipc { - export interface BroadcastMessage { - timestamp: number; - receiver: string; - sender: string; +function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} - type: string; - data: any; +interface ProcessQuery { + timestamp: number + query_id: string; +} + +export interface ChannelMessage { + channel_id: string; + type: string; + data: any; +} + +export interface ProcessQueryResponse { + request_timestamp: number + request_query_id: string; + + device_id: string; + protocol: number; +} + +export interface CertificateAcceptCallback { + request_id: string; +} +export interface CertificateAcceptSucceeded { } + +export abstract class BasicIPCHandler { + protected static readonly BROADCAST_UNIQUE_ID = "00000000-0000-4000-0000-000000000000"; + protected static readonly PROTOCOL_VERSION = 1; + + protected _channels: Channel[] = []; + protected unique_id; + + protected constructor() { } + + setup() { + this.unique_id = uuidv4(); /* lets get an unique identifier */ } - function uuidv4() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); + get_local_address() { return this.unique_id; } + + abstract send_message(type: string, data: any, target?: string); + + protected handle_message(message: BroadcastMessage) { + //log.trace(LogCategory.IPC, tr("Received message %o"), message); + + if(message.receiver === BasicIPCHandler.BROADCAST_UNIQUE_ID) { + if(message.type == "process-query") { + log.debug(LogCategory.IPC, tr("Received a device query from %s."), message.sender); + this.send_message("process-query-response", { + request_query_id: (message.data).query_id, + request_timestamp: (message.data).timestamp, + + device_id: this.unique_id, + protocol: BasicIPCHandler.PROTOCOL_VERSION + } as ProcessQueryResponse, message.sender); + return; + } + } else if(message.receiver === this.unique_id) { + if(message.type == "process-query-response") { + const response: ProcessQueryResponse = message.data; + if(this._query_results[response.request_query_id]) + this._query_results[response.request_query_id].push(response); + else { + log.warn(LogCategory.IPC, tr("Received a query response for an unknown request.")); + } + return; + } + else if(message.type == "certificate-accept-callback") { + const data: CertificateAcceptCallback = message.data; + if(!this._cert_accept_callbacks[data.request_id]) { + log.warn(LogCategory.IPC, tr("Received certificate accept callback for an unknown request ID.")); + return; + } + this._cert_accept_callbacks[data.request_id](); + delete this._cert_accept_callbacks[data.request_id]; + + this.send_message("certificate-accept-succeeded", { + + } as CertificateAcceptSucceeded, message.sender); + return; + } + else if(message.type == "certificate-accept-succeeded") { + if(!this._cert_accept_succeeded[message.sender]) { + log.warn(LogCategory.IPC, tr("Received certificate accept succeeded, but haven't a callback.")); + return; + } + this._cert_accept_succeeded[message.sender](); + return; + } + } + if(message.type === "channel") { + const data: ChannelMessage = message.data; + + let channel_invoked = false; + for(const channel of this._channels) + if(channel.channel_id === data.channel_id && (typeof(channel.target_id) === "undefined" || channel.target_id === message.sender)) { + if(channel.message_handler) + channel.message_handler(message.sender, message.receiver === BasicIPCHandler.BROADCAST_UNIQUE_ID, data); + channel_invoked = true; + } + if(!channel_invoked) { + console.warn(tr("Received channel message for unknown channel (%s)"), data.channel_id); + } + } } - interface ProcessQuery { - timestamp: number - query_id: string; + create_channel(target_id?: string, channel_id?: string) { + let channel: Channel = { + target_id: target_id, + channel_id: channel_id || uuidv4(), + message_handler: undefined, + send_message: (type: string, data: any, target?: string) => { + if(typeof target !== "undefined") { + if(typeof channel.target_id === "string" && target != channel.target_id) + throw "target id does not match channel target"; + } + + this.send_message("channel", { + type: type, + data: data, + channel_id: channel.channel_id + } as ChannelMessage, target || channel.target_id || BasicIPCHandler.BROADCAST_UNIQUE_ID); + } + }; + + this._channels.push(channel); + return channel; } - export interface ChannelMessage { - channel_id: string; - type: string; - data: any; + channels() : Channel[] { return this._channels; } + + delete_channel(channel: Channel) { + this._channels = this._channels.filter(e => e !== channel); } - export interface ProcessQueryResponse { - request_timestamp: number - request_query_id: string; + private _query_results: {[key: string]:ProcessQueryResponse[]} = {}; + async query_processes(timeout?: number) : Promise { + const query_id = uuidv4(); + this._query_results[query_id] = []; - device_id: string; - protocol: number; + this.send_message("process-query", { + query_id: query_id, + timestamp: Date.now() + } as ProcessQuery); + + await new Promise(resolve => setTimeout(resolve, timeout || 250)); + const result = this._query_results[query_id]; + delete this._query_results[query_id]; + return result; } - export interface CertificateAcceptCallback { + private _cert_accept_callbacks: {[key: string]:(() => any)} = {}; + register_certificate_accept_callback(callback: () => any) : string { + const id = uuidv4(); + this._cert_accept_callbacks[id] = callback; + return this.unique_id + ":" + id; + } + + private _cert_accept_succeeded: {[sender: string]:(() => any)} = {}; + post_certificate_accpected(id: string, timeout?: number) : Promise { + return new Promise((resolve, reject) => { + const data = id.split(":"); + const timeout_id = setTimeout(() => { + delete this._cert_accept_succeeded[data[0]]; + clearTimeout(timeout_id); + reject("timeout"); + }, timeout || 250); + this._cert_accept_succeeded[data[0]] = () => { + delete this._cert_accept_succeeded[data[0]]; + clearTimeout(timeout_id); + resolve(); + }; + this.send_message("certificate-accept-callback", { + request_id: data[1] + } as CertificateAcceptCallback, data[0]); + }) + } +} + +export interface Channel { + readonly channel_id: string; + target_id?: string; + + message_handler: (remote_id: string, broadcast: boolean, message: ChannelMessage) => any; + send_message(type: string, message: any, target?: string); +} + +class BroadcastChannelIPC extends BasicIPCHandler { + private static readonly CHANNEL_NAME = "TeaSpeak-Web"; + + private channel: BroadcastChannel; + + constructor() { + super(); + } + + setup() { + super.setup(); + + this.channel = new BroadcastChannel(BroadcastChannelIPC.CHANNEL_NAME); + this.channel.onmessage = this.on_message.bind(this); + this.channel.onmessageerror = this.on_error.bind(this); + } + + private on_message(event: MessageEvent) { + if(typeof(event.data) !== "string") { + log.warn(LogCategory.IPC, tr("Received message with an invalid type (%s): %o"), typeof(event.data), event.data); + return; + } + + let message: BroadcastMessage; + try { + message = JSON.parse(event.data); + } catch(error) { + log.error(LogCategory.IPC, tr("Received an invalid encoded message: %o"), event.data); + return; + } + super.handle_message(message); + } + + private on_error(event: MessageEvent) { + log.warn(LogCategory.IPC, tr("Received error: %o"), event); + } + + send_message(type: string, data: any, target?: string) { + const message: BroadcastMessage = {} as any; + + message.sender = this.unique_id; + message.receiver = target ? target : BasicIPCHandler.BROADCAST_UNIQUE_ID; + message.timestamp = Date.now(); + message.type = type; + message.data = data; + + this.channel.postMessage(JSON.stringify(message)); + } +} + +export namespace connect { + export type ConnectRequestData = { + address: string; + + profile?: string; + username?: string; + password?: { + value: string; + hashed: boolean; + }; + } + + export interface ConnectOffer { + request_id: string; + data: ConnectRequestData; + } + + export interface ConnectOfferAnswer { + request_id: string; + accepted: boolean; + } + + export interface ConnectExecute { request_id: string; } - export interface CertificateAcceptSucceeded { } - export abstract class BasicIPCHandler { - protected static readonly BROADCAST_UNIQUE_ID = "00000000-0000-4000-0000-000000000000"; - protected static readonly PROTOCOL_VERSION = 1; + export interface ConnectExecuted { + request_id: string; + succeeded: boolean; + message?: string; + } - protected _channels: Channel[] = []; - protected unique_id; + /* The connect process: + * 1. Broadcast an offer + * 2. Wait 50ms for all offer responses or until the first one respond with "ok" + * 3. Select (if possible) on accepted offer and execute the connect + */ + export class ConnectHandler { + private static readonly CHANNEL_NAME = "connect"; - protected constructor() { } + readonly ipc_handler: BasicIPCHandler; + private ipc_channel: Channel; - setup() { - this.unique_id = uuidv4(); /* lets get an unique identifier */ + public callback_available: (data: ConnectRequestData) => boolean = () => false; + public callback_execute: (data: ConnectRequestData) => boolean | string = () => false; + + + private _pending_connect_offers: { + id: string; + data: ConnectRequestData; + timeout: number; + + remote_handler: string; + }[] = []; + + private _pending_connects_requests: { + id: string; + + data: ConnectRequestData; + timeout: number; + + callback_success: () => any; + callback_failed: (message: string) => any; + callback_avail: () => Promise; + + remote_handler?: string; + }[] = []; + + constructor(ipc_handler: BasicIPCHandler) { + this.ipc_handler = ipc_handler; } - get_local_address() { return this.unique_id; } + public setup() { + this.ipc_channel = this.ipc_handler.create_channel(undefined, ConnectHandler.CHANNEL_NAME); + this.ipc_channel.message_handler = this.on_message.bind(this); + } - abstract send_message(type: string, data: any, target?: string); + private on_message(sender: string, broadcast: boolean, message: ChannelMessage) { + if(broadcast) { + if(message.type == "offer") { + const data = message.data as ConnectOffer; - protected handle_message(message: BroadcastMessage) { - //log.trace(LogCategory.IPC, tr("Received message %o"), message); + const response = { + accepted: this.callback_available(data.data), + request_id: data.request_id + } as ConnectOfferAnswer; - if(message.receiver === BasicIPCHandler.BROADCAST_UNIQUE_ID) { - if(message.type == "process-query") { - log.debug(LogCategory.IPC, tr("Received a device query from %s."), message.sender); - this.send_message("process-query-response", { - request_query_id: (message.data).query_id, - request_timestamp: (message.data).timestamp, + if(response.accepted) { + log.debug(LogCategory.IPC, tr("Received new connect offer from %s: %s"), sender, data.request_id); - device_id: this.unique_id, - protocol: BasicIPCHandler.PROTOCOL_VERSION - } as ProcessQueryResponse, message.sender); - return; - } - } else if(message.receiver === this.unique_id) { - if(message.type == "process-query-response") { - const response: ProcessQueryResponse = message.data; - if(this._query_results[response.request_query_id]) - this._query_results[response.request_query_id].push(response); - else { - log.warn(LogCategory.IPC, tr("Received a query response for an unknown request.")); + const ld = { + remote_handler: sender, + data: data.data, + id: data.request_id, + timeout: 0 + }; + this._pending_connect_offers.push(ld); + ld.timeout = setTimeout(() => { + log.debug(LogCategory.IPC, tr("Dropping connect request %s, because we never received an execute."), ld.id); + this._pending_connect_offers.remove(ld); + }, 120 * 1000) as any; } - return; + this.ipc_channel.send_message("offer-answer", response, sender); } - else if(message.type == "certificate-accept-callback") { - const data: CertificateAcceptCallback = message.data; - if(!this._cert_accept_callbacks[data.request_id]) { - log.warn(LogCategory.IPC, tr("Received certificate accept callback for an unknown request ID.")); + } else { + if(message.type == "offer-answer") { + const data = message.data as ConnectOfferAnswer; + const request = this._pending_connects_requests.find(e => e.id === data.request_id); + if(!request) { + log.warn(LogCategory.IPC, tr("Received connect offer answer with unknown request id (%s)."), data.request_id); return; } - this._cert_accept_callbacks[data.request_id](); - delete this._cert_accept_callbacks[data.request_id]; - - this.send_message("certificate-accept-succeeded", { - - } as CertificateAcceptSucceeded, message.sender); - return; - } - else if(message.type == "certificate-accept-succeeded") { - if(!this._cert_accept_succeeded[message.sender]) { - log.warn(LogCategory.IPC, tr("Received certificate accept succeeded, but haven't a callback.")); + if(!data.accepted) { + log.debug(LogCategory.IPC, tr("Client %s rejected the connect offer (%s)."), sender, request.id); return; } - this._cert_accept_succeeded[message.sender](); - return; - } - } - if(message.type === "channel") { - const data: ChannelMessage = message.data; - - let channel_invoked = false; - for(const channel of this._channels) - if(channel.channel_id === data.channel_id && (typeof(channel.target_id) === "undefined" || channel.target_id === message.sender)) { - if(channel.message_handler) - channel.message_handler(message.sender, message.receiver === BasicIPCHandler.BROADCAST_UNIQUE_ID, data); - channel_invoked = true; + if(request.remote_handler) { + log.debug(LogCategory.IPC, tr("Client %s accepted the connect offer (%s), but offer has already been accepted."), sender, request.id); + return; } - if(!channel_invoked) { - console.warn(tr("Received channel message for unknown channel (%s)"), data.channel_id); + + log.debug(LogCategory.IPC, tr("Client %s accepted the connect offer (%s). Request local acceptance."), sender, request.id); + request.remote_handler = sender; + clearTimeout(request.timeout); + + request.callback_avail().then(flag => { + if(!flag) { + request.callback_failed("local avail rejected"); + return; + } + + log.debug(LogCategory.IPC, tr("Executing connect with client %s"), request.remote_handler); + this.ipc_channel.send_message("execute", { + request_id: request.id + } as ConnectExecute, request.remote_handler); + request.timeout = setTimeout(() => { + request.callback_failed("connect execute timeout"); + }, 1000) as any; + }).catch(error => { + log.error(LogCategory.IPC, tr("Local avail callback caused an error: %o"), error); + request.callback_failed(tr("local avail callback caused an error")); + }); + + } + else if(message.type == "executed") { + const data = message.data as ConnectExecuted; + const request = this._pending_connects_requests.find(e => e.id === data.request_id); + if(!request) { + log.warn(LogCategory.IPC, tr("Received connect executed with unknown request id (%s)."), data.request_id); + return; + } + + if(request.remote_handler != sender) { + log.warn(LogCategory.IPC, tr("Received connect executed for request %s, but from wrong client: %s (expected %s)"), data.request_id, sender, request.remote_handler); + return; + } + + log.debug(LogCategory.IPC, tr("Received connect executed response from client %s for request %s. Succeeded: %o (%s)"), sender, data.request_id, data.succeeded, data.message); + clearTimeout(request.timeout); + if(data.succeeded) + request.callback_success(); + else + request.callback_failed(data.message); + } + else if(message.type == "execute") { + const data = message.data as ConnectExecute; + const request = this._pending_connect_offers.find(e => e.id === data.request_id); + if(!request) { + log.warn(LogCategory.IPC, tr("Received connect execute with unknown request id (%s)."), data.request_id); + return; + } + + if(request.remote_handler != sender) { + log.warn(LogCategory.IPC, tr("Received connect execute for request %s, but from wrong client: %s (expected %s)"), data.request_id, sender, request.remote_handler); + return; + } + clearTimeout(request.timeout); + this._pending_connect_offers.remove(request); + + log.debug(LogCategory.IPC, tr("Executing connect for %s"), data.request_id); + const cr = this.callback_execute(request.data); + + const response = { + request_id: data.request_id, + + succeeded: typeof(cr) !== "string" && cr, + message: typeof(cr) === "string" ? cr : "", + } as ConnectExecuted; + this.ipc_channel.send_message("executed", response, request.remote_handler); } } } - create_channel(target_id?: string, channel_id?: string) { - let channel: Channel = { - target_id: target_id, - channel_id: channel_id || uuidv4(), - message_handler: undefined, - send_message: (type: string, data: any, target?: string) => { - if(typeof target !== "undefined") { - if(typeof channel.target_id === "string" && target != channel.target_id) - throw "target id does not match channel target"; - } + post_connect_request(data: ConnectRequestData, callback_avail: () => Promise) : Promise { + return new Promise((resolve, reject) => { + const pd = { + data: data, + id: uuidv4(), + timeout: 0, - this.send_message("channel", { - type: type, - data: data, - channel_id: channel.channel_id - } as ChannelMessage, target || channel.target_id || BasicIPCHandler.BROADCAST_UNIQUE_ID); - } - }; + callback_success: () => { + this._pending_connects_requests.remove(pd); + clearTimeout(pd.timeout); + resolve(); + }, - this._channels.push(channel); - return channel; - } + callback_failed: error => { + this._pending_connects_requests.remove(pd); + clearTimeout(pd.timeout); + reject(error); + }, - channels() : Channel[] { return this._channels; } - - delete_channel(channel: Channel) { - this._channels = this._channels.filter(e => e !== channel); - } - - private _query_results: {[key: string]:ProcessQueryResponse[]} = {}; - async query_processes(timeout?: number) : Promise { - const query_id = uuidv4(); - this._query_results[query_id] = []; - - this.send_message("process-query", { - query_id: query_id, - timestamp: Date.now() - } as ProcessQuery); - - await new Promise(resolve => setTimeout(resolve, timeout || 250)); - const result = this._query_results[query_id]; - delete this._query_results[query_id]; - return result; - } - - private _cert_accept_callbacks: {[key: string]:(() => any)} = {}; - register_certificate_accept_callback(callback: () => any) : string { - const id = uuidv4(); - this._cert_accept_callbacks[id] = callback; - return this.unique_id + ":" + id; - } - - private _cert_accept_succeeded: {[sender: string]:(() => any)} = {}; - post_certificate_accpected(id: string, timeout?: number) : Promise { - return new Promise((resolve, reject) => { - const data = id.split(":"); - const timeout_id = setTimeout(() => { - delete this._cert_accept_succeeded[data[0]]; - clearTimeout(timeout_id); - reject("timeout"); - }, timeout || 250); - this._cert_accept_succeeded[data[0]] = () => { - delete this._cert_accept_succeeded[data[0]]; - clearTimeout(timeout_id); - resolve(); + callback_avail: callback_avail, }; - this.send_message("certificate-accept-callback", { - request_id: data[1] - } as CertificateAcceptCallback, data[0]); + this._pending_connects_requests.push(pd); + + this.ipc_channel.send_message("offer", { + request_id: pd.id, + data: pd.data + } as ConnectOffer); + pd.timeout = setTimeout(() => { + pd.callback_failed("received no response to offer"); + }, 50) as any; }) } } +} - export interface Channel { - readonly channel_id: string; - target_id?: string; +export namespace mproxy { + export interface MethodProxyInvokeData { + method_name: string; + arguments: any[]; + promise_id: string; + } + export interface MethodProxyResultData { + promise_id: string; + result: any; + success: boolean; + } + export interface MethodProxyCallback { + promise: Promise; + promise_id: string; - message_handler: (remote_id: string, broadcast: boolean, message: ChannelMessage) => any; - send_message(type: string, message: any, target?: string); + resolve: (object: any) => any; + reject: (object: any) => any; } - class BroadcastChannelIPC extends BasicIPCHandler { - private static readonly CHANNEL_NAME = "TeaSpeak-Web"; + export type MethodProxyConnectParameters = { + channel_id: string; + client_id: string; + } + export abstract class MethodProxy { + readonly ipc_handler: BasicIPCHandler; + private _ipc_channel: Channel; + private _ipc_parameters: MethodProxyConnectParameters; - private channel: BroadcastChannel; + private readonly _local: boolean; + private readonly _slave: boolean; - constructor() { - super(); + private _connected: boolean; + private _proxied_methods: {[key: string]:() => Promise} = {}; + private _proxied_callbacks: {[key: string]:MethodProxyCallback} = {}; + + protected constructor(ipc_handler: BasicIPCHandler, connect_params?: MethodProxyConnectParameters) { + this.ipc_handler = ipc_handler; + this._ipc_parameters = connect_params; + this._connected = false; + this._slave = typeof(connect_params) !== "undefined"; + this._local = typeof(connect_params) !== "undefined" && connect_params.channel_id === "local" && connect_params.client_id === "local"; } - setup() { - super.setup(); + protected setup() { + if(this._local) { + this._connected = true; + this.on_connected(); + } else { + if(this._slave) + this._ipc_channel = this.ipc_handler.create_channel(this._ipc_parameters.client_id, this._ipc_parameters.channel_id); + else + this._ipc_channel = this.ipc_handler.create_channel(); - this.channel = new BroadcastChannel(BroadcastChannelIPC.CHANNEL_NAME); - this.channel.onmessage = this.on_message.bind(this); - this.channel.onmessageerror = this.on_error.bind(this); + this._ipc_channel.message_handler = this._handle_message.bind(this); + if(this._slave) + this._ipc_channel.send_message("initialize", {}); + } } - private on_message(event: MessageEvent) { - if(typeof(event.data) !== "string") { - log.warn(LogCategory.IPC, tr("Received message with an invalid type (%s): %o"), typeof(event.data), event.data); + protected finalize() { + if(!this._local) { + if(this._connected) + this._ipc_channel.send_message("finalize", {}); + + this.ipc_handler.delete_channel(this._ipc_channel); + this._ipc_channel = undefined; + } + for(const promise of Object.values(this._proxied_callbacks)) + promise.reject("disconnected"); + this._proxied_callbacks = {}; + + this._connected = false; + this.on_disconnected(); + } + + protected register_method(method: (...args: any[]) => Promise | string) { + let method_name: string; + if(typeof method === "function") { + log.debug(LogCategory.IPC, tr("Registering method proxy for %s"), method.name); + method_name = method.name; + } else { + log.debug(LogCategory.IPC, tr("Registering method proxy for %s"), method); + method_name = method; + } + + if(!this[method_name]) + throw "method is missing in current object"; + + this._proxied_methods[method_name] = this[method_name]; + if(!this._local) { + this[method_name] = (...args: any[]) => { + if(!this._connected) + return Promise.reject("not connected"); + + const proxy_callback = { + promise_id: uuidv4() + } as MethodProxyCallback; + this._proxied_callbacks[proxy_callback.promise_id] = proxy_callback; + proxy_callback.promise = new Promise((resolve, reject) => { + proxy_callback.resolve = resolve; + proxy_callback.reject = reject; + }); + + this._ipc_channel.send_message("invoke", { + promise_id: proxy_callback.promise_id, + arguments: [...args], + method_name: method_name + } as MethodProxyInvokeData); + return proxy_callback.promise; + } + } + } + + private _handle_message(remote_id: string, boradcast: boolean, message: ChannelMessage) { + if(message.type === "finalize") { + this._handle_finalize(); + } else if(message.type === "initialize") { + this._handle_remote_callback(remote_id); + } else if(message.type === "invoke") { + this._handle_invoke(message.data); + } else if(message.type === "result") { + this._handle_result(message.data); + } + } + + private _handle_finalize() { + this.on_disconnected(); + this.finalize(); + this._connected = false; + } + + private _handle_remote_callback(remote_id: string) { + if(!this._ipc_channel.target_id) { + if(this._slave) + throw "initialize wrong state!"; + + this._ipc_channel.target_id = remote_id; /* now we're able to send messages */ + this.on_connected(); + this._ipc_channel.send_message("initialize", true); + } else { + if(!this._slave) + throw "initialize wrong state!"; + + this.on_connected(); + } + this._connected = true; + } + + private _send_result(promise_id: string, success: boolean, message: any) { + this._ipc_channel.send_message("result", { + promise_id: promise_id, + result: message, + success: success + } as MethodProxyResultData); + } + + private _handle_invoke(data: MethodProxyInvokeData) { + if(this._proxied_methods[data.method_name]) + throw "we could not invoke a local proxied method!"; + + if(!this[data.method_name]) { + this._send_result(data.promise_id, false, "missing method"); return; } - let message: BroadcastMessage; try { - message = JSON.parse(event.data); + log.info(LogCategory.IPC, tr("Invoking method %s with arguments: %o"), data.method_name, data.arguments); + + const promise = this[data.method_name](...data.arguments); + promise.then(result => { + log.info(LogCategory.IPC, tr("Result: %o"), result); + this._send_result(data.promise_id, true, result); + }).catch(error => { + this._send_result(data.promise_id, false, error); + }); } catch(error) { - log.error(LogCategory.IPC, tr("Received an invalid encoded message: %o"), event.data); + this._send_result(data.promise_id, false, error); return; } - super.handle_message(message); } - private on_error(event: MessageEvent) { - log.warn(LogCategory.IPC, tr("Received error: %o"), event); + private _handle_result(data: MethodProxyResultData) { + if(!this._proxied_callbacks[data.promise_id]) { + console.warn(tr("Received proxy method result for unknown promise")); + return; + } + const callback = this._proxied_callbacks[data.promise_id]; + delete this._proxied_callbacks[data.promise_id]; + + if(data.success) + callback.resolve(data.result); + else + callback.reject(data.result); } - send_message(type: string, data: any, target?: string) { - const message: BroadcastMessage = {} as any; + generate_connect_parameters() : MethodProxyConnectParameters { + if(this._slave) + throw "only masters can generate connect parameters!"; + if(!this._ipc_channel) + throw "please call setup() before"; - message.sender = this.unique_id; - message.receiver = target ? target : BasicIPCHandler.BROADCAST_UNIQUE_ID; - message.timestamp = Date.now(); - message.type = type; - message.data = data; - - this.channel.postMessage(JSON.stringify(message)); - } - } - - export namespace connect { - export type ConnectRequestData = { - address: string; - - profile?: string; - username?: string; - password?: { - value: string; - hashed: boolean; + return { + channel_id: this._ipc_channel.channel_id, + client_id: this.ipc_handler.get_local_address() }; } - export interface ConnectOffer { - request_id: string; - data: ConnectRequestData; - } + is_slave() { return this._local || this._slave; } /* the popout modal */ + is_master() { return this._local || !this._slave; } /* the host (teaweb application) */ - export interface ConnectOfferAnswer { - request_id: string; - accepted: boolean; - } - - export interface ConnectExecute { - request_id: string; - } - - export interface ConnectExecuted { - request_id: string; - succeeded: boolean; - message?: string; - } - - /* The connect process: - * 1. Broadcast an offer - * 2. Wait 50ms for all offer responses or until the first one respond with "ok" - * 3. Select (if possible) on accepted offer and execute the connect - */ - export class ConnectHandler { - private static readonly CHANNEL_NAME = "connect"; - - readonly ipc_handler: BasicIPCHandler; - private ipc_channel: Channel; - - public callback_available: (data: ConnectRequestData) => boolean = () => false; - public callback_execute: (data: ConnectRequestData) => boolean | string = () => false; - - - private _pending_connect_offers: { - id: string; - data: ConnectRequestData; - timeout: number; - - remote_handler: string; - }[] = []; - - private _pending_connects_requests: { - id: string; - - data: ConnectRequestData; - timeout: number; - - callback_success: () => any; - callback_failed: (message: string) => any; - callback_avail: () => Promise; - - remote_handler?: string; - }[] = []; - - constructor(ipc_handler: BasicIPCHandler) { - this.ipc_handler = ipc_handler; - } - - public setup() { - this.ipc_channel = this.ipc_handler.create_channel(undefined, ConnectHandler.CHANNEL_NAME); - this.ipc_channel.message_handler = this.on_message.bind(this); - } - - private on_message(sender: string, broadcast: boolean, message: ChannelMessage) { - if(broadcast) { - if(message.type == "offer") { - const data = message.data as ConnectOffer; - - const response = { - accepted: this.callback_available(data.data), - request_id: data.request_id - } as ConnectOfferAnswer; - - if(response.accepted) { - log.debug(LogCategory.IPC, tr("Received new connect offer from %s: %s"), sender, data.request_id); - - const ld = { - remote_handler: sender, - data: data.data, - id: data.request_id, - timeout: 0 - }; - this._pending_connect_offers.push(ld); - ld.timeout = setTimeout(() => { - log.debug(LogCategory.IPC, tr("Dropping connect request %s, because we never received an execute."), ld.id); - this._pending_connect_offers.remove(ld); - }, 120 * 1000) as any; - } - this.ipc_channel.send_message("offer-answer", response, sender); - } - } else { - if(message.type == "offer-answer") { - const data = message.data as ConnectOfferAnswer; - const request = this._pending_connects_requests.find(e => e.id === data.request_id); - if(!request) { - log.warn(LogCategory.IPC, tr("Received connect offer answer with unknown request id (%s)."), data.request_id); - return; - } - if(!data.accepted) { - log.debug(LogCategory.IPC, tr("Client %s rejected the connect offer (%s)."), sender, request.id); - return; - } - if(request.remote_handler) { - log.debug(LogCategory.IPC, tr("Client %s accepted the connect offer (%s), but offer has already been accepted."), sender, request.id); - return; - } - - log.debug(LogCategory.IPC, tr("Client %s accepted the connect offer (%s). Request local acceptance."), sender, request.id); - request.remote_handler = sender; - clearTimeout(request.timeout); - - request.callback_avail().then(flag => { - if(!flag) { - request.callback_failed("local avail rejected"); - return; - } - - log.debug(LogCategory.IPC, tr("Executing connect with client %s"), request.remote_handler); - this.ipc_channel.send_message("execute", { - request_id: request.id - } as ConnectExecute, request.remote_handler); - request.timeout = setTimeout(() => { - request.callback_failed("connect execute timeout"); - }, 1000) as any; - }).catch(error => { - log.error(LogCategory.IPC, tr("Local avail callback caused an error: %o"), error); - request.callback_failed(tr("local avail callback caused an error")); - }); - - } - else if(message.type == "executed") { - const data = message.data as ConnectExecuted; - const request = this._pending_connects_requests.find(e => e.id === data.request_id); - if(!request) { - log.warn(LogCategory.IPC, tr("Received connect executed with unknown request id (%s)."), data.request_id); - return; - } - - if(request.remote_handler != sender) { - log.warn(LogCategory.IPC, tr("Received connect executed for request %s, but from wrong client: %s (expected %s)"), data.request_id, sender, request.remote_handler); - return; - } - - log.debug(LogCategory.IPC, tr("Received connect executed response from client %s for request %s. Succeeded: %o (%s)"), sender, data.request_id, data.succeeded, data.message); - clearTimeout(request.timeout); - if(data.succeeded) - request.callback_success(); - else - request.callback_failed(data.message); - } - else if(message.type == "execute") { - const data = message.data as ConnectExecute; - const request = this._pending_connect_offers.find(e => e.id === data.request_id); - if(!request) { - log.warn(LogCategory.IPC, tr("Received connect execute with unknown request id (%s)."), data.request_id); - return; - } - - if(request.remote_handler != sender) { - log.warn(LogCategory.IPC, tr("Received connect execute for request %s, but from wrong client: %s (expected %s)"), data.request_id, sender, request.remote_handler); - return; - } - clearTimeout(request.timeout); - this._pending_connect_offers.remove(request); - - log.debug(LogCategory.IPC, tr("Executing connect for %s"), data.request_id); - const cr = this.callback_execute(request.data); - - const response = { - request_id: data.request_id, - - succeeded: typeof(cr) !== "string" && cr, - message: typeof(cr) === "string" ? cr : "", - } as ConnectExecuted; - this.ipc_channel.send_message("executed", response, request.remote_handler); - } - } - } - - post_connect_request(data: ConnectRequestData, callback_avail: () => Promise) : Promise { - return new Promise((resolve, reject) => { - const pd = { - data: data, - id: uuidv4(), - timeout: 0, - - callback_success: () => { - this._pending_connects_requests.remove(pd); - clearTimeout(pd.timeout); - resolve(); - }, - - callback_failed: error => { - this._pending_connects_requests.remove(pd); - clearTimeout(pd.timeout); - reject(error); - }, - - callback_avail: callback_avail, - }; - this._pending_connects_requests.push(pd); - - this.ipc_channel.send_message("offer", { - request_id: pd.id, - data: pd.data - } as ConnectOffer); - pd.timeout = setTimeout(() => { - pd.callback_failed("received no response to offer"); - }, 50) as any; - }) - } - } + protected abstract on_connected(); + protected abstract on_disconnected(); } +} - export namespace mproxy { - export interface MethodProxyInvokeData { - method_name: string; - arguments: any[]; - promise_id: string; - } - export interface MethodProxyResultData { - promise_id: string; - result: any; - success: boolean; - } - export interface MethodProxyCallback { - promise: Promise; - promise_id: string; +let handler: BasicIPCHandler; +let connect_handler: connect.ConnectHandler; - resolve: (object: any) => any; - reject: (object: any) => any; - } +export function setup() { + if(!supported()) + return; - export type MethodProxyConnectParameters = { - channel_id: string; - client_id: string; - } - export abstract class MethodProxy { - readonly ipc_handler: BasicIPCHandler; - private _ipc_channel: Channel; - private _ipc_parameters: MethodProxyConnectParameters; + handler = new BroadcastChannelIPC(); + handler.setup(); - private readonly _local: boolean; - private readonly _slave: boolean; + connect_handler = new connect.ConnectHandler(handler); + connect_handler.setup(); +} - private _connected: boolean; - private _proxied_methods: {[key: string]:() => Promise} = {}; - private _proxied_callbacks: {[key: string]:MethodProxyCallback} = {}; +export function get_handler() { + return handler; +} - protected constructor(ipc_handler: BasicIPCHandler, connect_params?: MethodProxyConnectParameters) { - this.ipc_handler = ipc_handler; - this._ipc_parameters = connect_params; - this._connected = false; - this._slave = typeof(connect_params) !== "undefined"; - this._local = typeof(connect_params) !== "undefined" && connect_params.channel_id === "local" && connect_params.client_id === "local"; - } +export function get_connect_handler() { + return connect_handler; +} - protected setup() { - if(this._local) { - this._connected = true; - this.on_connected(); - } else { - if(this._slave) - this._ipc_channel = this.ipc_handler.create_channel(this._ipc_parameters.client_id, this._ipc_parameters.channel_id); - else - this._ipc_channel = this.ipc_handler.create_channel(); - - this._ipc_channel.message_handler = this._handle_message.bind(this); - if(this._slave) - this._ipc_channel.send_message("initialize", {}); - } - } - - protected finalize() { - if(!this._local) { - if(this._connected) - this._ipc_channel.send_message("finalize", {}); - - this.ipc_handler.delete_channel(this._ipc_channel); - this._ipc_channel = undefined; - } - for(const promise of Object.values(this._proxied_callbacks)) - promise.reject("disconnected"); - this._proxied_callbacks = {}; - - this._connected = false; - this.on_disconnected(); - } - - protected register_method(method: (...args: any[]) => Promise | string) { - let method_name: string; - if(typeof method === "function") { - log.debug(LogCategory.IPC, tr("Registering method proxy for %s"), method.name); - method_name = method.name; - } else { - log.debug(LogCategory.IPC, tr("Registering method proxy for %s"), method); - method_name = method; - } - - if(!this[method_name]) - throw "method is missing in current object"; - - this._proxied_methods[method_name] = this[method_name]; - if(!this._local) { - this[method_name] = (...args: any[]) => { - if(!this._connected) - return Promise.reject("not connected"); - - const proxy_callback = { - promise_id: uuidv4() - } as MethodProxyCallback; - this._proxied_callbacks[proxy_callback.promise_id] = proxy_callback; - proxy_callback.promise = new Promise((resolve, reject) => { - proxy_callback.resolve = resolve; - proxy_callback.reject = reject; - }); - - this._ipc_channel.send_message("invoke", { - promise_id: proxy_callback.promise_id, - arguments: [...args], - method_name: method_name - } as MethodProxyInvokeData); - return proxy_callback.promise; - } - } - } - - private _handle_message(remote_id: string, boradcast: boolean, message: ChannelMessage) { - if(message.type === "finalize") { - this._handle_finalize(); - } else if(message.type === "initialize") { - this._handle_remote_callback(remote_id); - } else if(message.type === "invoke") { - this._handle_invoke(message.data); - } else if(message.type === "result") { - this._handle_result(message.data); - } - } - - private _handle_finalize() { - this.on_disconnected(); - this.finalize(); - this._connected = false; - } - - private _handle_remote_callback(remote_id: string) { - if(!this._ipc_channel.target_id) { - if(this._slave) - throw "initialize wrong state!"; - - this._ipc_channel.target_id = remote_id; /* now we're able to send messages */ - this.on_connected(); - this._ipc_channel.send_message("initialize", true); - } else { - if(!this._slave) - throw "initialize wrong state!"; - - this.on_connected(); - } - this._connected = true; - } - - private _send_result(promise_id: string, success: boolean, message: any) { - this._ipc_channel.send_message("result", { - promise_id: promise_id, - result: message, - success: success - } as MethodProxyResultData); - } - - private _handle_invoke(data: MethodProxyInvokeData) { - if(this._proxied_methods[data.method_name]) - throw "we could not invoke a local proxied method!"; - - if(!this[data.method_name]) { - this._send_result(data.promise_id, false, "missing method"); - return; - } - - try { - log.info(LogCategory.IPC, tr("Invoking method %s with arguments: %o"), data.method_name, data.arguments); - - const promise = this[data.method_name](...data.arguments); - promise.then(result => { - log.info(LogCategory.IPC, tr("Result: %o"), result); - this._send_result(data.promise_id, true, result); - }).catch(error => { - this._send_result(data.promise_id, false, error); - }); - } catch(error) { - this._send_result(data.promise_id, false, error); - return; - } - } - - private _handle_result(data: MethodProxyResultData) { - if(!this._proxied_callbacks[data.promise_id]) { - console.warn(tr("Received proxy method result for unknown promise")); - return; - } - const callback = this._proxied_callbacks[data.promise_id]; - delete this._proxied_callbacks[data.promise_id]; - - if(data.success) - callback.resolve(data.result); - else - callback.reject(data.result); - } - - generate_connect_parameters() : MethodProxyConnectParameters { - if(this._slave) - throw "only masters can generate connect parameters!"; - if(!this._ipc_channel) - throw "please call setup() before"; - - return { - channel_id: this._ipc_channel.channel_id, - client_id: this.ipc_handler.get_local_address() - }; - } - - is_slave() { return this._local || this._slave; } /* the popout modal */ - is_master() { return this._local || !this._slave; } /* the host (teaweb application) */ - - protected abstract on_connected(); - protected abstract on_disconnected(); - } - } - - let handler: BasicIPCHandler; - let connect_handler: connect.ConnectHandler; - - export function setup() { - if(!supported()) - return; - - handler = new BroadcastChannelIPC(); - handler.setup(); - - connect_handler = new connect.ConnectHandler(handler); - connect_handler.setup(); - } - - export function get_handler() { - return handler; - } - - export function get_connect_handler() { - return connect_handler; - } - - export function supported() { - /* ios does not support this */ - return typeof(window.BroadcastChannel) !== "undefined"; - } +export function supported() { + /* ios does not support this */ + return typeof(window.BroadcastChannel) !== "undefined"; } \ No newline at end of file diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index 85c3b8af..8a0b6731 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -1,14 +1,38 @@ -/// -/// -/// -/// -/// -/// -/// -/// -/// +import {ChannelTree} from "tc-shared/ui/view"; +import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase"; +import {PermissionManager} from "tc-shared/permission/PermissionManager"; +import {GroupManager} from "tc-shared/permission/GroupManager"; +import {ServerSettings, Settings, StaticSettings} from "tc-shared/settings"; +import {Sound, SoundManager} from "tc-shared/sound/Sounds"; +import {LocalClientEntry} from "tc-shared/ui/client"; +import {ServerLog} from "tc-shared/ui/frames/server_log"; +import {ConnectionProfile, default_profile, find_profile} from "tc-shared/profiles/ConnectionProfile"; +import {ServerAddress} from "tc-shared/ui/server"; +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import * as server_log from "tc-shared/ui/frames/server_log"; +import {createErrorModal, createInfoModal, createInputModal, Modal} from "tc-shared/ui/elements/Modal"; +import {hashPassword} from "tc-shared/utils/helpers"; +import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler"; +import * as htmltags from "./ui/htmltags"; +import {ChannelEntry} from "tc-shared/ui/channel"; +import {InputStartResult, InputState} from "tc-shared/voice/RecorderBase"; +import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; +import {guid} from "tc-shared/crypto/uid"; +import * as bipc from "./BrowserIPC"; +import {FileManager, spawn_upload_transfer, UploadKey} from "tc-shared/FileManager"; +import {RecorderProfile} from "tc-shared/voice/RecorderProfile"; +import {Frame} from "tc-shared/ui/frames/chat_frame"; +import {Hostbanner} from "tc-shared/ui/frames/hostbanner"; +import {server_connections} from "tc-shared/ui/frames/connection_handlers"; +import {connection_log, Regex} from "tc-shared/ui/modal/ModalConnect"; +import {control_bar} from "tc-shared/ui/frames/ControlBar"; +import {formatMessage} from "tc-shared/ui/frames/chat"; +import {spawnAvatarUpload} from "tc-shared/ui/modal/ModalAvatar"; +import * as connection from "tc-backend/connection"; +import * as dns from "tc-backend/dns"; -enum DisconnectReason { +export enum DisconnectReason { HANDLER_DESTROYED, REQUESTED, DNS_FAILED, @@ -28,7 +52,7 @@ enum DisconnectReason { UNKNOWN } -enum ConnectionState { +export enum ConnectionState { UNCONNECTED, CONNECTING, INITIALISING, @@ -36,7 +60,7 @@ enum ConnectionState { DISCONNECTING } -enum ViewReasonId { +export enum ViewReasonId { VREASON_USER_ACTION = 0, VREASON_MOVED = 1, VREASON_SYSTEM = 2, @@ -51,7 +75,7 @@ enum ViewReasonId { VREASON_SERVER_SHUTDOWN = 11 } -interface VoiceStatus { +export interface VoiceStatus { input_hardware: boolean; input_muted: boolean; output_muted: boolean; @@ -68,7 +92,7 @@ interface VoiceStatus { queries_visible: boolean; } -interface ConnectParameters { +export interface ConnectParameters { nickname?: string; channel?: { target: string | number; @@ -79,20 +103,21 @@ interface ConnectParameters { auto_reconnect_attempt?: boolean; } -class ConnectionHandler { +declare const native_client; +export class ConnectionHandler { channelTree: ChannelTree; - serverConnection: connection.AbstractServerConnection; + serverConnection: AbstractServerConnection; fileManager: FileManager; permissions: PermissionManager; groups: GroupManager; - side_bar: chat.Frame; + side_bar: Frame; settings: ServerSettings; - sound: sound.SoundManager; + sound: SoundManager; hostbanner: Hostbanner; @@ -121,15 +146,15 @@ class ConnectionHandler { }; invoke_resized_on_activate: boolean = false; - log: log.ServerLog; + log: ServerLog; constructor() { this.settings = new ServerSettings(); - this.log = new log.ServerLog(this); + this.log = new ServerLog(this); this.channelTree = new ChannelTree(this); - this.side_bar = new chat.Frame(this); - this.sound = new sound.SoundManager(this); + this.side_bar = new Frame(this); + this.sound = new SoundManager(this); this.hostbanner = new Hostbanner(this); this.serverConnection = connection.spawn_server_connection(this); @@ -169,7 +194,7 @@ class ConnectionHandler { setup() { } - async startConnection(addr: string, profile: profiles.ConnectionProfile, user_action: boolean, parameters: ConnectParameters) { + async startConnection(addr: string, profile: ConnectionProfile, user_action: boolean, parameters: ConnectParameters) { this.tab_set_name(tr("Connecting")); this.cancel_reconnect(false); this._reconnect_attempt = parameters.auto_reconnect_attempt || false; @@ -192,7 +217,7 @@ class ConnectionHandler { } } log.info(LogCategory.CLIENT, tr("Start connection to %s:%d"), server_address.host, server_address.port); - this.log.log(log.server.Type.CONNECTION_BEGIN, { + this.log.log(server_log.Type.CONNECTION_BEGIN, { address: { server_hostname: server_address.host, server_port: server_address.port @@ -203,7 +228,7 @@ class ConnectionHandler { if(parameters.password && !parameters.password.hashed){ try { - const password = await helpers.hashPassword(parameters.password.password); + const password = await hashPassword(parameters.password.password); parameters.password = { hashed: true, password: password @@ -221,9 +246,9 @@ class ConnectionHandler { } const original_address = {host: server_address.host, port: server_address.port}; - if(dns.supported() && !server_address.host.match(Modals.Regex.IP_V4) && !server_address.host.match(Modals.Regex.IP_V6)) { + if(dns.supported() && !server_address.host.match(Regex.IP_V4) && !server_address.host.match(Regex.IP_V6)) { const id = ++this._connect_initialize_id; - this.log.log(log.server.Type.CONNECTION_HOSTNAME_RESOLVE, {}); + this.log.log(server_log.Type.CONNECTION_HOSTNAME_RESOLVE, {}); try { const resolved = await dns.resolve_address(server_address, { timeout: 5000 }) || {} as any; if(id != this._connect_initialize_id) @@ -231,7 +256,7 @@ class ConnectionHandler { server_address.host = typeof(resolved.target_ip) === "string" ? resolved.target_ip : server_address.host; server_address.port = typeof(resolved.target_port) === "number" ? resolved.target_port : server_address.port; - this.log.log(log.server.Type.CONNECTION_HOSTNAME_RESOLVED, { + this.log.log(server_log.Type.CONNECTION_HOSTNAME_RESOLVED, { address: { server_port: server_address.port, server_hostname: server_address.host @@ -245,7 +270,7 @@ class ConnectionHandler { } } - await this.serverConnection.connect(server_address, new connection.HandshakeHandler(profile, parameters)); + await this.serverConnection.connect(server_address, new HandshakeHandler(profile, parameters)); setTimeout(() => { const connected = this.serverConnection.connected(); if(user_action && connected) { @@ -270,7 +295,7 @@ class ConnectionHandler { return this._clientId; } - getServerConnection() : connection.AbstractServerConnection { return this.serverConnection; } + getServerConnection() : AbstractServerConnection { return this.serverConnection; } /** @@ -380,7 +405,7 @@ class ConnectionHandler { popup.close(); /* no need, but nicer */ - const profile = profiles.find_profile(properties.connect_profile) || profiles.default_profile(); + const profile = find_profile(properties.connect_profile) || default_profile(); const cprops = this.reconnect_properties(profile); this.startConnection(properties.connect_address, profile, true, cprops); }); @@ -418,12 +443,12 @@ class ConnectionHandler { case DisconnectReason.HANDLER_DESTROYED: if(data) { this.sound.play(Sound.CONNECTION_DISCONNECTED); - this.log.log(log.server.Type.DISCONNECTED, {}); + this.log.log(server_log.Type.DISCONNECTED, {}); } break; case DisconnectReason.DNS_FAILED: log.error(LogCategory.CLIENT, tr("Failed to resolve hostname: %o"), data); - this.log.log(log.server.Type.CONNECTION_HOSTNAME_RESOLVE_ERROR, { + this.log.log(server_log.Type.CONNECTION_HOSTNAME_RESOLVE_ERROR, { message: data as any }); this.sound.play(Sound.CONNECTION_REFUSED); @@ -431,7 +456,7 @@ class ConnectionHandler { case DisconnectReason.CONNECT_FAILURE: if(this._reconnect_attempt) { auto_reconnect = true; - this.log.log(log.server.Type.CONNECTION_FAILED, {}); + this.log.log(server_log.Type.CONNECTION_FAILED, {}); break; } if(data) @@ -452,7 +477,7 @@ class ConnectionHandler { this._certificate_modal = createErrorModal( tr("Could not connect"), - MessageHelper.formatMessage(tr(error_message_format), this.generate_ssl_certificate_accept()) + formatMessage(tr(error_message_format), this.generate_ssl_certificate_accept()) ); this._certificate_modal.close_listener.push(() => this._certificate_modal = undefined); this._certificate_modal.open(); @@ -470,7 +495,7 @@ class ConnectionHandler { case DisconnectReason.HANDSHAKE_TEAMSPEAK_REQUIRED: createErrorModal( tr("Target server is a TeamSpeak server"), - MessageHelper.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.")) + 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.")) ).open(); this.sound.play(Sound.CONNECTION_DISCONNECTED); auto_reconnect = false; @@ -478,7 +503,7 @@ class ConnectionHandler { case DisconnectReason.IDENTITY_TOO_LOW: createErrorModal( tr("Identity level is too low"), - MessageHelper.formatMessage(tr("You've been disconnected, because your Identity level is too low.{:br:}You need at least a level of {0}"), data["extra_message"]) + formatMessage(tr("You've been disconnected, because your Identity level is too low.{:br:}You need at least a level of {0}"), data["extra_message"]) ).open(); this.sound.play(Sound.CONNECTION_DISCONNECTED); @@ -506,7 +531,7 @@ class ConnectionHandler { break; case DisconnectReason.SERVER_CLOSED: - this.log.log(log.server.Type.SERVER_CLOSED, {message: data.reasonmsg}); + this.log.log(server_log.Type.SERVER_CLOSED, {message: data.reasonmsg}); createErrorModal( tr("Server closed"), @@ -518,7 +543,7 @@ class ConnectionHandler { auto_reconnect = true; break; case DisconnectReason.SERVER_REQUIRES_PASSWORD: - this.log.log(log.server.Type.SERVER_REQUIRES_PASSWORD, {}); + this.log.log(server_log.Type.SERVER_REQUIRES_PASSWORD, {}); createInputModal(tr("Server password"), tr("Enter server password:"), password => password.length != 0, password => { if(!(typeof password === "string")) return; @@ -540,7 +565,7 @@ class ConnectionHandler { const have_invoker = typeof(data["invokerid"]) !== "undefined" && parseInt(data["invokerid"]) !== 0; const modal = createErrorModal( tr("You've been kicked"), - MessageHelper.formatMessage( + formatMessage( have_invoker ? tr("You've been kicked from the server by {0}:{:br:}{1}") : tr("You've been kicked from the server:{:br:}{1}"), have_invoker ? htmltags.generate_client_object({ client_id: parseInt(data["invokerid"]), client_unique_id: data["invokeruid"], client_name: data["invokername"]}) : @@ -559,7 +584,7 @@ class ConnectionHandler { this.sound.play(Sound.CONNECTION_BANNED); break; case DisconnectReason.CLIENT_BANNED: - this.log.log(log.server.Type.SERVER_BANNED, { + this.log.log(server_log.Type.SERVER_BANNED, { invoker: { client_name: data["invokername"], client_id: parseInt(data["invokerid"]), @@ -592,7 +617,7 @@ class ConnectionHandler { log.info(LogCategory.NETWORKING, tr("Allowed to auto reconnect but cant reconnect because we dont have any information left...")); return; } - this.log.log(log.server.Type.RECONNECT_SCHEDULED, {timeout: 5000}); + this.log.log(server_log.Type.RECONNECT_SCHEDULED, {timeout: 5000}); log.info(LogCategory.NETWORKING, tr("Allowed to auto reconnect. Reconnecting in 5000ms")); const server_address = this.serverConnection.remote_address(); @@ -600,7 +625,7 @@ class ConnectionHandler { this._reconnect_timer = setTimeout(() => { this._reconnect_timer = undefined; - this.log.log(log.server.Type.RECONNECT_EXECUTE, {}); + this.log.log(server_log.Type.RECONNECT_EXECUTE, {}); log.info(LogCategory.NETWORKING, tr("Reconnecting...")); this.startConnection(server_address.host + ":" + server_address.port, profile, false, Object.assign(this.reconnect_properties(profile), {auto_reconnect_attempt: true})); @@ -610,7 +635,7 @@ class ConnectionHandler { cancel_reconnect(log_event: boolean) { if(this._reconnect_timer) { - if(log_event) this.log.log(log.server.Type.RECONNECT_CANCELED, {}); + if(log_event) this.log.log(server_log.Type.RECONNECT_CANCELED, {}); clearTimeout(this._reconnect_timer); this._reconnect_timer = undefined; } @@ -665,7 +690,7 @@ class ConnectionHandler { if(Object.keys(property_update).length > 0) { this.serverConnection.send_command("clientupdate", property_update).catch(error => { log.warn(LogCategory.GENERAL, tr("Failed to update client audio hardware properties. Error: %o"), error); - this.log.log(log.server.Type.ERROR_CUSTOM, {message: tr("Failed to update audio hardware properties.")}); + this.log.log(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to update audio hardware properties.")}); /* Update these properties anyways (for case the server fails to handle the command) */ const updates = []; @@ -708,15 +733,15 @@ class ConnectionHandler { const input = vconnection.voice_recorder().input; if(input) { if(active && this.serverConnection.connected()) { - if(input.current_state() === audio.recorder.InputState.PAUSED) { + if(input.current_state() === InputState.PAUSED) { input.start().then(result => { - if(result != audio.recorder.InputStartResult.EOK) + if(result != InputStartResult.EOK) throw result; }).catch(error => { log.warn(LogCategory.VOICE, tr("Failed to start microphone input (%s)."), error); if(Date.now() - (this._last_record_error_popup || 0) > 10 * 1000) { this._last_record_error_popup = Date.now(); - createErrorModal(tr("Failed to start recording"), MessageHelper.formatMessage(tr("Microphone start failed.{:br:}Error: {}"), error)).open(); + createErrorModal(tr("Failed to start recording"), formatMessage(tr("Microphone start failed.{:br:}Error: {}"), error)).open(); } }); } @@ -741,7 +766,7 @@ class ConnectionHandler { client_output_hardware: this.client_status.sound_playback_supported }).catch(error => { log.warn(LogCategory.GENERAL, tr("Failed to sync handler state with server. Error: %o"), error); - this.log.log(log.server.Type.ERROR_CUSTOM, {message: tr("Failed to sync handler state with server.")}); + this.log.log(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to sync handler state with server.")}); }); } @@ -761,7 +786,7 @@ class ConnectionHandler { client_away_message: typeof(this.client_status.away) === "string" ? this.client_status.away : "", }).catch(error => { log.warn(LogCategory.GENERAL, tr("Failed to update away status. Error: %o"), error); - this.log.log(log.server.Type.ERROR_CUSTOM, {message: tr("Failed to update away status.")}); + this.log.log(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to update away status.")}); }); control_bar.update_button_away(); @@ -781,10 +806,10 @@ class ConnectionHandler { }); } - reconnect_properties(profile?: profiles.ConnectionProfile) : ConnectParameters { + reconnect_properties(profile?: ConnectionProfile) : ConnectParameters { const name = (this.getClient() ? this.getClient().clientNickName() : "") || (this.serverConnection && this.serverConnection.handshake_handler() ? this.serverConnection.handshake_handler().parameters.nickname : "") || - settings.static_global(Settings.KEY_CONNECT_USERNAME, profile ? profile.default_username : undefined) || + StaticSettings.instance.static(Settings.KEY_CONNECT_USERNAME, profile ? profile.default_username : undefined) || "Another TeaSpeak user"; const channel = (this.getClient() && this.getClient().currentChannel() ? this.getClient().currentChannel().channelId : 0) || (this.serverConnection && this.serverConnection.handshake_handler() ? (this.serverConnection.handshake_handler().parameters.channel || {} as any).target : ""); @@ -798,7 +823,7 @@ class ConnectionHandler { } update_avatar() { - Modals.spawnAvatarUpload(data => { + spawnAvatarUpload(data => { if(typeof(data) === "undefined") return; if(data === null) { @@ -814,16 +839,16 @@ class ConnectionHandler { let message; if(error instanceof CommandResult) - message = MessageHelper.formatMessage(tr("Failed to delete avatar.{:br:}Error: {0}"), error.extra_message || error.message); + message = formatMessage(tr("Failed to delete avatar.{:br:}Error: {0}"), error.extra_message || error.message); if(!message) - message = MessageHelper.formatMessage(tr("Failed to delete avatar.{:br:}Lookup the console for more details")); + message = formatMessage(tr("Failed to delete avatar.{:br:}Lookup the console for more details")); createErrorModal(tr("Failed to delete avatar"), message).open(); return; }); } else { log.info(LogCategory.CLIENT, tr("Uploading new avatar")); (async () => { - let key: transfer.UploadKey; + let key: UploadKey; try { key = await this.fileManager.upload_file({ size: data.byteLength, @@ -840,28 +865,28 @@ class ConnectionHandler { //TODO: Resolve permission name //i_client_max_avatar_filesize if(error.id == ErrorID.PERMISSION_ERROR) { - message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Missing permission {0}"), error["failed_permid"]); + message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Missing permission {0}"), error["failed_permid"]); } else { - message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Error: {0}"), error.extra_message || error.message); + message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Error: {0}"), error.extra_message || error.message); } } if(!message) - message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details")); + message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details")); createErrorModal(tr("Failed to upload avatar"), message).open(); return; } try { - await transfer.spawn_upload_transfer(key).put_data(data); + await spawn_upload_transfer(key).put_data(data); } catch(error) { log.error(LogCategory.GENERAL, tr("Failed to upload avatar: %o"), error); let message; if(typeof(error) === "string") - message = MessageHelper.formatMessage(tr("Failed to upload avatar.{:br:}Error: {0}"), error); + message = formatMessage(tr("Failed to upload avatar.{:br:}Error: {0}"), error); if(!message) - message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details")); + message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details")); createErrorModal(tr("Failed to upload avatar"), message).open(); return; } @@ -874,9 +899,9 @@ class ConnectionHandler { let message; if(error instanceof CommandResult) - message = MessageHelper.formatMessage(tr("Failed to update avatar flag.{:br:}Error: {0}"), error.extra_message || error.message); + message = formatMessage(tr("Failed to update avatar flag.{:br:}Error: {0}"), error.extra_message || error.message); if(!message) - message = MessageHelper.formatMessage(tr("Failed to update avatar flag.{:br:}Lookup the console for more details")); + message = formatMessage(tr("Failed to update avatar flag.{:br:}Lookup the console for more details")); createErrorModal(tr("Failed to set avatar"), message).open(); return; } diff --git a/shared/js/FileManager.ts b/shared/js/FileManager.ts index e20cc336..a1167ffe 100644 --- a/shared/js/FileManager.ts +++ b/shared/js/FileManager.ts @@ -1,76 +1,80 @@ -/// -/// +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import {ChannelEntry} from "tc-shared/ui/channel"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {ServerCommand} from "tc-shared/connection/ConnectionBase"; +import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; +import {ClientEntry} from "tc-shared/ui/client"; +import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler"; -class FileEntry { +export class FileEntry { name: string; datetime: number; type: number; size: number; } -class FileListRequest { +export class FileListRequest { path: string; entries: FileEntry[]; callback: (entries: FileEntry[]) => void; } -namespace transfer { - export interface TransferKey { - client_transfer_id: number; - server_transfer_id: number; +export interface TransferKey { + client_transfer_id: number; + server_transfer_id: number; - key: string; + key: string; - file_path: string; - file_name: string; + file_path: string; + file_name: string; - peer: { - hosts: string[], - port: number; - }; + peer: { + hosts: string[], + port: number; + }; - total_size: number; - } - - export interface UploadOptions { - name: string; - path: string; - - channel?: ChannelEntry; - channel_password?: string; - - size: number; - overwrite: boolean; - } - - export interface DownloadTransfer { - get_key() : DownloadKey; - - request_file() : Promise; - } - - export interface UploadTransfer { - get_key(): UploadKey; - - put_data(data: BlobPart | File) : Promise; - } - - export type DownloadKey = TransferKey; - export type UploadKey = TransferKey; - - export function spawn_download_transfer(key: DownloadKey) : DownloadTransfer { - return new RequestFileDownload(key); - } - export function spawn_upload_transfer(key: UploadKey) : UploadTransfer { - return new RequestFileUpload(key); - } + total_size: number; } -class RequestFileDownload implements transfer.DownloadTransfer { - readonly transfer_key: transfer.DownloadKey; +export interface UploadOptions { + name: string; + path: string; - constructor(key: transfer.DownloadKey) { + channel?: ChannelEntry; + channel_password?: string; + + size: number; + overwrite: boolean; +} + +export interface DownloadTransfer { + get_key() : DownloadKey; + + request_file() : Promise; +} + +export interface UploadTransfer { + get_key(): UploadKey; + + put_data(data: BlobPart | File) : Promise; +} + +export type DownloadKey = TransferKey; +export type UploadKey = TransferKey; + +export function spawn_download_transfer(key: DownloadKey) : DownloadTransfer { + return new RequestFileDownload(key); +} +export function spawn_upload_transfer(key: UploadKey) : UploadTransfer { + return new RequestFileUpload(key); +} + +export class RequestFileDownload implements DownloadTransfer { + readonly transfer_key: DownloadKey; + + constructor(key: DownloadKey) { this.transfer_key = key; } @@ -97,18 +101,18 @@ class RequestFileDownload implements transfer.DownloadTransfer { return response; } - get_key(): transfer.DownloadKey { + get_key(): DownloadKey { return this.transfer_key; } } -class RequestFileUpload implements transfer.UploadTransfer { - readonly transfer_key: transfer.UploadKey; - constructor(key: transfer.DownloadKey) { +export class RequestFileUpload implements UploadTransfer { + readonly transfer_key: UploadKey; + constructor(key: DownloadKey) { this.transfer_key = key; } - get_key(): transfer.UploadKey { + get_key(): UploadKey { return this.transfer_key; } @@ -152,14 +156,14 @@ class RequestFileUpload implements transfer.UploadTransfer { } } -class FileManager extends connection.AbstractCommandHandler { +export class FileManager extends AbstractCommandHandler { handle: ConnectionHandler; icons: IconManager; avatars: AvatarManager; private listRequests: FileListRequest[] = []; - private pending_download_requests: transfer.DownloadKey[] = []; - private pending_upload_requests: transfer.UploadKey[] = []; + private pending_download_requests: DownloadKey[] = []; + private pending_upload_requests: UploadKey[] = []; private transfer_counter : number = 1; @@ -191,7 +195,7 @@ class FileManager extends connection.AbstractCommandHandler { this.avatars = undefined; } - handle_command(command: connection.ServerCommand): boolean { + handle_command(command: ServerCommand): boolean { switch (command.command) { case "notifyfilelist": this.notifyFileList(command.arguments); @@ -276,15 +280,15 @@ class FileManager extends connection.AbstractCommandHandler { /******************************** File download/upload ********************************/ - download_file(path: string, file: string, channel?: ChannelEntry, password?: string) : Promise { - const transfer_data: transfer.DownloadKey = { + download_file(path: string, file: string, channel?: ChannelEntry, password?: string) : Promise { + const transfer_data: DownloadKey = { file_name: file, file_path: path, client_transfer_id: this.transfer_counter++ } as any; this.pending_download_requests.push(transfer_data); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { transfer_data["_callback"] = resolve; this.handle.serverConnection.send_command("ftinitdownload", { "path": path, @@ -301,8 +305,8 @@ class FileManager extends connection.AbstractCommandHandler { }); } - upload_file(options: transfer.UploadOptions) : Promise { - const transfer_data: transfer.UploadKey = { + upload_file(options: UploadOptions) : Promise { + const transfer_data: UploadKey = { file_path: options.path, file_name: options.name, client_transfer_id: this.transfer_counter++, @@ -310,7 +314,7 @@ class FileManager extends connection.AbstractCommandHandler { } as any; this.pending_upload_requests.push(transfer_data); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { transfer_data["_callback"] = resolve; this.handle.serverConnection.send_command("ftinitupload", { "path": options.path, @@ -333,7 +337,7 @@ class FileManager extends connection.AbstractCommandHandler { json = json[0]; let clientftfid = parseInt(json["clientftfid"]); - let transfer: transfer.DownloadKey; + let transfer: DownloadKey; for(let e of this.pending_download_requests) if(e.client_transfer_id == clientftfid) { transfer = e; @@ -355,14 +359,14 @@ class FileManager extends connection.AbstractCommandHandler { if(transfer.peer.hosts[0].length == 0 || transfer.peer.hosts[0] == '0.0.0.0') transfer.peer.hosts[0] = this.handle.serverConnection.remote_address().host; - (transfer["_callback"] as (val: transfer.DownloadKey) => void)(transfer); + (transfer["_callback"] as (val: DownloadKey) => void)(transfer); this.pending_download_requests.remove(transfer); } private notifyStartUpload(json) { json = json[0]; - let transfer: transfer.UploadKey; + let transfer: UploadKey; let clientftfid = parseInt(json["clientftfid"]); for(let e of this.pending_upload_requests) if(e.client_transfer_id == clientftfid) { @@ -384,7 +388,7 @@ class FileManager extends connection.AbstractCommandHandler { if(transfer.peer.hosts[0].length == 0 || transfer.peer.hosts[0] == '0.0.0.0') transfer.peer.hosts[0] = this.handle.serverConnection.remote_address().host; - (transfer["_callback"] as (val: transfer.UploadKey) => void)(transfer); + (transfer["_callback"] as (val: UploadKey) => void)(transfer); this.pending_upload_requests.remove(transfer); } @@ -411,12 +415,12 @@ class FileManager extends connection.AbstractCommandHandler { } } -class Icon { +export class Icon { id: number; url: string; } -enum ImageType { +export enum ImageType { UNKNOWN, BITMAP, PNG, @@ -425,7 +429,7 @@ enum ImageType { JPEG } -function media_image_type(type: ImageType, file?: boolean) { +export function media_image_type(type: ImageType, file?: boolean) { switch (type) { case ImageType.BITMAP: return "bmp"; @@ -442,7 +446,7 @@ function media_image_type(type: ImageType, file?: boolean) { } } -function image_type(encoded_data: string | ArrayBuffer, base64_encoded?: boolean) { +export function image_type(encoded_data: string | ArrayBuffer, base64_encoded?: boolean) { const ab2str10 = () => { const buf = new Uint8Array(encoded_data as ArrayBuffer); if(buf.byteLength < 10) @@ -472,7 +476,7 @@ function image_type(encoded_data: string | ArrayBuffer, base64_encoded?: boolean return ImageType.UNKNOWN; } -class CacheManager { +export class CacheManager { readonly cache_name: string; private _cache_category: Cache; @@ -547,7 +551,7 @@ class CacheManager { } } -class IconManager { +export class IconManager { private static cache: CacheManager = new CacheManager("icons"); handle: FileManager; @@ -590,7 +594,7 @@ class IconManager { return this.handle.requestFileList("/icons"); } - create_icon_download(id: number) : Promise { + create_icon_download(id: number) : Promise { return this.handle.download_file("", "/icon_" + id); } @@ -665,7 +669,7 @@ class IconManager { private async _load_icon(id: number) : Promise { try { - let download_key: transfer.DownloadKey; + let download_key: DownloadKey; try { download_key = await this.create_icon_download(id); } catch(error) { @@ -673,7 +677,7 @@ class IconManager { throw "Failed to request icon"; } - const downloader = transfer.spawn_download_transfer(download_key); + const downloader = spawn_download_transfer(download_key); let response: Response; try { response = await downloader.request_file(); @@ -801,14 +805,14 @@ class IconManager { } } -class Avatar { +export class Avatar { client_avatar_id: string; /* the base64 uid thing from a-m */ avatar_id: string; /* client_flag_avatar */ url: string; type: ImageType; } -class AvatarManager { +export class AvatarManager { handle: FileManager; private static cache: CacheManager; @@ -867,14 +871,14 @@ class AvatarManager { }; } - create_avatar_download(client_avatar_id: string) : Promise { + create_avatar_download(client_avatar_id: string) : Promise { log.debug(LogCategory.GENERAL, "Requesting download for avatar %s", client_avatar_id); return this.handle.download_file("", "/avatar_" + client_avatar_id); } private async _load_avatar(client_avatar_id: string, avatar_version: string) { try { - let download_key: transfer.DownloadKey; + let download_key: DownloadKey; try { download_key = await this.create_avatar_download(client_avatar_id); } catch(error) { @@ -882,7 +886,7 @@ class AvatarManager { throw "failed to request avatar download"; } - const downloader = transfer.spawn_download_transfer(download_key); + const downloader = spawn_download_transfer(download_key); let response: Response; try { response = await downloader.request_file(); diff --git a/shared/js/MessageFormatter.ts b/shared/js/MessageFormatter.ts index 66bb2d7f..462423c0 100644 --- a/shared/js/MessageFormatter.ts +++ b/shared/js/MessageFormatter.ts @@ -1,250 +1,280 @@ -namespace messages.formatter { - export namespace bbcode { - const sanitizer_escaped = (key: string) => "[-- sescaped: " + key + " --]"; - const sanitizer_escaped_regex = /\[-- sescaped: ([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}) --]/; - const sanitizer_escaped_map: {[key: string]: string} = {}; +import {Settings, settings} from "tc-shared/settings"; +import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; +import {copy_to_clipboard} from "tc-shared/utils/helpers"; +import {guid} from "tc-shared/crypto/uid"; +import * as loader from "tc-loader"; +import * as image_preview from "./ui/frames/image_preview" - const yt_url_regex = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/; +declare const xbbcode; +export namespace bbcode { + const sanitizer_escaped = (key: string) => "[-- sescaped: " + key + " --]"; + const sanitizer_escaped_regex = /\[-- sescaped: ([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}) --]/; + const sanitizer_escaped_map: {[key: string]: string} = {}; - export interface FormatSettings { - is_chat_message?: boolean - } + const yt_url_regex = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/; - export function format(message: string, fsettings?: FormatSettings) : JQuery[] { - fsettings = fsettings || {}; - - single_url_parse: - if(fsettings.is_chat_message) { - /* try if its only one url */ - const raw_url = message.replace(/\[url(=\S+)?](\S+)\[\/url]/, "$2"); - let url: URL; - try { - url = new URL(raw_url); - } catch(error) { - break single_url_parse; - } - - single_url_yt: - { - const result = raw_url.match(yt_url_regex); - if(!result) break single_url_yt; - - return format("[yt]https://www.youtube.com/watch?v=" + result[5] + "[/yt]"); - } - - single_url_image: - { - const ext_index = url.pathname.lastIndexOf("."); - if(ext_index == -1) break single_url_image; - - const ext_name = url.pathname.substr(ext_index + 1).toLowerCase(); - if([ - "jpeg", "jpg", - "png", "bmp", "gif", - "tiff", "pdf", "svg" - ].findIndex(e => e === ext_name) == -1) break single_url_image; - - return format("[img]" + message + "[/img]"); - } - } - - const result = xbbcode.parse(message, { - tag_whitelist: [ - "b", "big", - "i", "italic", - "u", "underlined", - "s", "strikethrough", - "color", - "url", - "code", - "i-code", "icode", - "sub", "sup", - "size", - "hr", "br", - - "ul", "ol", "list", - "li", - - "table", - "tr", "td", "th", - - "yt", "youtube", - "img" - ] - }); - let html = result.build_html(); - if(typeof(window.twemoji) !== "undefined" && settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES)) - html = twemoji.parse(html); - - const container = $.spawn("div"); - let sanitized = DOMPurify.sanitize(html, { - ADD_ATTR: [ - "x-highlight-type", - "x-code-type", - "x-image-url" - ] - }); - - sanitized = sanitized.replace(sanitizer_escaped_regex, data => { - const uid = data.match(sanitizer_escaped_regex)[1]; - const value = sanitizer_escaped_map[uid]; - if(!value) return data; - delete sanitizer_escaped_map[uid]; - - return value; - }); - - container[0].innerHTML = sanitized; - - - container.find("a") - .attr('target', "_blank") - .on('contextmenu', event => { - if(event.isDefaultPrevented()) return; - event.preventDefault(); - - const url = $(event.target).attr("href"); - contextmenu.spawn_context_menu(event.pageX, event.pageY, { - callback: () => { - const win = window.open(url, '_blank'); - win.focus(); - }, - name: tr("Open URL"), - type: contextmenu.MenuEntryType.ENTRY, - icon_class: "client-browse-addon-online" - }, { - callback: () => { - //TODO - }, - name: tr("Open URL in Browser"), - type: contextmenu.MenuEntryType.ENTRY, - visible: !app.is_web() && false // Currently not possible - }, contextmenu.Entry.HR(), { - callback: () => copy_to_clipboard(url), - name: tr("Copy URL to clipboard"), - type: contextmenu.MenuEntryType.ENTRY, - icon_class: "client-copy" - }); - }); - - return [container.contents() as JQuery]; - //return result.root_tag.content.map(e => e.build_html()).map((entry, idx, array) => $.spawn("a").css("display", (idx == 0 ? "inline" : "") + "block").html(entry == "" && idx != 0 ? " " : entry)); - } - - export function load_image(entry: HTMLImageElement) { - const url = decodeURIComponent(entry.getAttribute("x-image-url") || ""); - const proxy_url = "https://images.weserv.nl/?url=" + encodeURIComponent(url); - - entry.onload = undefined; - entry.src = proxy_url; - - const parent = $(entry.parentElement); - parent.on('contextmenu', event => { - contextmenu.spawn_context_menu(event.pageX, event.pageY, { - callback: () => { - const win = window.open(url, '_blank'); - win.focus(); - }, - name: tr("Open image in browser"), - type: contextmenu.MenuEntryType.ENTRY, - icon_class: "client-browse-addon-online" - }, contextmenu.Entry.HR(), { - callback: () => copy_to_clipboard(url), - name: tr("Copy image URL to clipboard"), - type: contextmenu.MenuEntryType.ENTRY, - icon_class: "client-copy" - }) - }); - parent.css("cursor", "pointer").on('click', event => image_preview.preview_image(proxy_url, url)); - } - - loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { - name: "XBBCode code tag init", - function: async () => { - /* override default parser */ - xbbcode.register.register_parser({ - tag: ["code", "icode", "i-code"], - content_tags_whitelist: [], - - build_html(layer) : string { - const klass = layer.tag_normalized != 'code' ? "tag-hljs-inline-code" : "tag-hljs-code"; - const language = (layer.options || "").replace("\"", "'").toLowerCase(); - - /* remove heading empty lines */ - let text = layer.content.map(e => e.build_text()) - .reduce((a, b) => a.length == 0 && b.replace(/[ \n\r\t]+/g, "").length == 0 ? "" : a + b, "") - .replace(/^([ \n\r\t]*)(?=\n)+/g, ""); - if(text.startsWith("\r") || text.startsWith("\n")) - text = text.substr(1); - - let result: HighlightJSResult; - if(window.hljs.getLanguage(language)) - result = window.hljs.highlight(language, text, true); - else - result = window.hljs.highlightAuto(text); - - let html = '
';
-                        html += '';
-                        html += result.value;
-                        return html + "
"; - } - }); - - /* override the yt parser */ - const original_parser = xbbcode.register.find_parser("yt"); - if(original_parser) - xbbcode.register.register_parser({ - tag: ["yt", "youtube"], - build_html(layer): string { - const result = original_parser.build_html(layer); - if(!result.startsWith(""; - return sanitizer_escaped(uid); - } - }); - - /* the image parse & displayer */ - xbbcode.register.register_parser({ - tag: ["img", "image"], - build_html(layer): string { - const uid = guid(); - const fallback_value = "[img]" + layer.build_text() + "[/img]"; - - let target; - let content = layer.content.map(e => e.build_text()).join(""); - if (!layer.options) { - target = content; - } else - target = layer.options; - - let url: URL; - try { - url = new URL(target); - if(!url.hostname) throw ""; - } catch(error) { - return fallback_value; - } - - sanitizer_escaped_map[uid] = "
"; - return sanitizer_escaped(uid); - } - }) - }, - priority: 10 - }); + export interface FormatSettings { + is_chat_message?: boolean } - export function sanitize_text(text: string) : string { - return $(DOMPurify.sanitize("" + text + "", { + export function format(message: string, fsettings?: FormatSettings) : JQuery[] { + fsettings = fsettings || {}; + + single_url_parse: + if(fsettings.is_chat_message) { + /* try if its only one url */ + const raw_url = message.replace(/\[url(=\S+)?](\S+)\[\/url]/, "$2"); + let url: URL; + try { + url = new URL(raw_url); + } catch(error) { + break single_url_parse; + } + + single_url_yt: + { + const result = raw_url.match(yt_url_regex); + if(!result) break single_url_yt; + + return format("[yt]https://www.youtube.com/watch?v=" + result[5] + "[/yt]"); + } + + single_url_image: + { + const ext_index = url.pathname.lastIndexOf("."); + if(ext_index == -1) break single_url_image; + + const ext_name = url.pathname.substr(ext_index + 1).toLowerCase(); + if([ + "jpeg", "jpg", + "png", "bmp", "gif", + "tiff", "pdf", "svg" + ].findIndex(e => e === ext_name) == -1) break single_url_image; + + return format("[img]" + message + "[/img]"); + } + } + + const result = xbbcode.parse(message, { + tag_whitelist: [ + "b", "big", + "i", "italic", + "u", "underlined", + "s", "strikethrough", + "color", + "url", + "code", + "i-code", "icode", + "sub", "sup", + "size", + "hr", "br", + + "ul", "ol", "list", + "li", + + "table", + "tr", "td", "th", + + "yt", "youtube", + "img" + ] + }); + let html = result.build_html(); + if(typeof(window.twemoji) !== "undefined" && settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES)) + html = twemoji.parse(html); + + const container = $.spawn("div"); + let sanitized = DOMPurify.sanitize(html, { ADD_ATTR: [ "x-highlight-type", "x-code-type", "x-image-url" ] - })).text(); + }); + + sanitized = sanitized.replace(sanitizer_escaped_regex, data => { + const uid = data.match(sanitizer_escaped_regex)[1]; + const value = sanitizer_escaped_map[uid]; + if(!value) return data; + delete sanitizer_escaped_map[uid]; + + return value; + }); + + container[0].innerHTML = sanitized; + + + container.find("a") + .attr('target', "_blank") + .on('contextmenu', event => { + if(event.isDefaultPrevented()) return; + event.preventDefault(); + + const url = $(event.target).attr("href"); + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + callback: () => { + const win = window.open(url, '_blank'); + win.focus(); + }, + name: tr("Open URL"), + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-browse-addon-online" + }, { + callback: () => { + //TODO + }, + name: tr("Open URL in Browser"), + type: contextmenu.MenuEntryType.ENTRY, + visible: loader.version().type === "native" && false // Currently not possible + }, contextmenu.Entry.HR(), { + callback: () => copy_to_clipboard(url), + name: tr("Copy URL to clipboard"), + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-copy" + }); + }); + + return [container.contents() as JQuery]; + //return result.root_tag.content.map(e => e.build_html()).map((entry, idx, array) => $.spawn("a").css("display", (idx == 0 ? "inline" : "") + "block").html(entry == "" && idx != 0 ? " " : entry)); } + + export function load_image(entry: HTMLImageElement) { + const url = decodeURIComponent(entry.getAttribute("x-image-url") || ""); + const proxy_url = "https://images.weserv.nl/?url=" + encodeURIComponent(url); + + entry.onload = undefined; + entry.src = proxy_url; + + const parent = $(entry.parentElement); + parent.on('contextmenu', event => { + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + callback: () => { + const win = window.open(url, '_blank'); + win.focus(); + }, + name: tr("Open image in browser"), + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-browse-addon-online" + }, contextmenu.Entry.HR(), { + callback: () => copy_to_clipboard(url), + name: tr("Copy image URL to clipboard"), + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-copy" + }) + }); + parent.css("cursor", "pointer").on('click', event => image_preview.preview_image(proxy_url, url)); + } + + loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { + name: "XBBCode code tag init", + function: async () => { + /* override default parser */ + xbbcode.register.register_parser({ + tag: ["code", "icode", "i-code"], + content_tags_whitelist: [], + + build_html(layer) : string { + const klass = layer.tag_normalized != 'code' ? "tag-hljs-inline-code" : "tag-hljs-code"; + const language = (layer.options || "").replace("\"", "'").toLowerCase(); + + /* remove heading empty lines */ + let text = layer.content.map(e => e.build_text()) + .reduce((a, b) => a.length == 0 && b.replace(/[ \n\r\t]+/g, "").length == 0 ? "" : a + b, "") + .replace(/^([ \n\r\t]*)(?=\n)+/g, ""); + if(text.startsWith("\r") || text.startsWith("\n")) + text = text.substr(1); + + let result: HighlightJSResult; + if(window.hljs.getLanguage(language)) + result = window.hljs.highlight(language, text, true); + else + result = window.hljs.highlightAuto(text); + + let html = '
';
+                    html += '';
+                    html += result.value;
+                    return html + "
"; + } + }); + + /* override the yt parser */ + const original_parser = xbbcode.register.find_parser("yt"); + if(original_parser) + xbbcode.register.register_parser({ + tag: ["yt", "youtube"], + build_html(layer): string { + const result = original_parser.build_html(layer); + if(!result.startsWith(""; + return sanitizer_escaped(uid); + } + }); + + /* the image parse & displayer */ + xbbcode.register.register_parser({ + tag: ["img", "image"], + build_html(layer): string { + const uid = guid(); + const fallback_value = "[img]" + layer.build_text() + "[/img]"; + + let target; + let content = layer.content.map(e => e.build_text()).join(""); + if (!layer.options) { + target = content; + } else + target = layer.options; + + let url: URL; + try { + url = new URL(target); + if(!url.hostname) throw ""; + } catch(error) { + return fallback_value; + } + + sanitizer_escaped_map[uid] = "
"; + return sanitizer_escaped(uid); + } + }) + }, + priority: 10 + }); +} + +export function sanitize_text(text: string) : string { + return $(DOMPurify.sanitize("" + text + "", { + ADD_ATTR: [ + "x-highlight-type", + "x-code-type", + "x-image-url" + ] + })).text(); +} + +export function formatDate(secs: number) : string { + let years = Math.floor(secs / (60 * 60 * 24 * 365)); + let days = Math.floor(secs / (60 * 60 * 24)) % 365; + let hours = Math.floor(secs / (60 * 60)) % 24; + let minutes = Math.floor(secs / 60) % 60; + let seconds = Math.floor(secs % 60); + + let result = ""; + if(years > 0) + result += years + " " + tr("years") + " "; + if(years > 0 || days > 0) + result += days + " " + tr("days") + " "; + if(years > 0 || days > 0 || hours > 0) + result += hours + " " + tr("hours") + " "; + if(years > 0 || days > 0 || hours > 0 || minutes > 0) + result += minutes + " " + tr("minutes") + " "; + if(years > 0 || days > 0 || hours > 0 || minutes > 0 || seconds > 0) + result += seconds + " " + tr("seconds") + " "; + else + result = tr("now") + " "; + + return result.substr(0, result.length - 1); } \ No newline at end of file diff --git a/shared/js/PPTListener.ts b/shared/js/PPTListener.ts index b0d32f5d..d0439914 100644 --- a/shared/js/PPTListener.ts +++ b/shared/js/PPTListener.ts @@ -1,4 +1,4 @@ -enum KeyCode { +export enum KeyCode { KEY_CANCEL = 3, KEY_HELP = 6, KEY_BACK_SPACE = 8, @@ -118,59 +118,57 @@ enum KeyCode { KEY_META = 224 } -namespace ppt { - export enum EventType { - KEY_PRESS, - KEY_RELEASE, - KEY_TYPED - } +export enum EventType { + KEY_PRESS, + KEY_RELEASE, + KEY_TYPED +} - export enum SpecialKey { - CTRL, - WINDOWS, - SHIFT, - ALT - } +export enum SpecialKey { + CTRL, + WINDOWS, + SHIFT, + ALT +} - export interface KeyDescriptor { - key_code: string; +export interface KeyDescriptor { + key_code: string; - key_ctrl: boolean; - key_windows: boolean; - key_shift: boolean; - key_alt: boolean; - } + key_ctrl: boolean; + key_windows: boolean; + key_shift: boolean; + key_alt: boolean; +} - export interface KeyEvent extends KeyDescriptor { - readonly type: EventType; +export interface KeyEvent extends KeyDescriptor { + readonly type: EventType; - readonly key: string; - } + readonly key: string; +} - export interface KeyHook extends KeyDescriptor { - cancel: boolean; +export interface KeyHook extends KeyDescriptor { + cancel: boolean; - callback_press: () => any; - callback_release: () => any; - } + callback_press: () => any; + callback_release: () => any; +} - export function key_description(key: KeyDescriptor) { - let result = ""; - if(key.key_shift) - result += " + " + tr("Shift"); - if(key.key_alt) - result += " + " + tr("Alt"); - if(key.key_ctrl) - result += " + " + tr("CTRL"); - if(key.key_windows) - result += " + " + tr("Win"); +export function key_description(key: KeyDescriptor) { + let result = ""; + if(key.key_shift) + result += " + " + tr("Shift"); + if(key.key_alt) + result += " + " + tr("Alt"); + if(key.key_ctrl) + result += " + " + tr("CTRL"); + if(key.key_windows) + result += " + " + tr("Win"); - if(!result && !key.key_code) - return tr("unset"); + if(!result && !key.key_code) + return tr("unset"); - if(key.key_code) - result += " + " + key.key_code; - return result.substr(3); - } + if(key.key_code) + result += " + " + key.key_code; + return result.substr(3); } \ No newline at end of file diff --git a/shared/js/audio/audio.ts b/shared/js/audio/audio.ts deleted file mode 100644 index 4314f775..00000000 --- a/shared/js/audio/audio.ts +++ /dev/null @@ -1,10 +0,0 @@ -namespace audio { - export namespace player { - export interface Device { - device_id: string; - - driver: string; - name: string; - } - } -} \ No newline at end of file diff --git a/shared/js/audio/player.ts b/shared/js/audio/player.ts new file mode 100644 index 00000000..0afb5f7c --- /dev/null +++ b/shared/js/audio/player.ts @@ -0,0 +1,6 @@ +export interface Device { + device_id: string; + + driver: string; + name: string; +} \ No newline at end of file diff --git a/shared/js/bookmarks.ts b/shared/js/bookmarks.ts index cb12ca5e..c16a79bf 100644 --- a/shared/js/bookmarks.ts +++ b/shared/js/bookmarks.ts @@ -1,262 +1,260 @@ -namespace bookmarks { - function guid() { - function s4() { - return Math - .floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); - } - return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import {guid} from "tc-shared/crypto/uid"; +import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal"; +import {default_profile, find_profile} from "tc-shared/profiles/ConnectionProfile"; +import {server_connections} from "tc-shared/ui/frames/connection_handlers"; +import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect"; +import {control_bar} from "tc-shared/ui/frames/ControlBar"; +import * as top_menu from "./ui/frames/MenuBar"; + +export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => { + const profile = find_profile(mark.connect_profile) || default_profile(); + if(profile.valid()) { + const connection = (typeof(new_tab) !== "boolean" || !new_tab) ? server_connections.active_connection_handler() : server_connections.spawn_server_connection_handler(); + server_connections.set_active_connection_handler(connection); + connection.startConnection( + mark.server_properties.server_address + ":" + mark.server_properties.server_port, + profile, + true, + { + nickname: mark.nickname === "Another TeaSpeak user" || !mark.nickname ? profile.connect_username() : mark.nickname, + password: mark.server_properties.server_password_hash ? { + password: mark.server_properties.server_password_hash, + hashed: true + } : mark.server_properties.server_password ? { + hashed: false, + password: mark.server_properties.server_password + } : undefined + } + ); + } else { + spawnConnectModal({}, { + url: mark.server_properties.server_address + ":" + mark.server_properties.server_port, + enforce: true + }, { + profile: profile, + enforce: true + }) } +}; - export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => { - const profile = profiles.find_profile(mark.connect_profile) || profiles.default_profile(); - if(profile.valid()) { - const connection = (typeof(new_tab) !== "boolean" || !new_tab) ? server_connections.active_connection_handler() : server_connections.spawn_server_connection_handler(); - server_connections.set_active_connection_handler(connection); - connection.startConnection( - mark.server_properties.server_address + ":" + mark.server_properties.server_port, - profile, - true, - { - nickname: mark.nickname === "Another TeaSpeak user" || !mark.nickname ? profile.connect_username() : mark.nickname, - password: mark.server_properties.server_password_hash ? { - password: mark.server_properties.server_password_hash, - hashed: true - } : mark.server_properties.server_password ? { - hashed: false, - password: mark.server_properties.server_password - } : undefined - } - ); - } else { - Modals.spawnConnectModal({}, { - url: mark.server_properties.server_address + ":" + mark.server_properties.server_port, - enforce: true - }, { - profile: profile, - enforce: true - }) - } - }; +export interface ServerProperties { + server_address: string; + server_port: number; + server_password_hash?: string; + server_password?: string; +} - export interface ServerProperties { - server_address: string; - server_port: number; - server_password_hash?: string; - server_password?: string; - } +export enum BookmarkType { + ENTRY, + DIRECTORY +} - export enum BookmarkType { - ENTRY, - DIRECTORY - } +export interface Bookmark { + type: /* BookmarkType.ENTRY */ BookmarkType; + /* readonly */ parent: DirectoryBookmark; - export interface Bookmark { - type: /* BookmarkType.ENTRY */ BookmarkType; - /* readonly */ parent: DirectoryBookmark; + server_properties: ServerProperties; + display_name: string; + unique_id: string; - server_properties: ServerProperties; - display_name: string; - unique_id: string; + nickname: string; + default_channel?: number | string; + default_channel_password_hash?: string; + default_channel_password?: string; - nickname: string; - default_channel?: number | string; - default_channel_password_hash?: string; - default_channel_password?: string; + connect_profile: string; - connect_profile: string; + last_icon_id?: number; +} - last_icon_id?: number; - } +export interface DirectoryBookmark { + type: /* BookmarkType.DIRECTORY */ BookmarkType; + /* readonly */ parent: DirectoryBookmark; - export interface DirectoryBookmark { - type: /* BookmarkType.DIRECTORY */ BookmarkType; - /* readonly */ parent: DirectoryBookmark; + readonly content: (Bookmark | DirectoryBookmark)[]; + unique_id: string; + display_name: string; +} - readonly content: (Bookmark | DirectoryBookmark)[]; - unique_id: string; - display_name: string; - } +interface BookmarkConfig { + root_bookmark?: DirectoryBookmark; + default_added?: boolean; +} - interface BookmarkConfig { - root_bookmark?: DirectoryBookmark; - default_added?: boolean; - } - - let _bookmark_config: BookmarkConfig; - - function bookmark_config() : BookmarkConfig { - if(_bookmark_config) - return _bookmark_config; - - let bookmark_json = localStorage.getItem("bookmarks"); - let bookmarks; - try { - bookmarks = JSON.parse(bookmark_json) || {} as BookmarkConfig; - } catch(error) { - log.error(LogCategory.BOOKMARKS, tr("Failed to load bookmarks: %o"), error); - bookmarks = {} as any; - } - - _bookmark_config = bookmarks; - _bookmark_config.root_bookmark = _bookmark_config.root_bookmark || { content: [], display_name: "root", type: BookmarkType.DIRECTORY} as DirectoryBookmark; - - if(!_bookmark_config.default_added) { - _bookmark_config.default_added = true; - create_bookmark("TeaSpeak official Test-Server", _bookmark_config.root_bookmark, { - server_address: "ts.teaspeak.de", - server_port: 9987 - }, undefined); - - save_config(); - } - - const fix_parent = (parent: DirectoryBookmark, entry: Bookmark | DirectoryBookmark) => { - entry.parent = parent; - if(entry.type === BookmarkType.DIRECTORY) - for(const child of (entry as DirectoryBookmark).content) - fix_parent(entry as DirectoryBookmark, child); - }; - for(const entry of _bookmark_config.root_bookmark.content) - fix_parent(_bookmark_config.root_bookmark, entry); +let _bookmark_config: BookmarkConfig; +function bookmark_config() : BookmarkConfig { + if(_bookmark_config) return _bookmark_config; + + let bookmark_json = localStorage.getItem("bookmarks"); + let bookmarks; + try { + bookmarks = JSON.parse(bookmark_json) || {} as BookmarkConfig; + } catch(error) { + log.error(LogCategory.BOOKMARKS, tr("Failed to load bookmarks: %o"), error); + bookmarks = {} as any; } - function save_config() { - localStorage.setItem("bookmarks", JSON.stringify(bookmark_config(), (key, value) => { - if(key === "parent") - return undefined; - return value; - })); + _bookmark_config = bookmarks; + _bookmark_config.root_bookmark = _bookmark_config.root_bookmark || { content: [], display_name: "root", type: BookmarkType.DIRECTORY} as DirectoryBookmark; + + if(!_bookmark_config.default_added) { + _bookmark_config.default_added = true; + create_bookmark("TeaSpeak official Test-Server", _bookmark_config.root_bookmark, { + server_address: "ts.teaspeak.de", + server_port: 9987 + }, undefined); + + save_config(); } - export function bookmarks() : DirectoryBookmark { - return bookmark_config().root_bookmark; - } + const fix_parent = (parent: DirectoryBookmark, entry: Bookmark | DirectoryBookmark) => { + entry.parent = parent; + if(entry.type === BookmarkType.DIRECTORY) + for(const child of (entry as DirectoryBookmark).content) + fix_parent(entry as DirectoryBookmark, child); + }; + for(const entry of _bookmark_config.root_bookmark.content) + fix_parent(_bookmark_config.root_bookmark, entry); - export function bookmarks_flat() : Bookmark[] { - const result: Bookmark[] = []; - const _flat = (bookmark: Bookmark | DirectoryBookmark) => { - if(bookmark.type == BookmarkType.DIRECTORY) - for(const book of (bookmark as DirectoryBookmark).content) - _flat(book); - else - result.push(bookmark as Bookmark); - }; - _flat(bookmark_config().root_bookmark); - return result; - } + return _bookmark_config; +} - function find_bookmark_recursive(parent: DirectoryBookmark, uuid: string) : Bookmark | DirectoryBookmark { - for(const entry of parent.content) { - if(entry.unique_id == uuid) - return entry; - if(entry.type == BookmarkType.DIRECTORY) { - const result = find_bookmark_recursive(entry as DirectoryBookmark, uuid); - if(result) return result; - } - } - return undefined; - } +function save_config() { + localStorage.setItem("bookmarks", JSON.stringify(bookmark_config(), (key, value) => { + if(key === "parent") + return undefined; + return value; + })); +} - export function find_bookmark(uuid: string) : Bookmark | DirectoryBookmark | undefined { - return find_bookmark_recursive(bookmarks(), uuid); - } +export function bookmarks() : DirectoryBookmark { + return bookmark_config().root_bookmark; +} - export function parent_bookmark(bookmark: Bookmark) : DirectoryBookmark { - const books: (DirectoryBookmark | Bookmark)[] = [bookmarks()]; - while(!books.length) { - const directory = books.pop_front(); - if(directory.type == BookmarkType.DIRECTORY) { - const cast = directory; - - if(cast.content.indexOf(bookmark) != -1) - return cast; - books.push(...cast.content); - } - } - return bookmarks(); - } - - export function create_bookmark(display_name: string, directory: DirectoryBookmark, server_properties: ServerProperties, nickname: string) : Bookmark { - const bookmark = { - display_name: display_name, - server_properties: server_properties, - nickname: nickname, - type: BookmarkType.ENTRY, - connect_profile: "default", - unique_id: guid(), - parent: directory - } as Bookmark; - - directory.content.push(bookmark); - return bookmark; - } - - export function create_bookmark_directory(parent: DirectoryBookmark, name: string) : DirectoryBookmark { - const bookmark = { - type: BookmarkType.DIRECTORY, - - display_name: name, - content: [], - unique_id: guid(), - parent: parent - } as DirectoryBookmark; - - parent.content.push(bookmark); - return bookmark; - } - - //TODO test if the new parent is within the old bookmark - export function change_directory(parent: DirectoryBookmark, bookmark: Bookmark | DirectoryBookmark) { - delete_bookmark(bookmark); - parent.content.push(bookmark) - } - - export function save_bookmark(bookmark?: Bookmark | DirectoryBookmark) { - save_config(); /* nvm we dont give a fuck... saving everything */ - } - - function delete_bookmark_recursive(parent: DirectoryBookmark, bookmark: Bookmark | DirectoryBookmark) { - const index = parent.content.indexOf(bookmark); - if(index != -1) - parent.content.remove(bookmark); +export function bookmarks_flat() : Bookmark[] { + const result: Bookmark[] = []; + const _flat = (bookmark: Bookmark | DirectoryBookmark) => { + if(bookmark.type == BookmarkType.DIRECTORY) + for(const book of (bookmark as DirectoryBookmark).content) + _flat(book); else - for(const entry of parent.content) - if(entry.type == BookmarkType.DIRECTORY) - delete_bookmark_recursive(entry as DirectoryBookmark, bookmark) - } + result.push(bookmark as Bookmark); + }; + _flat(bookmark_config().root_bookmark); + return result; +} - export function delete_bookmark(bookmark: Bookmark | DirectoryBookmark) { - delete_bookmark_recursive(bookmarks(), bookmark) - } - - export function add_current_server() { - const ch = server_connections.active_connection_handler(); - if(ch && ch.connected) { - const ce = ch.getClient(); - const name = ce ? ce.clientNickName() : undefined; - createInputModal(tr("Enter bookmarks name"), tr("Please enter the bookmarks name:
"), text => text.length > 0, result => { - if(result) { - const bookmark = create_bookmark(result as string, bookmarks(), { - server_port: ch.serverConnection.remote_address().port, - server_address: ch.serverConnection.remote_address().host, - - server_password: "", - server_password_hash: "" - }, name); - save_bookmark(bookmark); - - control_bar.update_bookmarks(); - top_menu.rebuild_bookmarks(); - - createInfoModal(tr("Server added"), tr("Server has been successfully added to your bookmarks.")).open(); - } - }).open(); - } else { - createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open(); +function find_bookmark_recursive(parent: DirectoryBookmark, uuid: string) : Bookmark | DirectoryBookmark { + for(const entry of parent.content) { + if(entry.unique_id == uuid) + return entry; + if(entry.type == BookmarkType.DIRECTORY) { + const result = find_bookmark_recursive(entry as DirectoryBookmark, uuid); + if(result) return result; } } + return undefined; +} + +export function find_bookmark(uuid: string) : Bookmark | DirectoryBookmark | undefined { + return find_bookmark_recursive(bookmarks(), uuid); +} + +export function parent_bookmark(bookmark: Bookmark) : DirectoryBookmark { + const books: (DirectoryBookmark | Bookmark)[] = [bookmarks()]; + while(!books.length) { + const directory = books.pop_front(); + if(directory.type == BookmarkType.DIRECTORY) { + const cast = directory; + + if(cast.content.indexOf(bookmark) != -1) + return cast; + books.push(...cast.content); + } + } + return bookmarks(); +} + +export function create_bookmark(display_name: string, directory: DirectoryBookmark, server_properties: ServerProperties, nickname: string) : Bookmark { + const bookmark = { + display_name: display_name, + server_properties: server_properties, + nickname: nickname, + type: BookmarkType.ENTRY, + connect_profile: "default", + unique_id: guid(), + parent: directory + } as Bookmark; + + directory.content.push(bookmark); + return bookmark; +} + +export function create_bookmark_directory(parent: DirectoryBookmark, name: string) : DirectoryBookmark { + const bookmark = { + type: BookmarkType.DIRECTORY, + + display_name: name, + content: [], + unique_id: guid(), + parent: parent + } as DirectoryBookmark; + + parent.content.push(bookmark); + return bookmark; +} + +//TODO test if the new parent is within the old bookmark +export function change_directory(parent: DirectoryBookmark, bookmark: Bookmark | DirectoryBookmark) { + delete_bookmark(bookmark); + parent.content.push(bookmark) +} + +export function save_bookmark(bookmark?: Bookmark | DirectoryBookmark) { + save_config(); /* nvm we dont give a fuck... saving everything */ +} + +function delete_bookmark_recursive(parent: DirectoryBookmark, bookmark: Bookmark | DirectoryBookmark) { + const index = parent.content.indexOf(bookmark); + if(index != -1) + parent.content.remove(bookmark); + else + for(const entry of parent.content) + if(entry.type == BookmarkType.DIRECTORY) + delete_bookmark_recursive(entry as DirectoryBookmark, bookmark) +} + +export function delete_bookmark(bookmark: Bookmark | DirectoryBookmark) { + delete_bookmark_recursive(bookmarks(), bookmark) +} + +export function add_current_server() { + const ch = server_connections.active_connection_handler(); + if(ch && ch.connected) { + const ce = ch.getClient(); + const name = ce ? ce.clientNickName() : undefined; + createInputModal(tr("Enter bookmarks name"), tr("Please enter the bookmarks name:
"), text => text.length > 0, result => { + if(result) { + const bookmark = create_bookmark(result as string, bookmarks(), { + server_port: ch.serverConnection.remote_address().port, + server_address: ch.serverConnection.remote_address().host, + + server_password: "", + server_password_hash: "" + }, name); + save_bookmark(bookmark); + + control_bar.update_bookmarks(); + top_menu.rebuild_bookmarks(); + + createInfoModal(tr("Server added"), tr("Server has been successfully added to your bookmarks.")).open(); + } + }).open(); + } else { + createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open(); + } } \ No newline at end of file diff --git a/shared/js/connection/AbstractCommandHandler.ts b/shared/js/connection/AbstractCommandHandler.ts new file mode 100644 index 00000000..c545574c --- /dev/null +++ b/shared/js/connection/AbstractCommandHandler.ts @@ -0,0 +1,98 @@ +import { + AbstractServerConnection, + ServerCommand, + SingleCommandHandler +} from "tc-shared/connection/ConnectionBase"; + +export abstract class AbstractCommandHandler { + readonly connection: AbstractServerConnection; + + handler_boss: AbstractCommandHandlerBoss | undefined; + volatile_handler_boss: boolean = false; /* if true than the command handler could be registered twice to two or more handlers */ + + ignore_consumed: boolean = false; + + protected constructor(connection: AbstractServerConnection) { + this.connection = connection; + } + + /** + * @return If the command should be consumed + */ + abstract handle_command(command: ServerCommand) : boolean; +} + +export abstract class AbstractCommandHandlerBoss { + readonly connection: AbstractServerConnection; + protected command_handlers: AbstractCommandHandler[] = []; + /* TODO: Timeout */ + protected single_command_handler: SingleCommandHandler[] = []; + + protected constructor(connection: AbstractServerConnection) { + this.connection = connection; + } + + destroy() { + this.command_handlers = undefined; + this.single_command_handler = undefined; + } + + register_handler(handler: AbstractCommandHandler) { + if(!handler.volatile_handler_boss && handler.handler_boss) + throw "handler already registered"; + + this.command_handlers.remove(handler); /* just to be sure */ + this.command_handlers.push(handler); + handler.handler_boss = this; + } + + unregister_handler(handler: AbstractCommandHandler) { + if(!handler.volatile_handler_boss && handler.handler_boss !== this) { + console.warn(tr("Tried to unregister command handler which does not belong to the handler boss")); + return; + } + + this.command_handlers.remove(handler); + handler.handler_boss = undefined; + } + + + register_single_handler(handler: SingleCommandHandler) { + this.single_command_handler.push(handler); + } + + remove_single_handler(handler: SingleCommandHandler) { + this.single_command_handler.remove(handler); + } + + handlers() : AbstractCommandHandler[] { + return this.command_handlers; + } + + invoke_handle(command: ServerCommand) : boolean { + let flag_consumed = false; + + for(const handler of this.command_handlers) { + try { + if(!flag_consumed || handler.ignore_consumed) + flag_consumed = flag_consumed || handler.handle_command(command); + } catch(error) { + console.error(tr("Failed to invoke command handler. Invocation results in an exception: %o"), error); + } + } + + for(const handler of [...this.single_command_handler]) { + if(handler.command && handler.command != command.command) + continue; + + try { + if(handler.function(command)) + this.single_command_handler.remove(handler); + } catch(error) { + console.error(tr("Failed to invoke single command handler. Invocation results in an exception: %o"), error); + } + } + + return flag_consumed; + } +} \ No newline at end of file diff --git a/shared/js/connection/CommandHandler.ts b/shared/js/connection/CommandHandler.ts index f5e4bfed..1620d8b5 100644 --- a/shared/js/connection/CommandHandler.ts +++ b/shared/js/connection/CommandHandler.ts @@ -1,309 +1,465 @@ -/// +import * as log from "tc-shared/log"; +import * as server_log from "tc-shared/ui/frames/server_log"; +import { + AbstractServerConnection, CommandOptions, ServerCommand +} from "tc-shared/connection/ConnectionBase"; +import {Sound} from "tc-shared/sound/Sounds"; +import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; +import {LogCategory} from "tc-shared/log"; +import {createErrorModal, createInfoModal, createInputModal, createModal} from "tc-shared/ui/elements/Modal"; +import { + ClientConnectionInfo, + ClientEntry, + ClientType, + LocalClientEntry, + MusicClientEntry, + SongInfo +} from "tc-shared/ui/client"; +import {ChannelEntry} from "tc-shared/ui/channel"; +import {ConnectionHandler, DisconnectReason, ViewReasonId} from "tc-shared/ConnectionHandler"; +import {bbcode_chat, formatMessage} from "tc-shared/ui/frames/chat"; +import {server_connections} from "tc-shared/ui/frames/connection_handlers"; +import {spawnPoke} from "tc-shared/ui/modal/ModalPoke"; +import {PrivateConversationState} from "tc-shared/ui/frames/side/private_conversations"; +import {Conversation} from "tc-shared/ui/frames/side/conversations"; +import {AbstractCommandHandler, AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler"; -namespace connection { - import Conversation = chat.channel.Conversation; - import MusicInfo = chat.MusicInfo; +export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss { + constructor(connection: AbstractServerConnection) { + super(connection); + } +} - export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss { - constructor(connection: AbstractServerConnection) { - super(connection); +export class ConnectionCommandHandler extends AbstractCommandHandler { + readonly connection: AbstractServerConnection; + readonly connection_handler: ConnectionHandler; + + constructor(connection: AbstractServerConnection) { + super(connection); + this.connection_handler = connection.client; + + this["error"] = this.handleCommandResult; + this["channellist"] = this.handleCommandChannelList; + this["channellistfinished"] = this.handleCommandChannelListFinished; + this["notifychannelcreated"] = this.handleCommandChannelCreate; + this["notifychanneldeleted"] = this.handleCommandChannelDelete; + this["notifychannelhide"] = this.handleCommandChannelHide; + this["notifychannelshow"] = this.handleCommandChannelShow; + + this["notifyserverconnectioninfo"] = this.handleNotifyServerConnectionInfo; + this["notifyconnectioninfo"] = this.handleNotifyConnectionInfo; + + this["notifycliententerview"] = this.handleCommandClientEnterView; + this["notifyclientleftview"] = this.handleCommandClientLeftView; + this["notifyclientmoved"] = this.handleNotifyClientMoved; + this["initserver"] = this.handleCommandServerInit; + this["notifychannelmoved"] = this.handleNotifyChannelMoved; + this["notifychanneledited"] = this.handleNotifyChannelEdited; + this["notifytextmessage"] = this.handleNotifyTextMessage; + this["notifyclientchatcomposing"] = this.notifyClientChatComposing; + this["notifyclientchatclosed"] = this.handleNotifyClientChatClosed; + this["notifyclientupdated"] = this.handleNotifyClientUpdated; + this["notifyserveredited"] = this.handleNotifyServerEdited; + this["notifyserverupdated"] = this.handleNotifyServerUpdated; + + this["notifyclientpoke"] = this.handleNotifyClientPoke; + + this["notifymusicplayerinfo"] = this.handleNotifyMusicPlayerInfo; + + this["notifyservergroupclientadded"] = this.handleNotifyServerGroupClientAdd; + this["notifyservergroupclientdeleted"] = this.handleNotifyServerGroupClientRemove; + this["notifyclientchannelgroupchanged"] = this.handleNotifyClientChannelGroupChanged; + + this["notifychannelsubscribed"] = this.handleNotifyChannelSubscribed; + this["notifychannelunsubscribed"] = this.handleNotifyChannelUnsubscribed; + + this["notifyconversationhistory"] = this.handleNotifyConversationHistory; + this["notifyconversationmessagedelete"] = this.handleNotifyConversationMessageDelete; + + this["notifymusicstatusupdate"] = this.handleNotifyMusicStatusUpdate; + this["notifymusicplayersongchange"] = this.handleMusicPlayerSongChange; + + this["notifyplaylistsongadd"] = this.handleNotifyPlaylistSongAdd; + this["notifyplaylistsongremove"] = this.handleNotifyPlaylistSongRemove; + this["notifyplaylistsongreorder"] = this.handleNotifyPlaylistSongReorder; + this["notifyplaylistsongloaded"] = this.handleNotifyPlaylistSongLoaded; + } + + private loggable_invoker(unique_id, client_id, name) : server_log.base.Client | undefined { + const id = parseInt(client_id); + if(typeof(client_id) === "undefined" || Number.isNaN(id)) + return undefined; + + if(id == 0) + return { + client_id: 0, + client_unique_id: this.connection_handler.channelTree.server.properties.virtualserver_unique_identifier, + client_name: this.connection_handler.channelTree.server.properties.virtualserver_name, + }; + + return { + client_unique_id: unique_id, + client_name: name, + client_id: client_id + }; + } + + proxy_command_promise(promise: Promise, options: CommandOptions) { + if(!options.process_result) + return promise; + + return promise.catch(ex => { + if(options.process_result) { + if(ex instanceof CommandResult) { + let res = ex; + if(!res.success) { + if(res.id == ErrorID.PERMISSION_ERROR) { //Permission error + const permission = this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number); + res.message = tr("Insufficient client permissions. Failed on permission ") + (permission ? permission.name : "unknown"); + this.connection_handler.log.log(server_log.Type.ERROR_PERMISSION, { + permission: this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number) + }); + this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); + } else if(res.id != ErrorID.EMPTY_RESULT) { + this.connection_handler.log.log(server_log.Type.ERROR_CUSTOM, { + message: res.extra_message.length == 0 ? res.message : res.extra_message + }); + } + } + } else if(typeof(ex) === "string") { + this.connection_handler.log.log(server_log.Type.CONNECTION_COMMAND_ERROR, {error: ex}); + } else { + log.error(LogCategory.NETWORKING, tr("Invalid promise result type: %s. Result: %o"), typeof (ex), ex); + } + } + + return Promise.reject(ex); + }); + } + + handle_command(command: ServerCommand) : boolean { + if(this[command.command]) { + this[command.command](command.arguments); + return true; + } + + return false; + } + + set_handler(command: string, handler: any) { + this[command] = handler; + } + + unset_handler(command: string, handler?: any) { + if(handler && this[command] != handler) return; + this[command] = undefined; + } + + handleCommandResult(json) { + json = json[0]; //Only one bulk + + let code : string = json["return_code"]; + if(!code || code.length == 0) { + log.warn(LogCategory.NETWORKING, tr("Invalid return code! (%o)"), json); + return; + } + let retListeners = this.connection["_retListener"]; + + for(let e of retListeners) { + if(e.code != code) continue; + retListeners.remove(e); + let result = new CommandResult(json); + if(result.success) + e.resolve(result); + else + e.reject(result); + break; } } - export class ConnectionCommandHandler extends AbstractCommandHandler { - readonly connection: AbstractServerConnection; - readonly connection_handler: ConnectionHandler; - - constructor(connection: AbstractServerConnection) { - super(connection); - this.connection_handler = connection.client; - - this["error"] = this.handleCommandResult; - this["channellist"] = this.handleCommandChannelList; - this["channellistfinished"] = this.handleCommandChannelListFinished; - this["notifychannelcreated"] = this.handleCommandChannelCreate; - this["notifychanneldeleted"] = this.handleCommandChannelDelete; - this["notifychannelhide"] = this.handleCommandChannelHide; - this["notifychannelshow"] = this.handleCommandChannelShow; - - this["notifyserverconnectioninfo"] = this.handleNotifyServerConnectionInfo; - this["notifyconnectioninfo"] = this.handleNotifyConnectionInfo; - - this["notifycliententerview"] = this.handleCommandClientEnterView; - this["notifyclientleftview"] = this.handleCommandClientLeftView; - this["notifyclientmoved"] = this.handleNotifyClientMoved; - this["initserver"] = this.handleCommandServerInit; - this["notifychannelmoved"] = this.handleNotifyChannelMoved; - this["notifychanneledited"] = this.handleNotifyChannelEdited; - this["notifytextmessage"] = this.handleNotifyTextMessage; - this["notifyclientchatcomposing"] = this.notifyClientChatComposing; - this["notifyclientchatclosed"] = this.handleNotifyClientChatClosed; - this["notifyclientupdated"] = this.handleNotifyClientUpdated; - this["notifyserveredited"] = this.handleNotifyServerEdited; - this["notifyserverupdated"] = this.handleNotifyServerUpdated; - - this["notifyclientpoke"] = this.handleNotifyClientPoke; - - this["notifymusicplayerinfo"] = this.handleNotifyMusicPlayerInfo; - - this["notifyservergroupclientadded"] = this.handleNotifyServerGroupClientAdd; - this["notifyservergroupclientdeleted"] = this.handleNotifyServerGroupClientRemove; - this["notifyclientchannelgroupchanged"] = this.handleNotifyClientChannelGroupChanged; - - this["notifychannelsubscribed"] = this.handleNotifyChannelSubscribed; - this["notifychannelunsubscribed"] = this.handleNotifyChannelUnsubscribed; - - this["notifyconversationhistory"] = this.handleNotifyConversationHistory; - this["notifyconversationmessagedelete"] = this.handleNotifyConversationMessageDelete; - - this["notifymusicstatusupdate"] = this.handleNotifyMusicStatusUpdate; - this["notifymusicplayersongchange"] = this.handleMusicPlayerSongChange; - - this["notifyplaylistsongadd"] = this.handleNotifyPlaylistSongAdd; - this["notifyplaylistsongremove"] = this.handleNotifyPlaylistSongRemove; - this["notifyplaylistsongreorder"] = this.handleNotifyPlaylistSongReorder; - this["notifyplaylistsongloaded"] = this.handleNotifyPlaylistSongLoaded; + handleCommandServerInit(json){ + //We could setup the voice channel + if(this.connection.support_voice()) { + log.debug(LogCategory.NETWORKING, tr("Setting up voice")); + } else { + log.debug(LogCategory.NETWORKING, tr("Skipping voice setup (No voice bridge available)")); } - private loggable_invoker(unique_id, client_id, name) : log.server.base.Client | undefined { - const id = parseInt(client_id); - if(typeof(client_id) === "undefined" || Number.isNaN(id)) - return undefined; - if(id == 0) - return { - client_id: 0, - client_unique_id: this.connection_handler.channelTree.server.properties.virtualserver_unique_identifier, - client_name: this.connection_handler.channelTree.server.properties.virtualserver_name, - }; + json = json[0]; //Only one bulk - return { - client_unique_id: unique_id, - client_name: name, - client_id: client_id - }; + this.connection_handler.channelTree.registerClient(this.connection_handler.getClient()); + this.connection.client.side_bar.channel_conversations().reset(); + this.connection.client.clientId = parseInt(json["aclid"]); + this.connection.client.getClient().updateVariables( {key: "client_nickname", value: json["acn"]}); + + let updates: { + key: string, + value: string + }[] = []; + for(let key in json) { + if(key === "aclid") continue; + if(key === "acn") continue; + + updates.push({key: key, value: json[key]}); } + this.connection.client.channelTree.server.updateVariables(false, ...updates); - proxy_command_promise(promise: Promise, options: connection.CommandOptions) { - if(!options.process_result) - return promise; - - return promise.catch(ex => { - if(options.process_result) { - if(ex instanceof CommandResult) { - let res = ex; - if(!res.success) { - if(res.id == ErrorID.PERMISSION_ERROR) { //Permission error - const permission = this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number); - res.message = tr("Insufficient client permissions. Failed on permission ") + (permission ? permission.name : "unknown"); - this.connection_handler.log.log(log.server.Type.ERROR_PERMISSION, { - permission: this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number) - }); - this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); - } else if(res.id != ErrorID.EMPTY_RESULT) { - this.connection_handler.log.log(log.server.Type.ERROR_CUSTOM, { - message: res.extra_message.length == 0 ? res.message : res.extra_message - }); - } - } - } else if(typeof(ex) === "string") { - this.connection_handler.log.log(log.server.Type.CONNECTION_COMMAND_ERROR, {error: ex}); - } else { - log.error(LogCategory.NETWORKING, tr("Invalid promise result type: %s. Result: %o"), typeof (ex), ex); - } - } - - return Promise.reject(ex); - }); - } - - handle_command(command: ServerCommand) : boolean { - if(this[command.command]) { - this[command.command](command.arguments); - return true; - } - - return false; - } - - set_handler(command: string, handler: any) { - this[command] = handler; - } - - unset_handler(command: string, handler?: any) { - if(handler && this[command] != handler) return; - this[command] = undefined; - } - - handleCommandResult(json) { - json = json[0]; //Only one bulk - - let code : string = json["return_code"]; - if(!code || code.length == 0) { - log.warn(LogCategory.NETWORKING, tr("Invalid return code! (%o)"), json); - return; - } - let retListeners = this.connection["_retListener"]; - - for(let e of retListeners) { - if(e.code != code) continue; - retListeners.remove(e); - let result = new CommandResult(json); - if(result.success) - e.resolve(result); - else - e.reject(result); - break; - } - } - - handleCommandServerInit(json){ - //We could setup the voice channel - if(this.connection.support_voice()) { - log.debug(LogCategory.NETWORKING, tr("Setting up voice")); - } else { - log.debug(LogCategory.NETWORKING, tr("Skipping voice setup (No voice bridge available)")); - } - - - json = json[0]; //Only one bulk - - this.connection_handler.channelTree.registerClient(this.connection_handler.getClient()); - this.connection.client.side_bar.channel_conversations().reset(); - this.connection.client.clientId = parseInt(json["aclid"]); - this.connection.client.getClient().updateVariables( {key: "client_nickname", value: json["acn"]}); - - let updates: { - key: string, - value: string - }[] = []; - for(let key in json) { - if(key === "aclid") continue; - if(key === "acn") continue; - - updates.push({key: key, value: json[key]}); - } - this.connection.client.channelTree.server.updateVariables(false, ...updates); - - const properties = this.connection.client.channelTree.server.properties; - /* host message */ - if(properties.virtualserver_hostmessage_mode > 0) { - if(properties.virtualserver_hostmessage_mode == 1) { - /* show in log */ - this.connection_handler.log.log(log.server.Type.SERVER_HOST_MESSAGE, { - message: properties.virtualserver_hostmessage - }); - } else { - /* create modal/create modal and quit */ - createModal({ - header: tr("Host message"), - body: MessageHelper.bbcode_chat(properties.virtualserver_hostmessage), - footer: undefined - }).open(); - - if(properties.virtualserver_hostmessage_mode == 3) { - /* first let the client initialize his stuff */ - setTimeout(() => { - this.connection_handler.log.log(log.server.Type.SERVER_HOST_MESSAGE_DISCONNECT, { - message: properties.virtualserver_welcomemessage - }); - - this.connection.disconnect("host message disconnect"); - this.connection_handler.handleDisconnect(DisconnectReason.SERVER_HOSTMESSAGE); - this.connection_handler.sound.play(Sound.CONNECTION_DISCONNECTED); - }, 100); - } - } - } - - /* welcome message */ - if(properties.virtualserver_welcomemessage) { - this.connection_handler.log.log(log.server.Type.SERVER_WELCOME_MESSAGE, { - message: properties.virtualserver_welcomemessage + const properties = this.connection.client.channelTree.server.properties; + /* host message */ + if(properties.virtualserver_hostmessage_mode > 0) { + if(properties.virtualserver_hostmessage_mode == 1) { + /* show in log */ + this.connection_handler.log.log(server_log.Type.SERVER_HOST_MESSAGE, { + message: properties.virtualserver_hostmessage }); - } + } else { + /* create modal/create modal and quit */ + createModal({ + header: tr("Host message"), + body: bbcode_chat(properties.virtualserver_hostmessage), + footer: undefined + }).open(); - /* priviledge key */ - if(properties.virtualserver_ask_for_privilegekey) { - createInputModal(tr("Use a privilege key"), tr("This is a newly created server for which administrator privileges have not yet been claimed.
Please enter the \"privilege key\" that was automatically generated when this server was created to gain administrator permissions."), message => message.length > 0, result => { - if(!result) return; - const scon = server_connections.active_connection_handler(); - - if(scon.serverConnection.connected) - scon.serverConnection.send_command("tokenuse", { - token: result - }).then(() => { - createInfoModal(tr("Use privilege key"), tr("Privilege key successfully used!")).open(); - }).catch(error => { - createErrorModal(tr("Use privilege key"), MessageHelper.formatMessage(tr("Failed to use privilege key: {}"), error instanceof CommandResult ? error.message : error)).open(); + if(properties.virtualserver_hostmessage_mode == 3) { + /* first let the client initialize his stuff */ + setTimeout(() => { + this.connection_handler.log.log(server_log.Type.SERVER_HOST_MESSAGE_DISCONNECT, { + message: properties.virtualserver_welcomemessage }); - }, { field_placeholder: 'Enter Privilege Key' }).open(); - } - this.connection_handler.log.log(log.server.Type.CONNECTION_CONNECTED, { - own_client: this.connection_handler.getClient().log_data() - }); - this.connection_handler.sound.play(Sound.CONNECTION_CONNECTED); - this.connection.client.onConnected(); - } - - handleNotifyServerConnectionInfo(json) { - json = json[0]; - - /* everything is a number, so lets parse it */ - for(const key of Object.keys(json)) - json[key] = parseFloat(json[key]); - - this.connection_handler.channelTree.server.set_connection_info(json); - } - - handleNotifyConnectionInfo(json) { - json = json[0]; - - const object = new ClientConnectionInfo(); - /* everything is a number (except ip), so lets parse it */ - for(const key of Object.keys(json)) { - if(key === "connection_client_ip") - object[key] = json[key]; - else - object[key] = parseFloat(json[key]); - } - - const client = this.connection_handler.channelTree.findClient(parseInt(json["clid"])); - if(!client) { - log.warn(LogCategory.NETWORKING, tr("Received client connection info for unknown client (%o)"), json["clid"]); - return; - } - - client.set_connection_info(object); - } - - private createChannelFromJson(json, ignoreOrder: boolean = false) { - let tree = this.connection.client.channelTree; - - let channel = new ChannelEntry(parseInt(json["cid"]), json["channel_name"], tree.findChannel(json["cpid"])); - tree.insertChannel(channel); - if(json["channel_order"] !== "0") { - let prev = tree.findChannel(json["channel_order"]); - if(!prev && json["channel_order"] != 0) { - if(!ignoreOrder) { - log.error(LogCategory.NETWORKING, tr("Invalid channel order id!")); - return; - } + this.connection.disconnect("host message disconnect"); + this.connection_handler.handleDisconnect(DisconnectReason.SERVER_HOSTMESSAGE); + this.connection_handler.sound.play(Sound.CONNECTION_DISCONNECTED); + }, 100); } + } + } - let parent = tree.findChannel(json["cpid"]); - if(!parent && json["cpid"] != 0) { - log.error(LogCategory.NETWORKING, tr("Invalid channel parent")); + /* welcome message */ + if(properties.virtualserver_welcomemessage) { + this.connection_handler.log.log(server_log.Type.SERVER_WELCOME_MESSAGE, { + message: properties.virtualserver_welcomemessage + }); + } + + /* priviledge key */ + if(properties.virtualserver_ask_for_privilegekey) { + createInputModal(tr("Use a privilege key"), tr("This is a newly created server for which administrator privileges have not yet been claimed.
Please enter the \"privilege key\" that was automatically generated when this server was created to gain administrator permissions."), message => message.length > 0, result => { + if(!result) return; + const scon = server_connections.active_connection_handler(); + + if(scon.serverConnection.connected) + scon.serverConnection.send_command("tokenuse", { + token: result + }).then(() => { + createInfoModal(tr("Use privilege key"), tr("Privilege key successfully used!")).open(); + }).catch(error => { + createErrorModal(tr("Use privilege key"), formatMessage(tr("Failed to use privilege key: {}"), error instanceof CommandResult ? error.message : error)).open(); + }); + }, { field_placeholder: 'Enter Privilege Key' }).open(); + } + + this.connection_handler.log.log(server_log.Type.CONNECTION_CONNECTED, { + own_client: this.connection_handler.getClient().log_data() + }); + this.connection_handler.sound.play(Sound.CONNECTION_CONNECTED); + this.connection.client.onConnected(); + } + + handleNotifyServerConnectionInfo(json) { + json = json[0]; + + /* everything is a number, so lets parse it */ + for(const key of Object.keys(json)) + json[key] = parseFloat(json[key]); + + this.connection_handler.channelTree.server.set_connection_info(json); + } + + handleNotifyConnectionInfo(json) { + json = json[0]; + + const object = new ClientConnectionInfo(); + /* everything is a number (except ip), so lets parse it */ + for(const key of Object.keys(json)) { + if(key === "connection_client_ip") + object[key] = json[key]; + else + object[key] = parseFloat(json[key]); + } + + const client = this.connection_handler.channelTree.findClient(parseInt(json["clid"])); + if(!client) { + log.warn(LogCategory.NETWORKING, tr("Received client connection info for unknown client (%o)"), json["clid"]); + return; + } + + client.set_connection_info(object); + } + + private createChannelFromJson(json, ignoreOrder: boolean = false) { + let tree = this.connection.client.channelTree; + + let channel = new ChannelEntry(parseInt(json["cid"]), json["channel_name"], tree.findChannel(json["cpid"])); + tree.insertChannel(channel); + if(json["channel_order"] !== "0") { + let prev = tree.findChannel(json["channel_order"]); + if(!prev && json["channel_order"] != 0) { + if(!ignoreOrder) { + log.error(LogCategory.NETWORKING, tr("Invalid channel order id!")); return; } - tree.moveChannel(channel, prev, parent); //TODO test if channel exists! } - if(ignoreOrder) { - for(let ch of tree.channels) { - if(ch.properties.channel_order == channel.channelId) { - tree.moveChannel(ch, channel, channel.parent); //Corrent the order :) - } + + let parent = tree.findChannel(json["cpid"]); + if(!parent && json["cpid"] != 0) { + log.error(LogCategory.NETWORKING, tr("Invalid channel parent")); + return; + } + tree.moveChannel(channel, prev, parent); //TODO test if channel exists! + } + if(ignoreOrder) { + for(let ch of tree.channels) { + if(ch.properties.channel_order == channel.channelId) { + tree.moveChannel(ch, channel, channel.parent); //Corrent the order :) + } + } + } + + let updates: { + key: string, + value: string + }[] = []; + for(let key in json) { + if(key === "cid") continue; + if(key === "cpid") continue; + if(key === "invokerid") continue; + if(key === "invokername") continue; + if(key === "invokeruid") continue; + if(key === "reasonid") continue; + + updates.push({key: key, value: json[key]}); + } + channel.updateVariables(...updates); + } + + handleCommandChannelList(json) { + this.connection.client.channelTree.hide_channel_tree(); /* dont perform channel inserts on the dom to prevent style recalculations */ + log.debug(LogCategory.NETWORKING, tr("Got %d new channels"), json.length); + for(let index = 0; index < json.length; index++) + this.createChannelFromJson(json[index], true); + } + + + handleCommandChannelListFinished(json) { + this.connection.client.channelTree.show_channel_tree(); + } + + handleCommandChannelCreate(json) { + this.createChannelFromJson(json[0]); + } + + handleCommandChannelShow(json) { + this.createChannelFromJson(json[0]); //TODO may chat? + } + + handleCommandChannelDelete(json) { + let tree = this.connection.client.channelTree; + const conversations = this.connection.client.side_bar.channel_conversations(); + + log.info(LogCategory.NETWORKING, tr("Got %d channel deletions"), json.length); + for(let index = 0; index < json.length; index++) { + conversations.delete_conversation(parseInt(json[index]["cid"])); + let channel = tree.findChannel(json[index]["cid"]); + if(!channel) { + log.error(LogCategory.NETWORKING, tr("Invalid channel onDelete (Unknown channel)")); + continue; + } + tree.deleteChannel(channel); + } + } + + handleCommandChannelHide(json) { + let tree = this.connection.client.channelTree; + const conversations = this.connection.client.side_bar.channel_conversations(); + + log.info(LogCategory.NETWORKING, tr("Got %d channel hides"), json.length); + for(let index = 0; index < json.length; index++) { + conversations.delete_conversation(parseInt(json[index]["cid"])); + let channel = tree.findChannel(json[index]["cid"]); + if(!channel) { + log.error(LogCategory.NETWORKING, tr("Invalid channel on hide (Unknown channel)")); + continue; + } + tree.deleteChannel(channel); + } + } + + handleCommandClientEnterView(json) { + let tree = this.connection.client.channelTree; + + let client: ClientEntry; + let channel = undefined; + let old_channel = undefined; + let reason_id, reason_msg; + + let invokerid, invokername, invokeruid; + + for(const entry of json) { + /* attempt to update properties if given */ + channel = typeof(entry["ctid"]) !== "undefined" ? tree.findChannel(parseInt(entry["ctid"])) : channel; + old_channel = typeof(entry["cfid"]) !== "undefined" ? tree.findChannel(parseInt(entry["cfid"])) : old_channel; + reason_id = typeof(entry["reasonid"]) !== "undefined" ? entry["reasonid"] : reason_id; + reason_msg = typeof(entry["reason_msg"]) !== "undefined" ? entry["reason_msg"] : reason_msg; + + invokerid = typeof(entry["invokerid"]) !== "undefined" ? parseInt(entry["invokerid"]) : invokerid; + invokername = typeof(entry["invokername"]) !== "undefined" ? entry["invokername"] : invokername; + invokeruid = typeof(entry["invokeruid"]) !== "undefined" ? entry["invokeruid"] : invokeruid; + + client = tree.findClient(parseInt(entry["clid"])); + + if(!client) { + if(parseInt(entry["client_type_exact"]) == ClientType.CLIENT_MUSIC) { + client = new MusicClientEntry(parseInt(entry["clid"]), entry["client_nickname"]); + } else { + client = new ClientEntry(parseInt(entry["clid"]), entry["client_nickname"]); + } + + client.properties.client_type = parseInt(entry["client_type"]); + client = tree.insertClient(client, channel); + } else { + tree.moveClient(client, channel); + } + + if(this.connection_handler.client_status.queries_visible || client.properties.client_type != ClientType.CLIENT_QUERY) { + const own_channel = this.connection.client.getClient().currentChannel(); + this.connection_handler.log.log(server_log.Type.CLIENT_VIEW_ENTER, { + channel_from: old_channel ? old_channel.log_data() : undefined, + channel_to: channel ? channel.log_data() : undefined, + client: client.log_data(), + invoker: this.loggable_invoker(invokeruid, invokerid, invokername), + message:reason_msg, + reason: parseInt(reason_id), + own_channel: channel == own_channel + }); + + if(reason_id == ViewReasonId.VREASON_USER_ACTION) { + if(own_channel == channel) + if(old_channel) + this.connection_handler.sound.play(Sound.USER_ENTERED); + else + this.connection_handler.sound.play(Sound.USER_ENTERED_CONNECT); + } else if(reason_id == ViewReasonId.VREASON_MOVED) { + if(own_channel == channel) + this.connection_handler.sound.play(Sound.USER_ENTERED_MOVED); + } else if(reason_id == ViewReasonId.VREASON_CHANNEL_KICK) { + if(own_channel == channel) + this.connection_handler.sound.play(Sound.USER_ENTERED_KICKED); + } else if(reason_id == ViewReasonId.VREASON_SYSTEM) { + + } else { + console.warn(tr("Unknown reasonid for %o"), reason_id); } } @@ -311,159 +467,113 @@ namespace connection { key: string, value: string }[] = []; - for(let key in json) { - if(key === "cid") continue; - if(key === "cpid") continue; + + for(let key in entry) { + if(key == "cfid") continue; + if(key == "ctid") continue; if(key === "invokerid") continue; if(key === "invokername") continue; if(key === "invokeruid") continue; if(key === "reasonid") continue; - updates.push({key: key, value: json[key]}); + updates.push({key: key, value: entry[key]}); } - channel.updateVariables(...updates); - } - handleCommandChannelList(json) { - this.connection.client.channelTree.hide_channel_tree(); /* dont perform channel inserts on the dom to prevent style recalculations */ - log.debug(LogCategory.NETWORKING, tr("Got %d new channels"), json.length); - for(let index = 0; index < json.length; index++) - this.createChannelFromJson(json[index], true); - } + client.updateVariables(...updates); + /* if its a new client join, or a system reason (like we joined) */ + if(!old_channel || reason_id == 2) { + /* client new join */ + const conversation_manager = this.connection_handler.side_bar.private_conversations(); + const conversation = conversation_manager.find_conversation({ + unique_id: client.properties.client_unique_identifier, + client_id: client.clientId(), + name: client.clientNickName() + }, { + create: false, + attach: true + }); + if(conversation) + client.flag_text_unread = conversation.is_unread(); + } - handleCommandChannelListFinished(json) { - this.connection.client.channelTree.show_channel_tree(); - } - - handleCommandChannelCreate(json) { - this.createChannelFromJson(json[0]); - } - - handleCommandChannelShow(json) { - this.createChannelFromJson(json[0]); //TODO may chat? - } - - handleCommandChannelDelete(json) { - let tree = this.connection.client.channelTree; - const conversations = this.connection.client.side_bar.channel_conversations(); - - log.info(LogCategory.NETWORKING, tr("Got %d channel deletions"), json.length); - for(let index = 0; index < json.length; index++) { - conversations.delete_conversation(parseInt(json[index]["cid"])); - let channel = tree.findChannel(json[index]["cid"]); - if(!channel) { - log.error(LogCategory.NETWORKING, tr("Invalid channel onDelete (Unknown channel)")); - continue; - } - tree.deleteChannel(channel); + if(client instanceof LocalClientEntry) { + client.initializeListener(); + this.connection_handler.update_voice_status(); + this.connection_handler.side_bar.info_frame().update_channel_talk(); + const conversations = this.connection.client.side_bar.channel_conversations(); + conversations.set_current_channel(client.currentChannel().channelId); } } + } - handleCommandChannelHide(json) { + handleCommandClientLeftView(json) { + let reason_id = -1; + + for(const entry of json) { + reason_id = entry["reasonid"] || reason_id; let tree = this.connection.client.channelTree; - const conversations = this.connection.client.side_bar.channel_conversations(); - - log.info(LogCategory.NETWORKING, tr("Got %d channel hides"), json.length); - for(let index = 0; index < json.length; index++) { - conversations.delete_conversation(parseInt(json[index]["cid"])); - let channel = tree.findChannel(json[index]["cid"]); - if(!channel) { - log.error(LogCategory.NETWORKING, tr("Invalid channel on hide (Unknown channel)")); - continue; - } - tree.deleteChannel(channel); + let client = tree.findClient(entry["clid"]); + if(!client) { + log.error(LogCategory.NETWORKING, tr("Unknown client left!")); + return 0; } - } - - handleCommandClientEnterView(json) { - let tree = this.connection.client.channelTree; - - let client: ClientEntry; - let channel = undefined; - let old_channel = undefined; - let reason_id, reason_msg; - - let invokerid, invokername, invokeruid; - - for(const entry of json) { - /* attempt to update properties if given */ - channel = typeof(entry["ctid"]) !== "undefined" ? tree.findChannel(parseInt(entry["ctid"])) : channel; - old_channel = typeof(entry["cfid"]) !== "undefined" ? tree.findChannel(parseInt(entry["cfid"])) : old_channel; - reason_id = typeof(entry["reasonid"]) !== "undefined" ? entry["reasonid"] : reason_id; - reason_msg = typeof(entry["reason_msg"]) !== "undefined" ? entry["reason_msg"] : reason_msg; - - invokerid = typeof(entry["invokerid"]) !== "undefined" ? parseInt(entry["invokerid"]) : invokerid; - invokername = typeof(entry["invokername"]) !== "undefined" ? entry["invokername"] : invokername; - invokeruid = typeof(entry["invokeruid"]) !== "undefined" ? entry["invokeruid"] : invokeruid; - - client = tree.findClient(parseInt(entry["clid"])); - - if(!client) { - if(parseInt(entry["client_type_exact"]) == ClientType.CLIENT_MUSIC) { - client = new MusicClientEntry(parseInt(entry["clid"]), entry["client_nickname"]); - } else { - client = new ClientEntry(parseInt(entry["clid"]), entry["client_nickname"]); - } - - client.properties.client_type = parseInt(entry["client_type"]); - client = tree.insertClient(client, channel); + if(client == this.connection.client.getClient()) { + if(reason_id == ViewReasonId.VREASON_BAN) { + this.connection.client.handleDisconnect(DisconnectReason.CLIENT_BANNED, entry); + } else if(reason_id == ViewReasonId.VREASON_SERVER_KICK) { + this.connection.client.handleDisconnect(DisconnectReason.CLIENT_KICKED, entry); + } else if(reason_id == ViewReasonId.VREASON_SERVER_SHUTDOWN) { + this.connection.client.handleDisconnect(DisconnectReason.SERVER_CLOSED, entry); + } else if(reason_id == ViewReasonId.VREASON_SERVER_STOPPED) { + this.connection.client.handleDisconnect(DisconnectReason.SERVER_CLOSED, entry); } else { - tree.moveClient(client, channel); + this.connection.client.handleDisconnect(DisconnectReason.UNKNOWN, entry); } + this.connection_handler.side_bar.info_frame().update_channel_talk(); + return; + } - if(this.connection_handler.client_status.queries_visible || client.properties.client_type != ClientType.CLIENT_QUERY) { - const own_channel = this.connection.client.getClient().currentChannel(); - this.connection_handler.log.log(log.server.Type.CLIENT_VIEW_ENTER, { - channel_from: old_channel ? old_channel.log_data() : undefined, - channel_to: channel ? channel.log_data() : undefined, - client: client.log_data(), - invoker: this.loggable_invoker(invokeruid, invokerid, invokername), - message:reason_msg, - reason: parseInt(reason_id), - own_channel: channel == own_channel - }); + if(this.connection_handler.client_status.queries_visible || client.properties.client_type != ClientType.CLIENT_QUERY) { + const own_channel = this.connection.client.getClient().currentChannel(); + let channel_from = tree.findChannel(entry["cfid"]); + let channel_to = tree.findChannel(entry["ctid"]); + + const is_own_channel = channel_from == own_channel; + this.connection_handler.log.log(server_log.Type.CLIENT_VIEW_LEAVE, { + channel_from: channel_from ? channel_from.log_data() : undefined, + channel_to: channel_to ? channel_to.log_data() : undefined, + client: client.log_data(), + invoker: this.loggable_invoker(entry["invokeruid"], entry["invokerid"], entry["invokername"]), + message: entry["reasonmsg"], + reason: parseInt(entry["reasonid"]), + ban_time: parseInt(entry["bantime"]), + own_channel: is_own_channel + }); + + if(is_own_channel) { if(reason_id == ViewReasonId.VREASON_USER_ACTION) { - if(own_channel == channel) - if(old_channel) - this.connection_handler.sound.play(Sound.USER_ENTERED); - else - this.connection_handler.sound.play(Sound.USER_ENTERED_CONNECT); - } else if(reason_id == ViewReasonId.VREASON_MOVED) { - if(own_channel == channel) - this.connection_handler.sound.play(Sound.USER_ENTERED_MOVED); + this.connection_handler.sound.play(Sound.USER_LEFT); + } else if(reason_id == ViewReasonId.VREASON_SERVER_LEFT) { + this.connection_handler.sound.play(Sound.USER_LEFT_DISCONNECT); + } else if(reason_id == ViewReasonId.VREASON_SERVER_KICK) { + this.connection_handler.sound.play(Sound.USER_LEFT_KICKED_SERVER); } else if(reason_id == ViewReasonId.VREASON_CHANNEL_KICK) { - if(own_channel == channel) - this.connection_handler.sound.play(Sound.USER_ENTERED_KICKED); - } else if(reason_id == ViewReasonId.VREASON_SYSTEM) { - + this.connection_handler.sound.play(Sound.USER_LEFT_KICKED_CHANNEL); + } else if(reason_id == ViewReasonId.VREASON_BAN) { + this.connection_handler.sound.play(Sound.USER_LEFT_BANNED); + } else if(reason_id == ViewReasonId.VREASON_TIMEOUT) { + this.connection_handler.sound.play(Sound.USER_LEFT_TIMEOUT); + } else if(reason_id == ViewReasonId.VREASON_MOVED) { + this.connection_handler.sound.play(Sound.USER_LEFT_MOVED); } else { - console.warn(tr("Unknown reasonid for %o"), reason_id); + log.error(LogCategory.NETWORKING, tr("Unknown client left reason %d!"), reason_id); } } - let updates: { - key: string, - value: string - }[] = []; - - for(let key in entry) { - if(key == "cfid") continue; - if(key == "ctid") continue; - if(key === "invokerid") continue; - if(key === "invokername") continue; - if(key === "invokeruid") continue; - if(key === "reasonid") continue; - - updates.push({key: key, value: entry[key]}); - } - - client.updateVariables(...updates); - - /* if its a new client join, or a system reason (like we joined) */ - if(!old_channel || reason_id == 2) { - /* client new join */ + if(!channel_to) { + /* client left the server */ const conversation_manager = this.connection_handler.side_bar.private_conversations(); const conversation = conversation_manager.find_conversation({ unique_id: client.properties.client_unique_identifier, @@ -471,690 +581,599 @@ namespace connection { name: client.clientNickName() }, { create: false, - attach: true + attach: false }); - if(conversation) - client.flag_text_unread = conversation.is_unread(); - } - - if(client instanceof LocalClientEntry) { - client.initializeListener(); - this.connection_handler.update_voice_status(); - this.connection_handler.side_bar.info_frame().update_channel_talk(); - const conversations = this.connection.client.side_bar.channel_conversations(); - conversations.set_current_channel(client.currentChannel().channelId); - } - } - } - - handleCommandClientLeftView(json) { - let reason_id = -1; - - for(const entry of json) { - reason_id = entry["reasonid"] || reason_id; - let tree = this.connection.client.channelTree; - let client = tree.findClient(entry["clid"]); - if(!client) { - log.error(LogCategory.NETWORKING, tr("Unknown client left!")); - return 0; - } - if(client == this.connection.client.getClient()) { - if(reason_id == ViewReasonId.VREASON_BAN) { - this.connection.client.handleDisconnect(DisconnectReason.CLIENT_BANNED, entry); - } else if(reason_id == ViewReasonId.VREASON_SERVER_KICK) { - this.connection.client.handleDisconnect(DisconnectReason.CLIENT_KICKED, entry); - } else if(reason_id == ViewReasonId.VREASON_SERVER_SHUTDOWN) { - this.connection.client.handleDisconnect(DisconnectReason.SERVER_CLOSED, entry); - } else if(reason_id == ViewReasonId.VREASON_SERVER_STOPPED) { - this.connection.client.handleDisconnect(DisconnectReason.SERVER_CLOSED, entry); - } else { - this.connection.client.handleDisconnect(DisconnectReason.UNKNOWN, entry); - } - this.connection_handler.side_bar.info_frame().update_channel_talk(); - return; - } - - - if(this.connection_handler.client_status.queries_visible || client.properties.client_type != ClientType.CLIENT_QUERY) { - const own_channel = this.connection.client.getClient().currentChannel(); - let channel_from = tree.findChannel(entry["cfid"]); - let channel_to = tree.findChannel(entry["ctid"]); - - const is_own_channel = channel_from == own_channel; - this.connection_handler.log.log(log.server.Type.CLIENT_VIEW_LEAVE, { - channel_from: channel_from ? channel_from.log_data() : undefined, - channel_to: channel_to ? channel_to.log_data() : undefined, - client: client.log_data(), - invoker: this.loggable_invoker(entry["invokeruid"], entry["invokerid"], entry["invokername"]), - message: entry["reasonmsg"], - reason: parseInt(entry["reasonid"]), - ban_time: parseInt(entry["bantime"]), - own_channel: is_own_channel - }); - - if(is_own_channel) { - if(reason_id == ViewReasonId.VREASON_USER_ACTION) { - this.connection_handler.sound.play(Sound.USER_LEFT); - } else if(reason_id == ViewReasonId.VREASON_SERVER_LEFT) { - this.connection_handler.sound.play(Sound.USER_LEFT_DISCONNECT); - } else if(reason_id == ViewReasonId.VREASON_SERVER_KICK) { - this.connection_handler.sound.play(Sound.USER_LEFT_KICKED_SERVER); - } else if(reason_id == ViewReasonId.VREASON_CHANNEL_KICK) { - this.connection_handler.sound.play(Sound.USER_LEFT_KICKED_CHANNEL); - } else if(reason_id == ViewReasonId.VREASON_BAN) { - this.connection_handler.sound.play(Sound.USER_LEFT_BANNED); - } else if(reason_id == ViewReasonId.VREASON_TIMEOUT) { - this.connection_handler.sound.play(Sound.USER_LEFT_TIMEOUT); - } else if(reason_id == ViewReasonId.VREASON_MOVED) { - this.connection_handler.sound.play(Sound.USER_LEFT_MOVED); - } else { - log.error(LogCategory.NETWORKING, tr("Unknown client left reason %d!"), reason_id); - } - } - - if(!channel_to) { - /* client left the server */ - const conversation_manager = this.connection_handler.side_bar.private_conversations(); - const conversation = conversation_manager.find_conversation({ - unique_id: client.properties.client_unique_identifier, - client_id: client.clientId(), - name: client.clientNickName() - }, { - create: false, - attach: false - }); - if(conversation) { - conversation.set_state(chat.PrivateConversationState.DISCONNECTED); - } + if(conversation) { + conversation.set_state(PrivateConversationState.DISCONNECTED); } } - - tree.deleteClient(client); - } - } - - handleNotifyClientMoved(json) { - json = json[0]; //Only one bulk - let tree = this.connection.client.channelTree; - let client = tree.findClient(json["clid"]); - let self = client instanceof LocalClientEntry; - - let channel_to = tree.findChannel(parseInt(json["ctid"])); - let channel_from = tree.findChannel(parseInt(json["cfid"])); - - if(!client) { - log.error(LogCategory.NETWORKING, tr("Unknown client move (Client)!")); - return 0; } - if(!channel_to) { - log.error(LogCategory.NETWORKING, tr("Unknown client move (Channel to)!")); - return 0; - } - - if(!self) { - if(!channel_from) { - log.error(LogCategory.NETWORKING, tr("Unknown client move (Channel from)!")); - channel_from = client.currentChannel(); - } else if(channel_from != client.currentChannel()) { - log.error(LogCategory.NETWORKING, - tr("Client move from invalid source channel! Local client registered in channel %d but server send %d."), - client.currentChannel().channelId, channel_from.channelId - ); - } - } else { - channel_from = client.currentChannel(); - } - - tree.moveClient(client, channel_to); - - if(self) { - this.connection_handler.update_voice_status(channel_to); - - for(const entry of client.channelTree.clientsByChannel(channel_from)) { - if(entry !== client && entry.get_audio_handle()) { - entry.get_audio_handle().abort_replay(); - entry.speaking = false; - } - } - - const side_bar = this.connection_handler.side_bar; - side_bar.info_frame().update_channel_talk(); - - const conversation_to = side_bar.channel_conversations().conversation(channel_to.channelId, false); - if(conversation_to) - conversation_to.update_private_state(); - - if(channel_from) { - const conversation_from = side_bar.channel_conversations().conversation(channel_from.channelId, false); - if(conversation_from) - conversation_from.update_private_state(); - } - - side_bar.channel_conversations().update_chat_box(); - } - - const own_channel = this.connection.client.getClient().currentChannel(); - this.connection_handler.log.log(log.server.Type.CLIENT_VIEW_MOVE, { - channel_from: channel_from ? { - channel_id: channel_from.channelId, - channel_name: channel_from.channelName() - } : undefined, - channel_from_own: channel_from == own_channel, - - channel_to: channel_to ? { - channel_id: channel_to.channelId, - channel_name: channel_to.channelName() - } : undefined, - channel_to_own: channel_to == own_channel, - - client: { - client_id: client.clientId(), - client_name: client.clientNickName(), - client_unique_id: client.properties.client_unique_identifier - }, - client_own: self, - - invoker: this.loggable_invoker(json["invokeruid"], json["invokerid"], json["invokername"]), - - message: json["reasonmsg"], - reason: parseInt(json["reasonid"]), - }); - if(json["reasonid"] == ViewReasonId.VREASON_MOVED) { - if(self) - this.connection_handler.sound.play(Sound.USER_MOVED_SELF); - else if(own_channel == channel_to) - this.connection_handler.sound.play(Sound.USER_ENTERED_MOVED); - else if(own_channel == channel_from) - this.connection_handler.sound.play(Sound.USER_LEFT_MOVED); - } else if(json["reasonid"] == ViewReasonId.VREASON_USER_ACTION) { - if(self) {} //If we do an action we wait for the error response - else if(own_channel == channel_to) - this.connection_handler.sound.play(Sound.USER_ENTERED); - else if(own_channel == channel_from) - this.connection_handler.sound.play(Sound.USER_LEFT); - } else if(json["reasonid"] == ViewReasonId.VREASON_CHANNEL_KICK) { - if(self) { - this.connection_handler.sound.play(Sound.CHANNEL_KICKED); - } else if(own_channel == channel_to) - this.connection_handler.sound.play(Sound.USER_ENTERED_KICKED); - else if(own_channel == channel_from) - this.connection_handler.sound.play(Sound.USER_LEFT_KICKED_CHANNEL); - } else { - console.warn(tr("Unknown reason id %o"), json["reasonid"]); - } - } - - handleNotifyChannelMoved(json) { - json = json[0]; //Only one bulk - - let tree = this.connection.client.channelTree; - let channel = tree.findChannel(json["cid"]); - if(!channel) { - log.error(LogCategory.NETWORKING, tr("Unknown channel move (Channel)!")); - return 0; - } - - let prev = tree.findChannel(json["order"]); - if(!prev && json["order"] != 0) { - log.error(LogCategory.NETWORKING, tr("Unknown channel move (prev)!")); - return 0; - } - - let parent = tree.findChannel(json["cpid"]); - if(!parent && json["cpid"] != 0) { - log.error(LogCategory.NETWORKING, tr("Unknown channel move (parent)!")); - return 0; - } - - tree.moveChannel(channel, prev, parent); - } - - handleNotifyChannelEdited(json) { - json = json[0]; //Only one bulk - - let tree = this.connection.client.channelTree; - let channel = tree.findChannel(json["cid"]); - if(!channel) { - log.error(LogCategory.NETWORKING, tr("Unknown channel edit (Channel)!")); - return 0; - } - - let updates: { - key: string, - value: string - }[] = []; - for(let key in json) { - if(key === "cid") continue; - if(key === "invokerid") continue; - if(key === "invokername") continue; - if(key === "invokeruid") continue; - if(key === "reasonid") continue; - updates.push({key: key, value: json[key]}); - } - channel.updateVariables(...updates); - - if(this.connection_handler.getClient().currentChannel() === channel) { - //TODO: Playback sound that your channel has been edited - this.connection_handler.update_voice_status(); - } - } - - handleNotifyTextMessage(json) { - json = json[0]; //Only one bulk - - let mode = json["targetmode"]; - if(mode == 1){ - //json["invokerid"], json["invokername"], json["invokeruid"] - const target_client_id = parseInt(json["target"]); - const target_own = target_client_id === this.connection.client.getClientId(); - - if(target_own && target_client_id === json["invokerid"]) { - log.error(LogCategory.NETWORKING, tr("Received conversation message from invalid client id. Data: %o", json)); - return; - } - - const conversation_manager = this.connection_handler.side_bar.private_conversations(); - const conversation = conversation_manager.find_conversation({ - client_id: target_own ? parseInt(json["invokerid"]) : target_client_id, - unique_id: target_own ? json["invokeruid"] : undefined, - name: target_own ? json["invokername"] : undefined - }, { - create: target_own, - attach: target_own - }); - if(!conversation) { - log.error(LogCategory.NETWORKING, tr("Received conversation message for unknown conversation! (%s)"), target_own ? tr("Remote message") : tr("Own message")); - return; - } - - conversation.append_message(json["msg"], { - type: target_own ? "partner" : "self", - name: json["invokername"], - unique_id: json["invokeruid"], - client_id: parseInt(json["invokerid"]) - }); - - if(target_own) { - this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5}); - const client = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"])); - if(client) /* the client itself might be invisible */ - client.flag_text_unread = conversation.is_unread(); - } else { - this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5}); - } - } else if(mode == 2) { - const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"])); - const own_channel_id = this.connection.client.getClient().currentChannel().channelId; - const channel_id = typeof(json["cid"]) !== "undefined" ? parseInt(json["cid"]) : own_channel_id; - const channel = this.connection_handler.channelTree.findChannel(channel_id) || this.connection_handler.getClient().currentChannel(); - - if(json["invokerid"] == this.connection.client.clientId) - this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5}); - else if(channel_id == own_channel_id) { - this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5}); - } - - const conversations = this.connection_handler.side_bar.channel_conversations(); - const conversation = conversations.conversation(channel_id); - conversation.register_new_message({ - sender_database_id: invoker ? invoker.properties.client_database_id : 0, - sender_name: json["invokername"], - sender_unique_id: json["invokeruid"], - - timestamp: typeof(json["timestamp"]) === "undefined" ? Date.now() : parseInt(json["timestamp"]), - message: json["msg"] - }); - if(conversation.is_unread() && channel) - channel.flag_text_unread = true; - } else if(mode == 3) { - this.connection_handler.log.log(log.server.Type.GLOBAL_MESSAGE, { - message: json["msg"], - sender: { - client_unique_id: json["invokeruid"], - client_name: json["invokername"], - client_id: parseInt(json["invokerid"]) - } - }); - - const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"])); - const conversations = this.connection_handler.side_bar.channel_conversations(); - const conversation = conversations.conversation(0); - conversation.register_new_message({ - sender_database_id: invoker ? invoker.properties.client_database_id : 0, - sender_name: json["invokername"], - sender_unique_id: json["invokeruid"], - - timestamp: typeof(json["timestamp"]) === "undefined" ? Date.now() : parseInt(json["timestamp"]), - message: json["msg"] - }); - this.connection_handler.channelTree.server.flag_text_unread = conversation.is_unread(); - } - } - - notifyClientChatComposing(json) { - json = json[0]; - - const conversation_manager = this.connection_handler.side_bar.private_conversations(); - const conversation = conversation_manager.find_conversation({ - client_id: parseInt(json["clid"]), - unique_id: json["cluid"], - name: undefined - }, { - create: false, - attach: false - }); - if(!conversation) - return; - - conversation.trigger_typing(); - } - - handleNotifyClientChatClosed(json) { - json = json[0]; //Only one bulk - - //Chat partner has closed the conversation - - //clid: "6" - //cluid: "YoWmG+dRGKD+Rxb7SPLAM5+B9tY=" - - const conversation_manager = this.connection_handler.side_bar.private_conversations(); - const conversation = conversation_manager.find_conversation({ - client_id: parseInt(json["clid"]), - unique_id: json["cluid"], - name: undefined - }, { - create: false, - attach: false - }); - if(!conversation) { - log.warn(LogCategory.GENERAL, tr("Received chat close for client, but we haven't a chat open.")); - return; - } - conversation.set_state(chat.PrivateConversationState.CLOSED); - } - - handleNotifyClientUpdated(json) { - json = json[0]; //Only one bulk - - let client = this.connection.client.channelTree.findClient(json["clid"]); - if(!client) { - log.error(LogCategory.NETWORKING, tr("Tried to update an non existing client")); - return; - } - - let updates: { - key: string, - value: string - }[] = []; - for(let key in json) { - if(key == "clid") continue; - updates.push({key: key, value: json[key]}); - } - client.updateVariables(...updates); - } - - handleNotifyServerEdited(json) { - json = json[0]; - - let updates: { - key: string, - value: string - }[] = []; - for(let key in json) { - if(key === "invokerid") continue; - if(key === "invokername") continue; - if(key === "invokeruid") continue; - if(key === "reasonid") continue; - - updates.push({key: key, value: json[key]}); - } - this.connection.client.channelTree.server.updateVariables(false, ...updates); - } - - handleNotifyServerUpdated(json) { - json = json[0]; - - let updates: { - key: string, - value: string - }[] = []; - for(let key in json) { - if(key === "invokerid") continue; - if(key === "invokername") continue; - if(key === "invokeruid") continue; - if(key === "reasonid") continue; - - updates.push({key: key, value: json[key]}); - } - this.connection.client.channelTree.server.updateVariables(true, ...updates); - } - - handleNotifyMusicPlayerInfo(json) { - json = json[0]; - - let bot = this.connection.client.channelTree.find_client_by_dbid(json["bot_id"]); - if(!bot || !(bot instanceof MusicClientEntry)) { - log.warn(LogCategory.CLIENT, tr("Got music player info for unknown or invalid bot! (ID: %i, Entry: %o)"), json["bot_id"], bot); - return; - } - - bot.handlePlayerInfo(json); - } - - handleNotifyClientPoke(json) { - json = json[0]; - Modals.spawnPoke(this.connection_handler, { - id: parseInt(json["invokerid"]), - name: json["invokername"], - unique_id: json["invokeruid"] - }, json["msg"]); - - this.connection_handler.sound.play(Sound.USER_POKED_SELF); - } - - //TODO server chat message - handleNotifyServerGroupClientAdd(json) { - json = json[0]; - - const self = this.connection.client.getClient(); - if(json["clid"] == self.clientId()) - this.connection_handler.sound.play(Sound.GROUP_SERVER_ASSIGNED_SELF); - } - - //TODO server chat message - handleNotifyServerGroupClientRemove(json) { - json = json[0]; - - const self = this.connection.client.getClient(); - if(json["clid"] == self.clientId()) { - this.connection_handler.sound.play(Sound.GROUP_SERVER_REVOKED_SELF); - } else { - } - } - - //TODO server chat message - handleNotifyClientChannelGroupChanged(json) { - json = json[0]; - - const self = this.connection.client.getClient(); - if(json["clid"] == self.clientId()) { - this.connection_handler.sound.play(Sound.GROUP_CHANNEL_CHANGED_SELF); - } - } - - handleNotifyChannelSubscribed(json) { - for(const entry of json) { - const channel = this.connection.client.channelTree.findChannel(entry["cid"]); - if(!channel) { - console.warn(tr("Received channel subscribed for not visible channel (cid: %d)"), entry['cid']); - continue; - } - - channel.flag_subscribed = true; - } - } - - handleNotifyChannelUnsubscribed(json) { - for(const entry of json) { - const channel = this.connection.client.channelTree.findChannel(entry["cid"]); - if(!channel) { - console.warn(tr("Received channel unsubscribed for not visible channel (cid: %d)"), entry['cid']); - continue; - } - - channel.flag_subscribed = false; - for(const client of channel.clients(false)) - this.connection.client.channelTree.deleteClient(client); - } - } - - handleNotifyConversationHistory(json: any[]) { - const conversations = this.connection.client.side_bar.channel_conversations(); - const conversation = conversations.conversation(parseInt(json[0]["cid"])); - if(!conversation) { - log.warn(LogCategory.NETWORKING, tr("Received conversation history for invalid or unknown conversation (%o)"), json[0]["cid"]); - return; - } - - for(const entry of json) { - conversation.register_new_message({ - message: entry["msg"], - sender_unique_id: entry["sender_unique_id"], - sender_name: entry["sender_name"], - timestamp: parseInt(entry["timestamp"]), - sender_database_id: parseInt(entry["sender_database_id"]) - }, false); - } - - /* now update the boxes */ - /* No update needed because the command which triggers this notify should update the chat box on success - conversation.fix_scroll(true); - conversation.handle.update_chat_box(); - */ - } - - handleNotifyConversationMessageDelete(json: any[]) { - let conversation: Conversation; - const conversations = this.connection.client.side_bar.channel_conversations(); - for(const entry of json) { - if(typeof(entry["cid"]) !== "undefined") - conversation = conversations.conversation(parseInt(entry["cid"]), false); - if(!conversation) - continue; - - conversation.delete_messages(parseInt(entry["timestamp_begin"]), parseInt(entry["timestamp_end"]), parseInt(entry["cldbid"]), parseInt(entry["limit"])); - } - } - - handleNotifyMusicStatusUpdate(json: any[]) { - json = json[0]; - - const bot_id = parseInt(json["bot_id"]); - const client = this.connection.client.channelTree.find_client_by_dbid(bot_id); - if(!client) { - log.warn(LogCategory.CLIENT, tr("Received music bot status update for unknown bot (%d)"), bot_id); - return; - } - - client.events.fire("music_status_update", { - player_replay_index: parseInt(json["player_replay_index"]), - player_buffered_index: parseInt(json["player_buffered_index"]) - }); - } - - handleMusicPlayerSongChange(json: any[]) { - json = json[0]; - - const bot_id = parseInt(json["bot_id"]); - const client = this.connection.client.channelTree.find_client_by_dbid(bot_id); - if(!client) { - log.warn(LogCategory.CLIENT, tr("Received music bot status update for unknown bot (%d)"), bot_id); - return; - } - - const song_id = parseInt(json["song_id"]); - let song: SongInfo; - if(song_id) { - song = new SongInfo(); - JSON.map_to(song, json); - } - - client.events.fire("music_song_change", { - song: song - }); - } - - handleNotifyPlaylistSongAdd(json: any[]) { - json = json[0]; - - const playlist_id = parseInt(json["playlist_id"]); - const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id); - if(!client) { - log.warn(LogCategory.CLIENT, tr("Received playlist song add event, but we've no music bot for the playlist (%d)"), playlist_id); - return; - } - - client.events.fire("playlist_song_add", { - song: { - song_id: parseInt(json["song_id"]), - song_invoker: json["song_invoker"], - song_previous_song_id: parseInt(json["song_previous_song_id"]), - song_url: json["song_url"], - song_url_loader: json["song_url_loader"], - - song_loaded: json["song_loaded"] == true || json["song_loaded"] == "1", - song_metadata: json["song_metadata"] - } - }); - } - - handleNotifyPlaylistSongRemove(json: any[]) { - json = json[0]; - - const playlist_id = parseInt(json["playlist_id"]); - const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id); - if(!client) { - log.warn(LogCategory.CLIENT, tr("Received playlist song remove event, but we've no music bot for the playlist (%d)"), playlist_id); - return; - } - - const song_id = parseInt(json["song_id"]); - client.events.fire("playlist_song_remove", { song_id: song_id }); - } - - handleNotifyPlaylistSongReorder(json: any[]) { - json = json[0]; - - const playlist_id = parseInt(json["playlist_id"]); - const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id); - if(!client) { - log.warn(LogCategory.CLIENT, tr("Received playlist song reorder event, but we've no music bot for the playlist (%d)"), playlist_id); - return; - } - - const song_id = parseInt(json["song_id"]); - const previous_song_id = parseInt(json["song_previous_song_id"]); - client.events.fire("playlist_song_reorder", { song_id: song_id, previous_song_id: previous_song_id }); - } - - handleNotifyPlaylistSongLoaded(json: any[]) { - json = json[0]; - - const playlist_id = parseInt(json["playlist_id"]); - const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id); - if(!client) { - log.warn(LogCategory.CLIENT, tr("Received playlist song loaded event, but we've no music bot for the playlist (%d)"), playlist_id); - return; - } - - const song_id = parseInt(json["song_id"]); - client.events.fire("playlist_song_loaded", { - song_id: song_id, - success: json["success"] == 1, - error_msg: json["load_error_msg"], - metadata: json["song_metadata"] - }); + tree.deleteClient(client); } } + + handleNotifyClientMoved(json) { + json = json[0]; //Only one bulk + let tree = this.connection.client.channelTree; + let client = tree.findClient(json["clid"]); + let self = client instanceof LocalClientEntry; + + let channel_to = tree.findChannel(parseInt(json["ctid"])); + let channel_from = tree.findChannel(parseInt(json["cfid"])); + + if(!client) { + log.error(LogCategory.NETWORKING, tr("Unknown client move (Client)!")); + return 0; + } + + if(!channel_to) { + log.error(LogCategory.NETWORKING, tr("Unknown client move (Channel to)!")); + return 0; + } + + if(!self) { + if(!channel_from) { + log.error(LogCategory.NETWORKING, tr("Unknown client move (Channel from)!")); + channel_from = client.currentChannel(); + } else if(channel_from != client.currentChannel()) { + log.error(LogCategory.NETWORKING, + tr("Client move from invalid source channel! Local client registered in channel %d but server send %d."), + client.currentChannel().channelId, channel_from.channelId + ); + } + } else { + channel_from = client.currentChannel(); + } + + tree.moveClient(client, channel_to); + + if(self) { + this.connection_handler.update_voice_status(channel_to); + + for(const entry of client.channelTree.clientsByChannel(channel_from)) { + if(entry !== client && entry.get_audio_handle()) { + entry.get_audio_handle().abort_replay(); + entry.speaking = false; + } + } + + const side_bar = this.connection_handler.side_bar; + side_bar.info_frame().update_channel_talk(); + + const conversation_to = side_bar.channel_conversations().conversation(channel_to.channelId, false); + if(conversation_to) + conversation_to.update_private_state(); + + if(channel_from) { + const conversation_from = side_bar.channel_conversations().conversation(channel_from.channelId, false); + if(conversation_from) + conversation_from.update_private_state(); + } + + side_bar.channel_conversations().update_chat_box(); + } + + const own_channel = this.connection.client.getClient().currentChannel(); + this.connection_handler.log.log(server_log.Type.CLIENT_VIEW_MOVE, { + channel_from: channel_from ? { + channel_id: channel_from.channelId, + channel_name: channel_from.channelName() + } : undefined, + channel_from_own: channel_from == own_channel, + + channel_to: channel_to ? { + channel_id: channel_to.channelId, + channel_name: channel_to.channelName() + } : undefined, + channel_to_own: channel_to == own_channel, + + client: { + client_id: client.clientId(), + client_name: client.clientNickName(), + client_unique_id: client.properties.client_unique_identifier + }, + client_own: self, + + invoker: this.loggable_invoker(json["invokeruid"], json["invokerid"], json["invokername"]), + + message: json["reasonmsg"], + reason: parseInt(json["reasonid"]), + }); + if(json["reasonid"] == ViewReasonId.VREASON_MOVED) { + if(self) + this.connection_handler.sound.play(Sound.USER_MOVED_SELF); + else if(own_channel == channel_to) + this.connection_handler.sound.play(Sound.USER_ENTERED_MOVED); + else if(own_channel == channel_from) + this.connection_handler.sound.play(Sound.USER_LEFT_MOVED); + } else if(json["reasonid"] == ViewReasonId.VREASON_USER_ACTION) { + if(self) {} //If we do an action we wait for the error response + else if(own_channel == channel_to) + this.connection_handler.sound.play(Sound.USER_ENTERED); + else if(own_channel == channel_from) + this.connection_handler.sound.play(Sound.USER_LEFT); + } else if(json["reasonid"] == ViewReasonId.VREASON_CHANNEL_KICK) { + if(self) { + this.connection_handler.sound.play(Sound.CHANNEL_KICKED); + } else if(own_channel == channel_to) + this.connection_handler.sound.play(Sound.USER_ENTERED_KICKED); + else if(own_channel == channel_from) + this.connection_handler.sound.play(Sound.USER_LEFT_KICKED_CHANNEL); + } else { + console.warn(tr("Unknown reason id %o"), json["reasonid"]); + } + } + + handleNotifyChannelMoved(json) { + json = json[0]; //Only one bulk + + let tree = this.connection.client.channelTree; + let channel = tree.findChannel(json["cid"]); + if(!channel) { + log.error(LogCategory.NETWORKING, tr("Unknown channel move (Channel)!")); + return 0; + } + + let prev = tree.findChannel(json["order"]); + if(!prev && json["order"] != 0) { + log.error(LogCategory.NETWORKING, tr("Unknown channel move (prev)!")); + return 0; + } + + let parent = tree.findChannel(json["cpid"]); + if(!parent && json["cpid"] != 0) { + log.error(LogCategory.NETWORKING, tr("Unknown channel move (parent)!")); + return 0; + } + + tree.moveChannel(channel, prev, parent); + } + + handleNotifyChannelEdited(json) { + json = json[0]; //Only one bulk + + let tree = this.connection.client.channelTree; + let channel = tree.findChannel(json["cid"]); + if(!channel) { + log.error(LogCategory.NETWORKING, tr("Unknown channel edit (Channel)!")); + return 0; + } + + let updates: { + key: string, + value: string + }[] = []; + for(let key in json) { + if(key === "cid") continue; + if(key === "invokerid") continue; + if(key === "invokername") continue; + if(key === "invokeruid") continue; + if(key === "reasonid") continue; + updates.push({key: key, value: json[key]}); + } + channel.updateVariables(...updates); + + if(this.connection_handler.getClient().currentChannel() === channel) { + //TODO: Playback sound that your channel has been edited + this.connection_handler.update_voice_status(); + } + } + + handleNotifyTextMessage(json) { + json = json[0]; //Only one bulk + + let mode = json["targetmode"]; + if(mode == 1){ + //json["invokerid"], json["invokername"], json["invokeruid"] + const target_client_id = parseInt(json["target"]); + const target_own = target_client_id === this.connection.client.getClientId(); + + if(target_own && target_client_id === json["invokerid"]) { + log.error(LogCategory.NETWORKING, tr("Received conversation message from invalid client id. Data: %o"), json); + return; + } + + const conversation_manager = this.connection_handler.side_bar.private_conversations(); + const conversation = conversation_manager.find_conversation({ + client_id: target_own ? parseInt(json["invokerid"]) : target_client_id, + unique_id: target_own ? json["invokeruid"] : undefined, + name: target_own ? json["invokername"] : undefined + }, { + create: target_own, + attach: target_own + }); + if(!conversation) { + log.error(LogCategory.NETWORKING, tr("Received conversation message for unknown conversation! (%s)"), target_own ? tr("Remote message") : tr("Own message")); + return; + } + + conversation.append_message(json["msg"], { + type: target_own ? "partner" : "self", + name: json["invokername"], + unique_id: json["invokeruid"], + client_id: parseInt(json["invokerid"]) + }); + + if(target_own) { + this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5}); + const client = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"])); + if(client) /* the client itself might be invisible */ + client.flag_text_unread = conversation.is_unread(); + } else { + this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5}); + } + } else if(mode == 2) { + const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"])); + const own_channel_id = this.connection.client.getClient().currentChannel().channelId; + const channel_id = typeof(json["cid"]) !== "undefined" ? parseInt(json["cid"]) : own_channel_id; + const channel = this.connection_handler.channelTree.findChannel(channel_id) || this.connection_handler.getClient().currentChannel(); + + if(json["invokerid"] == this.connection.client.clientId) + this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5}); + else if(channel_id == own_channel_id) { + this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5}); + } + + const conversations = this.connection_handler.side_bar.channel_conversations(); + const conversation = conversations.conversation(channel_id); + conversation.register_new_message({ + sender_database_id: invoker ? invoker.properties.client_database_id : 0, + sender_name: json["invokername"], + sender_unique_id: json["invokeruid"], + + timestamp: typeof(json["timestamp"]) === "undefined" ? Date.now() : parseInt(json["timestamp"]), + message: json["msg"] + }); + if(conversation.is_unread() && channel) + channel.flag_text_unread = true; + } else if(mode == 3) { + this.connection_handler.log.log(server_log.Type.GLOBAL_MESSAGE, { + message: json["msg"], + sender: { + client_unique_id: json["invokeruid"], + client_name: json["invokername"], + client_id: parseInt(json["invokerid"]) + } + }); + + const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"])); + const conversations = this.connection_handler.side_bar.channel_conversations(); + const conversation = conversations.conversation(0); + conversation.register_new_message({ + sender_database_id: invoker ? invoker.properties.client_database_id : 0, + sender_name: json["invokername"], + sender_unique_id: json["invokeruid"], + + timestamp: typeof(json["timestamp"]) === "undefined" ? Date.now() : parseInt(json["timestamp"]), + message: json["msg"] + }); + this.connection_handler.channelTree.server.flag_text_unread = conversation.is_unread(); + } + } + + notifyClientChatComposing(json) { + json = json[0]; + + const conversation_manager = this.connection_handler.side_bar.private_conversations(); + const conversation = conversation_manager.find_conversation({ + client_id: parseInt(json["clid"]), + unique_id: json["cluid"], + name: undefined + }, { + create: false, + attach: false + }); + if(!conversation) + return; + + conversation.trigger_typing(); + } + + handleNotifyClientChatClosed(json) { + json = json[0]; //Only one bulk + + //Chat partner has closed the conversation + + //clid: "6" + //cluid: "YoWmG+dRGKD+Rxb7SPLAM5+B9tY=" + + const conversation_manager = this.connection_handler.side_bar.private_conversations(); + const conversation = conversation_manager.find_conversation({ + client_id: parseInt(json["clid"]), + unique_id: json["cluid"], + name: undefined + }, { + create: false, + attach: false + }); + if(!conversation) { + log.warn(LogCategory.GENERAL, tr("Received chat close for client, but we haven't a chat open.")); + return; + } + conversation.set_state(PrivateConversationState.CLOSED); + } + + handleNotifyClientUpdated(json) { + json = json[0]; //Only one bulk + + let client = this.connection.client.channelTree.findClient(json["clid"]); + if(!client) { + log.error(LogCategory.NETWORKING, tr("Tried to update an non existing client")); + return; + } + + let updates: { + key: string, + value: string + }[] = []; + for(let key in json) { + if(key == "clid") continue; + updates.push({key: key, value: json[key]}); + } + client.updateVariables(...updates); + } + + handleNotifyServerEdited(json) { + json = json[0]; + + let updates: { + key: string, + value: string + }[] = []; + for(let key in json) { + if(key === "invokerid") continue; + if(key === "invokername") continue; + if(key === "invokeruid") continue; + if(key === "reasonid") continue; + + updates.push({key: key, value: json[key]}); + } + this.connection.client.channelTree.server.updateVariables(false, ...updates); + } + + handleNotifyServerUpdated(json) { + json = json[0]; + + let updates: { + key: string, + value: string + }[] = []; + for(let key in json) { + if(key === "invokerid") continue; + if(key === "invokername") continue; + if(key === "invokeruid") continue; + if(key === "reasonid") continue; + + updates.push({key: key, value: json[key]}); + } + this.connection.client.channelTree.server.updateVariables(true, ...updates); + } + + handleNotifyMusicPlayerInfo(json) { + json = json[0]; + + let bot = this.connection.client.channelTree.find_client_by_dbid(json["bot_id"]); + if(!bot || !(bot instanceof MusicClientEntry)) { + log.warn(LogCategory.CLIENT, tr("Got music player info for unknown or invalid bot! (ID: %i, Entry: %o)"), json["bot_id"], bot); + return; + } + + bot.handlePlayerInfo(json); + } + + handleNotifyClientPoke(json) { + json = json[0]; + spawnPoke(this.connection_handler, { + id: parseInt(json["invokerid"]), + name: json["invokername"], + unique_id: json["invokeruid"] + }, json["msg"]); + + this.connection_handler.sound.play(Sound.USER_POKED_SELF); + } + + //TODO server chat message + handleNotifyServerGroupClientAdd(json) { + json = json[0]; + + const self = this.connection.client.getClient(); + if(json["clid"] == self.clientId()) + this.connection_handler.sound.play(Sound.GROUP_SERVER_ASSIGNED_SELF); + } + + //TODO server chat message + handleNotifyServerGroupClientRemove(json) { + json = json[0]; + + const self = this.connection.client.getClient(); + if(json["clid"] == self.clientId()) { + this.connection_handler.sound.play(Sound.GROUP_SERVER_REVOKED_SELF); + } else { + } + } + + //TODO server chat message + handleNotifyClientChannelGroupChanged(json) { + json = json[0]; + + const self = this.connection.client.getClient(); + if(json["clid"] == self.clientId()) { + this.connection_handler.sound.play(Sound.GROUP_CHANNEL_CHANGED_SELF); + } + } + + handleNotifyChannelSubscribed(json) { + for(const entry of json) { + const channel = this.connection.client.channelTree.findChannel(entry["cid"]); + if(!channel) { + console.warn(tr("Received channel subscribed for not visible channel (cid: %d)"), entry['cid']); + continue; + } + + channel.flag_subscribed = true; + } + } + + handleNotifyChannelUnsubscribed(json) { + for(const entry of json) { + const channel = this.connection.client.channelTree.findChannel(entry["cid"]); + if(!channel) { + console.warn(tr("Received channel unsubscribed for not visible channel (cid: %d)"), entry['cid']); + continue; + } + + channel.flag_subscribed = false; + for(const client of channel.clients(false)) + this.connection.client.channelTree.deleteClient(client); + } + } + + handleNotifyConversationHistory(json: any[]) { + const conversations = this.connection.client.side_bar.channel_conversations(); + const conversation = conversations.conversation(parseInt(json[0]["cid"])); + if(!conversation) { + log.warn(LogCategory.NETWORKING, tr("Received conversation history for invalid or unknown conversation (%o)"), json[0]["cid"]); + return; + } + + for(const entry of json) { + conversation.register_new_message({ + message: entry["msg"], + sender_unique_id: entry["sender_unique_id"], + sender_name: entry["sender_name"], + timestamp: parseInt(entry["timestamp"]), + sender_database_id: parseInt(entry["sender_database_id"]) + }, false); + } + + /* now update the boxes */ + /* No update needed because the command which triggers this notify should update the chat box on success + conversation.fix_scroll(true); + conversation.handle.update_chat_box(); + */ + } + + handleNotifyConversationMessageDelete(json: any[]) { + let conversation: Conversation; + const conversations = this.connection.client.side_bar.channel_conversations(); + for(const entry of json) { + if(typeof(entry["cid"]) !== "undefined") + conversation = conversations.conversation(parseInt(entry["cid"]), false); + if(!conversation) + continue; + + conversation.delete_messages(parseInt(entry["timestamp_begin"]), parseInt(entry["timestamp_end"]), parseInt(entry["cldbid"]), parseInt(entry["limit"])); + } + } + + handleNotifyMusicStatusUpdate(json: any[]) { + json = json[0]; + + const bot_id = parseInt(json["bot_id"]); + const client = this.connection.client.channelTree.find_client_by_dbid(bot_id); + if(!client) { + log.warn(LogCategory.CLIENT, tr("Received music bot status update for unknown bot (%d)"), bot_id); + return; + } + + client.events.fire("music_status_update", { + player_replay_index: parseInt(json["player_replay_index"]), + player_buffered_index: parseInt(json["player_buffered_index"]) + }); + } + + handleMusicPlayerSongChange(json: any[]) { + json = json[0]; + + const bot_id = parseInt(json["bot_id"]); + const client = this.connection.client.channelTree.find_client_by_dbid(bot_id); + if(!client) { + log.warn(LogCategory.CLIENT, tr("Received music bot status update for unknown bot (%d)"), bot_id); + return; + } + + const song_id = parseInt(json["song_id"]); + let song: SongInfo; + if(song_id) { + song = new SongInfo(); + JSON.map_to(song, json); + } + + client.events.fire("music_song_change", { + song: song + }); + } + + handleNotifyPlaylistSongAdd(json: any[]) { + json = json[0]; + + const playlist_id = parseInt(json["playlist_id"]); + const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id); + if(!client) { + log.warn(LogCategory.CLIENT, tr("Received playlist song add event, but we've no music bot for the playlist (%d)"), playlist_id); + return; + } + + client.events.fire("playlist_song_add", { + song: { + song_id: parseInt(json["song_id"]), + song_invoker: json["song_invoker"], + song_previous_song_id: parseInt(json["song_previous_song_id"]), + song_url: json["song_url"], + song_url_loader: json["song_url_loader"], + + song_loaded: json["song_loaded"] == true || json["song_loaded"] == "1", + song_metadata: json["song_metadata"] + } + }); + } + + handleNotifyPlaylistSongRemove(json: any[]) { + json = json[0]; + + const playlist_id = parseInt(json["playlist_id"]); + const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id); + if(!client) { + log.warn(LogCategory.CLIENT, tr("Received playlist song remove event, but we've no music bot for the playlist (%d)"), playlist_id); + return; + } + + const song_id = parseInt(json["song_id"]); + client.events.fire("playlist_song_remove", { song_id: song_id }); + } + + handleNotifyPlaylistSongReorder(json: any[]) { + json = json[0]; + + const playlist_id = parseInt(json["playlist_id"]); + const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id); + if(!client) { + log.warn(LogCategory.CLIENT, tr("Received playlist song reorder event, but we've no music bot for the playlist (%d)"), playlist_id); + return; + } + + const song_id = parseInt(json["song_id"]); + const previous_song_id = parseInt(json["song_previous_song_id"]); + client.events.fire("playlist_song_reorder", { song_id: song_id, previous_song_id: previous_song_id }); + } + + handleNotifyPlaylistSongLoaded(json: any[]) { + json = json[0]; + + const playlist_id = parseInt(json["playlist_id"]); + const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id); + if(!client) { + log.warn(LogCategory.CLIENT, tr("Received playlist song loaded event, but we've no music bot for the playlist (%d)"), playlist_id); + return; + } + + const song_id = parseInt(json["song_id"]); + client.events.fire("playlist_song_loaded", { + song_id: song_id, + success: json["success"] == 1, + error_msg: json["load_error_msg"], + metadata: json["song_metadata"] + }); + } } \ No newline at end of file diff --git a/shared/js/connection/CommandHelper.ts b/shared/js/connection/CommandHelper.ts index 9eff210b..6685c841 100644 --- a/shared/js/connection/CommandHelper.ts +++ b/shared/js/connection/CommandHelper.ts @@ -1,448 +1,461 @@ -namespace connection { - export class CommandHelper extends AbstractCommandHandler { - private _who_am_i: any; - private _awaiters_unique_ids: {[unique_id: string]:((resolved: ClientNameInfo) => any)[]} = {}; - private _awaiters_unique_dbid: {[database_id: number]:((resolved: ClientNameInfo) => any)[]} = {}; +import {ServerCommand, SingleCommandHandler} from "tc-shared/connection/ConnectionBase"; +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import { + ClientNameInfo, + CommandResult, + ErrorID, Playlist, PlaylistInfo, PlaylistSong, + QueryList, + QueryListEntry, ServerGroupClient +} from "tc-shared/connection/ServerConnectionDeclaration"; +import {ChannelEntry} from "tc-shared/ui/channel"; +import {ClientEntry} from "tc-shared/ui/client"; +import {ChatType} from "tc-shared/ui/frames/chat"; +import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler"; - constructor(connection) { - super(connection); +export class CommandHelper extends AbstractCommandHandler { + private _who_am_i: any; + private _awaiters_unique_ids: {[unique_id: string]:((resolved: ClientNameInfo) => any)[]} = {}; + private _awaiters_unique_dbid: {[database_id: number]:((resolved: ClientNameInfo) => any)[]} = {}; - this.volatile_handler_boss = false; - this.ignore_consumed = true; + constructor(connection) { + super(connection); + + this.volatile_handler_boss = false; + this.ignore_consumed = true; + } + + initialize() { + this.connection.command_handler_boss().register_handler(this); + } + + destroy() { + if(this.connection) { + const hboss = this.connection.command_handler_boss(); + hboss && hboss.unregister_handler(this); + } + this._awaiters_unique_ids = undefined; + } + + handle_command(command: ServerCommand): boolean { + if(command.command == "notifyclientnamefromuid") + this.handle_notifyclientnamefromuid(command.arguments); + if(command.command == "notifyclientgetnamefromdbid") + this.handle_notifyclientgetnamefromdbid(command.arguments); + else + return false; + return true; + } + + joinChannel(channel: ChannelEntry, password?: string) : Promise { + return this.connection.send_command("clientmove", { + "clid": this.connection.client.getClientId(), + "cid": channel.getChannelId(), + "cpw": password || "" + }); + } + + sendMessage(message: string, type: ChatType, target?: ChannelEntry | ClientEntry) : Promise { + if(type == ChatType.SERVER) + return this.connection.send_command("sendtextmessage", {"targetmode": 3, "target": 0, "msg": message}); + else if(type == ChatType.CHANNEL) + return this.connection.send_command("sendtextmessage", {"targetmode": 2, "target": (target as ChannelEntry).getChannelId(), "msg": message}); + else if(type == ChatType.CLIENT) + return this.connection.send_command("sendtextmessage", {"targetmode": 1, "target": (target as ClientEntry).clientId(), "msg": message}); + } + + updateClient(key: string, value: string) : Promise { + let data = {}; + data[key] = value; + return this.connection.send_command("clientupdate", data); + } + + async info_from_uid(..._unique_ids: string[]) : Promise { + const response: ClientNameInfo[] = []; + const request = []; + const unique_ids = new Set(_unique_ids); + if(!unique_ids.size) return []; + + const unique_id_resolvers: {[unique_id: string]: (resolved: ClientNameInfo) => any} = {}; + + + for(const unique_id of unique_ids) { + request.push({'cluid': unique_id}); + (this._awaiters_unique_ids[unique_id] || (this._awaiters_unique_ids[unique_id] = [])) + .push(unique_id_resolvers[unique_id] = info => response.push(info)); } - initialize() { - this.connection.command_handler_boss().register_handler(this); - } - - destroy() { - if(this.connection) { - const hboss = this.connection.command_handler_boss(); - hboss && hboss.unregister_handler(this); + try { + await this.connection.send_command("clientgetnamefromuid", request); + } catch(error) { + if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) { + /* nothing */ + } else { + throw error; } - this._awaiters_unique_ids = undefined; + } finally { + /* cleanup */ + for(const unique_id of Object.keys(unique_id_resolvers)) + (this._awaiters_unique_ids[unique_id] || []).remove(unique_id_resolvers[unique_id]); } - handle_command(command: connection.ServerCommand): boolean { - if(command.command == "notifyclientnamefromuid") - this.handle_notifyclientnamefromuid(command.arguments); - if(command.command == "notifyclientgetnamefromdbid") - this.handle_notifyclientgetnamefromdbid(command.arguments); - else - return false; - return true; + return response; + } + + private handle_notifyclientgetnamefromdbid(json: any[]) { + for(const entry of json) { + const info: ClientNameInfo = { + client_unique_id: entry["cluid"], + client_nickname: entry["clname"], + client_database_id: parseInt(entry["cldbid"]) + }; + + const functions = this._awaiters_unique_dbid[info.client_database_id] || []; + delete this._awaiters_unique_dbid[info.client_database_id]; + + for(const fn of functions) + fn(info); + } + } + + async info_from_cldbid(..._cldbid: number[]) : Promise { + const response: ClientNameInfo[] = []; + const request = []; + const unique_cldbid = new Set(_cldbid); + if(!unique_cldbid.size) return []; + + const unique_cldbid_resolvers: {[dbid: number]: (resolved: ClientNameInfo) => any} = {}; + + + for(const cldbid of unique_cldbid) { + request.push({'cldbid': cldbid}); + (this._awaiters_unique_dbid[cldbid] || (this._awaiters_unique_dbid[cldbid] = [])) + .push(unique_cldbid_resolvers[cldbid] = info => response.push(info)); } - joinChannel(channel: ChannelEntry, password?: string) : Promise { - return this.connection.send_command("clientmove", { - "clid": this.connection.client.getClientId(), - "cid": channel.getChannelId(), - "cpw": password || "" - }); + try { + await this.connection.send_command("clientgetnamefromdbid", request); + } catch(error) { + if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) { + /* nothing */ + } else { + throw error; + } + } finally { + /* cleanup */ + for(const cldbid of Object.keys(unique_cldbid_resolvers)) + (this._awaiters_unique_dbid[cldbid] || []).remove(unique_cldbid_resolvers[cldbid]); } - sendMessage(message: string, type: ChatType, target?: ChannelEntry | ClientEntry) : Promise { - if(type == ChatType.SERVER) - return this.connection.send_command("sendtextmessage", {"targetmode": 3, "target": 0, "msg": message}); - else if(type == ChatType.CHANNEL) - return this.connection.send_command("sendtextmessage", {"targetmode": 2, "target": (target as ChannelEntry).getChannelId(), "msg": message}); - else if(type == ChatType.CLIENT) - return this.connection.send_command("sendtextmessage", {"targetmode": 1, "target": (target as ClientEntry).clientId(), "msg": message}); - } + return response; + } + + private handle_notifyclientnamefromuid(json: any[]) { + for(const entry of json) { + const info: ClientNameInfo = { + client_unique_id: entry["cluid"], + client_nickname: entry["clname"], + client_database_id: parseInt(entry["cldbid"]) + }; + + const functions = this._awaiters_unique_ids[entry["cluid"]] || []; + delete this._awaiters_unique_ids[entry["cluid"]]; + + for(const fn of functions) + fn(info); + } + } + + request_query_list(server_id: number = undefined) : Promise { + return new Promise((resolve, reject) => { + const single_handler = { + command: "notifyquerylist", + function: command => { + const json = command.arguments; + + const result = {} as QueryList; + + result.flag_all = json[0]["flag_all"]; + result.flag_own = json[0]["flag_own"]; + result.queries = []; + + for(const entry of json) { + const rentry = {} as QueryListEntry; + rentry.bounded_server = parseInt(entry["client_bound_server"]); + rentry.username = entry["client_login_name"]; + rentry.unique_id = entry["client_unique_identifier"]; + + result.queries.push(rentry); + } + + resolve(result); + return true; + } + }; + this.handler_boss.register_single_handler(single_handler); - updateClient(key: string, value: string) : Promise { let data = {}; - data[key] = value; - return this.connection.send_command("clientupdate", data); - } + if(server_id !== undefined) + data["server_id"] = server_id; - async info_from_uid(..._unique_ids: string[]) : Promise { - const response: ClientNameInfo[] = []; - const request = []; - const unique_ids = new Set(_unique_ids); - if(!unique_ids.size) return []; + this.connection.send_command("querylist", data).catch(error => { + this.handler_boss.remove_single_handler(single_handler); - const unique_id_resolvers: {[unique_id: string]: (resolved: ClientNameInfo) => any} = {}; - - - for(const unique_id of unique_ids) { - request.push({'cluid': unique_id}); - (this._awaiters_unique_ids[unique_id] || (this._awaiters_unique_ids[unique_id] = [])) - .push(unique_id_resolvers[unique_id] = info => response.push(info)); - } - - try { - await this.connection.send_command("clientgetnamefromuid", request); - } catch(error) { - if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) { - /* nothing */ - } else { - throw error; + if(error instanceof CommandResult) { + if(error.id == ErrorID.EMPTY_RESULT) { + resolve(undefined); + return; + } } - } finally { - /* cleanup */ - for(const unique_id of Object.keys(unique_id_resolvers)) - (this._awaiters_unique_ids[unique_id] || []).remove(unique_id_resolvers[unique_id]); - } + reject(error); + }); + }); + } - return response; - } + request_playlist_list() : Promise { + return new Promise((resolve, reject) => { + const single_handler: SingleCommandHandler = { + command: "notifyplaylistlist", + function: command => { + const json = command.arguments; + const result: Playlist[] = []; - private handle_notifyclientgetnamefromdbid(json: any[]) { - for(const entry of json) { - const info: ClientNameInfo = { - client_unique_id: entry["cluid"], - client_nickname: entry["clname"], - client_database_id: parseInt(entry["cldbid"]) - }; + for(const entry of json) { + try { + result.push({ + playlist_id: parseInt(entry["playlist_id"]), + playlist_bot_id: parseInt(entry["playlist_bot_id"]), + playlist_title: entry["playlist_title"], + playlist_type: parseInt(entry["playlist_type"]), + playlist_owner_dbid: parseInt(entry["playlist_owner_dbid"]), + playlist_owner_name: entry["playlist_owner_name"], - const functions = this._awaiters_unique_dbid[info.client_database_id] || []; - delete this._awaiters_unique_dbid[info.client_database_id]; + needed_power_modify: parseInt(entry["needed_power_modify"]), + needed_power_permission_modify: parseInt(entry["needed_power_permission_modify"]), + needed_power_delete: parseInt(entry["needed_power_delete"]), + needed_power_song_add: parseInt(entry["needed_power_song_add"]), + needed_power_song_move: parseInt(entry["needed_power_song_move"]), + needed_power_song_remove: parseInt(entry["needed_power_song_remove"]) + }); + } catch(error) { + log.error(LogCategory.NETWORKING, tr("Failed to parse playlist entry: %o"), error); + } + } - for(const fn of functions) - fn(info); - } - } - - async info_from_cldbid(..._cldbid: number[]) : Promise { - const response: ClientNameInfo[] = []; - const request = []; - const unique_cldbid = new Set(_cldbid); - if(!unique_cldbid.size) return []; - - const unique_cldbid_resolvers: {[dbid: number]: (resolved: ClientNameInfo) => any} = {}; - - - for(const cldbid of unique_cldbid) { - request.push({'cldbid': cldbid}); - (this._awaiters_unique_dbid[cldbid] || (this._awaiters_unique_dbid[cldbid] = [])) - .push(unique_cldbid_resolvers[cldbid] = info => response.push(info)); - } - - try { - await this.connection.send_command("clientgetnamefromdbid", request); - } catch(error) { - if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) { - /* nothing */ - } else { - throw error; + resolve(result); + return true; } - } finally { - /* cleanup */ - for(const cldbid of Object.keys(unique_cldbid_resolvers)) - (this._awaiters_unique_dbid[cldbid] || []).remove(unique_cldbid_resolvers[cldbid]); - } + }; + this.handler_boss.register_single_handler(single_handler); - return response; - } + this.connection.send_command("playlistlist").catch(error => { + this.handler_boss.remove_single_handler(single_handler); - private handle_notifyclientnamefromuid(json: any[]) { - for(const entry of json) { - const info: ClientNameInfo = { - client_unique_id: entry["cluid"], - client_nickname: entry["clname"], - client_database_id: parseInt(entry["cldbid"]) - }; - - const functions = this._awaiters_unique_ids[entry["cluid"]] || []; - delete this._awaiters_unique_ids[entry["cluid"]]; - - for(const fn of functions) - fn(info); - } - } - - request_query_list(server_id: number = undefined) : Promise { - return new Promise((resolve, reject) => { - const single_handler = { - command: "notifyquerylist", - function: command => { - const json = command.arguments; - - const result = {} as QueryList; - - result.flag_all = json[0]["flag_all"]; - result.flag_own = json[0]["flag_own"]; - result.queries = []; - - for(const entry of json) { - const rentry = {} as QueryListEntry; - rentry.bounded_server = parseInt(entry["client_bound_server"]); - rentry.username = entry["client_login_name"]; - rentry.unique_id = entry["client_unique_identifier"]; - - result.queries.push(rentry); - } - - resolve(result); - return true; - } - }; - this.handler_boss.register_single_handler(single_handler); - - let data = {}; - if(server_id !== undefined) - data["server_id"] = server_id; - - this.connection.send_command("querylist", data).catch(error => { - this.handler_boss.remove_single_handler(single_handler); - - if(error instanceof CommandResult) { - if(error.id == ErrorID.EMPTY_RESULT) { - resolve(undefined); - return; - } - } - reject(error); - }); - }); - } - - request_playlist_list() : Promise { - return new Promise((resolve, reject) => { - const single_handler: SingleCommandHandler = { - command: "notifyplaylistlist", - function: command => { - const json = command.arguments; - const result: Playlist[] = []; - - for(const entry of json) { - try { - result.push({ - playlist_id: parseInt(entry["playlist_id"]), - playlist_bot_id: parseInt(entry["playlist_bot_id"]), - playlist_title: entry["playlist_title"], - playlist_type: parseInt(entry["playlist_type"]), - playlist_owner_dbid: parseInt(entry["playlist_owner_dbid"]), - playlist_owner_name: entry["playlist_owner_name"], - - needed_power_modify: parseInt(entry["needed_power_modify"]), - needed_power_permission_modify: parseInt(entry["needed_power_permission_modify"]), - needed_power_delete: parseInt(entry["needed_power_delete"]), - needed_power_song_add: parseInt(entry["needed_power_song_add"]), - needed_power_song_move: parseInt(entry["needed_power_song_move"]), - needed_power_song_remove: parseInt(entry["needed_power_song_remove"]) - }); - } catch(error) { - log.error(LogCategory.NETWORKING, tr("Failed to parse playlist entry: %o"), error); - } - } - - resolve(result); - return true; - } - }; - this.handler_boss.register_single_handler(single_handler); - - this.connection.send_command("playlistlist").catch(error => { - this.handler_boss.remove_single_handler(single_handler); - - if(error instanceof CommandResult) { - if(error.id == ErrorID.EMPTY_RESULT) { - resolve([]); - return; - } - } - reject(error); - }) - }); - } - - request_playlist_songs(playlist_id: number) : Promise { - return new Promise((resolve, reject) => { - const single_handler: SingleCommandHandler = { - command: "notifyplaylistsonglist", - function: command => { - const json = command.arguments; - - if(json[0]["playlist_id"] != playlist_id) { - log.error(LogCategory.NETWORKING, tr("Received invalid notification for playlist songs")); - return false; - } - - const result: PlaylistSong[] = []; - - for(const entry of json) { - try { - result.push({ - song_id: parseInt(entry["song_id"]), - song_invoker: entry["song_invoker"], - song_previous_song_id: parseInt(entry["song_previous_song_id"]), - song_url: entry["song_url"], - song_url_loader: entry["song_url_loader"], - - song_loaded: entry["song_loaded"] == true || entry["song_loaded"] == "1", - song_metadata: entry["song_metadata"] - }); - } catch(error) { - log.error(LogCategory.NETWORKING, tr("Failed to parse playlist song entry: %o"), error); - } - } - - resolve(result); - return true; - } - }; - this.handler_boss.register_single_handler(single_handler); - - this.connection.send_command("playlistsonglist", {playlist_id: playlist_id}).catch(error => { - this.handler_boss.remove_single_handler(single_handler); - if(error instanceof CommandResult) { - if(error.id == ErrorID.EMPTY_RESULT) { - resolve([]); - return; - } - } - reject(error); - }) - }); - } - - request_playlist_client_list(playlist_id: number) : Promise { - return new Promise((resolve, reject) => { - const single_handler: SingleCommandHandler = { - command: "notifyplaylistclientlist", - function: command => { - const json = command.arguments; - - if(json[0]["playlist_id"] != playlist_id) { - log.error(LogCategory.NETWORKING, tr("Received invalid notification for playlist clients")); - return false; - } - - const result: number[] = []; - - for(const entry of json) - result.push(parseInt(entry["cldbid"])); - - resolve(result.filter(e => !isNaN(e))); - return true; - } - }; - this.handler_boss.register_single_handler(single_handler); - - this.connection.send_command("playlistclientlist", {playlist_id: playlist_id}).catch(error => { - this.handler_boss.remove_single_handler(single_handler); - if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) { + if(error instanceof CommandResult) { + if(error.id == ErrorID.EMPTY_RESULT) { resolve([]); return; } - reject(error); - }) - }); - } + } + reject(error); + }) + }); + } - async request_clients_by_server_group(group_id: number) : Promise { - //servergroupclientlist sgid=2 - //notifyservergroupclientlist sgid=6 cldbid=2 client_nickname=WolverinDEV client_unique_identifier=xxjnc14LmvTk+Lyrm8OOeo4tOqw= - return new Promise((resolve, reject) => { - const single_handler: SingleCommandHandler = { - command: "notifyservergroupclientlist", - function: command => { - if (command.arguments[0]["sgid"] != group_id) { - log.error(LogCategory.NETWORKING, tr("Received invalid notification for server group client list")); - return false; - } + request_playlist_songs(playlist_id: number) : Promise { + return new Promise((resolve, reject) => { + const single_handler: SingleCommandHandler = { + command: "notifyplaylistsonglist", + function: command => { + const json = command.arguments; - try { - const result: ServerGroupClient[] = []; - for(const entry of command.arguments) - result.push({ - client_database_id: parseInt(entry["cldbid"]), - client_nickname: entry["client_nickname"], - client_unique_identifier: entry["client_unique_identifier"] - }); - resolve(result); - } catch (error) { - log.error(LogCategory.NETWORKING, tr("Failed to parse server group client list: %o"), error); - reject("failed to parse info"); - } - - return true; + if(json[0]["playlist_id"] != playlist_id) { + log.error(LogCategory.NETWORKING, tr("Received invalid notification for playlist songs")); + return false; } - }; - this.handler_boss.register_single_handler(single_handler); - this.connection.send_command("servergroupclientlist", {sgid: group_id}).catch(error => { - this.handler_boss.remove_single_handler(single_handler); - reject(error); - }) - }); - } - - request_playlist_info(playlist_id: number) : Promise { - return new Promise((resolve, reject) => { - const single_handler: SingleCommandHandler = { - command: "notifyplaylistinfo", - function: command => { - const json = command.arguments[0]; - if (json["playlist_id"] != playlist_id) { - log.error(LogCategory.NETWORKING, tr("Received invalid notification for playlist info")); - return; - } + const result: PlaylistSong[] = []; + for(const entry of json) { try { - //resolve - resolve({ - playlist_id: parseInt(json["playlist_id"]), - playlist_title: json["playlist_title"], - playlist_description: json["playlist_description"], - playlist_type: parseInt(json["playlist_type"]), + result.push({ + song_id: parseInt(entry["song_id"]), + song_invoker: entry["song_invoker"], + song_previous_song_id: parseInt(entry["song_previous_song_id"]), + song_url: entry["song_url"], + song_url_loader: entry["song_url_loader"], - playlist_owner_dbid: parseInt(json["playlist_owner_dbid"]), - playlist_owner_name: json["playlist_owner_name"], - - playlist_flag_delete_played: json["playlist_flag_delete_played"] == true || json["playlist_flag_delete_played"] == "1", - playlist_flag_finished: json["playlist_flag_finished"] == true || json["playlist_flag_finished"] == "1", - playlist_replay_mode: parseInt(json["playlist_replay_mode"]), - playlist_current_song_id: parseInt(json["playlist_current_song_id"]), - - playlist_max_songs: parseInt(json["playlist_max_songs"]) + song_loaded: entry["song_loaded"] == true || entry["song_loaded"] == "1", + song_metadata: entry["song_metadata"] }); - } catch (error) { - log.error(LogCategory.NETWORKING, tr("Failed to parse playlist info: %o"), error); - reject("failed to parse info"); + } catch(error) { + log.error(LogCategory.NETWORKING, tr("Failed to parse playlist song entry: %o"), error); } - - return true; } - }; - this.handler_boss.register_single_handler(single_handler); - this.connection.send_command("playlistinfo", {playlist_id: playlist_id}).catch(error => { - this.handler_boss.remove_single_handler(single_handler); - reject(error); - }) - }); - } + resolve(result); + return true; + } + }; + this.handler_boss.register_single_handler(single_handler); - /** - * @deprecated - * Its just a workaround for the query management. - * There is no garante that the whoami trick will work forever - */ - current_virtual_server_id() : Promise { - if(this._who_am_i) - return Promise.resolve(parseInt(this._who_am_i["virtualserver_id"])); - - return new Promise((resolve, reject) => { - const single_handler: SingleCommandHandler = { - function: command => { - if(command.command != "" && command.command.indexOf("=") == -1) - return false; - - this._who_am_i = command.arguments[0]; - resolve(parseInt(this._who_am_i["virtualserver_id"])); - return true; + this.connection.send_command("playlistsonglist", {playlist_id: playlist_id}).catch(error => { + this.handler_boss.remove_single_handler(single_handler); + if(error instanceof CommandResult) { + if(error.id == ErrorID.EMPTY_RESULT) { + resolve([]); + return; } - }; - this.handler_boss.register_single_handler(single_handler); + } + reject(error); + }) + }); + } - this.connection.send_command("whoami").catch(error => { - this.handler_boss.remove_single_handler(single_handler); - reject(error); - }); + request_playlist_client_list(playlist_id: number) : Promise { + return new Promise((resolve, reject) => { + const single_handler: SingleCommandHandler = { + command: "notifyplaylistclientlist", + function: command => { + const json = command.arguments; + + if(json[0]["playlist_id"] != playlist_id) { + log.error(LogCategory.NETWORKING, tr("Received invalid notification for playlist clients")); + return false; + } + + const result: number[] = []; + + for(const entry of json) + result.push(parseInt(entry["cldbid"])); + + resolve(result.filter(e => !isNaN(e))); + return true; + } + }; + this.handler_boss.register_single_handler(single_handler); + + this.connection.send_command("playlistclientlist", {playlist_id: playlist_id}).catch(error => { + this.handler_boss.remove_single_handler(single_handler); + if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) { + resolve([]); + return; + } + reject(error); + }) + }); + } + + async request_clients_by_server_group(group_id: number) : Promise { + //servergroupclientlist sgid=2 + //notifyservergroupclientlist sgid=6 cldbid=2 client_nickname=WolverinDEV client_unique_identifier=xxjnc14LmvTk+Lyrm8OOeo4tOqw= + return new Promise((resolve, reject) => { + const single_handler: SingleCommandHandler = { + command: "notifyservergroupclientlist", + function: command => { + if (command.arguments[0]["sgid"] != group_id) { + log.error(LogCategory.NETWORKING, tr("Received invalid notification for server group client list")); + return false; + } + + try { + const result: ServerGroupClient[] = []; + for(const entry of command.arguments) + result.push({ + client_database_id: parseInt(entry["cldbid"]), + client_nickname: entry["client_nickname"], + client_unique_identifier: entry["client_unique_identifier"] + }); + resolve(result); + } catch (error) { + log.error(LogCategory.NETWORKING, tr("Failed to parse server group client list: %o"), error); + reject("failed to parse info"); + } + + return true; + } + }; + this.handler_boss.register_single_handler(single_handler); + + this.connection.send_command("servergroupclientlist", {sgid: group_id}).catch(error => { + this.handler_boss.remove_single_handler(single_handler); + reject(error); + }) + }); + } + + request_playlist_info(playlist_id: number) : Promise { + return new Promise((resolve, reject) => { + const single_handler: SingleCommandHandler = { + command: "notifyplaylistinfo", + function: command => { + const json = command.arguments[0]; + if (json["playlist_id"] != playlist_id) { + log.error(LogCategory.NETWORKING, tr("Received invalid notification for playlist info")); + return; + } + + try { + //resolve + resolve({ + playlist_id: parseInt(json["playlist_id"]), + playlist_title: json["playlist_title"], + playlist_description: json["playlist_description"], + playlist_type: parseInt(json["playlist_type"]), + + playlist_owner_dbid: parseInt(json["playlist_owner_dbid"]), + playlist_owner_name: json["playlist_owner_name"], + + playlist_flag_delete_played: json["playlist_flag_delete_played"] == true || json["playlist_flag_delete_played"] == "1", + playlist_flag_finished: json["playlist_flag_finished"] == true || json["playlist_flag_finished"] == "1", + playlist_replay_mode: parseInt(json["playlist_replay_mode"]), + playlist_current_song_id: parseInt(json["playlist_current_song_id"]), + + playlist_max_songs: parseInt(json["playlist_max_songs"]) + }); + } catch (error) { + log.error(LogCategory.NETWORKING, tr("Failed to parse playlist info: %o"), error); + reject("failed to parse info"); + } + + return true; + } + }; + this.handler_boss.register_single_handler(single_handler); + + this.connection.send_command("playlistinfo", {playlist_id: playlist_id}).catch(error => { + this.handler_boss.remove_single_handler(single_handler); + reject(error); + }) + }); + } + + /** + * @deprecated + * Its just a workaround for the query management. + * There is no garante that the whoami trick will work forever + */ + current_virtual_server_id() : Promise { + if(this._who_am_i) + return Promise.resolve(parseInt(this._who_am_i["virtualserver_id"])); + + return new Promise((resolve, reject) => { + const single_handler: SingleCommandHandler = { + function: command => { + if(command.command != "" && command.command.indexOf("=") == -1) + return false; + + this._who_am_i = command.arguments[0]; + resolve(parseInt(this._who_am_i["virtualserver_id"])); + return true; + } + }; + this.handler_boss.register_single_handler(single_handler); + + this.connection.send_command("whoami").catch(error => { + this.handler_boss.remove_single_handler(single_handler); + reject(error); }); - } + }); } } \ No newline at end of file diff --git a/shared/js/connection/ConnectionBase.ts b/shared/js/connection/ConnectionBase.ts index cf975fdb..d2a87040 100644 --- a/shared/js/connection/ConnectionBase.ts +++ b/shared/js/connection/ConnectionBase.ts @@ -1,216 +1,129 @@ -namespace connection { - export interface CommandOptions { - flagset?: string[]; /* default: [] */ - process_result?: boolean; /* default: true */ +import {CommandHelper} from "tc-shared/connection/CommandHelper"; +import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler"; +import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; +import {ServerAddress} from "tc-shared/ui/server"; +import {RecorderProfile} from "tc-shared/voice/RecorderProfile"; +import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler"; +import {AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler"; - timeout?: number /* default: 1000 */; +export interface CommandOptions { + flagset?: string[]; /* default: [] */ + process_result?: boolean; /* default: true */ + + timeout?: number /* default: 1000 */; +} +export const CommandOptionDefaults: CommandOptions = { + flagset: [], + process_result: true, + timeout: 1000 +}; + +export type ConnectionStateListener = (old_state: ConnectionState, new_state: ConnectionState) => any; +export abstract class AbstractServerConnection { + readonly client: ConnectionHandler; + readonly command_helper: CommandHelper; + + protected constructor(client: ConnectionHandler) { + this.client = client; + + this.command_helper = new CommandHelper(this); } - export const CommandOptionDefaults: CommandOptions = { - flagset: [], - process_result: true, - timeout: 1000 + + /* resolved as soon a connection has been established. This does not means that the authentication had yet been done! */ + abstract connect(address: ServerAddress, handshake: HandshakeHandler, timeout?: number) : Promise; + + abstract connected() : boolean; + abstract disconnect(reason?: string) : Promise; + + abstract support_voice() : boolean; + abstract voice_connection() : voice.AbstractVoiceConnection | undefined; + + abstract command_handler_boss() : AbstractCommandHandlerBoss; + abstract send_command(command: string, data?: any | any[], options?: CommandOptions) : Promise; + + abstract get onconnectionstatechanged() : ConnectionStateListener; + abstract set onconnectionstatechanged(listener: ConnectionStateListener); + + abstract remote_address() : ServerAddress; /* only valid when connected */ + abstract handshake_handler() : HandshakeHandler; /* only valid when connected */ + + abstract ping() : { + native: number, + javascript?: number }; +} - export type ConnectionStateListener = (old_state: ConnectionState, new_state: ConnectionState) => any; - export abstract class AbstractServerConnection { - readonly client: ConnectionHandler; - readonly command_helper: CommandHelper; +export namespace voice { + export enum PlayerState { + PREBUFFERING, + PLAYING, + BUFFERING, + STOPPING, + STOPPED + } - protected constructor(client: ConnectionHandler) { - this.client = client; + export type LatencySettings = { + min_buffer: number; /* milliseconds */ + max_buffer: number; /* milliseconds */ + } - this.command_helper = new CommandHelper(this); + export interface VoiceClient { + client_id: number; + + callback_playback: () => any; + callback_stopped: () => any; + + callback_state_changed: (new_state: PlayerState) => any; + + get_state() : PlayerState; + + get_volume() : number; + set_volume(volume: number) : void; + + abort_replay(); + + support_latency_settings() : boolean; + + reset_latency_settings(); + latency_settings(settings?: LatencySettings) : LatencySettings; + + support_flush() : boolean; + flush(); + } + + export abstract class AbstractVoiceConnection { + readonly connection: AbstractServerConnection; + + protected constructor(connection: AbstractServerConnection) { + this.connection = connection; } - /* resolved as soon a connection has been established. This does not means that the authentication had yet been done! */ - abstract connect(address: ServerAddress, handshake: HandshakeHandler, timeout?: number) : Promise; - abstract connected() : boolean; - abstract disconnect(reason?: string) : Promise; + abstract encoding_supported(codec: number) : boolean; + abstract decoding_supported(codec: number) : boolean; - abstract support_voice() : boolean; - abstract voice_connection() : voice.AbstractVoiceConnection | undefined; + abstract register_client(client_id: number) : VoiceClient; + abstract available_clients() : VoiceClient[]; + abstract unregister_client(client: VoiceClient) : Promise; - abstract command_handler_boss() : AbstractCommandHandlerBoss; - abstract send_command(command: string, data?: any | any[], options?: CommandOptions) : Promise; + abstract voice_recorder() : RecorderProfile; + abstract acquire_voice_recorder(recorder: RecorderProfile | undefined) : Promise; - abstract get onconnectionstatechanged() : ConnectionStateListener; - abstract set onconnectionstatechanged(listener: ConnectionStateListener); - - abstract remote_address() : ServerAddress; /* only valid when connected */ - abstract handshake_handler() : HandshakeHandler; /* only valid when connected */ - - abstract ping() : { - native: number, - javascript?: number - }; + abstract get_encoder_codec() : number; + abstract set_encoder_codec(codec: number); } +} - export namespace voice { - export enum PlayerState { - PREBUFFERING, - PLAYING, - BUFFERING, - STOPPING, - STOPPED - } +export class ServerCommand { + command: string; + arguments: any[]; +} - export type LatencySettings = { - min_buffer: number; /* milliseconds */ - max_buffer: number; /* milliseconds */ - } +export interface SingleCommandHandler { + name?: string; + command?: string; + timeout?: number; - export interface VoiceClient { - client_id: number; - - callback_playback: () => any; - callback_stopped: () => any; - - callback_state_changed: (new_state: PlayerState) => any; - - get_state() : PlayerState; - - get_volume() : number; - set_volume(volume: number) : void; - - abort_replay(); - - support_latency_settings() : boolean; - - reset_latency_settings(); - latency_settings(settings?: LatencySettings) : LatencySettings; - - support_flush() : boolean; - flush(); - } - - export abstract class AbstractVoiceConnection { - readonly connection: AbstractServerConnection; - - protected constructor(connection: AbstractServerConnection) { - this.connection = connection; - } - - abstract connected() : boolean; - abstract encoding_supported(codec: number) : boolean; - abstract decoding_supported(codec: number) : boolean; - - abstract register_client(client_id: number) : VoiceClient; - abstract available_clients() : VoiceClient[]; - abstract unregister_client(client: VoiceClient) : Promise; - - abstract voice_recorder() : RecorderProfile; - abstract acquire_voice_recorder(recorder: RecorderProfile | undefined) : Promise; - - abstract get_encoder_codec() : number; - abstract set_encoder_codec(codec: number); - } - } - - export class ServerCommand { - command: string; - arguments: any[]; - } - - export abstract class AbstractCommandHandler { - readonly connection: AbstractServerConnection; - - handler_boss: AbstractCommandHandlerBoss | undefined; - volatile_handler_boss: boolean = false; /* if true than the command handler could be registered twice to two or more handlers */ - - ignore_consumed: boolean = false; - - protected constructor(connection: AbstractServerConnection) { - this.connection = connection; - } - - /** - * @return If the command should be consumed - */ - abstract handle_command(command: ServerCommand) : boolean; - } - - export interface SingleCommandHandler { - name?: string; - command?: string; - timeout?: number; - - /* if the return is true then the command handler will be removed */ - function: (command: ServerCommand) => boolean; - } - - export abstract class AbstractCommandHandlerBoss { - readonly connection: AbstractServerConnection; - protected command_handlers: AbstractCommandHandler[] = []; - /* TODO: Timeout */ - protected single_command_handler: SingleCommandHandler[] = []; - - protected constructor(connection: AbstractServerConnection) { - this.connection = connection; - } - - destroy() { - this.command_handlers = undefined; - this.single_command_handler = undefined; - } - - register_handler(handler: AbstractCommandHandler) { - if(!handler.volatile_handler_boss && handler.handler_boss) - throw "handler already registered"; - - this.command_handlers.remove(handler); /* just to be sure */ - this.command_handlers.push(handler); - handler.handler_boss = this; - } - - unregister_handler(handler: AbstractCommandHandler) { - if(!handler.volatile_handler_boss && handler.handler_boss !== this) { - console.warn(tr("Tried to unregister command handler which does not belong to the handler boss")); - return; - } - - this.command_handlers.remove(handler); - handler.handler_boss = undefined; - } - - - register_single_handler(handler: SingleCommandHandler) { - this.single_command_handler.push(handler); - } - - remove_single_handler(handler: SingleCommandHandler) { - this.single_command_handler.remove(handler); - } - - handlers() : AbstractCommandHandler[] { - return this.command_handlers; - } - - invoke_handle(command: ServerCommand) : boolean { - let flag_consumed = false; - - for(const handler of this.command_handlers) { - try { - if(!flag_consumed || handler.ignore_consumed) - flag_consumed = flag_consumed || handler.handle_command(command); - } catch(error) { - console.error(tr("Failed to invoke command handler. Invocation results in an exception: %o"), error); - } - } - - for(const handler of [...this.single_command_handler]) { - if(handler.command && handler.command != command.command) - continue; - - try { - if(handler.function(command)) - this.single_command_handler.remove(handler); - } catch(error) { - console.error(tr("Failed to invoke single command handler. Invocation results in an exception: %o"), error); - } - } - - return flag_consumed; - } - } + /* if the return is true then the command handler will be removed */ + function: (command: ServerCommand) => boolean; } \ No newline at end of file diff --git a/shared/js/connection/HandshakeHandler.ts b/shared/js/connection/HandshakeHandler.ts index de9c4910..14143346 100644 --- a/shared/js/connection/HandshakeHandler.ts +++ b/shared/js/connection/HandshakeHandler.ts @@ -1,146 +1,153 @@ -namespace connection { - export interface HandshakeIdentityHandler { - connection: AbstractServerConnection; +import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; +import {IdentitifyType} from "tc-shared/profiles/Identity"; +import {TeaSpeakIdentity} from "tc-shared/profiles/identities/TeamSpeakIdentity"; +import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase"; +import {ConnectionProfile} from "tc-shared/profiles/ConnectionProfile"; +import {settings} from "tc-shared/settings"; +import {ConnectParameters, DisconnectReason} from "tc-shared/ConnectionHandler"; - start_handshake(); - register_callback(callback: (success: boolean, message?: string) => any); +export interface HandshakeIdentityHandler { + connection: AbstractServerConnection; + + start_handshake(); + register_callback(callback: (success: boolean, message?: string) => any); +} + +declare const native_client; +export class HandshakeHandler { + private connection: AbstractServerConnection; + private handshake_handler: HandshakeIdentityHandler; + private failed = false; + + readonly profile: ConnectionProfile; + readonly parameters: ConnectParameters; + + constructor(profile: ConnectionProfile, parameters: ConnectParameters) { + this.profile = profile; + this.parameters = parameters; } - export class HandshakeHandler { - private connection: AbstractServerConnection; - private handshake_handler: HandshakeIdentityHandler; - private failed = false; + setConnection(con: AbstractServerConnection) { + this.connection = con; + } - readonly profile: profiles.ConnectionProfile; - readonly parameters: ConnectParameters; - - constructor(profile: profiles.ConnectionProfile, parameters: ConnectParameters) { - this.profile = profile; - this.parameters = parameters; + initialize() { + this.handshake_handler = this.profile.spawn_identity_handshake_handler(this.connection); + if(!this.handshake_handler) { + this.handshake_failed("failed to create identity handler"); + return; } - setConnection(con: AbstractServerConnection) { - this.connection = con; - } - - initialize() { - this.handshake_handler = this.profile.spawn_identity_handshake_handler(this.connection); - if(!this.handshake_handler) { - this.handshake_failed("failed to create identity handler"); - return; - } - - this.handshake_handler.register_callback((flag, message) => { - if(flag) - this.handshake_finished(); - else - this.handshake_failed(message); - }); - } - - get_identity_handler() : HandshakeIdentityHandler { - return this.handshake_handler; - } - - startHandshake() { - this.handshake_handler.start_handshake(); - } - - on_teamspeak() { - const type = this.profile.selected_type(); - if(type == profiles.identities.IdentitifyType.TEAMSPEAK) + this.handshake_handler.register_callback((flag, message) => { + if(flag) this.handshake_finished(); - else { + else + this.handshake_failed(message); + }); + } - if(this.failed) return; + get_identity_handler() : HandshakeIdentityHandler { + return this.handshake_handler; + } - this.failed = true; - this.connection.client.handleDisconnect(DisconnectReason.HANDSHAKE_TEAMSPEAK_REQUIRED); - } - } + startHandshake() { + this.handshake_handler.start_handshake(); + } + + on_teamspeak() { + const type = this.profile.selected_type(); + if(type == IdentitifyType.TEAMSPEAK) + this.handshake_finished(); + else { - private handshake_failed(message: string) { if(this.failed) return; this.failed = true; - this.connection.client.handleDisconnect(DisconnectReason.HANDSHAKE_FAILED, message); - } - - private handshake_finished(version?: string) { - const _native = window["native"]; - if(native_client && _native && _native.client_version && !version) { - _native.client_version() - .then( this.handshake_finished.bind(this)) - .catch(error => { - console.error(tr("Failed to get version:")); - console.error(error); - this.handshake_finished("?.?.?"); - }); - return; - } - - const git_version = settings.static_global("version", "unknown"); - const browser_name = (navigator.browserSpecs || {})["name"] || " "; - let data = { - client_nickname: this.parameters.nickname || "Another TeaSpeak user", - client_platform: (browser_name ? browser_name + " " : "") + navigator.platform, - client_version: "TeaWeb " + git_version + " (" + navigator.userAgent + ")", - client_version_sign: undefined, - - client_default_channel: (this.parameters.channel || {} as any).target, - client_default_channel_password: (this.parameters.channel || {} as any).password, - client_default_token: this.parameters.token, - - client_server_password: this.parameters.password ? this.parameters.password.password : undefined, - client_browser_engine: navigator.product, - - client_input_hardware: this.connection.client.client_status.input_hardware, - client_output_hardware: false, - client_input_muted: this.connection.client.client_status.input_muted, - client_output_muted: this.connection.client.client_status.output_muted, - }; - - //0.0.1 [Build: 1549713549] Linux 7XvKmrk7uid2ixHFeERGqcC8vupeQqDypLtw2lY9slDNPojEv//F47UaDLG+TmVk4r6S0TseIKefzBpiRtLDAQ== - - if(version) { - data.client_version = "TeaClient "; - data.client_version += " " + version; - - const os = require("os"); - const arch_mapping = { - "x32": "32bit", - "x64": "64bit" - }; - - data.client_version += " " + (arch_mapping[os.arch()] || os.arch()); - - const os_mapping = { - "win32": "Windows", - "linux": "Linux" - }; - data.client_platform = (os_mapping[os.platform()] || os.platform()); - } - - /* required to keep compatibility */ - if(this.profile.selected_type() === profiles.identities.IdentitifyType.TEAMSPEAK) { - data["client_key_offset"] = (this.profile.selected_identity() as profiles.identities.TeaSpeakIdentity).hash_number; - } - - this.connection.send_command("clientinit", data).catch(error => { - if(error instanceof CommandResult) { - if(error.id == 1028) { - this.connection.client.handleDisconnect(DisconnectReason.SERVER_REQUIRES_PASSWORD); - } else if(error.id == 783 || error.id == 519) { - error.extra_message = isNaN(parseInt(error.extra_message)) ? "8" : error.extra_message; - this.connection.client.handleDisconnect(DisconnectReason.IDENTITY_TOO_LOW, error); - } else if(error.id == 3329) { - this.connection.client.handleDisconnect(DisconnectReason.HANDSHAKE_BANNED, error); - } else { - this.connection.client.handleDisconnect(DisconnectReason.CLIENT_KICKED, error); - } - } else - this.connection.disconnect(); - }); + this.connection.client.handleDisconnect(DisconnectReason.HANDSHAKE_TEAMSPEAK_REQUIRED); } } + + private handshake_failed(message: string) { + if(this.failed) return; + + this.failed = true; + this.connection.client.handleDisconnect(DisconnectReason.HANDSHAKE_FAILED, message); + } + + private handshake_finished(version?: string) { + const _native = window["native"]; + if(native_client && _native && _native.client_version && !version) { + _native.client_version() + .then( this.handshake_finished.bind(this)) + .catch(error => { + console.error(tr("Failed to get version:")); + console.error(error); + this.handshake_finished("?.?.?"); + }); + return; + } + + const git_version = settings.static_global("version", "unknown"); + const browser_name = (navigator.browserSpecs || {})["name"] || " "; + let data = { + client_nickname: this.parameters.nickname || "Another TeaSpeak user", + client_platform: (browser_name ? browser_name + " " : "") + navigator.platform, + client_version: "TeaWeb " + git_version + " (" + navigator.userAgent + ")", + client_version_sign: undefined, + + client_default_channel: (this.parameters.channel || {} as any).target, + client_default_channel_password: (this.parameters.channel || {} as any).password, + client_default_token: this.parameters.token, + + client_server_password: this.parameters.password ? this.parameters.password.password : undefined, + client_browser_engine: navigator.product, + + client_input_hardware: this.connection.client.client_status.input_hardware, + client_output_hardware: false, + client_input_muted: this.connection.client.client_status.input_muted, + client_output_muted: this.connection.client.client_status.output_muted, + }; + + //0.0.1 [Build: 1549713549] Linux 7XvKmrk7uid2ixHFeERGqcC8vupeQqDypLtw2lY9slDNPojEv//F47UaDLG+TmVk4r6S0TseIKefzBpiRtLDAQ== + + if(version) { + data.client_version = "TeaClient "; + data.client_version += " " + version; + + const os = require("os"); + const arch_mapping = { + "x32": "32bit", + "x64": "64bit" + }; + + data.client_version += " " + (arch_mapping[os.arch()] || os.arch()); + + const os_mapping = { + "win32": "Windows", + "linux": "Linux" + }; + data.client_platform = (os_mapping[os.platform()] || os.platform()); + } + + /* required to keep compatibility */ + if(this.profile.selected_type() === IdentitifyType.TEAMSPEAK) { + data["client_key_offset"] = (this.profile.selected_identity() as TeaSpeakIdentity).hash_number; + } + + this.connection.send_command("clientinit", data).catch(error => { + if(error instanceof CommandResult) { + if(error.id == 1028) { + this.connection.client.handleDisconnect(DisconnectReason.SERVER_REQUIRES_PASSWORD); + } else if(error.id == 783 || error.id == 519) { + error.extra_message = isNaN(parseInt(error.extra_message)) ? "8" : error.extra_message; + this.connection.client.handleDisconnect(DisconnectReason.IDENTITY_TOO_LOW, error); + } else if(error.id == 3329) { + this.connection.client.handleDisconnect(DisconnectReason.HANDSHAKE_BANNED, error); + } else { + this.connection.client.handleDisconnect(DisconnectReason.CLIENT_KICKED, error); + } + } else + this.connection.disconnect(); + }); + } } \ No newline at end of file diff --git a/shared/js/connection/ServerConnectionDeclaration.ts b/shared/js/connection/ServerConnectionDeclaration.ts index c2a71825..362e9163 100644 --- a/shared/js/connection/ServerConnectionDeclaration.ts +++ b/shared/js/connection/ServerConnectionDeclaration.ts @@ -1,4 +1,6 @@ -enum ErrorID { +import {LaterPromise} from "tc-shared/utils/LaterPromise"; + +export enum ErrorID { NOT_IMPLEMENTED = 0x2, COMMAND_NOT_FOUND = 0x100, @@ -15,7 +17,7 @@ enum ErrorID { CONVERSATION_IS_PRIVATE = 0x2202 } -class CommandResult { +export class CommandResult { success: boolean; id: number; message: string; @@ -35,39 +37,39 @@ class CommandResult { } } -interface ClientNameInfo { +export interface ClientNameInfo { //cluid=tYzKUryn\/\/Y8VBMf8PHUT6B1eiE= name=Exp clname=Exp cldbid=9 client_unique_id: string; client_nickname: string; client_database_id: number; } -interface ClientNameFromUid { +export interface ClientNameFromUid { promise: LaterPromise, keys: string[], response: ClientNameInfo[] } -interface ServerGroupClient { +export interface ServerGroupClient { client_nickname: string; client_unique_identifier: string; client_database_id: number; } -interface QueryListEntry { +export interface QueryListEntry { username: string; unique_id: string; bounded_server: number; } -interface QueryList { +export interface QueryList { flag_own: boolean; flag_all: boolean; queries: QueryListEntry[]; } -interface Playlist { +export interface Playlist { playlist_id: number; playlist_bot_id: number; playlist_title: string; @@ -83,7 +85,7 @@ interface Playlist { needed_power_song_remove: number; } -interface PlaylistInfo { +export interface PlaylistInfo { playlist_id: number, playlist_title: string, playlist_description: string, @@ -100,7 +102,7 @@ interface PlaylistInfo { playlist_max_songs: number } -interface PlaylistSong { +export interface PlaylistSong { song_id: number; song_previous_song_id: number; song_invoker: string; diff --git a/shared/js/crypto/asn1.ts b/shared/js/crypto/asn1.ts index 8184a4aa..c11ec360 100644 --- a/shared/js/crypto/asn1.ts +++ b/shared/js/crypto/asn1.ts @@ -14,534 +14,532 @@ // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -namespace asn1 { - declare class Int10 { - constructor(value?: any); +declare class Int10 { + constructor(value?: any); - sub(sub: number); - mulAdd(mul: number, add: number); - simplify(); + sub(sub: number); + mulAdd(mul: number, add: number); + simplify(); +} + +const ellipsis = "\u2026"; + +function string_cut(str, len) { + if (str.length > len) + str = str.substring(0, len) + ellipsis; + return str; +} + +export class Stream { + private static HEX_DIGITS = "0123456789ABCDEF"; + private static reTimeS = /^(\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/; + private static reTimeL = /^(\d\d\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/; + + position: number; + data: string | ArrayBuffer; + + constructor(data: string | Stream | ArrayBuffer, position: number) { + if (data instanceof Stream) + this.data = data.data; + else + this.data = data; + + this.position = position; } - const ellipsis = "\u2026"; + length() : number { + if (this.data instanceof ArrayBuffer) + return this.data.byteLength; + return this.data.length; + } - function string_cut(str, len) { - if (str.length > len) - str = str.substring(0, len) + ellipsis; + get(position?: number) { + if (position === undefined) + position = this.position++; + + if (position >= this.length()) + throw 'Requesting byte offset ' + this.position + ' on a stream of length ' + this.length(); + + return (typeof(this.data) === "string") ? this.data.charCodeAt(position) : this.data[position]; + } + + hexByte(byte: number) { + return Stream.HEX_DIGITS.charAt((byte >> 4) & 0xF) + Stream.HEX_DIGITS.charAt(byte & 0xF); + } + + parseStringISO(start, end) { + let s = ""; + for (let i = start; i < end; ++i) + s += String.fromCharCode(this.get(i)); + return s; + } + + parseStringUTF(start, end) { + let s = ""; + for (let i = start; i < end;) { + let c = this.get(i++); + if (c < 128) + s += String.fromCharCode(c); + else if ((c > 191) && (c < 224)) + s += String.fromCharCode(((c & 0x1F) << 6) | (this.get(i++) & 0x3F)); + else + s += String.fromCharCode(((c & 0x0F) << 12) | ((this.get(i++) & 0x3F) << 6) | (this.get(i++) & 0x3F)); + } + return s; + } + + parseStringBMP(start, end) { + let str = "", hi, lo; + for (let i = start; i < end;) { + hi = this.get(i++); + lo = this.get(i++); + str += String.fromCharCode((hi << 8) | lo); + } return str; } - export class Stream { - private static HEX_DIGITS = "0123456789ABCDEF"; - private static reTimeS = /^(\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/; - private static reTimeL = /^(\d\d\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/; - - position: number; - data: string | ArrayBuffer; - - constructor(data: string | Stream | ArrayBuffer, position: number) { - if (data instanceof Stream) - this.data = data.data; - else - this.data = data; - - this.position = position; + parseTime(start, end, shortYear) { + let s = this.parseStringISO(start, end), + m = (shortYear ? Stream.reTimeS : Stream.reTimeL).exec(s); + if (!m) + return "Unrecognized time: " + s; + if (shortYear) { + // to avoid querying the timer, use the fixed range [1970, 2069] + // it will conform with ITU X.400 [-10, +40] sliding window until 2030 + //m[1] = +m[1]; + //m[1] += (parseInt(m[1]) < 70) ? 2000 : 1900; + throw "fixme!"; } - - length() : number { - if (this.data instanceof ArrayBuffer) - return this.data.byteLength; - return this.data.length; + s = m[1] + "-" + m[2] + "-" + m[3] + " " + m[4]; + if (m[5]) { + s += ":" + m[5]; + if (m[6]) { + s += ":" + m[6]; + if (m[7]) + s += "." + m[7]; + } } - - get(position?: number) { - if (position === undefined) - position = this.position++; - - if (position >= this.length()) - throw 'Requesting byte offset ' + this.position + ' on a stream of length ' + this.length(); - - return (typeof(this.data) === "string") ? this.data.charCodeAt(position) : this.data[position]; + if (m[8]) { + s += " UTC"; + if (m[8] != 'Z') { + s += m[8]; + if (m[9]) + s += ":" + m[9]; + } } + return s; + }; - hexByte(byte: number) { - return Stream.HEX_DIGITS.charAt((byte >> 4) & 0xF) + Stream.HEX_DIGITS.charAt(byte & 0xF); + parseInteger(start, end) { + let current: number = this.get(start); + + let negative = (current > 127); + let padding = negative ? 255 : 0; + let length; + let descriptor: number | string; + + // skip unuseful bits (not allowed in DER) + while (current == padding && ++start < end) + current = this.get(start); + + length = end - start; + if (length === 0) + return negative ? '-1' : '0'; + + // show bit length of huge integers + if (length > 4) { + descriptor = current; + length <<= 3; /* calculate bit length */ + + while (((descriptor ^ padding) & 0x80) == 0) { + descriptor <<= 1; + --length; + } + descriptor = "(" + length + " bit)\n"; } + // decode the integer + if (negative) current = current - 256; - parseStringISO(start, end) { - let s = ""; - for (let i = start; i < end; ++i) - s += String.fromCharCode(this.get(i)); - return s; - } - - parseStringUTF(start, end) { - let s = ""; - for (let i = start; i < end;) { - let c = this.get(i++); - if (c < 128) - s += String.fromCharCode(c); - else if ((c > 191) && (c < 224)) - s += String.fromCharCode(((c & 0x1F) << 6) | (this.get(i++) & 0x3F)); - else - s += String.fromCharCode(((c & 0x0F) << 12) | ((this.get(i++) & 0x3F) << 6) | (this.get(i++) & 0x3F)); - } - return s; - } - - parseStringBMP(start, end) { - let str = "", hi, lo; - for (let i = start; i < end;) { - hi = this.get(i++); - lo = this.get(i++); - str += String.fromCharCode((hi << 8) | lo); - } - return str; - } - - parseTime(start, end, shortYear) { - let s = this.parseStringISO(start, end), - m = (shortYear ? Stream.reTimeS : Stream.reTimeL).exec(s); - if (!m) - return "Unrecognized time: " + s; - if (shortYear) { - // to avoid querying the timer, use the fixed range [1970, 2069] - // it will conform with ITU X.400 [-10, +40] sliding window until 2030 - //m[1] = +m[1]; - //m[1] += (parseInt(m[1]) < 70) ? 2000 : 1900; - throw "fixme!"; - } - s = m[1] + "-" + m[2] + "-" + m[3] + " " + m[4]; - if (m[5]) { - s += ":" + m[5]; - if (m[6]) { - s += ":" + m[6]; - if (m[7]) - s += "." + m[7]; - } - } - if (m[8]) { - s += " UTC"; - if (m[8] != 'Z') { - s += m[8]; - if (m[9]) - s += ":" + m[9]; - } - } - return s; - }; - - parseInteger(start, end) { - let current: number = this.get(start); - - let negative = (current > 127); - let padding = negative ? 255 : 0; - let length; - let descriptor: number | string; - - // skip unuseful bits (not allowed in DER) - while (current == padding && ++start < end) - current = this.get(start); - - length = end - start; - if (length === 0) - return negative ? '-1' : '0'; - - // show bit length of huge integers - if (length > 4) { - descriptor = current; - length <<= 3; /* calculate bit length */ - - while (((descriptor ^ padding) & 0x80) == 0) { - descriptor <<= 1; - --length; - } - descriptor = "(" + length + " bit)\n"; - } - // decode the integer - if (negative) current = current - 256; - - let number = ""; - if(typeof(Int10) !== "undefined") { - let n = new Int10(current); - for (let i = start + 1; i < end; ++i) - n.mulAdd(256, this.get(i)); - number = n.toString(); - } else { - let n = 0; - for (let i = start + 1; i < end; ++i) { - n <<= 8; - n += this.get(i); - } - number = n.toString(); - } - return descriptor + number; - }; - - isASCII(start: number, end: number) { - for (let i = start; i < end; ++i) { - const c = this.get(i); - if (c < 32 || c > 176) - return false; - } - return true; - }; - - parseBitString(start, end, maxLength) { - let unusedBit = this.get(start), - lenBit = ((end - start - 1) << 3) - unusedBit, - intro = "(" + lenBit + " bit)\n", - s = ""; + let number = ""; + if(typeof(Int10) !== "undefined") { + let n = new Int10(current); + for (let i = start + 1; i < end; ++i) + n.mulAdd(256, this.get(i)); + number = n.toString(); + } else { + let n = 0; for (let i = start + 1; i < end; ++i) { - let b = this.get(i), - skip = (i == end - 1) ? unusedBit : 0; - for (let j = 7; j >= skip; --j) - s += (b >> j) & 1 ? "1" : "0"; + n <<= 8; + n += this.get(i); + } + number = n.toString(); + } + return descriptor + number; + }; + + isASCII(start: number, end: number) { + for (let i = start; i < end; ++i) { + const c = this.get(i); + if (c < 32 || c > 176) + return false; + } + return true; + }; + + parseBitString(start, end, maxLength) { + let unusedBit = this.get(start), + lenBit = ((end - start - 1) << 3) - unusedBit, + intro = "(" + lenBit + " bit)\n", + s = ""; + for (let i = start + 1; i < end; ++i) { + let b = this.get(i), + skip = (i == end - 1) ? unusedBit : 0; + for (let j = 7; j >= skip; --j) + s += (b >> j) & 1 ? "1" : "0"; + if (s.length > maxLength) + return intro + string_cut(s, maxLength); + } + return intro + s; + }; + + parseOctetString(start, end, maxLength) { + if (this.isASCII(start, end)) + return string_cut(this.parseStringISO(start, end), maxLength); + let len = end - start, + s = "(" + len + " byte)\n"; + maxLength /= 2; // we work in bytes + if (len > maxLength) + end = start + maxLength; + for (let i = start; i < end; ++i) + s += this.hexByte(this.get(i)); + if (len > maxLength) + s += ellipsis; + return s; + }; + + parseOID(start, end, maxLength) { + let s = '', + n = new Int10(), + bits = 0; + for (let i = start; i < end; ++i) { + let v = this.get(i); + n.mulAdd(128, v & 0x7F); + bits += 7; + if (!(v & 0x80)) { // finished + if (s === '') { + n = n.simplify(); + if (n instanceof Int10) { + n.sub(80); + s = "2." + n.toString(); + } else { + let m = n < 80 ? n < 40 ? 0 : 1 : 2; + s = m + "." + (n - m * 40); + } + } else + s += "." + n.toString(); if (s.length > maxLength) - return intro + string_cut(s, maxLength); - } - return intro + s; - }; - - parseOctetString(start, end, maxLength) { - if (this.isASCII(start, end)) - return string_cut(this.parseStringISO(start, end), maxLength); - let len = end - start, - s = "(" + len + " byte)\n"; - maxLength /= 2; // we work in bytes - if (len > maxLength) - end = start + maxLength; - for (let i = start; i < end; ++i) - s += this.hexByte(this.get(i)); - if (len > maxLength) - s += ellipsis; - return s; - }; - - parseOID(start, end, maxLength) { - let s = '', - n = new Int10(), + return string_cut(s, maxLength); + n = new Int10(); bits = 0; - for (let i = start; i < end; ++i) { - let v = this.get(i); - n.mulAdd(128, v & 0x7F); - bits += 7; - if (!(v & 0x80)) { // finished - if (s === '') { - n = n.simplify(); - if (n instanceof Int10) { - n.sub(80); - s = "2." + n.toString(); - } else { - let m = n < 80 ? n < 40 ? 0 : 1 : 2; - s = m + "." + (n - m * 40); - } - } else - s += "." + n.toString(); - if (s.length > maxLength) - return string_cut(s, maxLength); - n = new Int10(); - bits = 0; - } - } - if (bits > 0) - s += ".incomplete"; - /* FIXME - if (typeof oids === 'object') { - let oid = oids[s]; - if (oid) { - if (oid.d) s += "\n" + oid.d; - if (oid.c) s += "\n" + oid.c; - if (oid.w) s += "\n(warning!)"; - } - } - */ - return s; - }; - } - - export enum TagClass { - UNIVERSAL = 0x00, - APPLICATION = 0x01, - CONTEXT = 0x02, - PRIVATE = 0x03 - } - - export enum TagType { - EOC = 0x00, - BOOLEAN = 0x01, - INTEGER = 0x02, - BIT_STRING = 0x03, - OCTET_STRING = 0x04, - NULL = 0x05, - OBJECT_IDENTIFIER = 0x06, - ObjectDescriptor = 0x07, - EXTERNAL = 0x08, - REAL = 0x09, - ENUMERATED = 0x0A, - EMBEDDED_PDV = 0x0B, - UTF8String = 0x0C, - SEQUENCE = 0x10, - SET = 0x11, - NumericString = 0x12, - PrintableString = 0x13, // ASCII subset - TeletextString = 0x14, // aka T61String - VideotexString = 0x15, - IA5String = 0x16, // ASCII - UTCTime = 0x17, - GeneralizedTime = 0x18, - GraphicString = 0x19, - VisibleString = 0x1A, // ASCII subset - GeneralString = 0x1B, - UniversalString = 0x1C, - BMPString = 0x1E - } - - class ASN1Tag { - tagClass: TagClass; - type: TagType; - tagConstructed: boolean; - tagNumber: number; - - constructor(stream: Stream) { - let buf = stream.get(); - this.tagClass = buf >> 6; - this.tagConstructed = ((buf & 0x20) !== 0); - this.tagNumber = buf & 0x1F; - if (this.tagNumber == 0x1F) { // long tag - let n = new Int10(); - do { - buf = stream.get(); - n.mulAdd(128, buf & 0x7F); - } while (buf & 0x80); - this.tagNumber = n.simplify(); } } + if (bits > 0) + s += ".incomplete"; + /* FIXME + if (typeof oids === 'object') { + let oid = oids[s]; + if (oid) { + if (oid.d) s += "\n" + oid.d; + if (oid.c) s += "\n" + oid.c; + if (oid.w) s += "\n(warning!)"; + } + } + */ + return s; + }; +} - isUniversal() { - return this.tagClass === 0x00; - }; +export enum TagClass { + UNIVERSAL = 0x00, + APPLICATION = 0x01, + CONTEXT = 0x02, + PRIVATE = 0x03 +} - isEOC() { - return this.tagClass === 0x00 && this.tagNumber === 0x00; - }; +export enum TagType { + EOC = 0x00, + BOOLEAN = 0x01, + INTEGER = 0x02, + BIT_STRING = 0x03, + OCTET_STRING = 0x04, + NULL = 0x05, + OBJECT_IDENTIFIER = 0x06, + ObjectDescriptor = 0x07, + EXTERNAL = 0x08, + REAL = 0x09, + ENUMERATED = 0x0A, + EMBEDDED_PDV = 0x0B, + UTF8String = 0x0C, + SEQUENCE = 0x10, + SET = 0x11, + NumericString = 0x12, + PrintableString = 0x13, // ASCII subset + TeletextString = 0x14, // aka T61String + VideotexString = 0x15, + IA5String = 0x16, // ASCII + UTCTime = 0x17, + GeneralizedTime = 0x18, + GraphicString = 0x19, + VisibleString = 0x1A, // ASCII subset + GeneralString = 0x1B, + UniversalString = 0x1C, + BMPString = 0x1E +} + +class ASN1Tag { + tagClass: TagClass; + type: TagType; + tagConstructed: boolean; + tagNumber: number; + + constructor(stream: Stream) { + let buf = stream.get(); + this.tagClass = buf >> 6; + this.tagConstructed = ((buf & 0x20) !== 0); + this.tagNumber = buf & 0x1F; + if (this.tagNumber == 0x1F) { // long tag + let n = new Int10(); + do { + buf = stream.get(); + n.mulAdd(128, buf & 0x7F); + } while (buf & 0x80); + this.tagNumber = n.simplify(); + } } - export class ASN1 { - stream: Stream; - header: number; - length: number; - tag: ASN1Tag; - children: ASN1[]; + isUniversal() { + return this.tagClass === 0x00; + }; - constructor(stream: Stream, header: number, length: number, tag: ASN1Tag, children: ASN1[]) { - this.stream = stream; - this.header = header; - this.length = length; - this.tag = tag; - this.children = children; + isEOC() { + return this.tagClass === 0x00 && this.tagNumber === 0x00; + }; +} + +export class ASN1 { + stream: Stream; + header: number; + length: number; + tag: ASN1Tag; + children: ASN1[]; + + constructor(stream: Stream, header: number, length: number, tag: ASN1Tag, children: ASN1[]) { + this.stream = stream; + this.header = header; + this.length = length; + this.tag = tag; + this.children = children; + } + + content(max_length?: number, type?: TagType) { // a preview of the content (intended for humans) + if (this.tag === undefined) return null; + if (max_length === undefined) + max_length = Infinity; + + let content = this.posContent(), + len = Math.abs(this.length); + + if (!this.tag.isUniversal()) { + if (this.children !== null) + return "(" + this.children.length + " elem)"; + return this.stream.parseOctetString(content, content + len, max_length); } - - content(max_length?: number, type?: TagType) { // a preview of the content (intended for humans) - if (this.tag === undefined) return null; - if (max_length === undefined) - max_length = Infinity; - - let content = this.posContent(), - len = Math.abs(this.length); - - if (!this.tag.isUniversal()) { + switch (type || this.tag.tagNumber) { + case 0x01: // BOOLEAN + return (this.stream.get(content) === 0) ? "false" : "true"; + case 0x02: // INTEGER + return this.stream.parseInteger(content, content + len); + case 0x03: // BIT_STRING + return this.children ? "(" + this.children.length + " elem)" : + this.stream.parseBitString(content, content + len, max_length); + case 0x04: // OCTET_STRING + return this.children ? "(" + this.children.length + " elem)" : + this.stream.parseOctetString(content, content + len, max_length); + //case 0x05: // NULL + case 0x06: // OBJECT_IDENTIFIER + return this.stream.parseOID(content, content + len, max_length); + //case 0x07: // ObjectDescriptor + //case 0x08: // EXTERNAL + //case 0x09: // REAL + //case 0x0A: // ENUMERATED + //case 0x0B: // EMBEDDED_PDV + case 0x10: // SEQUENCE + case 0x11: // SET if (this.children !== null) return "(" + this.children.length + " elem)"; - return this.stream.parseOctetString(content, content + len, max_length); - } - switch (type || this.tag.tagNumber) { - case 0x01: // BOOLEAN - return (this.stream.get(content) === 0) ? "false" : "true"; - case 0x02: // INTEGER - return this.stream.parseInteger(content, content + len); - case 0x03: // BIT_STRING - return this.children ? "(" + this.children.length + " elem)" : - this.stream.parseBitString(content, content + len, max_length); - case 0x04: // OCTET_STRING - return this.children ? "(" + this.children.length + " elem)" : - this.stream.parseOctetString(content, content + len, max_length); - //case 0x05: // NULL - case 0x06: // OBJECT_IDENTIFIER - return this.stream.parseOID(content, content + len, max_length); - //case 0x07: // ObjectDescriptor - //case 0x08: // EXTERNAL - //case 0x09: // REAL - //case 0x0A: // ENUMERATED - //case 0x0B: // EMBEDDED_PDV - case 0x10: // SEQUENCE - case 0x11: // SET - if (this.children !== null) - return "(" + this.children.length + " elem)"; - else - return "(no elem)"; - case 0x0C: // UTF8String - return string_cut(this.stream.parseStringUTF(content, content + len), max_length); - case 0x12: // NumericString - case 0x13: // PrintableString - case 0x14: // TeletexString - case 0x15: // VideotexString - case 0x16: // IA5String - //case 0x19: // GraphicString - case 0x1A: // VisibleString - //case 0x1B: // GeneralString - //case 0x1C: // UniversalString - return string_cut(this.stream.parseStringISO(content, content + len), max_length); - case 0x1E: // BMPString - return string_cut(this.stream.parseStringBMP(content, content + len), max_length); - case 0x17: // UTCTime - case 0x18: // GeneralizedTime - return this.stream.parseTime(content, content + len, (this.tag.tagNumber == 0x17)); - } - return null; - }; - - typeName(): string { - switch (this.tag.tagClass) { - case 0: // universal - return TagType[this.tag.tagNumber] || ("Universal_" + this.tag.tagNumber.toString()); - case 1: - return "Application_" + this.tag.tagNumber.toString(); - case 2: - return "[" + this.tag.tagNumber.toString() + "]"; // Context - case 3: - return "Private_" + this.tag.tagNumber.toString(); - } - }; - - toString() { - return this.typeName() + "@" + this.stream.position + "[header:" + this.header + ",length:" + this.length + ",sub:" + ((this.children === null) ? 'null' : this.children.length) + "]"; + else + return "(no elem)"; + case 0x0C: // UTF8String + return string_cut(this.stream.parseStringUTF(content, content + len), max_length); + case 0x12: // NumericString + case 0x13: // PrintableString + case 0x14: // TeletexString + case 0x15: // VideotexString + case 0x16: // IA5String + //case 0x19: // GraphicString + case 0x1A: // VisibleString + //case 0x1B: // GeneralString + //case 0x1C: // UniversalString + return string_cut(this.stream.parseStringISO(content, content + len), max_length); + case 0x1E: // BMPString + return string_cut(this.stream.parseStringBMP(content, content + len), max_length); + case 0x17: // UTCTime + case 0x18: // GeneralizedTime + return this.stream.parseTime(content, content + len, (this.tag.tagNumber == 0x17)); } + return null; + }; - toPrettyString(indent) { - if (indent === undefined) indent = ''; - let s = indent + this.typeName() + " @" + this.stream.position; - if (this.length >= 0) - s += "+"; - s += this.length; - if (this.tag.tagConstructed) - s += " (constructed)"; - else if ((this.tag.isUniversal() && ((this.tag.tagNumber == 0x03) || (this.tag.tagNumber == 0x04))) && (this.children !== null)) - s += " (encapsulates)"; - let content = this.content(); - if (content) - s += ": " + content.replace(/\n/g, '|'); - s += "\n"; - if (this.children !== null) { - indent += ' '; - for (let i = 0, max = this.children.length; i < max; ++i) - s += this.children[i].toPrettyString(indent); - } - return s; - }; + typeName(): string { + switch (this.tag.tagClass) { + case 0: // universal + return TagType[this.tag.tagNumber] || ("Universal_" + this.tag.tagNumber.toString()); + case 1: + return "Application_" + this.tag.tagNumber.toString(); + case 2: + return "[" + this.tag.tagNumber.toString() + "]"; // Context + case 3: + return "Private_" + this.tag.tagNumber.toString(); + } + }; - posStart() { - return this.stream.position; - }; + toString() { + return this.typeName() + "@" + this.stream.position + "[header:" + this.header + ",length:" + this.length + ",sub:" + ((this.children === null) ? 'null' : this.children.length) + "]"; + } - posContent() { - return this.stream.position + this.header; - }; + toPrettyString(indent) { + if (indent === undefined) indent = ''; + let s = indent + this.typeName() + " @" + this.stream.position; + if (this.length >= 0) + s += "+"; + s += this.length; + if (this.tag.tagConstructed) + s += " (constructed)"; + else if ((this.tag.isUniversal() && ((this.tag.tagNumber == 0x03) || (this.tag.tagNumber == 0x04))) && (this.children !== null)) + s += " (encapsulates)"; + let content = this.content(); + if (content) + s += ": " + content.replace(/\n/g, '|'); + s += "\n"; + if (this.children !== null) { + indent += ' '; + for (let i = 0, max = this.children.length; i < max; ++i) + s += this.children[i].toPrettyString(indent); + } + return s; + }; - posEnd() { - return this.stream.position + this.header + Math.abs(this.length); - }; + posStart() { + return this.stream.position; + }; - static decodeLength(stream: Stream) { - let buf = stream.get(); - const len = buf & 0x7F; - if (len == buf) - return len; - if (len > 6) // no reason to use Int10, as it would be a huge buffer anyways - throw "Length over 48 bits not supported at position " + (stream.position - 1); - if (len === 0) - return null; // undefined + posContent() { + return this.stream.position + this.header; + }; - buf = 0; - for (let i = 0; i < len; ++i) - buf = (buf << 8) + stream.get(); - return buf; - }; + posEnd() { + return this.stream.position + this.header + Math.abs(this.length); + }; - static encodeLength(buffer: Uint8Array, offset: number, length: number) { - if(length < 0x7F) { - buffer[offset] = length; - } else { - buffer[offset] = 0x80; - let index = 1; - while(length > 0) { - buffer[offset + index++] = length & 0xFF; - length >>= 8; - buffer[offset] += 1; - } + static decodeLength(stream: Stream) { + let buf = stream.get(); + const len = buf & 0x7F; + if (len == buf) + return len; + if (len > 6) // no reason to use Int10, as it would be a huge buffer anyways + throw "Length over 48 bits not supported at position " + (stream.position - 1); + if (len === 0) + return null; // undefined + + buf = 0; + for (let i = 0; i < len; ++i) + buf = (buf << 8) + stream.get(); + return buf; + }; + + static encodeLength(buffer: Uint8Array, offset: number, length: number) { + if(length < 0x7F) { + buffer[offset] = length; + } else { + buffer[offset] = 0x80; + let index = 1; + while(length > 0) { + buffer[offset + index++] = length & 0xFF; + length >>= 8; + buffer[offset] += 1; } } } +} - function decode0(stream: Stream) { - const streamStart = new Stream(stream, 0); /* copy */ - const tag = new ASN1Tag(stream); - let len = ASN1.decodeLength(stream); - const start = stream.position; - const length_header = start - streamStart.position; - let children = null; - const query_children = () => { - children = []; - if (len !== null) { - const end = start + len; - if (end > stream.length()) - throw 'Container at offset ' + start + ' has a length of ' + len + ', which is past the end of the stream'; - while (stream.position < end) - children[children.length] = decode0(stream); - if (stream.position != end) - throw 'Content size is not correct for container at offset ' + start; - } else { - // undefined length - try { - while (true) { - const s = decode0(stream); - if (s.tag.isEOC()) break; - children[children.length] = s; - } - len = start - stream.position; // undefined lengths are represented as negative values - } catch (e) { - throw 'Exception while decoding undefined length content at offset ' + start + ': ' + e; - } - } - }; - if (tag.tagConstructed) { - // must have valid content - query_children(); - } else if (tag.isUniversal() && ((tag.tagNumber == 0x03) || (tag.tagNumber == 0x04))) { - // sometimes BitString and OctetString are used to encapsulate ASN.1 +function decode0(stream: Stream) { + const streamStart = new Stream(stream, 0); /* copy */ + const tag = new ASN1Tag(stream); + let len = ASN1.decodeLength(stream); + const start = stream.position; + const length_header = start - streamStart.position; + let children = null; + const query_children = () => { + children = []; + if (len !== null) { + const end = start + len; + if (end > stream.length()) + throw 'Container at offset ' + start + ' has a length of ' + len + ', which is past the end of the stream'; + while (stream.position < end) + children[children.length] = decode0(stream); + if (stream.position != end) + throw 'Content size is not correct for container at offset ' + start; + } else { + // undefined length try { - if (tag.tagNumber == 0x03) - if (stream.get() != 0) - throw "BIT STRINGs with unused bits cannot encapsulate."; - query_children(); - for (let i = 0; i < children.length; ++i) - if (children[i].tag.isEOC()) - throw 'EOC is not supposed to be actual content.'; + while (true) { + const s = decode0(stream); + if (s.tag.isEOC()) break; + children[children.length] = s; + } + len = start - stream.position; // undefined lengths are represented as negative values } catch (e) { - // but silently ignore when they don't - children = null; - //DEBUG console.log('Could not decode structure at ' + start + ':', e); + throw 'Exception while decoding undefined length content at offset ' + start + ': ' + e; } } - if (children === null) { - if (len === null) - throw "We can't skip over an invalid tag with undefined length at offset " + start; - stream.position = start + Math.abs(len); + }; + if (tag.tagConstructed) { + // must have valid content + query_children(); + } else if (tag.isUniversal() && ((tag.tagNumber == 0x03) || (tag.tagNumber == 0x04))) { + // sometimes BitString and OctetString are used to encapsulate ASN.1 + try { + if (tag.tagNumber == 0x03) + if (stream.get() != 0) + throw "BIT STRINGs with unused bits cannot encapsulate."; + query_children(); + for (let i = 0; i < children.length; ++i) + if (children[i].tag.isEOC()) + throw 'EOC is not supposed to be actual content.'; + } catch (e) { + // but silently ignore when they don't + children = null; + //DEBUG console.log('Could not decode structure at ' + start + ':', e); } - return new ASN1(streamStart, length_header, len, tag, children); } + if (children === null) { + if (len === null) + throw "We can't skip over an invalid tag with undefined length at offset " + start; + stream.position = start + Math.abs(len); + } + return new ASN1(streamStart, length_header, len, tag, children); +} - export function decode(stream: string | ArrayBuffer) { - return decode0(new Stream(stream, 0)); - } +export function decode(stream: string | ArrayBuffer) { + return decode0(new Stream(stream, 0)); } \ No newline at end of file diff --git a/shared/js/crypto/crc32.ts b/shared/js/crypto/crc32.ts index 8122a8a2..52e14979 100644 --- a/shared/js/crypto/crc32.ts +++ b/shared/js/crypto/crc32.ts @@ -1,4 +1,4 @@ -class Crc32 { +export class Crc32 { private static readonly lookup = [ 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, diff --git a/shared/js/crypto/hex.ts b/shared/js/crypto/hex.ts index 9c700341..642488d0 100644 --- a/shared/js/crypto/hex.ts +++ b/shared/js/crypto/hex.ts @@ -1,20 +1,18 @@ -namespace hex { - export function encode(buffer) { - let hexCodes = []; - let view = new DataView(buffer); - for (let i = 0; i < view.byteLength % 4; i ++) { - let value = view.getUint32(i * 4); - let stringValue = value.toString(16); - let padding = '00000000'; - let paddedValue = (padding + stringValue).slice(-padding.length); - hexCodes.push(paddedValue); - } - for (let i = (view.byteLength % 4) * 4; i < view.byteLength; i++) { - let value = view.getUint8(i).toString(16); - let padding = '00'; - hexCodes.push((padding + value).slice(-padding.length)); - } - - return hexCodes.join(""); +export function encode(buffer) { + let hexCodes = []; + let view = new DataView(buffer); + for (let i = 0; i < view.byteLength % 4; i ++) { + let value = view.getUint32(i * 4); + let stringValue = value.toString(16); + let padding = '00000000'; + let paddedValue = (padding + stringValue).slice(-padding.length); + hexCodes.push(paddedValue); } + for (let i = (view.byteLength % 4) * 4; i < view.byteLength; i++) { + let value = view.getUint8(i).toString(16); + let padding = '00'; + hexCodes.push((padding + value).slice(-padding.length)); + } + + return hexCodes.join(""); } \ No newline at end of file diff --git a/shared/js/crypto/sha.ts b/shared/js/crypto/sha.ts index 28998cff..a3d28f9f 100644 --- a/shared/js/crypto/sha.ts +++ b/shared/js/crypto/sha.ts @@ -6,405 +6,367 @@ declare class _sha1 { /* interface Window { - TextEncoder: any; +TextEncoder: any; } */ -namespace sha { - /* - * [js-sha1]{@link https://github.com/emn178/js-sha1} - * - * @version 0.6.0 - * @author Chen, Yi-Cyuan [emn178@gmail.com] - * @copyright Chen, Yi-Cyuan 2014-2017 - * @license MIT - */ - /*jslint bitwise: true */ - (function() { - 'use strict'; +/* + * [js-sha1]{@link https://github.com/emn178/js-sha1} + * + * @version 0.6.0 + * @author Chen, Yi-Cyuan [emn178@gmail.com] + * @copyright Chen, Yi-Cyuan 2014-2017 + * @license MIT + */ +/*jslint bitwise: true */ +(function() { + 'use strict'; - let root: any = typeof window === 'object' ? window : {}; - let NODE_JS = !root.JS_SHA1_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node; - if (NODE_JS) { - root = global; + let root: any = typeof window === 'object' ? window : {}; + let HEX_CHARS = '0123456789abcdef'.split(''); + let EXTRA = [-2147483648, 8388608, 32768, 128]; + let SHIFT = [24, 16, 8, 0]; + let OUTPUT_TYPES = ['hex', 'array', 'digest', 'arrayBuffer']; + + let blocks = []; + + let createOutputMethod = function (outputType) { + return function (message) { + return new Sha1(true).update(message)[outputType](); + }; + }; + + let createMethod = function () { + let method: any = createOutputMethod('hex'); + method.create = function () { + return new (Sha1 as any)(); + }; + method.update = function (message) { + return method.create().update(message); + }; + for (var i = 0; i < OUTPUT_TYPES.length; ++i) { + var type = OUTPUT_TYPES[i]; + method[type] = createOutputMethod(type); } - let COMMON_JS = !root.JS_SHA1_NO_COMMON_JS && typeof module === 'object' && module.exports; - let AMD = typeof define === 'function' && (define as any).amd; - let HEX_CHARS = '0123456789abcdef'.split(''); - let EXTRA = [-2147483648, 8388608, 32768, 128]; - let SHIFT = [24, 16, 8, 0]; - let OUTPUT_TYPES = ['hex', 'array', 'digest', 'arrayBuffer']; + return method; + }; - let blocks = []; - - let createOutputMethod = function (outputType) { - return function (message) { - return new Sha1(true).update(message)[outputType](); - }; - }; - - let createMethod = function () { - let method: any = createOutputMethod('hex'); - if (NODE_JS) { - method = nodeWrap(method); - } - method.create = function () { - return new (Sha1 as any)(); - }; - method.update = function (message) { - return method.create().update(message); - }; - for (var i = 0; i < OUTPUT_TYPES.length; ++i) { - var type = OUTPUT_TYPES[i]; - method[type] = createOutputMethod(type); - } - return method; - }; - - var nodeWrap = function (method) { - var crypto = eval("require('crypto')"); - var Buffer = eval("require('buffer').Buffer"); - var nodeMethod = function (message) { - if (typeof message === 'string') { - return crypto.createHash('sha1').update(message, 'utf8').digest('hex'); - } else if (message.constructor === ArrayBuffer) { - message = new Uint8Array(message); - } else if (message.length === undefined) { - return method(message); - } - return crypto.createHash('sha1').update(new Buffer(message)).digest('hex'); - }; - return nodeMethod; - }; - - function Sha1(sharedMemory) { - if (sharedMemory) { - blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] = - blocks[4] = blocks[5] = blocks[6] = blocks[7] = - blocks[8] = blocks[9] = blocks[10] = blocks[11] = - blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; - this.blocks = blocks; - } else { - this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - } - - this.h0 = 0x67452301; - this.h1 = 0xEFCDAB89; - this.h2 = 0x98BADCFE; - this.h3 = 0x10325476; - this.h4 = 0xC3D2E1F0; - - this.block = this.start = this.bytes = this.hBytes = 0; - this.finalized = this.hashed = false; - this.first = true; + function Sha1(sharedMemory) { + if (sharedMemory) { + blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] = + blocks[4] = blocks[5] = blocks[6] = blocks[7] = + blocks[8] = blocks[9] = blocks[10] = blocks[11] = + blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + this.blocks = blocks; + } else { + this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; } - Sha1.prototype.update = function (message) { - if (this.finalized) { - return; - } - var notString = typeof(message) !== 'string'; - if (notString && message.constructor === root.ArrayBuffer) { - message = new Uint8Array(message); - } - var code, index = 0, i, length = message.length || 0, blocks = this.blocks; + this.h0 = 0x67452301; + this.h1 = 0xEFCDAB89; + this.h2 = 0x98BADCFE; + this.h3 = 0x10325476; + this.h4 = 0xC3D2E1F0; - while (index < length) { - if (this.hashed) { - this.hashed = false; - blocks[0] = this.block; - blocks[16] = blocks[1] = blocks[2] = blocks[3] = - blocks[4] = blocks[5] = blocks[6] = blocks[7] = - blocks[8] = blocks[9] = blocks[10] = blocks[11] = - blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; - } + this.block = this.start = this.bytes = this.hBytes = 0; + this.finalized = this.hashed = false; + this.first = true; + } - if(notString) { - for (i = this.start; index < length && i < 64; ++index) { - blocks[i >> 2] |= message[index] << SHIFT[i++ & 3]; - } - } else { - for (i = this.start; index < length && i < 64; ++index) { - code = message.charCodeAt(index); - if (code < 0x80) { - blocks[i >> 2] |= code << SHIFT[i++ & 3]; - } else if (code < 0x800) { - blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3]; - blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; - } else if (code < 0xd800 || code >= 0xe000) { - blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3]; - blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; - blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; - } else { - code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); - blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3]; - blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3]; - blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; - blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; - } - } - } + Sha1.prototype.update = function (message) { + if (this.finalized) { + return; + } + var notString = typeof(message) !== 'string'; + if (notString && message.constructor === root.ArrayBuffer) { + message = new Uint8Array(message); + } + var code, index = 0, i, length = message.length || 0, blocks = this.blocks; - this.lastByteIndex = i; - this.bytes += i - this.start; - if (i >= 64) { - this.block = blocks[16]; - this.start = i - 64; - this.hash(); - this.hashed = true; - } else { - this.start = i; - } - } - if (this.bytes > 4294967295) { - this.hBytes += this.bytes / 4294967296 << 0; - this.bytes = this.bytes % 4294967296; - } - return this; - }; - - Sha1.prototype.finalize = function () { - if (this.finalized) { - return; - } - this.finalized = true; - var blocks = this.blocks, i = this.lastByteIndex; - blocks[16] = this.block; - blocks[i >> 2] |= EXTRA[i & 3]; - this.block = blocks[16]; - if (i >= 56) { - if (!this.hashed) { - this.hash(); - } + while (index < length) { + if (this.hashed) { + this.hashed = false; blocks[0] = this.block; blocks[16] = blocks[1] = blocks[2] = blocks[3] = blocks[4] = blocks[5] = blocks[6] = blocks[7] = blocks[8] = blocks[9] = blocks[10] = blocks[11] = blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; } - blocks[14] = this.hBytes << 3 | this.bytes >>> 29; - blocks[15] = this.bytes << 3; - this.hash(); - }; - Sha1.prototype.hash = function () { - var a = this.h0, b = this.h1, c = this.h2, d = this.h3, e = this.h4; - var f, j, t, blocks = this.blocks; - - for(j = 16; j < 80; ++j) { - t = blocks[j - 3] ^ blocks[j - 8] ^ blocks[j - 14] ^ blocks[j - 16]; - blocks[j] = (t << 1) | (t >>> 31); + if(notString) { + for (i = this.start; index < length && i < 64; ++index) { + blocks[i >> 2] |= message[index] << SHIFT[i++ & 3]; + } + } else { + for (i = this.start; index < length && i < 64; ++index) { + code = message.charCodeAt(index); + if (code < 0x80) { + blocks[i >> 2] |= code << SHIFT[i++ & 3]; + } else if (code < 0x800) { + blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else if (code < 0xd800 || code >= 0xe000) { + blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); + blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } + } } - for(j = 0; j < 20; j += 5) { - f = (b & c) | ((~b) & d); - t = (a << 5) | (a >>> 27); - e = t + f + e + 1518500249 + blocks[j] << 0; - b = (b << 30) | (b >>> 2); - - f = (a & b) | ((~a) & c); - t = (e << 5) | (e >>> 27); - d = t + f + d + 1518500249 + blocks[j + 1] << 0; - a = (a << 30) | (a >>> 2); - - f = (e & a) | ((~e) & b); - t = (d << 5) | (d >>> 27); - c = t + f + c + 1518500249 + blocks[j + 2] << 0; - e = (e << 30) | (e >>> 2); - - f = (d & e) | ((~d) & a); - t = (c << 5) | (c >>> 27); - b = t + f + b + 1518500249 + blocks[j + 3] << 0; - d = (d << 30) | (d >>> 2); - - f = (c & d) | ((~c) & e); - t = (b << 5) | (b >>> 27); - a = t + f + a + 1518500249 + blocks[j + 4] << 0; - c = (c << 30) | (c >>> 2); - } - - for(; j < 40; j += 5) { - f = b ^ c ^ d; - t = (a << 5) | (a >>> 27); - e = t + f + e + 1859775393 + blocks[j] << 0; - b = (b << 30) | (b >>> 2); - - f = a ^ b ^ c; - t = (e << 5) | (e >>> 27); - d = t + f + d + 1859775393 + blocks[j + 1] << 0; - a = (a << 30) | (a >>> 2); - - f = e ^ a ^ b; - t = (d << 5) | (d >>> 27); - c = t + f + c + 1859775393 + blocks[j + 2] << 0; - e = (e << 30) | (e >>> 2); - - f = d ^ e ^ a; - t = (c << 5) | (c >>> 27); - b = t + f + b + 1859775393 + blocks[j + 3] << 0; - d = (d << 30) | (d >>> 2); - - f = c ^ d ^ e; - t = (b << 5) | (b >>> 27); - a = t + f + a + 1859775393 + blocks[j + 4] << 0; - c = (c << 30) | (c >>> 2); - } - - for(; j < 60; j += 5) { - f = (b & c) | (b & d) | (c & d); - t = (a << 5) | (a >>> 27); - e = t + f + e - 1894007588 + blocks[j] << 0; - b = (b << 30) | (b >>> 2); - - f = (a & b) | (a & c) | (b & c); - t = (e << 5) | (e >>> 27); - d = t + f + d - 1894007588 + blocks[j + 1] << 0; - a = (a << 30) | (a >>> 2); - - f = (e & a) | (e & b) | (a & b); - t = (d << 5) | (d >>> 27); - c = t + f + c - 1894007588 + blocks[j + 2] << 0; - e = (e << 30) | (e >>> 2); - - f = (d & e) | (d & a) | (e & a); - t = (c << 5) | (c >>> 27); - b = t + f + b - 1894007588 + blocks[j + 3] << 0; - d = (d << 30) | (d >>> 2); - - f = (c & d) | (c & e) | (d & e); - t = (b << 5) | (b >>> 27); - a = t + f + a - 1894007588 + blocks[j + 4] << 0; - c = (c << 30) | (c >>> 2); - } - - for(; j < 80; j += 5) { - f = b ^ c ^ d; - t = (a << 5) | (a >>> 27); - e = t + f + e - 899497514 + blocks[j] << 0; - b = (b << 30) | (b >>> 2); - - f = a ^ b ^ c; - t = (e << 5) | (e >>> 27); - d = t + f + d - 899497514 + blocks[j + 1] << 0; - a = (a << 30) | (a >>> 2); - - f = e ^ a ^ b; - t = (d << 5) | (d >>> 27); - c = t + f + c - 899497514 + blocks[j + 2] << 0; - e = (e << 30) | (e >>> 2); - - f = d ^ e ^ a; - t = (c << 5) | (c >>> 27); - b = t + f + b - 899497514 + blocks[j + 3] << 0; - d = (d << 30) | (d >>> 2); - - f = c ^ d ^ e; - t = (b << 5) | (b >>> 27); - a = t + f + a - 899497514 + blocks[j + 4] << 0; - c = (c << 30) | (c >>> 2); - } - - this.h0 = this.h0 + a << 0; - this.h1 = this.h1 + b << 0; - this.h2 = this.h2 + c << 0; - this.h3 = this.h3 + d << 0; - this.h4 = this.h4 + e << 0; - }; - - Sha1.prototype.hex = function () { - this.finalize(); - - var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4; - - return HEX_CHARS[(h0 >> 28) & 0x0F] + HEX_CHARS[(h0 >> 24) & 0x0F] + - HEX_CHARS[(h0 >> 20) & 0x0F] + HEX_CHARS[(h0 >> 16) & 0x0F] + - HEX_CHARS[(h0 >> 12) & 0x0F] + HEX_CHARS[(h0 >> 8) & 0x0F] + - HEX_CHARS[(h0 >> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] + - HEX_CHARS[(h1 >> 28) & 0x0F] + HEX_CHARS[(h1 >> 24) & 0x0F] + - HEX_CHARS[(h1 >> 20) & 0x0F] + HEX_CHARS[(h1 >> 16) & 0x0F] + - HEX_CHARS[(h1 >> 12) & 0x0F] + HEX_CHARS[(h1 >> 8) & 0x0F] + - HEX_CHARS[(h1 >> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] + - HEX_CHARS[(h2 >> 28) & 0x0F] + HEX_CHARS[(h2 >> 24) & 0x0F] + - HEX_CHARS[(h2 >> 20) & 0x0F] + HEX_CHARS[(h2 >> 16) & 0x0F] + - HEX_CHARS[(h2 >> 12) & 0x0F] + HEX_CHARS[(h2 >> 8) & 0x0F] + - HEX_CHARS[(h2 >> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] + - HEX_CHARS[(h3 >> 28) & 0x0F] + HEX_CHARS[(h3 >> 24) & 0x0F] + - HEX_CHARS[(h3 >> 20) & 0x0F] + HEX_CHARS[(h3 >> 16) & 0x0F] + - HEX_CHARS[(h3 >> 12) & 0x0F] + HEX_CHARS[(h3 >> 8) & 0x0F] + - HEX_CHARS[(h3 >> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] + - HEX_CHARS[(h4 >> 28) & 0x0F] + HEX_CHARS[(h4 >> 24) & 0x0F] + - HEX_CHARS[(h4 >> 20) & 0x0F] + HEX_CHARS[(h4 >> 16) & 0x0F] + - HEX_CHARS[(h4 >> 12) & 0x0F] + HEX_CHARS[(h4 >> 8) & 0x0F] + - HEX_CHARS[(h4 >> 4) & 0x0F] + HEX_CHARS[h4 & 0x0F]; - }; - - Sha1.prototype.toString = Sha1.prototype.hex; - - Sha1.prototype.digest = function () { - this.finalize(); - - var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4; - - return [ - (h0 >> 24) & 0xFF, (h0 >> 16) & 0xFF, (h0 >> 8) & 0xFF, h0 & 0xFF, - (h1 >> 24) & 0xFF, (h1 >> 16) & 0xFF, (h1 >> 8) & 0xFF, h1 & 0xFF, - (h2 >> 24) & 0xFF, (h2 >> 16) & 0xFF, (h2 >> 8) & 0xFF, h2 & 0xFF, - (h3 >> 24) & 0xFF, (h3 >> 16) & 0xFF, (h3 >> 8) & 0xFF, h3 & 0xFF, - (h4 >> 24) & 0xFF, (h4 >> 16) & 0xFF, (h4 >> 8) & 0xFF, h4 & 0xFF - ]; - }; - - Sha1.prototype.array = Sha1.prototype.digest; - - Sha1.prototype.arrayBuffer = function () { - this.finalize(); - - var buffer = new ArrayBuffer(20); - var dataView = new DataView(buffer); - dataView.setUint32(0, this.h0); - dataView.setUint32(4, this.h1); - dataView.setUint32(8, this.h2); - dataView.setUint32(12, this.h3); - dataView.setUint32(16, this.h4); - return buffer; - }; - - var exports = createMethod(); - - if (COMMON_JS) { - module.exports = exports; - } else { - root._sha1 = exports; - if (AMD) { - define(function () { - return exports; - }); + this.lastByteIndex = i; + this.bytes += i - this.start; + if (i >= 64) { + this.block = blocks[16]; + this.start = i - 64; + this.hash(); + this.hashed = true; + } else { + this.start = i; } } - })(); + if (this.bytes > 4294967295) { + this.hBytes += this.bytes / 4294967296 << 0; + this.bytes = this.bytes % 4294967296; + } + return this; + }; - export function encode_text(buffer: string) : ArrayBuffer { - if ((window as any).TextEncoder) { - return new TextEncoder().encode(buffer).buffer; + Sha1.prototype.finalize = function () { + if (this.finalized) { + return; } - let utf8 = unescape(encodeURIComponent(buffer)); - let result = new Uint8Array(utf8.length); - for (let i = 0; i < utf8.length; i++) { - result[i] = utf8.charCodeAt(i); + this.finalized = true; + var blocks = this.blocks, i = this.lastByteIndex; + blocks[16] = this.block; + blocks[i >> 2] |= EXTRA[i & 3]; + this.block = blocks[16]; + if (i >= 56) { + if (!this.hashed) { + this.hash(); + } + blocks[0] = this.block; + blocks[16] = blocks[1] = blocks[2] = blocks[3] = + blocks[4] = blocks[5] = blocks[6] = blocks[7] = + blocks[8] = blocks[9] = blocks[10] = blocks[11] = + blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; } - return result.buffer; + blocks[14] = this.hBytes << 3 | this.bytes >>> 29; + blocks[15] = this.bytes << 3; + this.hash(); + }; + + Sha1.prototype.hash = function () { + var a = this.h0, b = this.h1, c = this.h2, d = this.h3, e = this.h4; + var f, j, t, blocks = this.blocks; + + for(j = 16; j < 80; ++j) { + t = blocks[j - 3] ^ blocks[j - 8] ^ blocks[j - 14] ^ blocks[j - 16]; + blocks[j] = (t << 1) | (t >>> 31); + } + + for(j = 0; j < 20; j += 5) { + f = (b & c) | ((~b) & d); + t = (a << 5) | (a >>> 27); + e = t + f + e + 1518500249 + blocks[j] << 0; + b = (b << 30) | (b >>> 2); + + f = (a & b) | ((~a) & c); + t = (e << 5) | (e >>> 27); + d = t + f + d + 1518500249 + blocks[j + 1] << 0; + a = (a << 30) | (a >>> 2); + + f = (e & a) | ((~e) & b); + t = (d << 5) | (d >>> 27); + c = t + f + c + 1518500249 + blocks[j + 2] << 0; + e = (e << 30) | (e >>> 2); + + f = (d & e) | ((~d) & a); + t = (c << 5) | (c >>> 27); + b = t + f + b + 1518500249 + blocks[j + 3] << 0; + d = (d << 30) | (d >>> 2); + + f = (c & d) | ((~c) & e); + t = (b << 5) | (b >>> 27); + a = t + f + a + 1518500249 + blocks[j + 4] << 0; + c = (c << 30) | (c >>> 2); + } + + for(; j < 40; j += 5) { + f = b ^ c ^ d; + t = (a << 5) | (a >>> 27); + e = t + f + e + 1859775393 + blocks[j] << 0; + b = (b << 30) | (b >>> 2); + + f = a ^ b ^ c; + t = (e << 5) | (e >>> 27); + d = t + f + d + 1859775393 + blocks[j + 1] << 0; + a = (a << 30) | (a >>> 2); + + f = e ^ a ^ b; + t = (d << 5) | (d >>> 27); + c = t + f + c + 1859775393 + blocks[j + 2] << 0; + e = (e << 30) | (e >>> 2); + + f = d ^ e ^ a; + t = (c << 5) | (c >>> 27); + b = t + f + b + 1859775393 + blocks[j + 3] << 0; + d = (d << 30) | (d >>> 2); + + f = c ^ d ^ e; + t = (b << 5) | (b >>> 27); + a = t + f + a + 1859775393 + blocks[j + 4] << 0; + c = (c << 30) | (c >>> 2); + } + + for(; j < 60; j += 5) { + f = (b & c) | (b & d) | (c & d); + t = (a << 5) | (a >>> 27); + e = t + f + e - 1894007588 + blocks[j] << 0; + b = (b << 30) | (b >>> 2); + + f = (a & b) | (a & c) | (b & c); + t = (e << 5) | (e >>> 27); + d = t + f + d - 1894007588 + blocks[j + 1] << 0; + a = (a << 30) | (a >>> 2); + + f = (e & a) | (e & b) | (a & b); + t = (d << 5) | (d >>> 27); + c = t + f + c - 1894007588 + blocks[j + 2] << 0; + e = (e << 30) | (e >>> 2); + + f = (d & e) | (d & a) | (e & a); + t = (c << 5) | (c >>> 27); + b = t + f + b - 1894007588 + blocks[j + 3] << 0; + d = (d << 30) | (d >>> 2); + + f = (c & d) | (c & e) | (d & e); + t = (b << 5) | (b >>> 27); + a = t + f + a - 1894007588 + blocks[j + 4] << 0; + c = (c << 30) | (c >>> 2); + } + + for(; j < 80; j += 5) { + f = b ^ c ^ d; + t = (a << 5) | (a >>> 27); + e = t + f + e - 899497514 + blocks[j] << 0; + b = (b << 30) | (b >>> 2); + + f = a ^ b ^ c; + t = (e << 5) | (e >>> 27); + d = t + f + d - 899497514 + blocks[j + 1] << 0; + a = (a << 30) | (a >>> 2); + + f = e ^ a ^ b; + t = (d << 5) | (d >>> 27); + c = t + f + c - 899497514 + blocks[j + 2] << 0; + e = (e << 30) | (e >>> 2); + + f = d ^ e ^ a; + t = (c << 5) | (c >>> 27); + b = t + f + b - 899497514 + blocks[j + 3] << 0; + d = (d << 30) | (d >>> 2); + + f = c ^ d ^ e; + t = (b << 5) | (b >>> 27); + a = t + f + a - 899497514 + blocks[j + 4] << 0; + c = (c << 30) | (c >>> 2); + } + + this.h0 = this.h0 + a << 0; + this.h1 = this.h1 + b << 0; + this.h2 = this.h2 + c << 0; + this.h3 = this.h3 + d << 0; + this.h4 = this.h4 + e << 0; + }; + + Sha1.prototype.hex = function () { + this.finalize(); + + var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4; + + return HEX_CHARS[(h0 >> 28) & 0x0F] + HEX_CHARS[(h0 >> 24) & 0x0F] + + HEX_CHARS[(h0 >> 20) & 0x0F] + HEX_CHARS[(h0 >> 16) & 0x0F] + + HEX_CHARS[(h0 >> 12) & 0x0F] + HEX_CHARS[(h0 >> 8) & 0x0F] + + HEX_CHARS[(h0 >> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] + + HEX_CHARS[(h1 >> 28) & 0x0F] + HEX_CHARS[(h1 >> 24) & 0x0F] + + HEX_CHARS[(h1 >> 20) & 0x0F] + HEX_CHARS[(h1 >> 16) & 0x0F] + + HEX_CHARS[(h1 >> 12) & 0x0F] + HEX_CHARS[(h1 >> 8) & 0x0F] + + HEX_CHARS[(h1 >> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] + + HEX_CHARS[(h2 >> 28) & 0x0F] + HEX_CHARS[(h2 >> 24) & 0x0F] + + HEX_CHARS[(h2 >> 20) & 0x0F] + HEX_CHARS[(h2 >> 16) & 0x0F] + + HEX_CHARS[(h2 >> 12) & 0x0F] + HEX_CHARS[(h2 >> 8) & 0x0F] + + HEX_CHARS[(h2 >> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] + + HEX_CHARS[(h3 >> 28) & 0x0F] + HEX_CHARS[(h3 >> 24) & 0x0F] + + HEX_CHARS[(h3 >> 20) & 0x0F] + HEX_CHARS[(h3 >> 16) & 0x0F] + + HEX_CHARS[(h3 >> 12) & 0x0F] + HEX_CHARS[(h3 >> 8) & 0x0F] + + HEX_CHARS[(h3 >> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] + + HEX_CHARS[(h4 >> 28) & 0x0F] + HEX_CHARS[(h4 >> 24) & 0x0F] + + HEX_CHARS[(h4 >> 20) & 0x0F] + HEX_CHARS[(h4 >> 16) & 0x0F] + + HEX_CHARS[(h4 >> 12) & 0x0F] + HEX_CHARS[(h4 >> 8) & 0x0F] + + HEX_CHARS[(h4 >> 4) & 0x0F] + HEX_CHARS[h4 & 0x0F]; + }; + + Sha1.prototype.toString = Sha1.prototype.hex; + + Sha1.prototype.digest = function () { + this.finalize(); + + var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4; + + return [ + (h0 >> 24) & 0xFF, (h0 >> 16) & 0xFF, (h0 >> 8) & 0xFF, h0 & 0xFF, + (h1 >> 24) & 0xFF, (h1 >> 16) & 0xFF, (h1 >> 8) & 0xFF, h1 & 0xFF, + (h2 >> 24) & 0xFF, (h2 >> 16) & 0xFF, (h2 >> 8) & 0xFF, h2 & 0xFF, + (h3 >> 24) & 0xFF, (h3 >> 16) & 0xFF, (h3 >> 8) & 0xFF, h3 & 0xFF, + (h4 >> 24) & 0xFF, (h4 >> 16) & 0xFF, (h4 >> 8) & 0xFF, h4 & 0xFF + ]; + }; + + Sha1.prototype.array = Sha1.prototype.digest; + + Sha1.prototype.arrayBuffer = function () { + this.finalize(); + + const buffer = new ArrayBuffer(20); + const dataView = new DataView(buffer); + dataView.setUint32(0, this.h0); + dataView.setUint32(4, this.h1); + dataView.setUint32(8, this.h2); + dataView.setUint32(12, this.h3); + dataView.setUint32(16, this.h4); + return buffer; + }; + + createMethod(); +})(); + +export function encode_text(buffer: string) : ArrayBuffer { + if ((window as any).TextEncoder) { + return new TextEncoder().encode(buffer).buffer; } - export function sha1(message: string | ArrayBuffer) : PromiseLike { - if(!(typeof(message) === "string" || message instanceof ArrayBuffer)) throw "Invalid type!"; - - let buffer = message instanceof ArrayBuffer ? message : encode_text(message as string); - - if(!crypto || !crypto.subtle || !crypto.subtle.digest || /Edge/.test(navigator.userAgent)) - return new Promise(resolve => { - resolve(_sha1.arrayBuffer(buffer as ArrayBuffer)); - }); - else - return crypto.subtle.digest("SHA-1", buffer); + let utf8 = unescape(encodeURIComponent(buffer)); + let result = new Uint8Array(utf8.length); + for (let i = 0; i < utf8.length; i++) { + result[i] = utf8.charCodeAt(i); } + return result.buffer; +} -} \ No newline at end of file +export function sha1(message: string | ArrayBuffer) : PromiseLike { + if(!(typeof(message) === "string" || message instanceof ArrayBuffer)) throw "Invalid type!"; + + let buffer = message instanceof ArrayBuffer ? message : encode_text(message as string); + + if(!crypto || !crypto.subtle || !crypto.subtle.digest || /Edge/.test(navigator.userAgent)) + return new Promise(resolve => { + resolve(_sha1.arrayBuffer(buffer as ArrayBuffer)); + }); + else + return crypto.subtle.digest("SHA-1", buffer); +} diff --git a/shared/js/crypto/uid.ts b/shared/js/crypto/uid.ts new file mode 100644 index 00000000..c97ab5f0 --- /dev/null +++ b/shared/js/crypto/uid.ts @@ -0,0 +1,10 @@ +function s4() { + return Math + .floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); +} + +export function guid() { + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); +} \ No newline at end of file diff --git a/shared/js/dns.ts b/shared/js/dns.ts index 866b0daf..146ebcdf 100644 --- a/shared/js/dns.ts +++ b/shared/js/dns.ts @@ -1,24 +1,22 @@ -namespace dns { - export interface AddressTarget { - target_ip: string; - target_port?: number; - } +export interface AddressTarget { + target_ip: string; + target_port?: number; +} - export interface ResolveOptions { - timeout?: number; - allow_cache?: boolean; - max_depth?: number; +export interface ResolveOptions { + timeout?: number; + allow_cache?: boolean; + max_depth?: number; - allow_srv?: boolean; - allow_cname?: boolean; - allow_any?: boolean; - allow_a?: boolean; - allow_aaaa?: boolean; - } + allow_srv?: boolean; + allow_cname?: boolean; + allow_any?: boolean; + allow_a?: boolean; + allow_aaaa?: boolean; +} - export const default_options: ResolveOptions = { - timeout: 5000, - allow_cache: true, - max_depth: 5 - }; -} \ No newline at end of file +export const default_options: ResolveOptions = { + timeout: 5000, + allow_cache: true, + max_depth: 5 +}; \ No newline at end of file diff --git a/shared/js/events.ts b/shared/js/events.ts index 348621a8..d57c9c06 100644 --- a/shared/js/events.ts +++ b/shared/js/events.ts @@ -1,604 +1,605 @@ -namespace events { - export interface EventConvert { - as() : All[T]; +//TODO: Combine EventConvert and Event? +import {MusicClientEntry, SongInfo} from "tc-shared/ui/client"; +import {PlaylistSong} from "tc-shared/connection/ServerConnectionDeclaration"; +import {guid} from "tc-shared/crypto/uid"; + +export interface EventConvert { + as() : All[T]; +} + +export interface Event { + readonly type: T; +} + +export class SingletonEvent implements Event<"singletone-instance"> { + static readonly instance = new SingletonEvent(); + + readonly type = "singletone-instance"; + private constructor() { } +} + +export class Registry { + private readonly registry_uuid; + + private handler: {[key: string]: ((event) => void)[]} = {}; + private connections: {[key: string]:Registry[]} = {}; + private debug_prefix = undefined; + + constructor() { + this.registry_uuid = "evreg_data_" + guid(); } - export interface Event { - readonly type: T; + + enable_debug(prefix: string) { this.debug_prefix = prefix || "---"; } + disable_debug() { this.debug_prefix = undefined; } + + on(event: T, handler: (event?: Events[T] & Event & EventConvert) => void); + on(events: (keyof Events)[], handler: (event?: Event & EventConvert) => void); + on(events, handler) { + if(!Array.isArray(events)) + events = [events]; + + handler[this.registry_uuid] = { + singleshot: false + }; + for(const event of events) { + const handlers = this.handler[event] || (this.handler[event] = []); + handlers.push(handler); + } } - export class SingletonEvent implements Event<"singletone-instance"> { - static readonly instance = new SingletonEvent(); + /* one */ + one(event: T, handler: (event?: Events[T] & Event & EventConvert) => void); + one(events: (keyof Events)[], handler: (event?: Event & EventConvert) => void); + one(events, handler) { + if(!Array.isArray(events)) + events = [events]; - readonly type = "singletone-instance"; - private constructor() { } + for(const event of events) { + const handlers = this.handler[event] || (this.handler[event] = []); + + handler[this.registry_uuid] = { singleshot: true }; + handlers.push(handler); + } } - export class Registry { - private readonly registry_uuid; + off(handler: (event?: Event) => void); + off(event: T, handler: (event?: Event & EventConvert) => void); + off(event: (keyof Events)[], handler: (event?: Event & EventConvert) => void); + off(handler_or_events, handler?) { + if(typeof handler_or_events === "function") { + for(const key of Object.keys(this.handler)) + this.handler[key].remove(handler_or_events); + } else { + if(!Array.isArray(handler_or_events)) + handler_or_events = [handler_or_events]; - private handler: {[key: string]: ((event) => void)[]} = {}; - private connections: {[key: string]:Registry[]} = {}; - private debug_prefix = undefined; + for(const event of handler_or_events) { + const handlers = this.handler[event]; + if(handlers) handlers.remove(handler); + } + } + } - constructor() { - this.registry_uuid = "evreg_data_" + guid(); + connect(event: T, target: Registry) { + (this.connections[event as string] || (this.connections[event as string] = [])).push(target as any); + } + + disconnect(event: T, target: Registry) { + (this.connections[event as string] || []).remove(target as any); + } + + disconnect_all(target: Registry) { + for(const event of Object.keys(this.connections)) + this.connections[event].remove(target as any); + } + + fire(event_type: T, data?: Events[T]) { + if(this.debug_prefix) console.log("[%s] Trigger event: %s", this.debug_prefix, event_type); + + const event = Object.assign(typeof data === "undefined" ? SingletonEvent.instance : data, { + type: event_type, + as: function () { return this; } + }); + + for(const handler of (this.handler[event_type as string] || [])) { + handler(event); + + const reg_data = handler[this.registry_uuid]; + if(typeof reg_data === "object" && reg_data.singleshot) + this.handler[event_type as string].remove(handler); } + for(const evhandler of (this.connections[event_type as string] || [])) + evhandler.fire(event_type as any, event as any); + } - enable_debug(prefix: string) { this.debug_prefix = prefix || "---"; } - disable_debug() { this.debug_prefix = undefined; } + fire_async(event_type: T, data?: Events[T]) { + setTimeout(() => this.fire(event_type, data)); + } - on(event: T, handler: (event?: Events[T] & Event & EventConvert) => void); - on(events: (keyof Events)[], handler: (event?: Event & EventConvert) => void); - on(events, handler) { - if(!Array.isArray(events)) - events = [events]; + destory() { + this.handler = {}; + } +} - handler[this.registry_uuid] = { - singleshot: false - }; - for(const event of events) { - const handlers = this.handler[event] || (this.handler[event] = []); - handlers.push(handler); +export namespace channel_tree { + export interface client { + "enter_view": {}, + "left_view": {}, + + "property_update": { + properties: string[] + }, + + "music_status_update": { + player_buffered_index: number, + player_replay_index: number + }, + "music_song_change": { + "song": SongInfo + }, + + /* TODO: Move this out of the music bots interface? */ + "playlist_song_add": { song: PlaylistSong }, + "playlist_song_remove": { song_id: number }, + "playlist_song_reorder": { song_id: number, previous_song_id: number }, + "playlist_song_loaded": { song_id: number, success: boolean, error_msg?: string, metadata?: string }, + } +} + +export namespace sidebar { + export interface music { + "open": {}, /* triggers when frame should be shown */ + "close": {}, /* triggers when frame will be closed */ + + "bot_change": { + old: MusicClientEntry | undefined, + new: MusicClientEntry | undefined + }, + "bot_property_update": { + properties: string[] + }, + + "action_play": {}, + "action_pause": {}, + "action_song_set": { song_id: number }, + "action_forward": {}, + "action_rewind": {}, + "action_forward_ms": { + units: number; + }, + "action_rewind_ms": { + units: number; + }, + "action_song_add": {}, + "action_song_delete": { song_id: number }, + "action_playlist_reload": {}, + + "playtime_move_begin": {}, + "playtime_move_end": { + canceled: boolean, + target_time?: number + }, + + "reorder_begin": { song_id: number; entry: JQuery }, + "reorder_end": { song_id: number; canceled: boolean; entry: JQuery; previous_entry?: number }, + + "player_time_update": channel_tree.client["music_status_update"], + "player_song_change": channel_tree.client["music_song_change"], + + "playlist_song_add": channel_tree.client["playlist_song_add"] & { insert_effect?: boolean }, + "playlist_song_remove": channel_tree.client["playlist_song_remove"], + "playlist_song_reorder": channel_tree.client["playlist_song_reorder"], + "playlist_song_loaded": channel_tree.client["playlist_song_loaded"] & { html_entry?: JQuery }, + } +} + +export namespace modal { + export type BotStatusType = "name" | "description" | "volume" | "country_code" | "channel_commander" | "priority_speaker"; + export type PlaylistStatusType = "replay_mode" | "finished" | "delete_played" | "max_size" | "notify_song_change"; + export interface music_manage { + show_container: { container: "settings" | "permissions"; }; + + /* setting relevant */ + query_bot_status: {}, + bot_status: { + status: "success" | "error"; + error_msg?: string; + data?: { + name: string, + description: string, + volume: number, + + country_code: string, + default_country_code: string, + + channel_commander: boolean, + priority_speaker: boolean, + + client_version: string, + client_platform: string, + + uptime_mode: number, + bot_type: number + } + }, + set_bot_status: { + key: BotStatusType, + value: any + }, + set_bot_status_result: { + key: BotStatusType, + status: "success" | "error" | "timeout", + error_msg?: string, + value?: any + } + + query_playlist_status: {}, + playlist_status: { + status: "success" | "error", + error_msg?: string, + data?: { + replay_mode: number, + finished: boolean, + delete_played: boolean, + max_size: number, + notify_song_change: boolean + } + }, + set_playlist_status: { + key: PlaylistStatusType, + value: any + }, + set_playlist_status_result: { + key: PlaylistStatusType, + status: "success" | "error" | "timeout", + error_msg?: string, + value?: any + } + + /* permission relevant */ + show_client_list: {}, + hide_client_list: {}, + + filter_client_list: { filter: string | undefined }, + + "refresh_permissions": {}, + + query_special_clients: {}, + special_client_list: { + status: "success" | "error" | "error-permission", + error_msg?: string, + clients?: { + name: string, + unique_id: string, + database_id: number + }[] + }, + + search_client: { text: string }, + search_client_result: { + status: "error" | "timeout" | "empty" | "success", + error_msg?: string, + client?: { + name: string, + unique_id: string, + database_id: number + } + }, + + /* sets a client to set the permission for */ + special_client_set: { + client?: { + name: string, + unique_id: string, + database_id: number + } + }, + + "query_general_permissions": {}, + "general_permissions": { + status: "error" | "timeout" | "success", + error_msg?: string, + permissions?: {[key: string]:number} + }, + "set_general_permission_result": { + status: "error" | "success", + key: string, + value?: number, + error_msg?: string + }, + "set_general_permission": { /* try to change a permission for the server */ + key: string, + value: number + }, + + + "query_client_permissions": { client_database_id: number }, + "client_permissions": { + status: "error" | "timeout" | "success", + client_database_id: number, + error_msg?: string, + permissions?: {[key: string]:number} + }, + "set_client_permission_result": { + status: "error" | "success", + client_database_id: number, + key: string, + value?: number, + error_msg?: string + }, + "set_client_permission": { /* try to change a permission for the server */ + client_database_id: number, + key: string, + value: number + }, + + "query_group_permissions": { permission_name: string }, + "group_permissions": { + permission_name: string; + status: "error" | "timeout" | "success" + groups?: { + name: string, + value: number, + id: number + }[], + error_msg?: string + } + } + + export interface newcomer { + "show_step": { + "step": "welcome" | "microphone" | "identity" | "finish" + }, + "exit_guide": { + ask_yesno: boolean + }, + + "modal-shown": {}, + + + "step-status": { + next_button: boolean, + previous_button: boolean + } + } + + export namespace settings { + export type ProfileInfo = { + id: string, + name: string, + nickname: string, + identity_type: "teaforo" | "teamspeak" | "nickname", + + identity_forum?: { + valid: boolean, + fallback_name: string + }, + identity_nickname?: { + name: string, + fallback_name: string + }, + identity_teamspeak?: { + unique_id: string, + fallback_name: string } } - /* one */ - one(event: T, handler: (event?: Events[T] & Event & EventConvert) => void); - one(events: (keyof Events)[], handler: (event?: Event & EventConvert) => void); - one(events, handler) { - if(!Array.isArray(events)) - events = [events]; + export interface profiles { + "reload-profile": { profile_id?: string }, + "select-profile": { profile_id: string }, - for(const event of events) { - const handlers = this.handler[event] || (this.handler[event] = []); + "query-profile-list": { }, + "query-profile-list-result": { + status: "error" | "success" | "timeout", - handler[this.registry_uuid] = { singleshot: true }; - handlers.push(handler); - } - } - - off(handler: (event?: Event) => void); - off(event: T, handler: (event?: Event & EventConvert) => void); - off(event: (keyof Events)[], handler: (event?: Event & EventConvert) => void); - off(handler_or_events, handler?) { - if(typeof handler_or_events === "function") { - for(const key of Object.keys(this.handler)) - this.handler[key].remove(handler_or_events); - } else { - if(!Array.isArray(handler_or_events)) - handler_or_events = [handler_or_events]; - - for(const event of handler_or_events) { - const handlers = this.handler[event]; - if(handlers) handlers.remove(handler); - } - } - } - - connect(event: T, target: Registry) { - (this.connections[event as string] || (this.connections[event as string] = [])).push(target as any); - } - - disconnect(event: T, target: Registry) { - (this.connections[event as string] || []).remove(target as any); - } - - disconnect_all(target: Registry) { - for(const event of Object.keys(this.connections)) - this.connections[event].remove(target as any); - } - - fire(event_type: T, data?: Events[T]) { - if(this.debug_prefix) console.log("[%s] Trigger event: %s", this.debug_prefix, event_type); - - const event = Object.assign(typeof data === "undefined" ? SingletonEvent.instance : data, { - type: event_type, - as: function () { return this; } - }); - - for(const handler of (this.handler[event_type as string] || [])) { - handler(event); - - const reg_data = handler[this.registry_uuid]; - if(typeof reg_data === "object" && reg_data.singleshot) - this.handler[event_type as string].remove(handler); + error?: string; + profiles?: ProfileInfo[] } - for(const evhandler of (this.connections[event_type as string] || [])) - evhandler.fire(event_type as any, event as any); + "query-profile": { profile_id: string }, + "query-profile-result": { + status: "error" | "success" | "timeout", + profile_id: string, + + error?: string; + info?: ProfileInfo + }, + + "select-identity-type": { + profile_id: string, + identity_type: "teamspeak" | "teaforo" | "nickname" | "unset" + }, + + "query-profile-validity": { profile_id: string }, + "query-profile-validity-result": { + profile_id: string, + status: "error" | "success" | "timeout", + + error?: string, + valid?: boolean + } + + "create-profile": { name: string }, + "create-profile-result": { + status: "error" | "success" | "timeout", + name: string; + + profile_id?: string; + error?: string; + }, + + "delete-profile": { profile_id: string }, + "delete-profile-result": { + status: "error" | "success" | "timeout", + profile_id: string, + error?: string + } + + "set-default-profile": { profile_id: string }, + "set-default-profile-result": { + status: "error" | "success" | "timeout", + + /* the profile which now has the id "default" */ + old_profile_id: string, + + /* the "default" profile which now has a new id */ + new_profile_id?: string + + error?: string; + } + + /* profile name events */ + "set-profile-name": { + profile_id: string, + name: string + }, + "set-profile-name-result": { + status: "error" | "success" | "timeout", + profile_id: string, + name?: string + }, + + /* profile nickname events */ + "set-default-name": { + profile_id: string, + name: string | null + }, + "set-default-name-result": { + status: "error" | "success" | "timeout", + profile_id: string, + name?: string | null + }, + + "query-identity-teamspeak": { profile_id: string }, + "query-identity-teamspeak-result": { + status: "error" | "success" | "timeout", + profile_id: string, + + error?: string, + level?: number + } + + "set-identity-name-name": { profile_id: string, name: string }, + "set-identity-name-name-result": { + status: "error" | "success" | "timeout", + profile_id: string, + + error?: string, + name?: string + }, + + "generate-identity-teamspeak": { profile_id: string }, + "generate-identity-teamspeak-result": { + profile_id: string, + status: "error" | "success" | "timeout", + + error?: string, + + level?: number + unique_id?: string + }, + + "improve-identity-teamspeak-level": { profile_id: string }, + "improve-identity-teamspeak-level-update": { + profile_id: string, + new_level: number + }, + + "import-identity-teamspeak": { profile_id: string }, + "import-identity-teamspeak-result": { + profile_id: string, + + level?: number + unique_id?: string + } + + "export-identity-teamspeak": { + profile_id: string, + filename: string + }, + + + "setup-forum-connection": {} } - fire_async(event_type: T, data?: Events[T]) { - setTimeout(() => this.fire(event_type, data)); - } + export type MicrophoneSettings = "volume" | "vad-type" | "ppt-key" | "ppt-release-delay" | "ppt-release-delay-active" | "threshold-threshold"; + export interface microphone { + "query-devices": { refresh_list: boolean }, + "query-device-result": { + status: "success" | "error" | "timeout", - destory() { - this.handler = {}; - } - } - - namespace global { - - } - - export namespace channel_tree { - export interface client { - "enter_view": {}, - "left_view": {}, - - "property_update": { - properties: string[] - }, - - "music_status_update": { - player_buffered_index: number, - player_replay_index: number - }, - "music_song_change": { - "song": SongInfo - }, - - /* TODO: Move this out of the music bots interface? */ - "playlist_song_add": { song: PlaylistSong }, - "playlist_song_remove": { song_id: number }, - "playlist_song_reorder": { song_id: number, previous_song_id: number }, - "playlist_song_loaded": { song_id: number, success: boolean, error_msg?: string, metadata?: string }, - } - } - - export namespace sidebar { - export interface music { - "open": {}, /* triggers when frame should be shown */ - "close": {}, /* triggers when frame will be closed */ - - "bot_change": { - old: MusicClientEntry | undefined, - new: MusicClientEntry | undefined - }, - "bot_property_update": { - properties: string[] - }, - - "action_play": {}, - "action_pause": {}, - "action_song_set": { song_id: number }, - "action_forward": {}, - "action_rewind": {}, - "action_forward_ms": { - units: number; - }, - "action_rewind_ms": { - units: number; - }, - "action_song_add": {}, - "action_song_delete": { song_id: number }, - "action_playlist_reload": {}, - - "playtime_move_begin": {}, - "playtime_move_end": { - canceled: boolean, - target_time?: number - }, - - "reorder_begin": { song_id: number; entry: JQuery }, - "reorder_end": { song_id: number; canceled: boolean; entry: JQuery; previous_entry?: number }, - - "player_time_update": channel_tree.client["music_status_update"], - "player_song_change": channel_tree.client["music_song_change"], - - "playlist_song_add": channel_tree.client["playlist_song_add"] & { insert_effect?: boolean }, - "playlist_song_remove": channel_tree.client["playlist_song_remove"], - "playlist_song_reorder": channel_tree.client["playlist_song_reorder"], - "playlist_song_loaded": channel_tree.client["playlist_song_loaded"] & { html_entry?: JQuery }, - } - } - - export namespace modal { - export type BotStatusType = "name" | "description" | "volume" | "country_code" | "channel_commander" | "priority_speaker"; - export type PlaylistStatusType = "replay_mode" | "finished" | "delete_played" | "max_size" | "notify_song_change"; - export interface music_manage { - show_container: { container: "settings" | "permissions"; }; - - /* setting relevant */ - query_bot_status: {}, - bot_status: { - status: "success" | "error"; - error_msg?: string; - data?: { + error?: string, + devices?: { + id: string, name: string, - description: string, + driver: string + }[] + active_device?: string; + }, + + "query-settings": {}, + "query-settings-result": { + status: "success" | "error" | "timeout", + + error?: string, + info?: { volume: number, + vad_type: string, - country_code: string, - default_country_code: string, - - channel_commander: boolean, - priority_speaker: boolean, - - client_version: string, - client_platform: string, - - uptime_mode: number, - bot_type: number + vad_ppt: { + key: any, /* ppt.KeyDescriptor */ + release_delay: number, + release_delay_active: boolean + }, + vad_threshold: { + threshold: number + } } }, - set_bot_status: { - key: BotStatusType, - value: any - }, - set_bot_status_result: { - key: BotStatusType, + + "set-device": { device_id: string }, + "set-device-result": { + device_id: string, status: "success" | "error" | "timeout", - error_msg?: string, - value?: any - } - query_playlist_status: {}, - playlist_status: { - status: "success" | "error", - error_msg?: string, - data?: { - replay_mode: number, - finished: boolean, - delete_played: boolean, - max_size: number, - notify_song_change: boolean - } + error?: string }, - set_playlist_status: { - key: PlaylistStatusType, - value: any + + "set-setting": { + setting: MicrophoneSettings; + value: any; }, - set_playlist_status_result: { - key: PlaylistStatusType, + "set-setting-result": { + setting: MicrophoneSettings, status: "success" | "error" | "timeout", - error_msg?: string, + + error?: string, value?: any - } + }, - /* permission relevant */ - show_client_list: {}, - hide_client_list: {}, + "update-device-level": { + devices: { + device_id: string, + status: "success" | "error", - filter_client_list: { filter: string | undefined }, - - "refresh_permissions": {}, - - query_special_clients: {}, - special_client_list: { - status: "success" | "error" | "error-permission", - error_msg?: string, - clients?: { - name: string, - unique_id: string, - database_id: number + level?: number, + error?: string }[] }, - search_client: { text: string }, - search_client_result: { - status: "error" | "timeout" | "empty" | "success", - error_msg?: string, - client?: { - name: string, - unique_id: string, - database_id: number - } - }, - - /* sets a client to set the permission for */ - special_client_set: { - client?: { - name: string, - unique_id: string, - database_id: number - } - }, - - "query_general_permissions": {}, - "general_permissions": { - status: "error" | "timeout" | "success", - error_msg?: string, - permissions?: {[key: string]:number} - }, - "set_general_permission_result": { - status: "error" | "success", - key: string, - value?: number, - error_msg?: string - }, - "set_general_permission": { /* try to change a permission for the server */ - key: string, - value: number - }, - - - "query_client_permissions": { client_database_id: number }, - "client_permissions": { - status: "error" | "timeout" | "success", - client_database_id: number, - error_msg?: string, - permissions?: {[key: string]:number} - }, - "set_client_permission_result": { - status: "error" | "success", - client_database_id: number, - key: string, - value?: number, - error_msg?: string - }, - "set_client_permission": { /* try to change a permission for the server */ - client_database_id: number, - key: string, - value: number - }, - - "query_group_permissions": { permission_name: string }, - "group_permissions": { - permission_name: string; - status: "error" | "timeout" | "success" - groups?: { - name: string, - value: number, - id: number - }[], - error_msg?: string - } - } - - export interface newcomer { - "show_step": { - "step": "welcome" | "microphone" | "identity" | "finish" - }, - "exit_guide": { - ask_yesno: boolean - }, - - "modal-shown": {}, - - - "step-status": { - next_button: boolean, - previous_button: boolean - } - } - - export namespace settings { - export type ProfileInfo = { - id: string, - name: string, - nickname: string, - identity_type: "teaforo" | "teamspeak" | "nickname", - - identity_forum?: { - valid: boolean, - fallback_name: string - }, - identity_nickname?: { - name: string, - fallback_name: string - }, - identity_teamspeak?: { - unique_id: string, - fallback_name: string - } - } - - export interface profiles { - "reload-profile": { profile_id?: string }, - "select-profile": { profile_id: string }, - - "query-profile-list": { }, - "query-profile-list-result": { - status: "error" | "success" | "timeout", - - error?: string; - profiles?: ProfileInfo[] - } - - "query-profile": { profile_id: string }, - "query-profile-result": { - status: "error" | "success" | "timeout", - profile_id: string, - - error?: string; - info?: ProfileInfo - }, - - "select-identity-type": { - profile_id: string, - identity_type: "teamspeak" | "teaforo" | "nickname" | "unset" - }, - - "query-profile-validity": { profile_id: string }, - "query-profile-validity-result": { - profile_id: string, - status: "error" | "success" | "timeout", - - error?: string, - valid?: boolean - } - - "create-profile": { name: string }, - "create-profile-result": { - status: "error" | "success" | "timeout", - name: string; - - profile_id?: string; - error?: string; - }, - - "delete-profile": { profile_id: string }, - "delete-profile-result": { - status: "error" | "success" | "timeout", - profile_id: string, - error?: string - } - - "set-default-profile": { profile_id: string }, - "set-default-profile-result": { - status: "error" | "success" | "timeout", - - /* the profile which now has the id "default" */ - old_profile_id: string, - - /* the "default" profile which now has a new id */ - new_profile_id?: string - - error?: string; - } - - /* profile name events */ - "set-profile-name": { - profile_id: string, - name: string - }, - "set-profile-name-result": { - status: "error" | "success" | "timeout", - profile_id: string, - name?: string - }, - - /* profile nickname events */ - "set-default-name": { - profile_id: string, - name: string | null - }, - "set-default-name-result": { - status: "error" | "success" | "timeout", - profile_id: string, - name?: string | null - }, - - "query-identity-teamspeak": { profile_id: string }, - "query-identity-teamspeak-result": { - status: "error" | "success" | "timeout", - profile_id: string, - - error?: string, - level?: number - } - - "set-identity-name-name": { profile_id: string, name: string }, - "set-identity-name-name-result": { - status: "error" | "success" | "timeout", - profile_id: string, - - error?: string, - name?: string - }, - - "generate-identity-teamspeak": { profile_id: string }, - "generate-identity-teamspeak-result": { - profile_id: string, - status: "error" | "success" | "timeout", - - error?: string, - - level?: number - unique_id?: string - }, - - "improve-identity-teamspeak-level": { profile_id: string }, - "improve-identity-teamspeak-level-update": { - profile_id: string, - new_level: number - }, - - "import-identity-teamspeak": { profile_id: string }, - "import-identity-teamspeak-result": { - profile_id: string, - - level?: number - unique_id?: string - } - - "export-identity-teamspeak": { - profile_id: string, - filename: string - }, - - - "setup-forum-connection": {} - } - - export type MicrophoneSettings = "volume" | "vad-type" | "ppt-key" | "ppt-release-delay" | "ppt-release-delay-active" | "threshold-threshold"; - export interface microphone { - "query-devices": { refresh_list: boolean }, - "query-device-result": { - status: "success" | "error" | "timeout", - - error?: string, - devices?: { - id: string, - name: string, - driver: string - }[] - active_device?: string; - }, - - "query-settings": {}, - "query-settings-result": { - status: "success" | "error" | "timeout", - - error?: string, - info?: { - volume: number, - vad_type: string, - - vad_ppt: { - key: ppt.KeyDescriptor, - release_delay: number, - release_delay_active: boolean - }, - vad_threshold: { - threshold: number - } - } - }, - - "set-device": { device_id: string }, - "set-device-result": { - device_id: string, - status: "success" | "error" | "timeout", - - error?: string - }, - - "set-setting": { - setting: MicrophoneSettings; - value: any; - }, - "set-setting-result": { - setting: MicrophoneSettings, - status: "success" | "error" | "timeout", - - error?: string, - value?: any - }, - - "update-device-level": { - devices: { - device_id: string, - status: "success" | "error", - - level?: number, - error?: string - }[] - }, - - "audio-initialized": {}, - "deinitialize": {} - } + "audio-initialized": {}, + "deinitialize": {} } } - } +/* +//Some test code const eclient = new events.Registry(); const emusic = new events.Registry(); eclient.connect("playlist_song_loaded", emusic); eclient.connect("playlist_song_loaded", emusic); +*/ \ No newline at end of file diff --git a/shared/js/i18n/country.ts b/shared/js/i18n/country.ts index ce152e30..5fe3ce89 100644 --- a/shared/js/i18n/country.ts +++ b/shared/js/i18n/country.ts @@ -1,1504 +1,1501 @@ - -namespace i18n { - interface CountryInfo { - name: string; - alpha_2: string; - alpha_3: string; - un_code: number; - } - const country_infos: CountryInfo[] = []; - const alpha_2_map: {[name: string]:CountryInfo} = {}; - - const fill_country_infos = (array: CountryInfo[]) => { - array.push({ - name: "Afghanistan", - alpha_2: "AF", - alpha_3: "AFG", - un_code: 4 - }); - array.push({ - name: "Aland Islands", - alpha_2: "AX", - alpha_3: "ALA", - un_code: 248 - }); - array.push({ - name: "Albania", - alpha_2: "AL", - alpha_3: "ALB", - un_code: 8 - }); - array.push({ - name: "Algeria", - alpha_2: "DZ", - alpha_3: "DZA", - un_code: 12 - }); - array.push({ - name: "American Samoa", - alpha_2: "AS", - alpha_3: "ASM", - un_code: 16 - }); - array.push({ - name: "Andorra", - alpha_2: "AD", - alpha_3: "AND", - un_code: 20 - }); - array.push({ - name: "Angola", - alpha_2: "AO", - alpha_3: "AGO", - un_code: 24 - }); - array.push({ - name: "Anguilla", - alpha_2: "AI", - alpha_3: "AIA", - un_code: 660 - }); - array.push({ - name: "Antarctica", - alpha_2: "AQ", - alpha_3: "ATA", - un_code: 10 - }); - array.push({ - name: "Antigua and Barbuda", - alpha_2: "AG", - alpha_3: "ATG", - un_code: 28 - }); - array.push({ - name: "Argentina", - alpha_2: "AR", - alpha_3: "ARG", - un_code: 32 - }); - array.push({ - name: "Armenia", - alpha_2: "AM", - alpha_3: "ARM", - un_code: 51 - }); - array.push({ - name: "Aruba", - alpha_2: "AW", - alpha_3: "ABW", - un_code: 533 - }); - array.push({ - name: "Australia", - alpha_2: "AU", - alpha_3: "AUS", - un_code: 36 - }); - array.push({ - name: "Austria", - alpha_2: "AT", - alpha_3: "AUT", - un_code: 40 - }); - array.push({ - name: "Azerbaijan", - alpha_2: "AZ", - alpha_3: "AZE", - un_code: 31 - }); - array.push({ - name: "Bahamas", - alpha_2: "BS", - alpha_3: "BHS", - un_code: 44 - }); - array.push({ - name: "Bahrain", - alpha_2: "BH", - alpha_3: "BHR", - un_code: 48 - }); - array.push({ - name: "Bangladesh", - alpha_2: "BD", - alpha_3: "BGD", - un_code: 50 - }); - array.push({ - name: "Barbados", - alpha_2: "BB", - alpha_3: "BRB", - un_code: 52 - }); - array.push({ - name: "Belarus", - alpha_2: "BY", - alpha_3: "BLR", - un_code: 112 - }); - array.push({ - name: "Belgium", - alpha_2: "BE", - alpha_3: "BEL", - un_code: 56 - }); - array.push({ - name: "Belize", - alpha_2: "BZ", - alpha_3: "BLZ", - un_code: 84 - }); - array.push({ - name: "Benin", - alpha_2: "BJ", - alpha_3: "BEN", - un_code: 204 - }); - array.push({ - name: "Bermuda", - alpha_2: "BM", - alpha_3: "BMU", - un_code: 60 - }); - array.push({ - name: "Bhutan", - alpha_2: "BT", - alpha_3: "BTN", - un_code: 64 - }); - array.push({ - name: "Bolivia", - alpha_2: "BO", - alpha_3: "BOL", - un_code: 68 - }); - array.push({ - name: "Bosnia and Herzegovina", - alpha_2: "BA", - alpha_3: "BIH", - un_code: 70 - }); - array.push({ - name: "Botswana", - alpha_2: "BW", - alpha_3: "BWA", - un_code: 72 - }); - array.push({ - name: "Bouvet Island", - alpha_2: "BV", - alpha_3: "BVT", - un_code: 74 - }); - array.push({ - name: "Brazil", - alpha_2: "BR", - alpha_3: "BRA", - un_code: 76 - }); - array.push({ - name: "British Virgin Islands", - alpha_2: "VG", - alpha_3: "VGB", - un_code: 92 - }); - array.push({ - name: "British Indian Ocean Territory", - alpha_2: "IO", - alpha_3: "IOT", - un_code: 86 - }); - array.push({ - name: "Brunei Darussalam", - alpha_2: "BN", - alpha_3: "BRN", - un_code: 96 - }); - array.push({ - name: "Bulgaria", - alpha_2: "BG", - alpha_3: "BGR", - un_code: 100 - }); - array.push({ - name: "Burkina Faso", - alpha_2: "BF", - alpha_3: "BFA", - un_code: 854 - }); - array.push({ - name: "Burundi", - alpha_2: "BI", - alpha_3: "BDI", - un_code: 108 - }); - array.push({ - name: "Cambodia", - alpha_2: "KH", - alpha_3: "KHM", - un_code: 116 - }); - array.push({ - name: "Cameroon", - alpha_2: "CM", - alpha_3: "CMR", - un_code: 120 - }); - array.push({ - name: "Canada", - alpha_2: "CA", - alpha_3: "CAN", - un_code: 124 - }); - array.push({ - name: "Cape Verde", - alpha_2: "CV", - alpha_3: "CPV", - un_code: 132 - }); - array.push({ - name: "Cayman Islands", - alpha_2: "KY", - alpha_3: "CYM", - un_code: 136 - }); - array.push({ - name: "Central African Republic", - alpha_2: "CF", - alpha_3: "CAF", - un_code: 140 - }); - array.push({ - name: "Chad", - alpha_2: "TD", - alpha_3: "TCD", - un_code: 148 - }); - array.push({ - name: "Chile", - alpha_2: "CL", - alpha_3: "CHL", - un_code: 152 - }); - array.push({ - name: "China", - alpha_2: "CN", - alpha_3: "CHN", - un_code: 156 - }); - array.push({ - name: "Hong Kong, SAR China", - alpha_2: "HK", - alpha_3: "HKG", - un_code: 344 - }); - array.push({ - name: "Macao, SAR China", - alpha_2: "MO", - alpha_3: "MAC", - un_code: 446 - }); - array.push({ - name: "Christmas Island", - alpha_2: "CX", - alpha_3: "CXR", - un_code: 162 - }); - array.push({ - name: "Cocos (Keeling) Islands", - alpha_2: "CC", - alpha_3: "CCK", - un_code: 166 - }); - array.push({ - name: "Colombia", - alpha_2: "CO", - alpha_3: "COL", - un_code: 170 - }); - array.push({ - name: "Comoros", - alpha_2: "KM", - alpha_3: "COM", - un_code: 174 - }); - array.push({ - name: "Congo (Brazzaville)", - alpha_2: "CG", - alpha_3: "COG", - un_code: 178 - }); - array.push({ - name: "Congo, (Kinshasa)", - alpha_2: "CD", - alpha_3: "COD", - un_code: 180 - }); - array.push({ - name: "Cook Islands", - alpha_2: "CK", - alpha_3: "COK", - un_code: 184 - }); - array.push({ - name: "Costa Rica", - alpha_2: "CR", - alpha_3: "CRI", - un_code: 188 - }); - array.push({ - name: "Côte d'Ivoire", - alpha_2: "CI", - alpha_3: "CIV", - un_code: 384 - }); - array.push({ - name: "Croatia", - alpha_2: "HR", - alpha_3: "HRV", - un_code: 191 - }); - array.push({ - name: "Cuba", - alpha_2: "CU", - alpha_3: "CUB", - un_code: 192 - }); - array.push({ - name: "Cyprus", - alpha_2: "CY", - alpha_3: "CYP", - un_code: 196 - }); - array.push({ - name: "Czech Republic", - alpha_2: "CZ", - alpha_3: "CZE", - un_code: 203 - }); - array.push({ - name: "Denmark", - alpha_2: "DK", - alpha_3: "DNK", - un_code: 208 - }); - array.push({ - name: "Djibouti", - alpha_2: "DJ", - alpha_3: "DJI", - un_code: 262 - }); - array.push({ - name: "Dominica", - alpha_2: "DM", - alpha_3: "DMA", - un_code: 212 - }); - array.push({ - name: "Dominican Republic", - alpha_2: "DO", - alpha_3: "DOM", - un_code: 214 - }); - array.push({ - name: "Ecuador", - alpha_2: "EC", - alpha_3: "ECU", - un_code: 218 - }); - array.push({ - name: "Egypt", - alpha_2: "EG", - alpha_3: "EGY", - un_code: 818 - }); - array.push({ - name: "El Salvador", - alpha_2: "SV", - alpha_3: "SLV", - un_code: 222 - }); - array.push({ - name: "Equatorial Guinea", - alpha_2: "GQ", - alpha_3: "GNQ", - un_code: 226 - }); - array.push({ - name: "Eritrea", - alpha_2: "ER", - alpha_3: "ERI", - un_code: 232 - }); - array.push({ - name: "Estonia", - alpha_2: "EE", - alpha_3: "EST", - un_code: 233 - }); - array.push({ - name: "Ethiopia", - alpha_2: "ET", - alpha_3: "ETH", - un_code: 231 - }); - array.push({ - name: "Falkland Islands (Malvinas)", - alpha_2: "FK", - alpha_3: "FLK", - un_code: 238 - }); - array.push({ - name: "Faroe Islands", - alpha_2: "FO", - alpha_3: "FRO", - un_code: 234 - }); - array.push({ - name: "Fiji", - alpha_2: "FJ", - alpha_3: "FJI", - un_code: 242 - }); - array.push({ - name: "Finland", - alpha_2: "FI", - alpha_3: "FIN", - un_code: 246 - }); - array.push({ - name: "France", - alpha_2: "FR", - alpha_3: "FRA", - un_code: 250 - }); - array.push({ - name: "French Guiana", - alpha_2: "GF", - alpha_3: "GUF", - un_code: 254 - }); - array.push({ - name: "French Polynesia", - alpha_2: "PF", - alpha_3: "PYF", - un_code: 258 - }); - array.push({ - name: "French Southern Territories", - alpha_2: "TF", - alpha_3: "ATF", - un_code: 260 - }); - array.push({ - name: "Gabon", - alpha_2: "GA", - alpha_3: "GAB", - un_code: 266 - }); - array.push({ - name: "Gambia", - alpha_2: "GM", - alpha_3: "GMB", - un_code: 270 - }); - array.push({ - name: "Georgia", - alpha_2: "GE", - alpha_3: "GEO", - un_code: 268 - }); - array.push({ - name: "Germany", - alpha_2: "DE", - alpha_3: "DEU", - un_code: 276 - }); - array.push({ - name: "Ghana", - alpha_2: "GH", - alpha_3: "GHA", - un_code: 288 - }); - array.push({ - name: "Gibraltar", - alpha_2: "GI", - alpha_3: "GIB", - un_code: 292 - }); - array.push({ - name: "Greece", - alpha_2: "GR", - alpha_3: "GRC", - un_code: 300 - }); - array.push({ - name: "Greenland", - alpha_2: "GL", - alpha_3: "GRL", - un_code: 304 - }); - array.push({ - name: "Grenada", - alpha_2: "GD", - alpha_3: "GRD", - un_code: 308 - }); - array.push({ - name: "Guadeloupe", - alpha_2: "GP", - alpha_3: "GLP", - un_code: 312 - }); - array.push({ - name: "Guam", - alpha_2: "GU", - alpha_3: "GUM", - un_code: 316 - }); - array.push({ - name: "Guatemala", - alpha_2: "GT", - alpha_3: "GTM", - un_code: 320 - }); - array.push({ - name: "Guernsey", - alpha_2: "GG", - alpha_3: "GGY", - un_code: 831 - }); - array.push({ - name: "Guinea", - alpha_2: "GN", - alpha_3: "GIN", - un_code: 324 - }); - array.push({ - name: "Guinea-Bissau", - alpha_2: "GW", - alpha_3: "GNB", - un_code: 624 - }); - array.push({ - name: "Guyana", - alpha_2: "GY", - alpha_3: "GUY", - un_code: 328 - }); - array.push({ - name: "Haiti", - alpha_2: "HT", - alpha_3: "HTI", - un_code: 332 - }); - array.push({ - name: "Heard and Mcdonald Islands", - alpha_2: "HM", - alpha_3: "HMD", - un_code: 334 - }); - array.push({ - name: "Holy See (Vatican City State)", - alpha_2: "VA", - alpha_3: "VAT", - un_code: 336 - }); - array.push({ - name: "Honduras", - alpha_2: "HN", - alpha_3: "HND", - un_code: 340 - }); - array.push({ - name: "Hungary", - alpha_2: "HU", - alpha_3: "HUN", - un_code: 348 - }); - array.push({ - name: "Iceland", - alpha_2: "IS", - alpha_3: "ISL", - un_code: 352 - }); - array.push({ - name: "India", - alpha_2: "IN", - alpha_3: "IND", - un_code: 356 - }); - array.push({ - name: "Indonesia", - alpha_2: "ID", - alpha_3: "IDN", - un_code: 360 - }); - array.push({ - name: "Iran, Islamic Republic of", - alpha_2: "IR", - alpha_3: "IRN", - un_code: 364 - }); - array.push({ - name: "Iraq", - alpha_2: "IQ", - alpha_3: "IRQ", - un_code: 368 - }); - array.push({ - name: "Ireland", - alpha_2: "IE", - alpha_3: "IRL", - un_code: 372 - }); - array.push({ - name: "Isle of Man", - alpha_2: "IM", - alpha_3: "IMN", - un_code: 833 - }); - array.push({ - name: "Israel", - alpha_2: "IL", - alpha_3: "ISR", - un_code: 376 - }); - array.push({ - name: "Italy", - alpha_2: "IT", - alpha_3: "ITA", - un_code: 380 - }); - array.push({ - name: "Jamaica", - alpha_2: "JM", - alpha_3: "JAM", - un_code: 388 - }); - array.push({ - name: "Japan", - alpha_2: "JP", - alpha_3: "JPN", - un_code: 392 - }); - array.push({ - name: "Jersey", - alpha_2: "JE", - alpha_3: "JEY", - un_code: 832 - }); - array.push({ - name: "Jordan", - alpha_2: "JO", - alpha_3: "JOR", - un_code: 400 - }); - array.push({ - name: "Kazakhstan", - alpha_2: "KZ", - alpha_3: "KAZ", - un_code: 398 - }); - array.push({ - name: "Kenya", - alpha_2: "KE", - alpha_3: "KEN", - un_code: 404 - }); - array.push({ - name: "Kiribati", - alpha_2: "KI", - alpha_3: "KIR", - un_code: 296 - }); - array.push({ - name: "Korea (North)", - alpha_2: "KP", - alpha_3: "PRK", - un_code: 408 - }); - array.push({ - name: "Korea (South)", - alpha_2: "KR", - alpha_3: "KOR", - un_code: 410 - }); - array.push({ - name: "Kuwait", - alpha_2: "KW", - alpha_3: "KWT", - un_code: 414 - }); - array.push({ - name: "Kyrgyzstan", - alpha_2: "KG", - alpha_3: "KGZ", - un_code: 417 - }); - array.push({ - name: "Lao PDR", - alpha_2: "LA", - alpha_3: "LAO", - un_code: 418 - }); - array.push({ - name: "Latvia", - alpha_2: "LV", - alpha_3: "LVA", - un_code: 428 - }); - array.push({ - name: "Lebanon", - alpha_2: "LB", - alpha_3: "LBN", - un_code: 422 - }); - array.push({ - name: "Lesotho", - alpha_2: "LS", - alpha_3: "LSO", - un_code: 426 - }); - array.push({ - name: "Liberia", - alpha_2: "LR", - alpha_3: "LBR", - un_code: 430 - }); - array.push({ - name: "Libya", - alpha_2: "LY", - alpha_3: "LBY", - un_code: 434 - }); - array.push({ - name: "Liechtenstein", - alpha_2: "LI", - alpha_3: "LIE", - un_code: 438 - }); - array.push({ - name: "Lithuania", - alpha_2: "LT", - alpha_3: "LTU", - un_code: 440 - }); - array.push({ - name: "Luxembourg", - alpha_2: "LU", - alpha_3: "LUX", - un_code: 442 - }); - array.push({ - name: "Macedonia, Republic of", - alpha_2: "MK", - alpha_3: "MKD", - un_code: 807 - }); - array.push({ - name: "Madagascar", - alpha_2: "MG", - alpha_3: "MDG", - un_code: 450 - }); - array.push({ - name: "Malawi", - alpha_2: "MW", - alpha_3: "MWI", - un_code: 454 - }); - array.push({ - name: "Malaysia", - alpha_2: "MY", - alpha_3: "MYS", - un_code: 458 - }); - array.push({ - name: "Maldives", - alpha_2: "MV", - alpha_3: "MDV", - un_code: 462 - }); - array.push({ - name: "Mali", - alpha_2: "ML", - alpha_3: "MLI", - un_code: 466 - }); - array.push({ - name: "Malta", - alpha_2: "MT", - alpha_3: "MLT", - un_code: 470 - }); - array.push({ - name: "Marshall Islands", - alpha_2: "MH", - alpha_3: "MHL", - un_code: 584 - }); - array.push({ - name: "Martinique", - alpha_2: "MQ", - alpha_3: "MTQ", - un_code: 474 - }); - array.push({ - name: "Mauritania", - alpha_2: "MR", - alpha_3: "MRT", - un_code: 478 - }); - array.push({ - name: "Mauritius", - alpha_2: "MU", - alpha_3: "MUS", - un_code: 480 - }); - array.push({ - name: "Mayotte", - alpha_2: "YT", - alpha_3: "MYT", - un_code: 175 - }); - array.push({ - name: "Mexico", - alpha_2: "MX", - alpha_3: "MEX", - un_code: 484 - }); - array.push({ - name: "Micronesia, Federated States of", - alpha_2: "FM", - alpha_3: "FSM", - un_code: 583 - }); - array.push({ - name: "Moldova", - alpha_2: "MD", - alpha_3: "MDA", - un_code: 498 - }); - array.push({ - name: "Monaco", - alpha_2: "MC", - alpha_3: "MCO", - un_code: 492 - }); - array.push({ - name: "Mongolia", - alpha_2: "MN", - alpha_3: "MNG", - un_code: 496 - }); - array.push({ - name: "Montenegro", - alpha_2: "ME", - alpha_3: "MNE", - un_code: 499 - }); - array.push({ - name: "Montserrat", - alpha_2: "MS", - alpha_3: "MSR", - un_code: 500 - }); - array.push({ - name: "Morocco", - alpha_2: "MA", - alpha_3: "MAR", - un_code: 504 - }); - array.push({ - name: "Mozambique", - alpha_2: "MZ", - alpha_3: "MOZ", - un_code: 508 - }); - array.push({ - name: "Myanmar", - alpha_2: "MM", - alpha_3: "MMR", - un_code: 104 - }); - array.push({ - name: "Namibia", - alpha_2: "NA", - alpha_3: "NAM", - un_code: 516 - }); - array.push({ - name: "Nauru", - alpha_2: "NR", - alpha_3: "NRU", - un_code: 520 - }); - array.push({ - name: "Nepal", - alpha_2: "NP", - alpha_3: "NPL", - un_code: 524 - }); - array.push({ - name: "Netherlands", - alpha_2: "NL", - alpha_3: "NLD", - un_code: 528 - }); - array.push({ - name: "Netherlands Antilles", - alpha_2: "AN", - alpha_3: "ANT", - un_code: 530 - }); - array.push({ - name: "New Caledonia", - alpha_2: "NC", - alpha_3: "NCL", - un_code: 540 - }); - array.push({ - name: "New Zealand", - alpha_2: "NZ", - alpha_3: "NZL", - un_code: 554 - }); - array.push({ - name: "Nicaragua", - alpha_2: "NI", - alpha_3: "NIC", - un_code: 558 - }); - array.push({ - name: "Niger", - alpha_2: "NE", - alpha_3: "NER", - un_code: 562 - }); - array.push({ - name: "Nigeria", - alpha_2: "NG", - alpha_3: "NGA", - un_code: 566 - }); - array.push({ - name: "Niue", - alpha_2: "NU", - alpha_3: "NIU", - un_code: 570 - }); - array.push({ - name: "Norfolk Island", - alpha_2: "NF", - alpha_3: "NFK", - un_code: 574 - }); - array.push({ - name: "Northern Mariana Islands", - alpha_2: "MP", - alpha_3: "MNP", - un_code: 580 - }); - array.push({ - name: "Norway", - alpha_2: "NO", - alpha_3: "NOR", - un_code: 578 - }); - array.push({ - name: "Oman", - alpha_2: "OM", - alpha_3: "OMN", - un_code: 512 - }); - array.push({ - name: "Pakistan", - alpha_2: "PK", - alpha_3: "PAK", - un_code: 586 - }); - array.push({ - name: "Palau", - alpha_2: "PW", - alpha_3: "PLW", - un_code: 585 - }); - array.push({ - name: "Palestinian Territory", - alpha_2: "PS", - alpha_3: "PSE", - un_code: 275 - }); - array.push({ - name: "Panama", - alpha_2: "PA", - alpha_3: "PAN", - un_code: 591 - }); - array.push({ - name: "Papua New Guinea", - alpha_2: "PG", - alpha_3: "PNG", - un_code: 598 - }); - array.push({ - name: "Paraguay", - alpha_2: "PY", - alpha_3: "PRY", - un_code: 600 - }); - array.push({ - name: "Peru", - alpha_2: "PE", - alpha_3: "PER", - un_code: 604 - }); - array.push({ - name: "Philippines", - alpha_2: "PH", - alpha_3: "PHL", - un_code: 608 - }); - array.push({ - name: "Pitcairn", - alpha_2: "PN", - alpha_3: "PCN", - un_code: 612 - }); - array.push({ - name: "Poland", - alpha_2: "PL", - alpha_3: "POL", - un_code: 616 - }); - array.push({ - name: "Portugal", - alpha_2: "PT", - alpha_3: "PRT", - un_code: 620 - }); - array.push({ - name: "Puerto Rico", - alpha_2: "PR", - alpha_3: "PRI", - un_code: 630 - }); - array.push({ - name: "Qatar", - alpha_2: "QA", - alpha_3: "QAT", - un_code: 634 - }); - array.push({ - name: "Réunion", - alpha_2: "RE", - alpha_3: "REU", - un_code: 638 - }); - array.push({ - name: "Romania", - alpha_2: "RO", - alpha_3: "ROU", - un_code: 642 - }); - array.push({ - name: "Russian Federation", - alpha_2: "RU", - alpha_3: "RUS", - un_code: 643 - }); - array.push({ - name: "Rwanda", - alpha_2: "RW", - alpha_3: "RWA", - un_code: 646 - }); - array.push({ - name: "Saint-Barthélemy", - alpha_2: "BL", - alpha_3: "BLM", - un_code: 652 - }); - array.push({ - name: "Saint Helena", - alpha_2: "SH", - alpha_3: "SHN", - un_code: 654 - }); - array.push({ - name: "Saint Kitts and Nevis", - alpha_2: "KN", - alpha_3: "KNA", - un_code: 659 - }); - array.push({ - name: "Saint Lucia", - alpha_2: "LC", - alpha_3: "LCA", - un_code: 662 - }); - array.push({ - name: "Saint-Martin (French part)", - alpha_2: "MF", - alpha_3: "MAF", - un_code: 663 - }); - array.push({ - name: "Saint Pierre and Miquelon", - alpha_2: "PM", - alpha_3: "SPM", - un_code: 666 - }); - array.push({ - name: "Saint Vincent and Grenadines", - alpha_2: "VC", - alpha_3: "VCT", - un_code: 670 - }); - array.push({ - name: "Samoa", - alpha_2: "WS", - alpha_3: "WSM", - un_code: 882 - }); - array.push({ - name: "San Marino", - alpha_2: "SM", - alpha_3: "SMR", - un_code: 674 - }); - array.push({ - name: "Sao Tome and Principe", - alpha_2: "ST", - alpha_3: "STP", - un_code: 678 - }); - array.push({ - name: "Saudi Arabia", - alpha_2: "SA", - alpha_3: "SAU", - un_code: 682 - }); - array.push({ - name: "Senegal", - alpha_2: "SN", - alpha_3: "SEN", - un_code: 686 - }); - array.push({ - name: "Serbia", - alpha_2: "RS", - alpha_3: "SRB", - un_code: 688 - }); - array.push({ - name: "Seychelles", - alpha_2: "SC", - alpha_3: "SYC", - un_code: 690 - }); - array.push({ - name: "Sierra Leone", - alpha_2: "SL", - alpha_3: "SLE", - un_code: 694 - }); - array.push({ - name: "Singapore", - alpha_2: "SG", - alpha_3: "SGP", - un_code: 702 - }); - array.push({ - name: "Slovakia", - alpha_2: "SK", - alpha_3: "SVK", - un_code: 703 - }); - array.push({ - name: "Slovenia", - alpha_2: "SI", - alpha_3: "SVN", - un_code: 705 - }); - array.push({ - name: "Solomon Islands", - alpha_2: "SB", - alpha_3: "SLB", - un_code: 90 - }); - array.push({ - name: "Somalia", - alpha_2: "SO", - alpha_3: "SOM", - un_code: 706 - }); - array.push({ - name: "South Africa", - alpha_2: "ZA", - alpha_3: "ZAF", - un_code: 710 - }); - array.push({ - name: "South Georgia and the South Sandwich Islands", - alpha_2: "GS", - alpha_3: "SGS", - un_code: 239 - }); - array.push({ - name: "South Sudan", - alpha_2: "SS", - alpha_3: "SSD", - un_code: 728 - }); - array.push({ - name: "Spain", - alpha_2: "ES", - alpha_3: "ESP", - un_code: 724 - }); - array.push({ - name: "Sri Lanka", - alpha_2: "LK", - alpha_3: "LKA", - un_code: 144 - }); - array.push({ - name: "Sudan", - alpha_2: "SD", - alpha_3: "SDN", - un_code: 736 - }); - array.push({ - name: "Suriname", - alpha_2: "SR", - alpha_3: "SUR", - un_code: 740 - }); - array.push({ - name: "Svalbard and Jan Mayen Islands", - alpha_2: "SJ", - alpha_3: "SJM", - un_code: 744 - }); - array.push({ - name: "Swaziland", - alpha_2: "SZ", - alpha_3: "SWZ", - un_code: 748 - }); - array.push({ - name: "Sweden", - alpha_2: "SE", - alpha_3: "SWE", - un_code: 752 - }); - array.push({ - name: "Switzerland", - alpha_2: "CH", - alpha_3: "CHE", - un_code: 756 - }); - array.push({ - name: "Syrian Arab Republic (Syria)", - alpha_2: "SY", - alpha_3: "SYR", - un_code: 760 - }); - array.push({ - name: "Taiwan, Republic of China", - alpha_2: "TW", - alpha_3: "TWN", - un_code: 158 - }); - array.push({ - name: "Tajikistan", - alpha_2: "TJ", - alpha_3: "TJK", - un_code: 762 - }); - array.push({ - name: "Tanzania, United Republic of", - alpha_2: "TZ", - alpha_3: "TZA", - un_code: 834 - }); - array.push({ - name: "Thailand", - alpha_2: "TH", - alpha_3: "THA", - un_code: 764 - }); - array.push({ - name: "Timor-Leste", - alpha_2: "TL", - alpha_3: "TLS", - un_code: 626 - }); - array.push({ - name: "Togo", - alpha_2: "TG", - alpha_3: "TGO", - un_code: 768 - }); - array.push({ - name: "Tokelau", - alpha_2: "TK", - alpha_3: "TKL", - un_code: 772 - }); - array.push({ - name: "Tonga", - alpha_2: "TO", - alpha_3: "TON", - un_code: 776 - }); - array.push({ - name: "Trinidad and Tobago", - alpha_2: "TT", - alpha_3: "TTO", - un_code: 780 - }); - array.push({ - name: "Tunisia", - alpha_2: "TN", - alpha_3: "TUN", - un_code: 788 - }); - array.push({ - name: "Turkey", - alpha_2: "TR", - alpha_3: "TUR", - un_code: 792 - }); - array.push({ - name: "Turkmenistan", - alpha_2: "TM", - alpha_3: "TKM", - un_code: 795 - }); - array.push({ - name: "Turks and Caicos Islands", - alpha_2: "TC", - alpha_3: "TCA", - un_code: 796 - }); - array.push({ - name: "Tuvalu", - alpha_2: "TV", - alpha_3: "TUV", - un_code: 798 - }); - array.push({ - name: "Uganda", - alpha_2: "UG", - alpha_3: "UGA", - un_code: 800 - }); - array.push({ - name: "Ukraine", - alpha_2: "UA", - alpha_3: "UKR", - un_code: 804 - }); - array.push({ - name: "United Arab Emirates", - alpha_2: "AE", - alpha_3: "ARE", - un_code: 784 - }); - array.push({ - name: "United Kingdom", - alpha_2: "GB", - alpha_3: "GBR", - un_code: 826 - }); - array.push({ - name: "United States of America", - alpha_2: "US", - alpha_3: "USA", - un_code: 840 - }); - array.push({ - name: "US Minor Outlying Islands", - alpha_2: "UM", - alpha_3: "UMI", - un_code: 581 - }); - array.push({ - name: "Uruguay", - alpha_2: "UY", - alpha_3: "URY", - un_code: 858 - }); - array.push({ - name: "Uzbekistan", - alpha_2: "UZ", - alpha_3: "UZB", - un_code: 860 - }); - array.push({ - name: "Vanuatu", - alpha_2: "VU", - alpha_3: "VUT", - un_code: 548 - }); - array.push({ - name: "Venezuela (Bolivarian Republic)", - alpha_2: "VE", - alpha_3: "VEN", - un_code: 862 - }); - array.push({ - name: "Viet Nam", - alpha_2: "VN", - alpha_3: "VNM", - un_code: 704 - }); - array.push({ - name: "Virgin Islands, US", - alpha_2: "VI", - alpha_3: "VIR", - un_code: 850 - }); - array.push({ - name: "Wallis and Futuna Islands", - alpha_2: "WF", - alpha_3: "WLF", - un_code: 876 - }); - array.push({ - name: "Western Sahara", - alpha_2: "EH", - alpha_3: "ESH", - un_code: 732 - }); - array.push({ - name: "Yemen", - alpha_2: "YE", - alpha_3: "YEM", - un_code: 887 - }); - array.push({ - name: "Zambia", - alpha_2: "ZM", - alpha_3: "ZMB", - un_code: 894 - }); - array.push({ - name: "Zimbabwe", - alpha_2: "ZW", - alpha_3: "ZWE", - un_code: 716 - }); - }; - - export function country_name(alpha_code: string, fallback?: string) { - return (alpha_2_map[alpha_code.toUpperCase()] || {name: fallback || tr("unknown country")}).name; - } - - fill_country_infos(country_infos); - for(const country of country_infos) - alpha_2_map[country.alpha_2] = country; +interface CountryInfo { + name: string; + alpha_2: string; + alpha_3: string; + un_code: number; } +const country_infos: CountryInfo[] = []; +const alpha_2_map: {[name: string]:CountryInfo} = {}; + +const fill_country_infos = (array: CountryInfo[]) => { + array.push({ + name: "Afghanistan", + alpha_2: "AF", + alpha_3: "AFG", + un_code: 4 + }); + array.push({ + name: "Aland Islands", + alpha_2: "AX", + alpha_3: "ALA", + un_code: 248 + }); + array.push({ + name: "Albania", + alpha_2: "AL", + alpha_3: "ALB", + un_code: 8 + }); + array.push({ + name: "Algeria", + alpha_2: "DZ", + alpha_3: "DZA", + un_code: 12 + }); + array.push({ + name: "American Samoa", + alpha_2: "AS", + alpha_3: "ASM", + un_code: 16 + }); + array.push({ + name: "Andorra", + alpha_2: "AD", + alpha_3: "AND", + un_code: 20 + }); + array.push({ + name: "Angola", + alpha_2: "AO", + alpha_3: "AGO", + un_code: 24 + }); + array.push({ + name: "Anguilla", + alpha_2: "AI", + alpha_3: "AIA", + un_code: 660 + }); + array.push({ + name: "Antarctica", + alpha_2: "AQ", + alpha_3: "ATA", + un_code: 10 + }); + array.push({ + name: "Antigua and Barbuda", + alpha_2: "AG", + alpha_3: "ATG", + un_code: 28 + }); + array.push({ + name: "Argentina", + alpha_2: "AR", + alpha_3: "ARG", + un_code: 32 + }); + array.push({ + name: "Armenia", + alpha_2: "AM", + alpha_3: "ARM", + un_code: 51 + }); + array.push({ + name: "Aruba", + alpha_2: "AW", + alpha_3: "ABW", + un_code: 533 + }); + array.push({ + name: "Australia", + alpha_2: "AU", + alpha_3: "AUS", + un_code: 36 + }); + array.push({ + name: "Austria", + alpha_2: "AT", + alpha_3: "AUT", + un_code: 40 + }); + array.push({ + name: "Azerbaijan", + alpha_2: "AZ", + alpha_3: "AZE", + un_code: 31 + }); + array.push({ + name: "Bahamas", + alpha_2: "BS", + alpha_3: "BHS", + un_code: 44 + }); + array.push({ + name: "Bahrain", + alpha_2: "BH", + alpha_3: "BHR", + un_code: 48 + }); + array.push({ + name: "Bangladesh", + alpha_2: "BD", + alpha_3: "BGD", + un_code: 50 + }); + array.push({ + name: "Barbados", + alpha_2: "BB", + alpha_3: "BRB", + un_code: 52 + }); + array.push({ + name: "Belarus", + alpha_2: "BY", + alpha_3: "BLR", + un_code: 112 + }); + array.push({ + name: "Belgium", + alpha_2: "BE", + alpha_3: "BEL", + un_code: 56 + }); + array.push({ + name: "Belize", + alpha_2: "BZ", + alpha_3: "BLZ", + un_code: 84 + }); + array.push({ + name: "Benin", + alpha_2: "BJ", + alpha_3: "BEN", + un_code: 204 + }); + array.push({ + name: "Bermuda", + alpha_2: "BM", + alpha_3: "BMU", + un_code: 60 + }); + array.push({ + name: "Bhutan", + alpha_2: "BT", + alpha_3: "BTN", + un_code: 64 + }); + array.push({ + name: "Bolivia", + alpha_2: "BO", + alpha_3: "BOL", + un_code: 68 + }); + array.push({ + name: "Bosnia and Herzegovina", + alpha_2: "BA", + alpha_3: "BIH", + un_code: 70 + }); + array.push({ + name: "Botswana", + alpha_2: "BW", + alpha_3: "BWA", + un_code: 72 + }); + array.push({ + name: "Bouvet Island", + alpha_2: "BV", + alpha_3: "BVT", + un_code: 74 + }); + array.push({ + name: "Brazil", + alpha_2: "BR", + alpha_3: "BRA", + un_code: 76 + }); + array.push({ + name: "British Virgin Islands", + alpha_2: "VG", + alpha_3: "VGB", + un_code: 92 + }); + array.push({ + name: "British Indian Ocean Territory", + alpha_2: "IO", + alpha_3: "IOT", + un_code: 86 + }); + array.push({ + name: "Brunei Darussalam", + alpha_2: "BN", + alpha_3: "BRN", + un_code: 96 + }); + array.push({ + name: "Bulgaria", + alpha_2: "BG", + alpha_3: "BGR", + un_code: 100 + }); + array.push({ + name: "Burkina Faso", + alpha_2: "BF", + alpha_3: "BFA", + un_code: 854 + }); + array.push({ + name: "Burundi", + alpha_2: "BI", + alpha_3: "BDI", + un_code: 108 + }); + array.push({ + name: "Cambodia", + alpha_2: "KH", + alpha_3: "KHM", + un_code: 116 + }); + array.push({ + name: "Cameroon", + alpha_2: "CM", + alpha_3: "CMR", + un_code: 120 + }); + array.push({ + name: "Canada", + alpha_2: "CA", + alpha_3: "CAN", + un_code: 124 + }); + array.push({ + name: "Cape Verde", + alpha_2: "CV", + alpha_3: "CPV", + un_code: 132 + }); + array.push({ + name: "Cayman Islands", + alpha_2: "KY", + alpha_3: "CYM", + un_code: 136 + }); + array.push({ + name: "Central African Republic", + alpha_2: "CF", + alpha_3: "CAF", + un_code: 140 + }); + array.push({ + name: "Chad", + alpha_2: "TD", + alpha_3: "TCD", + un_code: 148 + }); + array.push({ + name: "Chile", + alpha_2: "CL", + alpha_3: "CHL", + un_code: 152 + }); + array.push({ + name: "China", + alpha_2: "CN", + alpha_3: "CHN", + un_code: 156 + }); + array.push({ + name: "Hong Kong, SAR China", + alpha_2: "HK", + alpha_3: "HKG", + un_code: 344 + }); + array.push({ + name: "Macao, SAR China", + alpha_2: "MO", + alpha_3: "MAC", + un_code: 446 + }); + array.push({ + name: "Christmas Island", + alpha_2: "CX", + alpha_3: "CXR", + un_code: 162 + }); + array.push({ + name: "Cocos (Keeling) Islands", + alpha_2: "CC", + alpha_3: "CCK", + un_code: 166 + }); + array.push({ + name: "Colombia", + alpha_2: "CO", + alpha_3: "COL", + un_code: 170 + }); + array.push({ + name: "Comoros", + alpha_2: "KM", + alpha_3: "COM", + un_code: 174 + }); + array.push({ + name: "Congo (Brazzaville)", + alpha_2: "CG", + alpha_3: "COG", + un_code: 178 + }); + array.push({ + name: "Congo, (Kinshasa)", + alpha_2: "CD", + alpha_3: "COD", + un_code: 180 + }); + array.push({ + name: "Cook Islands", + alpha_2: "CK", + alpha_3: "COK", + un_code: 184 + }); + array.push({ + name: "Costa Rica", + alpha_2: "CR", + alpha_3: "CRI", + un_code: 188 + }); + array.push({ + name: "Côte d'Ivoire", + alpha_2: "CI", + alpha_3: "CIV", + un_code: 384 + }); + array.push({ + name: "Croatia", + alpha_2: "HR", + alpha_3: "HRV", + un_code: 191 + }); + array.push({ + name: "Cuba", + alpha_2: "CU", + alpha_3: "CUB", + un_code: 192 + }); + array.push({ + name: "Cyprus", + alpha_2: "CY", + alpha_3: "CYP", + un_code: 196 + }); + array.push({ + name: "Czech Republic", + alpha_2: "CZ", + alpha_3: "CZE", + un_code: 203 + }); + array.push({ + name: "Denmark", + alpha_2: "DK", + alpha_3: "DNK", + un_code: 208 + }); + array.push({ + name: "Djibouti", + alpha_2: "DJ", + alpha_3: "DJI", + un_code: 262 + }); + array.push({ + name: "Dominica", + alpha_2: "DM", + alpha_3: "DMA", + un_code: 212 + }); + array.push({ + name: "Dominican Republic", + alpha_2: "DO", + alpha_3: "DOM", + un_code: 214 + }); + array.push({ + name: "Ecuador", + alpha_2: "EC", + alpha_3: "ECU", + un_code: 218 + }); + array.push({ + name: "Egypt", + alpha_2: "EG", + alpha_3: "EGY", + un_code: 818 + }); + array.push({ + name: "El Salvador", + alpha_2: "SV", + alpha_3: "SLV", + un_code: 222 + }); + array.push({ + name: "Equatorial Guinea", + alpha_2: "GQ", + alpha_3: "GNQ", + un_code: 226 + }); + array.push({ + name: "Eritrea", + alpha_2: "ER", + alpha_3: "ERI", + un_code: 232 + }); + array.push({ + name: "Estonia", + alpha_2: "EE", + alpha_3: "EST", + un_code: 233 + }); + array.push({ + name: "Ethiopia", + alpha_2: "ET", + alpha_3: "ETH", + un_code: 231 + }); + array.push({ + name: "Falkland Islands (Malvinas)", + alpha_2: "FK", + alpha_3: "FLK", + un_code: 238 + }); + array.push({ + name: "Faroe Islands", + alpha_2: "FO", + alpha_3: "FRO", + un_code: 234 + }); + array.push({ + name: "Fiji", + alpha_2: "FJ", + alpha_3: "FJI", + un_code: 242 + }); + array.push({ + name: "Finland", + alpha_2: "FI", + alpha_3: "FIN", + un_code: 246 + }); + array.push({ + name: "France", + alpha_2: "FR", + alpha_3: "FRA", + un_code: 250 + }); + array.push({ + name: "French Guiana", + alpha_2: "GF", + alpha_3: "GUF", + un_code: 254 + }); + array.push({ + name: "French Polynesia", + alpha_2: "PF", + alpha_3: "PYF", + un_code: 258 + }); + array.push({ + name: "French Southern Territories", + alpha_2: "TF", + alpha_3: "ATF", + un_code: 260 + }); + array.push({ + name: "Gabon", + alpha_2: "GA", + alpha_3: "GAB", + un_code: 266 + }); + array.push({ + name: "Gambia", + alpha_2: "GM", + alpha_3: "GMB", + un_code: 270 + }); + array.push({ + name: "Georgia", + alpha_2: "GE", + alpha_3: "GEO", + un_code: 268 + }); + array.push({ + name: "Germany", + alpha_2: "DE", + alpha_3: "DEU", + un_code: 276 + }); + array.push({ + name: "Ghana", + alpha_2: "GH", + alpha_3: "GHA", + un_code: 288 + }); + array.push({ + name: "Gibraltar", + alpha_2: "GI", + alpha_3: "GIB", + un_code: 292 + }); + array.push({ + name: "Greece", + alpha_2: "GR", + alpha_3: "GRC", + un_code: 300 + }); + array.push({ + name: "Greenland", + alpha_2: "GL", + alpha_3: "GRL", + un_code: 304 + }); + array.push({ + name: "Grenada", + alpha_2: "GD", + alpha_3: "GRD", + un_code: 308 + }); + array.push({ + name: "Guadeloupe", + alpha_2: "GP", + alpha_3: "GLP", + un_code: 312 + }); + array.push({ + name: "Guam", + alpha_2: "GU", + alpha_3: "GUM", + un_code: 316 + }); + array.push({ + name: "Guatemala", + alpha_2: "GT", + alpha_3: "GTM", + un_code: 320 + }); + array.push({ + name: "Guernsey", + alpha_2: "GG", + alpha_3: "GGY", + un_code: 831 + }); + array.push({ + name: "Guinea", + alpha_2: "GN", + alpha_3: "GIN", + un_code: 324 + }); + array.push({ + name: "Guinea-Bissau", + alpha_2: "GW", + alpha_3: "GNB", + un_code: 624 + }); + array.push({ + name: "Guyana", + alpha_2: "GY", + alpha_3: "GUY", + un_code: 328 + }); + array.push({ + name: "Haiti", + alpha_2: "HT", + alpha_3: "HTI", + un_code: 332 + }); + array.push({ + name: "Heard and Mcdonald Islands", + alpha_2: "HM", + alpha_3: "HMD", + un_code: 334 + }); + array.push({ + name: "Holy See (Vatican City State)", + alpha_2: "VA", + alpha_3: "VAT", + un_code: 336 + }); + array.push({ + name: "Honduras", + alpha_2: "HN", + alpha_3: "HND", + un_code: 340 + }); + array.push({ + name: "Hungary", + alpha_2: "HU", + alpha_3: "HUN", + un_code: 348 + }); + array.push({ + name: "Iceland", + alpha_2: "IS", + alpha_3: "ISL", + un_code: 352 + }); + array.push({ + name: "India", + alpha_2: "IN", + alpha_3: "IND", + un_code: 356 + }); + array.push({ + name: "Indonesia", + alpha_2: "ID", + alpha_3: "IDN", + un_code: 360 + }); + array.push({ + name: "Iran, Islamic Republic of", + alpha_2: "IR", + alpha_3: "IRN", + un_code: 364 + }); + array.push({ + name: "Iraq", + alpha_2: "IQ", + alpha_3: "IRQ", + un_code: 368 + }); + array.push({ + name: "Ireland", + alpha_2: "IE", + alpha_3: "IRL", + un_code: 372 + }); + array.push({ + name: "Isle of Man", + alpha_2: "IM", + alpha_3: "IMN", + un_code: 833 + }); + array.push({ + name: "Israel", + alpha_2: "IL", + alpha_3: "ISR", + un_code: 376 + }); + array.push({ + name: "Italy", + alpha_2: "IT", + alpha_3: "ITA", + un_code: 380 + }); + array.push({ + name: "Jamaica", + alpha_2: "JM", + alpha_3: "JAM", + un_code: 388 + }); + array.push({ + name: "Japan", + alpha_2: "JP", + alpha_3: "JPN", + un_code: 392 + }); + array.push({ + name: "Jersey", + alpha_2: "JE", + alpha_3: "JEY", + un_code: 832 + }); + array.push({ + name: "Jordan", + alpha_2: "JO", + alpha_3: "JOR", + un_code: 400 + }); + array.push({ + name: "Kazakhstan", + alpha_2: "KZ", + alpha_3: "KAZ", + un_code: 398 + }); + array.push({ + name: "Kenya", + alpha_2: "KE", + alpha_3: "KEN", + un_code: 404 + }); + array.push({ + name: "Kiribati", + alpha_2: "KI", + alpha_3: "KIR", + un_code: 296 + }); + array.push({ + name: "Korea (North)", + alpha_2: "KP", + alpha_3: "PRK", + un_code: 408 + }); + array.push({ + name: "Korea (South)", + alpha_2: "KR", + alpha_3: "KOR", + un_code: 410 + }); + array.push({ + name: "Kuwait", + alpha_2: "KW", + alpha_3: "KWT", + un_code: 414 + }); + array.push({ + name: "Kyrgyzstan", + alpha_2: "KG", + alpha_3: "KGZ", + un_code: 417 + }); + array.push({ + name: "Lao PDR", + alpha_2: "LA", + alpha_3: "LAO", + un_code: 418 + }); + array.push({ + name: "Latvia", + alpha_2: "LV", + alpha_3: "LVA", + un_code: 428 + }); + array.push({ + name: "Lebanon", + alpha_2: "LB", + alpha_3: "LBN", + un_code: 422 + }); + array.push({ + name: "Lesotho", + alpha_2: "LS", + alpha_3: "LSO", + un_code: 426 + }); + array.push({ + name: "Liberia", + alpha_2: "LR", + alpha_3: "LBR", + un_code: 430 + }); + array.push({ + name: "Libya", + alpha_2: "LY", + alpha_3: "LBY", + un_code: 434 + }); + array.push({ + name: "Liechtenstein", + alpha_2: "LI", + alpha_3: "LIE", + un_code: 438 + }); + array.push({ + name: "Lithuania", + alpha_2: "LT", + alpha_3: "LTU", + un_code: 440 + }); + array.push({ + name: "Luxembourg", + alpha_2: "LU", + alpha_3: "LUX", + un_code: 442 + }); + array.push({ + name: "Macedonia, Republic of", + alpha_2: "MK", + alpha_3: "MKD", + un_code: 807 + }); + array.push({ + name: "Madagascar", + alpha_2: "MG", + alpha_3: "MDG", + un_code: 450 + }); + array.push({ + name: "Malawi", + alpha_2: "MW", + alpha_3: "MWI", + un_code: 454 + }); + array.push({ + name: "Malaysia", + alpha_2: "MY", + alpha_3: "MYS", + un_code: 458 + }); + array.push({ + name: "Maldives", + alpha_2: "MV", + alpha_3: "MDV", + un_code: 462 + }); + array.push({ + name: "Mali", + alpha_2: "ML", + alpha_3: "MLI", + un_code: 466 + }); + array.push({ + name: "Malta", + alpha_2: "MT", + alpha_3: "MLT", + un_code: 470 + }); + array.push({ + name: "Marshall Islands", + alpha_2: "MH", + alpha_3: "MHL", + un_code: 584 + }); + array.push({ + name: "Martinique", + alpha_2: "MQ", + alpha_3: "MTQ", + un_code: 474 + }); + array.push({ + name: "Mauritania", + alpha_2: "MR", + alpha_3: "MRT", + un_code: 478 + }); + array.push({ + name: "Mauritius", + alpha_2: "MU", + alpha_3: "MUS", + un_code: 480 + }); + array.push({ + name: "Mayotte", + alpha_2: "YT", + alpha_3: "MYT", + un_code: 175 + }); + array.push({ + name: "Mexico", + alpha_2: "MX", + alpha_3: "MEX", + un_code: 484 + }); + array.push({ + name: "Micronesia, Federated States of", + alpha_2: "FM", + alpha_3: "FSM", + un_code: 583 + }); + array.push({ + name: "Moldova", + alpha_2: "MD", + alpha_3: "MDA", + un_code: 498 + }); + array.push({ + name: "Monaco", + alpha_2: "MC", + alpha_3: "MCO", + un_code: 492 + }); + array.push({ + name: "Mongolia", + alpha_2: "MN", + alpha_3: "MNG", + un_code: 496 + }); + array.push({ + name: "Montenegro", + alpha_2: "ME", + alpha_3: "MNE", + un_code: 499 + }); + array.push({ + name: "Montserrat", + alpha_2: "MS", + alpha_3: "MSR", + un_code: 500 + }); + array.push({ + name: "Morocco", + alpha_2: "MA", + alpha_3: "MAR", + un_code: 504 + }); + array.push({ + name: "Mozambique", + alpha_2: "MZ", + alpha_3: "MOZ", + un_code: 508 + }); + array.push({ + name: "Myanmar", + alpha_2: "MM", + alpha_3: "MMR", + un_code: 104 + }); + array.push({ + name: "Namibia", + alpha_2: "NA", + alpha_3: "NAM", + un_code: 516 + }); + array.push({ + name: "Nauru", + alpha_2: "NR", + alpha_3: "NRU", + un_code: 520 + }); + array.push({ + name: "Nepal", + alpha_2: "NP", + alpha_3: "NPL", + un_code: 524 + }); + array.push({ + name: "Netherlands", + alpha_2: "NL", + alpha_3: "NLD", + un_code: 528 + }); + array.push({ + name: "Netherlands Antilles", + alpha_2: "AN", + alpha_3: "ANT", + un_code: 530 + }); + array.push({ + name: "New Caledonia", + alpha_2: "NC", + alpha_3: "NCL", + un_code: 540 + }); + array.push({ + name: "New Zealand", + alpha_2: "NZ", + alpha_3: "NZL", + un_code: 554 + }); + array.push({ + name: "Nicaragua", + alpha_2: "NI", + alpha_3: "NIC", + un_code: 558 + }); + array.push({ + name: "Niger", + alpha_2: "NE", + alpha_3: "NER", + un_code: 562 + }); + array.push({ + name: "Nigeria", + alpha_2: "NG", + alpha_3: "NGA", + un_code: 566 + }); + array.push({ + name: "Niue", + alpha_2: "NU", + alpha_3: "NIU", + un_code: 570 + }); + array.push({ + name: "Norfolk Island", + alpha_2: "NF", + alpha_3: "NFK", + un_code: 574 + }); + array.push({ + name: "Northern Mariana Islands", + alpha_2: "MP", + alpha_3: "MNP", + un_code: 580 + }); + array.push({ + name: "Norway", + alpha_2: "NO", + alpha_3: "NOR", + un_code: 578 + }); + array.push({ + name: "Oman", + alpha_2: "OM", + alpha_3: "OMN", + un_code: 512 + }); + array.push({ + name: "Pakistan", + alpha_2: "PK", + alpha_3: "PAK", + un_code: 586 + }); + array.push({ + name: "Palau", + alpha_2: "PW", + alpha_3: "PLW", + un_code: 585 + }); + array.push({ + name: "Palestinian Territory", + alpha_2: "PS", + alpha_3: "PSE", + un_code: 275 + }); + array.push({ + name: "Panama", + alpha_2: "PA", + alpha_3: "PAN", + un_code: 591 + }); + array.push({ + name: "Papua New Guinea", + alpha_2: "PG", + alpha_3: "PNG", + un_code: 598 + }); + array.push({ + name: "Paraguay", + alpha_2: "PY", + alpha_3: "PRY", + un_code: 600 + }); + array.push({ + name: "Peru", + alpha_2: "PE", + alpha_3: "PER", + un_code: 604 + }); + array.push({ + name: "Philippines", + alpha_2: "PH", + alpha_3: "PHL", + un_code: 608 + }); + array.push({ + name: "Pitcairn", + alpha_2: "PN", + alpha_3: "PCN", + un_code: 612 + }); + array.push({ + name: "Poland", + alpha_2: "PL", + alpha_3: "POL", + un_code: 616 + }); + array.push({ + name: "Portugal", + alpha_2: "PT", + alpha_3: "PRT", + un_code: 620 + }); + array.push({ + name: "Puerto Rico", + alpha_2: "PR", + alpha_3: "PRI", + un_code: 630 + }); + array.push({ + name: "Qatar", + alpha_2: "QA", + alpha_3: "QAT", + un_code: 634 + }); + array.push({ + name: "Réunion", + alpha_2: "RE", + alpha_3: "REU", + un_code: 638 + }); + array.push({ + name: "Romania", + alpha_2: "RO", + alpha_3: "ROU", + un_code: 642 + }); + array.push({ + name: "Russian Federation", + alpha_2: "RU", + alpha_3: "RUS", + un_code: 643 + }); + array.push({ + name: "Rwanda", + alpha_2: "RW", + alpha_3: "RWA", + un_code: 646 + }); + array.push({ + name: "Saint-Barthélemy", + alpha_2: "BL", + alpha_3: "BLM", + un_code: 652 + }); + array.push({ + name: "Saint Helena", + alpha_2: "SH", + alpha_3: "SHN", + un_code: 654 + }); + array.push({ + name: "Saint Kitts and Nevis", + alpha_2: "KN", + alpha_3: "KNA", + un_code: 659 + }); + array.push({ + name: "Saint Lucia", + alpha_2: "LC", + alpha_3: "LCA", + un_code: 662 + }); + array.push({ + name: "Saint-Martin (French part)", + alpha_2: "MF", + alpha_3: "MAF", + un_code: 663 + }); + array.push({ + name: "Saint Pierre and Miquelon", + alpha_2: "PM", + alpha_3: "SPM", + un_code: 666 + }); + array.push({ + name: "Saint Vincent and Grenadines", + alpha_2: "VC", + alpha_3: "VCT", + un_code: 670 + }); + array.push({ + name: "Samoa", + alpha_2: "WS", + alpha_3: "WSM", + un_code: 882 + }); + array.push({ + name: "San Marino", + alpha_2: "SM", + alpha_3: "SMR", + un_code: 674 + }); + array.push({ + name: "Sao Tome and Principe", + alpha_2: "ST", + alpha_3: "STP", + un_code: 678 + }); + array.push({ + name: "Saudi Arabia", + alpha_2: "SA", + alpha_3: "SAU", + un_code: 682 + }); + array.push({ + name: "Senegal", + alpha_2: "SN", + alpha_3: "SEN", + un_code: 686 + }); + array.push({ + name: "Serbia", + alpha_2: "RS", + alpha_3: "SRB", + un_code: 688 + }); + array.push({ + name: "Seychelles", + alpha_2: "SC", + alpha_3: "SYC", + un_code: 690 + }); + array.push({ + name: "Sierra Leone", + alpha_2: "SL", + alpha_3: "SLE", + un_code: 694 + }); + array.push({ + name: "Singapore", + alpha_2: "SG", + alpha_3: "SGP", + un_code: 702 + }); + array.push({ + name: "Slovakia", + alpha_2: "SK", + alpha_3: "SVK", + un_code: 703 + }); + array.push({ + name: "Slovenia", + alpha_2: "SI", + alpha_3: "SVN", + un_code: 705 + }); + array.push({ + name: "Solomon Islands", + alpha_2: "SB", + alpha_3: "SLB", + un_code: 90 + }); + array.push({ + name: "Somalia", + alpha_2: "SO", + alpha_3: "SOM", + un_code: 706 + }); + array.push({ + name: "South Africa", + alpha_2: "ZA", + alpha_3: "ZAF", + un_code: 710 + }); + array.push({ + name: "South Georgia and the South Sandwich Islands", + alpha_2: "GS", + alpha_3: "SGS", + un_code: 239 + }); + array.push({ + name: "South Sudan", + alpha_2: "SS", + alpha_3: "SSD", + un_code: 728 + }); + array.push({ + name: "Spain", + alpha_2: "ES", + alpha_3: "ESP", + un_code: 724 + }); + array.push({ + name: "Sri Lanka", + alpha_2: "LK", + alpha_3: "LKA", + un_code: 144 + }); + array.push({ + name: "Sudan", + alpha_2: "SD", + alpha_3: "SDN", + un_code: 736 + }); + array.push({ + name: "Suriname", + alpha_2: "SR", + alpha_3: "SUR", + un_code: 740 + }); + array.push({ + name: "Svalbard and Jan Mayen Islands", + alpha_2: "SJ", + alpha_3: "SJM", + un_code: 744 + }); + array.push({ + name: "Swaziland", + alpha_2: "SZ", + alpha_3: "SWZ", + un_code: 748 + }); + array.push({ + name: "Sweden", + alpha_2: "SE", + alpha_3: "SWE", + un_code: 752 + }); + array.push({ + name: "Switzerland", + alpha_2: "CH", + alpha_3: "CHE", + un_code: 756 + }); + array.push({ + name: "Syrian Arab Republic (Syria)", + alpha_2: "SY", + alpha_3: "SYR", + un_code: 760 + }); + array.push({ + name: "Taiwan, Republic of China", + alpha_2: "TW", + alpha_3: "TWN", + un_code: 158 + }); + array.push({ + name: "Tajikistan", + alpha_2: "TJ", + alpha_3: "TJK", + un_code: 762 + }); + array.push({ + name: "Tanzania, United Republic of", + alpha_2: "TZ", + alpha_3: "TZA", + un_code: 834 + }); + array.push({ + name: "Thailand", + alpha_2: "TH", + alpha_3: "THA", + un_code: 764 + }); + array.push({ + name: "Timor-Leste", + alpha_2: "TL", + alpha_3: "TLS", + un_code: 626 + }); + array.push({ + name: "Togo", + alpha_2: "TG", + alpha_3: "TGO", + un_code: 768 + }); + array.push({ + name: "Tokelau", + alpha_2: "TK", + alpha_3: "TKL", + un_code: 772 + }); + array.push({ + name: "Tonga", + alpha_2: "TO", + alpha_3: "TON", + un_code: 776 + }); + array.push({ + name: "Trinidad and Tobago", + alpha_2: "TT", + alpha_3: "TTO", + un_code: 780 + }); + array.push({ + name: "Tunisia", + alpha_2: "TN", + alpha_3: "TUN", + un_code: 788 + }); + array.push({ + name: "Turkey", + alpha_2: "TR", + alpha_3: "TUR", + un_code: 792 + }); + array.push({ + name: "Turkmenistan", + alpha_2: "TM", + alpha_3: "TKM", + un_code: 795 + }); + array.push({ + name: "Turks and Caicos Islands", + alpha_2: "TC", + alpha_3: "TCA", + un_code: 796 + }); + array.push({ + name: "Tuvalu", + alpha_2: "TV", + alpha_3: "TUV", + un_code: 798 + }); + array.push({ + name: "Uganda", + alpha_2: "UG", + alpha_3: "UGA", + un_code: 800 + }); + array.push({ + name: "Ukraine", + alpha_2: "UA", + alpha_3: "UKR", + un_code: 804 + }); + array.push({ + name: "United Arab Emirates", + alpha_2: "AE", + alpha_3: "ARE", + un_code: 784 + }); + array.push({ + name: "United Kingdom", + alpha_2: "GB", + alpha_3: "GBR", + un_code: 826 + }); + array.push({ + name: "United States of America", + alpha_2: "US", + alpha_3: "USA", + un_code: 840 + }); + array.push({ + name: "US Minor Outlying Islands", + alpha_2: "UM", + alpha_3: "UMI", + un_code: 581 + }); + array.push({ + name: "Uruguay", + alpha_2: "UY", + alpha_3: "URY", + un_code: 858 + }); + array.push({ + name: "Uzbekistan", + alpha_2: "UZ", + alpha_3: "UZB", + un_code: 860 + }); + array.push({ + name: "Vanuatu", + alpha_2: "VU", + alpha_3: "VUT", + un_code: 548 + }); + array.push({ + name: "Venezuela (Bolivarian Republic)", + alpha_2: "VE", + alpha_3: "VEN", + un_code: 862 + }); + array.push({ + name: "Viet Nam", + alpha_2: "VN", + alpha_3: "VNM", + un_code: 704 + }); + array.push({ + name: "Virgin Islands, US", + alpha_2: "VI", + alpha_3: "VIR", + un_code: 850 + }); + array.push({ + name: "Wallis and Futuna Islands", + alpha_2: "WF", + alpha_3: "WLF", + un_code: 876 + }); + array.push({ + name: "Western Sahara", + alpha_2: "EH", + alpha_3: "ESH", + un_code: 732 + }); + array.push({ + name: "Yemen", + alpha_2: "YE", + alpha_3: "YEM", + un_code: 887 + }); + array.push({ + name: "Zambia", + alpha_2: "ZM", + alpha_3: "ZMB", + un_code: 894 + }); + array.push({ + name: "Zimbabwe", + alpha_2: "ZW", + alpha_3: "ZWE", + un_code: 716 + }); +}; + +export function country_name(alpha_code: string, fallback?: string) { + return (alpha_2_map[alpha_code.toUpperCase()] || {name: fallback || tr("unknown country")}).name; +} + +fill_country_infos(country_infos); +for(const country of country_infos) + alpha_2_map[country.alpha_2] = country; diff --git a/shared/js/i18n/localize.ts b/shared/js/i18n/localize.ts index 1e469c39..11ca2368 100644 --- a/shared/js/i18n/localize.ts +++ b/shared/js/i18n/localize.ts @@ -1,324 +1,317 @@ -function guid() { - function s4() { - return Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); - } - return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import {guid} from "tc-shared/crypto/uid"; +import {StaticSettings} from "tc-shared/settings"; +import {createErrorModal} from "tc-shared/ui/elements/Modal"; +import * as loader from "tc-loader"; +import {formatMessage} from "tc-shared/ui/frames/chat"; + +export interface TranslationKey { + message: string; + line?: number; + character?: number; + filename?: string; } -namespace i18n { - export interface TranslationKey { - message: string; - line?: number; - character?: number; - filename?: string; +export interface Translation { + key: TranslationKey; + translated: string; + flags?: string[]; +} + +export interface Contributor { + name: string; + email: string; +} + +export interface TranslationFile { + path: string; + full_url: string; + + translations: Translation[]; +} + +export interface RepositoryTranslation { + key: string; + path: string; + + country_code: string; + name: string; + contributors: Contributor[]; +} + +export interface TranslationRepository { + unique_id: string; + url: string; + name?: string; + contact?: string; + translations?: RepositoryTranslation[]; + load_timestamp?: number; +} + +let translations: Translation[] = []; +let fast_translate: { [key:string]:string; } = {}; +export function tr(message: string, key?: string) { + const sloppy = fast_translate[message]; + if(sloppy) return sloppy; + + log.info(LogCategory.I18N, "Translating \"%s\". Default: \"%s\"", key, message); + + let translated = message; + for(const translation of translations) { + if(translation.key.message == message) { + translated = translation.translated; + break; + } } - export interface Translation { - key: TranslationKey; - translated: string; - flags?: string[]; - } + fast_translate[message] = translated; + return translated; +} - export interface Contributor { - name: string; - email: string; - } +export function tra(message: string, ...args: any[]) { + message = tr(message); + return formatMessage(message, ...args); +} - export interface TranslationFile { - path: string; - full_url: string; +async function load_translation_file(url: string, path: string) : Promise { + return new Promise((resolve, reject) => { + $.ajax({ + url: url, + async: true, + success: result => { + try { + const file = (typeof(result) === "string" ? JSON.parse(result) : result) as TranslationFile; + if(!file) { + reject("Invalid json"); + return; + } - translations: Translation[]; - } + file.full_url = url; + file.path = path; - export interface RepositoryTranslation { - key: string; - path: string; - - country_code: string; - name: string; - contributors: Contributor[]; - } - - export interface TranslationRepository { - unique_id: string; - url: string; - name?: string; - contact?: string; - translations?: RepositoryTranslation[]; - load_timestamp?: number; - } - - let translations: Translation[] = []; - let fast_translate: { [key:string]:string; } = {}; - export function tr(message: string, key?: string) { - const sloppy = fast_translate[message]; - if(sloppy) return sloppy; - - log.info(LogCategory.I18N, "Translating \"%s\". Default: \"%s\"", key, message); - - let translated = message; - for(const translation of translations) { - if(translation.key.message == message) { - translated = translation.translated; - break; + //TODO: Validate file + resolve(file); + } catch(error) { + log.warn(LogCategory.I18N, tr("Failed to load translation file %s. Failed to parse or process json: %o"), url, error); + reject(tr("Failed to process or parse json!")); + } + }, + error: (xhr, error) => { + reject(tr("Failed to load file: ") + error); } + }) + }); +} + +export function load_file(url: string, path: string) : Promise { + return load_translation_file(url, path).then(async result => { + /* TODO: Improve this test?!*/ + try { + tr("Dummy translation test"); + } catch(error) { + throw "dummy test failed"; } - fast_translate[message] = translated; - return translated; - } + log.info(LogCategory.I18N, tr("Successfully initialized up translation file from %s"), url); + translations = result.translations; + }).catch(error => { + log.warn(LogCategory.I18N, tr("Failed to load translation file from \"%s\". Error: %o"), url, error); + return Promise.reject(error); + }); +} - export function tra(message: string, ...args: any[]) { - message = tr(message); - return MessageHelper.formatMessage(message, ...args); - } - - async function load_translation_file(url: string, path: string) : Promise { - return new Promise((resolve, reject) => { +async function load_repository0(repo: TranslationRepository, reload: boolean) { + if(!repo.load_timestamp || repo.load_timestamp < 1000 || reload) { + const info_json = await new Promise((resolve, reject) => { $.ajax({ - url: url, + url: repo.url + "/info.json", async: true, + cache: !reload, success: result => { - try { - const file = (typeof(result) === "string" ? JSON.parse(result) : result) as TranslationFile; - if(!file) { - reject("Invalid json"); - return; - } - - file.full_url = url; - file.path = path; - - //TODO: Validate file - resolve(file); - } catch(error) { - log.warn(LogCategory.I18N, tr("Failed to load translation file %s. Failed to parse or process json: %o"), url, error); - reject(tr("Failed to process or parse json!")); + const file = (typeof(result) === "string" ? JSON.parse(result) : result) as TranslationFile; + if(!file) { + reject("Invalid json"); + return; } + + resolve(file); }, error: (xhr, error) => { reject(tr("Failed to load file: ") + error); } }) }); + + Object.assign(repo, info_json); } - export function load_file(url: string, path: string) : Promise { - return load_translation_file(url, path).then(async result => { - /* TODO: Improve this test?!*/ - try { - tr("Dummy translation test"); - } catch(error) { - throw "dummy test failed"; - } + if(!repo.unique_id) + repo.unique_id = guid(); - log.info(LogCategory.I18N, tr("Successfully initialized up translation file from %s"), url); - translations = result.translations; - }).catch(error => { - log.warn(LogCategory.I18N, tr("Failed to load translation file from \"%s\". Error: %o"), url, error); - return Promise.reject(error); - }); + repo.translations = repo.translations || []; + repo.load_timestamp = Date.now(); +} + +export async function load_repository(url: string) : Promise { + const result = {} as TranslationRepository; + result.url = url; + await load_repository0(result, false); + return result; +} + +export namespace config { + export interface TranslationConfig { + current_repository_url?: string; + current_language?: string; + + current_translation_url?: string; + current_translation_path?: string; } - async function load_repository0(repo: TranslationRepository, reload: boolean) { - if(!repo.load_timestamp || repo.load_timestamp < 1000 || reload) { - const info_json = await new Promise((resolve, reject) => { - $.ajax({ - url: repo.url + "/info.json", - async: true, - cache: !reload, - success: result => { - const file = (typeof(result) === "string" ? JSON.parse(result) : result) as TranslationFile; - if(!file) { - reject("Invalid json"); - return; - } + export interface RepositoryConfig { + repositories?: { + url?: string; + repository?: TranslationRepository; + }[]; + } - resolve(file); - }, - error: (xhr, error) => { - reject(tr("Failed to load file: ") + error); - } - }) + const repository_config_key = "i18n.repository"; + let _cached_repository_config: RepositoryConfig; + export function repository_config() { + if(_cached_repository_config) + return _cached_repository_config; + + const config_string = localStorage.getItem(repository_config_key); + let config: RepositoryConfig; + try { + config = config_string ? JSON.parse(config_string) : {}; + } catch(error) { + log.error(LogCategory.I18N, tr("Failed to parse repository config: %o"), error); + } + config.repositories = config.repositories || []; + for(const repo of config.repositories) + (repo.repository || {load_timestamp: 0}).load_timestamp = 0; + + if(config.repositories.length == 0) { + //Add the default TeaSpeak repository + load_repository(StaticSettings.instance.static("i18n.default_repository", "https://web.teaspeak.de/i18n/")).then(repo => { + log.info(LogCategory.I18N, tr("Successfully added default repository from \"%s\"."), repo.url); + register_repository(repo); + }).catch(error => { + log.warn(LogCategory.I18N, tr("Failed to add default repository. Error: %o"), error); }); - - Object.assign(repo, info_json); } - if(!repo.unique_id) - repo.unique_id = guid(); - - repo.translations = repo.translations || []; - repo.load_timestamp = Date.now(); + return _cached_repository_config = config; } - export async function load_repository(url: string) : Promise { - const result = {} as TranslationRepository; - result.url = url; - await load_repository0(result, false); - return result; + export function save_repository_config() { + localStorage.setItem(repository_config_key, JSON.stringify(_cached_repository_config)); } - export namespace config { - export interface TranslationConfig { - current_repository_url?: string; - current_language?: string; + const translation_config_key = "i18n.translation"; + let _cached_translation_config: TranslationConfig; - current_translation_url?: string; - current_translation_path?: string; - } - - export interface RepositoryConfig { - repositories?: { - url?: string; - repository?: TranslationRepository; - }[]; - } - - const repository_config_key = "i18n.repository"; - let _cached_repository_config: RepositoryConfig; - export function repository_config() { - if(_cached_repository_config) - return _cached_repository_config; - - const config_string = localStorage.getItem(repository_config_key); - let config: RepositoryConfig; - try { - config = config_string ? JSON.parse(config_string) : {}; - } catch(error) { - log.error(LogCategory.I18N, tr("Failed to parse repository config: %o"), error); - } - config.repositories = config.repositories || []; - for(const repo of config.repositories) - (repo.repository || {load_timestamp: 0}).load_timestamp = 0; - - if(config.repositories.length == 0) { - //Add the default TeaSpeak repository - load_repository(StaticSettings.instance.static("i18n.default_repository", "https://web.teaspeak.de/i18n/")).then(repo => { - log.info(LogCategory.I18N, tr("Successfully added default repository from \"%s\"."), repo.url); - register_repository(repo); - }).catch(error => { - log.warn(LogCategory.I18N, tr("Failed to add default repository. Error: %o"), error); - }); - } - - return _cached_repository_config = config; - } - - export function save_repository_config() { - localStorage.setItem(repository_config_key, JSON.stringify(_cached_repository_config)); - } - - const translation_config_key = "i18n.translation"; - let _cached_translation_config: TranslationConfig; - - export function translation_config() : TranslationConfig { - if(_cached_translation_config) - return _cached_translation_config; - - const config_string = localStorage.getItem(translation_config_key); - try { - _cached_translation_config = config_string ? JSON.parse(config_string) : {}; - } catch(error) { - log.error(LogCategory.I18N, tr("Failed to initialize translation config. Using default one. Error: %o"), error); - _cached_translation_config = {} as any; - } + export function translation_config() : TranslationConfig { + if(_cached_translation_config) return _cached_translation_config; + + const config_string = localStorage.getItem(translation_config_key); + try { + _cached_translation_config = config_string ? JSON.parse(config_string) : {}; + } catch(error) { + log.error(LogCategory.I18N, tr("Failed to initialize translation config. Using default one. Error: %o"), error); + _cached_translation_config = {} as any; } - - export function save_translation_config() { - localStorage.setItem(translation_config_key, JSON.stringify(_cached_translation_config)); - } + return _cached_translation_config; } - export function register_repository(repository: TranslationRepository) { - if(!repository) return; - - for(const repo of config.repository_config().repositories) - if(repo.url == repository.url) return; - - config.repository_config().repositories.push(repository); - config.save_repository_config(); - } - - export function registered_repositories() : TranslationRepository[] { - return config.repository_config().repositories.map(e => e.repository || {url: e.url, load_timestamp: 0} as TranslationRepository); - } - - export function delete_repository(repository: TranslationRepository) { - if(!repository) return; - - for(const repo of [...config.repository_config().repositories]) - if(repo.url == repository.url) { - config.repository_config().repositories.remove(repo); - } - config.save_repository_config(); - } - - export async function iterate_repositories(callback_entry: (repository: TranslationRepository) => any) { - const promises = []; - - for(const repository of registered_repositories()) { - promises.push(load_repository0(repository, false).then(() => callback_entry(repository)).catch(error => { - log.warn(LogCategory.I18N, "Failed to fetch repository %s. error: %o", repository.url, error); - })); - } - - await Promise.all(promises); - } - - export function select_translation(repository: TranslationRepository, entry: RepositoryTranslation) { - const cfg = config.translation_config(); - - if(entry && repository) { - cfg.current_language = entry.name; - cfg.current_repository_url = repository.url; - cfg.current_translation_url = repository.url + entry.path; - cfg.current_translation_path = entry.path; - } else { - cfg.current_language = undefined; - cfg.current_repository_url = undefined; - cfg.current_translation_url = undefined; - cfg.current_translation_path = undefined; - } - - config.save_translation_config(); - } - - /* ATTENTION: This method is called before most other library inizialisations! */ - export async function initialize() { - const rcfg = config.repository_config(); /* initialize */ - const cfg = config.translation_config(); - - if(cfg.current_translation_url) { - try { - await load_file(cfg.current_translation_url, cfg.current_translation_path); - } catch (error) { - console.error(tr("Failed to initialize selected translation: %o"), error); - const show_error = () => { - createErrorModal(tr("Translation System"), tra("Failed to load current selected translation file.{:br:}File: {0}{:br:}Error: {1}{:br:}{:br:}Using default fallback translations.", cfg.current_translation_url, error)).open() - }; - if(loader.running()) - loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { - priority: 10, - function: async () => show_error(), - name: "I18N error display" - }); - else - show_error(); - } - } - // await load_file("http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/web/environment/development/i18n/de_DE.translation"); - // await load_file("http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/web/environment/development/i18n/test.json"); + export function save_translation_config() { + localStorage.setItem(translation_config_key, JSON.stringify(_cached_translation_config)); } } -// @ts-ignore -const tr: typeof i18n.tr = i18n.tr; -const tra: typeof i18n.tra = i18n.tra; +export function register_repository(repository: TranslationRepository) { + if(!repository) return; -(window as any).tr = i18n.tr; -(window as any).tra = i18n.tra; \ No newline at end of file + for(const repo of config.repository_config().repositories) + if(repo.url == repository.url) return; + + config.repository_config().repositories.push(repository); + config.save_repository_config(); +} + +export function registered_repositories() : TranslationRepository[] { + return config.repository_config().repositories.map(e => e.repository || {url: e.url, load_timestamp: 0} as TranslationRepository); +} + +export function delete_repository(repository: TranslationRepository) { + if(!repository) return; + + for(const repo of [...config.repository_config().repositories]) + if(repo.url == repository.url) { + config.repository_config().repositories.remove(repo); + } + config.save_repository_config(); +} + +export async function iterate_repositories(callback_entry: (repository: TranslationRepository) => any) { + const promises = []; + + for(const repository of registered_repositories()) { + promises.push(load_repository0(repository, false).then(() => callback_entry(repository)).catch(error => { + log.warn(LogCategory.I18N, "Failed to fetch repository %s. error: %o", repository.url, error); + })); + } + + await Promise.all(promises); +} + +export function select_translation(repository: TranslationRepository, entry: RepositoryTranslation) { + const cfg = config.translation_config(); + + if(entry && repository) { + cfg.current_language = entry.name; + cfg.current_repository_url = repository.url; + cfg.current_translation_url = repository.url + entry.path; + cfg.current_translation_path = entry.path; + } else { + cfg.current_language = undefined; + cfg.current_repository_url = undefined; + cfg.current_translation_url = undefined; + cfg.current_translation_path = undefined; + } + + config.save_translation_config(); +} + +/* ATTENTION: This method is called before most other library initialisations! */ +export async function initialize() { + const rcfg = config.repository_config(); /* initialize */ + const cfg = config.translation_config(); + + if(cfg.current_translation_url) { + try { + await load_file(cfg.current_translation_url, cfg.current_translation_path); + } catch (error) { + console.error(tr("Failed to initialize selected translation: %o"), error); + const show_error = () => { + createErrorModal(tr("Translation System"), tra("Failed to load current selected translation file.{:br:}File: {0}{:br:}Error: {1}{:br:}{:br:}Using default fallback translations.", cfg.current_translation_url, error)).open() + }; + if(loader.running()) + loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { + priority: 10, + function: async () => show_error(), + name: "I18N error display" + }); + else + show_error(); + } + } + // await load_file("http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/web/environment/development/i18n/de_DE.translation"); + // await load_file("http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/web/environment/development/i18n/test.json"); +} + +window.tr = tr; +window.tra = tra; \ No newline at end of file diff --git a/shared/js/log.ts b/shared/js/log.ts index 4ea9d119..21c4768a 100644 --- a/shared/js/log.ts +++ b/shared/js/log.ts @@ -1,6 +1,8 @@ //Used by CertAccept popup +import {settings} from "tc-shared/settings"; +import * as loader from "tc-loader"; -enum LogCategory { +export enum LogCategory { CHANNEL, CHANNEL_PROPERTIES, /* separating channel and channel properties because on channel init logging is a big bottleneck */ CLIENT, @@ -19,233 +21,238 @@ enum LogCategory { DNS } -namespace log { - export enum LogType { - TRACE, - DEBUG, - INFO, - WARNING, - ERROR +export enum LogType { + TRACE, + DEBUG, + INFO, + WARNING, + ERROR +} + +let category_mapping = new Map([ + [LogCategory.CHANNEL, "Channel "], + [LogCategory.CHANNEL_PROPERTIES, "Channel "], + [LogCategory.CLIENT, "Client "], + [LogCategory.SERVER, "Server "], + [LogCategory.BOOKMARKS, "Bookmark "], + [LogCategory.PERMISSIONS, "Permission "], + [LogCategory.GENERAL, "General "], + [LogCategory.NETWORKING, "Network "], + [LogCategory.VOICE, "Voice "], + [LogCategory.AUDIO, "Audio "], + [LogCategory.CHANNEL, "Chat "], + [LogCategory.I18N, "I18N "], + [LogCategory.IDENTITIES, "Identities "], + [LogCategory.IPC, "IPC "], + [LogCategory.STATISTICS, "Statistics "], + [LogCategory.DNS, "DNS "] +]); + +export let enabled_mapping = new Map([ + [LogCategory.CHANNEL, true], + [LogCategory.CHANNEL_PROPERTIES, false], + [LogCategory.CLIENT, true], + [LogCategory.SERVER, true], + [LogCategory.BOOKMARKS, true], + [LogCategory.PERMISSIONS, true], + [LogCategory.GENERAL, true], + [LogCategory.NETWORKING, true], + [LogCategory.VOICE, true], + [LogCategory.AUDIO, true], + [LogCategory.CHAT, true], + [LogCategory.I18N, false], + [LogCategory.IDENTITIES, true], + [LogCategory.IPC, true], + [LogCategory.STATISTICS, true], + [LogCategory.DNS, true] +]); + +//Values will be overridden by initialize() +export let level_mapping = new Map([ + [LogType.TRACE, true], + [LogType.DEBUG, true], + [LogType.INFO, true], + [LogType.WARNING, true], + [LogType.ERROR, true] +]); + +enum GroupMode { + NATIVE, + PREFIX +} +const group_mode: GroupMode = GroupMode.PREFIX; + +//Category Example: ?log.i18n.enabled=0 +//Level Example A: ?log.level.trace.enabled=0 +//Level Example B: ?log.level=0 +export function initialize(default_level: LogType) { + for(const category of Object.keys(LogCategory).map(e => parseInt(e))) { + if(isNaN(category)) continue; + const category_name = LogCategory[category].toLowerCase(); + enabled_mapping.set(category, settings.static_global("log." + category_name.toLowerCase() + ".enabled", enabled_mapping.get(category))); } - let category_mapping = new Map([ - [LogCategory.CHANNEL, "Channel "], - [LogCategory.CHANNEL_PROPERTIES, "Channel "], - [LogCategory.CLIENT, "Client "], - [LogCategory.SERVER, "Server "], - [LogCategory.BOOKMARKS, "Bookmark "], - [LogCategory.PERMISSIONS, "Permission "], - [LogCategory.GENERAL, "General "], - [LogCategory.NETWORKING, "Network "], - [LogCategory.VOICE, "Voice "], - [LogCategory.AUDIO, "Audio "], - [LogCategory.CHANNEL, "Chat "], - [LogCategory.I18N, "I18N "], - [LogCategory.IDENTITIES, "Identities "], - [LogCategory.IPC, "IPC "], - [LogCategory.STATISTICS, "Statistics "], - [LogCategory.DNS, "DNS "] - ]); + const base_level = settings.static_global("log.level", default_level); - export let enabled_mapping = new Map([ - [LogCategory.CHANNEL, true], - [LogCategory.CHANNEL_PROPERTIES, false], - [LogCategory.CLIENT, true], - [LogCategory.SERVER, true], - [LogCategory.BOOKMARKS, true], - [LogCategory.PERMISSIONS, true], - [LogCategory.GENERAL, true], - [LogCategory.NETWORKING, true], - [LogCategory.VOICE, true], - [LogCategory.AUDIO, true], - [LogCategory.CHAT, true], - [LogCategory.I18N, false], - [LogCategory.IDENTITIES, true], - [LogCategory.IPC, true], - [LogCategory.STATISTICS, true], - [LogCategory.DNS, true] - ]); + for(const level of Object.keys(LogType).map(e => parseInt(e))) { + if(isNaN(level)) continue; - //Values will be overridden by initialize() - export let level_mapping = new Map([ - [LogType.TRACE, true], - [LogType.DEBUG, true], - [LogType.INFO, true], - [LogType.WARNING, true], - [LogType.ERROR, true] - ]); - - enum GroupMode { - NATIVE, - PREFIX - } - const group_mode: GroupMode = GroupMode.PREFIX; - - //Category Example: ?log.i18n.enabled=0 - //Level Example A: ?log.level.trace.enabled=0 - //Level Example B: ?log.level=0 - export function initialize(default_level: LogType) { - for(const category of Object.keys(LogCategory).map(e => parseInt(e))) { - if(isNaN(category)) continue; - const category_name = LogCategory[category].toLowerCase(); - enabled_mapping.set(category, settings.static_global("log." + category_name.toLowerCase() + ".enabled", enabled_mapping.get(category))); - } - - const base_level = settings.static_global("log.level", default_level); - - for(const level of Object.keys(LogType).map(e => parseInt(e))) { - if(isNaN(level)) continue; - - const level_name = LogType[level].toLowerCase(); - level_mapping.set(level, settings.static_global("log." + level_name + ".enabled", level >= base_level)); - } - } - - function logDirect(type: LogType, message: string, ...optionalParams: any[]) { - if(!level_mapping.get(type)) - return; - - switch (type) { - case LogType.TRACE: - case LogType.DEBUG: - console.debug(message, ...optionalParams); - break; - case LogType.INFO: - console.log(message, ...optionalParams); - break; - case LogType.WARNING: - console.warn(message, ...optionalParams); - break; - case LogType.ERROR: - console.error(message, ...optionalParams); - break; - } - } - - export function log(type: LogType, category: LogCategory, message: string, ...optionalParams: any[]) { - if(!enabled_mapping.get(category)) return; - - optionalParams.unshift(category_mapping.get(category)); - message = "[%s] " + message; - logDirect(type, message, ...optionalParams); - } - - export function trace(category: LogCategory, message: string, ...optionalParams: any[]) { - log(LogType.TRACE, category, message, ...optionalParams); - } - - export function debug(category: LogCategory, message: string, ...optionalParams: any[]) { - log(LogType.DEBUG, category, message, ...optionalParams); - } - - export function info(category: LogCategory, message: string, ...optionalParams: any[]) { - log(LogType.INFO, category, message, ...optionalParams); - } - - export function warn(category: LogCategory, message: string, ...optionalParams: any[]) { - log(LogType.WARNING, category, message, ...optionalParams); - } - - export function error(category: LogCategory, message: string, ...optionalParams: any[]) { - log(LogType.ERROR, category, message, ...optionalParams); - } - - export function group(level: LogType, category: LogCategory, name: string, ...optionalParams: any[]) : Group { - name = "[%s] " + name; - optionalParams.unshift(category_mapping.get(category)); - - return new Group(group_mode, level, category, name, optionalParams); - } - - export function table(level: LogType, category: LogCategory, title: string, arguments: any) { - if(group_mode == GroupMode.NATIVE) { - console.groupCollapsed(title); - console.table(arguments); - console.groupEnd(); - } else { - if(!enabled_mapping.get(category) || !level_mapping.get(level)) - return; - logDirect(level, tr("Snipped table \"%s\""), title); - } - } - - export class Group { - readonly mode: GroupMode; - readonly level: LogType; - readonly category: LogCategory; - readonly enabled: boolean; - - owner: Group = undefined; - - private readonly name: string; - private readonly optionalParams: any[][]; - private _collapsed: boolean = false; - private initialized = false; - private _log_prefix: string; - - constructor(mode: GroupMode, level: LogType, category: LogCategory, name: string, optionalParams: any[][], owner: Group = undefined) { - this.level = level; - this.mode = mode; - this.category = category; - this.name = name; - this.optionalParams = optionalParams; - this.enabled = enabled_mapping.get(category); - } - - group(level: LogType, name: string, ...optionalParams: any[]) : Group { - return new Group(this.mode, level, this.category, name, optionalParams, this); - } - - collapsed(flag: boolean = true) : this { - this._collapsed = flag; - return this; - } - - log(message: string, ...optionalParams: any[]) : this { - if(!this.enabled) - return this; - - if(!this.initialized) { - if(this.mode == GroupMode.NATIVE) { - if(this._collapsed && console.groupCollapsed) - console.groupCollapsed(this.name, ...this.optionalParams); - else - console.group(this.name, ...this.optionalParams); - } else { - this._log_prefix = " "; - let parent = this.owner; - while(parent) { - if(parent.mode == GroupMode.PREFIX) - this._log_prefix = this._log_prefix + parent._log_prefix; - else - break; - } - } - this.initialized = true; - } - if(this.mode == GroupMode.NATIVE) - logDirect(this.level, message, ...optionalParams); - else { - logDirect(this.level, "[%s] " + this._log_prefix + message, category_mapping.get(this.category), ...optionalParams); - } - return this; - } - - end() { - if(this.initialized) { - if(this.mode == GroupMode.NATIVE) - console.groupEnd(); - } - } - - get prefix() : string { - return this._log_prefix; - } - - set prefix(prefix: string) { - this._log_prefix = prefix; - } + const level_name = LogType[level].toLowerCase(); + level_mapping.set(level, settings.static_global("log." + level_name + ".enabled", level >= base_level)); } } -import LogType = log.LogType; \ No newline at end of file +function logDirect(type: LogType, message: string, ...optionalParams: any[]) { + if(!level_mapping.get(type)) + return; + + switch (type) { + case LogType.TRACE: + case LogType.DEBUG: + console.debug(message, ...optionalParams); + break; + case LogType.INFO: + console.log(message, ...optionalParams); + break; + case LogType.WARNING: + console.warn(message, ...optionalParams); + break; + case LogType.ERROR: + console.error(message, ...optionalParams); + break; + } +} + +export function log(type: LogType, category: LogCategory, message: string, ...optionalParams: any[]) { + if(!enabled_mapping.get(category)) return; + + optionalParams.unshift(category_mapping.get(category)); + message = "[%s] " + message; + logDirect(type, message, ...optionalParams); +} + +export function trace(category: LogCategory, message: string, ...optionalParams: any[]) { + log(LogType.TRACE, category, message, ...optionalParams); +} + +export function debug(category: LogCategory, message: string, ...optionalParams: any[]) { + log(LogType.DEBUG, category, message, ...optionalParams); +} + +export function info(category: LogCategory, message: string, ...optionalParams: any[]) { + log(LogType.INFO, category, message, ...optionalParams); +} + +export function warn(category: LogCategory, message: string, ...optionalParams: any[]) { + log(LogType.WARNING, category, message, ...optionalParams); +} + +export function error(category: LogCategory, message: string, ...optionalParams: any[]) { + log(LogType.ERROR, category, message, ...optionalParams); +} + +export function group(level: LogType, category: LogCategory, name: string, ...optionalParams: any[]) : Group { + name = "[%s] " + name; + optionalParams.unshift(category_mapping.get(category)); + + return new Group(group_mode, level, category, name, optionalParams); +} + +export function table(level: LogType, category: LogCategory, title: string, args: any) { + if(group_mode == GroupMode.NATIVE) { + console.groupCollapsed(title); + console.table(args); + console.groupEnd(); + } else { + if(!enabled_mapping.get(category) || !level_mapping.get(level)) + return; + logDirect(level, tr("Snipped table \"%s\""), title); + } +} + +export class Group { + readonly mode: GroupMode; + readonly level: LogType; + readonly category: LogCategory; + readonly enabled: boolean; + + owner: Group = undefined; + + private readonly name: string; + private readonly optionalParams: any[][]; + private _collapsed: boolean = false; + private initialized = false; + private _log_prefix: string; + + constructor(mode: GroupMode, level: LogType, category: LogCategory, name: string, optionalParams: any[][], owner: Group = undefined) { + this.level = level; + this.mode = mode; + this.category = category; + this.name = name; + this.optionalParams = optionalParams; + this.enabled = enabled_mapping.get(category); + } + + group(level: LogType, name: string, ...optionalParams: any[]) : Group { + return new Group(this.mode, level, this.category, name, optionalParams, this); + } + + collapsed(flag: boolean = true) : this { + this._collapsed = flag; + return this; + } + + log(message: string, ...optionalParams: any[]) : this { + if(!this.enabled) + return this; + + if(!this.initialized) { + if(this.mode == GroupMode.NATIVE) { + if(this._collapsed && console.groupCollapsed) + console.groupCollapsed(this.name, ...this.optionalParams); + else + console.group(this.name, ...this.optionalParams); + } else { + this._log_prefix = " "; + let parent = this.owner; + while(parent) { + if(parent.mode == GroupMode.PREFIX) + this._log_prefix = this._log_prefix + parent._log_prefix; + else + break; + } + } + this.initialized = true; + } + if(this.mode == GroupMode.NATIVE) + logDirect(this.level, message, ...optionalParams); + else { + logDirect(this.level, "[%s] " + this._log_prefix + message, category_mapping.get(this.category), ...optionalParams); + } + return this; + } + + end() { + if(this.initialized) { + if(this.mode == GroupMode.NATIVE) + console.groupEnd(); + } + } + + get prefix() : string { + return this._log_prefix; + } + + set prefix(prefix: string) { + this._log_prefix = prefix; + } +} + +loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { + name: "log enabled initialisation", + function: async () => initialize(loader.version().debug_mode ? LogType.TRACE : LogType.INFO), + priority: 150 +}); + +/* initialize global logging system, use by the loader for example */ +window.log = module.exports; \ No newline at end of file diff --git a/shared/js/main.ts b/shared/js/main.ts index f2e910bc..2c0b1121 100644 --- a/shared/js/main.ts +++ b/shared/js/main.ts @@ -1,31 +1,41 @@ -/// -/// -/// -/// -/// -/// -/// -/// -/// +import * as loader from "tc-loader"; +import {settings, Settings} from "tc-shared/settings"; +import * as profiles from "tc-shared/profiles/ConnectionProfile"; +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; +import * as bipc from "./BrowserIPC"; +import * as sound from "./sound/Sounds"; +import * as i18n from "./i18n/localize"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {createInfoModal} from "tc-shared/ui/elements/Modal"; +import {tra} from "./i18n/localize"; +import {RequestFileUpload} from "tc-shared/FileManager"; +import * as stats from "./stats"; +import * as fidentity from "./profiles/identities/TeaForumIdentity"; +import {default_recorder, RecorderProfile, set_default_recorder} from "tc-shared/voice/RecorderProfile"; +import * as cmanager from "tc-shared/ui/frames/connection_handlers"; +import {server_connections, ServerConnectionManager} from "tc-shared/ui/frames/connection_handlers"; +import * as control_bar from "tc-shared/ui/frames/ControlBar"; +import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect"; +import * as top_menu from "./ui/frames/MenuBar"; +import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; +import {formatMessage} from "tc-shared/ui/frames/chat"; +import {openModalNewcomer} from "tc-shared/ui/modal/ModalNewcomer"; +import * as aplayer from "tc-backend/audio/player"; +import * as arecorder from "tc-backend/audio/recorder"; +import * as ppt from "tc-backend/ppt"; -import spawnYesNo = Modals.spawnYesNo; +/* required import for init */ +require("./proto").initialize(); +require("./ui/elements/ContextDivider").initialize(); const js_render = window.jsrender || $; const native_client = window.require !== undefined; -function getUserMediaFunctionPromise() : (constraints: MediaStreamConstraints) => Promise { - if('mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices) - return constraints => navigator.mediaDevices.getUserMedia(constraints); - - const _callbacked_function = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; - if(!_callbacked_function) - return undefined; - - return constraints => new Promise((resolve, reject) => _callbacked_function(constraints, resolve, reject)); -} - -interface Window { - open_connected_question: () => Promise; +declare global { + interface Window { + open_connected_question: () => Promise; + } } function setup_close() { @@ -50,7 +60,7 @@ function setup_close() { })); const exit = () => { - const {remote} = require('electron'); + const {remote} = window.require('electron'); remote.getCurrentWindow().close(); }; @@ -133,7 +143,7 @@ async function initialize_app() { try { //Initialize main template const main = $("#tmpl_main").renderTag({ multi_session: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION), - app_version: app.ui_version() + app_version: loader.version().ui }).dividerfy(); $("body").append(main); @@ -143,21 +153,21 @@ async function initialize_app() { return; } - control_bar = new ControlBar($("#control_bar")); /* setup the control bar */ + control_bar.set_control_bar(new control_bar.ControlBar($("#control_bar"))); /* setup the control bar */ - if(!audio.player.initialize()) + if(!aplayer.initialize()) console.warn(tr("Failed to initialize audio controller!")); - audio.player.on_ready(() => { - if(audio.player.set_master_volume) - audio.player.on_ready(() => audio.player.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER) / 100)); + aplayer.on_ready(() => { + if(aplayer.set_master_volume) + aplayer.on_ready(() => aplayer.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER) / 100)); else - log.warn(LogCategory.GENERAL, tr("Client does not support audio.player.set_master_volume()... May client is too old?")); - if(audio.recorder.device_refresh_available()) - audio.recorder.refresh_devices(); + log.warn(LogCategory.GENERAL, tr("Client does not support aplayer.set_master_volume()... May client is too old?")); + if(arecorder.device_refresh_available()) + arecorder.refresh_devices(); }); - default_recorder = new RecorderProfile("default"); + set_default_recorder(new RecorderProfile("default")); default_recorder.initialize().catch(error => { log.error(LogCategory.AUDIO, tr("Failed to initialize default recorder: %o"), error); }); @@ -180,78 +190,6 @@ async function initialize_app() { setup_close(); } -function str2ab8(str) { - const buf = new ArrayBuffer(str.length); - const bufView = new Uint8Array(buf); - for (let i = 0, strLen = str.length; i < strLen; i++) { - bufView[i] = str.charCodeAt(i); - } - return buf; -} - -/* FIXME Dont use atob, because it sucks for non UTF-8 tings */ -function arrayBufferBase64(base64: string) { - base64 = atob(base64); - const buf = new ArrayBuffer(base64.length); - const bufView = new Uint8Array(buf); - for (let i = 0, strLen = base64.length; i < strLen; i++) { - bufView[i] = base64.charCodeAt(i); - } - return buf; -} - -function base64_encode_ab(source: ArrayBufferLike) { - const encodings = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - let base64 = ""; - - const bytes = new Uint8Array(source); - const byte_length = bytes.byteLength; - const byte_reminder = byte_length % 3; - const main_length = byte_length - byte_reminder; - - let a, b, c, d; - let chunk; - - // Main loop deals with bytes in chunks of 3 - for (let i = 0; i < main_length; i = i + 3) { - // Combine the three bytes into a single integer - chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]; - - // Use bitmasks to extract 6-bit segments from the triplet - a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18 - b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12 - c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6 - d = (chunk & 63) >> 0; // 63 = (2^6 - 1) << 0 - - // Convert the raw binary segments to the appropriate ASCII encoding - base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]; - } - - // Deal with the remaining bytes and padding - if (byte_reminder == 1) { - chunk = bytes[main_length]; - - a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2 - - // Set the 4 least significant bits to zero - b = (chunk & 3) << 4; // 3 = 2^2 - 1 - - base64 += encodings[a] + encodings[b] + '=='; - } else if (byte_reminder == 2) { - chunk = (bytes[main_length] << 8) | bytes[main_length + 1]; - - a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10 - b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4 - - // Set the 2 least significant bits to zero - c = (chunk & 15) << 2; // 15 = 2^4 - 1 - - base64 += encodings[a] + encodings[b] + encodings[c] + '='; - } - - return base64 -} - /* class TestProxy extends bipc.MethodProxy { constructor(params: bipc.MethodProxyConnectParameters) { @@ -313,7 +251,7 @@ function handle_connect_request(properties: bipc.connect.ConnectRequestData, con }); server_connections.set_active_connection_handler(connection); } else { - Modals.spawnConnectModal({},{ + spawnConnectModal({},{ url: properties.address, enforce: true }, { @@ -356,14 +294,14 @@ function main() { top_menu.initialize(); - server_connections = new ServerConnectionManager($("#connection-handlers")); - control_bar.initialise(); /* before connection handler to allow property apply */ + cmanager.initialize(new ServerConnectionManager($("#connection-handlers"))); + control_bar.control_bar.initialise(); /* before connection handler to allow property apply */ const initial_handler = server_connections.spawn_server_connection_handler(); initial_handler.acquire_recorder(default_recorder, false); - control_bar.set_connection_handler(initial_handler); + control_bar.control_bar.set_connection_handler(initial_handler); /** Setup the XF forum identity **/ - profiles.identities.update_forum(); + fidentity.update_forum(); let _resize_timeout: NodeJS.Timer; $(window).on('resize', event => { @@ -481,7 +419,7 @@ function main() { /* for testing */ if(settings.static_global(Settings.KEY_USER_IS_NEW)) { - const modal = Modals.openModalNewcomer(); + const modal = openModalNewcomer(); modal.close_listener.push(() => settings.changeGlobal(Settings.KEY_USER_IS_NEW, false)); } } @@ -492,12 +430,12 @@ const task_teaweb_starter: loader.Task = { try { await initialize_app(); main(); - if(!audio.player.initialized()) { + if(!aplayer.initialized()) { log.info(LogCategory.VOICE, tr("Initialize audio controller later!")); - if(!audio.player.initializeFromGesture) { - console.error(tr("Missing audio.player.initializeFromGesture")); + if(!aplayer.initializeFromGesture) { + console.error(tr("Missing aplayer.initializeFromGesture")); } else - $(document).one('click', event => audio.player.initializeFromGesture()); + $(document).one('click', event => aplayer.initializeFromGesture()); } } catch (ex) { console.error(ex.stack); @@ -543,7 +481,7 @@ const task_connect_handler: loader.Task = { "You could now close this page."; createInfoModal( tr("Connecting successfully within other instance"), - MessageHelper.formatMessage(tr(message), connect_data.address), + formatMessage(tr(message), connect_data.address), { closeable: false, footer: undefined @@ -610,7 +548,7 @@ const task_certificate_callback: loader.Task = { "This page will close in {0} seconds."; createInfoModal( tr("Certificate acccepted successfully"), - MessageHelper.formatMessage(tr(message), seconds_tag), + formatMessage(tr(message), seconds_tag), { closeable: false, footer: undefined @@ -650,7 +588,7 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { try { await initialize(); - if(app.is_web()) { + if(loader.version().type == "web") { loader.register_task(loader.Stage.LOADED, task_certificate_callback); } else { loader.register_task(loader.Stage.LOADED, task_teaweb_starter); @@ -667,3 +605,15 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { priority: 1000 }); +loader.register_task(loader.Stage.LOADED, { + name: "error task", + function: async () => { + if(Settings.instance.static(Settings.KEY_LOAD_DUMMY_ERROR, false)) { + loader.critical_error("The tea is cold!", "Argh, this is evil! Cold tea dosn't taste good."); + throw "The tea is cold!"; + } + }, + priority: 20 +}); + +export = {}; \ No newline at end of file diff --git a/shared/js/permission/GroupManager.ts b/shared/js/permission/GroupManager.ts index c781f468..2c27181f 100644 --- a/shared/js/permission/GroupManager.ts +++ b/shared/js/permission/GroupManager.ts @@ -1,17 +1,24 @@ -/// +import {LaterPromise} from "tc-shared/utils/LaterPromise"; +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import {PermissionManager, PermissionValue} from "tc-shared/permission/PermissionManager"; +import {ServerCommand} from "tc-shared/connection/ConnectionBase"; +import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler"; -enum GroupType { +export enum GroupType { QUERY, TEMPLATE, NORMAL } -enum GroupTarget { +export enum GroupTarget { SERVER, CHANNEL } -class GroupProperties { +export class GroupProperties { iconid: number = 0; sortid: number = 0; @@ -19,12 +26,12 @@ class GroupProperties { namemode: number = 0; } -class GroupPermissionRequest { +export class GroupPermissionRequest { group_id: number; promise: LaterPromise; } -class Group { +export class Group { properties: GroupProperties = new GroupProperties(); readonly handle: GroupManager; @@ -63,7 +70,7 @@ class Group { } } -class GroupManager extends connection.AbstractCommandHandler { +export class GroupManager extends AbstractCommandHandler { readonly handle: ConnectionHandler; serverGroups: Group[] = []; @@ -83,7 +90,7 @@ class GroupManager extends connection.AbstractCommandHandler { this.channelGroups = undefined; } - handle_command(command: connection.ServerCommand): boolean { + handle_command(command: ServerCommand): boolean { switch (command.command) { case "notifyservergrouplist": case "notifychannelgrouplist": @@ -160,7 +167,7 @@ class GroupManager extends connection.AbstractCommandHandler { } let group = new Group(this,parseInt(target == GroupTarget.SERVER ? groupData["sgid"] : groupData["cgid"]), target, type, groupData["name"]); - for(let key in groupData as any) { + for(let key in Object.keys(groupData)) { if(key == "sgid") continue; if(key == "cgid") continue; if(key == "type") continue; diff --git a/shared/js/permission/PermissionManager.ts b/shared/js/permission/PermissionManager.ts index 9b558d1c..2942df71 100644 --- a/shared/js/permission/PermissionManager.ts +++ b/shared/js/permission/PermissionManager.ts @@ -1,358 +1,13 @@ -/// -/// -/// +import * as log from "tc-shared/log"; +import {LogCategory, LogType} from "tc-shared/log"; +import PermissionType from "tc-shared/permission/PermissionType"; +import {LaterPromise} from "tc-shared/utils/LaterPromise"; +import {ServerCommand} from "tc-shared/connection/ConnectionBase"; +import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler"; -enum PermissionType { - B_SERVERINSTANCE_HELP_VIEW = "b_serverinstance_help_view", - B_SERVERINSTANCE_VERSION_VIEW = "b_serverinstance_version_view", - B_SERVERINSTANCE_INFO_VIEW = "b_serverinstance_info_view", - B_SERVERINSTANCE_VIRTUALSERVER_LIST = "b_serverinstance_virtualserver_list", - B_SERVERINSTANCE_BINDING_LIST = "b_serverinstance_binding_list", - B_SERVERINSTANCE_PERMISSION_LIST = "b_serverinstance_permission_list", - B_SERVERINSTANCE_PERMISSION_FIND = "b_serverinstance_permission_find", - B_VIRTUALSERVER_CREATE = "b_virtualserver_create", - B_VIRTUALSERVER_DELETE = "b_virtualserver_delete", - B_VIRTUALSERVER_START_ANY = "b_virtualserver_start_any", - B_VIRTUALSERVER_STOP_ANY = "b_virtualserver_stop_any", - B_VIRTUALSERVER_CHANGE_MACHINE_ID = "b_virtualserver_change_machine_id", - B_VIRTUALSERVER_CHANGE_TEMPLATE = "b_virtualserver_change_template", - B_SERVERQUERY_LOGIN = "b_serverquery_login", - B_SERVERINSTANCE_TEXTMESSAGE_SEND = "b_serverinstance_textmessage_send", - B_SERVERINSTANCE_LOG_VIEW = "b_serverinstance_log_view", - B_SERVERINSTANCE_LOG_ADD = "b_serverinstance_log_add", - B_SERVERINSTANCE_STOP = "b_serverinstance_stop", - B_SERVERINSTANCE_MODIFY_SETTINGS = "b_serverinstance_modify_settings", - B_SERVERINSTANCE_MODIFY_QUERYGROUP = "b_serverinstance_modify_querygroup", - B_SERVERINSTANCE_MODIFY_TEMPLATES = "b_serverinstance_modify_templates", - B_VIRTUALSERVER_SELECT = "b_virtualserver_select", - B_VIRTUALSERVER_SELECT_GODMODE = "b_virtualserver_select_godmode", - B_VIRTUALSERVER_INFO_VIEW = "b_virtualserver_info_view", - B_VIRTUALSERVER_CONNECTIONINFO_VIEW = "b_virtualserver_connectioninfo_view", - B_VIRTUALSERVER_CHANNEL_LIST = "b_virtualserver_channel_list", - B_VIRTUALSERVER_CHANNEL_SEARCH = "b_virtualserver_channel_search", - B_VIRTUALSERVER_CLIENT_LIST = "b_virtualserver_client_list", - B_VIRTUALSERVER_CLIENT_SEARCH = "b_virtualserver_client_search", - B_VIRTUALSERVER_CLIENT_DBLIST = "b_virtualserver_client_dblist", - B_VIRTUALSERVER_CLIENT_DBSEARCH = "b_virtualserver_client_dbsearch", - B_VIRTUALSERVER_CLIENT_DBINFO = "b_virtualserver_client_dbinfo", - B_VIRTUALSERVER_PERMISSION_FIND = "b_virtualserver_permission_find", - B_VIRTUALSERVER_CUSTOM_SEARCH = "b_virtualserver_custom_search", - B_VIRTUALSERVER_START = "b_virtualserver_start", - B_VIRTUALSERVER_STOP = "b_virtualserver_stop", - B_VIRTUALSERVER_TOKEN_LIST = "b_virtualserver_token_list", - B_VIRTUALSERVER_TOKEN_ADD = "b_virtualserver_token_add", - B_VIRTUALSERVER_TOKEN_USE = "b_virtualserver_token_use", - B_VIRTUALSERVER_TOKEN_DELETE = "b_virtualserver_token_delete", - B_VIRTUALSERVER_LOG_VIEW = "b_virtualserver_log_view", - B_VIRTUALSERVER_LOG_ADD = "b_virtualserver_log_add", - B_VIRTUALSERVER_JOIN_IGNORE_PASSWORD = "b_virtualserver_join_ignore_password", - B_VIRTUALSERVER_NOTIFY_REGISTER = "b_virtualserver_notify_register", - B_VIRTUALSERVER_NOTIFY_UNREGISTER = "b_virtualserver_notify_unregister", - B_VIRTUALSERVER_SNAPSHOT_CREATE = "b_virtualserver_snapshot_create", - B_VIRTUALSERVER_SNAPSHOT_DEPLOY = "b_virtualserver_snapshot_deploy", - B_VIRTUALSERVER_PERMISSION_RESET = "b_virtualserver_permission_reset", - B_VIRTUALSERVER_MODIFY_NAME = "b_virtualserver_modify_name", - B_VIRTUALSERVER_MODIFY_WELCOMEMESSAGE = "b_virtualserver_modify_welcomemessage", - B_VIRTUALSERVER_MODIFY_MAXCLIENTS = "b_virtualserver_modify_maxclients", - B_VIRTUALSERVER_MODIFY_RESERVED_SLOTS = "b_virtualserver_modify_reserved_slots", - B_VIRTUALSERVER_MODIFY_PASSWORD = "b_virtualserver_modify_password", - B_VIRTUALSERVER_MODIFY_DEFAULT_SERVERGROUP = "b_virtualserver_modify_default_servergroup", - B_VIRTUALSERVER_MODIFY_DEFAULT_MUSICGROUP = "b_virtualserver_modify_default_musicgroup", - B_VIRTUALSERVER_MODIFY_DEFAULT_CHANNELGROUP = "b_virtualserver_modify_default_channelgroup", - B_VIRTUALSERVER_MODIFY_DEFAULT_CHANNELADMINGROUP = "b_virtualserver_modify_default_channeladmingroup", - B_VIRTUALSERVER_MODIFY_CHANNEL_FORCED_SILENCE = "b_virtualserver_modify_channel_forced_silence", - B_VIRTUALSERVER_MODIFY_COMPLAIN = "b_virtualserver_modify_complain", - B_VIRTUALSERVER_MODIFY_ANTIFLOOD = "b_virtualserver_modify_antiflood", - B_VIRTUALSERVER_MODIFY_FT_SETTINGS = "b_virtualserver_modify_ft_settings", - B_VIRTUALSERVER_MODIFY_FT_QUOTAS = "b_virtualserver_modify_ft_quotas", - B_VIRTUALSERVER_MODIFY_HOSTMESSAGE = "b_virtualserver_modify_hostmessage", - B_VIRTUALSERVER_MODIFY_HOSTBANNER = "b_virtualserver_modify_hostbanner", - B_VIRTUALSERVER_MODIFY_HOSTBUTTON = "b_virtualserver_modify_hostbutton", - B_VIRTUALSERVER_MODIFY_PORT = "b_virtualserver_modify_port", - B_VIRTUALSERVER_MODIFY_HOST = "b_virtualserver_modify_host", - B_VIRTUALSERVER_MODIFY_DEFAULT_MESSAGES = "b_virtualserver_modify_default_messages", - B_VIRTUALSERVER_MODIFY_AUTOSTART = "b_virtualserver_modify_autostart", - B_VIRTUALSERVER_MODIFY_NEEDED_IDENTITY_SECURITY_LEVEL = "b_virtualserver_modify_needed_identity_security_level", - B_VIRTUALSERVER_MODIFY_PRIORITY_SPEAKER_DIMM_MODIFICATOR = "b_virtualserver_modify_priority_speaker_dimm_modificator", - B_VIRTUALSERVER_MODIFY_LOG_SETTINGS = "b_virtualserver_modify_log_settings", - B_VIRTUALSERVER_MODIFY_MIN_CLIENT_VERSION = "b_virtualserver_modify_min_client_version", - B_VIRTUALSERVER_MODIFY_ICON_ID = "b_virtualserver_modify_icon_id", - B_VIRTUALSERVER_MODIFY_WEBLIST = "b_virtualserver_modify_weblist", - B_VIRTUALSERVER_MODIFY_CODEC_ENCRYPTION_MODE = "b_virtualserver_modify_codec_encryption_mode", - B_VIRTUALSERVER_MODIFY_TEMPORARY_PASSWORDS = "b_virtualserver_modify_temporary_passwords", - B_VIRTUALSERVER_MODIFY_TEMPORARY_PASSWORDS_OWN = "b_virtualserver_modify_temporary_passwords_own", - B_VIRTUALSERVER_MODIFY_CHANNEL_TEMP_DELETE_DELAY_DEFAULT = "b_virtualserver_modify_channel_temp_delete_delay_default", - B_VIRTUALSERVER_MODIFY_MUSIC_BOT_LIMIT = "b_virtualserver_modify_music_bot_limit", - B_VIRTUALSERVER_MODIFY_COUNTRY_CODE = "b_virtualserver_modify_country_code", - I_CHANNEL_MIN_DEPTH = "i_channel_min_depth", - I_CHANNEL_MAX_DEPTH = "i_channel_max_depth", - B_CHANNEL_GROUP_INHERITANCE_END = "b_channel_group_inheritance_end", - I_CHANNEL_PERMISSION_MODIFY_POWER = "i_channel_permission_modify_power", - I_CHANNEL_NEEDED_PERMISSION_MODIFY_POWER = "i_channel_needed_permission_modify_power", - B_CHANNEL_INFO_VIEW = "b_channel_info_view", - B_CHANNEL_CREATE_CHILD = "b_channel_create_child", - B_CHANNEL_CREATE_PERMANENT = "b_channel_create_permanent", - B_CHANNEL_CREATE_SEMI_PERMANENT = "b_channel_create_semi_permanent", - B_CHANNEL_CREATE_TEMPORARY = "b_channel_create_temporary", - B_CHANNEL_CREATE_PRIVATE = "b_channel_create_private", - B_CHANNEL_CREATE_WITH_TOPIC = "b_channel_create_with_topic", - B_CHANNEL_CREATE_WITH_DESCRIPTION = "b_channel_create_with_description", - B_CHANNEL_CREATE_WITH_PASSWORD = "b_channel_create_with_password", - B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX8 = "b_channel_create_modify_with_codec_speex8", - B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX16 = "b_channel_create_modify_with_codec_speex16", - B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX32 = "b_channel_create_modify_with_codec_speex32", - B_CHANNEL_CREATE_MODIFY_WITH_CODEC_CELTMONO48 = "b_channel_create_modify_with_codec_celtmono48", - B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE = "b_channel_create_modify_with_codec_opusvoice", - B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC = "b_channel_create_modify_with_codec_opusmusic", - I_CHANNEL_CREATE_MODIFY_WITH_CODEC_MAXQUALITY = "i_channel_create_modify_with_codec_maxquality", - I_CHANNEL_CREATE_MODIFY_WITH_CODEC_LATENCY_FACTOR_MIN = "i_channel_create_modify_with_codec_latency_factor_min", - I_CHANNEL_CREATE_MODIFY_CONVERSATION_HISTORY_LENGTH = "i_channel_create_modify_conversation_history_length", - B_CHANNEL_CREATE_MODIFY_CONVERSATION_HISTORY_UNLIMITED = "b_channel_create_modify_conversation_history_unlimited", - B_CHANNEL_CREATE_MODIFY_CONVERSATION_PRIVATE = "b_channel_create_modify_conversation_private", - B_CHANNEL_CREATE_WITH_MAXCLIENTS = "b_channel_create_with_maxclients", - B_CHANNEL_CREATE_WITH_MAXFAMILYCLIENTS = "b_channel_create_with_maxfamilyclients", - B_CHANNEL_CREATE_WITH_SORTORDER = "b_channel_create_with_sortorder", - B_CHANNEL_CREATE_WITH_DEFAULT = "b_channel_create_with_default", - B_CHANNEL_CREATE_WITH_NEEDED_TALK_POWER = "b_channel_create_with_needed_talk_power", - B_CHANNEL_CREATE_MODIFY_WITH_FORCE_PASSWORD = "b_channel_create_modify_with_force_password", - I_CHANNEL_CREATE_MODIFY_WITH_TEMP_DELETE_DELAY = "i_channel_create_modify_with_temp_delete_delay", - B_CHANNEL_MODIFY_PARENT = "b_channel_modify_parent", - B_CHANNEL_MODIFY_MAKE_DEFAULT = "b_channel_modify_make_default", - B_CHANNEL_MODIFY_MAKE_PERMANENT = "b_channel_modify_make_permanent", - B_CHANNEL_MODIFY_MAKE_SEMI_PERMANENT = "b_channel_modify_make_semi_permanent", - B_CHANNEL_MODIFY_MAKE_TEMPORARY = "b_channel_modify_make_temporary", - B_CHANNEL_MODIFY_NAME = "b_channel_modify_name", - B_CHANNEL_MODIFY_TOPIC = "b_channel_modify_topic", - B_CHANNEL_MODIFY_DESCRIPTION = "b_channel_modify_description", - B_CHANNEL_MODIFY_PASSWORD = "b_channel_modify_password", - B_CHANNEL_MODIFY_CODEC = "b_channel_modify_codec", - B_CHANNEL_MODIFY_CODEC_QUALITY = "b_channel_modify_codec_quality", - B_CHANNEL_MODIFY_CODEC_LATENCY_FACTOR = "b_channel_modify_codec_latency_factor", - B_CHANNEL_MODIFY_MAXCLIENTS = "b_channel_modify_maxclients", - B_CHANNEL_MODIFY_MAXFAMILYCLIENTS = "b_channel_modify_maxfamilyclients", - B_CHANNEL_MODIFY_SORTORDER = "b_channel_modify_sortorder", - B_CHANNEL_MODIFY_NEEDED_TALK_POWER = "b_channel_modify_needed_talk_power", - I_CHANNEL_MODIFY_POWER = "i_channel_modify_power", - I_CHANNEL_NEEDED_MODIFY_POWER = "i_channel_needed_modify_power", - B_CHANNEL_MODIFY_MAKE_CODEC_ENCRYPTED = "b_channel_modify_make_codec_encrypted", - B_CHANNEL_MODIFY_TEMP_DELETE_DELAY = "b_channel_modify_temp_delete_delay", - B_CHANNEL_DELETE_PERMANENT = "b_channel_delete_permanent", - B_CHANNEL_DELETE_SEMI_PERMANENT = "b_channel_delete_semi_permanent", - B_CHANNEL_DELETE_TEMPORARY = "b_channel_delete_temporary", - B_CHANNEL_DELETE_FLAG_FORCE = "b_channel_delete_flag_force", - I_CHANNEL_DELETE_POWER = "i_channel_delete_power", - B_CHANNEL_CONVERSATION_MESSAGE_DELETE = "b_channel_conversation_message_delete", - I_CHANNEL_NEEDED_DELETE_POWER = "i_channel_needed_delete_power", - B_CHANNEL_JOIN_PERMANENT = "b_channel_join_permanent", - B_CHANNEL_JOIN_SEMI_PERMANENT = "b_channel_join_semi_permanent", - B_CHANNEL_JOIN_TEMPORARY = "b_channel_join_temporary", - B_CHANNEL_JOIN_IGNORE_PASSWORD = "b_channel_join_ignore_password", - B_CHANNEL_JOIN_IGNORE_MAXCLIENTS = "b_channel_join_ignore_maxclients", - B_CHANNEL_IGNORE_VIEW_POWER = "b_channel_ignore_view_power", - I_CHANNEL_JOIN_POWER = "i_channel_join_power", - I_CHANNEL_NEEDED_JOIN_POWER = "i_channel_needed_join_power", - B_CHANNEL_IGNORE_JOIN_POWER = "b_channel_ignore_join_power", - B_CHANNEL_IGNORE_DESCRIPTION_VIEW_POWER = "b_channel_ignore_description_view_power", - I_CHANNEL_VIEW_POWER = "i_channel_view_power", - I_CHANNEL_NEEDED_VIEW_POWER = "i_channel_needed_view_power", - I_CHANNEL_SUBSCRIBE_POWER = "i_channel_subscribe_power", - I_CHANNEL_NEEDED_SUBSCRIBE_POWER = "i_channel_needed_subscribe_power", - I_CHANNEL_DESCRIPTION_VIEW_POWER = "i_channel_description_view_power", - I_CHANNEL_NEEDED_DESCRIPTION_VIEW_POWER = "i_channel_needed_description_view_power", - I_ICON_ID = "i_icon_id", - I_MAX_ICON_FILESIZE = "i_max_icon_filesize", - I_MAX_PLAYLIST_SIZE = "i_max_playlist_size", - I_MAX_PLAYLISTS = "i_max_playlists", - B_ICON_MANAGE = "b_icon_manage", - B_GROUP_IS_PERMANENT = "b_group_is_permanent", - I_GROUP_AUTO_UPDATE_TYPE = "i_group_auto_update_type", - I_GROUP_AUTO_UPDATE_MAX_VALUE = "i_group_auto_update_max_value", - I_GROUP_SORT_ID = "i_group_sort_id", - I_GROUP_SHOW_NAME_IN_TREE = "i_group_show_name_in_tree", - B_VIRTUALSERVER_SERVERGROUP_CREATE = "b_virtualserver_servergroup_create", - B_VIRTUALSERVER_SERVERGROUP_LIST = "b_virtualserver_servergroup_list", - B_VIRTUALSERVER_SERVERGROUP_PERMISSION_LIST = "b_virtualserver_servergroup_permission_list", - B_VIRTUALSERVER_SERVERGROUP_CLIENT_LIST = "b_virtualserver_servergroup_client_list", - B_VIRTUALSERVER_CHANNELGROUP_CREATE = "b_virtualserver_channelgroup_create", - B_VIRTUALSERVER_CHANNELGROUP_LIST = "b_virtualserver_channelgroup_list", - B_VIRTUALSERVER_CHANNELGROUP_PERMISSION_LIST = "b_virtualserver_channelgroup_permission_list", - B_VIRTUALSERVER_CHANNELGROUP_CLIENT_LIST = "b_virtualserver_channelgroup_client_list", - B_VIRTUALSERVER_CLIENT_PERMISSION_LIST = "b_virtualserver_client_permission_list", - B_VIRTUALSERVER_CHANNEL_PERMISSION_LIST = "b_virtualserver_channel_permission_list", - B_VIRTUALSERVER_CHANNELCLIENT_PERMISSION_LIST = "b_virtualserver_channelclient_permission_list", - B_VIRTUALSERVER_PLAYLIST_PERMISSION_LIST = "b_virtualserver_playlist_permission_list", - I_SERVER_GROUP_MODIFY_POWER = "i_server_group_modify_power", - I_SERVER_GROUP_NEEDED_MODIFY_POWER = "i_server_group_needed_modify_power", - I_SERVER_GROUP_MEMBER_ADD_POWER = "i_server_group_member_add_power", - I_SERVER_GROUP_SELF_ADD_POWER = "i_server_group_self_add_power", - I_SERVER_GROUP_NEEDED_MEMBER_ADD_POWER = "i_server_group_needed_member_add_power", - I_SERVER_GROUP_MEMBER_REMOVE_POWER = "i_server_group_member_remove_power", - I_SERVER_GROUP_SELF_REMOVE_POWER = "i_server_group_self_remove_power", - I_SERVER_GROUP_NEEDED_MEMBER_REMOVE_POWER = "i_server_group_needed_member_remove_power", - I_CHANNEL_GROUP_MODIFY_POWER = "i_channel_group_modify_power", - I_CHANNEL_GROUP_NEEDED_MODIFY_POWER = "i_channel_group_needed_modify_power", - I_CHANNEL_GROUP_MEMBER_ADD_POWER = "i_channel_group_member_add_power", - I_CHANNEL_GROUP_SELF_ADD_POWER = "i_channel_group_self_add_power", - I_CHANNEL_GROUP_NEEDED_MEMBER_ADD_POWER = "i_channel_group_needed_member_add_power", - I_CHANNEL_GROUP_MEMBER_REMOVE_POWER = "i_channel_group_member_remove_power", - I_CHANNEL_GROUP_SELF_REMOVE_POWER = "i_channel_group_self_remove_power", - I_CHANNEL_GROUP_NEEDED_MEMBER_REMOVE_POWER = "i_channel_group_needed_member_remove_power", - I_GROUP_MEMBER_ADD_POWER = "i_group_member_add_power", - I_GROUP_NEEDED_MEMBER_ADD_POWER = "i_group_needed_member_add_power", - I_GROUP_MEMBER_REMOVE_POWER = "i_group_member_remove_power", - I_GROUP_NEEDED_MEMBER_REMOVE_POWER = "i_group_needed_member_remove_power", - I_GROUP_MODIFY_POWER = "i_group_modify_power", - I_GROUP_NEEDED_MODIFY_POWER = "i_group_needed_modify_power", - I_PERMISSION_MODIFY_POWER = "i_permission_modify_power", - B_PERMISSION_MODIFY_POWER_IGNORE = "b_permission_modify_power_ignore", - B_VIRTUALSERVER_SERVERGROUP_DELETE = "b_virtualserver_servergroup_delete", - B_VIRTUALSERVER_CHANNELGROUP_DELETE = "b_virtualserver_channelgroup_delete", - I_CLIENT_PERMISSION_MODIFY_POWER = "i_client_permission_modify_power", - I_CLIENT_NEEDED_PERMISSION_MODIFY_POWER = "i_client_needed_permission_modify_power", - I_CLIENT_MAX_CLONES_UID = "i_client_max_clones_uid", - I_CLIENT_MAX_CLONES_IP = "i_client_max_clones_ip", - I_CLIENT_MAX_CLONES_HWID = "i_client_max_clones_hwid", - I_CLIENT_MAX_IDLETIME = "i_client_max_idletime", - I_CLIENT_MAX_AVATAR_FILESIZE = "i_client_max_avatar_filesize", - I_CLIENT_MAX_CHANNEL_SUBSCRIPTIONS = "i_client_max_channel_subscriptions", - I_CLIENT_MAX_CHANNELS = "i_client_max_channels", - I_CLIENT_MAX_TEMPORARY_CHANNELS = "i_client_max_temporary_channels", - I_CLIENT_MAX_SEMI_CHANNELS = "i_client_max_semi_channels", - I_CLIENT_MAX_PERMANENT_CHANNELS = "i_client_max_permanent_channels", - B_CLIENT_USE_PRIORITY_SPEAKER = "b_client_use_priority_speaker", - B_CLIENT_SKIP_CHANNELGROUP_PERMISSIONS = "b_client_skip_channelgroup_permissions", - B_CLIENT_FORCE_PUSH_TO_TALK = "b_client_force_push_to_talk", - B_CLIENT_IGNORE_BANS = "b_client_ignore_bans", - B_CLIENT_IGNORE_VPN = "b_client_ignore_vpn", - B_CLIENT_IGNORE_ANTIFLOOD = "b_client_ignore_antiflood", - B_CLIENT_ENFORCE_VALID_HWID = "b_client_enforce_valid_hwid", - B_CLIENT_ALLOW_INVALID_PACKET = "b_client_allow_invalid_packet", - B_CLIENT_ALLOW_INVALID_BADGES = "b_client_allow_invalid_badges", - B_CLIENT_ISSUE_CLIENT_QUERY_COMMAND = "b_client_issue_client_query_command", - B_CLIENT_USE_RESERVED_SLOT = "b_client_use_reserved_slot", - B_CLIENT_USE_CHANNEL_COMMANDER = "b_client_use_channel_commander", - B_CLIENT_REQUEST_TALKER = "b_client_request_talker", - B_CLIENT_AVATAR_DELETE_OTHER = "b_client_avatar_delete_other", - B_CLIENT_IS_STICKY = "b_client_is_sticky", - B_CLIENT_IGNORE_STICKY = "b_client_ignore_sticky", - B_CLIENT_MUSIC_CREATE_PERMANENT = "b_client_music_create_permanent", - B_CLIENT_MUSIC_CREATE_SEMI_PERMANENT = "b_client_music_create_semi_permanent", - B_CLIENT_MUSIC_CREATE_TEMPORARY = "b_client_music_create_temporary", - B_CLIENT_MUSIC_MODIFY_PERMANENT = "b_client_music_modify_permanent", - B_CLIENT_MUSIC_MODIFY_SEMI_PERMANENT = "b_client_music_modify_semi_permanent", - B_CLIENT_MUSIC_MODIFY_TEMPORARY = "b_client_music_modify_temporary", - I_CLIENT_MUSIC_CREATE_MODIFY_MAX_VOLUME = "i_client_music_create_modify_max_volume", - I_CLIENT_MUSIC_LIMIT = "i_client_music_limit", - I_CLIENT_MUSIC_NEEDED_DELETE_POWER = "i_client_music_needed_delete_power", - I_CLIENT_MUSIC_DELETE_POWER = "i_client_music_delete_power", - I_CLIENT_MUSIC_PLAY_POWER = "i_client_music_play_power", - I_CLIENT_MUSIC_NEEDED_PLAY_POWER = "i_client_music_needed_play_power", - I_CLIENT_MUSIC_MODIFY_POWER = "i_client_music_modify_power", - I_CLIENT_MUSIC_NEEDED_MODIFY_POWER = "i_client_music_needed_modify_power", - I_CLIENT_MUSIC_RENAME_POWER = "i_client_music_rename_power", - I_CLIENT_MUSIC_NEEDED_RENAME_POWER = "i_client_music_needed_rename_power", - B_PLAYLIST_CREATE = "b_playlist_create", - I_PLAYLIST_VIEW_POWER = "i_playlist_view_power", - I_PLAYLIST_NEEDED_VIEW_POWER = "i_playlist_needed_view_power", - I_PLAYLIST_MODIFY_POWER = "i_playlist_modify_power", - I_PLAYLIST_NEEDED_MODIFY_POWER = "i_playlist_needed_modify_power", - I_PLAYLIST_PERMISSION_MODIFY_POWER = "i_playlist_permission_modify_power", - I_PLAYLIST_NEEDED_PERMISSION_MODIFY_POWER = "i_playlist_needed_permission_modify_power", - I_PLAYLIST_DELETE_POWER = "i_playlist_delete_power", - I_PLAYLIST_NEEDED_DELETE_POWER = "i_playlist_needed_delete_power", - I_PLAYLIST_SONG_ADD_POWER = "i_playlist_song_add_power", - I_PLAYLIST_SONG_NEEDED_ADD_POWER = "i_playlist_song_needed_add_power", - I_PLAYLIST_SONG_REMOVE_POWER = "i_playlist_song_remove_power", - I_PLAYLIST_SONG_NEEDED_REMOVE_POWER = "i_playlist_song_needed_remove_power", - B_CLIENT_INFO_VIEW = "b_client_info_view", - B_CLIENT_PERMISSIONOVERVIEW_VIEW = "b_client_permissionoverview_view", - B_CLIENT_PERMISSIONOVERVIEW_OWN = "b_client_permissionoverview_own", - B_CLIENT_REMOTEADDRESS_VIEW = "b_client_remoteaddress_view", - I_CLIENT_SERVERQUERY_VIEW_POWER = "i_client_serverquery_view_power", - I_CLIENT_NEEDED_SERVERQUERY_VIEW_POWER = "i_client_needed_serverquery_view_power", - B_CLIENT_CUSTOM_INFO_VIEW = "b_client_custom_info_view", - B_CLIENT_MUSIC_CHANNEL_LIST = "b_client_music_channel_list", - B_CLIENT_MUSIC_SERVER_LIST = "b_client_music_server_list", - I_CLIENT_MUSIC_INFO = "i_client_music_info", - I_CLIENT_MUSIC_NEEDED_INFO = "i_client_music_needed_info", - I_CLIENT_KICK_FROM_SERVER_POWER = "i_client_kick_from_server_power", - I_CLIENT_NEEDED_KICK_FROM_SERVER_POWER = "i_client_needed_kick_from_server_power", - I_CLIENT_KICK_FROM_CHANNEL_POWER = "i_client_kick_from_channel_power", - I_CLIENT_NEEDED_KICK_FROM_CHANNEL_POWER = "i_client_needed_kick_from_channel_power", - I_CLIENT_BAN_POWER = "i_client_ban_power", - I_CLIENT_NEEDED_BAN_POWER = "i_client_needed_ban_power", - I_CLIENT_MOVE_POWER = "i_client_move_power", - I_CLIENT_NEEDED_MOVE_POWER = "i_client_needed_move_power", - I_CLIENT_COMPLAIN_POWER = "i_client_complain_power", - I_CLIENT_NEEDED_COMPLAIN_POWER = "i_client_needed_complain_power", - B_CLIENT_COMPLAIN_LIST = "b_client_complain_list", - B_CLIENT_COMPLAIN_DELETE_OWN = "b_client_complain_delete_own", - B_CLIENT_COMPLAIN_DELETE = "b_client_complain_delete", - B_CLIENT_BAN_LIST = "b_client_ban_list", - B_CLIENT_BAN_LIST_GLOBAL = "b_client_ban_list_global", - B_CLIENT_BAN_TRIGGER_LIST = "b_client_ban_trigger_list", - B_CLIENT_BAN_CREATE = "b_client_ban_create", - B_CLIENT_BAN_CREATE_GLOBAL = "b_client_ban_create_global", - B_CLIENT_BAN_NAME = "b_client_ban_name", - B_CLIENT_BAN_IP = "b_client_ban_ip", - B_CLIENT_BAN_HWID = "b_client_ban_hwid", - B_CLIENT_BAN_EDIT = "b_client_ban_edit", - B_CLIENT_BAN_EDIT_GLOBAL = "b_client_ban_edit_global", - B_CLIENT_BAN_DELETE_OWN = "b_client_ban_delete_own", - B_CLIENT_BAN_DELETE = "b_client_ban_delete", - B_CLIENT_BAN_DELETE_OWN_GLOBAL = "b_client_ban_delete_own_global", - B_CLIENT_BAN_DELETE_GLOBAL = "b_client_ban_delete_global", - I_CLIENT_BAN_MAX_BANTIME = "i_client_ban_max_bantime", - I_CLIENT_PRIVATE_TEXTMESSAGE_POWER = "i_client_private_textmessage_power", - I_CLIENT_NEEDED_PRIVATE_TEXTMESSAGE_POWER = "i_client_needed_private_textmessage_power", - B_CLIENT_EVEN_TEXTMESSAGE_SEND = "b_client_even_textmessage_send", - B_CLIENT_SERVER_TEXTMESSAGE_SEND = "b_client_server_textmessage_send", - B_CLIENT_CHANNEL_TEXTMESSAGE_SEND = "b_client_channel_textmessage_send", - B_CLIENT_OFFLINE_TEXTMESSAGE_SEND = "b_client_offline_textmessage_send", - I_CLIENT_TALK_POWER = "i_client_talk_power", - I_CLIENT_NEEDED_TALK_POWER = "i_client_needed_talk_power", - I_CLIENT_POKE_POWER = "i_client_poke_power", - I_CLIENT_NEEDED_POKE_POWER = "i_client_needed_poke_power", - B_CLIENT_SET_FLAG_TALKER = "b_client_set_flag_talker", - I_CLIENT_WHISPER_POWER = "i_client_whisper_power", - I_CLIENT_NEEDED_WHISPER_POWER = "i_client_needed_whisper_power", - B_CLIENT_MODIFY_DESCRIPTION = "b_client_modify_description", - B_CLIENT_MODIFY_OWN_DESCRIPTION = "b_client_modify_own_description", - B_CLIENT_USE_BBCODE_ANY = "b_client_use_bbcode_any", - B_CLIENT_USE_BBCODE_URL = "b_client_use_bbcode_url", - B_CLIENT_USE_BBCODE_IMAGE = "b_client_use_bbcode_image", - B_CLIENT_MODIFY_DBPROPERTIES = "b_client_modify_dbproperties", - B_CLIENT_DELETE_DBPROPERTIES = "b_client_delete_dbproperties", - B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN = "b_client_create_modify_serverquery_login", - B_CLIENT_QUERY_CREATE = "b_client_query_create", - B_CLIENT_QUERY_LIST = "b_client_query_list", - B_CLIENT_QUERY_LIST_OWN = "b_client_query_list_own", - B_CLIENT_QUERY_RENAME = "b_client_query_rename", - B_CLIENT_QUERY_RENAME_OWN = "b_client_query_rename_own", - B_CLIENT_QUERY_CHANGE_PASSWORD = "b_client_query_change_password", - B_CLIENT_QUERY_CHANGE_OWN_PASSWORD = "b_client_query_change_own_password", - B_CLIENT_QUERY_CHANGE_PASSWORD_GLOBAL = "b_client_query_change_password_global", - B_CLIENT_QUERY_DELETE = "b_client_query_delete", - B_CLIENT_QUERY_DELETE_OWN = "b_client_query_delete_own", - B_FT_IGNORE_PASSWORD = "b_ft_ignore_password", - B_FT_TRANSFER_LIST = "b_ft_transfer_list", - I_FT_FILE_UPLOAD_POWER = "i_ft_file_upload_power", - I_FT_NEEDED_FILE_UPLOAD_POWER = "i_ft_needed_file_upload_power", - I_FT_FILE_DOWNLOAD_POWER = "i_ft_file_download_power", - I_FT_NEEDED_FILE_DOWNLOAD_POWER = "i_ft_needed_file_download_power", - I_FT_FILE_DELETE_POWER = "i_ft_file_delete_power", - I_FT_NEEDED_FILE_DELETE_POWER = "i_ft_needed_file_delete_power", - I_FT_FILE_RENAME_POWER = "i_ft_file_rename_power", - I_FT_NEEDED_FILE_RENAME_POWER = "i_ft_needed_file_rename_power", - I_FT_FILE_BROWSE_POWER = "i_ft_file_browse_power", - I_FT_NEEDED_FILE_BROWSE_POWER = "i_ft_needed_file_browse_power", - I_FT_DIRECTORY_CREATE_POWER = "i_ft_directory_create_power", - I_FT_NEEDED_DIRECTORY_CREATE_POWER = "i_ft_needed_directory_create_power", - I_FT_QUOTA_MB_DOWNLOAD_PER_CLIENT = "i_ft_quota_mb_download_per_client", - I_FT_QUOTA_MB_UPLOAD_PER_CLIENT = "i_ft_quota_mb_upload_per_client" -} - -class PermissionInfo { +export class PermissionInfo { name: string; id: number; description: string; @@ -363,21 +18,21 @@ class PermissionInfo { } } -class PermissionGroup { +export class PermissionGroup { begin: number; end: number; deep: number; name: string; } -class GroupedPermissions { +export class GroupedPermissions { group: PermissionGroup; permissions: PermissionInfo[]; children: GroupedPermissions[]; parent: GroupedPermissions; } -class PermissionValue { +export class PermissionValue { readonly type: PermissionInfo; value: number; flag_skip: boolean; @@ -411,75 +66,74 @@ class PermissionValue { } } -class NeededPermissionValue extends PermissionValue { +export class NeededPermissionValue extends PermissionValue { constructor(type, value) { super(type, value); } } -namespace permissions { - export type PermissionRequestKeys = { - client_id?: number; - channel_id?: number; - playlist_id?: number; +export type PermissionRequestKeys = { + client_id?: number; + channel_id?: number; + playlist_id?: number; +} + +export type PermissionRequest = PermissionRequestKeys & { + timeout_id: any; + promise: LaterPromise; +}; + +export namespace find { + export type Entry = { + type: "server" | "channel" | "client" | "client_channel" | "channel_group" | "server_group"; + value: number; + id: number; } - export type PermissionRequest = PermissionRequestKeys & { - timeout_id: any; - promise: LaterPromise; - }; + export type Client = Entry & { + type: "client", - export namespace find { - export type Entry = { - type: "server" | "channel" | "client" | "client_channel" | "channel_group" | "server_group"; - value: number; - id: number; - } + client_id: number; + } - export type Client = Entry & { - type: "client", + export type Channel = Entry & { + type: "channel", - client_id: number; - } + channel_id: number; + } - export type Channel = Entry & { - type: "channel", + export type Server = Entry & { + type: "server" + } - channel_id: number; - } + export type ClientChannel = Entry & { + type: "client_channel", - export type Server = Entry & { - type: "server" - } + client_id: number; + channel_id: number; + } - export type ClientChannel = Entry & { - type: "client_channel", + export type ChannelGroup = Entry & { + type: "channel_group", - client_id: number; - channel_id: number; - } + group_id: number; + } - export type ChannelGroup = Entry & { - type: "channel_group", + export type ServerGroup = Entry & { + type: "server_group", - group_id: number; - } - - export type ServerGroup = Entry & { - type: "server_group", - - group_id: number; - } + group_id: number; } } -type RequestLists = +export type RequestLists = "requests_channel_permissions" | "requests_client_permissions" | "requests_client_channel_permissions" | "requests_playlist_permissions" | "requests_playlist_client_permissions"; -class PermissionManager extends connection.AbstractCommandHandler { + +export class PermissionManager extends AbstractCommandHandler { readonly handle: ConnectionHandler; permissionList: PermissionInfo[] = []; @@ -488,11 +142,11 @@ class PermissionManager extends connection.AbstractCommandHandler { needed_permission_change_listener: {[permission: string]:(() => any)[]} = {}; - requests_channel_permissions: permissions.PermissionRequest[] = []; - requests_client_permissions: permissions.PermissionRequest[] = []; - requests_client_channel_permissions: permissions.PermissionRequest[] = []; - requests_playlist_permissions: permissions.PermissionRequest[] = []; - requests_playlist_client_permissions: permissions.PermissionRequest[] = []; + requests_channel_permissions: PermissionRequest[] = []; + requests_client_permissions: PermissionRequest[] = []; + requests_client_channel_permissions: PermissionRequest[] = []; + requests_playlist_permissions: PermissionRequest[] = []; + requests_playlist_client_permissions: PermissionRequest[] = []; requests_permfind: { timeout_id: number, @@ -603,7 +257,7 @@ class PermissionManager extends connection.AbstractCommandHandler { this._cacheNeededPermissions = undefined; } - handle_command(command: connection.ServerCommand): boolean { + handle_command(command: ServerCommand): boolean { switch (command.command) { case "notifyclientneededpermissions": this.onNeededPermissions(command.arguments); @@ -775,7 +429,7 @@ class PermissionManager extends connection.AbstractCommandHandler { }, "success", PermissionManager.parse_permission_bulk(json, this.handle.permissions)); } - private execute_channel_permission_request(request: permissions.PermissionRequestKeys) { + private execute_channel_permission_request(request: PermissionRequestKeys) { this.handle.serverConnection.send_command("channelpermlist", {"cid": request.channel_id}).catch(error => { if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) this.fullfill_permission_request("requests_channel_permissions", request, "success", []); @@ -785,7 +439,7 @@ class PermissionManager extends connection.AbstractCommandHandler { } requestChannelPermissions(channelId: number) : Promise { - const keys: permissions.PermissionRequestKeys = { + const keys: PermissionRequestKeys = { channel_id: channelId }; return this.execute_permission_request("requests_channel_permissions", keys, this.execute_channel_permission_request.bind(this)); @@ -799,7 +453,7 @@ class PermissionManager extends connection.AbstractCommandHandler { }, "success", PermissionManager.parse_permission_bulk(json, this.handle.permissions)); } - private execute_client_permission_request(request: permissions.PermissionRequestKeys) { + private execute_client_permission_request(request: PermissionRequestKeys) { this.handle.serverConnection.send_command("clientpermlist", {cldbid: request.client_id}).catch(error => { if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) this.fullfill_permission_request("requests_client_permissions", request, "success", []); @@ -809,7 +463,7 @@ class PermissionManager extends connection.AbstractCommandHandler { } requestClientPermissions(client_id: number) : Promise { - const keys: permissions.PermissionRequestKeys = { + const keys: PermissionRequestKeys = { client_id: client_id }; return this.execute_permission_request("requests_client_permissions", keys, this.execute_client_permission_request.bind(this)); @@ -826,7 +480,7 @@ class PermissionManager extends connection.AbstractCommandHandler { }, "success", PermissionManager.parse_permission_bulk(json, this.handle.permissions)); } - private execute_client_channel_permission_request(request: permissions.PermissionRequestKeys) { + private execute_client_channel_permission_request(request: PermissionRequestKeys) { this.handle.serverConnection.send_command("channelclientpermlist", {cldbid: request.client_id, cid: request.channel_id}) .catch(error => { if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) @@ -837,7 +491,7 @@ class PermissionManager extends connection.AbstractCommandHandler { } requestClientChannelPermissions(client_id: number, channel_id: number) : Promise { - const keys: permissions.PermissionRequestKeys = { + const keys: PermissionRequestKeys = { client_id: client_id }; return this.execute_permission_request("requests_client_channel_permissions", keys, this.execute_client_channel_permission_request.bind(this)); @@ -852,7 +506,7 @@ class PermissionManager extends connection.AbstractCommandHandler { }, "success", PermissionManager.parse_permission_bulk(json, this.handle.permissions)); } - private execute_playlist_permission_request(request: permissions.PermissionRequestKeys) { + private execute_playlist_permission_request(request: PermissionRequestKeys) { this.handle.serverConnection.send_command("playlistpermlist", {playlist_id: request.playlist_id}) .catch(error => { if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) @@ -863,7 +517,7 @@ class PermissionManager extends connection.AbstractCommandHandler { } requestPlaylistPermissions(playlist_id: number) : Promise { - const keys: permissions.PermissionRequestKeys = { + const keys: PermissionRequestKeys = { playlist_id: playlist_id }; return this.execute_permission_request("requests_playlist_permissions", keys, this.execute_playlist_permission_request.bind(this)); @@ -880,7 +534,7 @@ class PermissionManager extends connection.AbstractCommandHandler { }, "success", PermissionManager.parse_permission_bulk(json, this.handle.permissions)); } - private execute_playlist_client_permission_request(request: permissions.PermissionRequestKeys) { + private execute_playlist_client_permission_request(request: PermissionRequestKeys) { this.handle.serverConnection.send_command("playlistclientpermlist", {playlist_id: request.playlist_id, cldbid: request.client_id}) .catch(error => { if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) @@ -891,7 +545,7 @@ class PermissionManager extends connection.AbstractCommandHandler { } requestPlaylistClientPermissions(playlist_id: number, client_database_id: number) : Promise { - const keys: permissions.PermissionRequestKeys = { + const keys: PermissionRequestKeys = { playlist_id: playlist_id, client_id: client_database_id }; @@ -907,8 +561,8 @@ class PermissionManager extends connection.AbstractCommandHandler { }; private execute_permission_request(list: RequestLists, - criteria: permissions.PermissionRequestKeys, - execute: (criteria: permissions.PermissionRequestKeys) => any) : Promise { + criteria: PermissionRequestKeys, + execute: (criteria: PermissionRequestKeys) => any) : Promise { for(const request of this[list]) if(this.criteria_equal(request, criteria) && request.promise.time() + 1000 < Date.now()) return request.promise; @@ -922,7 +576,7 @@ class PermissionManager extends connection.AbstractCommandHandler { return result.promise; }; - private fullfill_permission_request(list: RequestLists, criteria: permissions.PermissionRequestKeys, status: "success" | "error", result: any) { + private fullfill_permission_request(list: RequestLists, criteria: PermissionRequestKeys, status: "success" | "error", result: any) { for(const request of this[list]) { if(this.criteria_equal(request, criteria)) { this[list].remove(request); @@ -932,7 +586,7 @@ class PermissionManager extends connection.AbstractCommandHandler { } } - find_permission(...permissions: string[]) : Promise { + find_permission(...permissions: string[]) : Promise { const permission_ids = []; for(const permission of permissions) { const info = this.resolveInfo(permission); @@ -942,11 +596,11 @@ class PermissionManager extends connection.AbstractCommandHandler { } if(!permission_ids.length) return Promise.resolve([]); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const single_handler = { command: "notifypermfind", function: command => { - const result: permissions.find.Entry[] = []; + const result: find.Entry[] = []; for(const entry of command.arguments) { const perm_id = parseInt(entry["p"]); if(permission_ids.indexOf(perm_id) === -1) return; /* not our permfind result */ @@ -960,32 +614,32 @@ class PermissionManager extends connection.AbstractCommandHandler { data = { type: "server_group", group_id: parseInt(entry["id1"]), - } as permissions.find.ServerGroup; + } as find.ServerGroup; break; case 1: data = { type: "client", client_id: parseInt(entry["id2"]), - } as permissions.find.Client; + } as find.Client; break; case 2: data = { type: "channel", channel_id: parseInt(entry["id2"]), - } as permissions.find.Channel; + } as find.Channel; break; case 3: data = { type: "channel_group", group_id: parseInt(entry["id1"]), - } as permissions.find.ChannelGroup; + } as find.ChannelGroup; break; case 4: data = { type: "client_channel", client_id: parseInt(entry["id1"]), channel_id: parseInt(entry["id1"]), - } as permissions.find.ClientChannel; + } as find.ClientChannel; break; default: continue; diff --git a/shared/js/permission/PermissionType.ts b/shared/js/permission/PermissionType.ts new file mode 100644 index 00000000..99ae8d07 --- /dev/null +++ b/shared/js/permission/PermissionType.ts @@ -0,0 +1,350 @@ +export enum PermissionType { + B_SERVERINSTANCE_HELP_VIEW = "b_serverinstance_help_view", + B_SERVERINSTANCE_VERSION_VIEW = "b_serverinstance_version_view", + B_SERVERINSTANCE_INFO_VIEW = "b_serverinstance_info_view", + B_SERVERINSTANCE_VIRTUALSERVER_LIST = "b_serverinstance_virtualserver_list", + B_SERVERINSTANCE_BINDING_LIST = "b_serverinstance_binding_list", + B_SERVERINSTANCE_PERMISSION_LIST = "b_serverinstance_permission_list", + B_SERVERINSTANCE_PERMISSION_FIND = "b_serverinstance_permission_find", + B_VIRTUALSERVER_CREATE = "b_virtualserver_create", + B_VIRTUALSERVER_DELETE = "b_virtualserver_delete", + B_VIRTUALSERVER_START_ANY = "b_virtualserver_start_any", + B_VIRTUALSERVER_STOP_ANY = "b_virtualserver_stop_any", + B_VIRTUALSERVER_CHANGE_MACHINE_ID = "b_virtualserver_change_machine_id", + B_VIRTUALSERVER_CHANGE_TEMPLATE = "b_virtualserver_change_template", + B_SERVERQUERY_LOGIN = "b_serverquery_login", + B_SERVERINSTANCE_TEXTMESSAGE_SEND = "b_serverinstance_textmessage_send", + B_SERVERINSTANCE_LOG_VIEW = "b_serverinstance_log_view", + B_SERVERINSTANCE_LOG_ADD = "b_serverinstance_log_add", + B_SERVERINSTANCE_STOP = "b_serverinstance_stop", + B_SERVERINSTANCE_MODIFY_SETTINGS = "b_serverinstance_modify_settings", + B_SERVERINSTANCE_MODIFY_QUERYGROUP = "b_serverinstance_modify_querygroup", + B_SERVERINSTANCE_MODIFY_TEMPLATES = "b_serverinstance_modify_templates", + B_VIRTUALSERVER_SELECT = "b_virtualserver_select", + B_VIRTUALSERVER_SELECT_GODMODE = "b_virtualserver_select_godmode", + B_VIRTUALSERVER_INFO_VIEW = "b_virtualserver_info_view", + B_VIRTUALSERVER_CONNECTIONINFO_VIEW = "b_virtualserver_connectioninfo_view", + B_VIRTUALSERVER_CHANNEL_LIST = "b_virtualserver_channel_list", + B_VIRTUALSERVER_CHANNEL_SEARCH = "b_virtualserver_channel_search", + B_VIRTUALSERVER_CLIENT_LIST = "b_virtualserver_client_list", + B_VIRTUALSERVER_CLIENT_SEARCH = "b_virtualserver_client_search", + B_VIRTUALSERVER_CLIENT_DBLIST = "b_virtualserver_client_dblist", + B_VIRTUALSERVER_CLIENT_DBSEARCH = "b_virtualserver_client_dbsearch", + B_VIRTUALSERVER_CLIENT_DBINFO = "b_virtualserver_client_dbinfo", + B_VIRTUALSERVER_PERMISSION_FIND = "b_virtualserver_permission_find", + B_VIRTUALSERVER_CUSTOM_SEARCH = "b_virtualserver_custom_search", + B_VIRTUALSERVER_START = "b_virtualserver_start", + B_VIRTUALSERVER_STOP = "b_virtualserver_stop", + B_VIRTUALSERVER_TOKEN_LIST = "b_virtualserver_token_list", + B_VIRTUALSERVER_TOKEN_ADD = "b_virtualserver_token_add", + B_VIRTUALSERVER_TOKEN_USE = "b_virtualserver_token_use", + B_VIRTUALSERVER_TOKEN_DELETE = "b_virtualserver_token_delete", + B_VIRTUALSERVER_LOG_VIEW = "b_virtualserver_log_view", + B_VIRTUALSERVER_LOG_ADD = "b_virtualserver_log_add", + B_VIRTUALSERVER_JOIN_IGNORE_PASSWORD = "b_virtualserver_join_ignore_password", + B_VIRTUALSERVER_NOTIFY_REGISTER = "b_virtualserver_notify_register", + B_VIRTUALSERVER_NOTIFY_UNREGISTER = "b_virtualserver_notify_unregister", + B_VIRTUALSERVER_SNAPSHOT_CREATE = "b_virtualserver_snapshot_create", + B_VIRTUALSERVER_SNAPSHOT_DEPLOY = "b_virtualserver_snapshot_deploy", + B_VIRTUALSERVER_PERMISSION_RESET = "b_virtualserver_permission_reset", + B_VIRTUALSERVER_MODIFY_NAME = "b_virtualserver_modify_name", + B_VIRTUALSERVER_MODIFY_WELCOMEMESSAGE = "b_virtualserver_modify_welcomemessage", + B_VIRTUALSERVER_MODIFY_MAXCLIENTS = "b_virtualserver_modify_maxclients", + B_VIRTUALSERVER_MODIFY_RESERVED_SLOTS = "b_virtualserver_modify_reserved_slots", + B_VIRTUALSERVER_MODIFY_PASSWORD = "b_virtualserver_modify_password", + B_VIRTUALSERVER_MODIFY_DEFAULT_SERVERGROUP = "b_virtualserver_modify_default_servergroup", + B_VIRTUALSERVER_MODIFY_DEFAULT_MUSICGROUP = "b_virtualserver_modify_default_musicgroup", + B_VIRTUALSERVER_MODIFY_DEFAULT_CHANNELGROUP = "b_virtualserver_modify_default_channelgroup", + B_VIRTUALSERVER_MODIFY_DEFAULT_CHANNELADMINGROUP = "b_virtualserver_modify_default_channeladmingroup", + B_VIRTUALSERVER_MODIFY_CHANNEL_FORCED_SILENCE = "b_virtualserver_modify_channel_forced_silence", + B_VIRTUALSERVER_MODIFY_COMPLAIN = "b_virtualserver_modify_complain", + B_VIRTUALSERVER_MODIFY_ANTIFLOOD = "b_virtualserver_modify_antiflood", + B_VIRTUALSERVER_MODIFY_FT_SETTINGS = "b_virtualserver_modify_ft_settings", + B_VIRTUALSERVER_MODIFY_FT_QUOTAS = "b_virtualserver_modify_ft_quotas", + B_VIRTUALSERVER_MODIFY_HOSTMESSAGE = "b_virtualserver_modify_hostmessage", + B_VIRTUALSERVER_MODIFY_HOSTBANNER = "b_virtualserver_modify_hostbanner", + B_VIRTUALSERVER_MODIFY_HOSTBUTTON = "b_virtualserver_modify_hostbutton", + B_VIRTUALSERVER_MODIFY_PORT = "b_virtualserver_modify_port", + B_VIRTUALSERVER_MODIFY_HOST = "b_virtualserver_modify_host", + B_VIRTUALSERVER_MODIFY_DEFAULT_MESSAGES = "b_virtualserver_modify_default_messages", + B_VIRTUALSERVER_MODIFY_AUTOSTART = "b_virtualserver_modify_autostart", + B_VIRTUALSERVER_MODIFY_NEEDED_IDENTITY_SECURITY_LEVEL = "b_virtualserver_modify_needed_identity_security_level", + B_VIRTUALSERVER_MODIFY_PRIORITY_SPEAKER_DIMM_MODIFICATOR = "b_virtualserver_modify_priority_speaker_dimm_modificator", + B_VIRTUALSERVER_MODIFY_LOG_SETTINGS = "b_virtualserver_modify_log_settings", + B_VIRTUALSERVER_MODIFY_MIN_CLIENT_VERSION = "b_virtualserver_modify_min_client_version", + B_VIRTUALSERVER_MODIFY_ICON_ID = "b_virtualserver_modify_icon_id", + B_VIRTUALSERVER_MODIFY_WEBLIST = "b_virtualserver_modify_weblist", + B_VIRTUALSERVER_MODIFY_CODEC_ENCRYPTION_MODE = "b_virtualserver_modify_codec_encryption_mode", + B_VIRTUALSERVER_MODIFY_TEMPORARY_PASSWORDS = "b_virtualserver_modify_temporary_passwords", + B_VIRTUALSERVER_MODIFY_TEMPORARY_PASSWORDS_OWN = "b_virtualserver_modify_temporary_passwords_own", + B_VIRTUALSERVER_MODIFY_CHANNEL_TEMP_DELETE_DELAY_DEFAULT = "b_virtualserver_modify_channel_temp_delete_delay_default", + B_VIRTUALSERVER_MODIFY_MUSIC_BOT_LIMIT = "b_virtualserver_modify_music_bot_limit", + B_VIRTUALSERVER_MODIFY_COUNTRY_CODE = "b_virtualserver_modify_country_code", + I_CHANNEL_MIN_DEPTH = "i_channel_min_depth", + I_CHANNEL_MAX_DEPTH = "i_channel_max_depth", + B_CHANNEL_GROUP_INHERITANCE_END = "b_channel_group_inheritance_end", + I_CHANNEL_PERMISSION_MODIFY_POWER = "i_channel_permission_modify_power", + I_CHANNEL_NEEDED_PERMISSION_MODIFY_POWER = "i_channel_needed_permission_modify_power", + B_CHANNEL_INFO_VIEW = "b_channel_info_view", + B_CHANNEL_CREATE_CHILD = "b_channel_create_child", + B_CHANNEL_CREATE_PERMANENT = "b_channel_create_permanent", + B_CHANNEL_CREATE_SEMI_PERMANENT = "b_channel_create_semi_permanent", + B_CHANNEL_CREATE_TEMPORARY = "b_channel_create_temporary", + B_CHANNEL_CREATE_PRIVATE = "b_channel_create_private", + B_CHANNEL_CREATE_WITH_TOPIC = "b_channel_create_with_topic", + B_CHANNEL_CREATE_WITH_DESCRIPTION = "b_channel_create_with_description", + B_CHANNEL_CREATE_WITH_PASSWORD = "b_channel_create_with_password", + B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX8 = "b_channel_create_modify_with_codec_speex8", + B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX16 = "b_channel_create_modify_with_codec_speex16", + B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX32 = "b_channel_create_modify_with_codec_speex32", + B_CHANNEL_CREATE_MODIFY_WITH_CODEC_CELTMONO48 = "b_channel_create_modify_with_codec_celtmono48", + B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE = "b_channel_create_modify_with_codec_opusvoice", + B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC = "b_channel_create_modify_with_codec_opusmusic", + I_CHANNEL_CREATE_MODIFY_WITH_CODEC_MAXQUALITY = "i_channel_create_modify_with_codec_maxquality", + I_CHANNEL_CREATE_MODIFY_WITH_CODEC_LATENCY_FACTOR_MIN = "i_channel_create_modify_with_codec_latency_factor_min", + I_CHANNEL_CREATE_MODIFY_CONVERSATION_HISTORY_LENGTH = "i_channel_create_modify_conversation_history_length", + B_CHANNEL_CREATE_MODIFY_CONVERSATION_HISTORY_UNLIMITED = "b_channel_create_modify_conversation_history_unlimited", + B_CHANNEL_CREATE_MODIFY_CONVERSATION_PRIVATE = "b_channel_create_modify_conversation_private", + B_CHANNEL_CREATE_WITH_MAXCLIENTS = "b_channel_create_with_maxclients", + B_CHANNEL_CREATE_WITH_MAXFAMILYCLIENTS = "b_channel_create_with_maxfamilyclients", + B_CHANNEL_CREATE_WITH_SORTORDER = "b_channel_create_with_sortorder", + B_CHANNEL_CREATE_WITH_DEFAULT = "b_channel_create_with_default", + B_CHANNEL_CREATE_WITH_NEEDED_TALK_POWER = "b_channel_create_with_needed_talk_power", + B_CHANNEL_CREATE_MODIFY_WITH_FORCE_PASSWORD = "b_channel_create_modify_with_force_password", + I_CHANNEL_CREATE_MODIFY_WITH_TEMP_DELETE_DELAY = "i_channel_create_modify_with_temp_delete_delay", + B_CHANNEL_MODIFY_PARENT = "b_channel_modify_parent", + B_CHANNEL_MODIFY_MAKE_DEFAULT = "b_channel_modify_make_default", + B_CHANNEL_MODIFY_MAKE_PERMANENT = "b_channel_modify_make_permanent", + B_CHANNEL_MODIFY_MAKE_SEMI_PERMANENT = "b_channel_modify_make_semi_permanent", + B_CHANNEL_MODIFY_MAKE_TEMPORARY = "b_channel_modify_make_temporary", + B_CHANNEL_MODIFY_NAME = "b_channel_modify_name", + B_CHANNEL_MODIFY_TOPIC = "b_channel_modify_topic", + B_CHANNEL_MODIFY_DESCRIPTION = "b_channel_modify_description", + B_CHANNEL_MODIFY_PASSWORD = "b_channel_modify_password", + B_CHANNEL_MODIFY_CODEC = "b_channel_modify_codec", + B_CHANNEL_MODIFY_CODEC_QUALITY = "b_channel_modify_codec_quality", + B_CHANNEL_MODIFY_CODEC_LATENCY_FACTOR = "b_channel_modify_codec_latency_factor", + B_CHANNEL_MODIFY_MAXCLIENTS = "b_channel_modify_maxclients", + B_CHANNEL_MODIFY_MAXFAMILYCLIENTS = "b_channel_modify_maxfamilyclients", + B_CHANNEL_MODIFY_SORTORDER = "b_channel_modify_sortorder", + B_CHANNEL_MODIFY_NEEDED_TALK_POWER = "b_channel_modify_needed_talk_power", + I_CHANNEL_MODIFY_POWER = "i_channel_modify_power", + I_CHANNEL_NEEDED_MODIFY_POWER = "i_channel_needed_modify_power", + B_CHANNEL_MODIFY_MAKE_CODEC_ENCRYPTED = "b_channel_modify_make_codec_encrypted", + B_CHANNEL_MODIFY_TEMP_DELETE_DELAY = "b_channel_modify_temp_delete_delay", + B_CHANNEL_DELETE_PERMANENT = "b_channel_delete_permanent", + B_CHANNEL_DELETE_SEMI_PERMANENT = "b_channel_delete_semi_permanent", + B_CHANNEL_DELETE_TEMPORARY = "b_channel_delete_temporary", + B_CHANNEL_DELETE_FLAG_FORCE = "b_channel_delete_flag_force", + I_CHANNEL_DELETE_POWER = "i_channel_delete_power", + B_CHANNEL_CONVERSATION_MESSAGE_DELETE = "b_channel_conversation_message_delete", + I_CHANNEL_NEEDED_DELETE_POWER = "i_channel_needed_delete_power", + B_CHANNEL_JOIN_PERMANENT = "b_channel_join_permanent", + B_CHANNEL_JOIN_SEMI_PERMANENT = "b_channel_join_semi_permanent", + B_CHANNEL_JOIN_TEMPORARY = "b_channel_join_temporary", + B_CHANNEL_JOIN_IGNORE_PASSWORD = "b_channel_join_ignore_password", + B_CHANNEL_JOIN_IGNORE_MAXCLIENTS = "b_channel_join_ignore_maxclients", + B_CHANNEL_IGNORE_VIEW_POWER = "b_channel_ignore_view_power", + I_CHANNEL_JOIN_POWER = "i_channel_join_power", + I_CHANNEL_NEEDED_JOIN_POWER = "i_channel_needed_join_power", + B_CHANNEL_IGNORE_JOIN_POWER = "b_channel_ignore_join_power", + B_CHANNEL_IGNORE_DESCRIPTION_VIEW_POWER = "b_channel_ignore_description_view_power", + I_CHANNEL_VIEW_POWER = "i_channel_view_power", + I_CHANNEL_NEEDED_VIEW_POWER = "i_channel_needed_view_power", + I_CHANNEL_SUBSCRIBE_POWER = "i_channel_subscribe_power", + I_CHANNEL_NEEDED_SUBSCRIBE_POWER = "i_channel_needed_subscribe_power", + I_CHANNEL_DESCRIPTION_VIEW_POWER = "i_channel_description_view_power", + I_CHANNEL_NEEDED_DESCRIPTION_VIEW_POWER = "i_channel_needed_description_view_power", + I_ICON_ID = "i_icon_id", + I_MAX_ICON_FILESIZE = "i_max_icon_filesize", + I_MAX_PLAYLIST_SIZE = "i_max_playlist_size", + I_MAX_PLAYLISTS = "i_max_playlists", + B_ICON_MANAGE = "b_icon_manage", + B_GROUP_IS_PERMANENT = "b_group_is_permanent", + I_GROUP_AUTO_UPDATE_TYPE = "i_group_auto_update_type", + I_GROUP_AUTO_UPDATE_MAX_VALUE = "i_group_auto_update_max_value", + I_GROUP_SORT_ID = "i_group_sort_id", + I_GROUP_SHOW_NAME_IN_TREE = "i_group_show_name_in_tree", + B_VIRTUALSERVER_SERVERGROUP_CREATE = "b_virtualserver_servergroup_create", + B_VIRTUALSERVER_SERVERGROUP_LIST = "b_virtualserver_servergroup_list", + B_VIRTUALSERVER_SERVERGROUP_PERMISSION_LIST = "b_virtualserver_servergroup_permission_list", + B_VIRTUALSERVER_SERVERGROUP_CLIENT_LIST = "b_virtualserver_servergroup_client_list", + B_VIRTUALSERVER_CHANNELGROUP_CREATE = "b_virtualserver_channelgroup_create", + B_VIRTUALSERVER_CHANNELGROUP_LIST = "b_virtualserver_channelgroup_list", + B_VIRTUALSERVER_CHANNELGROUP_PERMISSION_LIST = "b_virtualserver_channelgroup_permission_list", + B_VIRTUALSERVER_CHANNELGROUP_CLIENT_LIST = "b_virtualserver_channelgroup_client_list", + B_VIRTUALSERVER_CLIENT_PERMISSION_LIST = "b_virtualserver_client_permission_list", + B_VIRTUALSERVER_CHANNEL_PERMISSION_LIST = "b_virtualserver_channel_permission_list", + B_VIRTUALSERVER_CHANNELCLIENT_PERMISSION_LIST = "b_virtualserver_channelclient_permission_list", + B_VIRTUALSERVER_PLAYLIST_PERMISSION_LIST = "b_virtualserver_playlist_permission_list", + I_SERVER_GROUP_MODIFY_POWER = "i_server_group_modify_power", + I_SERVER_GROUP_NEEDED_MODIFY_POWER = "i_server_group_needed_modify_power", + I_SERVER_GROUP_MEMBER_ADD_POWER = "i_server_group_member_add_power", + I_SERVER_GROUP_SELF_ADD_POWER = "i_server_group_self_add_power", + I_SERVER_GROUP_NEEDED_MEMBER_ADD_POWER = "i_server_group_needed_member_add_power", + I_SERVER_GROUP_MEMBER_REMOVE_POWER = "i_server_group_member_remove_power", + I_SERVER_GROUP_SELF_REMOVE_POWER = "i_server_group_self_remove_power", + I_SERVER_GROUP_NEEDED_MEMBER_REMOVE_POWER = "i_server_group_needed_member_remove_power", + I_CHANNEL_GROUP_MODIFY_POWER = "i_channel_group_modify_power", + I_CHANNEL_GROUP_NEEDED_MODIFY_POWER = "i_channel_group_needed_modify_power", + I_CHANNEL_GROUP_MEMBER_ADD_POWER = "i_channel_group_member_add_power", + I_CHANNEL_GROUP_SELF_ADD_POWER = "i_channel_group_self_add_power", + I_CHANNEL_GROUP_NEEDED_MEMBER_ADD_POWER = "i_channel_group_needed_member_add_power", + I_CHANNEL_GROUP_MEMBER_REMOVE_POWER = "i_channel_group_member_remove_power", + I_CHANNEL_GROUP_SELF_REMOVE_POWER = "i_channel_group_self_remove_power", + I_CHANNEL_GROUP_NEEDED_MEMBER_REMOVE_POWER = "i_channel_group_needed_member_remove_power", + I_GROUP_MEMBER_ADD_POWER = "i_group_member_add_power", + I_GROUP_NEEDED_MEMBER_ADD_POWER = "i_group_needed_member_add_power", + I_GROUP_MEMBER_REMOVE_POWER = "i_group_member_remove_power", + I_GROUP_NEEDED_MEMBER_REMOVE_POWER = "i_group_needed_member_remove_power", + I_GROUP_MODIFY_POWER = "i_group_modify_power", + I_GROUP_NEEDED_MODIFY_POWER = "i_group_needed_modify_power", + I_PERMISSION_MODIFY_POWER = "i_permission_modify_power", + B_PERMISSION_MODIFY_POWER_IGNORE = "b_permission_modify_power_ignore", + B_VIRTUALSERVER_SERVERGROUP_DELETE = "b_virtualserver_servergroup_delete", + B_VIRTUALSERVER_CHANNELGROUP_DELETE = "b_virtualserver_channelgroup_delete", + I_CLIENT_PERMISSION_MODIFY_POWER = "i_client_permission_modify_power", + I_CLIENT_NEEDED_PERMISSION_MODIFY_POWER = "i_client_needed_permission_modify_power", + I_CLIENT_MAX_CLONES_UID = "i_client_max_clones_uid", + I_CLIENT_MAX_CLONES_IP = "i_client_max_clones_ip", + I_CLIENT_MAX_CLONES_HWID = "i_client_max_clones_hwid", + I_CLIENT_MAX_IDLETIME = "i_client_max_idletime", + I_CLIENT_MAX_AVATAR_FILESIZE = "i_client_max_avatar_filesize", + I_CLIENT_MAX_CHANNEL_SUBSCRIPTIONS = "i_client_max_channel_subscriptions", + I_CLIENT_MAX_CHANNELS = "i_client_max_channels", + I_CLIENT_MAX_TEMPORARY_CHANNELS = "i_client_max_temporary_channels", + I_CLIENT_MAX_SEMI_CHANNELS = "i_client_max_semi_channels", + I_CLIENT_MAX_PERMANENT_CHANNELS = "i_client_max_permanent_channels", + B_CLIENT_USE_PRIORITY_SPEAKER = "b_client_use_priority_speaker", + B_CLIENT_SKIP_CHANNELGROUP_PERMISSIONS = "b_client_skip_channelgroup_permissions", + B_CLIENT_FORCE_PUSH_TO_TALK = "b_client_force_push_to_talk", + B_CLIENT_IGNORE_BANS = "b_client_ignore_bans", + B_CLIENT_IGNORE_VPN = "b_client_ignore_vpn", + B_CLIENT_IGNORE_ANTIFLOOD = "b_client_ignore_antiflood", + B_CLIENT_ENFORCE_VALID_HWID = "b_client_enforce_valid_hwid", + B_CLIENT_ALLOW_INVALID_PACKET = "b_client_allow_invalid_packet", + B_CLIENT_ALLOW_INVALID_BADGES = "b_client_allow_invalid_badges", + B_CLIENT_ISSUE_CLIENT_QUERY_COMMAND = "b_client_issue_client_query_command", + B_CLIENT_USE_RESERVED_SLOT = "b_client_use_reserved_slot", + B_CLIENT_USE_CHANNEL_COMMANDER = "b_client_use_channel_commander", + B_CLIENT_REQUEST_TALKER = "b_client_request_talker", + B_CLIENT_AVATAR_DELETE_OTHER = "b_client_avatar_delete_other", + B_CLIENT_IS_STICKY = "b_client_is_sticky", + B_CLIENT_IGNORE_STICKY = "b_client_ignore_sticky", + B_CLIENT_MUSIC_CREATE_PERMANENT = "b_client_music_create_permanent", + B_CLIENT_MUSIC_CREATE_SEMI_PERMANENT = "b_client_music_create_semi_permanent", + B_CLIENT_MUSIC_CREATE_TEMPORARY = "b_client_music_create_temporary", + B_CLIENT_MUSIC_MODIFY_PERMANENT = "b_client_music_modify_permanent", + B_CLIENT_MUSIC_MODIFY_SEMI_PERMANENT = "b_client_music_modify_semi_permanent", + B_CLIENT_MUSIC_MODIFY_TEMPORARY = "b_client_music_modify_temporary", + I_CLIENT_MUSIC_CREATE_MODIFY_MAX_VOLUME = "i_client_music_create_modify_max_volume", + I_CLIENT_MUSIC_LIMIT = "i_client_music_limit", + I_CLIENT_MUSIC_NEEDED_DELETE_POWER = "i_client_music_needed_delete_power", + I_CLIENT_MUSIC_DELETE_POWER = "i_client_music_delete_power", + I_CLIENT_MUSIC_PLAY_POWER = "i_client_music_play_power", + I_CLIENT_MUSIC_NEEDED_PLAY_POWER = "i_client_music_needed_play_power", + I_CLIENT_MUSIC_MODIFY_POWER = "i_client_music_modify_power", + I_CLIENT_MUSIC_NEEDED_MODIFY_POWER = "i_client_music_needed_modify_power", + I_CLIENT_MUSIC_RENAME_POWER = "i_client_music_rename_power", + I_CLIENT_MUSIC_NEEDED_RENAME_POWER = "i_client_music_needed_rename_power", + B_PLAYLIST_CREATE = "b_playlist_create", + I_PLAYLIST_VIEW_POWER = "i_playlist_view_power", + I_PLAYLIST_NEEDED_VIEW_POWER = "i_playlist_needed_view_power", + I_PLAYLIST_MODIFY_POWER = "i_playlist_modify_power", + I_PLAYLIST_NEEDED_MODIFY_POWER = "i_playlist_needed_modify_power", + I_PLAYLIST_PERMISSION_MODIFY_POWER = "i_playlist_permission_modify_power", + I_PLAYLIST_NEEDED_PERMISSION_MODIFY_POWER = "i_playlist_needed_permission_modify_power", + I_PLAYLIST_DELETE_POWER = "i_playlist_delete_power", + I_PLAYLIST_NEEDED_DELETE_POWER = "i_playlist_needed_delete_power", + I_PLAYLIST_SONG_ADD_POWER = "i_playlist_song_add_power", + I_PLAYLIST_SONG_NEEDED_ADD_POWER = "i_playlist_song_needed_add_power", + I_PLAYLIST_SONG_REMOVE_POWER = "i_playlist_song_remove_power", + I_PLAYLIST_SONG_NEEDED_REMOVE_POWER = "i_playlist_song_needed_remove_power", + B_CLIENT_INFO_VIEW = "b_client_info_view", + B_CLIENT_PERMISSIONOVERVIEW_VIEW = "b_client_permissionoverview_view", + B_CLIENT_PERMISSIONOVERVIEW_OWN = "b_client_permissionoverview_own", + B_CLIENT_REMOTEADDRESS_VIEW = "b_client_remoteaddress_view", + I_CLIENT_SERVERQUERY_VIEW_POWER = "i_client_serverquery_view_power", + I_CLIENT_NEEDED_SERVERQUERY_VIEW_POWER = "i_client_needed_serverquery_view_power", + B_CLIENT_CUSTOM_INFO_VIEW = "b_client_custom_info_view", + B_CLIENT_MUSIC_CHANNEL_LIST = "b_client_music_channel_list", + B_CLIENT_MUSIC_SERVER_LIST = "b_client_music_server_list", + I_CLIENT_MUSIC_INFO = "i_client_music_info", + I_CLIENT_MUSIC_NEEDED_INFO = "i_client_music_needed_info", + I_CLIENT_KICK_FROM_SERVER_POWER = "i_client_kick_from_server_power", + I_CLIENT_NEEDED_KICK_FROM_SERVER_POWER = "i_client_needed_kick_from_server_power", + I_CLIENT_KICK_FROM_CHANNEL_POWER = "i_client_kick_from_channel_power", + I_CLIENT_NEEDED_KICK_FROM_CHANNEL_POWER = "i_client_needed_kick_from_channel_power", + I_CLIENT_BAN_POWER = "i_client_ban_power", + I_CLIENT_NEEDED_BAN_POWER = "i_client_needed_ban_power", + I_CLIENT_MOVE_POWER = "i_client_move_power", + I_CLIENT_NEEDED_MOVE_POWER = "i_client_needed_move_power", + I_CLIENT_COMPLAIN_POWER = "i_client_complain_power", + I_CLIENT_NEEDED_COMPLAIN_POWER = "i_client_needed_complain_power", + B_CLIENT_COMPLAIN_LIST = "b_client_complain_list", + B_CLIENT_COMPLAIN_DELETE_OWN = "b_client_complain_delete_own", + B_CLIENT_COMPLAIN_DELETE = "b_client_complain_delete", + B_CLIENT_BAN_LIST = "b_client_ban_list", + B_CLIENT_BAN_LIST_GLOBAL = "b_client_ban_list_global", + B_CLIENT_BAN_TRIGGER_LIST = "b_client_ban_trigger_list", + B_CLIENT_BAN_CREATE = "b_client_ban_create", + B_CLIENT_BAN_CREATE_GLOBAL = "b_client_ban_create_global", + B_CLIENT_BAN_NAME = "b_client_ban_name", + B_CLIENT_BAN_IP = "b_client_ban_ip", + B_CLIENT_BAN_HWID = "b_client_ban_hwid", + B_CLIENT_BAN_EDIT = "b_client_ban_edit", + B_CLIENT_BAN_EDIT_GLOBAL = "b_client_ban_edit_global", + B_CLIENT_BAN_DELETE_OWN = "b_client_ban_delete_own", + B_CLIENT_BAN_DELETE = "b_client_ban_delete", + B_CLIENT_BAN_DELETE_OWN_GLOBAL = "b_client_ban_delete_own_global", + B_CLIENT_BAN_DELETE_GLOBAL = "b_client_ban_delete_global", + I_CLIENT_BAN_MAX_BANTIME = "i_client_ban_max_bantime", + I_CLIENT_PRIVATE_TEXTMESSAGE_POWER = "i_client_private_textmessage_power", + I_CLIENT_NEEDED_PRIVATE_TEXTMESSAGE_POWER = "i_client_needed_private_textmessage_power", + B_CLIENT_EVEN_TEXTMESSAGE_SEND = "b_client_even_textmessage_send", + B_CLIENT_SERVER_TEXTMESSAGE_SEND = "b_client_server_textmessage_send", + B_CLIENT_CHANNEL_TEXTMESSAGE_SEND = "b_client_channel_textmessage_send", + B_CLIENT_OFFLINE_TEXTMESSAGE_SEND = "b_client_offline_textmessage_send", + I_CLIENT_TALK_POWER = "i_client_talk_power", + I_CLIENT_NEEDED_TALK_POWER = "i_client_needed_talk_power", + I_CLIENT_POKE_POWER = "i_client_poke_power", + I_CLIENT_NEEDED_POKE_POWER = "i_client_needed_poke_power", + B_CLIENT_SET_FLAG_TALKER = "b_client_set_flag_talker", + I_CLIENT_WHISPER_POWER = "i_client_whisper_power", + I_CLIENT_NEEDED_WHISPER_POWER = "i_client_needed_whisper_power", + B_CLIENT_MODIFY_DESCRIPTION = "b_client_modify_description", + B_CLIENT_MODIFY_OWN_DESCRIPTION = "b_client_modify_own_description", + B_CLIENT_USE_BBCODE_ANY = "b_client_use_bbcode_any", + B_CLIENT_USE_BBCODE_URL = "b_client_use_bbcode_url", + B_CLIENT_USE_BBCODE_IMAGE = "b_client_use_bbcode_image", + B_CLIENT_MODIFY_DBPROPERTIES = "b_client_modify_dbproperties", + B_CLIENT_DELETE_DBPROPERTIES = "b_client_delete_dbproperties", + B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN = "b_client_create_modify_serverquery_login", + B_CLIENT_QUERY_CREATE = "b_client_query_create", + B_CLIENT_QUERY_LIST = "b_client_query_list", + B_CLIENT_QUERY_LIST_OWN = "b_client_query_list_own", + B_CLIENT_QUERY_RENAME = "b_client_query_rename", + B_CLIENT_QUERY_RENAME_OWN = "b_client_query_rename_own", + B_CLIENT_QUERY_CHANGE_PASSWORD = "b_client_query_change_password", + B_CLIENT_QUERY_CHANGE_OWN_PASSWORD = "b_client_query_change_own_password", + B_CLIENT_QUERY_CHANGE_PASSWORD_GLOBAL = "b_client_query_change_password_global", + B_CLIENT_QUERY_DELETE = "b_client_query_delete", + B_CLIENT_QUERY_DELETE_OWN = "b_client_query_delete_own", + B_FT_IGNORE_PASSWORD = "b_ft_ignore_password", + B_FT_TRANSFER_LIST = "b_ft_transfer_list", + I_FT_FILE_UPLOAD_POWER = "i_ft_file_upload_power", + I_FT_NEEDED_FILE_UPLOAD_POWER = "i_ft_needed_file_upload_power", + I_FT_FILE_DOWNLOAD_POWER = "i_ft_file_download_power", + I_FT_NEEDED_FILE_DOWNLOAD_POWER = "i_ft_needed_file_download_power", + I_FT_FILE_DELETE_POWER = "i_ft_file_delete_power", + I_FT_NEEDED_FILE_DELETE_POWER = "i_ft_needed_file_delete_power", + I_FT_FILE_RENAME_POWER = "i_ft_file_rename_power", + I_FT_NEEDED_FILE_RENAME_POWER = "i_ft_needed_file_rename_power", + I_FT_FILE_BROWSE_POWER = "i_ft_file_browse_power", + I_FT_NEEDED_FILE_BROWSE_POWER = "i_ft_needed_file_browse_power", + I_FT_DIRECTORY_CREATE_POWER = "i_ft_directory_create_power", + I_FT_NEEDED_DIRECTORY_CREATE_POWER = "i_ft_needed_directory_create_power", + I_FT_QUOTA_MB_DOWNLOAD_PER_CLIENT = "i_ft_quota_mb_download_per_client", + I_FT_QUOTA_MB_UPLOAD_PER_CLIENT = "i_ft_quota_mb_upload_per_client" +} +export default PermissionType; \ No newline at end of file diff --git a/shared/js/profiles/ConnectionProfile.ts b/shared/js/profiles/ConnectionProfile.ts index f30cbe54..b09649f2 100644 --- a/shared/js/profiles/ConnectionProfile.ts +++ b/shared/js/profiles/ConnectionProfile.ts @@ -1,251 +1,257 @@ -namespace profiles { - export class ConnectionProfile { - id: string; +import {decode_identity, IdentitifyType, Identity} from "tc-shared/profiles/Identity"; +import {guid} from "tc-shared/crypto/uid"; +import {TeaForumIdentity} from "tc-shared/profiles/identities/TeaForumIdentity"; +import {TeaSpeakIdentity} from "tc-shared/profiles/identities/TeamSpeakIdentity"; +import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase"; +import {HandshakeIdentityHandler} from "tc-shared/connection/HandshakeHandler"; +import {createErrorModal} from "tc-shared/ui/elements/Modal"; +import {formatMessage} from "tc-shared/ui/frames/chat"; - profile_name: string; - default_username: string; - default_password: string; +export class ConnectionProfile { + id: string; - selected_identity_type: string = "unset"; - identities: { [key: string]: identities.Identity } = {}; + profile_name: string; + default_username: string; + default_password: string; - constructor(id: string) { - this.id = id; - } + selected_identity_type: string = "unset"; + identities: { [key: string]: Identity } = {}; - connect_username(): string { - if (this.default_username && this.default_username !== "Another TeaSpeak user") - return this.default_username; + constructor(id: string) { + this.id = id; + } - let selected = this.selected_identity(); - let name = selected ? selected.fallback_name() : undefined; - return name || "Another TeaSpeak user"; - } + connect_username(): string { + if (this.default_username && this.default_username !== "Another TeaSpeak user") + return this.default_username; - selected_identity(current_type?: identities.IdentitifyType): identities.Identity { - if (!current_type) - current_type = this.selected_type(); + let selected = this.selected_identity(); + let name = selected ? selected.fallback_name() : undefined; + return name || "Another TeaSpeak user"; + } - if (current_type === undefined) - return undefined; - - if (current_type == identities.IdentitifyType.TEAFORO) { - return identities.static_forum_identity(); - } else if (current_type == identities.IdentitifyType.TEAMSPEAK || current_type == identities.IdentitifyType.NICKNAME) { - return this.identities[identities.IdentitifyType[current_type].toLowerCase()]; - } + selected_identity(current_type?: IdentitifyType): Identity { + if (!current_type) + current_type = this.selected_type(); + if (current_type === undefined) return undefined; + + if (current_type == IdentitifyType.TEAFORO) { + return TeaForumIdentity.identity(); + } else if (current_type == IdentitifyType.TEAMSPEAK || current_type == IdentitifyType.NICKNAME) { + return this.identities[IdentitifyType[current_type].toLowerCase()]; } - selected_type?(): identities.IdentitifyType { - return this.selected_identity_type ? identities.IdentitifyType[this.selected_identity_type.toUpperCase()] : undefined; - } - - set_identity(type: identities.IdentitifyType, identity: identities.Identity) { - this.identities[identities.IdentitifyType[type].toLowerCase()] = identity; - } - - spawn_identity_handshake_handler?(connection: connection.AbstractServerConnection): connection.HandshakeIdentityHandler { - const identity = this.selected_identity(); - if (!identity) - return undefined; - return identity.spawn_identity_handshake_handler(connection); - } - - encode?(): string { - const identity_data = {}; - for (const key in this.identities) - if (this.identities[key]) - identity_data[key] = this.identities[key].encode(); - - return JSON.stringify({ - version: 1, - username: this.default_username, - password: this.default_password, - profile_name: this.profile_name, - identity_type: this.selected_identity_type, - identity_data: identity_data, - id: this.id - }); - } - - valid(): boolean { - const identity = this.selected_identity(); - if (!identity || !identity.valid()) return false; - - return true; - } + return undefined; } - async function decode_profile(data): Promise { - data = JSON.parse(data); - if (data.version !== 1) - return "invalid version"; - - const result: ConnectionProfile = new ConnectionProfile(data.id); - result.default_username = data.username; - result.default_password = data.password; - result.profile_name = data.profile_name; - result.selected_identity_type = (data.identity_type || "").toLowerCase(); - - if (data.identity_data) { - for (const key in data.identity_data) { - const type = identities.IdentitifyType[key.toUpperCase() as string]; - const _data = data.identity_data[key]; - if (type == undefined) continue; - - const identity = await identities.decode_identity(type, _data); - if (identity == undefined) continue; - - result.identities[key.toLowerCase()] = identity; - } - } - - return result; + selected_type?(): IdentitifyType { + return this.selected_identity_type ? IdentitifyType[this.selected_identity_type.toUpperCase()] : undefined; } - interface ProfilesData { - version: number; - profiles: string[]; + set_identity(type: IdentitifyType, identity: Identity) { + this.identities[IdentitifyType[type].toLowerCase()] = identity; } - let available_profiles: ConnectionProfile[] = []; - - export async function load() { - available_profiles = []; - - const profiles_json = localStorage.getItem("profiles"); - let profiles_data: ProfilesData = (() => { - try { - return profiles_json ? JSON.parse(profiles_json) : {version: 0} as any; - } catch (error) { - debugger; - console.error(tr("Invalid profile json! Resetting profiles :( (%o)"), profiles_json); - createErrorModal(tr("Profile data invalid"), MessageHelper.formatMessage(tr("The profile data is invalid.{:br:}This might cause data loss."))).open(); - return {version: 0}; - } - })(); - - if (profiles_data.version === 0) { - profiles_data = { - version: 1, - profiles: [] - }; - } - if (profiles_data.version == 1) { - for (const profile_data of profiles_data.profiles) { - const profile = await decode_profile(profile_data); - if (typeof (profile) === 'string') { - console.error(tr("Failed to load profile. Reason: %s, Profile data: %s"), profile, profiles_data); - continue; - } - available_profiles.push(profile); - } - } - - if (!find_profile("default")) { //Create a default profile and teaforo profile - { - const profile = create_new_profile("default", "default"); - profile.default_password = ""; - profile.default_username = ""; - profile.profile_name = "Default Profile"; - - /* generate default identity */ - try { - const identity = await identities.TeaSpeakIdentity.generate_new(); - let active = true; - setTimeout(() => { - active = false; - }, 1000); - await identity.improve_level(8, 1, () => active); - profile.set_identity(identities.IdentitifyType.TEAMSPEAK, identity); - profile.selected_identity_type = identities.IdentitifyType[identities.IdentitifyType.TEAMSPEAK]; - } catch (error) { - createErrorModal(tr("Failed to generate default identity"), tr("Failed to generate default identity!
Please manually generate the identity within your settings => profiles")).open(); - } - } - - { /* forum identity (works only when connected to the forum) */ - const profile = create_new_profile("TeaSpeak Forum", "teaforo"); - profile.default_password = ""; - profile.default_username = ""; - profile.profile_name = "TeaSpeak Forum profile"; - - profile.set_identity(identities.IdentitifyType.TEAFORO, identities.static_forum_identity()); - profile.selected_identity_type = identities.IdentitifyType[identities.IdentitifyType.TEAFORO]; - } - - save(); - } + spawn_identity_handshake_handler?(connection: AbstractServerConnection): HandshakeIdentityHandler { + const identity = this.selected_identity(); + if (!identity) + return undefined; + return identity.spawn_identity_handshake_handler(connection); } - export function create_new_profile(name: string, id?: string): ConnectionProfile { - const profile = new ConnectionProfile(id || guid()); - profile.profile_name = name; - profile.default_username = ""; - available_profiles.push(profile); - return profile; - } + encode?(): string { + const identity_data = {}; + for (const key in this.identities) + if (this.identities[key]) + identity_data[key] = this.identities[key].encode(); - let _requires_save = false; - - export function save() { - const profiles: string[] = []; - for (const profile of available_profiles) - profiles.push(profile.encode()); - - const data = JSON.stringify({ + return JSON.stringify({ version: 1, - profiles: profiles + username: this.default_username, + password: this.default_password, + profile_name: this.profile_name, + identity_type: this.selected_identity_type, + identity_data: identity_data, + id: this.id }); - localStorage.setItem("profiles", data); } - export function mark_need_save() { - _requires_save = true; + valid(): boolean { + const identity = this.selected_identity(); + + return !!identity && identity.valid(); } +} - export function requires_save(): boolean { - return _requires_save; - } +async function decode_profile(data): Promise { + data = JSON.parse(data); + if (data.version !== 1) + return "invalid version"; - export function profiles(): ConnectionProfile[] { - return available_profiles; - } + const result: ConnectionProfile = new ConnectionProfile(data.id); + result.default_username = data.username; + result.default_password = data.password; + result.profile_name = data.profile_name; + result.selected_identity_type = (data.identity_type || "").toLowerCase(); - export function find_profile(id: string): ConnectionProfile | undefined { - for (const profile of profiles()) - if (profile.id == id) - return profile; + if (data.identity_data) { + for (const key of Object.keys(data.identity_data)) { + const type = IdentitifyType[key.toUpperCase() as string]; + const _data = data.identity_data[key]; + if (type == undefined) continue; - return undefined; - } + const identity = await decode_identity(type, _data); + if (identity == undefined) continue; - export function find_profile_by_name(name: string): ConnectionProfile | undefined { - name = name.toLowerCase(); - for (const profile of profiles()) - if ((profile.profile_name || "").toLowerCase() == name) - return profile; - - return undefined; - } - - - export function default_profile(): ConnectionProfile { - return find_profile("default"); - } - - export function set_default_profile(profile: ConnectionProfile) { - const old_default = default_profile(); - if (old_default && old_default != profile) { - old_default.id = guid(); + result.identities[key.toLowerCase()] = identity; } - profile.id = "default"; - return old_default; } - export function delete_profile(profile: ConnectionProfile) { - available_profiles.remove(profile); + return result; +} + +interface ProfilesData { + version: number; + profiles: string[]; +} + +let available_profiles: ConnectionProfile[] = []; + +export async function load() { + available_profiles = []; + + const profiles_json = localStorage.getItem("profiles"); + let profiles_data: ProfilesData = (() => { + try { + return profiles_json ? JSON.parse(profiles_json) : {version: 0} as any; + } catch (error) { + debugger; + console.error(tr("Invalid profile json! Resetting profiles :( (%o)"), profiles_json); + createErrorModal(tr("Profile data invalid"), formatMessage(tr("The profile data is invalid.{:br:}This might cause data loss."))).open(); + return {version: 0}; + } + })(); + + if (profiles_data.version === 0) { + profiles_data = { + version: 1, + profiles: [] + }; } + if (profiles_data.version == 1) { + for (const profile_data of profiles_data.profiles) { + const profile = await decode_profile(profile_data); + if (typeof (profile) === 'string') { + console.error(tr("Failed to load profile. Reason: %s, Profile data: %s"), profile, profiles_data); + continue; + } + available_profiles.push(profile); + } + } + + if (!find_profile("default")) { //Create a default profile and teaforo profile + { + const profile = create_new_profile("default", "default"); + profile.default_password = ""; + profile.default_username = ""; + profile.profile_name = "Default Profile"; + + /* generate default identity */ + try { + const identity = await TeaSpeakIdentity.generate_new(); + let active = true; + setTimeout(() => { + active = false; + }, 1000); + await identity.improve_level(8, 1, () => active); + profile.set_identity(IdentitifyType.TEAMSPEAK, identity); + profile.selected_identity_type = IdentitifyType[IdentitifyType.TEAMSPEAK]; + } catch (error) { + createErrorModal(tr("Failed to generate default identity"), tr("Failed to generate default identity!
Please manually generate the identity within your settings => profiles")).open(); + } + } + + { /* forum identity (works only when connected to the forum) */ + const profile = create_new_profile("TeaSpeak Forum", "teaforo"); + profile.default_password = ""; + profile.default_username = ""; + profile.profile_name = "TeaSpeak Forum profile"; + + profile.set_identity(IdentitifyType.TEAFORO, TeaForumIdentity.identity()); + profile.selected_identity_type = IdentitifyType[IdentitifyType.TEAFORO]; + } + + save(); + } +} + +export function create_new_profile(name: string, id?: string): ConnectionProfile { + const profile = new ConnectionProfile(id || guid()); + profile.profile_name = name; + profile.default_username = ""; + available_profiles.push(profile); + return profile; +} + +let _requires_save = false; + +export function save() { + const profiles: string[] = []; + for (const profile of available_profiles) + profiles.push(profile.encode()); + + const data = JSON.stringify({ + version: 1, + profiles: profiles + }); + localStorage.setItem("profiles", data); +} + +export function mark_need_save() { + _requires_save = true; +} + +export function requires_save(): boolean { + return _requires_save; +} + +export function profiles(): ConnectionProfile[] { + return available_profiles; +} + +export function find_profile(id: string): ConnectionProfile | undefined { + for (const profile of profiles()) + if (profile.id == id) + return profile; + + return undefined; +} + +export function find_profile_by_name(name: string): ConnectionProfile | undefined { + name = name.toLowerCase(); + for (const profile of profiles()) + if ((profile.profile_name || "").toLowerCase() == name) + return profile; + + return undefined; +} + + +export function default_profile(): ConnectionProfile { + return find_profile("default"); +} + +export function set_default_profile(profile: ConnectionProfile) { + const old_default = default_profile(); + if (old_default && old_default != profile) { + old_default.id = guid(); + } + profile.id = "default"; + return old_default; +} + +export function delete_profile(profile: ConnectionProfile) { + available_profiles.remove(profile); } \ No newline at end of file diff --git a/shared/js/profiles/Identity.ts b/shared/js/profiles/Identity.ts index c658f46d..9f79adef 100644 --- a/shared/js/profiles/Identity.ts +++ b/shared/js/profiles/Identity.ts @@ -1,110 +1,119 @@ -namespace profiles.identities { - export enum IdentitifyType { - TEAFORO, - TEAMSPEAK, - NICKNAME +import {AbstractServerConnection, ServerCommand} from "tc-shared/connection/ConnectionBase"; +import {HandshakeIdentityHandler} from "tc-shared/connection/HandshakeHandler"; +import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler"; + +export enum IdentitifyType { + TEAFORO, + TEAMSPEAK, + NICKNAME +} + +export interface Identity { + fallback_name(): string | undefined ; + uid() : string; + type() : IdentitifyType; + + valid() : boolean; + + encode?() : string; + decode(data: string) : Promise; + + spawn_identity_handshake_handler(connection: AbstractServerConnection) : HandshakeIdentityHandler; +} + +/* avoid circular dependencies here */ +export async function decode_identity(type: IdentitifyType, data: string) : Promise { + let identity: Identity; + switch (type) { + case IdentitifyType.NICKNAME: + const nidentity = require("tc-shared/profiles/identities/NameIdentity"); + identity = new nidentity.NameIdentity(); + break; + case IdentitifyType.TEAFORO: + const fidentity = require("tc-shared/profiles/identities/TeaForumIdentity"); + identity = new fidentity.TeaForumIdentity(undefined); + break; + case IdentitifyType.TEAMSPEAK: + const tidentity = require("tc-shared/profiles/identities/TeamSpeakIdentity"); + identity = new tidentity.TeaSpeakIdentity(undefined, undefined); + break; + } + if(!identity) + return undefined; + + try { + await identity.decode(data) + } catch(error) { + /* todo better error handling! */ + console.error(error); + return undefined; } - export interface Identity { - fallback_name(): string | undefined ; - uid() : string; - type() : IdentitifyType; + return identity; +} - valid() : boolean; +export function create_identity(type: IdentitifyType) { + let identity: Identity; + switch (type) { + case IdentitifyType.NICKNAME: + const nidentity = require("tc-shared/profiles/identities/NameIdentity"); + identity = new nidentity.NameIdentity(); + break; + case IdentitifyType.TEAFORO: + const fidentity = require("tc-shared/profiles/identities/TeaForumIdentity"); + identity = new fidentity.TeaForumIdentity(undefined); + break; + case IdentitifyType.TEAMSPEAK: + const tidentity = require("tc-shared/profiles/identities/TeamSpeakIdentity"); + identity = new tidentity.TeaSpeakIdentity(undefined, undefined); + break; + } + return identity; +} - encode?() : string; - decode(data: string) : Promise; +export class HandshakeCommandHandler extends AbstractCommandHandler { + readonly handle: T; - spawn_identity_handshake_handler(connection: connection.AbstractServerConnection) : connection.HandshakeIdentityHandler; + constructor(connection: AbstractServerConnection, handle: T) { + super(connection); + this.handle = handle; } - export async function decode_identity(type: IdentitifyType, data: string) : Promise { - let identity: Identity; - switch (type) { - case IdentitifyType.NICKNAME: - identity = new NameIdentity(); - break; - case IdentitifyType.TEAFORO: - identity = new TeaForumIdentity(undefined); - break; - case IdentitifyType.TEAMSPEAK: - identity = new TeaSpeakIdentity(undefined, undefined); - break; - } - if(!identity) - return undefined; - try { - await identity.decode(data) - } catch(error) { - /* todo better error handling! */ - console.error(error); - return undefined; + handle_command(command: ServerCommand): boolean { + if($.isFunction(this[command.command])) + this[command.command](command.arguments); + else if(command.command == "error") { + return false; + } else { + console.warn(tr("Received unknown command while handshaking (%o)"), command); } + return true; + } +} - return identity; +export abstract class AbstractHandshakeIdentityHandler implements HandshakeIdentityHandler { + connection: AbstractServerConnection; + + protected callbacks: ((success: boolean, message?: string) => any)[] = []; + + protected constructor(connection: AbstractServerConnection) { + this.connection = connection; } - export function create_identity(type: IdentitifyType) { - let identity: Identity; - switch (type) { - case IdentitifyType.NICKNAME: - identity = new NameIdentity(); - break; - case IdentitifyType.TEAFORO: - identity = new TeaForumIdentity(undefined); - break; - case IdentitifyType.TEAMSPEAK: - identity = new TeaSpeakIdentity(undefined, undefined); - break; - } - return identity; + register_callback(callback: (success: boolean, message?: string) => any) { + this.callbacks.push(callback); } - export class HandshakeCommandHandler extends connection.AbstractCommandHandler { - readonly handle: T; + abstract start_handshake(); - constructor(connection: connection.AbstractServerConnection, handle: T) { - super(connection); - this.handle = handle; - } - - - handle_command(command: connection.ServerCommand): boolean { - if($.isFunction(this[command.command])) - this[command.command](command.arguments); - else if(command.command == "error") { - return false; - } else { - console.warn(tr("Received unknown command while handshaking (%o)"), command); - } - return true; - } + protected trigger_success() { + for(const callback of this.callbacks) + callback(true); } - export abstract class AbstractHandshakeIdentityHandler implements connection.HandshakeIdentityHandler { - connection: connection.AbstractServerConnection; - - protected callbacks: ((success: boolean, message?: string) => any)[] = []; - - protected constructor(connection: connection.AbstractServerConnection) { - this.connection = connection; - } - - register_callback(callback: (success: boolean, message?: string) => any) { - this.callbacks.push(callback); - } - - abstract start_handshake(); - - protected trigger_success() { - for(const callback of this.callbacks) - callback(true); - } - - protected trigger_fail(message: string) { - for(const callback of this.callbacks) - callback(false, message); - } + protected trigger_fail(message: string) { + for(const callback of this.callbacks) + callback(false, message); } } \ No newline at end of file diff --git a/shared/js/profiles/identities/NameIdentity.ts b/shared/js/profiles/identities/NameIdentity.ts index 51cbefc7..c0a14aa2 100644 --- a/shared/js/profiles/identities/NameIdentity.ts +++ b/shared/js/profiles/identities/NameIdentity.ts @@ -1,88 +1,97 @@ -/// +import { + AbstractHandshakeIdentityHandler, + HandshakeCommandHandler, + IdentitifyType, + Identity +} from "tc-shared/profiles/Identity"; +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; +import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase"; +import {HandshakeIdentityHandler} from "tc-shared/connection/HandshakeHandler"; -namespace profiles.identities { - class NameHandshakeHandler extends AbstractHandshakeIdentityHandler { - readonly identity: NameIdentity; - handler: HandshakeCommandHandler; +console.error(AbstractHandshakeIdentityHandler); +class NameHandshakeHandler extends AbstractHandshakeIdentityHandler { + readonly identity: NameIdentity; + handler: HandshakeCommandHandler; - constructor(connection: connection.AbstractServerConnection, identity: profiles.identities.NameIdentity) { - super(connection); - this.identity = identity; + constructor(connection: AbstractServerConnection, identity: NameIdentity) { + super(connection); + this.identity = identity; - this.handler = new HandshakeCommandHandler(connection, this); - this.handler["handshakeidentityproof"] = () => this.trigger_fail("server requested unexpected proof"); - } - - start_handshake() { - this.connection.command_handler_boss().register_handler(this.handler); - this.connection.send_command("handshakebegin", { - intention: 0, - authentication_method: this.identity.type(), - client_nickname: this.identity.name() - }).catch(error => { - log.error(LogCategory.IDENTITIES, tr("Failed to initialize name based handshake. Error: %o"), error); - if(error instanceof CommandResult) - error = error.extra_message || error.message; - this.trigger_fail("failed to execute begin (" + error + ")"); - }).then(() => this.trigger_success()); - } - - protected trigger_fail(message: string) { - this.connection.command_handler_boss().unregister_handler(this.handler); - super.trigger_fail(message); - } - - protected trigger_success() { - this.connection.command_handler_boss().unregister_handler(this.handler); - super.trigger_success(); - } + this.handler = new HandshakeCommandHandler(connection, this); + this.handler["handshakeidentityproof"] = () => this.trigger_fail("server requested unexpected proof"); } - export class NameIdentity implements Identity { - private _name: string; + start_handshake() { + this.connection.command_handler_boss().register_handler(this.handler); + this.connection.send_command("handshakebegin", { + intention: 0, + authentication_method: this.identity.type(), + client_nickname: this.identity.name() + }).catch(error => { + log.error(LogCategory.IDENTITIES, tr("Failed to initialize name based handshake. Error: %o"), error); + if(error instanceof CommandResult) + error = error.extra_message || error.message; + this.trigger_fail("failed to execute begin (" + error + ")"); + }).then(() => this.trigger_success()); + } - constructor(name?: string) { - this._name = name; - } + protected trigger_fail(message: string) { + this.connection.command_handler_boss().unregister_handler(this.handler); + super.trigger_fail(message); + } - set_name(name: string) { this._name = name; } + protected trigger_success() { + this.connection.command_handler_boss().unregister_handler(this.handler); + super.trigger_success(); + } +} - name() : string { return this._name; } +export class NameIdentity implements Identity { + private _name: string; - fallback_name(): string | undefined { - return this._name; - } + constructor(name?: string) { + this._name = name; + } - uid(): string { - return btoa(this._name); //FIXME hash! - } + set_name(name: string) { this._name = name; } - type(): IdentitifyType { - return IdentitifyType.NICKNAME; - } + name() : string { return this._name; } - valid(): boolean { - return this._name != undefined && this._name.length >= 5; - } + fallback_name(): string | undefined { + return this._name; + } - decode(data) : Promise { - data = JSON.parse(data); - if(data.version !== 1) - throw "invalid version"; + uid(): string { + return btoa(this._name); //FIXME hash! + } - this._name = data["name"]; - return; - } + type(): IdentitifyType { + return IdentitifyType.NICKNAME; + } - encode?() : string { - return JSON.stringify({ - version: 1, - name: this._name - }); - } + valid(): boolean { + return this._name != undefined && this._name.length >= 5; + } - spawn_identity_handshake_handler(connection: connection.AbstractServerConnection) : connection.HandshakeIdentityHandler { - return new NameHandshakeHandler(connection, this); - } + decode(data) : Promise { + data = JSON.parse(data); + if(data.version !== 1) + throw "invalid version"; + + this._name = data["name"]; + return; + } + + encode?() : string { + return JSON.stringify({ + version: 1, + name: this._name + }); + } + + spawn_identity_handshake_handler(connection: AbstractServerConnection) : HandshakeIdentityHandler { + return new NameHandshakeHandler(connection, this); } } \ No newline at end of file diff --git a/shared/js/profiles/identities/TeaForumIdentity.ts b/shared/js/profiles/identities/TeaForumIdentity.ts index 2f44e31e..0e1ebb51 100644 --- a/shared/js/profiles/identities/TeaForumIdentity.ts +++ b/shared/js/profiles/identities/TeaForumIdentity.ts @@ -1,122 +1,135 @@ -/// +import { + AbstractHandshakeIdentityHandler, + HandshakeCommandHandler, + IdentitifyType, + Identity +} from "tc-shared/profiles/Identity"; +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; +import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase"; +import {HandshakeIdentityHandler} from "tc-shared/connection/HandshakeHandler"; +import * as forum from "./teaspeak-forum"; -namespace profiles.identities { - class TeaForumHandshakeHandler extends AbstractHandshakeIdentityHandler { - readonly identity: TeaForumIdentity; - handler: HandshakeCommandHandler; +class TeaForumHandshakeHandler extends AbstractHandshakeIdentityHandler { + readonly identity: TeaForumIdentity; + handler: HandshakeCommandHandler; - constructor(connection: connection.AbstractServerConnection, identity: profiles.identities.TeaForumIdentity) { - super(connection); - this.identity = identity; - this.handler = new HandshakeCommandHandler(connection, this); - this.handler["handshakeidentityproof"] = this.handle_proof.bind(this); - } - - start_handshake() { - this.connection.command_handler_boss().register_handler(this.handler); - this.connection.send_command("handshakebegin", { - intention: 0, - authentication_method: this.identity.type(), - data: this.identity.data().data_json() - }).catch(error => { - log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeaForum based handshake. Error: %o"), error); - - if(error instanceof CommandResult) - error = error.extra_message || error.message; - this.trigger_fail("failed to execute begin (" + error + ")"); - }); - } - - - private handle_proof(json) { - this.connection.send_command("handshakeindentityproof", { - proof: this.identity.data().data_sign() - }).catch(error => { - log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error); - - if(error instanceof CommandResult) - error = error.extra_message || error.message; - this.trigger_fail("failed to execute proof (" + error + ")"); - }).then(() => this.trigger_success()); - } - - protected trigger_fail(message: string) { - this.connection.command_handler_boss().unregister_handler(this.handler); - super.trigger_fail(message); - } - - protected trigger_success() { - this.connection.command_handler_boss().unregister_handler(this.handler); - super.trigger_success(); - } + constructor(connection: AbstractServerConnection, identity: TeaForumIdentity) { + super(connection); + this.identity = identity; + this.handler = new HandshakeCommandHandler(connection, this); + this.handler["handshakeidentityproof"] = this.handle_proof.bind(this); } - export class TeaForumIdentity implements Identity { - private readonly identity_data: forum.Data; + start_handshake() { + this.connection.command_handler_boss().register_handler(this.handler); + this.connection.send_command("handshakebegin", { + intention: 0, + authentication_method: this.identity.type(), + data: this.identity.data().data_json() + }).catch(error => { + log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeaForum based handshake. Error: %o"), error); - valid() : boolean { - return !!this.identity_data && !this.identity_data.is_expired(); - } - - constructor(data: forum.Data) { - this.identity_data = data; - } - - data() : forum.Data { - return this.identity_data; - } - - decode(data) : Promise { - data = JSON.parse(data); - if(data.version !== 1) - throw "invalid version"; - - return; - } - - encode() : string { - return JSON.stringify({ - version: 1 - }); - } - - spawn_identity_handshake_handler(connection: connection.AbstractServerConnection) : connection.HandshakeIdentityHandler { - return new TeaForumHandshakeHandler(connection, this); - } - - fallback_name(): string | undefined { - return this.identity_data ? this.identity_data.name() : undefined; - } - - type(): profiles.identities.IdentitifyType { - return IdentitifyType.TEAFORO; - } - - uid(): string { - //FIXME: Real UID! - return "TeaForo#" + ((this.identity_data ? this.identity_data.name() : "Another TeaSpeak user")); - } + if(error instanceof CommandResult) + error = error.extra_message || error.message; + this.trigger_fail("failed to execute begin (" + error + ")"); + }); } - let static_identity: TeaForumIdentity; - export function set_static_identity(identity: TeaForumIdentity) { - static_identity = identity; + private handle_proof(json) { + this.connection.send_command("handshakeindentityproof", { + proof: this.identity.data().data_sign() + }).catch(error => { + log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error); + + if(error instanceof CommandResult) + error = error.extra_message || error.message; + this.trigger_fail("failed to execute proof (" + error + ")"); + }).then(() => this.trigger_success()); } - export function update_forum() { - if(forum.logged_in() && (!static_identity || static_identity.data() !== forum.data())) { - static_identity = new TeaForumIdentity(forum.data()); - } else { - static_identity = undefined; - } + protected trigger_fail(message: string) { + this.connection.command_handler_boss().unregister_handler(this.handler); + super.trigger_fail(message); } - export function valid_static_forum_identity() : boolean { - return static_identity && static_identity.valid(); + protected trigger_success() { + this.connection.command_handler_boss().unregister_handler(this.handler); + super.trigger_success(); + } +} + +export class TeaForumIdentity implements Identity { + private readonly identity_data: forum.Data; + + valid() : boolean { + return !!this.identity_data && !this.identity_data.is_expired(); } - export function static_forum_identity() : TeaForumIdentity | undefined { + constructor(data: forum.Data) { + this.identity_data = data; + } + + data() { + return this.identity_data; + } + + decode(data) : Promise { + data = JSON.parse(data); + if(data.version !== 1) + throw "invalid version"; + + return; + } + + encode() : string { + return JSON.stringify({ + version: 1 + }); + } + + spawn_identity_handshake_handler(connection: AbstractServerConnection) : HandshakeIdentityHandler { + return new TeaForumHandshakeHandler(connection, this); + } + + fallback_name(): string | undefined { + return this.identity_data ? this.identity_data.name() : undefined; + } + + type(): IdentitifyType { + return IdentitifyType.TEAFORO; + } + + uid(): string { + //FIXME: Real UID! + return "TeaForo#" + ((this.identity_data ? this.identity_data.name() : "Another TeaSpeak user")); + } + + public static identity() { return static_identity; } +} + +let static_identity: TeaForumIdentity; + +export function set_static_identity(identity: TeaForumIdentity) { + static_identity = identity; +} + +export function update_forum() { + if(forum.logged_in() && (!static_identity || static_identity.data() !== forum.data())) { + static_identity = new TeaForumIdentity(forum.data()); + } else { + static_identity = undefined; + } +} + +export function valid_static_forum_identity() : boolean { + return static_identity && static_identity.valid(); +} + +export function static_forum_identity() : TeaForumIdentity | undefined { + return static_identity; } \ No newline at end of file diff --git a/shared/js/profiles/identities/TeamSpeakIdentity.ts b/shared/js/profiles/identities/TeamSpeakIdentity.ts index b7c5be9c..9c70f1f0 100644 --- a/shared/js/profiles/identities/TeamSpeakIdentity.ts +++ b/shared/js/profiles/identities/TeamSpeakIdentity.ts @@ -1,88 +1,121 @@ -/// +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import * as asn1 from "tc-shared/crypto/asn1"; +import * as sha from "tc-shared/crypto/sha"; -namespace profiles.identities { - export namespace CryptoHelper { - export function base64_url_encode(str){ - return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, ''); +import { + AbstractHandshakeIdentityHandler, + HandshakeCommandHandler, + IdentitifyType, + Identity +} from "tc-shared/profiles/Identity"; +import {settings} from "tc-shared/settings"; +import {arrayBufferBase64, base64_encode_ab, str2ab8} from "tc-shared/utils/buffers"; +import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase"; +import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; +import {HandshakeIdentityHandler} from "tc-shared/connection/HandshakeHandler"; + +export namespace CryptoHelper { + export function base64_url_encode(str){ + return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + } + + export function base64_url_decode(str: string, pad?: boolean){ + if(typeof(pad) === 'undefined' || pad) + str = (str + '===').slice(0, str.length + (str.length % 4)); + return str.replace(/-/g, '+').replace(/_/g, '/'); + } + + export function arraybuffer_to_string(buf) { + return String.fromCharCode.apply(null, new Uint16Array(buf)); + } + + export async function export_ecc_key(crypto_key: CryptoKey, public_key: boolean) { + /* + Tomcrypt public key export: + if (type == PK_PRIVATE) { + flags[0] = 1; + err = der_encode_sequence_multi(out, outlen, + LTC_ASN1_BIT_STRING, 1UL, flags, + LTC_ASN1_SHORT_INTEGER, 1UL, &key_size, + LTC_ASN1_INTEGER, 1UL, key->pubkey.x, + LTC_ASN1_INTEGER, 1UL, key->pubkey.y, + LTC_ASN1_INTEGER, 1UL, key->k, + LTC_ASN1_EOL, 0UL, NULL); + } else { + flags[0] = 0; + err = der_encode_sequence_multi(out, outlen, + LTC_ASN1_BIT_STRING, 1UL, flags, + LTC_ASN1_SHORT_INTEGER, 1UL, &key_size, + LTC_ASN1_INTEGER, 1UL, key->pubkey.x, + LTC_ASN1_INTEGER, 1UL, key->pubkey.y, + LTC_ASN1_EOL, 0UL, NULL); + } + + */ + + const key_data = await crypto.subtle.exportKey("jwk", crypto_key); + + let index = 0; + const length = public_key ? 79 : 114; /* max lengths! Depends on the padding could be less */ + const buffer = new Uint8Array(length); /* fixed ASN1 length */ + { /* the initial sequence */ + buffer[index++] = 0x30; /* type */ + buffer[index++] = 0x00; /* we will set the sequence length later */ + } + { /* the flags bit string */ + buffer[index++] = 0x03; /* type */ + buffer[index++] = 0x02; /* length */ + buffer[index++] = 0x07; /* data */ + buffer[index++] = public_key ? 0x00 : 0x80; /* flag 1 or 0 (1 = private key)*/ + } + { /* key size (const 32 for P-256) */ + buffer[index++] = 0x02; /* type */ + buffer[index++] = 0x01; /* length */ + buffer[index++] = 0x20; + } + try { /* Public kex X */ + buffer[index++] = 0x02; /* type */ + buffer[index++] = 0x20; /* length */ + + const raw = atob(base64_url_decode(key_data.x, false)); + if(raw.charCodeAt(0) > 0x7F) { + buffer[index - 1] += 1; + buffer[index++] = 0; + } + + for(let i = 0; i < 32; i++) + buffer[index++] = raw.charCodeAt(i); + } catch(error) { + if(error instanceof DOMException) + throw "failed to parse x coordinate (invalid base64)"; + throw error; } - export function base64_url_decode(str: string, pad?: boolean){ - if(typeof(pad) === 'undefined' || pad) - str = (str + '===').slice(0, str.length + (str.length % 4)); - return str.replace(/-/g, '+').replace(/_/g, '/'); + try { /* Public kex Y */ + buffer[index++] = 0x02; /* type */ + buffer[index++] = 0x20; /* length */ + + const raw = atob(base64_url_decode(key_data.y, false)); + if(raw.charCodeAt(0) > 0x7F) { + buffer[index - 1] += 1; + buffer[index++] = 0; + } + + for(let i = 0; i < 32; i++) + buffer[index++] = raw.charCodeAt(i); + } catch(error) { + if(error instanceof DOMException) + throw "failed to parse y coordinate (invalid base64)"; + throw error; } - export function arraybuffer_to_string(buf) { - return String.fromCharCode.apply(null, new Uint16Array(buf)); - } - - export async function export_ecc_key(crypto_key: CryptoKey, public_key: boolean) { - /* - Tomcrypt public key export: - if (type == PK_PRIVATE) { - flags[0] = 1; - err = der_encode_sequence_multi(out, outlen, - LTC_ASN1_BIT_STRING, 1UL, flags, - LTC_ASN1_SHORT_INTEGER, 1UL, &key_size, - LTC_ASN1_INTEGER, 1UL, key->pubkey.x, - LTC_ASN1_INTEGER, 1UL, key->pubkey.y, - LTC_ASN1_INTEGER, 1UL, key->k, - LTC_ASN1_EOL, 0UL, NULL); - } else { - flags[0] = 0; - err = der_encode_sequence_multi(out, outlen, - LTC_ASN1_BIT_STRING, 1UL, flags, - LTC_ASN1_SHORT_INTEGER, 1UL, &key_size, - LTC_ASN1_INTEGER, 1UL, key->pubkey.x, - LTC_ASN1_INTEGER, 1UL, key->pubkey.y, - LTC_ASN1_EOL, 0UL, NULL); - } - - */ - - const key_data = await crypto.subtle.exportKey("jwk", crypto_key); - - let index = 0; - const length = public_key ? 79 : 114; /* max lengths! Depends on the padding could be less */ - const buffer = new Uint8Array(length); /* fixed ASN1 length */ - { /* the initial sequence */ - buffer[index++] = 0x30; /* type */ - buffer[index++] = 0x00; /* we will set the sequence length later */ - } - { /* the flags bit string */ - buffer[index++] = 0x03; /* type */ - buffer[index++] = 0x02; /* length */ - buffer[index++] = 0x07; /* data */ - buffer[index++] = public_key ? 0x00 : 0x80; /* flag 1 or 0 (1 = private key)*/ - } - { /* key size (const 32 for P-256) */ - buffer[index++] = 0x02; /* type */ - buffer[index++] = 0x01; /* length */ - buffer[index++] = 0x20; - } - try { /* Public kex X */ + if(!public_key) { + try { /* Public kex K */ buffer[index++] = 0x02; /* type */ buffer[index++] = 0x20; /* length */ - const raw = atob(base64_url_decode(key_data.x, false)); - if(raw.charCodeAt(0) > 0x7F) { - buffer[index - 1] += 1; - buffer[index++] = 0; - } - - for(let i = 0; i < 32; i++) - buffer[index++] = raw.charCodeAt(i); - } catch(error) { - if(error instanceof DOMException) - throw "failed to parse x coordinate (invalid base64)"; - throw error; - } - - try { /* Public kex Y */ - buffer[index++] = 0x02; /* type */ - buffer[index++] = 0x20; /* length */ - - const raw = atob(base64_url_decode(key_data.y, false)); + const raw = atob(base64_url_decode(key_data.d, false)); if(raw.charCodeAt(0) > 0x7F) { buffer[index - 1] += 1; buffer[index++] = 0; @@ -95,777 +128,751 @@ namespace profiles.identities { throw "failed to parse y coordinate (invalid base64)"; throw error; } - - if(!public_key) { - try { /* Public kex K */ - buffer[index++] = 0x02; /* type */ - buffer[index++] = 0x20; /* length */ - - const raw = atob(base64_url_decode(key_data.d, false)); - if(raw.charCodeAt(0) > 0x7F) { - buffer[index - 1] += 1; - buffer[index++] = 0; - } - - for(let i = 0; i < 32; i++) - buffer[index++] = raw.charCodeAt(i); - } catch(error) { - if(error instanceof DOMException) - throw "failed to parse y coordinate (invalid base64)"; - throw error; - } - } - - buffer[1] = index - 2; /* set the final sequence length */ - - return base64_encode_ab(buffer.buffer.slice(0, index)); } - const crypt_key = "b9dfaa7bee6ac57ac7b65f1094a1c155e747327bc2fe5d51c512023fe54a280201004e90ad1daaae1075d53b7d571c30e063b5a62a4a017bb394833aa0983e6e"; - function c_strlen(buffer: Uint8Array, offset: number) : number { - let index = 0; - while(index + offset < buffer.length && buffer[index + offset] != 0) - index++; - return index; - } + buffer[1] = index - 2; /* set the final sequence length */ - export async function decrypt_ts_identity(buffer: Uint8Array) : Promise { - /* buffer could contains a zero! */ - const hash = new Uint8Array(await sha.sha1(buffer.buffer.slice(20, 20 + c_strlen(buffer, 20)))); - for(let i = 0; i < 20; i++) - buffer[i] ^= hash[i]; - - const length = Math.min(buffer.length, 100); - for(let i = 0; i < length; i++) - buffer[i] ^= crypt_key.charCodeAt(i); - - return arraybuffer_to_string(buffer); - } - - export async function encrypt_ts_identity(buffer: Uint8Array) : Promise { - const length = Math.min(buffer.length, 100); - for(let i = 0; i < length; i++) - buffer[i] ^= crypt_key.charCodeAt(i); - - const hash = new Uint8Array(await sha.sha1(buffer.buffer.slice(20, 20 + c_strlen(buffer, 20)))); - for(let i = 0; i < 20; i++) - buffer[i] ^= hash[i]; - - return base64_encode_ab(buffer); - } - - /** - * @param buffer base64 encoded ASN.1 string - */ - export function decode_tomcrypt_key(buffer: string) { - let decoded; - - try { - decoded = asn1.decode(atob(buffer)); - } catch(error) { - if(error instanceof DOMException) - throw "failed to parse key buffer (invalid base64)"; - throw error; - } - - let {x, y, k} = { - x: decoded.children[2].content(Infinity, asn1.TagType.VisibleString), - y: decoded.children[3].content(Infinity, asn1.TagType.VisibleString), - k: decoded.children[4].content(Infinity, asn1.TagType.VisibleString) - }; - - if(x.length > 32) { - if(x.charCodeAt(0) != 0) - throw "Invalid X coordinate! (Too long)"; - x = x.substr(1); - } - - if(y.length > 32) { - if(y.charCodeAt(0) != 0) - throw "Invalid Y coordinate! (Too long)"; - y = y.substr(1); - } - - if(k.length > 32) { - if(k.charCodeAt(0) != 0) - throw "Invalid private coordinate! (Too long)"; - k = k.substr(1); - } - - /* - console.log("Key x: %s (%d)", btoa(x), x.length); - console.log("Key y: %s (%d)", btoa(y), y.length); - console.log("Key k: %s (%d)", btoa(k), k.length); - */ - return { - crv: "P-256", - d: base64_url_encode(btoa(k)), - x: base64_url_encode(btoa(x)), - y: base64_url_encode(btoa(y)), - - ext: true, - key_ops:["deriveKey", "sign"], - kty:"EC", - }; - } + return base64_encode_ab(buffer.buffer.slice(0, index)); } - class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler { - identity: TeaSpeakIdentity; - handler: HandshakeCommandHandler; + const crypt_key = "b9dfaa7bee6ac57ac7b65f1094a1c155e747327bc2fe5d51c512023fe54a280201004e90ad1daaae1075d53b7d571c30e063b5a62a4a017bb394833aa0983e6e"; + function c_strlen(buffer: Uint8Array, offset: number) : number { + let index = 0; + while(index + offset < buffer.length && buffer[index + offset] != 0) + index++; + return index; + } - constructor(connection: connection.AbstractServerConnection, identity: TeaSpeakIdentity) { - super(connection); - this.identity = identity; - this.handler = new HandshakeCommandHandler(connection, this); - this.handler["handshakeidentityproof"] = this.handle_proof.bind(this); + export async function decrypt_ts_identity(buffer: Uint8Array) : Promise { + /* buffer could contains a zero! */ + const hash = new Uint8Array(await sha.sha1(buffer.buffer.slice(20, 20 + c_strlen(buffer, 20)))); + for(let i = 0; i < 20; i++) + buffer[i] ^= hash[i]; + + const length = Math.min(buffer.length, 100); + for(let i = 0; i < length; i++) + buffer[i] ^= crypt_key.charCodeAt(i); + + return arraybuffer_to_string(buffer); + } + + export async function encrypt_ts_identity(buffer: Uint8Array) : Promise { + const length = Math.min(buffer.length, 100); + for(let i = 0; i < length; i++) + buffer[i] ^= crypt_key.charCodeAt(i); + + const hash = new Uint8Array(await sha.sha1(buffer.buffer.slice(20, 20 + c_strlen(buffer, 20)))); + for(let i = 0; i < 20; i++) + buffer[i] ^= hash[i]; + + return base64_encode_ab(buffer); + } + + /** + * @param buffer base64 encoded ASN.1 string + */ + export function decode_tomcrypt_key(buffer: string) { + let decoded; + + try { + decoded = asn1.decode(atob(buffer)); + } catch(error) { + if(error instanceof DOMException) + throw "failed to parse key buffer (invalid base64)"; + throw error; } - start_handshake() { - this.connection.command_handler_boss().register_handler(this.handler); - this.connection.send_command("handshakebegin", { - intention: 0, - authentication_method: this.identity.type(), - publicKey: this.identity.public_key - }).catch(error => { - log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeamSpeak based handshake. Error: %o"), error); + let {x, y, k} = { + x: decoded.children[2].content(Infinity, asn1.TagType.VisibleString), + y: decoded.children[3].content(Infinity, asn1.TagType.VisibleString), + k: decoded.children[4].content(Infinity, asn1.TagType.VisibleString) + }; + + if(x.length > 32) { + if(x.charCodeAt(0) != 0) + throw "Invalid X coordinate! (Too long)"; + x = x.substr(1); + } + + if(y.length > 32) { + if(y.charCodeAt(0) != 0) + throw "Invalid Y coordinate! (Too long)"; + y = y.substr(1); + } + + if(k.length > 32) { + if(k.charCodeAt(0) != 0) + throw "Invalid private coordinate! (Too long)"; + k = k.substr(1); + } + + return { + crv: "P-256", + d: base64_url_encode(btoa(k)), + x: base64_url_encode(btoa(x)), + y: base64_url_encode(btoa(y)), + + ext: true, + key_ops:["deriveKey", "sign"], + kty:"EC", + }; + } +} + +class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler { + identity: TeaSpeakIdentity; + handler: HandshakeCommandHandler; + + constructor(connection: AbstractServerConnection, identity: TeaSpeakIdentity) { + super(connection); + this.identity = identity; + this.handler = new HandshakeCommandHandler(connection, this); + this.handler["handshakeidentityproof"] = this.handle_proof.bind(this); + } + + start_handshake() { + this.connection.command_handler_boss().register_handler(this.handler); + this.connection.send_command("handshakebegin", { + intention: 0, + authentication_method: this.identity.type(), + publicKey: this.identity.public_key + }).catch(error => { + log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeamSpeak based handshake. Error: %o"), error); + + if(error instanceof CommandResult) + error = error.extra_message || error.message; + this.trigger_fail("failed to execute begin (" + error + ")"); + }); + } + + private handle_proof(json) { + if(!json[0]["digest"]) { + this.trigger_fail("server too old"); + return; + } + + this.identity.sign_message(json[0]["message"], json[0]["digest"]).then(proof => { + this.connection.send_command("handshakeindentityproof", {proof: proof}).catch(error => { + log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error); if(error instanceof CommandResult) error = error.extra_message || error.message; - this.trigger_fail("failed to execute begin (" + error + ")"); - }); - } - - private handle_proof(json) { - if(!json[0]["digest"]) { - this.trigger_fail("server too old"); - return; - } - - this.identity.sign_message(json[0]["message"], json[0]["digest"]).then(proof => { - this.connection.send_command("handshakeindentityproof", {proof: proof}).catch(error => { - log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error); - - if(error instanceof CommandResult) - error = error.extra_message || error.message; - this.trigger_fail("failed to execute proof (" + error + ")"); - }).then(() => this.trigger_success()); - }).catch(error => { - this.trigger_fail("failed to sign message"); - }); - } - - protected trigger_fail(message: string) { - this.connection.command_handler_boss().unregister_handler(this.handler); - super.trigger_fail(message); - } - - protected trigger_success() { - this.connection.command_handler_boss().unregister_handler(this.handler); - super.trigger_success(); - } + this.trigger_fail("failed to execute proof (" + error + ")"); + }).then(() => this.trigger_success()); + }).catch(error => { + this.trigger_fail("failed to sign message"); + }); } - class IdentityPOWWorker { - private _worker: Worker; - private _current_hash: string; - private _best_level: number; - - async initialize(key: string) { - this._worker = new Worker(settings.static("worker_directory", "js/workers/") + "WorkerPOW.js"); - - /* initialize */ - await new Promise((resolve, reject) => { - const timeout_id = setTimeout(() => reject("timeout"), 1000); - - this._worker.onmessage = event => { - clearTimeout(timeout_id); - - if(!event.data) { - reject("invalid data"); - return; - } - - if(!event.data.success) { - reject("initialize failed (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")"); - return; - } - - this._worker.onmessage = event => this.handle_message(event.data); - resolve(); - }; - this._worker.onerror = event => { - log.error(LogCategory.IDENTITIES, tr("POW Worker error %o"), event); - clearTimeout(timeout_id); - reject("Failed to load worker (" + event.message + ")"); - }; - }); - - /* set data */ - await new Promise((resolve, reject) => { - this._worker.postMessage({ - type: "set_data", - private_key: key, - code: "set_data" - }); - - const timeout_id = setTimeout(() => reject("timeout (data)"), 1000); - - this._worker.onmessage = event => { - clearTimeout(timeout_id); - - if (!event.data) { - reject("invalid data"); - return; - } - - if (!event.data.success) { - reject("initialize of data failed (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")"); - return; - } - - this._worker.onmessage = event => this.handle_message(event.data); - resolve(); - }; - }); - } - - async mine(hash: string, iterations: number, target: number, timeout?: number) : Promise { - this._current_hash = hash; - if(target < this._best_level) - return true; - - return await new Promise((resolve, reject) => { - this._worker.postMessage({ - type: "mine", - hash: this._current_hash, - iterations: iterations, - target: target, - code: "mine" - }); - - const timeout_id = setTimeout(() => reject("timeout (mine)"), timeout || 5000); - - this._worker.onmessage = event => { - this._worker.onmessage = event => this.handle_message(event.data); - - clearTimeout(timeout_id); - if (!event.data) { - reject("invalid data"); - return; - } - - if (!event.data.success) { - reject("mining failed (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")"); - return; - } - - if(event.data.result) { - this._best_level = event.data.level; - this._current_hash = event.data.hash; - resolve(true); - } else { - resolve(false); /* no result */ - } - }; - }); - } - - current_hash() : string { - return this._current_hash; - } - - current_level() : number { - return this._best_level; - } - - async finalize(timeout?: number) { - try { - await new Promise((resolve, reject) => { - this._worker.postMessage({ - type: "finalize", - code: "finalize" - }); - - const timeout_id = setTimeout(() => reject("timeout"), timeout || 250); - - this._worker.onmessage = event => { - this._worker.onmessage = event => this.handle_message(event.data); - - clearTimeout(timeout_id); - - if (!event.data) { - reject("invalid data"); - return; - } - - if (!event.data.success) { - reject("failed to finalize (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")"); - return; - } - - resolve(); - }; - }); - } catch(error) { - log.error(LogCategory.IDENTITIES, tr("Failed to finalize POW worker! (%o)"), error); - } - - this._worker.terminate(); - this._worker = undefined; - } - - private handle_message(message: any) { - log.info(LogCategory.IDENTITIES, tr("Received message: %o"), message); - } + protected trigger_fail(message: string) { + this.connection.command_handler_boss().unregister_handler(this.handler); + super.trigger_fail(message); } - export class TeaSpeakIdentity implements Identity { - static async generate_new() : Promise { - let key: CryptoKeyPair; - try { - key = await crypto.subtle.generateKey({name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]); - } catch(e) { - log.error(LogCategory.IDENTITIES, tr("Could not generate a new key: %o"), e); - throw "Failed to generate keypair"; - } - const private_key = await CryptoHelper.export_ecc_key(key.privateKey, false); + protected trigger_success() { + this.connection.command_handler_boss().unregister_handler(this.handler); + super.trigger_success(); + } +} - const identity = new TeaSpeakIdentity(private_key, "0", undefined, false); - await identity.initialize(); - return identity; - } +class IdentityPOWWorker { + private _worker: Worker; + private _current_hash: string; + private _best_level: number; - static async import_ts(ts_string: string, ini?: boolean) : Promise { - const parse_string = string => { - /* parsing without INI structure */ - const V_index = string.indexOf('V'); - if(V_index == -1) throw "invalid input (missing V)"; + async initialize(key: string) { + this._worker = new Worker(settings.static("worker_directory", "js/workers/") + "WorkerPOW.js"); - return { - hash: string.substr(0, V_index), - data: string.substr(V_index + 1), - name: "TeaSpeak user" + /* initialize */ + await new Promise((resolve, reject) => { + const timeout_id = setTimeout(() => reject("timeout"), 1000); + + this._worker.onmessage = event => { + clearTimeout(timeout_id); + + if(!event.data) { + reject("invalid data"); + return; } + + if(!event.data.success) { + reject("initialize failed (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")"); + return; + } + + this._worker.onmessage = event => this.handle_message(event.data); + resolve(); }; + this._worker.onerror = event => { + log.error(LogCategory.IDENTITIES, tr("POW Worker error %o"), event); + clearTimeout(timeout_id); + reject("Failed to load worker (" + event.message + ")"); + }; + }); - const {hash, data, name} = (!ini ? () => parse_string(ts_string) : () => { - /* parsing with INI structure */ - let identity: string, name: string; - - for(const line of ts_string.split("\n")) { - if(line.startsWith("identity=")) - identity = line.substr(9); - else if(line.startsWith("nickname=")) - name = line.substr(9); - } - - if(!identity) throw "missing identity keyword"; - identity = identity.match(/^"?([0-9]+V[0-9a-zA-Z+\/]+[=]+)"?$/)[1]; - if(!identity) throw "invalid identity key value"; - - const result = parse_string(identity); - result.name = name || result.name; - return result; - })(); - - if(!ts_string.match(/[0-9]+/g)) throw "invalid hash!"; - - let buffer; - try { - buffer = new Uint8Array(arrayBufferBase64(data)); - } catch(error) { - log.error(LogCategory.IDENTITIES, tr("Failed to decode given base64 data (%s)"), data); - throw "failed to base data (base64 decode failed)"; - } - const key64 = await CryptoHelper.decrypt_ts_identity(new Uint8Array(arrayBufferBase64(data))); - - const identity = new TeaSpeakIdentity(key64, hash, name, false); - await identity.initialize(); - return identity; - } - - hash_number: string; /* hash suffix for the private key */ - private_key: string; /* base64 representation of the private key */ - _name: string; - - public_key: string; /* only set when initialized */ - - private _initialized: boolean; - private _crypto_key: CryptoKey; - private _crypto_key_sign: CryptoKey; - - private _unique_id: string; - - constructor(private_key?: string, hash?: string, name?: string, initialize?: boolean) { - this.private_key = private_key; - this.hash_number = hash || "0"; - this._name = name; - - if(this.private_key && (typeof(initialize) === "undefined" || initialize)) { - this.initialize().catch(error => { - log.error(LogCategory.IDENTITIES, "Failed to initialize TeaSpeakIdentity (%s)", error); - this._initialized = false; - }); - } - } - - fallback_name(): string | undefined { - return this._name; - } - - uid(): string { - return this._unique_id; - } - - type(): IdentitifyType { - return IdentitifyType.TEAMSPEAK; - } - - valid(): boolean { - return this._initialized && !!this._crypto_key && !!this._crypto_key_sign; - } - - async decode(data: string) : Promise { - const json = JSON.parse(data); - if(!json) throw "invalid json"; - - if(json.version == 2) { - this.private_key = json.key; - this.hash_number = json.hash; - this._name = json.name; - } else if(json.version == 1) { - const key = json.key; - this._name = json.name; - - const clone = await TeaSpeakIdentity.import_ts(key, false); - this.private_key = clone.private_key; - this.hash_number = clone.hash_number; - } else - throw "invalid version"; - - await this.initialize(); - } - - encode?() : string { - return JSON.stringify({ - key: this.private_key, - hash: this.hash_number, - name: this._name, - version: 2 + /* set data */ + await new Promise((resolve, reject) => { + this._worker.postMessage({ + type: "set_data", + private_key: key, + code: "set_data" }); - } - async level() : Promise { - if(!this._initialized || !this.public_key) - throw "not initialized"; + const timeout_id = setTimeout(() => reject("timeout (data)"), 1000); - const hash = new Uint8Array(await sha.sha1(this.public_key + this.hash_number)); + this._worker.onmessage = event => { + clearTimeout(timeout_id); - let level = 0; - while(level < hash.byteLength && hash[level] == 0) - level++; - - if(level >= hash.byteLength) { - level = 256; - } else { - let byte = hash[level]; - level <<= 3; - while((byte & 0x1) == 0) { - level++; - byte >>= 1; + if (!event.data) { + reject("invalid data"); + return; } - } - return level; - } + if (!event.data.success) { + reject("initialize of data failed (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")"); + return; + } - /** - * @param {string} a - * @param {string} b - * @description b must be smaller (in bytes) then a - */ - private string_add(a: string, b: string) { - const char_result: number[] = []; - const char_a = [...a].reverse().map(e => e.charCodeAt(0)); - const char_b = [...b].reverse().map(e => e.charCodeAt(0)); + this._worker.onmessage = event => this.handle_message(event.data); + resolve(); + }; + }); + } - let carry = false; - while(char_b.length > 0) { - let result = char_b.pop_front() + char_a.pop_front() + (carry ? 1 : 0) - 48; - if((carry = result > 57)) - result -= 10; - char_result.push(result); - } + async mine(hash: string, iterations: number, target: number, timeout?: number) : Promise { + this._current_hash = hash; + if(target < this._best_level) + return true; - while(char_a.length > 0) { - let result = char_a.pop_front() + (carry ? 1 : 0); - if((carry = result > 57)) - result -= 10; - char_result.push(result); - } + return await new Promise((resolve, reject) => { + this._worker.postMessage({ + type: "mine", + hash: this._current_hash, + iterations: iterations, + target: target, + code: "mine" + }); - if(carry) - char_result.push(49); + const timeout_id = setTimeout(() => reject("timeout (mine)"), timeout || 5000); - return String.fromCharCode.apply(null, char_result.slice().reverse()); - } + this._worker.onmessage = event => { + this._worker.onmessage = event => this.handle_message(event.data); + clearTimeout(timeout_id); + if (!event.data) { + reject("invalid data"); + return; + } - async improve_level_for(time: number, threads: number) : Promise { - let active = true; - setTimeout(() => active = false, time); + if (!event.data.success) { + reject("mining failed (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")"); + return; + } - return await this.improve_level(-1, threads, () => active); - } - - async improve_level(target: number, threads: number, active_callback: () => boolean, callback_level?: (current: number) => any, callback_status?: (hash_rate: number) => any) : Promise { - if(!this._initialized || !this.public_key) - throw "not initialized"; - if(target == -1) /* get the highest level possible */ - target = 0; - else if(target <= await this.level()) - return true; - - const workers: IdentityPOWWorker[] = []; - - const iterations = 100000; - let current_hash; - const next_hash = () => { - if(!current_hash) - return (current_hash = this.hash_number); - - if(current_hash.length < iterations.toString().length) { - current_hash = this.string_add(iterations.toString(), current_hash); + if(event.data.result) { + this._best_level = event.data.level; + this._current_hash = event.data.hash; + resolve(true); } else { - current_hash = this.string_add(current_hash, iterations.toString()); + resolve(false); /* no result */ } - return current_hash; }; + }); + } - { /* init */ - const initialize_promise: Promise[] = []; - for(let index = 0; index < threads; index++) { - const worker = new IdentityPOWWorker(); - workers.push(worker); - initialize_promise.push(worker.initialize(this.public_key)); - } + current_hash() : string { + return this._current_hash; + } - try { - await Promise.all(initialize_promise); - } catch(error) { - log.error(LogCategory.IDENTITIES, error); - throw "failed to initialize"; - } + current_level() : number { + return this._best_level; + } + + async finalize(timeout?: number) { + try { + await new Promise((resolve, reject) => { + this._worker.postMessage({ + type: "finalize", + code: "finalize" + }); + + const timeout_id = setTimeout(() => reject("timeout"), timeout || 250); + + this._worker.onmessage = event => { + this._worker.onmessage = event => this.handle_message(event.data); + + clearTimeout(timeout_id); + + if (!event.data) { + reject("invalid data"); + return; + } + + if (!event.data.success) { + reject("failed to finalize (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")"); + return; + } + + resolve(); + }; + }); + } catch(error) { + log.error(LogCategory.IDENTITIES, tr("Failed to finalize POW worker! (%o)"), error); + } + + this._worker.terminate(); + this._worker = undefined; + } + + private handle_message(message: any) { + log.info(LogCategory.IDENTITIES, tr("Received message: %o"), message); + } +} + +export class TeaSpeakIdentity implements Identity { + static async generate_new() : Promise { + let key: CryptoKeyPair; + try { + key = await crypto.subtle.generateKey({name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]); + } catch(e) { + log.error(LogCategory.IDENTITIES, tr("Could not generate a new key: %o"), e); + throw "Failed to generate keypair"; + } + const private_key = await CryptoHelper.export_ecc_key(key.privateKey, false); + + const identity = new TeaSpeakIdentity(private_key, "0", undefined, false); + await identity.initialize(); + return identity; + } + + static async import_ts(ts_string: string, ini?: boolean) : Promise { + const parse_string = string => { + /* parsing without INI structure */ + const V_index = string.indexOf('V'); + if(V_index == -1) throw "invalid input (missing V)"; + + return { + hash: string.substr(0, V_index), + data: string.substr(V_index + 1), + name: "TeaSpeak user" + } + }; + + const {hash, data, name} = (!ini ? () => parse_string(ts_string) : () => { + /* parsing with INI structure */ + let identity: string, name: string; + + for(const line of ts_string.split("\n")) { + if(line.startsWith("identity=")) + identity = line.substr(9); + else if(line.startsWith("nickname=")) + name = line.substr(9); } - let result = false; - let best_level = 0; - let target_level = target > 0 ? target : await this.level() + 1; + if(!identity) throw "missing identity keyword"; + identity = identity.match(/^"?([0-9]+V[0-9a-zA-Z+\/]+[=]+)"?$/)[1]; + if(!identity) throw "invalid identity key value"; - const worker_promise: Promise[] = []; + const result = parse_string(identity); + result.name = name || result.name; + return result; + })(); - const hash_timestamps: number[] = []; - let last_hashrate_update: number = 0; + if(!ts_string.match(/[0-9]+/g)) throw "invalid hash!"; - const update_hashrate = () => { - if(!callback_status) return; - const now = Date.now(); - hash_timestamps.push(now); + let buffer; + try { + buffer = new Uint8Array(arrayBufferBase64(data)); + } catch(error) { + log.error(LogCategory.IDENTITIES, tr("Failed to decode given base64 data (%s)"), data); + throw "failed to base data (base64 decode failed)"; + } + const key64 = await CryptoHelper.decrypt_ts_identity(new Uint8Array(arrayBufferBase64(data))); - if(last_hashrate_update + 1000 < now) { - last_hashrate_update = now; + const identity = new TeaSpeakIdentity(key64, hash, name, false); + await identity.initialize(); + return identity; + } - const timeout = now - 10 * 1000; /* 10s */ - const rounds = hash_timestamps.filter(e => e > timeout); - callback_status(Math.ceil((rounds.length * iterations) / Math.ceil((now - rounds[0]) / 1000))) - } - }; + hash_number: string; /* hash suffix for the private key */ + private_key: string; /* base64 representation of the private key */ + _name: string; + + public_key: string; /* only set when initialized */ + + private _initialized: boolean; + private _crypto_key: CryptoKey; + private _crypto_key_sign: CryptoKey; + + private _unique_id: string; + + constructor(private_key?: string, hash?: string, name?: string, initialize?: boolean) { + this.private_key = private_key; + this.hash_number = hash || "0"; + this._name = name; + + if(this.private_key && (typeof(initialize) === "undefined" || initialize)) { + this.initialize().catch(error => { + log.error(LogCategory.IDENTITIES, "Failed to initialize TeaSpeakIdentity (%s)", error); + this._initialized = false; + }); + } + } + + fallback_name(): string | undefined { + return this._name; + } + + uid(): string { + return this._unique_id; + } + + type(): IdentitifyType { + return IdentitifyType.TEAMSPEAK; + } + + valid(): boolean { + return this._initialized && !!this._crypto_key && !!this._crypto_key_sign; + } + + async decode(data: string) : Promise { + const json = JSON.parse(data); + if(!json) throw "invalid json"; + + if(json.version == 2) { + this.private_key = json.key; + this.hash_number = json.hash; + this._name = json.name; + } else if(json.version == 1) { + const key = json.key; + this._name = json.name; + + const clone = await TeaSpeakIdentity.import_ts(key, false); + this.private_key = clone.private_key; + this.hash_number = clone.hash_number; + } else + throw "invalid version"; + + await this.initialize(); + } + + encode?() : string { + return JSON.stringify({ + key: this.private_key, + hash: this.hash_number, + name: this._name, + version: 2 + }); + } + + async level() : Promise { + if(!this._initialized || !this.public_key) + throw "not initialized"; + + const hash = new Uint8Array(await sha.sha1(this.public_key + this.hash_number)); + + let level = 0; + while(level < hash.byteLength && hash[level] == 0) + level++; + + if(level >= hash.byteLength) { + level = 256; + } else { + let byte = hash[level]; + level <<= 3; + while((byte & 0x1) == 0) { + level++; + byte >>= 1; + } + } + + return level; + } + + /** + * @param {string} a + * @param {string} b + * @description b must be smaller (in bytes) then a + */ + private string_add(a: string, b: string) { + const char_result: number[] = []; + const char_a = [...a].reverse().map(e => e.charCodeAt(0)); + const char_b = [...b].reverse().map(e => e.charCodeAt(0)); + + let carry = false; + while(char_b.length > 0) { + let result = char_b.pop_front() + char_a.pop_front() + (carry ? 1 : 0) - 48; + if((carry = result > 57)) + result -= 10; + char_result.push(result); + } + + while(char_a.length > 0) { + let result = char_a.pop_front() + (carry ? 1 : 0); + if((carry = result > 57)) + result -= 10; + char_result.push(result); + } + + if(carry) + char_result.push(49); + + return String.fromCharCode.apply(null, char_result.slice().reverse()); + } + + + async improve_level_for(time: number, threads: number) : Promise { + let active = true; + setTimeout(() => active = false, time); + + return await this.improve_level(-1, threads, () => active); + } + + async improve_level(target: number, threads: number, active_callback: () => boolean, callback_level?: (current: number) => any, callback_status?: (hash_rate: number) => any) : Promise { + if(!this._initialized || !this.public_key) + throw "not initialized"; + if(target == -1) /* get the highest level possible */ + target = 0; + else if(target <= await this.level()) + return true; + + const workers: IdentityPOWWorker[] = []; + + const iterations = 100000; + let current_hash; + const next_hash = () => { + if(!current_hash) + return (current_hash = this.hash_number); + + if(current_hash.length < iterations.toString().length) { + current_hash = this.string_add(iterations.toString(), current_hash); + } else { + current_hash = this.string_add(current_hash, iterations.toString()); + } + return current_hash; + }; + + { /* init */ + const initialize_promise: Promise[] = []; + for(let index = 0; index < threads; index++) { + const worker = new IdentityPOWWorker(); + workers.push(worker); + initialize_promise.push(worker.initialize(this.public_key)); + } try { - result = await new Promise((resolve, reject) => { - let active = true; + await Promise.all(initialize_promise); + } catch(error) { + log.error(LogCategory.IDENTITIES, error); + throw "failed to initialize"; + } + } - const exit = () => { - const timeout = setTimeout(() => resolve(true), 1000); - Promise.all(worker_promise).then(result => { - clearTimeout(timeout); - resolve(true); - }).catch(error => resolve(true)); - active = false; + let result = false; + let best_level = 0; + let target_level = target > 0 ? target : await this.level() + 1; + + const worker_promise: Promise[] = []; + + const hash_timestamps: number[] = []; + let last_hashrate_update: number = 0; + + const update_hashrate = () => { + if(!callback_status) return; + const now = Date.now(); + hash_timestamps.push(now); + + if(last_hashrate_update + 1000 < now) { + last_hashrate_update = now; + + const timeout = now - 10 * 1000; /* 10s */ + const rounds = hash_timestamps.filter(e => e > timeout); + callback_status(Math.ceil((rounds.length * iterations) / Math.ceil((now - rounds[0]) / 1000))) + } + }; + + try { + result = await new Promise((resolve, reject) => { + let active = true; + + const exit = () => { + const timeout = setTimeout(() => resolve(true), 1000); + Promise.all(worker_promise).then(result => { + clearTimeout(timeout); + resolve(true); + }).catch(error => resolve(true)); + active = false; + }; + + for(const worker of workers) { + const worker_mine = () => { + if(!active) return; + + const promise = worker.mine(next_hash(), iterations, target_level); + const p = promise.then(result => { + update_hashrate(); + + worker_promise.remove(p); + + if(result.valueOf()) { + if(worker.current_level() > best_level) { + this.hash_number = worker.current_hash(); + + log.info(LogCategory.IDENTITIES, "Found new best at %s (%d). Old was %d", this.hash_number, worker.current_level(), best_level); + best_level = worker.current_level(); + if(callback_level) + callback_level(best_level); + } + + if(active) { + if(target > 0) + exit(); + else + target_level = best_level + 1; + } + } + + if(active && (active = active_callback())) + setTimeout(() => worker_mine(), 0); + else { + exit(); + } + + return Promise.resolve(); + }).catch(error => { + worker_promise.remove(p); + + log.warn(LogCategory.IDENTITIES, "POW worker error %o", error); + reject(error); + + return Promise.resolve(); + }); + + worker_promise.push(p); }; - for(const worker of workers) { - const worker_mine = () => { - if(!active) return; - - const promise = worker.mine(next_hash(), iterations, target_level); - const p = promise.then(result => { - update_hashrate(); - - worker_promise.remove(p); - - if(result.valueOf()) { - if(worker.current_level() > best_level) { - this.hash_number = worker.current_hash(); - - log.info(LogCategory.IDENTITIES, "Found new best at %s (%d). Old was %d", this.hash_number, worker.current_level(), best_level); - best_level = worker.current_level(); - if(callback_level) - callback_level(best_level); - } - - if(active) { - if(target > 0) - exit(); - else - target_level = best_level + 1; - } - } - - if(active && (active = active_callback())) - setTimeout(() => worker_mine(), 0); - else { - exit(); - } - - return Promise.resolve(); - }).catch(error => { - worker_promise.remove(p); - - log.warn(LogCategory.IDENTITIES, "POW worker error %o", error); - reject(error); - - return Promise.resolve(); - }); - - worker_promise.push(p); - }; - - worker_mine(); - } - }); - } catch(error) { - //error already printed before reject had been called - } - - { /* shutdown */ - const finalize_promise: Promise[] = []; - for(const worker of workers) - finalize_promise.push(worker.finalize(250)); - - try { - await Promise.all(finalize_promise); - } catch(error) { - log.error(LogCategory.IDENTITIES, error); - throw "failed to finalize"; + worker_mine(); } - } - - - return result; + }); + } catch(error) { + //error already printed before reject had been called } - private async initialize() { - if(!this.private_key) - throw "Invalid private key"; - - let jwk: any; - try { - jwk = await CryptoHelper.decode_tomcrypt_key(this.private_key); - if(!jwk) - throw "result undefined"; - } catch(error) { - throw "failed to parse key (" + error + ")"; - } + { /* shutdown */ + const finalize_promise: Promise[] = []; + for(const worker of workers) + finalize_promise.push(worker.finalize(250)); try { - this._crypto_key_sign = await crypto.subtle.importKey("jwk", jwk, {name:'ECDSA', namedCurve: 'P-256'}, false, ["sign"]); + await Promise.all(finalize_promise); } catch(error) { log.error(LogCategory.IDENTITIES, error); - throw "failed to create crypto sign key"; + throw "failed to finalize"; } - - try { - this._crypto_key = await crypto.subtle.importKey("jwk", jwk, {name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]); - } catch(error) { - log.error(LogCategory.IDENTITIES, error); - throw "failed to create crypto key"; - } - - try { - this.public_key = await CryptoHelper.export_ecc_key(this._crypto_key, true); - this._unique_id = base64_encode_ab(await sha.sha1(this.public_key)); - } catch(error) { - log.error(LogCategory.IDENTITIES, error); - throw "failed to calculate unique id"; - } - - this._initialized = true; - //const public_key = await profiles.identities.CryptoHelper.export_ecc_key(key, true); } - async export_ts(ini?: boolean) : Promise { - if(!this.private_key) - throw "Invalid private key"; - const identity = this.hash_number + "V" + await CryptoHelper.encrypt_ts_identity(new Uint8Array(str2ab8(this.private_key))); - if(!ini) return identity; + return result; + } - return "[Identity]\n" + - "id=TeaWeb-Exported\n" + - "identity=\"" + identity + "\"\n" + - "nickname=\"" + this.fallback_name() + "\"\n" + - "phonetic_nickname="; + private async initialize() { + if(!this.private_key) + throw "Invalid private key"; + + let jwk: any; + try { + jwk = await CryptoHelper.decode_tomcrypt_key(this.private_key); + if(!jwk) + throw "result undefined"; + } catch(error) { + throw "failed to parse key (" + error + ")"; } - async sign_message(message: string, hash: string = "SHA-256") : Promise { - /* bring this to libtomcrypt format */ - const sign_buffer = await crypto.subtle.sign({ - name: "ECDSA", - hash: hash - }, this._crypto_key_sign, str2ab8(message)); - const sign = new Uint8Array(sign_buffer); - /* first 32 r bits | last 32 s bits */ - - const buffer = new Uint8Array(72); - let index = 0; - - { /* the initial sequence */ - buffer[index++] = 0x30; /* type */ - buffer[index++] = 0x00; /* we will set the sequence length later */ - } - { /* integer r */ - buffer[index++] = 0x02; /* type */ - buffer[index++] = 0x20; /* length */ - - if(sign[0] > 0x7F) { - buffer[index - 1] += 1; - buffer[index++] = 0; - } - - for(let i = 0; i < 32; i++) - buffer[index++] = sign[i]; - } - { /* integer s */ - buffer[index++] = 0x02; /* type */ - buffer[index++] = 0x20; /* length */ - - if(sign[32] > 0x7F) { - buffer[index - 1] += 1; - buffer[index++] = 0; - } - - for(let i = 0; i < 32; i++) - buffer[index++] = sign[32 + i]; - } - buffer[1] = index - 2; - - return base64_encode_ab(buffer.subarray(0, index)); + try { + this._crypto_key_sign = await crypto.subtle.importKey("jwk", jwk, {name:'ECDSA', namedCurve: 'P-256'}, false, ["sign"]); + } catch(error) { + log.error(LogCategory.IDENTITIES, error); + throw "failed to create crypto sign key"; } - spawn_identity_handshake_handler(connection: connection.AbstractServerConnection): connection.HandshakeIdentityHandler { - return new TeaSpeakHandshakeHandler(connection, this); + try { + this._crypto_key = await crypto.subtle.importKey("jwk", jwk, {name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]); + } catch(error) { + log.error(LogCategory.IDENTITIES, error); + throw "failed to create crypto key"; } + + try { + this.public_key = await CryptoHelper.export_ecc_key(this._crypto_key, true); + this._unique_id = base64_encode_ab(await sha.sha1(this.public_key)); + } catch(error) { + log.error(LogCategory.IDENTITIES, error); + throw "failed to calculate unique id"; + } + + this._initialized = true; + } + + async export_ts(ini?: boolean) : Promise { + if(!this.private_key) + throw "Invalid private key"; + + const identity = this.hash_number + "V" + await CryptoHelper.encrypt_ts_identity(new Uint8Array(str2ab8(this.private_key))); + if(!ini) return identity; + + return "[Identity]\n" + + "id=TeaWeb-Exported\n" + + "identity=\"" + identity + "\"\n" + + "nickname=\"" + this.fallback_name() + "\"\n" + + "phonetic_nickname="; + } + + async sign_message(message: string, hash: string = "SHA-256") : Promise { + /* bring this to libtomcrypt format */ + const sign_buffer = await crypto.subtle.sign({ + name: "ECDSA", + hash: hash + }, this._crypto_key_sign, str2ab8(message)); + const sign = new Uint8Array(sign_buffer); + /* first 32 r bits | last 32 s bits */ + + const buffer = new Uint8Array(72); + let index = 0; + + { /* the initial sequence */ + buffer[index++] = 0x30; /* type */ + buffer[index++] = 0x00; /* we will set the sequence length later */ + } + { /* integer r */ + buffer[index++] = 0x02; /* type */ + buffer[index++] = 0x20; /* length */ + + if(sign[0] > 0x7F) { + buffer[index - 1] += 1; + buffer[index++] = 0; + } + + for(let i = 0; i < 32; i++) + buffer[index++] = sign[i]; + } + { /* integer s */ + buffer[index++] = 0x02; /* type */ + buffer[index++] = 0x20; /* length */ + + if(sign[32] > 0x7F) { + buffer[index - 1] += 1; + buffer[index++] = 0; + } + + for(let i = 0; i < 32; i++) + buffer[index++] = sign[32 + i]; + } + buffer[1] = index - 2; + + return base64_encode_ab(buffer.subarray(0, index)); + } + + spawn_identity_handshake_handler(connection: AbstractServerConnection): HandshakeIdentityHandler { + return new TeaSpeakHandshakeHandler(connection, this); } } \ No newline at end of file diff --git a/shared/js/profiles/identities/teaspeak-forum.ts b/shared/js/profiles/identities/teaspeak-forum.ts index 1ab6cd53..3ea4ff81 100644 --- a/shared/js/profiles/identities/teaspeak-forum.ts +++ b/shared/js/profiles/identities/teaspeak-forum.ts @@ -1,5 +1,11 @@ -interface Window { - grecaptcha: GReCaptcha; +import {settings, Settings} from "tc-shared/settings"; +import * as loader from "tc-loader"; +import * as fidentity from "./TeaForumIdentity"; + +declare global { + interface Window { + grecaptcha: GReCaptcha; + } } interface GReCaptcha { @@ -18,349 +24,347 @@ interface GReCaptcha { reset(widget_id?: string); } -namespace forum { - export namespace gcaptcha { - export async function initialize() { - if(typeof(window.grecaptcha) === "undefined") { - let script = document.createElement("script"); - script.async = true; +export namespace gcaptcha { + export async function initialize() { + if(typeof(window.grecaptcha) === "undefined") { + let script = document.createElement("script"); + script.async = true; - let timeout; - const callback_name = "captcha_callback_" + Math.random().toString().replace(".", ""); - try { - await new Promise((resolve, reject) => { - script.onerror = reject; - window[callback_name] = resolve; - script.src = "https://www.google.com/recaptcha/api.js?onload=" + encodeURIComponent(callback_name) + "&render=explicit"; - - document.body.append(script); - timeout = setTimeout(() => reject("timeout"), 15000); - }); - } catch(error) { - script.remove(); - script = undefined; - - console.error(tr("Failed to fetch recaptcha javascript source: %o"), error); - throw tr("failed to download source"); - } finally { - if(script) - script.onerror = undefined; - delete window[callback_name]; - clearTimeout(timeout); - } - } - - if(typeof(window.grecaptcha) === "undefined") - throw tr("failed to load recaptcha"); - } - - export async function spawn(container: JQuery, key: string, callback_data: (token: string) => any) { + let timeout; + const callback_name = "captcha_callback_" + Math.random().toString().replace(".", ""); try { - await initialize(); + await new Promise((resolve, reject) => { + script.onerror = reject; + window[callback_name] = resolve; + script.src = "https://www.google.com/recaptcha/api.js?onload=" + encodeURIComponent(callback_name) + "&render=explicit"; + + document.body.append(script); + timeout = setTimeout(() => reject("timeout"), 15000); + }); } catch(error) { - console.error(tr("Failed to initialize G-Recaptcha. Error: %o"), error); - throw tr("initialisation failed"); - } - if(container.attr("captcha-uuid")) - window.grecaptcha.reset(container.attr("captcha-uuid")); - else { - container.attr("captcha-uuid", window.grecaptcha.render(container[0], { - "sitekey": key, - callback: callback_data - })); + script.remove(); + script = undefined; + + console.error(tr("Failed to fetch recaptcha javascript source: %o"), error); + throw tr("failed to download source"); + } finally { + if(script) + script.onerror = undefined; + delete window[callback_name]; + timeout && clearTimeout(timeout); } } + + if(typeof(window.grecaptcha) === "undefined") + throw tr("failed to load recaptcha"); } - function api_url() { - return settings.static_global(Settings.KEY_TEAFORO_URL); - } - - export class Data { - readonly auth_key: string; - readonly raw: string; - readonly sign: string; - - parsed: { - user_id: number; - user_name: string; - - data_age: number; - - user_group_id: number; - - is_staff: boolean; - user_groups: number[]; - }; - - constructor(auth: string, raw: string, sign: string) { - this.auth_key = auth; - this.raw = raw; - this.sign = sign; - - this.parsed = JSON.parse(raw); + export async function spawn(container: JQuery, key: string, callback_data: (token: string) => any) { + try { + await initialize(); + } catch(error) { + console.error(tr("Failed to initialize G-Recaptcha. Error: %o"), error); + throw tr("initialisation failed"); + } + if(container.attr("captcha-uuid")) + window.grecaptcha.reset(container.attr("captcha-uuid")); + else { + container.attr("captcha-uuid", window.grecaptcha.render(container[0], { + "sitekey": key, + callback: callback_data + })); } - - - data_json() : string { return this.raw; } - data_sign() : string { return this.sign; } - - name() : string { return this.parsed.user_name; } - - user_id() { return this.parsed.user_id; } - user_group() { return this.parsed.user_group_id; } - - is_stuff() : boolean { return this.parsed.is_staff; } - is_premium() : boolean { return this.parsed.user_groups.indexOf(5) != -1; } - - data_age() : Date { return new Date(this.parsed.data_age); } - - is_expired() : boolean { return this.parsed.data_age + 48 * 60 * 60 * 1000 < Date.now(); } - should_renew() : boolean { return this.parsed.data_age + 24 * 60 * 60 * 1000 < Date.now(); } /* renew data all 24hrs */ } - let _data: Data | undefined; +} - export function logged_in() : boolean { - return !!_data && !_data.is_expired(); +function api_url() { + return settings.static_global(Settings.KEY_TEAFORO_URL); +} + +export class Data { + readonly auth_key: string; + readonly raw: string; + readonly sign: string; + + parsed: { + user_id: number; + user_name: string; + + data_age: number; + + user_group_id: number; + + is_staff: boolean; + user_groups: number[]; + }; + + constructor(auth: string, raw: string, sign: string) { + this.auth_key = auth; + this.raw = raw; + this.sign = sign; + + this.parsed = JSON.parse(raw); } - export function data() : Data { return _data; } - export interface LoginResult { - status: "success" | "captcha" | "error"; + data_json() : string { return this.raw; } + data_sign() : string { return this.sign; } - error_message?: string; - captcha?: { - type: "gre-captcha" | "unknown"; - data: any; /* in case of gre-captcha it would be the side key */ + name() : string { return this.parsed.user_name; } + + user_id() { return this.parsed.user_id; } + user_group() { return this.parsed.user_group_id; } + + is_stuff() : boolean { return this.parsed.is_staff; } + is_premium() : boolean { return this.parsed.user_groups.indexOf(5) != -1; } + + data_age() : Date { return new Date(this.parsed.data_age); } + + is_expired() : boolean { return this.parsed.data_age + 48 * 60 * 60 * 1000 < Date.now(); } + should_renew() : boolean { return this.parsed.data_age + 24 * 60 * 60 * 1000 < Date.now(); } /* renew data all 24hrs */ +} +let _data: Data | undefined; + +export function logged_in() : boolean { + return !!_data && !_data.is_expired(); +} + +export function data() : Data { return _data; } + +export interface LoginResult { + status: "success" | "captcha" | "error"; + + error_message?: string; + captcha?: { + type: "gre-captcha" | "unknown"; + data: any; /* in case of gre-captcha it would be the side key */ + }; +} + +export async function login(username: string, password: string, captcha?: any) : Promise { + let response; + try { + response = await new Promise((resolve, reject) => { + $.ajax({ + url: api_url() + "?web-api/v1/login", + type: "POST", + cache: false, + data: { + username: username, + password: password, + remember: true, + "g-recaptcha-response": captcha + }, + + crossDomain: true, + + success: resolve, + error: (xhr, status, error) => { + console.log(tr("Login request failed %o: %o"), status, error); + reject(tr("request failed")); + } + }) + }); + } catch(error) { + return { + status: "error", + error_message: tr("failed to send login request") }; } - export async function login(username: string, password: string, captcha?: any) : Promise { - let response; - try { - response = await new Promise((resolve, reject) => { - $.ajax({ - url: api_url() + "?web-api/v1/login", - type: "POST", - cache: false, - data: { - username: username, - password: password, - remember: true, - "g-recaptcha-response": captcha - }, + if(response["status"] !== "ok") { + console.error(tr("Response status not okey. Error happend: %o"), response); + return { + status: "error", + error_message: (response["errors"] || [])[0] || tr("Unknown error") + }; + } - crossDomain: true, + if(!response["success"]) { + console.error(tr("Login failed. Response %o"), response); - success: resolve, - error: (xhr, status, error) => { - console.log(tr("Login request failed %o: %o"), status, error); - reject(tr("request failed")); - } - }) - }); - } catch(error) { - return { - status: "error", - error_message: tr("failed to send login request") + let message = tr("failed to login"); + let captcha; + /* user/password wrong | and maybe captcha required */ + if(response["code"] == 1 || response["code"] == 3) + message = tr("Invalid username or password"); + if(response["code"] == 2 || response["code"] == 3) { + captcha = { + type: response["captcha"]["type"], + data: response["captcha"]["siteKey"] //TODO: Why so static here? }; - } - - if(response["status"] !== "ok") { - console.error(tr("Response status not okey. Error happend: %o"), response); - return { - status: "error", - error_message: (response["errors"] || [])[0] || tr("Unknown error") - }; - } - - if(!response["success"]) { - console.error(tr("Login failed. Response %o"), response); - - let message = tr("failed to login"); - let captcha; - /* user/password wrong | and maybe captcha required */ - if(response["code"] == 1 || response["code"] == 3) - message = tr("Invalid username or password"); - if(response["code"] == 2 || response["code"] == 3) { - captcha = { - type: response["captcha"]["type"], - data: response["captcha"]["siteKey"] //TODO: Why so static here? - }; - if(response["code"] == 2) - message = tr("captcha required"); - } - - return { - status: typeof(captcha) !== "undefined" ? "captcha" : "error", - error_message: message, - captcha: captcha - }; - } - //document.cookie = "user_data=" + response["data"] + ";path=/"; - //document.cookie = "user_sign=" + response["sign"] + ";path=/"; - - try { - _data = new Data(response["auth-key"], response["data"], response["sign"]); - localStorage.setItem("teaspeak-forum-data", response["data"]); - localStorage.setItem("teaspeak-forum-sign", response["sign"]); - localStorage.setItem("teaspeak-forum-auth", response["auth-key"]); - profiles.identities.update_forum(); - } catch(error) { - console.error(tr("Failed to parse forum given data: %o"), error); - return { - status: "error", - error_message: tr("Failed to parse response data") - } + if(response["code"] == 2) + message = tr("captcha required"); } return { - status: "success" + status: typeof(captcha) !== "undefined" ? "captcha" : "error", + error_message: message, + captcha: captcha }; } + //document.cookie = "user_data=" + response["data"] + ";path=/"; + //document.cookie = "user_sign=" + response["sign"] + ";path=/"; - export async function renew_data() : Promise<"success" | "login-required"> { - let response; - try { - response = await new Promise((resolve, reject) => { - $.ajax({ - url: api_url() + "?web-api/v1/renew-data", - type: "GET", - cache: false, - - crossDomain: true, - - data: { - "auth-key": _data.auth_key - }, - - success: resolve, - error: (xhr, status, error) => { - console.log(tr("Renew request failed %o: %o"), status, error); - reject(tr("request failed")); - } - }) - }); - } catch(error) { - throw tr("failed to send renew request"); + try { + _data = new Data(response["auth-key"], response["data"], response["sign"]); + localStorage.setItem("teaspeak-forum-data", response["data"]); + localStorage.setItem("teaspeak-forum-sign", response["sign"]); + localStorage.setItem("teaspeak-forum-auth", response["auth-key"]); + fidentity.update_forum(); + } catch(error) { + console.error(tr("Failed to parse forum given data: %o"), error); + return { + status: "error", + error_message: tr("Failed to parse response data") } + } - if(response["status"] !== "ok") { - console.error(tr("Response status not okey. Error happend: %o"), response); - throw (response["errors"] || [])[0] || tr("Unknown error"); + return { + status: "success" + }; +} + +export async function renew_data() : Promise<"success" | "login-required"> { + let response; + try { + response = await new Promise((resolve, reject) => { + $.ajax({ + url: api_url() + "?web-api/v1/renew-data", + type: "GET", + cache: false, + + crossDomain: true, + + data: { + "auth-key": _data.auth_key + }, + + success: resolve, + error: (xhr, status, error) => { + console.log(tr("Renew request failed %o: %o"), status, error); + reject(tr("request failed")); + } + }) + }); + } catch(error) { + throw tr("failed to send renew request"); + } + + if(response["status"] !== "ok") { + console.error(tr("Response status not okey. Error happend: %o"), response); + throw (response["errors"] || [])[0] || tr("Unknown error"); + } + + if(!response["success"]) { + if(response["code"] == 1) { + return "login-required"; } + throw "invalid error code (" + response["code"] + ")"; + } + if(!response["data"] || !response["sign"]) + throw tr("response missing data"); - if(!response["success"]) { - if(response["code"] == 1) { - return "login-required"; - } + console.debug(tr("Renew succeeded. Parsing data.")); + + try { + _data = new Data(_data.auth_key, response["data"], response["sign"]); + localStorage.setItem("teaspeak-forum-data", response["data"]); + localStorage.setItem("teaspeak-forum-sign", response["sign"]); + fidentity.update_forum(); + } catch(error) { + console.error(tr("Failed to parse forum given data: %o"), error); + throw tr("failed to parse data"); + } + + return "success"; +} + +export async function logout() : Promise { + if(!logged_in()) + return; + + let response; + try { + response = await new Promise((resolve, reject) => { + $.ajax({ + url: api_url() + "?web-api/v1/logout", + type: "GET", + cache: false, + + crossDomain: true, + + data: { + "auth-key": _data.auth_key + }, + + success: resolve, + error: (xhr, status, error) => { + console.log(tr("Logout request failed %o: %o"), status, error); + reject(tr("request failed")); + } + }) + }); + } catch(error) { + throw tr("failed to send logout request"); + } + + if(response["status"] !== "ok") { + console.error(tr("Response status not okey. Error happend: %o"), response); + throw (response["errors"] || [])[0] || tr("Unknown error"); + } + + if(!response["success"]) { + /* code 1 means not logged in, its an success */ + if(response["code"] != 1) { throw "invalid error code (" + response["code"] + ")"; } - if(!response["data"] || !response["sign"]) - throw tr("response missing data"); - - console.debug(tr("Renew succeeded. Parsing data.")); - - try { - _data = new Data(_data.auth_key, response["data"], response["sign"]); - localStorage.setItem("teaspeak-forum-data", response["data"]); - localStorage.setItem("teaspeak-forum-sign", response["sign"]); - profiles.identities.update_forum(); - } catch(error) { - console.error(tr("Failed to parse forum given data: %o"), error); - throw tr("failed to parse data"); - } - - return "success"; } - export async function logout() : Promise { - if(!logged_in()) + _data = undefined; + localStorage.removeItem("teaspeak-forum-data"); + localStorage.removeItem("teaspeak-forum-sign"); + localStorage.removeItem("teaspeak-forum-auth"); + fidentity.update_forum(); +} + +loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { + name: "TeaForo initialize", + priority: 10, + function: async () => { + const raw_data = localStorage.getItem("teaspeak-forum-data"); + const raw_sign = localStorage.getItem("teaspeak-forum-sign"); + const forum_auth = localStorage.getItem("teaspeak-forum-auth"); + if(!raw_data || !raw_sign || !forum_auth) { + console.log(tr("No TeaForo authentification found. TeaForo connection status: unconnected")); return; + } - let response; try { - response = await new Promise((resolve, reject) => { - $.ajax({ - url: api_url() + "?web-api/v1/logout", - type: "GET", - cache: false, - - crossDomain: true, - - data: { - "auth-key": _data.auth_key - }, - - success: resolve, - error: (xhr, status, error) => { - console.log(tr("Logout request failed %o: %o"), status, error); - reject(tr("request failed")); - } - }) - }); + _data = new Data(forum_auth, raw_data, raw_sign); } catch(error) { - throw tr("failed to send logout request"); + console.error(tr("Failed to initialize TeaForo connection from local data. Error: %o"), error); + return; + } + if(_data.should_renew()) { + console.info(tr("TeaForo data should be renewed. Executing renew.")); + renew_data().then(status => { + if(status === "success") { + console.info(tr("TeaForo data has been successfully renewed.")); + } else { + console.warn(tr("Failed to renew TeaForo data. New login required.")); + localStorage.removeItem("teaspeak-forum-data"); + localStorage.removeItem("teaspeak-forum-sign"); + localStorage.removeItem("teaspeak-forum-auth"); + } + }).catch(error => { + console.warn(tr("Failed to renew TeaForo data. An error occurred: %o"), error); + }); + return; } - if(response["status"] !== "ok") { - console.error(tr("Response status not okey. Error happend: %o"), response); - throw (response["errors"] || [])[0] || tr("Unknown error"); + if(_data && _data.is_expired()) { + console.error(tr("TeaForo data is expired. TeaForo connection isn't available!")); } - - if(!response["success"]) { - /* code 1 means not logged in, its an success */ - if(response["code"] != 1) { - throw "invalid error code (" + response["code"] + ")"; - } - } - - _data = undefined; - localStorage.removeItem("teaspeak-forum-data"); - localStorage.removeItem("teaspeak-forum-sign"); - localStorage.removeItem("teaspeak-forum-auth"); - profiles.identities.update_forum(); } - - loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { - name: "TeaForo initialize", - priority: 10, - function: async () => { - const raw_data = localStorage.getItem("teaspeak-forum-data"); - const raw_sign = localStorage.getItem("teaspeak-forum-sign"); - const forum_auth = localStorage.getItem("teaspeak-forum-auth"); - if(!raw_data || !raw_sign || !forum_auth) { - console.log(tr("No TeaForo authentification found. TeaForo connection status: unconnected")); - return; - } - - try { - _data = new Data(forum_auth, raw_data, raw_sign); - } catch(error) { - console.error(tr("Failed to initialize TeaForo connection from local data. Error: %o"), error); - return; - } - if(_data.should_renew()) { - console.info(tr("TeaForo data should be renewed. Executing renew.")); - renew_data().then(status => { - if(status === "success") { - console.info(tr("TeaForo data has been successfully renewed.")); - } else { - console.warn(tr("Failed to renew TeaForo data. New login required.")); - localStorage.removeItem("teaspeak-forum-data"); - localStorage.removeItem("teaspeak-forum-sign"); - localStorage.removeItem("teaspeak-forum-auth"); - } - }).catch(error => { - console.warn(tr("Failed to renew TeaForo data. An error occurred: %o"), error); - }); - return; - } - - if(_data && _data.is_expired()) { - console.error(tr("TeaForo data is expired. TeaForo connection isn't available!")); - } - } - }) -} \ No newline at end of file +}) \ No newline at end of file diff --git a/shared/js/proto.ts b/shared/js/proto.ts index 18a162a7..9d732c33 100644 --- a/shared/js/proto.ts +++ b/shared/js/proto.ts @@ -1,45 +1,109 @@ //Used by CertAccept popup -interface Array { - remove(elem?: T): boolean; - last?(): T; +declare global { + interface Array { + remove(elem?: T): boolean; + last?(): T; - pop_front(): T | undefined; + pop_front(): T | undefined; + } + + interface JSON { + map_to(object: T, json: any, variables?: string | string[], validator?: (map_field: string, map_value: string) => boolean, variable_direction?: number) : number; + map_field_to(object: T, value: any, field: string) : boolean; + } + + type JQueryScrollType = "height" | "width"; + interface JQuery { + render(values?: any) : string; + renderTag(values?: any) : JQuery; + hasScrollBar(direction?: JQueryScrollType) : boolean; + + + visible_height() : number; + visible_width() : number; + + /* bootstrap */ + alert() : JQuery; + modal(properties: any) : this; + bootstrapMaterialDesign() : this; + + /* first element which matches the selector, could be the element itself or a parent */ + firstParent(selector: string) : JQuery; + } + + interface JQueryStatic { + spawn(tagName: K): JQuery; + views: any; + } + + interface String { + format(...fmt): string; + format(arguments: string[]): string; + } + + interface Twemoji { + parse(message: string) : string; + } + let twemoji: Twemoji; + + interface HighlightJS { + listLanguages() : string[]; + getLanguage(name: string) : any | undefined; + + highlight(language: string, text: string, ignore_illegals?: boolean) : HighlightJSResult; + highlightAuto(text: string) : HighlightJSResult; + } + + interface HighlightJSResult { + language: string; + relevance: number; + + value: string; + second_best?: any; + } + + interface DOMPurify { + sanitize(html: string, config?: { + ADD_ATTR?: string[] + ADD_TAGS?: string[]; + }) : string; + } + let DOMPurify: DOMPurify; + + let remarkable: typeof window.remarkable; + + class webkitAudioContext extends AudioContext {} + class webkitOfflineAudioContext extends OfflineAudioContext {} + + interface Window { + readonly webkitAudioContext: typeof webkitAudioContext; + readonly AudioContext: typeof webkitAudioContext; + readonly OfflineAudioContext: typeof OfflineAudioContext; + readonly webkitOfflineAudioContext: typeof webkitOfflineAudioContext; + readonly RTCPeerConnection: typeof RTCPeerConnection; + readonly Pointer_stringify: any; + readonly jsrender: any; + + twemoji: Twemoji; + hljs: HighlightJS; + remarkable: any; + + require(id: string): any; + } + + interface Navigator { + browserSpecs: { + name: string, + version: string + }; + + mozGetUserMedia(constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback): void; + webkitGetUserMedia(constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback): void; + } } -interface JSON { - map_to(object: T, json: any, variables?: string | string[], validator?: (map_field: string, map_value: string) => boolean, variable_direction?: number) : number; - map_field_to(object: T, value: any, field: string) : boolean; -} - -type JQueryScrollType = "height" | "width"; -interface JQuery { - render(values?: any) : string; - renderTag(values?: any) : JQuery; - hasScrollBar(direction?: JQueryScrollType) : boolean; - - - visible_height() : number; - visible_width() : number; - - /* bootstrap */ - alert() : JQuery; - modal(properties: any) : this; - bootstrapMaterialDesign() : this; - - /* first element which matches the selector, could be the element itself or a parent */ - firstParent(selector: string) : JQuery; -} - -interface JQueryStatic { - spawn(tagName: K): JQuery; - views: any; -} - -interface String { - format(...fmt): string; - format(arguments: string[]): string; -} +export function initialize() { } if(!JSON.map_to) { JSON.map_to = function (object: T, json: any, variables?: string | string[], validator?: (map_field: string, map_value: string) => boolean, variable_direction?: number): number { @@ -131,6 +195,7 @@ if(typeof ($) !== "undefined") { return $(document.createElement(tagName) as any); } } + if(!$.fn.renderTag) { $.fn.renderTag = function (this: JQuery, values?: any) : JQuery { let result; @@ -184,7 +249,8 @@ if(typeof ($) !== "undefined") { const result = this.height(); this.attr("style", original_style || ""); return result; - } + }; + if(!$.fn.visible_width) $.fn.visible_width = function (this: JQuery) { const original_style = this.attr("style"); @@ -197,7 +263,8 @@ if(typeof ($) !== "undefined") { const result = this.width(); this.attr("style", original_style || ""); return result; - } + }; + if(!$.fn.firstParent) $.fn.firstParent = function (this: JQuery, selector: string) { if(this.is(selector)) @@ -232,30 +299,6 @@ function concatenate(resultConstructor, ...arrays) { return result; } -function formatDate(secs: number) : string { - let years = Math.floor(secs / (60 * 60 * 24 * 365)); - let days = Math.floor(secs / (60 * 60 * 24)) % 365; - let hours = Math.floor(secs / (60 * 60)) % 24; - let minutes = Math.floor(secs / 60) % 60; - let seconds = Math.floor(secs % 60); - - let result = ""; - if(years > 0) - result += years + " " + tr("years") + " "; - if(years > 0 || days > 0) - result += days + " " + tr("days") + " "; - if(years > 0 || days > 0 || hours > 0) - result += hours + " " + tr("hours") + " "; - if(years > 0 || days > 0 || hours > 0 || minutes > 0) - result += minutes + " " + tr("minutes") + " "; - if(years > 0 || days > 0 || hours > 0 || minutes > 0 || seconds > 0) - result += seconds + " " + tr("seconds") + " "; - else - result = tr("now") + " "; - - return result.substr(0, result.length - 1); -} - function calculate_width(text: string) : number { let element = $.spawn("div"); element.text(text) @@ -265,64 +308,4 @@ function calculate_width(text: string) : number { let size = element.width(); element.detach(); return size; -} - -interface Twemoji { - parse(message: string) : string; -} -declare let twemoji: Twemoji; - -interface HighlightJS { - listLanguages() : string[]; - getLanguage(name: string) : any | undefined; - - highlight(language: string, text: string, ignore_illegals?: boolean) : HighlightJSResult; - highlightAuto(text: string) : HighlightJSResult; -} - -interface HighlightJSResult { - language: string; - relevance: number; - - value: string; - second_best?: any; -} - -interface DOMPurify { - sanitize(html: string, config?: { - ADD_ATTR?: string[] - ADD_TAGS?: string[]; - }) : string; -} -declare let DOMPurify: DOMPurify; - -declare let remarkable: typeof window.remarkable; - -declare class webkitAudioContext extends AudioContext {} -declare class webkitOfflineAudioContext extends OfflineAudioContext {} - -interface Window { - readonly webkitAudioContext: typeof webkitAudioContext; - readonly AudioContext: typeof webkitAudioContext; - readonly OfflineAudioContext: typeof OfflineAudioContext; - readonly webkitOfflineAudioContext: typeof webkitOfflineAudioContext; - readonly RTCPeerConnection: typeof RTCPeerConnection; - readonly Pointer_stringify: any; - readonly jsrender: any; - - twemoji: Twemoji; - hljs: HighlightJS; - remarkable: any; - - require(id: string): any; -} - -interface Navigator { - browserSpecs: { - name: string, - version: string - }; - - mozGetUserMedia(constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback): void; - webkitGetUserMedia(constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback): void; } \ No newline at end of file diff --git a/shared/js/settings.ts b/shared/js/settings.ts index 59a9c8eb..10ad216a 100644 --- a/shared/js/settings.ts +++ b/shared/js/settings.ts @@ -1,6 +1,10 @@ -/// //Used by CertAccept popup +import {createErrorModal} from "tc-shared/ui/elements/Modal"; +import {LogCategory} from "tc-shared/log"; +import * as loader from "tc-loader"; +import * as log from "tc-shared/log"; + if(typeof(customElements) !== "undefined") { try { class X_Properties extends HTMLElement {} @@ -9,12 +13,12 @@ if(typeof(customElements) !== "undefined") { customElements.define('x-properties', X_Properties, { extends: 'div' }); customElements.define('x-property', X_Property, { extends: 'div' }); } catch(error) { - console.warn("failed to define costum elements"); + console.warn("failed to define costume elements"); } } /* T = value type */ -interface SettingsKey { +export interface SettingsKey { key: string; fallback_keys?: string | string[]; @@ -25,7 +29,7 @@ interface SettingsKey { require_restart?: boolean; } -class SettingsBase { +export class SettingsBase { protected static readonly UPDATE_DIRECT: boolean = true; protected static transformStO?(input?: string, _default?: T, default_type?: string) : T { @@ -77,7 +81,7 @@ class SettingsBase { } } -class StaticSettings extends SettingsBase { +export class StaticSettings extends SettingsBase { private static _instance: StaticSettings; static get instance() : StaticSettings { if(!this._instance) @@ -139,7 +143,7 @@ class StaticSettings extends SettingsBase { } } -class Settings extends StaticSettings { +export class Settings extends StaticSettings { static readonly KEY_USER_IS_NEW: SettingsKey = { key: 'user_is_new_user', default_value: true @@ -433,7 +437,7 @@ class Settings extends StaticSettings { } } -class ServerSettings extends SettingsBase { +export class ServerSettings extends SettingsBase { private cacheServer = {}; private _server_unique_id: string; private _server_save_worker: NodeJS.Timer; @@ -511,4 +515,4 @@ class ServerSettings extends SettingsBase { } } -let settings: Settings; \ No newline at end of file +export let settings: Settings; \ No newline at end of file diff --git a/shared/js/sound/Sounds.ts b/shared/js/sound/Sounds.ts index cbb75329..7f58514c 100644 --- a/shared/js/sound/Sounds.ts +++ b/shared/js/sound/Sounds.ts @@ -1,4 +1,10 @@ -enum Sound { +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import {settings} from "tc-shared/settings"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import * as sbackend from "tc-backend/audio/sounds"; + +export enum Sound { SOUND_TEST = "sound.test", SOUND_EGG = "sound.egg", @@ -61,235 +67,211 @@ enum Sound { GROUP_CHANNEL_CHANGED_SELF = "group.channel.changed.self" } -namespace sound { - export interface SoundHandle { - key: string; - filename: string; - } +export interface SoundHandle { + key: string; + filename: string; +} - export interface SoundFile { - path: string; - volume?: number; - } +export interface SoundFile { + path: string; + volume?: number; +} - let speech_mapping: {[key: string]:SoundHandle} = {}; +let speech_mapping: {[key: string]:SoundHandle} = {}; - let volume_require_save = false; - let speech_volume: {[key: string]:number} = {}; - let master_volume: number; +let volume_require_save = false; +let speech_volume: {[key: string]:number} = {}; +let master_volume: number; - let overlap_sounds: boolean; - let ignore_muted: boolean; +let overlap_sounds: boolean; +let ignore_muted: boolean; - let master_mixed: GainNode; +let master_mixed: GainNode; - function register_sound(key: string, file: string) { - speech_mapping[key] = {key: key, filename: file} as SoundHandle; - } +function register_sound(key: string, file: string) { + speech_mapping[key] = {key: key, filename: file} as SoundHandle; +} - export function get_sound_volume(sound: Sound, default_volume?: number) : number { - let result = speech_volume[sound]; - if(typeof(result) === "undefined") { - if(typeof(default_volume) !== "undefined") - result = default_volume; - else - result = 1; - } - return result; - } - - export function set_sound_volume(sound: Sound, volume: number) { - volume_require_save = volume_require_save || speech_volume[sound] != volume; - speech_volume[sound] = volume == 1 ? undefined : volume; - } - - export function get_master_volume() : number { - return master_volume; - } - - export function set_master_volume(volume: number) { - volume_require_save = volume_require_save || master_volume != volume; - master_volume = volume; - if(master_mixed) { - if(master_mixed.gain.setValueAtTime) - master_mixed.gain.setValueAtTime(volume, 0); - else - master_mixed.gain.value = volume; - } - } - - export function overlap_activated() : boolean { - return overlap_sounds; - } - - export function set_overlap_activated(flag: boolean) { - volume_require_save = volume_require_save || overlap_sounds != flag; - overlap_sounds = flag; - } - - export function ignore_output_muted() : boolean { - return ignore_muted; - } - - export function set_ignore_output_muted(flag: boolean) { - volume_require_save = volume_require_save || ignore_muted != flag; - ignore_muted = flag; - } - - export function reinitialisize_audio() { - const context = audio.player.context(); - const destination = audio.player.destination(); - - if(master_mixed) - master_mixed.disconnect(); - - master_mixed = context.createGain(); - if(master_mixed.gain.setValueAtTime) - master_mixed.gain.setValueAtTime(master_volume, 0); +export function get_sound_volume(sound: Sound, default_volume?: number) : number { + let result = speech_volume[sound]; + if(typeof(result) === "undefined") { + if(typeof(default_volume) !== "undefined") + result = default_volume; else - master_mixed.gain.value = master_volume; - master_mixed.connect(destination); + result = 1; } + return result; +} - export function save() { - if(volume_require_save) { - volume_require_save = false; +export function set_sound_volume(sound: Sound, volume: number) { + volume_require_save = volume_require_save || speech_volume[sound] != volume; + speech_volume[sound] = volume == 1 ? undefined : volume; +} - const data: any = {}; - data.version = 1; +export function get_master_volume() : number { + return master_volume; +} - for(const key in Sound) { - if(typeof(speech_volume[Sound[key]]) !== "undefined") - data[Sound[key]] = speech_volume[Sound[key]]; - } - data.master = master_volume; - data.overlap = overlap_sounds; - data.ignore_muted = ignore_muted; +export function set_master_volume(volume: number) { + volume_require_save = volume_require_save || master_volume != volume; + master_volume = volume; + if(master_mixed) { + if(master_mixed.gain.setValueAtTime) + master_mixed.gain.setValueAtTime(volume, 0); + else + master_mixed.gain.value = volume; + } +} - settings.changeGlobal("sound_volume", JSON.stringify(data)); +export function overlap_activated() : boolean { + return overlap_sounds; +} + +export function set_overlap_activated(flag: boolean) { + volume_require_save = volume_require_save || overlap_sounds != flag; + overlap_sounds = flag; +} + +export function ignore_output_muted() : boolean { + return ignore_muted; +} + +export function set_ignore_output_muted(flag: boolean) { + volume_require_save = volume_require_save || ignore_muted != flag; + ignore_muted = flag; +} + +export function save() { + if(volume_require_save) { + volume_require_save = false; + + const data: any = {}; + data.version = 1; + + for(const key of Object.keys(Sound)) { + if(typeof(speech_volume[Sound[key]]) !== "undefined") + data[Sound[key]] = speech_volume[Sound[key]]; } + data.master = master_volume; + data.overlap = overlap_sounds; + data.ignore_muted = ignore_muted; + + settings.changeGlobal("sound_volume", JSON.stringify(data)); + } +} + +export function initialize() : Promise { + $.ajaxSetup({ + beforeSend: function(jqXHR,settings){ + if (settings.dataType === 'binary') { + settings.xhr().responseType = 'arraybuffer'; + settings.processData = false; + } + } + }); + + /* volumes */ + { + const data = JSON.parse(settings.static_global("sound_volume", "{}")); + for(const sound_key of Object.keys(Sound)) { + if(typeof(data[Sound[sound_key]]) !== "undefined") + speech_volume[Sound[sound_key]] = data[Sound[sound_key]]; + } + + master_volume = typeof(data.master) === "number" ? data.master : 1; + overlap_sounds = typeof(data.overlap) === "boolean" ? data.overlap : true; + ignore_muted = typeof(data.ignore_muted) === "boolean" ? data.ignore_muted : false; } - export function initialize() : Promise { - $.ajaxSetup({ - beforeSend: function(jqXHR,settings){ - if (settings.dataType === 'binary') { - settings.xhr().responseType = 'arraybuffer'; - settings.processData = false; - } - } + register_sound("message.received", "effects/message_received.wav"); + register_sound("message.send", "effects/message_send.wav"); + + manager = new SoundManager(undefined); + return new Promise((resolve, reject) => { + $.ajax({ + url: "audio/speech/mapping.json", + success: response => { + if(typeof(response) === "string") + response = JSON.parse(response); + for(const entry of response) + register_sound(entry.key, "speech/" + entry.file); + resolve(); + }, + error: error => { + log.error(LogCategory.AUDIO, "error: %o", error); + reject(); + }, + timeout: 5000, + async: true, + type: 'GET' }); + }); +} - /* volumes */ - { - const data = JSON.parse(settings.static_global("sound_volume", "{}")); - for(const sound_key in Sound) { - if(typeof(data[Sound[sound_key]]) !== "undefined") - speech_volume[Sound[sound_key]] = data[Sound[sound_key]]; - } +export interface PlaybackOptions { + ignore_muted?: boolean; + ignore_overlap?: boolean; - master_volume = typeof(data.master) === "number" ? data.master : 1; - overlap_sounds = typeof(data.overlap) === "boolean" ? data.overlap : true; - ignore_muted = typeof(data.ignore_muted) === "boolean" ? data.ignore_muted : false; - } + default_volume?: number; - register_sound("message.received", "effects/message_received.wav"); - register_sound("message.send", "effects/message_send.wav"); + callback?: (flag: boolean) => any; +} - manager = new SoundManager(undefined); - audio.player.on_ready(reinitialisize_audio); - return new Promise((resolve, reject) => { - $.ajax({ - url: "audio/speech/mapping.json", - success: response => { - if(typeof(response) === "string") - response = JSON.parse(response); - for(const entry of response) - register_sound(entry.key, "speech/" + entry.file); - resolve(); - }, - error: error => { - log.error(LogCategory.AUDIO, "error: %o", error); - reject(); - }, - timeout: 5000, - async: true, - type: 'GET' - }); - }); +export async function resolve_sound(sound: Sound) : Promise { + const file: SoundHandle = speech_mapping[sound]; + if(!file) throw tr("Missing sound handle"); + + return file; +} + +export let manager: SoundManager; + +export class SoundManager { + private readonly _handle: ConnectionHandler; + private _playing_sounds: {[key: string]:number} = {}; + + constructor(handle: ConnectionHandler) { + this._handle = handle; } - export interface PlaybackOptions { - ignore_muted?: boolean; - ignore_overlap?: boolean; + play(_sound: Sound, options?: PlaybackOptions) { + options = options || {}; - default_volume?: number; + const volume = get_sound_volume(_sound, options.default_volume); + log.info(LogCategory.AUDIO, tr("Replaying sound %s (Sound volume: %o | Master volume %o)"), _sound, volume, master_volume); - callback?: (flag: boolean) => any; - } + if(volume == 0 || master_volume == 0) + return; - export async function resolve_sound(sound: Sound) : Promise { - const file: SoundHandle = speech_mapping[sound]; - if(!file) throw tr("Missing sound handle"); + if(this._handle && !options.ignore_muted && !ignore_output_muted() && this._handle.client_status.output_muted) + return; - return file; - } + resolve_sound(_sound).then(handle => { + if(!handle) return; - export let manager: SoundManager; - - export class SoundManager { - private readonly _handle: ConnectionHandler; - private _playing_sounds: {[key: string]:number} = {}; - - constructor(handle: ConnectionHandler) { - this._handle = handle; - } - - play(_sound: Sound, options?: PlaybackOptions) { - options = options || {}; - - const volume = get_sound_volume(_sound, options.default_volume); - log.info(LogCategory.AUDIO, tr("Replaying sound %s (Sound volume: %o | Master volume %o)"), _sound, volume, master_volume); - - if(volume == 0 || master_volume == 0) - return; - - if(this._handle && !options.ignore_muted && !sound.ignore_output_muted() && this._handle.client_status.output_muted) - return; - - const context = audio.player.context(); - if(!context) { - log.warn(LogCategory.AUDIO, tr("Tried to replay a sound without an audio context (Sound: %o). Dropping playback"), _sound); + if(!options.ignore_overlap && (this._playing_sounds[handle.filename] > 0) && !overlap_activated()) { + log.info(LogCategory.AUDIO, tr("Dropping requested playback for sound %s because it would overlap."), _sound); return; } - sound.resolve_sound(_sound).then(handle => { - if(!handle) return; - - if(!options.ignore_overlap && (this._playing_sounds[handle.filename] > 0) && !sound.overlap_activated()) { - log.info(LogCategory.AUDIO, tr("Dropping requested playback for sound %s because it would overlap."), _sound); - return; - } - - this._playing_sounds[handle.filename] = (this._playing_sounds[handle.filename] || 0) + 1; - audio.sounds.play_sound({ - path: "audio/" + handle.filename, - volume: volume * master_volume - }).then(() => { - if(options.callback) - options.callback(true); - }).catch(error => { - log.warn(LogCategory.AUDIO, tr("Failed to replay sound %s: %o"), handle.filename, error); - if(options.callback) - options.callback(false); - }).then(() => { - this._playing_sounds[handle.filename]--; - }); + this._playing_sounds[handle.filename] = (this._playing_sounds[handle.filename] || 0) + 1; + sbackend.play_sound({ + path: "audio/" + handle.filename, + volume: volume * master_volume + }).then(() => { + if(options.callback) + options.callback(true); }).catch(error => { - log.warn(LogCategory.AUDIO, tr("Failed to replay sound %o because it could not be resolved: %o"), sound, error); + log.warn(LogCategory.AUDIO, tr("Failed to replay sound %s: %o"), handle.filename, error); if(options.callback) options.callback(false); + }).then(() => { + this._playing_sounds[handle.filename]--; }); - } + }).catch(error => { + log.warn(LogCategory.AUDIO, tr("Failed to replay sound %o because it could not be resolved: %o"), _sound, error); + if(options.callback) + options.callback(false); + }); } } \ No newline at end of file diff --git a/shared/js/stats.ts b/shared/js/stats.ts index ce9ca84b..f18df9b7 100644 --- a/shared/js/stats.ts +++ b/shared/js/stats.ts @@ -1,243 +1,244 @@ -namespace stats { - const LOG_PREFIX = "[Statistics] "; +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; - export enum CloseCodes { - UNSET = 3000, - RECONNECT = 3001, - INTERNAL_ERROR = 3002, +const LOG_PREFIX = "[Statistics] "; - BANNED = 3100, +enum CloseCodes { + UNSET = 3000, + RECONNECT = 3001, + INTERNAL_ERROR = 3002, + + BANNED = 3100, +} + +enum ConnectionState { + CONNECTING, + INITIALIZING, + CONNECTED, + UNSET +} + +export class SessionConfig { + /* + * All collected statistics will only be cached by the stats server. + * No data will be saved. + */ + volatile_collection_only?: boolean; + + /* + * Anonymize all IP addresses which will be provided while the stats collection. + * This option is quite useless when volatile_collection_only is active. + */ + anonymize_ip_addresses?: boolean; +} + +export class Config extends SessionConfig { + verbose?: boolean; + + reconnect_interval?: number; +} + +export interface UserCountData { + online_users: number; + unique_online_users: number; +} + +export type UserCountListener = (data: UserCountData) => any; + +let reconnect_timer: NodeJS.Timer; +let current_config: Config; + +let last_user_count_update: number; +let user_count_listener: UserCountListener[] = []; + +const DEFAULT_CONFIG: Config = { + verbose: true, + reconnect_interval: 5000, + anonymize_ip_addresses: true, + volatile_collection_only: false +}; + +function initialize_config_object(target_object: any, source_object: any) : any { + for(const key of Object.keys(source_object)) { + if(typeof(source_object[key]) === 'object') + initialize_config_object(target_object[key] || (target_object[key] = {}), source_object[key]); + + if(typeof(target_object[key]) !== 'undefined') + continue; + + target_object[key] = source_object[key]; } - enum ConnectionState { - CONNECTING, - INITIALIZING, - CONNECTED, - UNSET - } + return target_object; +} - export class SessionConfig { - /* - * All collected statistics will only be cached by the stats server. - * No data will be saved. - */ - volatile_collection_only?: boolean; +export function initialize(config: Config) { + current_config = initialize_config_object(config || {}, DEFAULT_CONFIG); + if(current_config.verbose) + log.info(LogCategory.STATISTICS, tr("Initializing statistics with this config: %o"), current_config); - /* - * Anonymize all IP addresses which will be provided while the stats collection. - * This option is quite useless when volatile_collection_only is active. - */ - anonymize_ip_addresses?: boolean; - } + connection.start_connection(); +} - export class Config extends SessionConfig { - verbose?: boolean; +export function register_user_count_listener(listener: UserCountListener) { + user_count_listener.push(listener); +} - reconnect_interval?: number; - } +export function all_user_count_listener() : UserCountListener[] { + return user_count_listener; +} - export interface UserCountData { - online_users: number; - unique_online_users: number; - } +export function deregister_user_count_listener(listener: UserCountListener) { + user_count_listener.remove(listener); +} - export type UserCountListener = (data: UserCountData) => any; +namespace connection { + let connection: WebSocket; + export let connection_state: ConnectionState = ConnectionState.UNSET; - let reconnect_timer: NodeJS.Timer; - let current_config: Config; + export function start_connection() { + cancel_reconnect(); + close_connection(); - let last_user_count_update: number; - let user_count_listener: UserCountListener[] = []; + connection_state = ConnectionState.CONNECTING; - const DEFAULT_CONFIG: Config = { - verbose: true, - reconnect_interval: 5000, - anonymize_ip_addresses: true, - volatile_collection_only: false - }; + connection = new WebSocket('wss://web-stats.teaspeak.de:27790'); + if(!connection) + connection = new WebSocket('wss://localhost:27788'); - function initialize_config_object(target_object: any, source_object: any) : any { - for(const key of Object.keys(source_object)) { - if(typeof(source_object[key]) === 'object') - initialize_config_object(target_object[key] || (target_object[key] = {}), source_object[key]); + { + const connection_copy = connection; + connection.onclose = (event: CloseEvent) => { + if(connection_copy !== connection) return; - if(typeof(target_object[key]) !== 'undefined') - continue; + if(current_config.verbose) + log.warn(LogCategory.STATISTICS, tr("Lost connection to statistics server (Connection closed). Reason: %o. Event object: %o"), CloseCodes[event.code] || event.code, event); - target_object[key] = source_object[key]; - } - - return target_object; - } - - export function initialize(config: Config) { - current_config = initialize_config_object(config || {}, DEFAULT_CONFIG); - if(current_config.verbose) - log.info(LogCategory.STATISTICS, tr("Initializing statistics with this config: %o"), current_config); - - connection.start_connection(); - } - - export function register_user_count_listener(listener: UserCountListener) { - user_count_listener.push(listener); - } - - export function all_user_count_listener() : UserCountListener[] { - return user_count_listener; - } - - export function deregister_user_count_listener(listener: UserCountListener) { - user_count_listener.remove(listener); - } - - namespace connection { - let connection: WebSocket; - export let connection_state: ConnectionState = ConnectionState.UNSET; - - export function start_connection() { - cancel_reconnect(); - close_connection(); - - connection_state = ConnectionState.CONNECTING; - - connection = new WebSocket('wss://web-stats.teaspeak.de:27790'); - if(!connection) - connection = new WebSocket('wss://localhost:27788'); - - { - const connection_copy = connection; - connection.onclose = (event: CloseEvent) => { - if(connection_copy !== connection) return; - - if(current_config.verbose) - log.warn(LogCategory.STATISTICS, tr("Lost connection to statistics server (Connection closed). Reason: %o. Event object: %o"), CloseCodes[event.code] || event.code, event); - - if(event.code != CloseCodes.BANNED) - invoke_reconnect(); - }; - - connection.onopen = () => { - if(connection_copy !== connection) return; - - if(current_config.verbose) - log.info(LogCategory.STATISTICS, tr("Successfully connected to server. Initializing session.")); - - connection_state = ConnectionState.INITIALIZING; - initialize_session(); - }; - - connection.onerror = (event: ErrorEvent) => { - if(connection_copy !== connection) return; - - if(current_config.verbose) - log.warn(LogCategory.STATISTICS, tr("Received an error. Closing connection. Object: %o"), event); - - connection.close(CloseCodes.INTERNAL_ERROR); + if(event.code != CloseCodes.BANNED) invoke_reconnect(); - }; + }; - connection.onmessage = (event: MessageEvent) => { - if(connection_copy !== connection) return; + connection.onopen = () => { + if(connection_copy !== connection) return; - if(typeof(event.data) !== 'string') { - if(current_config.verbose) - log.info(LogCategory.STATISTICS, tr("Received an message which isn't a string. Event object: %o"), event); - return; - } + if(current_config.verbose) + log.info(LogCategory.STATISTICS, tr("Successfully connected to server. Initializing session.")); - handle_message(event.data as string); - }; - } + connection_state = ConnectionState.INITIALIZING; + initialize_session(); + }; + + connection.onerror = (event: ErrorEvent) => { + if(connection_copy !== connection) return; + + if(current_config.verbose) + log.warn(LogCategory.STATISTICS, tr("Received an error. Closing connection. Object: %o"), event); + + connection.close(CloseCodes.INTERNAL_ERROR); + invoke_reconnect(); + }; + + connection.onmessage = (event: MessageEvent) => { + if(connection_copy !== connection) return; + + if(typeof(event.data) !== 'string') { + if(current_config.verbose) + log.info(LogCategory.STATISTICS, tr("Received an message which isn't a string. Event object: %o"), event); + return; + } + + handle_message(event.data as string); + }; + } + } + + export function close_connection() { + if(connection) { + const connection_copy = connection; + connection = undefined; + + try { + connection_copy.close(3001); + } catch(_) {} + } + } + + function invoke_reconnect() { + close_connection(); + + if(reconnect_timer) { + clearTimeout(reconnect_timer); + reconnect_timer = undefined; } - export function close_connection() { - if(connection) { - const connection_copy = connection; - connection = undefined; - - try { - connection_copy.close(3001); - } catch(_) {} - } - } - - function invoke_reconnect() { - close_connection(); - - if(reconnect_timer) { - clearTimeout(reconnect_timer); - reconnect_timer = undefined; - } + if(current_config.verbose) + log.info(LogCategory.STATISTICS, tr("Scheduled reconnect in %dms"), current_config.reconnect_interval); + reconnect_timer = setTimeout(() => { if(current_config.verbose) - log.info(LogCategory.STATISTICS, tr("Scheduled reconnect in %dms"), current_config.reconnect_interval); + log.info(LogCategory.STATISTICS, tr("Reconnecting")); + start_connection(); + }, current_config.reconnect_interval); + } - reconnect_timer = setTimeout(() => { - if(current_config.verbose) - log.info(LogCategory.STATISTICS, tr("Reconnecting")); - start_connection(); - }, current_config.reconnect_interval); + export function cancel_reconnect() { + if(reconnect_timer) { + clearTimeout(reconnect_timer); + reconnect_timer = undefined; + } + } + + function send_message(type: string, data: any) { + connection.send(JSON.stringify({ + type: type, + data: data + })); + } + + function initialize_session() { + const config_object = {}; + for(const key in SessionConfig) { + if(SessionConfig.hasOwnProperty(key)) + config_object[key] = current_config[key]; } - export function cancel_reconnect() { - if(reconnect_timer) { - clearTimeout(reconnect_timer); - reconnect_timer = undefined; - } + send_message('initialize', { + config: config_object + }) + } + + function handle_message(message: string) { + const data_object = JSON.parse(message); + const type = data_object.type as string; + const data = data_object.data; + + if(typeof(handler[type]) === 'function') { + if(current_config.verbose) + log.debug(LogCategory.STATISTICS, tr("Handling message of type %s"), type); + handler[type](data); + } else if(current_config.verbose) { + log.warn(LogCategory.STATISTICS, tr("Received message with an unknown type (%s). Dropping message. Full message: %o"), type, data_object); + } + } + + namespace handler { + interface NotifyUserCount extends UserCountData { } + + function handle_notify_user_count(data: NotifyUserCount) { + last_user_count_update = Date.now(); + for(const listener of [...user_count_listener]) + listener(data); } - function send_message(type: string, data: any) { - connection.send(JSON.stringify({ - type: type, - data: data - })); + interface NotifyInitialized {} + function handle_notify_initialized(json: NotifyInitialized) { + if(current_config.verbose) + log.info(LogCategory.STATISTICS, tr("Session successfully initialized.")); + + connection_state = ConnectionState.CONNECTED; } - function initialize_session() { - const config_object = {}; - for(const key in SessionConfig) { - if(SessionConfig.hasOwnProperty(key)) - config_object[key] = current_config[key]; - } - - send_message('initialize', { - config: config_object - }) - } - - function handle_message(message: string) { - const data_object = JSON.parse(message); - const type = data_object.type as string; - const data = data_object.data; - - if(typeof(handler[type]) === 'function') { - if(current_config.verbose) - log.debug(LogCategory.STATISTICS, tr("Handling message of type %s"), type); - handler[type](data); - } else if(current_config.verbose) { - log.warn(LogCategory.STATISTICS, tr("Received message with an unknown type (%s). Dropping message. Full message: %o"), type, data_object); - } - } - - namespace handler { - interface NotifyUserCount extends UserCountData { } - - function handle_notify_user_count(data: NotifyUserCount) { - last_user_count_update = Date.now(); - for(const listener of [...user_count_listener]) - listener(data); - } - - interface NotifyInitialized {} - function handle_notify_initialized(json: NotifyInitialized) { - if(current_config.verbose) - log.info(LogCategory.STATISTICS, tr("Session successfully initialized.")); - - connection_state = ConnectionState.CONNECTED; - } - - handler["notifyinitialized"] = handle_notify_initialized; - handler["notifyusercount"] = handle_notify_user_count; - } + handler["notifyinitialized"] = handle_notify_initialized; + handler["notifyusercount"] = handle_notify_user_count; } } \ No newline at end of file diff --git a/shared/js/ui/channel.ts b/shared/js/ui/channel.ts index 1a510f05..7dc818e4 100644 --- a/shared/js/ui/channel.ts +++ b/shared/js/ui/channel.ts @@ -1,12 +1,26 @@ -/// -/// +import {ChannelTree} from "tc-shared/ui/view"; +import {ClientEntry} from "tc-shared/ui/client"; +import * as log from "tc-shared/log"; +import {LogCategory, LogType} from "tc-shared/log"; +import PermissionType from "tc-shared/permission/PermissionType"; +import {settings, Settings} from "tc-shared/settings"; +import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; +import {Sound} from "tc-shared/sound/Sounds"; +import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal"; +import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; +import * as htmltags from "./htmltags"; +import {hashPassword} from "tc-shared/utils/helpers"; +import * as server_log from "tc-shared/ui/frames/server_log"; +import {openChannelInfo} from "tc-shared/ui/modal/ModalChannelInfo"; +import {createChannelModal} from "tc-shared/ui/modal/ModalCreateChannel"; +import {formatMessage} from "tc-shared/ui/frames/chat"; -enum ChannelType { +export enum ChannelType { PERMANENT, SEMI_PERMANENT, TEMPORARY } -namespace ChannelType { +export namespace ChannelType { export function normalize(mode: ChannelType) { let value: string = ChannelType[mode]; value = value.toLowerCase(); @@ -14,13 +28,13 @@ namespace ChannelType { } } -enum ChannelSubscribeMode { +export enum ChannelSubscribeMode { SUBSCRIBED, UNSUBSCRIBED, INHERITED } -class ChannelProperties { +export class ChannelProperties { channel_order: number = 0; channel_name: string = ""; channel_name_phonetic: string = ""; @@ -55,7 +69,7 @@ class ChannelProperties { channel_conversation_history_length: number = -1; } -class ChannelEntry { +export class ChannelEntry { channelTree: ChannelTree; channelId: number; parent?: ChannelEntry; @@ -525,7 +539,7 @@ class ChannelEntry { name: tr("Show channel info"), callback: () => { trigger_close = false; - Modals.openChannelInfo(this); + openChannelInfo(this); }, icon_class: "client-about" }, @@ -565,7 +579,7 @@ class ChannelEntry { name: tr("Edit channel"), invalidPermission: !channelModify, callback: () => { - Modals.createChannelModal(this.channelTree.client, this, undefined, this.channelTree.client.permissions, (changes?, permissions?) => { + createChannelModal(this.channelTree.client, this, undefined, this.channelTree.client.permissions, (changes?, permissions?) => { if(changes) { changes["cid"] = this.channelId; this.channelTree.client.serverConnection.send_command("channeledit", changes); @@ -617,7 +631,7 @@ class ChannelEntry { error = error.extra_message || error.message; } - createErrorModal(tr("Failed to create bot"), MessageHelper.formatMessage(tr("Failed to create the music bot:
{0}"), error)).open(); + createErrorModal(tr("Failed to create bot"), formatMessage(tr("Failed to create the music bot:
{0}"), error)).open(); }); } }, @@ -834,7 +848,7 @@ class ChannelEntry { !this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_JOIN_IGNORE_PASSWORD).granted(1)) { createInputModal(tr("Channel password"), tr("Channel password:"), () => true, text => { if(typeof(text) == typeof(true)) return; - helpers.hashPassword(text as string).then(result => { + hashPassword(text as string).then(result => { this._cachedPassword = result; this.joinChannel(); this.updateChannelTypeIcon(); @@ -923,7 +937,7 @@ class ChannelEntry { this._tag_channel.find(".marker-text-unread").toggleClass("hidden", !flag); } - log_data() : log.server.base.Channel { + log_data() : server_log.base.Channel { return { channel_name: this.channelName(), channel_id: this.channelId diff --git a/shared/js/ui/client.ts b/shared/js/ui/client.ts index d898f8f7..d6a73852 100644 --- a/shared/js/ui/client.ts +++ b/shared/js/ui/client.ts @@ -1,8 +1,34 @@ -/// -/// -/// +import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; +import {channel_tree, Registry} from "tc-shared/events"; +import {ChannelTree} from "tc-shared/ui/view"; +import * as log from "tc-shared/log"; +import {LogCategory, LogType} from "tc-shared/log"; +import {Settings, settings} from "tc-shared/settings"; +import {KeyCode, SpecialKey} from "tc-shared/PPTListener"; +import {Sound} from "tc-shared/sound/Sounds"; +import {Group, GroupManager, GroupTarget, GroupType} from "tc-shared/permission/GroupManager"; +import PermissionType from "tc-shared/permission/PermissionType"; +import {createErrorModal, createInputModal} from "tc-shared/ui/elements/Modal"; +import * as htmltags from "tc-shared/ui/htmltags"; +import * as server_log from "tc-shared/ui/frames/server_log"; +import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; +import {ChannelEntry} from "tc-shared/ui/channel"; +import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler"; +import {voice} from "tc-shared/connection/ConnectionBase"; +import VoiceClient = voice.VoiceClient; +import {spawnPermissionEdit} from "tc-shared/ui/modal/permission/ModalPermissionEdit"; +import {createServerGroupAssignmentModal} from "tc-shared/ui/modal/ModalGroupAssignment"; +import {openClientInfo} from "tc-shared/ui/modal/ModalClientInfo"; +import {spawnBanClient} from "tc-shared/ui/modal/ModalBanClient"; +import {spawnChangeVolume} from "tc-shared/ui/modal/ModalChangeVolume"; +import {spawnChangeLatency} from "tc-shared/ui/modal/ModalChangeLatency"; +import {spawnPlaylistEdit} from "tc-shared/ui/modal/ModalPlaylistEdit"; +import {formatMessage} from "tc-shared/ui/frames/chat"; +import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; +import * as ppt from "tc-backend/ppt"; +import * as hex from "tc-shared/crypto/hex"; -enum ClientType { +export enum ClientType { CLIENT_VOICE, CLIENT_QUERY, CLIENT_INTERNAL, @@ -11,7 +37,7 @@ enum ClientType { CLIENT_UNDEFINED } -class ClientProperties { +export class ClientProperties { client_type: ClientType = ClientType.CLIENT_VOICE; //TeamSpeaks type client_type_exact: ClientType = ClientType.CLIENT_VOICE; @@ -57,7 +83,7 @@ class ClientProperties { client_is_priority_speaker: boolean = false; } -class ClientConnectionInfo { +export class ClientConnectionInfo { connection_bandwidth_received_last_minute_control: number = -1; connection_bandwidth_received_last_minute_keepalive: number = -1; connection_bandwidth_received_last_minute_speech: number = -1; @@ -109,8 +135,8 @@ class ClientConnectionInfo { connection_client_port: number = -1; } -class ClientEntry { - readonly events: events.Registry; +export class ClientEntry { + readonly events: Registry; protected _clientId: number; protected _channel: ChannelEntry; @@ -121,7 +147,7 @@ class ClientEntry { protected _speaking: boolean; protected _listener_initialized: boolean; - protected _audio_handle: connection.voice.VoiceClient; + protected _audio_handle: VoiceClient; protected _audio_volume: number; protected _audio_muted: boolean; @@ -136,7 +162,7 @@ class ClientEntry { channelTree: ChannelTree; constructor(clientId: number, clientName, properties: ClientProperties = new ClientProperties()) { - this.events = new events.Registry(); + this.events = new Registry(); this._properties = properties; this._properties.client_nickname = clientName; @@ -181,7 +207,7 @@ class ClientEntry { this._channel = undefined; } - set_audio_handle(handle: connection.voice.VoiceClient) { + set_audio_handle(handle: VoiceClient) { if(this._audio_handle === handle) return; @@ -200,7 +226,7 @@ class ClientEntry { handle.callback_stopped = () => this.speaking = false; } - get_audio_handle() : connection.voice.VoiceClient { + get_audio_handle() : VoiceClient { return this._audio_handle; } @@ -285,7 +311,7 @@ class ClientEntry { let clients = this.channelTree.currently_selected as (ClientEntry | ClientEntry[]); - if(ppt.key_pressed(ppt.SpecialKey.SHIFT)) { + if(ppt.key_pressed(SpecialKey.SHIFT)) { if(clients != this && !($.isArray(clients) && clients.indexOf(this) != -1)) clients = $.isArray(clients) ? [...clients, this] : [clients, this]; } else { @@ -418,20 +444,20 @@ class ClientEntry { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-permission_client", name: tr("Client permissions"), - callback: () => Modals.spawnPermissionEdit(this.channelTree.client, "clp", {unique_id: this.clientUid()}).open() + callback: () => spawnPermissionEdit(this.channelTree.client, "clp", {unique_id: this.clientUid()}).open() }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-permission_client", name: tr("Client channel permissions"), - callback: () => Modals.spawnPermissionEdit(this.channelTree.client, "clchp", {unique_id: this.clientUid(), channel_id: this._channel ? this._channel.channelId : undefined }).open() + callback: () => spawnPermissionEdit(this.channelTree.client, "clchp", {unique_id: this.clientUid(), channel_id: this._channel ? this._channel.channelId : undefined }).open() } ] }]; } open_assignment_modal() { - Modals.createServerGroupAssignmentModal(this, (groups, flag) => { + createServerGroupAssignmentModal(this, (groups, flag) => { if(groups.length == 0) return Promise.resolve(true); if(groups.length == 1) { @@ -490,7 +516,7 @@ class ClientEntry { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-about", name: tr("Show client info"), - callback: () => Modals.openClientInfo(this) + callback: () => openClientInfo(this) }, contextmenu.Entry.HR(), { @@ -582,7 +608,7 @@ class ClientEntry { name: tr("Ban client"), invalidPermission: !this.channelTree.client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).granted(1), callback: () => { - Modals.spawnBanClient(this.channelTree.client, [{ + spawnBanClient(this.channelTree.client, [{ name: this.properties.client_nickname, unique_id: this.properties.client_unique_identifier }], (data) => { @@ -622,7 +648,7 @@ class ClientEntry { icon_class: "client-volume", name: tr("Change Volume"), callback: () => { - Modals.spawnChangeVolume(this, true, this._audio_volume, undefined, volume => { + spawnChangeVolume(this, true, this._audio_volume, undefined, volume => { this._audio_volume = volume; this.channelTree.client.settings.changeServer("volume_client_" + this.clientUid(), volume); if(this._audio_handle) @@ -635,7 +661,7 @@ class ClientEntry { type: contextmenu.MenuEntryType.ENTRY, name: tr("Change playback latency"), callback: () => { - Modals.spawnChangeLatency(this, this._audio_handle.latency_settings(), () => { + spawnChangeLatency(this, this._audio_handle.latency_settings(), () => { this._audio_handle.reset_latency_settings(); return this._audio_handle.latency_settings(); }, settings => this._audio_handle.latency_settings(settings), this._audio_handle.support_flush ? () => { @@ -843,7 +869,7 @@ class ClientEntry { if(variable.key == "client_nickname") { if(variable.value !== old_value && typeof(old_value) === "string") { if(!(this instanceof LocalClientEntry)) { /* own changes will be logged somewhere else */ - this.channelTree.client.log.log(log.server.Type.CLIENT_NICKNAME_CHANGED, { + this.channelTree.client.log.log(server_log.Type.CLIENT_NICKNAME_CHANGED, { own_client: false, client: this.log_data(), new_name: variable.value, @@ -1082,7 +1108,7 @@ class ClientEntry { this.tag.css('padding-left', (5 + (index + 2) * 16) + "px"); } - log_data() : log.server.base.Client { + log_data() : server_log.base.Client { return { client_unique_id: this.properties.client_unique_identifier, client_name: this.clientNickName(), @@ -1123,7 +1149,7 @@ class ClientEntry { } } -class LocalClientEntry extends ClientEntry { +export class LocalClientEntry extends ClientEntry { handle: ConnectionHandler; private renaming: boolean; @@ -1216,14 +1242,14 @@ class LocalClientEntry extends ClientEntry { const old_name = this.clientNickName(); this.handle.serverConnection.command_helper.updateClient("client_nickname", text).then((e) => { settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, text); - this.channelTree.client.log.log(log.server.Type.CLIENT_NICKNAME_CHANGED, { + this.channelTree.client.log.log(server_log.Type.CLIENT_NICKNAME_CHANGED, { client: this.log_data(), old_name: old_name, new_name: text, own_client: true }); }).catch((e: CommandResult) => { - this.channelTree.client.log.log(log.server.Type.CLIENT_NICKNAME_CHANGE_FAILED, { + this.channelTree.client.log.log(server_log.Type.CLIENT_NICKNAME_CHANGE_FAILED, { reason: e.extra_message }); this.openRename(); @@ -1232,7 +1258,7 @@ class LocalClientEntry extends ClientEntry { } } -class MusicClientProperties extends ClientProperties { +export class MusicClientProperties extends ClientProperties { player_state: number = 0; player_volume: number = 0; @@ -1264,7 +1290,7 @@ class MusicClientProperties extends ClientProperties { } */ -class SongInfo { +export class SongInfo { song_id: number = 0; song_url: string = ""; song_invoker: number = 0; @@ -1277,7 +1303,7 @@ class SongInfo { song_length: number = 0; } -class MusicClientPlayerInfo extends SongInfo { +export class MusicClientPlayerInfo extends SongInfo { bot_id: number = 0; player_state: number = 0; @@ -1290,7 +1316,7 @@ class MusicClientPlayerInfo extends SongInfo { player_description: string = ""; } -class MusicClientEntry extends ClientEntry { +export class MusicClientEntry extends ClientEntry { private _info_promise: Promise; private _info_promise_age: number = 0; private _info_promise_resolve: any; @@ -1366,7 +1392,7 @@ class MusicClientEntry extends ClientEntry { this.channelTree.client.serverConnection.command_helper.request_playlist_list().then(lists => { for(const entry of lists) { if(entry.playlist_id == this.properties.client_playlist_id) { - Modals.spawnPlaylistEdit(this.channelTree.client, entry); + spawnPlaylistEdit(this.channelTree.client, entry); return; } } @@ -1435,7 +1461,7 @@ class MusicClientEntry extends ClientEntry { icon_class: "client-volume", name: tr("Change local volume"), callback: () => { - Modals.spawnChangeVolume(this, true, this._audio_handle.get_volume(), undefined, volume => { + spawnChangeVolume(this, true, this._audio_handle.get_volume(), undefined, volume => { this.channelTree.client.settings.changeServer("volume_client_" + this.clientUid(), volume); this._audio_handle.set_volume(volume); }); @@ -1450,7 +1476,7 @@ class MusicClientEntry extends ClientEntry { if(max_volume < 0) max_volume = 100; - Modals.spawnChangeVolume(this, false, this.properties.player_volume, max_volume / 100, value => { + spawnChangeVolume(this, false, this.properties.player_volume, max_volume / 100, value => { if(typeof(value) !== "number") return; @@ -1467,7 +1493,7 @@ class MusicClientEntry extends ClientEntry { type: contextmenu.MenuEntryType.ENTRY, name: tr("Change playback latency"), callback: () => { - Modals.spawnChangeLatency(this, this._audio_handle.latency_settings(), () => { + spawnChangeLatency(this, this._audio_handle.latency_settings(), () => { this._audio_handle.reset_latency_settings(); return this._audio_handle.latency_settings(); }, settings => this._audio_handle.latency_settings(settings), this._audio_handle.support_flush ? () => { @@ -1482,8 +1508,8 @@ class MusicClientEntry extends ClientEntry { icon_class: "client-delete", disabled: false, callback: () => { - const tag = $.spawn("div").append(MessageHelper.formatMessage(tr("Do you really want to delete {0}"), this.createChatTag(false))); - Modals.spawnYesNo(tr("Are you sure?"), $.spawn("div").append(tag), result => { + const tag = $.spawn("div").append(formatMessage(tr("Do you really want to delete {0}"), this.createChatTag(false))); + spawnYesNo(tr("Are you sure?"), $.spawn("div").append(tag), result => { if(result) { this.channelTree.client.serverConnection.send_command("musicbotdelete", { bot_id: this.properties.client_database_id diff --git a/shared/js/ui/client_move.ts b/shared/js/ui/client_move.ts index 5ccfa8a1..da548c80 100644 --- a/shared/js/ui/client_move.ts +++ b/shared/js/ui/client_move.ts @@ -1,6 +1,10 @@ -/// +import {ChannelTree} from "tc-shared/ui/view"; +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import {ClientEntry} from "tc-shared/ui/client"; +import {ChannelEntry} from "tc-shared/ui/channel"; -class ClientMover { +export class ClientMover { static readonly listener_root = $(document); static readonly move_element = $("#mouse-move"); readonly channel_tree: ChannelTree; diff --git a/shared/js/ui/elements/context_divider.ts b/shared/js/ui/elements/ContextDivider.ts similarity index 94% rename from shared/js/ui/elements/context_divider.ts rename to shared/js/ui/elements/ContextDivider.ts index 3ec65814..597716b4 100644 --- a/shared/js/ui/elements/context_divider.ts +++ b/shared/js/ui/elements/ContextDivider.ts @@ -1,5 +1,15 @@ -interface JQuery { - dividerfy() : this; +import {settings} from "tc-shared/settings"; +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; + +declare global { + interface JQuery { + dividerfy() : this; + } +} + +export function initialize() { + } if(!$.fn.dividerfy) { @@ -58,8 +68,8 @@ if(!$.fn.dividerfy) { Math.max(previous_offset.top + previous_element.height(), next_offset.top + next_element.height()); */ - let previous = 0; - let next = 0; + let previous; + let next; if(current < min) { previous = 0; next = 1; @@ -89,7 +99,7 @@ if(!$.fn.dividerfy) { })); }; - const listener_up = (event: MouseEvent) => { + const listener_up = () => { document.removeEventListener('mousemove', listener_move); document.removeEventListener('touchmove', listener_move); diff --git a/shared/js/ui/elements/context_menu.ts b/shared/js/ui/elements/ContextMenu.ts similarity index 68% rename from shared/js/ui/elements/context_menu.ts rename to shared/js/ui/elements/ContextMenu.ts index f56eb023..159d382a 100644 --- a/shared/js/ui/elements/context_menu.ts +++ b/shared/js/ui/elements/ContextMenu.ts @@ -1,82 +1,80 @@ -namespace contextmenu { - export interface MenuEntry { - callback?: () => void; - type: MenuEntryType; - name: (() => string) | string; - icon_class?: string; - icon_path?: string; - disabled?: boolean; - visible?: boolean; +export interface MenuEntry { + callback?: () => void; + type: MenuEntryType; + name: (() => string) | string; + icon_class?: string; + icon_path?: string; + disabled?: boolean; + visible?: boolean; - checkbox_checked?: boolean; + checkbox_checked?: boolean; - invalidPermission?: boolean; - sub_menu?: MenuEntry[]; - } + invalidPermission?: boolean; + sub_menu?: MenuEntry[]; +} - export enum MenuEntryType { - CLOSE, - ENTRY, - CHECKBOX, - HR, - SUB_MENU - } +export enum MenuEntryType { + CLOSE, + ENTRY, + CHECKBOX, + HR, + SUB_MENU +} - export class Entry { - static HR() { - return { - callback: () => {}, - type: MenuEntryType.HR, - name: "", - icon: "" - }; +export class Entry { + static HR() { + return { + callback: () => {}, + type: MenuEntryType.HR, + name: "", + icon: "" }; + }; - static CLOSE(callback: () => void) { - return { - callback: callback, - type: MenuEntryType.CLOSE, - name: "", - icon: "" - }; - } - } - - export interface ContextMenuProvider { - despawn_context_menu(); - spawn_context_menu(x: number, y: number, ...entries: MenuEntry[]); - - initialize(); - finalize(); - - html_format_enabled() : boolean; - } - - let provider: ContextMenuProvider; - export function spawn_context_menu(x: number, y: number, ...entries: MenuEntry[]) { - if(!provider) { - console.error(tr("Failed to spawn context menu! Missing provider!")); - return; - } - - provider.spawn_context_menu(x, y, ...entries); - } - - export function despawn_context_menu() { - if(!provider) - return; - - provider.despawn_context_menu(); - } - - export function get_provider() : ContextMenuProvider { return provider; } - export function set_provider(_provider: ContextMenuProvider) { - provider = _provider; - provider.initialize(); + static CLOSE(callback: () => void) { + return { + callback: callback, + type: MenuEntryType.CLOSE, + name: "", + icon: "" + }; } } -class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider { +export interface ContextMenuProvider { + despawn_context_menu(); + spawn_context_menu(x: number, y: number, ...entries: MenuEntry[]); + + initialize(); + finalize(); + + html_format_enabled() : boolean; +} + +let provider: ContextMenuProvider; +export function spawn_context_menu(x: number, y: number, ...entries: MenuEntry[]) { + if(!provider) { + console.error(tr("Failed to spawn context menu! Missing provider!")); + return; + } + + provider.spawn_context_menu(x, y, ...entries); +} + +export function despawn_context_menu() { + if(!provider) + return; + + provider.despawn_context_menu(); +} + +export function get_provider() : ContextMenuProvider { return provider; } +export function set_provider(_provider: ContextMenuProvider) { + provider = _provider; + provider.initialize(); +} + +class HTMLContextMenuProvider implements ContextMenuProvider { private _global_click_listener: (event) => any; private _context_menu: JQuery; private _close_callbacks: (() => any)[] = []; @@ -118,10 +116,10 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider { } } - private generate_tag(entry: contextmenu.MenuEntry) : JQuery { - if(entry.type == contextmenu.MenuEntryType.HR) { + private generate_tag(entry: MenuEntry) : JQuery { + if(entry.type == MenuEntryType.HR) { return $.spawn("hr"); - } else if(entry.type == contextmenu.MenuEntryType.ENTRY) { + } else if(entry.type == MenuEntryType.ENTRY) { let icon = entry.icon_class; if(!icon || icon.length == 0) icon = "icon_empty"; else icon = "icon " + icon; @@ -141,7 +139,7 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider { }); } return tag; - } else if(entry.type == contextmenu.MenuEntryType.CHECKBOX) { + } else if(entry.type == MenuEntryType.CHECKBOX) { let checkbox = $.spawn("label").addClass("ccheckbox"); $.spawn("input").attr("type", "checkbox").prop("checked", !!entry.checkbox_checked).appendTo(checkbox); $.spawn("span").addClass("checkmark").appendTo(checkbox); @@ -161,7 +159,7 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider { }); } return tag; - } else if(entry.type == contextmenu.MenuEntryType.SUB_MENU) { + } else if(entry.type == MenuEntryType.SUB_MENU) { let icon = entry.icon_class; if(!icon || icon.length == 0) icon = "icon_empty"; else icon = "icon " + icon; @@ -187,7 +185,7 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider { return $.spawn("div").text("undefined"); } - spawn_context_menu(x: number, y: number, ...entries: contextmenu.MenuEntry[]) { + spawn_context_menu(x: number, y: number, ...entries: MenuEntry[]) { this._visible = true; let menu_tag = this._context_menu || (this._context_menu = $(".context-menu")); @@ -200,7 +198,7 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider { if(typeof(entry.visible) === 'boolean' && !entry.visible) continue; - if(entry.type == contextmenu.MenuEntryType.CLOSE) { + if(entry.type == MenuEntryType.CLOSE) { if(entry.callback) this._close_callbacks.push(entry.callback); } else @@ -225,5 +223,4 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider { return true; } } - -contextmenu.set_provider(new HTMLContextMenuProvider()); \ No newline at end of file +set_provider(new HTMLContextMenuProvider()); \ No newline at end of file diff --git a/shared/js/ui/elements/modal.ts b/shared/js/ui/elements/Modal.ts similarity index 85% rename from shared/js/ui/elements/modal.ts rename to shared/js/ui/elements/Modal.ts index a0030309..1af05d0f 100644 --- a/shared/js/ui/elements/modal.ts +++ b/shared/js/ui/elements/Modal.ts @@ -1,13 +1,13 @@ -/// +import {KeyCode} from "tc-shared/PPTListener"; -enum ElementType { +export enum ElementType { HEADER, BODY, FOOTER } -type BodyCreator = (() => JQuery | JQuery[] | string) | string | JQuery | JQuery[]; -const ModalFunctions = { +export type BodyCreator = (() => JQuery | JQuery[] | string) | string | JQuery | JQuery[]; +export const ModalFunctions = { divify: function (val: JQuery) { if(val.length > 1) return $.spawn("div").append(val); @@ -54,7 +54,7 @@ const ModalFunctions = { } }; -class ModalProperties { +export class ModalProperties { template?: string; header: BodyCreator = () => "HEADER"; body: BodyCreator = () => "BODY"; @@ -184,7 +184,7 @@ let _global_modal_count = 0; let _global_modal_last: HTMLElement; let _global_modal_last_time: number; -class Modal { +export class Modal { private _htmlTag: JQuery; properties: ModalProperties; shown: boolean; @@ -296,11 +296,11 @@ class Modal { } } -function createModal(data: ModalProperties | any) : Modal { +export function createModal(data: ModalProperties | any) : Modal { return new Modal(ModalFunctions.warpProperties(data)); } -class InputModalProperties extends ModalProperties { +export class InputModalProperties extends ModalProperties { maxLength?: number; field_title?: string; @@ -310,7 +310,7 @@ class InputModalProperties extends ModalProperties { error_message?: string; } -function createInputModal(headMessage: BodyCreator, question: BodyCreator, validator: (input: string) => boolean, callback: (flag: boolean | string) => void, props: InputModalProperties | any = {}) : Modal { +export function createInputModal(headMessage: BodyCreator, question: BodyCreator, validator: (input: string) => boolean, callback: (flag: boolean | string) => void, props: InputModalProperties | any = {}) : Modal { props = ModalFunctions.warpProperties(props); props.template_properties || (props.template_properties = {}); props.template_properties.field_title = props.field_title; @@ -370,7 +370,7 @@ function createInputModal(headMessage: BodyCreator, question: BodyCreator, valid return modal; } -function createErrorModal(header: BodyCreator, message: BodyCreator, props: ModalProperties | any = { footer: undefined }) { +export function createErrorModal(header: BodyCreator, message: BodyCreator, props: ModalProperties | any = { footer: undefined }) { props = ModalFunctions.warpProperties(props); (props.template_properties || (props.template_properties = {})).header_class = "modal-header-error"; @@ -382,7 +382,7 @@ function createErrorModal(header: BodyCreator, message: BodyCreator, props: Moda return modal; } -function createInfoModal(header: BodyCreator, message: BodyCreator, props: ModalProperties | any = { footer: undefined }) { +export function createInfoModal(header: BodyCreator, message: BodyCreator, props: ModalProperties | any = { footer: undefined }) { props = ModalFunctions.warpProperties(props); (props.template_properties || (props.template_properties = {})).header_class = "modal-header-info"; @@ -394,52 +394,6 @@ function createInfoModal(header: BodyCreator, message: BodyCreator, props: Modal return modal; } -/* extend jquery */ - -interface ModalElements { - header?: BodyCreator; - body?: BodyCreator; - footer?: BodyCreator; -} - -interface JQuery { - modalize(entry_callback?: (header: JQuery, body: JQuery, footer: JQuery) => ModalElements | void, properties?: ModalProperties | any) : Modal; -} - -$.fn.modalize = function (this: JQuery, entry_callback?: (header: JQuery, body: JQuery, footer: JQuery) => ModalElements | void, properties?: ModalProperties | any) : Modal { - properties = properties || {} as ModalProperties; - entry_callback = entry_callback || ((a,b,c) => undefined); - - let tag_modal = this[0].tagName.toLowerCase() == "modal" ? this : undefined; /* TODO may throw exception? */ - - let tag_head = tag_modal ? tag_modal.find("modal-header") : ModalFunctions.jqueriefy(properties.header); - let tag_body = tag_modal ? tag_modal.find("modal-body") : this; - let tag_footer = tag_modal ? tag_modal.find("modal-footer") : ModalFunctions.jqueriefy(properties.footer); - - const result = entry_callback(tag_head as any, tag_body, tag_footer as any) || {}; - properties.header = result.header || tag_head; - properties.body = result.body || tag_body; - properties.footer = result.footer || tag_footer; - return createModal(properties); -}; - - - - - - - - - - - - - - - - - - diff --git a/shared/js/ui/elements/NetGraph.ts b/shared/js/ui/elements/NetGraph.ts new file mode 100644 index 00000000..6f713191 --- /dev/null +++ b/shared/js/ui/elements/NetGraph.ts @@ -0,0 +1,433 @@ +export type Entry = { + timestamp: number; + + upload?: number; + download?: number; + + highlight?: boolean; +} + +export type Style = { + background_color: string; + + separator_color: string; + separator_count: number; + separator_width: number; + + upload: { + fill: string; + stroke: string; + strike_width: number; + }, + download: { + fill: string; + stroke: string; + strike_width: number; + } +} + +export type TimeSpan = { + origin: { + begin: number; + end: number; + time: number; + }, + target: { + begin: number; + end: number; + time: number; + } +} + +/* Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves + * + * Assuming A was the last point in the line plotted and B is the new point, + * we draw a curve with control points P and Q as below. + * + * A---P + * | + * | + * | + * Q---B + * + * Importantly, A and P are at the same y coordinate, as are B and Q. This is + * so adjacent curves appear to flow as one. + */ +export class Graph { + private static _loops: (() => any)[] = []; + + readonly canvas: HTMLCanvasElement; + public style: Style = { + background_color: "#28292b", + //background_color: "red", + + separator_color: "#283036", + //separator_color: 'blue', + separator_count: 10, + separator_width: 1, + + + upload: { + fill: "#2d3f4d", + stroke: "#336e9f", + strike_width: 2, + }, + + download: { + fill: "#532c26", + stroke: "#a9321c", + strike_width: 2, + } + }; + + private _canvas_context: CanvasRenderingContext2D; + private _entries: Entry[] = []; + private _entry_max = { + upload: 1, + download: 1, + }; + private _max_space = 1.12; + private _max_gap = 5; + private _listener_mouse_move; + private _listener_mouse_out; + private _animate_loop; + + _time_span: TimeSpan = { + origin: { + begin: 0, + end: 1, + time: 0 + }, + target: { + begin: 0, + end: 1, + time: 1 + } + }; + + private _detailed_shown = false; + callback_detailed_info: (upload: number, download: number, timestamp: number, event: MouseEvent) => any; + callback_detailed_hide: () => any; + + constructor(canvas: HTMLCanvasElement) { + this.canvas = canvas; + this._animate_loop = () => this.draw(); + this.recalculate_cache(); /* initialize cache */ + } + + initialize() { + this._canvas_context = this.canvas.getContext("2d"); + + Graph._loops.push(this._animate_loop); + if(Graph._loops.length == 1) { + const static_loop = () => { + Graph._loops.forEach(l => l()); + if(Graph._loops.length > 0) + requestAnimationFrame(static_loop); + else + console.log("STATIC terminate!"); + }; + static_loop(); + } + + this.canvas.onmousemove = this.on_mouse_move.bind(this); + this.canvas.onmouseleave = this.on_mouse_leave.bind(this); + } + + terminate() { + Graph._loops.remove(this._animate_loop); + } + + max_gap_size(value?: number) : number { return typeof(value) === "number" ? (this._max_gap = value) : this._max_gap; } + + private recalculate_cache(time_span?: boolean) { + this._entries = this._entries.sort((a, b) => a.timestamp - b.timestamp); + this._entry_max = { + download: 1, + upload: 1 + }; + if(time_span) { + this._time_span = { + origin: { + begin: 0, + end: 0, + time: 0 + }, + target: { + begin: this._entries.length > 0 ? this._entries[0].timestamp : 0, + end: this._entries.length > 0 ? this._entries.last().timestamp : 0, + time: 0 + } + }; + } + + for(const entry of this._entries) { + if(typeof(entry.upload) === "number") + this._entry_max.upload = Math.max(this._entry_max.upload, entry.upload); + + if(typeof(entry.download) === "number") + this._entry_max.download = Math.max(this._entry_max.download, entry.download); + } + + this._entry_max.upload *= this._max_space; + this._entry_max.download *= this._max_space; + } + + insert_entry(entry: Entry) { + if(this._entries.length > 0 && entry.timestamp < this._entries.last().timestamp) + throw "invalid timestamp"; + + this._entries.push(entry); + + if(typeof(entry.upload) === "number") + this._entry_max.upload = Math.max(this._entry_max.upload, entry.upload * this._max_space); + + if(typeof(entry.download) === "number") + this._entry_max.download = Math.max(this._entry_max.download, entry.download * this._max_space); + } + + insert_entries(entries: Entry[]) { + this._entries.push(...entries); + this.recalculate_cache(); + this.cleanup(); + } + + resize() { + this.canvas.style.height = "100%"; + this.canvas.style.width = "100%"; + const cstyle = getComputedStyle(this.canvas); + + this.canvas.width = parseInt(cstyle.width); + this.canvas.height = parseInt(cstyle.height); + } + + cleanup() { + const time = this.calculate_time_span(); + + let index = 0; + for(;index < this._entries.length; index++) { + if(this._entries[index].timestamp < time.begin) + continue; + + if(index == 0) + return; + break; + } + + /* keep the last entry as a reference point to the left */ + if(index > 1) { + this._entries.splice(0, index - 1); + this.recalculate_cache(); + } + } + + calculate_time_span() : { begin: number; end: number } { + const time = Date.now(); + if(time >= this._time_span.target.time) + return this._time_span.target; + + if(time <= this._time_span.origin.time) + return this._time_span.origin; + + const ob = this._time_span.origin.begin; + const oe = this._time_span.origin.end; + const ot = this._time_span.origin.time; + + const tb = this._time_span.target.begin; + const te = this._time_span.target.end; + const tt = this._time_span.target.time; + + const offset = (time - ot) / (tt - ot); + return { + begin: ob + (tb - ob) * offset, + end: oe + (te - oe) * offset, + }; + } + + draw() { + let ctx = this._canvas_context; + + const height = this.canvas.height; + const width = this.canvas.width; + + //console.log("Painting on %ox%o", height, width); + + ctx.shadowBlur = 0; + ctx.filter = ""; + ctx.lineCap = "square"; + + ctx.fillStyle = this.style.background_color; + ctx.fillRect(0, 0, width, height); + + /* first of all print the separators */ + { + const sw = this.style.separator_width; + const swh = this.style.separator_width / 2; + + ctx.lineWidth = sw; + ctx.strokeStyle = this.style.separator_color; + + ctx.beginPath(); + /* horizontal */ + { + const dw = width / this.style.separator_count; + let dx = dw / 2; + while(dx < width) { + ctx.moveTo(Math.floor(dx - swh) + .5, .5); + ctx.lineTo(Math.floor(dx - swh) + .5, Math.floor(height) + .5); + dx += dw; + } + } + + /* vertical */ + { + const dh = height / 3; //tree lines (top, center, bottom) + + let dy = dh / 2; + while(dy < height) { + ctx.moveTo(.5, Math.floor(dy - swh) + .5); + ctx.lineTo(Math.floor(width) + .5, Math.floor(dy - swh) + .5); + dy += dh; + } + } + ctx.stroke(); + ctx.closePath(); + } + + /* draw the lines */ + { + const t = this.calculate_time_span(); + const tb = t.begin; /* time begin */ + const dt = t.end - t.begin; /* delta time */ + const dtw = width / dt; /* delta time width */ + + const draw_graph = (type: "upload" | "download", direction: number, max: number) => { + const hy = Math.floor(height / 2); /* half y */ + const by = hy - direction * this.style[type].strike_width; /* the "base" line */ + + const marked_points: ({x: number, y: number})[] = []; + + ctx.beginPath(); + ctx.moveTo(0, by); + + let x, y, lx = 0, ly = by; /* last x, last y */ + + const floor = a => a; //Math.floor; + for(const entry of this._entries) { + x = floor((entry.timestamp - tb) * dtw); + if(typeof entry[type] === "number") + y = floor(hy - direction * Math.max(hy * (entry[type] / max), this.style[type].strike_width)); + else + y = hy - direction * this.style[type].strike_width; + + if(entry.timestamp < tb) { + lx = x; + ly = y; + + continue; + } + + if(x - lx > this._max_gap && this._max_gap > 0) { + ctx.lineTo(lx, by); + ctx.lineTo(x, by); + ctx.lineTo(x, y); + + lx = x; + ly = y; + continue; + } + + ctx.bezierCurveTo((x + lx) / 2, ly, (x + lx) / 2, y, x, y); + if(entry.highlight) + marked_points.push({x: x, y: y}); + + lx = x; + ly = y; + } + + ctx.strokeStyle = this.style[type].stroke; + ctx.lineWidth = this.style[type].strike_width; + ctx.lineJoin = "miter"; + ctx.stroke(); + + //Close the path and fill + ctx.lineTo(width, hy); + ctx.lineTo(0, hy); + + ctx.fillStyle = this.style[type].fill; + ctx.fill(); + + ctx.closePath(); + + { + ctx.beginPath(); + const radius = 3; + for(const point of marked_points) { + ctx.moveTo(point.x, point.y); + ctx.ellipse(point.x, point.y, radius, radius, 0, 0, 2 * Math.PI, false); + + } + ctx.stroke(); + ctx.fill(); + + ctx.closePath(); + } + }; + + const shared_max = Math.max(this._entry_max.upload, this._entry_max.download); + draw_graph("upload", 1, shared_max); + draw_graph("download", -1, shared_max); + } + } + + private on_mouse_move(event: MouseEvent) { + const offset = event.offsetX; + const max_offset = this.canvas.width; + + if(offset < 0) return; + if(offset > max_offset) return; + + const time_span = this.calculate_time_span(); + const time = time_span.begin + (time_span.end - time_span.begin) * (offset / max_offset); + let index = 0; + for(;index < this._entries.length; index++) { + if(this._entries[index].timestamp > time) + break; + } + + const entry_before = this._entries[index - 1]; /* In JS negative array access is allowed and returns undefined */ + const entry_next = this._entries[index]; /* In JS negative array access is allowed and returns undefined */ + let entry: Entry; + if(!entry_before || !entry_next) { + entry = entry_before || entry_next; + } else { + const dn = entry_next.timestamp - time; + const db = time - entry_before.timestamp; + if(dn > db) + entry = entry_before; + else + entry = entry_next; + } + + if(!entry) { + this.on_mouse_leave(event); + } else { + this._entries.forEach(e => e.highlight = false); + this._detailed_shown = true; + entry.highlight = true; + + if(this.callback_detailed_info) + this.callback_detailed_info(entry.upload, entry.download, entry.timestamp, event); + } + + } + + private on_mouse_leave(event: MouseEvent) { + if(!this._detailed_shown) return; + this._detailed_shown = false; + + this._entries.forEach(e => e.highlight = false); + if(this.callback_detailed_hide) + this.callback_detailed_hide(); + } +} \ No newline at end of file diff --git a/shared/js/ui/elements/slider.ts b/shared/js/ui/elements/Slider.ts similarity index 92% rename from shared/js/ui/elements/slider.ts rename to shared/js/ui/elements/Slider.ts index 0e956585..f704ed6d 100644 --- a/shared/js/ui/elements/slider.ts +++ b/shared/js/ui/elements/Slider.ts @@ -1,4 +1,6 @@ -interface SliderOptions { +import * as tooltip from "tc-shared/ui/elements/Tooltip"; + +export interface SliderOptions { min_value?: number; max_value?: number; initial_value?: number; @@ -8,11 +10,11 @@ interface SliderOptions { value_field?: JQuery | JQuery[]; } -interface Slider { +export interface Slider { value(value?: number) : number; } -function sliderfy(slider: JQuery, options?: SliderOptions) : Slider { +export function sliderfy(slider: JQuery, options?: SliderOptions) : Slider { options = Object.assign( { initial_value: 0, min_value: 0, @@ -30,7 +32,7 @@ function sliderfy(slider: JQuery, options?: SliderOptions) : Slider { throw "invalid step size"; - const tool = tooltip(slider); /* add the tooltip functionality */ + const tool = tooltip.initialize(slider); /* add the tooltip functionality */ const filler = slider.find(".filler"); const thumb = slider.find(".thumb"); const tooltip_text = slider.find(".tooltip a"); diff --git a/shared/js/ui/elements/tab.ts b/shared/js/ui/elements/Tab.ts similarity index 95% rename from shared/js/ui/elements/tab.ts rename to shared/js/ui/elements/Tab.ts index fb34577c..109476b5 100644 --- a/shared/js/ui/elements/tab.ts +++ b/shared/js/ui/elements/Tab.ts @@ -1,13 +1,12 @@ -/// +declare global { + interface JQuery { + asTabWidget(copy?: boolean) : JQuery; + tabify(copy?: boolean) : this; -interface JQuery { - asTabWidget(copy?: boolean) : JQuery; - tabify(copy?: boolean) : this; - - changeElementType(type: string) : JQuery; + changeElementType(type: string) : JQuery; + } } - if(typeof (customElements) !== "undefined") { try { class X_Tab extends HTMLElement {} @@ -26,7 +25,7 @@ if(typeof (customElements) !== "undefined") { console.warn(tr("Could not defied tab customElements!")); } -var TabFunctions = { +export const TabFunctions = { tabify(template: JQuery, copy: boolean = true) : JQuery { console.log("Tabify: copy=" + copy); console.log(template); diff --git a/shared/js/ui/elements/Tooltip.ts b/shared/js/ui/elements/Tooltip.ts new file mode 100644 index 00000000..b6657c6a --- /dev/null +++ b/shared/js/ui/elements/Tooltip.ts @@ -0,0 +1,79 @@ +let _global_tooltip: JQuery; +export type Handle = { + show(); + is_shown(); + hide(); + update(); +} +export function initialize(entry: JQuery, callbacks?: { + on_show?(tag: JQuery), + on_hide?(tag: JQuery) +}) : Handle { + if(!callbacks) callbacks = {}; + + let _show; + let _hide; + let _shown; + let _update; + + entry.find(".container-tooltip").each((index, _node) => { + const node = $(_node) as JQuery; + const node_content = node.find(".tooltip"); + + let _force_show = false, _flag_shown = false; + + const mouseenter = (event?) => { + const bounds = node[0].getBoundingClientRect(); + + if(!_global_tooltip) { + _global_tooltip = $("#global-tooltip"); + } + + _global_tooltip[0].style.left = (bounds.left + bounds.width / 2) + "px"; + _global_tooltip[0].style.top = bounds.top + "px"; + _global_tooltip[0].classList.add("shown"); + + _global_tooltip[0].innerHTML = node_content[0].innerHTML; + callbacks.on_show && callbacks.on_show(_global_tooltip); + _flag_shown = _flag_shown || !!event; /* if event is undefined then it has been triggered by hand */ + }; + + const mouseexit = () => { + if(_global_tooltip) { + if(!_force_show) { + callbacks.on_hide && callbacks.on_hide(_global_tooltip); + _global_tooltip[0].classList.remove("shown"); + } + _flag_shown = false; + } + }; + + _node.addEventListener("mouseenter", mouseenter); + + _node.addEventListener("mouseleave", mouseexit); + + _show = () => { + _force_show = true; + mouseenter(); + }; + + _hide = () => { + _force_show = false; + if(!_flag_shown) + mouseexit(); + }; + + _update = () => { + if(_flag_shown || _force_show) + mouseenter(); + }; + + _shown = () => _flag_shown || _force_show; + }); + return { + hide: _hide || (() => {}), + show: _show || (() => {}), + is_shown: _shown || (() => false), + update: _update || (() => {}) + }; +} \ No newline at end of file diff --git a/shared/js/ui/elements/net_graph.ts b/shared/js/ui/elements/net_graph.ts deleted file mode 100644 index 5e440872..00000000 --- a/shared/js/ui/elements/net_graph.ts +++ /dev/null @@ -1,435 +0,0 @@ -namespace net.graph { - export type Entry = { - timestamp: number; - - upload?: number; - download?: number; - - highlight?: boolean; - } - - export type Style = { - background_color: string; - - separator_color: string; - separator_count: number; - separator_width: number; - - upload: { - fill: string; - stroke: string; - strike_width: number; - }, - download: { - fill: string; - stroke: string; - strike_width: number; - } - } - - export type TimeSpan = { - origin: { - begin: number; - end: number; - time: number; - }, - target: { - begin: number; - end: number; - time: number; - } - } - - /* Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves - * - * Assuming A was the last point in the line plotted and B is the new point, - * we draw a curve with control points P and Q as below. - * - * A---P - * | - * | - * | - * Q---B - * - * Importantly, A and P are at the same y coordinate, as are B and Q. This is - * so adjacent curves appear to flow as one. - */ - export class Graph { - private static _loops: (() => any)[] = []; - - readonly canvas: HTMLCanvasElement; - public style: Style = { - background_color: "#28292b", - //background_color: "red", - - separator_color: "#283036", - //separator_color: 'blue', - separator_count: 10, - separator_width: 1, - - - upload: { - fill: "#2d3f4d", - stroke: "#336e9f", - strike_width: 2, - }, - - download: { - fill: "#532c26", - stroke: "#a9321c", - strike_width: 2, - } - }; - - private _canvas_context: CanvasRenderingContext2D; - private _entries: Entry[] = []; - private _entry_max = { - upload: 1, - download: 1, - }; - private _max_space = 1.12; - private _max_gap = 5; - private _listener_mouse_move; - private _listener_mouse_out; - private _animate_loop; - - _time_span: TimeSpan = { - origin: { - begin: 0, - end: 1, - time: 0 - }, - target: { - begin: 0, - end: 1, - time: 1 - } - }; - - private _detailed_shown = false; - callback_detailed_info: (upload: number, download: number, timestamp: number, event: MouseEvent) => any; - callback_detailed_hide: () => any; - - constructor(canvas: HTMLCanvasElement) { - this.canvas = canvas; - this._animate_loop = () => this.draw(); - this.recalculate_cache(); /* initialize cache */ - } - - initialize() { - this._canvas_context = this.canvas.getContext("2d"); - - Graph._loops.push(this._animate_loop); - if(Graph._loops.length == 1) { - const static_loop = () => { - Graph._loops.forEach(l => l()); - if(Graph._loops.length > 0) - requestAnimationFrame(static_loop); - else - console.log("STATIC terminate!"); - }; - static_loop(); - } - - this.canvas.onmousemove = this.on_mouse_move.bind(this); - this.canvas.onmouseleave = this.on_mouse_leave.bind(this); - } - - terminate() { - Graph._loops.remove(this._animate_loop); - } - - max_gap_size(value?: number) : number { return typeof(value) === "number" ? (this._max_gap = value) : this._max_gap; } - - private recalculate_cache(time_span?: boolean) { - this._entries = this._entries.sort((a, b) => a.timestamp - b.timestamp); - this._entry_max = { - download: 1, - upload: 1 - }; - if(time_span) { - this._time_span = { - origin: { - begin: 0, - end: 0, - time: 0 - }, - target: { - begin: this._entries.length > 0 ? this._entries[0].timestamp : 0, - end: this._entries.length > 0 ? this._entries.last().timestamp : 0, - time: 0 - } - }; - } - - for(const entry of this._entries) { - if(typeof(entry.upload) === "number") - this._entry_max.upload = Math.max(this._entry_max.upload, entry.upload); - - if(typeof(entry.download) === "number") - this._entry_max.download = Math.max(this._entry_max.download, entry.download); - } - - this._entry_max.upload *= this._max_space; - this._entry_max.download *= this._max_space; - } - - insert_entry(entry: Entry) { - if(this._entries.length > 0 && entry.timestamp < this._entries.last().timestamp) - throw "invalid timestamp"; - - this._entries.push(entry); - - if(typeof(entry.upload) === "number") - this._entry_max.upload = Math.max(this._entry_max.upload, entry.upload * this._max_space); - - if(typeof(entry.download) === "number") - this._entry_max.download = Math.max(this._entry_max.download, entry.download * this._max_space); - } - - insert_entries(entries: Entry[]) { - this._entries.push(...entries); - this.recalculate_cache(); - this.cleanup(); - } - - resize() { - this.canvas.style.height = "100%"; - this.canvas.style.width = "100%"; - const cstyle = getComputedStyle(this.canvas); - - this.canvas.width = parseInt(cstyle.width); - this.canvas.height = parseInt(cstyle.height); - } - - cleanup() { - const time = this.calculate_time_span(); - - let index = 0; - for(;index < this._entries.length; index++) { - if(this._entries[index].timestamp < time.begin) - continue; - - if(index == 0) - return; - break; - } - - /* keep the last entry as a reference point to the left */ - if(index > 1) { - this._entries.splice(0, index - 1); - this.recalculate_cache(); - } - } - - calculate_time_span() : { begin: number; end: number } { - const time = Date.now(); - if(time >= this._time_span.target.time) - return this._time_span.target; - - if(time <= this._time_span.origin.time) - return this._time_span.origin; - - const ob = this._time_span.origin.begin; - const oe = this._time_span.origin.end; - const ot = this._time_span.origin.time; - - const tb = this._time_span.target.begin; - const te = this._time_span.target.end; - const tt = this._time_span.target.time; - - const offset = (time - ot) / (tt - ot); - return { - begin: ob + (tb - ob) * offset, - end: oe + (te - oe) * offset, - }; - } - - draw() { - let ctx = this._canvas_context; - - const height = this.canvas.height; - const width = this.canvas.width; - - //console.log("Painting on %ox%o", height, width); - - ctx.shadowBlur = 0; - ctx.filter = ""; - ctx.lineCap = "square"; - - ctx.fillStyle = this.style.background_color; - ctx.fillRect(0, 0, width, height); - - /* first of all print the separators */ - { - const sw = this.style.separator_width; - const swh = this.style.separator_width / 2; - - ctx.lineWidth = sw; - ctx.strokeStyle = this.style.separator_color; - - ctx.beginPath(); - /* horizontal */ - { - const dw = width / this.style.separator_count; - let dx = dw / 2; - while(dx < width) { - ctx.moveTo(Math.floor(dx - swh) + .5, .5); - ctx.lineTo(Math.floor(dx - swh) + .5, Math.floor(height) + .5); - dx += dw; - } - } - - /* vertical */ - { - const dh = height / 3; //tree lines (top, center, bottom) - - let dy = dh / 2; - while(dy < height) { - ctx.moveTo(.5, Math.floor(dy - swh) + .5); - ctx.lineTo(Math.floor(width) + .5, Math.floor(dy - swh) + .5); - dy += dh; - } - } - ctx.stroke(); - ctx.closePath(); - } - - /* draw the lines */ - { - const t = this.calculate_time_span(); - const tb = t.begin; /* time begin */ - const dt = t.end - t.begin; /* delta time */ - const dtw = width / dt; /* delta time width */ - - const draw_graph = (type: "upload" | "download", direction: number, max: number) => { - const hy = Math.floor(height / 2); /* half y */ - const by = hy - direction * this.style[type].strike_width; /* the "base" line */ - - const marked_points: ({x: number, y: number})[] = []; - - ctx.beginPath(); - ctx.moveTo(0, by); - - let x, y, lx = 0, ly = by; /* last x, last y */ - - const floor = a => a; //Math.floor; - for(const entry of this._entries) { - x = floor((entry.timestamp - tb) * dtw); - if(typeof entry[type] === "number") - y = floor(hy - direction * Math.max(hy * (entry[type] / max), this.style[type].strike_width)); - else - y = hy - direction * this.style[type].strike_width; - - if(entry.timestamp < tb) { - lx = x; - ly = y; - - continue; - } - - if(x - lx > this._max_gap && this._max_gap > 0) { - ctx.lineTo(lx, by); - ctx.lineTo(x, by); - ctx.lineTo(x, y); - - lx = x; - ly = y; - continue; - } - - ctx.bezierCurveTo((x + lx) / 2, ly, (x + lx) / 2, y, x, y); - if(entry.highlight) - marked_points.push({x: x, y: y}); - - lx = x; - ly = y; - } - - ctx.strokeStyle = this.style[type].stroke; - ctx.lineWidth = this.style[type].strike_width; - ctx.lineJoin = "miter"; - ctx.stroke(); - - //Close the path and fill - ctx.lineTo(width, hy); - ctx.lineTo(0, hy); - - ctx.fillStyle = this.style[type].fill; - ctx.fill(); - - ctx.closePath(); - - { - ctx.beginPath(); - const radius = 3; - for(const point of marked_points) { - ctx.moveTo(point.x, point.y); - ctx.ellipse(point.x, point.y, radius, radius, 0, 0, 2 * Math.PI, false); - - } - ctx.stroke(); - ctx.fill(); - - ctx.closePath(); - } - }; - - const shared_max = Math.max(this._entry_max.upload, this._entry_max.download); - draw_graph("upload", 1, shared_max); - draw_graph("download", -1, shared_max); - } - } - - private on_mouse_move(event: MouseEvent) { - const offset = event.offsetX; - const max_offset = this.canvas.width; - - if(offset < 0) return; - if(offset > max_offset) return; - - const time_span = this.calculate_time_span(); - const time = time_span.begin + (time_span.end - time_span.begin) * (offset / max_offset); - let index = 0; - for(;index < this._entries.length; index++) { - if(this._entries[index].timestamp > time) - break; - } - - const entry_before = this._entries[index - 1]; /* In JS negative array access is allowed and returns undefined */ - const entry_next = this._entries[index]; /* In JS negative array access is allowed and returns undefined */ - let entry: Entry; - if(!entry_before || !entry_next) { - entry = entry_before || entry_next; - } else { - const dn = entry_next.timestamp - time; - const db = time - entry_before.timestamp; - if(dn > db) - entry = entry_before; - else - entry = entry_next; - } - - if(!entry) { - this.on_mouse_leave(event); - } else { - this._entries.forEach(e => e.highlight = false); - this._detailed_shown = true; - entry.highlight = true; - - if(this.callback_detailed_info) - this.callback_detailed_info(entry.upload, entry.download, entry.timestamp, event); - } - - } - - private on_mouse_leave(event: MouseEvent) { - if(!this._detailed_shown) return; - this._detailed_shown = false; - - this._entries.forEach(e => e.highlight = false); - if(this.callback_detailed_hide) - this.callback_detailed_hide(); - } - } -} \ No newline at end of file diff --git a/shared/js/ui/elements/tooltip.ts b/shared/js/ui/elements/tooltip.ts deleted file mode 100644 index b64f96b5..00000000 --- a/shared/js/ui/elements/tooltip.ts +++ /dev/null @@ -1,85 +0,0 @@ -function tooltip(entry: JQuery) { - return tooltip.initialize(entry); -} - -namespace tooltip { - let _global_tooltip: JQuery; - export type Handle = { - show(); - is_shown(); - hide(); - update(); - } - export function initialize(entry: JQuery, callbacks?: { - on_show?(tag: JQuery), - on_hide?(tag: JQuery) - }) : Handle { - if(!callbacks) callbacks = {}; - - let _show; - let _hide; - let _shown; - let _update; - - entry.find(".container-tooltip").each((index, _node) => { - const node = $(_node) as JQuery; - const node_content = node.find(".tooltip"); - - let _force_show = false, _flag_shown = false; - - const mouseenter = (event?) => { - const bounds = node[0].getBoundingClientRect(); - - if(!_global_tooltip) { - _global_tooltip = $("#global-tooltip"); - } - - _global_tooltip[0].style.left = (bounds.left + bounds.width / 2) + "px"; - _global_tooltip[0].style.top = bounds.top + "px"; - _global_tooltip[0].classList.add("shown"); - - _global_tooltip[0].innerHTML = node_content[0].innerHTML; - callbacks.on_show && callbacks.on_show(_global_tooltip); - _flag_shown = _flag_shown || !!event; /* if event is undefined then it has been triggered by hand */ - }; - - const mouseexit = () => { - if(_global_tooltip) { - if(!_force_show) { - callbacks.on_hide && callbacks.on_hide(_global_tooltip); - _global_tooltip[0].classList.remove("shown"); - } - _flag_shown = false; - } - }; - - _node.addEventListener("mouseenter", mouseenter); - - _node.addEventListener("mouseleave", mouseexit); - - _show = () => { - _force_show = true; - mouseenter(); - }; - - _hide = () => { - _force_show = false; - if(!_flag_shown) - mouseexit(); - }; - - _update = () => { - if(_flag_shown || _force_show) - mouseenter(); - }; - - _shown = () => _flag_shown || _force_show; - }); - return { - hide: _hide || (() => {}), - show: _show || (() => {}), - is_shown: _shown || (() => false), - update: _update || (() => {}) - }; - } -} \ No newline at end of file diff --git a/shared/js/ui/frames/ControlBar.ts b/shared/js/ui/frames/ControlBar.ts index b2d6dad3..a873cf34 100644 --- a/shared/js/ui/frames/ControlBar.ts +++ b/shared/js/ui/frames/ControlBar.ts @@ -1,24 +1,40 @@ -/// -/// -/// -/* - client_output_hardware Value: '1' - client_output_muted Value: '0' - client_outputonly_muted Value: '0' +import {ConnectionHandler, DisconnectReason} from "tc-shared/ConnectionHandler"; +import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal"; +import {manager, Sound} from "tc-shared/sound/Sounds"; +import {default_recorder} from "tc-shared/voice/RecorderProfile"; +import {Settings, settings} from "tc-shared/settings"; +import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings"; +import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect"; +import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; +import PermissionType from "tc-shared/permission/PermissionType"; +import {spawnPermissionEdit} from "tc-shared/ui/modal/permission/ModalPermissionEdit"; +import {openBanList} from "tc-shared/ui/modal/ModalBanList"; +import { + add_current_server, + Bookmark, + bookmarks, + BookmarkType, + boorkmak_connect, + DirectoryBookmark +} from "tc-shared/bookmarks"; +import {IconManager} from "tc-shared/FileManager"; +import {spawnBookmarkModal} from "tc-shared/ui/modal/ModalBookmarks"; +import {spawnQueryCreate} from "tc-shared/ui/modal/ModalQuery"; +import {spawnQueryManage} from "tc-shared/ui/modal/ModalQueryManage"; +import {spawnPlaylistManage} from "tc-shared/ui/modal/ModalPlaylistList"; +import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; +import {server_connections} from "tc-shared/ui/frames/connection_handlers"; +import {formatMessage} from "tc-shared/ui/frames/chat"; +import * as slog from "tc-shared/ui/frames/server_log"; +import * as top_menu from "./MenuBar"; - client_input_hardware Value: '1' - client_input_muted Value: '0' +export let control_bar: ControlBar; /* global variable to access the control bar */ +export function set_control_bar(bar: ControlBar) { control_bar = bar; } - client_away Value: '0' - client_away_message Value: '' - */ - -let control_bar: ControlBar; /* global variable to access the control bar */ - -type MicrophoneState = "disabled" | "muted" | "enabled"; -type HeadphoneState = "muted" | "enabled"; -type AwayState = "away-global" | "away" | "online"; -class ControlBar { +export type MicrophoneState = "disabled" | "muted" | "enabled"; +export type HeadphoneState = "muted" | "enabled"; +export type AwayState = "away-global" | "away" | "online"; +export class ControlBar { private _button_away_active: AwayState; private _button_microphone: MicrophoneState; private _button_speakers: HeadphoneState; @@ -363,10 +379,10 @@ class ControlBar { private on_toggle_microphone() { if(this._button_microphone === "disabled" || this._button_microphone === "muted") { this.button_microphone = "enabled"; - sound.manager.play(Sound.MICROPHONE_ACTIVATED); + manager.play(Sound.MICROPHONE_ACTIVATED); } else { this.button_microphone = "muted"; - sound.manager.play(Sound.MICROPHONE_MUTED); + manager.play(Sound.MICROPHONE_MUTED); } if(this.connection_handler) { @@ -384,10 +400,10 @@ class ControlBar { private on_toggle_sound() { if(this._button_speakers === "muted") { this.button_speaker = "enabled"; - sound.manager.play(Sound.SOUND_ACTIVATED); + manager.play(Sound.SOUND_ACTIVATED); } else { this.button_speaker = "muted"; - sound.manager.play(Sound.SOUND_MUTED); + manager.play(Sound.SOUND_MUTED); } if(this.connection_handler) { @@ -421,20 +437,20 @@ class ControlBar { } private on_open_settings() { - Modals.spawnSettingsModal(); + spawnSettingsModal(); } private on_open_connect() { if(this.connection_handler) this.connection_handler.cancel_reconnect(true); - Modals.spawnConnectModal({}, { + spawnConnectModal({}, { url: "ts.TeaSpeak.de", enforce: false }); } private on_open_connect_new_tab() { - Modals.spawnConnectModal({ + spawnConnectModal({ default_connect_new_tab: true }, { url: "ts.TeaSpeak.de", @@ -470,7 +486,7 @@ class ControlBar { this.connection_handler.handleDisconnect(DisconnectReason.REQUESTED); //TODO message? this.update_connection_state(); this.connection_handler.sound.play(Sound.CONNECTION_DISCONNECTED); - this.connection_handler.log.log(log.server.Type.DISCONNECTED, {}); + this.connection_handler.log.log(slog.Type.DISCONNECTED, {}); } private on_token_use() { @@ -483,7 +499,7 @@ class ControlBar { createInfoModal(tr("Use token"), tr("Toke successfully used!")).open(); }).catch(error => { //TODO tr - createErrorModal(tr("Use token"), MessageHelper.formatMessage(tr("Failed to use token: {}"), error instanceof CommandResult ? error.message : error)).open(); + createErrorModal(tr("Use token"), formatMessage(tr("Failed to use token: {}"), error instanceof CommandResult ? error.message : error)).open(); }); }).open(); } @@ -497,7 +513,7 @@ class ControlBar { button.addClass("activated"); setTimeout(() => { if(this.connection_handler) - Modals.spawnPermissionEdit(this.connection_handler).open(); + spawnPermissionEdit(this.connection_handler).open(); else createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open(); button.removeClass("activated"); @@ -508,7 +524,7 @@ class ControlBar { if(!this.connection_handler.serverConnection) return; if(this.connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_BAN_LIST).granted(1)) { - Modals.openBanList(this.connection_handler); + openBanList(this.connection_handler); } else { createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to view the ban list")).open(); this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); @@ -516,7 +532,7 @@ class ControlBar { } private on_bookmark_server_add() { - bookmarks.add_current_server(); + add_current_server(); } update_bookmark_status() { @@ -530,13 +546,13 @@ class ControlBar { let tag_bookmark = this.htmlTag.find(".btn_bookmark > .dropdown"); tag_bookmark.find(".bookmark, .directory").remove(); - const build_entry = (bookmark: bookmarks.DirectoryBookmark | bookmarks.Bookmark) => { - if(bookmark.type == bookmarks.BookmarkType.ENTRY) { - const mark = bookmark; + const build_entry = (bookmark: DirectoryBookmark | Bookmark) => { + if(bookmark.type == BookmarkType.ENTRY) { + const mark = bookmark; const bookmark_connect = (new_tab: boolean) => { this.htmlTag.find(".btn_bookmark").find(".dropdown").removeClass("displayed"); //FIXME Not working - bookmarks.boorkmak_connect(mark, new_tab); + boorkmak_connect(mark, new_tab); }; return $.spawn("div") @@ -580,7 +596,7 @@ class ControlBar { }) ) } else { - const mark = bookmark; + const mark = bookmark; const container = $.spawn("div").addClass("sub-menu dropdown"); const result = $.spawn("div") @@ -609,19 +625,19 @@ class ControlBar { } }; - for(const bookmark of bookmarks.bookmarks().content) { + for(const bookmark of bookmarks().content) { const entry = build_entry(bookmark); tag_bookmark.append(entry); } } private on_bookmark_manage() { - Modals.spawnBookmarkModal(); + spawnBookmarkModal(); } private on_open_query_create() { if(this.connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN).granted(1)) { - Modals.spawnQueryCreate(this.connection_handler); + spawnQueryCreate(this.connection_handler); } else { createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to create a server query login")).open(); this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); @@ -630,7 +646,7 @@ class ControlBar { private on_open_query_manage() { if(this.connection_handler && this.connection_handler.connected) { - Modals.spawnQueryManage(this.connection_handler); + spawnQueryManage(this.connection_handler); } else { createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open(); } @@ -638,7 +654,7 @@ class ControlBar { private on_open_playlist_manage() { if(this.connection_handler && this.connection_handler.connected) { - Modals.spawnPlaylistManage(this.connection_handler); + spawnPlaylistManage(this.connection_handler); } else { createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open(); } diff --git a/shared/js/ui/frames/MenuBar.ts b/shared/js/ui/frames/MenuBar.ts index 04885afa..0ef579ad 100644 --- a/shared/js/ui/frames/MenuBar.ts +++ b/shared/js/ui/frames/MenuBar.ts @@ -1,541 +1,574 @@ -namespace top_menu { - export interface HRItem { } +import {Icon, IconManager} from "tc-shared/FileManager"; +import {spawnBookmarkModal} from "tc-shared/ui/modal/ModalBookmarks"; +import { + add_current_server, + Bookmark, + bookmarks, + BookmarkType, + boorkmak_connect, + DirectoryBookmark +} from "tc-shared/bookmarks"; +import {ConnectionHandler, DisconnectReason} from "tc-shared/ConnectionHandler"; +import {Sound} from "tc-shared/sound/Sounds"; +import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect"; +import {spawnPermissionEdit} from "tc-shared/ui/modal/permission/ModalPermissionEdit"; +import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal"; +import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; +import PermissionType from "tc-shared/permission/PermissionType"; +import {openBanList} from "tc-shared/ui/modal/ModalBanList"; +import {spawnQueryManage} from "tc-shared/ui/modal/ModalQueryManage"; +import {spawnQueryCreate} from "tc-shared/ui/modal/ModalQuery"; +import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings"; +import {spawnAbout} from "tc-shared/ui/modal/ModalAbout"; +import {server_connections} from "tc-shared/ui/frames/connection_handlers"; +import {control_bar} from "tc-shared/ui/frames/ControlBar"; +import * as loader from "tc-loader"; +import {formatMessage} from "tc-shared/ui/frames/chat"; +import * as slog from "tc-shared/ui/frames/server_log"; - export interface MenuItem { - append_item(label: string): MenuItem; - append_hr(): HRItem; - delete_item(item: MenuItem | HRItem); - items() : (MenuItem | HRItem)[]; +export interface HRItem { } - icon(klass?: string | Promise | Icon) : string; - label(value?: string) : string; - visible(value?: boolean) : boolean; - disabled(value?: boolean) : boolean; - click(callback: () => any) : this; - } +export interface MenuItem { + append_item(label: string): MenuItem; + append_hr(): HRItem; + delete_item(item: MenuItem | HRItem); + items() : (MenuItem | HRItem)[]; - export interface MenuBarDriver { - initialize(); + icon(klass?: string | Promise | Icon) : string; + label(value?: string) : string; + visible(value?: boolean) : boolean; + disabled(value?: boolean) : boolean; + click(callback: () => any) : this; +} - append_item(label: string) : MenuItem; - delete_item(item: MenuItem); - items() : MenuItem[]; +export interface MenuBarDriver { + initialize(); - flush_changes(); - } + append_item(label: string) : MenuItem; + delete_item(item: MenuItem); + items() : MenuItem[]; - let _driver: MenuBarDriver; - export function driver() : MenuBarDriver { - return _driver; - } + flush_changes(); +} - export function set_driver(driver: MenuBarDriver) { - _driver = driver; - } +let _driver: MenuBarDriver; +export function driver() : MenuBarDriver { + return _driver; +} - export interface NativeActions { - open_dev_tools(); - reload_page(); +export function set_driver(driver: MenuBarDriver) { + _driver = driver; +} - check_native_update(); - open_change_log(); +export interface NativeActions { + open_dev_tools(); + reload_page(); - quit(); + check_native_update(); + open_change_log(); - show_dev_tools(): boolean; - } - export let native_actions: NativeActions; + quit(); - namespace html { - class HTMLHrItem implements top_menu.HRItem { - readonly html_tag: JQuery; + show_dev_tools(): boolean; +} +export let native_actions: NativeActions; - constructor() { - this.html_tag = $.spawn("hr"); - } +namespace html { + class HTMLHrItem implements HRItem { + readonly html_tag: JQuery; + + constructor() { + this.html_tag = $.spawn("hr"); } + } - class HTMLMenuItem implements top_menu.MenuItem { - readonly html_tag: JQuery; - readonly _label_tag: JQuery; - readonly _label_icon_tag: JQuery; - readonly _label_text_tag: JQuery; - readonly _submenu_tag: JQuery; + class HTMLMenuItem implements MenuItem { + readonly html_tag: JQuery; + readonly _label_tag: JQuery; + readonly _label_icon_tag: JQuery; + readonly _label_text_tag: JQuery; + readonly _submenu_tag: JQuery; - private _items: (MenuItem | HRItem)[] = []; - private _label: string; - private _callback_click: () => any; + private _items: (MenuItem | HRItem)[] = []; + private _label: string; + private _callback_click: () => any; - constructor(label: string, mode: "side" | "down") { - this._label = label; + constructor(label: string, mode: "side" | "down") { + this._label = label; - this.html_tag = $.spawn("div").addClass("container-menu-item type-" + mode); + this.html_tag = $.spawn("div").addClass("container-menu-item type-" + mode); - this._label_tag = $.spawn("div").addClass("menu-item"); - this._label_icon_tag = $.spawn("div").addClass("container-icon").appendTo(this._label_tag); - $.spawn("div").addClass("container-label").append( - this._label_text_tag = $.spawn("a").text(label) - ).appendTo(this._label_tag); - this._label_tag.on('click', event => { - if(event.isDefaultPrevented()) - return; + this._label_tag = $.spawn("div").addClass("menu-item"); + this._label_icon_tag = $.spawn("div").addClass("container-icon").appendTo(this._label_tag); + $.spawn("div").addClass("container-label").append( + this._label_text_tag = $.spawn("a").text(label) + ).appendTo(this._label_tag); + this._label_tag.on('click', event => { + if(event.isDefaultPrevented()) + return; - const disabled = this.html_tag.hasClass("disabled"); - if(this._callback_click && !disabled) { - this._callback_click(); - } + const disabled = this.html_tag.hasClass("disabled"); + if(this._callback_click && !disabled) { + this._callback_click(); + } - event.preventDefault(); - if(disabled) - event.stopPropagation(); - else - HTMLMenuBarDriver.instance().close(); - }); - - this._submenu_tag = $.spawn("div").addClass("sub-menu"); - - this.html_tag.append(this._label_tag); - this.html_tag.append(this._submenu_tag); - } - - append_item(label: string): top_menu.MenuItem { - const item = new HTMLMenuItem(label, "side"); - this._items.push(item); - this._submenu_tag.append(item.html_tag); - this.html_tag.addClass('sub-entries'); - return item; - } - - append_hr(): HRItem { - const item = new HTMLHrItem(); - this._items.push(item); - this._submenu_tag.append(item.html_tag); - return item; - } - - delete_item(item: top_menu.MenuItem | top_menu.HRItem) { - this._items.remove(item); - (item as any).html_tag.detach(); - this.html_tag.toggleClass('sub-entries', this._items.length > 0); - } - - disabled(value?: boolean): boolean { - if(typeof(value) === "undefined") - return this.html_tag.hasClass("disabled"); - - this.html_tag.toggleClass("disabled", value); - return value; - } - - items(): (top_menu.MenuItem | top_menu.HRItem)[] { - return this._items; - } - - label(value?: string): string { - if(typeof(value) === "undefined" || this._label === value) - return this._label; - - return this._label; - } - - visible(value?: boolean): boolean { - if(typeof(value) === "undefined") - return this.html_tag.is(':visible'); //FIXME! - - this.html_tag.toggle(!!value); - return value; - } - - click(callback: () => any): this { - this._callback_click = callback; - return this; - } - - icon(klass?: string | Promise | Icon): string { - this._label_icon_tag.children().remove(); - if(typeof(klass) === "string") - $.spawn("div").addClass("icon_em " + klass).appendTo(this._label_icon_tag); + event.preventDefault(); + if(disabled) + event.stopPropagation(); else - IconManager.generate_tag(klass).appendTo(this._label_icon_tag); - return ""; - } + HTMLMenuBarDriver.instance().close(); + }); + this._submenu_tag = $.spawn("div").addClass("sub-menu"); + + this.html_tag.append(this._label_tag); + this.html_tag.append(this._submenu_tag); } - export class HTMLMenuBarDriver implements MenuBarDriver { - private static _instance: HTMLMenuBarDriver; - public static instance() : HTMLMenuBarDriver { - if(!this._instance) - this._instance = new HTMLMenuBarDriver(); - return this._instance; - } - - readonly html_tag: JQuery; - - private _items: MenuItem[] = []; - constructor() { - this.html_tag = $.spawn("div").addClass("top-menu-bar"); - } - - append_item(label: string): top_menu.MenuItem { - const item = new HTMLMenuItem(label, "down"); - this._items.push(item); - - this.html_tag.append(item.html_tag); - item._label_tag.on('click', enable_event => { - enable_event.preventDefault(); - - this.close(); - item.html_tag.addClass("active"); - - setTimeout(() => { - $(document).one('click focusout', event => { - if(event.isDefaultPrevented()) return; - event.preventDefault(); - - item.html_tag.removeClass("active"); - }); - }, 0); - }); - return item; - } - - close() { - this.html_tag.find(".active").removeClass("active"); - } - - delete_item(item: MenuItem) { - return undefined; - } - - items(): top_menu.MenuItem[] { - return this._items; - } - - flush_changes() { /* unused, all changed were made instantly */ } - - initialize() { - $("#top-menu-bar").replaceWith(this.html_tag); - } + append_item(label: string): MenuItem { + const item = new HTMLMenuItem(label, "side"); + this._items.push(item); + this._submenu_tag.append(item.html_tag); + this.html_tag.addClass('sub-entries'); + return item; } + + append_hr(): HRItem { + const item = new HTMLHrItem(); + this._items.push(item); + this._submenu_tag.append(item.html_tag); + return item; + } + + delete_item(item: MenuItem | HRItem) { + this._items.remove(item); + (item as any).html_tag.detach(); + this.html_tag.toggleClass('sub-entries', this._items.length > 0); + } + + disabled(value?: boolean): boolean { + if(typeof(value) === "undefined") + return this.html_tag.hasClass("disabled"); + + this.html_tag.toggleClass("disabled", value); + return value; + } + + items(): (MenuItem | HRItem)[] { + return this._items; + } + + label(value?: string): string { + if(typeof(value) === "undefined" || this._label === value) + return this._label; + + return this._label; + } + + visible(value?: boolean): boolean { + if(typeof(value) === "undefined") + return this.html_tag.is(':visible'); //FIXME! + + this.html_tag.toggle(!!value); + return value; + } + + click(callback: () => any): this { + this._callback_click = callback; + return this; + } + + icon(klass?: string | Promise | Icon): string { + this._label_icon_tag.children().remove(); + if(typeof(klass) === "string") + $.spawn("div").addClass("icon_em " + klass).appendTo(this._label_icon_tag); + else + IconManager.generate_tag(klass).appendTo(this._label_icon_tag); + return ""; + } + } - let _items_bookmark: { - root: MenuItem, - manage: MenuItem, - add_current: MenuItem - }; - - export function rebuild_bookmarks() { - if(!_items_bookmark) { - _items_bookmark = { - root: driver().append_item(tr("Favorites")), - - add_current: undefined, - manage: undefined - }; - _items_bookmark.manage = _items_bookmark.root.append_item(tr("Manage bookmarks")); - _items_bookmark.manage.icon("client-bookmark_manager"); - _items_bookmark.manage.click(() => Modals.spawnBookmarkModal()); - - _items_bookmark.add_current = _items_bookmark.root.append_item(tr("Add current server to bookmarks")); - _items_bookmark.add_current.icon('client-bookmark_add'); - _items_bookmark.add_current.click(() => bookmarks.add_current_server()); - _state_updater["bookmarks.ac"] = { item: _items_bookmark.add_current, conditions: [condition_connected]}; + export class HTMLMenuBarDriver implements MenuBarDriver { + private static _instance: HTMLMenuBarDriver; + public static instance() : HTMLMenuBarDriver { + if(!this._instance) + this._instance = new HTMLMenuBarDriver(); + return this._instance; } - _items_bookmark.root.items().filter(e => e !== _items_bookmark.add_current && e !== _items_bookmark.manage).forEach(e => { - _items_bookmark.root.delete_item(e); - }); - _items_bookmark.root.append_hr(); + readonly html_tag: JQuery; - const build_bookmark = (root: MenuItem, entry: bookmarks.DirectoryBookmark | bookmarks.Bookmark) => { - if(entry.type == bookmarks.BookmarkType.DIRECTORY) { - const directory = entry as bookmarks.DirectoryBookmark; - const item = root.append_item(directory.display_name); - item.icon('client-folder'); - for(const entry of directory.content) - build_bookmark(item, entry); - if(directory.content.length == 0) - item.disabled(true); - } else { - const bookmark = entry as bookmarks.Bookmark; - const item = root.append_item(bookmark.display_name); - item.icon(IconManager.load_cached_icon(bookmark.last_icon_id || 0)); - item.click(() => bookmarks.boorkmak_connect(bookmark)); - } + private _items: MenuItem[] = []; + constructor() { + this.html_tag = $.spawn("div").addClass("top-menu-bar"); + } + + append_item(label: string): MenuItem { + const item = new HTMLMenuItem(label, "down"); + this._items.push(item); + + this.html_tag.append(item.html_tag); + item._label_tag.on('click', enable_event => { + enable_event.preventDefault(); + + this.close(); + item.html_tag.addClass("active"); + + setTimeout(() => { + $(document).one('click focusout', event => { + if(event.isDefaultPrevented()) return; + event.preventDefault(); + + item.html_tag.removeClass("active"); + }); + }, 0); + }); + return item; + } + + close() { + this.html_tag.find(".active").removeClass("active"); + } + + delete_item(item: MenuItem) { + return undefined; + } + + items(): MenuItem[] { + return this._items; + } + + flush_changes() { /* unused, all changed were made instantly */ } + + initialize() { + $("#top-menu-bar").replaceWith(this.html_tag); + } + } +} + +let _items_bookmark: { + root: MenuItem, + manage: MenuItem, + add_current: MenuItem +}; + +export function rebuild_bookmarks() { + if(!_items_bookmark) { + _items_bookmark = { + root: driver().append_item(tr("Favorites")), + + add_current: undefined, + manage: undefined }; + _items_bookmark.manage = _items_bookmark.root.append_item(tr("Manage bookmarks")); + _items_bookmark.manage.icon("client-bookmark_manager"); + _items_bookmark.manage.click(() => spawnBookmarkModal()); - for(const entry of bookmarks.bookmarks().content) - build_bookmark(_items_bookmark.root, entry); - driver().flush_changes(); + _items_bookmark.add_current = _items_bookmark.root.append_item(tr("Add current server to bookmarks")); + _items_bookmark.add_current.icon('client-bookmark_add'); + _items_bookmark.add_current.click(() => add_current_server()); + _state_updater["bookmarks.ac"] = { item: _items_bookmark.add_current, conditions: [condition_connected]}; } - /* will be called on connection handler change or on client connect state or mic state change etc... */ - let _state_updater: {[key: string]:{ item: MenuItem; conditions: (() => boolean)[], update_handler?: (item: MenuItem) => any }} = {}; - export function update_state() { - for(const _key of Object.keys(_state_updater)) { - const item = _state_updater[_key]; - if(item.update_handler) { - if(item.update_handler(item.item)) - continue; - } - let enabled = true; - for(const condition of item.conditions) - if(!condition()) { - enabled = false; - break; - } - item.item.disabled(!enabled); + _items_bookmark.root.items().filter(e => e !== _items_bookmark.add_current && e !== _items_bookmark.manage).forEach(e => { + _items_bookmark.root.delete_item(e); + }); + _items_bookmark.root.append_hr(); + + const build_bookmark = (root: MenuItem, entry: DirectoryBookmark | Bookmark) => { + if(entry.type == BookmarkType.DIRECTORY) { + const directory = entry as DirectoryBookmark; + const item = root.append_item(directory.display_name); + item.icon('client-folder'); + for(const entry of directory.content) + build_bookmark(item, entry); + if(directory.content.length == 0) + item.disabled(true); + } else { + const bookmark = entry as Bookmark; + const item = root.append_item(bookmark.display_name); + item.icon(IconManager.load_cached_icon(bookmark.last_icon_id || 0)); + item.click(() => boorkmak_connect(bookmark)); } - driver().flush_changes(); - } - - const condition_connected = () => { - const scon = server_connections ? server_connections.active_connection_handler() : undefined; - return scon && scon.connected; }; - declare namespace native { - export function initialize(); + for(const entry of bookmarks().content) + build_bookmark(_items_bookmark.root, entry); + driver().flush_changes(); +} + +/* will be called on connection handler change or on client connect state or mic state change etc... */ +let _state_updater: {[key: string]:{ item: MenuItem; conditions: (() => boolean)[], update_handler?: (item: MenuItem) => any }} = {}; +export function update_state() { + for(const _key of Object.keys(_state_updater)) { + const item = _state_updater[_key]; + if(item.update_handler) { + if(item.update_handler(item.item)) + continue; + } + let enabled = true; + for(const condition of item.conditions) + if(!condition()) { + enabled = false; + break; + } + item.item.disabled(!enabled); + } + driver().flush_changes(); +} + +const condition_connected = () => { + const scon = server_connections ? server_connections.active_connection_handler() : undefined; + return scon && scon.connected; +}; + +declare namespace native { + export function initialize(); +} + +export function initialize() { + const driver_ = driver(); + driver_.initialize(); + + /* build connection */ + let item: MenuItem; + { + const menu = driver_.append_item(tr("Connection")); + item = menu.append_item(tr("Connect to a server")); + item.icon('client-connect'); + item.click(() => spawnConnectModal({})); + + const do_disconnect = (handlers: ConnectionHandler[]) => { + for(const handler of handlers) { + handler.cancel_reconnect(true); + handler.handleDisconnect(DisconnectReason.REQUESTED); //TODO message? + server_connections.active_connection_handler().serverConnection.disconnect(); + handler.sound.play(Sound.CONNECTION_DISCONNECTED); + handler.log.log(slog.Type.DISCONNECTED, {}); + } + control_bar.update_connection_state(); + update_state(); + }; + item = menu.append_item(tr("Disconnect from current server")); + item.icon('client-disconnect'); + item.disabled(true); + item.click(() => { + const handler = server_connections.active_connection_handler(); + do_disconnect([handler]); + }); + _state_updater["connection.dc"] = { item: item, conditions: [() => condition_connected()]}; + + item = menu.append_item(tr("Disconnect from all servers")); + item.icon('client-disconnect'); + item.click(() => { + do_disconnect(server_connections.server_connection_handlers()); + }); + _state_updater["connection.dca"] = { item: item, conditions: [], update_handler: (item) => { + item.visible(server_connections && server_connections.server_connection_handlers().length > 1); + return true; + }}; + + if(loader.version().type !== "web") { + menu.append_hr(); + + item = menu.append_item(tr("Quit")); + item.icon('client-close_button'); + item.click(() => native_actions.quit()); + } + } + { + rebuild_bookmarks(); } - export function initialize() { - const driver = top_menu.driver(); - driver.initialize(); - - /* build connection */ - let item: MenuItem; - { - const menu = driver.append_item(tr("Connection")); - item = menu.append_item(tr("Connect to a server")); - item.icon('client-connect'); - item.click(() => Modals.spawnConnectModal({})); - - const do_disconnect = (handlers: ConnectionHandler[]) => { - for(const handler of handlers) { - handler.cancel_reconnect(true); - handler.handleDisconnect(DisconnectReason.REQUESTED); //TODO message? - server_connections.active_connection_handler().serverConnection.disconnect(); - handler.sound.play(Sound.CONNECTION_DISCONNECTED); - handler.log.log(log.server.Type.DISCONNECTED, {}); - } - control_bar.update_connection_state(); - update_state(); - }; - item = menu.append_item(tr("Disconnect from current server")); - item.icon('client-disconnect'); - item.disabled(true); - item.click(() => { - const handler = server_connections.active_connection_handler(); - do_disconnect([handler]); - }); - _state_updater["connection.dc"] = { item: item, conditions: [() => condition_connected()]}; - - item = menu.append_item(tr("Disconnect from all servers")); - item.icon('client-disconnect'); - item.click(() => { - do_disconnect(server_connections.server_connection_handlers()); - }); - _state_updater["connection.dca"] = { item: item, conditions: [], update_handler: (item) => { - item.visible(server_connections && server_connections.server_connection_handlers().length > 1); - return true; - }}; - - if(!app.is_web()) { - menu.append_hr(); - - item = menu.append_item(tr("Quit")); - item.icon('client-close_button'); - item.click(() => native_actions.quit()); - } - } - { - rebuild_bookmarks(); - } - - if(false) { - const menu = driver.append_item(tr("Self")); - /* Microphone | Sound | Away */ - } - - { - const menu = driver.append_item(tr("Permissions")); - - item = menu.append_item(tr("Server Groups")); - item.icon("client-permission_server_groups"); - item.click(() => { - Modals.spawnPermissionEdit(server_connections.active_connection_handler(), "sg").open(); - }); - _state_updater["permission.sg"] = { item: item, conditions: [condition_connected]}; - - item = menu.append_item(tr("Client Permissions")); - item.icon("client-permission_client"); - item.click(() => { - Modals.spawnPermissionEdit(server_connections.active_connection_handler(), "clp").open(); - }); - _state_updater["permission.clp"] = { item: item, conditions: [condition_connected]}; - - item = menu.append_item(tr("Channel Client Permissions")); - item.icon("client-permission_client"); - item.click(() => { - Modals.spawnPermissionEdit(server_connections.active_connection_handler(), "clchp").open(); - }); - _state_updater["permission.chclp"] = { item: item, conditions: [condition_connected]}; - - item = menu.append_item(tr("Channel Groups")); - item.icon("client-permission_channel"); - item.click(() => { - Modals.spawnPermissionEdit(server_connections.active_connection_handler(), "cg").open(); - }); - _state_updater["permission.cg"] = { item: item, conditions: [condition_connected]}; - - item = menu.append_item(tr("Channel Permissions")); - item.icon("client-permission_channel"); - item.click(() => { - Modals.spawnPermissionEdit(server_connections.active_connection_handler(), "chp").open(); - }); - _state_updater["permission.cp"] = { item: item, conditions: [condition_connected]}; - - menu.append_hr(); - item = menu.append_item(tr("List Privilege Keys")); - item.icon("client-token"); - item.click(() => { - createErrorModal(tr("Not implemented"), tr("Privilege key list is not implemented yet!")).open(); - }); - _state_updater["permission.pk"] = { item: item, conditions: [condition_connected]}; - - item = menu.append_item(tr("Use Privilege Key")); - item.icon("client-token_use"); - item.click(() => { - //TODO: Fixeme use one method for the control bar and here! - createInputModal(tr("Use token"), tr("Please enter your token/privilege key"), message => message.length > 0, result => { - if(!result) return; - const scon = server_connections.active_connection_handler(); - - if(scon.serverConnection.connected) - scon.serverConnection.send_command("tokenuse", { - token: result - }).then(() => { - createInfoModal(tr("Use token"), tr("Toke successfully used!")).open(); - }).catch(error => { - //TODO tr - createErrorModal(tr("Use token"), MessageHelper.formatMessage(tr("Failed to use token: {}"), error instanceof CommandResult ? error.message : error)).open(); - }); - }).open(); - }); - _state_updater["permission.upk"] = { item: item, conditions: [condition_connected]}; - } - - { - const menu = driver.append_item(tr("Tools")); - - /* - item = menu.append_item(tr("Manage Playlists")); - item.icon('client-music'); - item.click(() => { - const scon = server_connections.active_connection_handler(); - if(scon && scon.connected) { - Modals.spawnPlaylistManage(scon); - } else { - createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open(); - } - }); - _state_updater["tools.pl"] = { item: item, conditions: [condition_connected]}; - */ - - item = menu.append_item(tr("Ban List")); - item.icon('client-ban_list'); - item.click(() => { - const scon = server_connections.active_connection_handler(); - if(scon && scon.connected) { - if(scon.permissions.neededPermission(PermissionType.B_CLIENT_BAN_LIST).granted(1)) { - Modals.openBanList(scon); - } else { - createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to view the ban list")).open(); - scon.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); - } - } else { - createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open(); - } - }); - _state_updater["tools.bl"] = { item: item, conditions: [condition_connected]}; - - item = menu.append_item(tr("Query List")); - item.icon('client-server_query'); - item.click(() => { - const scon = server_connections.active_connection_handler(); - if(scon && scon.connected) { - if(scon.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_LIST).granted(1) || scon.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_LIST_OWN).granted(1)) { - Modals.spawnQueryManage(scon); - } else { - createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to view the server query list")).open(); - scon.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); - } - } else { - createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open(); - } - }); - _state_updater["tools.ql"] = { item: item, conditions: [condition_connected]}; - - item = menu.append_item(tr("Query Create")); - item.icon('client-server_query'); - item.click(() => { - const scon = server_connections.active_connection_handler(); - if(scon && scon.connected) { - if(scon.permissions.neededPermission(PermissionType.B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN).granted(1) || scon.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_CREATE).granted(1)) { - Modals.spawnQueryCreate(scon); - } else { - createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to create a server query login")).open(); - scon.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); - } - } else { - createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open(); - } - }); - _state_updater["tools.qc"] = { item: item, conditions: [condition_connected]}; - menu.append_hr(); - - item = menu.append_item(tr("Settings")); - item.icon("client-settings"); - item.click(() => Modals.spawnSettingsModal()); - } - - { - const menu = driver.append_item(tr("Help")); - - if(!app.is_web()) { - item = menu.append_item(tr("Check for updates")); - item.click(() => native_actions.check_native_update()); - - item = menu.append_item(tr("Open changelog")); - item.click(() => native_actions.open_change_log()); - } - - item = menu.append_item(tr("Visit TeaSpeak.de")); - item.click(() => window.open('https://teaspeak.de/', '_blank')); - - item = menu.append_item(tr("Visit TeaSpeak forum")); - item.click(() => window.open('https://forum.teaspeak.de/', '_blank')); - - if(!app.is_web() && typeof(native_actions.show_dev_tools) === "function" && native_actions.show_dev_tools()) { - menu.append_hr(); - item = menu.append_item(tr("Open developer tools")); - item.click(() => native_actions.open_dev_tools()); - - item = menu.append_item(tr("Reload UI")); - item.click(() => native_actions.reload_page()); - } - - menu.append_hr(); - item = menu.append_item(app.is_web() ? tr("About TeaWeb") : tr("About TeaClient")); - item.click(() => Modals.spawnAbout()) - } - - update_state(); + if(false) { + const menu = driver_.append_item(tr("Self")); + /* Microphone | Sound | Away */ } - /* default is HTML, the client will override this */ - set_driver(html.HTMLMenuBarDriver.instance()); -} \ No newline at end of file + { + const menu = driver_.append_item(tr("Permissions")); + + item = menu.append_item(tr("Server Groups")); + item.icon("client-permission_server_groups"); + item.click(() => { + spawnPermissionEdit(server_connections.active_connection_handler(), "sg").open(); + }); + _state_updater["permission.sg"] = { item: item, conditions: [condition_connected]}; + + item = menu.append_item(tr("Client Permissions")); + item.icon("client-permission_client"); + item.click(() => { + spawnPermissionEdit(server_connections.active_connection_handler(), "clp").open(); + }); + _state_updater["permission.clp"] = { item: item, conditions: [condition_connected]}; + + item = menu.append_item(tr("Channel Client Permissions")); + item.icon("client-permission_client"); + item.click(() => { + spawnPermissionEdit(server_connections.active_connection_handler(), "clchp").open(); + }); + _state_updater["permission.chclp"] = { item: item, conditions: [condition_connected]}; + + item = menu.append_item(tr("Channel Groups")); + item.icon("client-permission_channel"); + item.click(() => { + spawnPermissionEdit(server_connections.active_connection_handler(), "cg").open(); + }); + _state_updater["permission.cg"] = { item: item, conditions: [condition_connected]}; + + item = menu.append_item(tr("Channel Permissions")); + item.icon("client-permission_channel"); + item.click(() => { + spawnPermissionEdit(server_connections.active_connection_handler(), "chp").open(); + }); + _state_updater["permission.cp"] = { item: item, conditions: [condition_connected]}; + + menu.append_hr(); + item = menu.append_item(tr("List Privilege Keys")); + item.icon("client-token"); + item.click(() => { + createErrorModal(tr("Not implemented"), tr("Privilege key list is not implemented yet!")).open(); + }); + _state_updater["permission.pk"] = { item: item, conditions: [condition_connected]}; + + item = menu.append_item(tr("Use Privilege Key")); + item.icon("client-token_use"); + item.click(() => { + //TODO: Fixeme use one method for the control bar and here! + createInputModal(tr("Use token"), tr("Please enter your token/privilege key"), message => message.length > 0, result => { + if(!result) return; + const scon = server_connections.active_connection_handler(); + + if(scon.serverConnection.connected) + scon.serverConnection.send_command("tokenuse", { + token: result + }).then(() => { + createInfoModal(tr("Use token"), tr("Toke successfully used!")).open(); + }).catch(error => { + //TODO tr + createErrorModal(tr("Use token"), formatMessage(tr("Failed to use token: {}"), error instanceof CommandResult ? error.message : error)).open(); + }); + }).open(); + }); + _state_updater["permission.upk"] = { item: item, conditions: [condition_connected]}; + } + + { + const menu = driver_.append_item(tr("Tools")); + + /* + item = menu.append_item(tr("Manage Playlists")); + item.icon('client-music'); + item.click(() => { + const scon = server_connections.active_connection_handler(); + if(scon && scon.connected) { + Modals.spawnPlaylistManage(scon); + } else { + createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open(); + } + }); + _state_updater["tools.pl"] = { item: item, conditions: [condition_connected]}; + */ + + item = menu.append_item(tr("Ban List")); + item.icon('client-ban_list'); + item.click(() => { + const scon = server_connections.active_connection_handler(); + if(scon && scon.connected) { + if(scon.permissions.neededPermission(PermissionType.B_CLIENT_BAN_LIST).granted(1)) { + openBanList(scon); + } else { + createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to view the ban list")).open(); + scon.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); + } + } else { + createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open(); + } + }); + _state_updater["tools.bl"] = { item: item, conditions: [condition_connected]}; + + item = menu.append_item(tr("Query List")); + item.icon('client-server_query'); + item.click(() => { + const scon = server_connections.active_connection_handler(); + if(scon && scon.connected) { + if(scon.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_LIST).granted(1) || scon.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_LIST_OWN).granted(1)) { + spawnQueryManage(scon); + } else { + createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to view the server query list")).open(); + scon.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); + } + } else { + createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open(); + } + }); + _state_updater["tools.ql"] = { item: item, conditions: [condition_connected]}; + + item = menu.append_item(tr("Query Create")); + item.icon('client-server_query'); + item.click(() => { + const scon = server_connections.active_connection_handler(); + if(scon && scon.connected) { + if(scon.permissions.neededPermission(PermissionType.B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN).granted(1) || scon.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_CREATE).granted(1)) { + spawnQueryCreate(scon); + } else { + createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to create a server query login")).open(); + scon.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); + } + } else { + createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open(); + } + }); + _state_updater["tools.qc"] = { item: item, conditions: [condition_connected]}; + menu.append_hr(); + + item = menu.append_item(tr("Settings")); + item.icon("client-settings"); + item.click(() => spawnSettingsModal()); + } + + { + const menu = driver_.append_item(tr("Help")); + + if(loader.version().type !== "web") { + item = menu.append_item(tr("Check for updates")); + item.click(() => native_actions.check_native_update()); + + item = menu.append_item(tr("Open changelog")); + item.click(() => native_actions.open_change_log()); + } + + item = menu.append_item(tr("Visit TeaSpeak.de")); + item.click(() => window.open('https://teaspeak.de/', '_blank')); + + item = menu.append_item(tr("Visit TeaSpeak forum")); + item.click(() => window.open('https://forum.teaspeak.de/', '_blank')); + + if(loader.version().type !== "web" && typeof(native_actions.show_dev_tools) === "function" && native_actions.show_dev_tools()) { + menu.append_hr(); + item = menu.append_item(tr("Open developer tools")); + item.click(() => native_actions.open_dev_tools()); + + item = menu.append_item(tr("Reload UI")); + item.click(() => native_actions.reload_page()); + } + + menu.append_hr(); + item = menu.append_item(loader.version().type === "web" ? tr("About TeaWeb") : tr("About TeaClient")); + item.click(() => spawnAbout()) + } + + update_state(); +} + +/* default is HTML, the client will override this */ +loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { + function: async () => { + if(!driver()) + set_driver(html.HTMLMenuBarDriver.instance()); + }, + priority: 100, + name: "Menu bar init" +}); \ No newline at end of file diff --git a/shared/js/ui/frames/chat.ts b/shared/js/ui/frames/chat.ts index 9de0d55b..a4e6ba7c 100644 --- a/shared/js/ui/frames/chat.ts +++ b/shared/js/ui/frames/chat.ts @@ -1,4 +1,10 @@ -enum ChatType { +import {LogCategory} from "tc-shared/log"; +import {settings, Settings} from "tc-shared/settings"; +import * as log from "tc-shared/log"; +import {bbcode} from "tc-shared/MessageFormatter"; +import * as loader from "tc-loader"; + +export enum ChatType { GENERAL, SERVER, CHANNEL, @@ -6,245 +12,243 @@ enum ChatType { } declare const xbbcode: any; -namespace MessageHelper { - export function htmlEscape(message: string) : string[] { - const div = document.createElement('div'); - div.innerText = message; - message = div.innerHTML; - return message.replace(/ /g, ' ').split(/
/); - } +export function htmlEscape(message: string) : string[] { + const div = document.createElement('div'); + div.innerText = message; + message = div.innerHTML; + return message.replace(/ /g, ' ').split(/
/); +} - export function formatElement(object: any, escape_html: boolean = true) : JQuery[] { - if($.isArray(object)) { - let result = []; - for(let element of object) - result.push(...formatElement(element, escape_html)); - return result; - } else if(typeof(object) == "string") { - if(object.length == 0) return []; +export function formatElement(object: any, escape_html: boolean = true) : JQuery[] { + if($.isArray(object)) { + let result = []; + for(let element of object) + result.push(...formatElement(element, escape_html)); + return result; + } else if(typeof(object) == "string") { + if(object.length == 0) return []; - return escape_html ? - htmlEscape(object).map((entry, idx, array) => $.spawn("a").css("display", (idx == 0 || idx + 1 == array.length ? "inline" : "") + "block").html(entry == "" && idx != 0 ? " " : entry)) : - [$.spawn("div").css("display", "inline-block").html(object)]; - } else if(typeof(object) === "object") { - if(object instanceof $) - return [object as any]; - return formatElement(""); - } else if(typeof(object) === "function") return formatElement(object(), escape_html); - else if(typeof(object) === "undefined") return formatElement(""); - else if(typeof(object) === "number") return [$.spawn("a").text(object)]; - return formatElement(""); - } + return escape_html ? + htmlEscape(object).map((entry, idx, array) => $.spawn("a").css("display", (idx == 0 || idx + 1 == array.length ? "inline" : "") + "block").html(entry == "" && idx != 0 ? " " : entry)) : + [$.spawn("div").css("display", "inline-block").html(object)]; + } else if(typeof(object) === "object") { + if(object instanceof $) + return [object as any]; + return formatElement(""); + } else if(typeof(object) === "function") return formatElement(object(), escape_html); + else if(typeof(object) === "undefined") return formatElement(""); + else if(typeof(object) === "number") return [$.spawn("a").text(object)]; + return formatElement(""); +} - export function formatMessage(pattern: string, ...objects: any[]) : JQuery[] { - let begin = 0, found = 0; +export function formatMessage(pattern: string, ...objects: any[]) : JQuery[] { + let begin = 0, found = 0; - let result: JQuery[] = []; - do { - found = pattern.indexOf('{', found); - if(found == -1 || pattern.length <= found + 1) { - result.push(...formatElement(pattern.substr(begin))); - break; - } + let result: JQuery[] = []; + do { + found = pattern.indexOf('{', found); + if(found == -1 || pattern.length <= found + 1) { + result.push(...formatElement(pattern.substr(begin))); + break; + } - if(found > 0 && pattern[found - 1] == '\\') { - //TODO remove the escape! + if(found > 0 && pattern[found - 1] == '\\') { + //TODO remove the escape! + found++; + continue; + } + + result.push(...formatElement(pattern.substr(begin, found - begin))); //Append the text + + let offset = 0; + if(pattern[found + 1] == ':') { + offset++; /* the beginning : */ + while (pattern[found + 1 + offset] != ':' && found + 1 + offset < pattern.length) offset++; + const tag = pattern.substr(found + 2, offset - 1); + + offset++; /* the ending : */ + if(pattern[found + offset + 1] != '}' && found + 1 + offset < pattern.length) { found++; continue; } - result.push(...formatElement(pattern.substr(begin, found - begin))); //Append the text - - let offset = 0; - if(pattern[found + 1] == ':') { - offset++; /* the beginning : */ - while (pattern[found + 1 + offset] != ':' && found + 1 + offset < pattern.length) offset++; - const tag = pattern.substr(found + 2, offset - 1); - - offset++; /* the ending : */ - if(pattern[found + offset + 1] != '}' && found + 1 + offset < pattern.length) { - found++; - continue; - } - - result.push($.spawn(tag as any)); - } else { - let number; - while ("0123456789".includes(pattern[found + 1 + offset])) offset++; - number = parseInt(offset > 0 ? pattern.substr(found + 1, offset) : "0"); - if(pattern[found + offset + 1] != '}') { - found++; - continue; - } - - if(objects.length < number) - log.warn(LogCategory.GENERAL, tr("Message to format contains invalid index (%o)"), number); - - result.push(...formatElement(objects[number])); + result.push($.spawn(tag as any)); + } else { + let number; + while ("0123456789".includes(pattern[found + 1 + offset])) offset++; + number = parseInt(offset > 0 ? pattern.substr(found + 1, offset) : "0"); + if(pattern[found + offset + 1] != '}') { + found++; + continue; } - found = found + 1 + offset; - begin = found + 1; - } while(found++); + if(objects.length < number) + log.warn(LogCategory.GENERAL, tr("Message to format contains invalid index (%o)"), number); - return result; - } - - //TODO: Remove this (only legacy) - export function bbcode_chat(message: string) : JQuery[] { - return messages.formatter.bbcode.format(message, { - is_chat_message: true - }); - } - - export namespace network { - export const KB = 1024; - export const MB = 1024 * KB; - export const GB = 1024 * MB; - export const TB = 1024 * GB; - - export function format_bytes(value: number, options?: { - time?: string, - unit?: string, - exact?: boolean - }) : string { - options = options || {}; - if(typeof options.exact !== "boolean") - options.exact = true; - if(typeof options.unit !== "string") - options.unit = "Bytes"; - - let points = value.toFixed(0).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); - - let v, unit; - if(value > 2 * TB) { - unit = "TB"; - v = value / TB; - } else if(value > GB) { - unit = "GB"; - v = value / GB; - } else if(value > MB) { - unit = "MB"; - v = value / MB; - } else if(value > KB) { - unit = "KB"; - v = value / KB; - } else { - unit = ""; - v = value; - } - - let result = ""; - if(options.exact || !unit) { - result += points; - if(options.unit) { - result += " " + options.unit; - if(options.time) - result += "/" + options.time; - } - } - if(unit) { - result += (result ? " / " : "") + v.toFixed(2) + " " + unit; - if(options.time) - result += "/" + options.time; - } - return result; + result.push(...formatElement(objects[number])); } - } - export const K = 1000; - export const M = 1000 * K; - export const G = 1000 * M; - export const T = 1000 * G; - export function format_number(value: number, options?: { + found = found + 1 + offset; + begin = found + 1; + } while(found++); + + return result; +} + +//TODO: Remove this (only legacy) +export function bbcode_chat(message: string) : JQuery[] { + return bbcode.format(message, { + is_chat_message: true + }); +} + +export namespace network { + export const KB = 1024; + export const MB = 1024 * KB; + export const GB = 1024 * MB; + export const TB = 1024 * GB; + + export function format_bytes(value: number, options?: { time?: string, - unit?: string - }) { - options = Object.assign(options || {}, {}); + unit?: string, + exact?: boolean + }) : string { + options = options || {}; + if(typeof options.exact !== "boolean") + options.exact = true; + if(typeof options.unit !== "string") + options.unit = "Bytes"; let points = value.toFixed(0).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); let v, unit; - if(value > 2 * T) { - unit = "T"; - v = value / T; - } else if(value > G) { - unit = "G"; - v = value / G; - } else if(value > M) { - unit = "M"; - v = value / M; - } else if(value > K) { - unit = "K"; - v = value / K; + if(value > 2 * TB) { + unit = "TB"; + v = value / TB; + } else if(value > GB) { + unit = "GB"; + v = value / GB; + } else if(value > MB) { + unit = "MB"; + v = value / MB; + } else if(value > KB) { + unit = "KB"; + v = value / KB; } else { unit = ""; v = value; } - if(unit && options.time) - unit = unit + "/" + options.time; - return points + " " + (options.unit || "") + (unit ? (" / " + v.toFixed(2) + " " + unit) : ""); - } - export const TIME_SECOND = 1000; - export const TIME_MINUTE = 60 * TIME_SECOND; - export const TIME_HOUR = 60 * TIME_MINUTE; - export const TIME_DAY = 24 * TIME_HOUR; - export const TIME_WEEK = 7 * TIME_DAY; - - export function format_time(time: number, default_value: string) { let result = ""; - if(time > TIME_WEEK) { - const amount = Math.floor(time / TIME_WEEK); - result += " " + amount + " " + (amount > 1 ? tr("Weeks") : tr("Week")); - time -= amount * TIME_WEEK; + if(options.exact || !unit) { + result += points; + if(options.unit) { + result += " " + options.unit; + if(options.time) + result += "/" + options.time; + } } - - if(time > TIME_DAY) { - const amount = Math.floor(time / TIME_DAY); - result += " " + amount + " " + (amount > 1 ? tr("Days") : tr("Day")); - time -= amount * TIME_DAY; + if(unit) { + result += (result ? " / " : "") + v.toFixed(2) + " " + unit; + if(options.time) + result += "/" + options.time; } + return result; + } +} - if(time > TIME_HOUR) { - const amount = Math.floor(time / TIME_HOUR); - result += " " + amount + " " + (amount > 1 ? tr("Hours") : tr("Hour")); - time -= amount * TIME_HOUR; - } +export const K = 1000; +export const M = 1000 * K; +export const G = 1000 * M; +export const T = 1000 * G; +export function format_number(value: number, options?: { + time?: string, + unit?: string +}) { + options = Object.assign(options || {}, {}); - if(time > TIME_MINUTE) { - const amount = Math.floor(time / TIME_MINUTE); - result += " " + amount + " " + (amount > 1 ? tr("Minutes") : tr("Minute")); - time -= amount * TIME_MINUTE; - } + let points = value.toFixed(0).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); - if(time > TIME_SECOND) { - const amount = Math.floor(time / TIME_SECOND); - result += " " + amount + " " + (amount > 1 ? tr("Seconds") : tr("Second")); - time -= amount * TIME_SECOND; - } + let v, unit; + if(value > 2 * T) { + unit = "T"; + v = value / T; + } else if(value > G) { + unit = "G"; + v = value / G; + } else if(value > M) { + unit = "M"; + v = value / M; + } else if(value > K) { + unit = "K"; + v = value / K; + } else { + unit = ""; + v = value; + } + if(unit && options.time) + unit = unit + "/" + options.time; + return points + " " + (options.unit || "") + (unit ? (" / " + v.toFixed(2) + " " + unit) : ""); +} - return result.length > 0 ? result.substring(1) : default_value; +export const TIME_SECOND = 1000; +export const TIME_MINUTE = 60 * TIME_SECOND; +export const TIME_HOUR = 60 * TIME_MINUTE; +export const TIME_DAY = 24 * TIME_HOUR; +export const TIME_WEEK = 7 * TIME_DAY; + +export function format_time(time: number, default_value: string) { + let result = ""; + if(time > TIME_WEEK) { + const amount = Math.floor(time / TIME_WEEK); + result += " " + amount + " " + (amount > 1 ? tr("Weeks") : tr("Week")); + time -= amount * TIME_WEEK; } - let _icon_size_style: JQuery; - export function set_icon_size(size: string) { - if(!_icon_size_style) - _icon_size_style = $.spawn("style").appendTo($("#style")); - - _icon_size_style.text("\n" + - ".message > .emoji {\n" + - " height: " + size + "!important;\n" + - " width: " + size + "!important;\n" + - "}\n" - ); + if(time > TIME_DAY) { + const amount = Math.floor(time / TIME_DAY); + result += " " + amount + " " + (amount > 1 ? tr("Days") : tr("Day")); + time -= amount * TIME_DAY; } - loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { - name: "icon size init", - function: async () => { - MessageHelper.set_icon_size((settings.static_global(Settings.KEY_ICON_SIZE) / 100).toFixed(2) + "em"); - }, - priority: 10 - }); -} \ No newline at end of file + if(time > TIME_HOUR) { + const amount = Math.floor(time / TIME_HOUR); + result += " " + amount + " " + (amount > 1 ? tr("Hours") : tr("Hour")); + time -= amount * TIME_HOUR; + } + + if(time > TIME_MINUTE) { + const amount = Math.floor(time / TIME_MINUTE); + result += " " + amount + " " + (amount > 1 ? tr("Minutes") : tr("Minute")); + time -= amount * TIME_MINUTE; + } + + if(time > TIME_SECOND) { + const amount = Math.floor(time / TIME_SECOND); + result += " " + amount + " " + (amount > 1 ? tr("Seconds") : tr("Second")); + time -= amount * TIME_SECOND; + } + + return result.length > 0 ? result.substring(1) : default_value; +} + +let _icon_size_style: JQuery; +export function set_icon_size(size: string) { + if(!_icon_size_style) + _icon_size_style = $.spawn("style").appendTo($("#style")); + + _icon_size_style.text("\n" + + ".message > .emoji {\n" + + " height: " + size + "!important;\n" + + " width: " + size + "!important;\n" + + "}\n" + ); +} + +loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { + name: "icon size init", + function: async () => { + set_icon_size((settings.static_global(Settings.KEY_ICON_SIZE) / 100).toFixed(2) + "em"); + }, + priority: 10 +}); \ No newline at end of file diff --git a/shared/js/ui/frames/chat_frame.ts b/shared/js/ui/frames/chat_frame.ts index b871ce95..a584719a 100644 --- a/shared/js/ui/frames/chat_frame.ts +++ b/shared/js/ui/frames/chat_frame.ts @@ -1,417 +1,426 @@ /* the bar on the right with the chats (Channel & Client) */ -namespace chat { - declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; - declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; +import {ClientEntry, MusicClientEntry} from "tc-shared/ui/client"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {ChannelEntry} from "tc-shared/ui/channel"; +import {ServerEntry} from "tc-shared/ui/server"; +import {openMusicManage} from "tc-shared/ui/modal/ModalMusicManage"; +import {formatMessage} from "tc-shared/ui/frames/chat"; +import {PrivateConverations} from "tc-shared/ui/frames/side/private_conversations"; +import {ClientInfo} from "tc-shared/ui/frames/side/client_info"; +import {MusicInfo} from "tc-shared/ui/frames/side/music_info"; +import {ConversationManager} from "tc-shared/ui/frames/side/conversations"; - export enum InfoFrameMode { - NONE = "none", - CHANNEL_CHAT = "channel_chat", - PRIVATE_CHAT = "private_chat", - CLIENT_INFO = "client_info", - MUSIC_BOT = "music_bot" +declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; +declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; + +export enum InfoFrameMode { + NONE = "none", + CHANNEL_CHAT = "channel_chat", + PRIVATE_CHAT = "private_chat", + CLIENT_INFO = "client_info", + MUSIC_BOT = "music_bot" +} +export class InfoFrame { + private readonly handle: Frame; + private _html_tag: JQuery; + private _mode: InfoFrameMode; + + private _value_ping: JQuery; + private _ping_updater: number; + + private _channel_text: ChannelEntry; + private _channel_voice: ChannelEntry; + + private _button_conversation: HTMLElement; + + private _button_bot_manage: JQuery; + private _button_song_add: JQuery; + + constructor(handle: Frame) { + this.handle = handle; + this._build_html_tag(); + this.update_channel_talk(); + this.update_channel_text(); + this.set_mode(InfoFrameMode.CHANNEL_CHAT); + this._ping_updater = setInterval(() => this.update_ping(), 2000); + this.update_ping(); } - export class InfoFrame { - private readonly handle: Frame; - private _html_tag: JQuery; - private _mode: InfoFrameMode; - private _value_ping: JQuery; - private _ping_updater: number; + html_tag() : JQuery { return this._html_tag; } + destroy() { + clearInterval(this._ping_updater); - private _channel_text: ChannelEntry; - private _channel_voice: ChannelEntry; + this._html_tag && this._html_tag.remove(); + this._html_tag = undefined; + this._value_ping = undefined; + } - private _button_conversation: HTMLElement; + private _build_html_tag() { + this._html_tag = $("#tmpl_frame_chat_info").renderTag(); + this._html_tag.find(".button-switch-chat-channel").on('click', () => this.handle.show_channel_conversations()); + this._value_ping = this._html_tag.find(".value-ping"); + this._html_tag.find(".chat-counter").on('click', event => this.handle.show_private_conversations()); + this._button_conversation = this._html_tag.find(".button.open-conversation").on('click', event => { + const selected_client = this.handle.client_info().current_client(); + if(!selected_client) return; - private _button_bot_manage: JQuery; - private _button_song_add: JQuery; + const conversation = selected_client ? this.handle.private_conversations().find_conversation({ + name: selected_client.properties.client_nickname, + unique_id: selected_client.properties.client_unique_identifier, + client_id: selected_client.clientId() + }, { create: true, attach: true }) : undefined; + if(!conversation) return; - constructor(handle: Frame) { - this.handle = handle; - this._build_html_tag(); - this.update_channel_talk(); - this.update_channel_text(); - this.set_mode(InfoFrameMode.CHANNEL_CHAT); - this._ping_updater = setInterval(() => this.update_ping(), 2000); - this.update_ping(); + this.handle.private_conversations().set_selected_conversation(conversation); + this.handle.show_private_conversations(); + })[0]; + + this._button_bot_manage = this._html_tag.find(".bot-manage").on('click', event => { + const bot = this.handle.music_info().current_bot(); + if(!bot) return; + + openMusicManage(this.handle.handle, bot); + }); + this._button_song_add = this._html_tag.find(".bot-add-song").on('click', event => { + this.handle.music_info().events.fire("action_song_add"); + }); + } + + update_ping() { + this._value_ping.removeClass("very-good good medium poor very-poor"); + const connection = this.handle.handle.serverConnection; + if(!this.handle.handle.connected || !connection) { + this._value_ping.text("Not connected"); + return; } - html_tag() : JQuery { return this._html_tag; } - destroy() { - clearInterval(this._ping_updater); - - this._html_tag && this._html_tag.remove(); - this._html_tag = undefined; - this._value_ping = undefined; + const ping = connection.ping(); + if(!ping || typeof(ping.native) !== "number") { + this._value_ping.text("Not available"); + return; } - private _build_html_tag() { - this._html_tag = $("#tmpl_frame_chat_info").renderTag(); - this._html_tag.find(".button-switch-chat-channel").on('click', () => this.handle.show_channel_conversations()); - this._value_ping = this._html_tag.find(".value-ping"); - this._html_tag.find(".chat-counter").on('click', event => this.handle.show_private_conversations()); - this._button_conversation = this._html_tag.find(".button.open-conversation").on('click', event => { - const selected_client = this.handle.client_info().current_client(); - if(!selected_client) return; - - const conversation = selected_client ? this.handle.private_conversations().find_conversation({ - name: selected_client.properties.client_nickname, - unique_id: selected_client.properties.client_unique_identifier, - client_id: selected_client.clientId() - }, { create: true, attach: true }) : undefined; - if(!conversation) return; - - this.handle.private_conversations().set_selected_conversation(conversation); - this.handle.show_private_conversations(); - })[0]; - - this._button_bot_manage = this._html_tag.find(".bot-manage").on('click', event => { - const bot = this.handle.music_info().current_bot(); - if(!bot) return; - - Modals.openMusicManage(this.handle.handle, bot); - }); - this._button_song_add = this._html_tag.find(".bot-add-song").on('click', event => { - this.handle.music_info().events.fire("action_song_add"); - }); + let value; + if(typeof(ping.javascript) !== "undefined") { + value = ping.javascript; + this._value_ping.text(ping.javascript.toFixed(0) + "ms").attr('title', 'Native: ' + ping.native.toFixed(3) + "ms \nJavascript: " + ping.javascript.toFixed(3) + "ms"); + } else { + value = ping.native; + this._value_ping.text(ping.native.toFixed(0) + "ms").attr('title', "Ping: " + ping.native.toFixed(3) + "ms"); } - update_ping() { - this._value_ping.removeClass("very-good good medium poor very-poor"); - const connection = this.handle.handle.serverConnection; - if(!this.handle.handle.connected || !connection) { - this._value_ping.text("Not connected"); - return; - } + if(value <= 10) + this._value_ping.addClass("very-good"); + else if(value <= 30) + this._value_ping.addClass("good"); + else if(value <= 60) + this._value_ping.addClass("medium"); + else if(value <= 150) + this._value_ping.addClass("poor"); + else + this._value_ping.addClass("very-poor"); + } - const ping = connection.ping(); - if(!ping || typeof(ping.native) !== "number") { - this._value_ping.text("Not available"); - return; - } + update_channel_talk() { + const client = this.handle.handle.getClient(); + const channel = client ? client.currentChannel() : undefined; + this._channel_voice = channel; - let value; - if(typeof(ping.javascript) !== "undefined") { - value = ping.javascript; - this._value_ping.text(ping.javascript.toFixed(0) + "ms").attr('title', 'Native: ' + ping.native.toFixed(3) + "ms \nJavascript: " + ping.javascript.toFixed(3) + "ms"); - } else { - value = ping.native; - this._value_ping.text(ping.native.toFixed(0) + "ms").attr('title', "Ping: " + ping.native.toFixed(3) + "ms"); - } + const html_tag = this._html_tag.find(".value-voice-channel"); + const html_limit_tag = this._html_tag.find(".value-voice-limit"); - if(value <= 10) - this._value_ping.addClass("very-good"); - else if(value <= 30) - this._value_ping.addClass("good"); - else if(value <= 60) - this._value_ping.addClass("medium"); - else if(value <= 150) - this._value_ping.addClass("poor"); + html_limit_tag.text(""); + html_tag.children().remove(); + + if(channel) { + if(channel.properties.channel_icon_id != 0) + client.handle.fileManager.icons.generateTag(channel.properties.channel_icon_id).appendTo(html_tag); + $.spawn("div").text(channel.formattedChannelName()).appendTo(html_tag); + + this.update_channel_limit(channel, html_limit_tag); + } else { + $.spawn("div").text("Not connected").appendTo(html_tag); + } + } + + update_channel_text() { + const channel_tree = this.handle.handle.connected ? this.handle.handle.channelTree : undefined; + const current_channel_id = channel_tree ? this.handle.channel_conversations().current_channel() : 0; + const channel = channel_tree ? channel_tree.findChannel(current_channel_id) : undefined; + this._channel_text = channel; + + const tag_container = this._html_tag.find(".mode-channel_chat.channel"); + const html_tag_title = tag_container.find(".title"); + const html_tag = tag_container.find(".value-text-channel"); + const html_limit_tag = tag_container.find(".value-text-limit"); + + /* reset */ + html_tag_title.text(tr("You're chatting in Channel")); + html_limit_tag.text(""); + html_tag.children().detach(); + + /* initialize */ + if(channel) { + if(channel.properties.channel_icon_id != 0) + this.handle.handle.fileManager.icons.generateTag(channel.properties.channel_icon_id).appendTo(html_tag); + $.spawn("div").text(channel.formattedChannelName()).appendTo(html_tag); + + this.update_channel_limit(channel, html_limit_tag); + } else if(channel_tree && current_channel_id > 0) { + html_tag.append(formatMessage(tr("Unknown channel id {}"), current_channel_id)); + } else if(channel_tree && current_channel_id == 0) { + const server = this.handle.handle.channelTree.server; + if(server.properties.virtualserver_icon_id != 0) + this.handle.handle.fileManager.icons.generateTag(server.properties.virtualserver_icon_id).appendTo(html_tag); + $.spawn("div").text(server.properties.virtualserver_name).appendTo(html_tag); + html_tag_title.text(tr("You're chatting in Server")); + + this.update_server_limit(server, html_limit_tag); + } else if(this.handle.handle.connected) { + $.spawn("div").text("No channel selected").appendTo(html_tag); + } else { + $.spawn("div").text("Not connected").appendTo(html_tag); + } + } + + update_channel_client_count(channel: ChannelEntry) { + if(channel === this._channel_text) + this.update_channel_limit(channel, this._html_tag.find(".value-text-limit")); + if(channel === this._channel_voice) + this.update_channel_limit(channel, this._html_tag.find(".value-voice-limit")); + } + + private update_channel_limit(channel: ChannelEntry, tag: JQuery) { + let channel_limit = tr("Unlimited"); + if(!channel.properties.channel_flag_maxclients_unlimited) + channel_limit = "" + channel.properties.channel_maxclients; + else if(!channel.properties.channel_flag_maxfamilyclients_unlimited) { + if(channel.properties.channel_maxfamilyclients >= 0) + channel_limit = "" + channel.properties.channel_maxfamilyclients; + } + tag.text(channel.clients(false).length + " / " + channel_limit); + } + + private update_server_limit(server: ServerEntry, tag: JQuery) { + const fn = () => { + let text = server.properties.virtualserver_clientsonline + " / " + server.properties.virtualserver_maxclients; + if(server.properties.virtualserver_reserved_slots) + text += " (" + server.properties.virtualserver_reserved_slots + " " + tr("Reserved") + ")"; + tag.text(text); + }; + + server.updateProperties().then(fn).catch(error => tag.text(tr("Failed to update info"))); + fn(); + } + + update_chat_counter() { + const conversations = this.handle.private_conversations().conversations(); + { + const count = conversations.filter(e => e.is_unread()).length; + const count_container = this._html_tag.find(".container-indicator"); + const count_tag = count_container.find(".chat-unread-counter"); + count_container.toggle(count > 0); + count_tag.text(count); + } + { + const count_tag = this._html_tag.find(".chat-counter"); + if(conversations.length == 0) + count_tag.text(tr("No conversations")); + else if(conversations.length == 1) + count_tag.text(tr("One conversation")); else - this._value_ping.addClass("very-poor"); - } - - update_channel_talk() { - const client = this.handle.handle.getClient(); - const channel = client ? client.currentChannel() : undefined; - this._channel_voice = channel; - - const html_tag = this._html_tag.find(".value-voice-channel"); - const html_limit_tag = this._html_tag.find(".value-voice-limit"); - - html_limit_tag.text(""); - html_tag.children().remove(); - - if(channel) { - if(channel.properties.channel_icon_id != 0) - client.handle.fileManager.icons.generateTag(channel.properties.channel_icon_id).appendTo(html_tag); - $.spawn("div").text(channel.formattedChannelName()).appendTo(html_tag); - - this.update_channel_limit(channel, html_limit_tag); - } else { - $.spawn("div").text("Not connected").appendTo(html_tag); - } - } - - update_channel_text() { - const channel_tree = this.handle.handle.connected ? this.handle.handle.channelTree : undefined; - const current_channel_id = channel_tree ? this.handle.channel_conversations().current_channel() : 0; - const channel = channel_tree ? channel_tree.findChannel(current_channel_id) : undefined; - this._channel_text = channel; - - const tag_container = this._html_tag.find(".mode-channel_chat.channel"); - const html_tag_title = tag_container.find(".title"); - const html_tag = tag_container.find(".value-text-channel"); - const html_limit_tag = tag_container.find(".value-text-limit"); - - /* reset */ - html_tag_title.text(tr("You're chatting in Channel")); - html_limit_tag.text(""); - html_tag.children().detach(); - - /* initialize */ - if(channel) { - if(channel.properties.channel_icon_id != 0) - this.handle.handle.fileManager.icons.generateTag(channel.properties.channel_icon_id).appendTo(html_tag); - $.spawn("div").text(channel.formattedChannelName()).appendTo(html_tag); - - this.update_channel_limit(channel, html_limit_tag); - } else if(channel_tree && current_channel_id > 0) { - html_tag.append(MessageHelper.formatMessage(tr("Unknown channel id {}"), current_channel_id)); - } else if(channel_tree && current_channel_id == 0) { - const server = this.handle.handle.channelTree.server; - if(server.properties.virtualserver_icon_id != 0) - this.handle.handle.fileManager.icons.generateTag(server.properties.virtualserver_icon_id).appendTo(html_tag); - $.spawn("div").text(server.properties.virtualserver_name).appendTo(html_tag); - html_tag_title.text(tr("You're chatting in Server")); - - this.update_server_limit(server, html_limit_tag); - } else if(this.handle.handle.connected) { - $.spawn("div").text("No channel selected").appendTo(html_tag); - } else { - $.spawn("div").text("Not connected").appendTo(html_tag); - } - } - - update_channel_client_count(channel: ChannelEntry) { - if(channel === this._channel_text) - this.update_channel_limit(channel, this._html_tag.find(".value-text-limit")); - if(channel === this._channel_voice) - this.update_channel_limit(channel, this._html_tag.find(".value-voice-limit")); - } - - private update_channel_limit(channel: ChannelEntry, tag: JQuery) { - let channel_limit = tr("Unlimited"); - if(!channel.properties.channel_flag_maxclients_unlimited) - channel_limit = "" + channel.properties.channel_maxclients; - else if(!channel.properties.channel_flag_maxfamilyclients_unlimited) { - if(channel.properties.channel_maxfamilyclients >= 0) - channel_limit = "" + channel.properties.channel_maxfamilyclients; - } - tag.text(channel.clients(false).length + " / " + channel_limit); - } - - private update_server_limit(server: ServerEntry, tag: JQuery) { - const fn = () => { - let text = server.properties.virtualserver_clientsonline + " / " + server.properties.virtualserver_maxclients; - if(server.properties.virtualserver_reserved_slots) - text += " (" + server.properties.virtualserver_reserved_slots + " " + tr("Reserved") + ")"; - tag.text(text); - }; - - server.updateProperties().then(fn).catch(error => tag.text(tr("Failed to update info"))); - fn(); - } - - update_chat_counter() { - const conversations = this.handle.private_conversations().conversations(); - { - const count = conversations.filter(e => e.is_unread()).length; - const count_container = this._html_tag.find(".container-indicator"); - const count_tag = count_container.find(".chat-unread-counter"); - count_container.toggle(count > 0); - count_tag.text(count); - } - { - const count_tag = this._html_tag.find(".chat-counter"); - if(conversations.length == 0) - count_tag.text(tr("No conversations")); - else if(conversations.length == 1) - count_tag.text(tr("One conversation")); - else - count_tag.text(conversations.length + " " + tr("conversations")); - } - } - - current_mode() : InfoFrameMode { - return this._mode; - } - - set_mode(mode: InfoFrameMode) { - for(const mode in InfoFrameMode) - this._html_tag.removeClass("mode-" + InfoFrameMode[mode]); - this._html_tag.addClass("mode-" + mode); - - if(mode === InfoFrameMode.CLIENT_INFO && this._button_conversation) { - //Will be called every time a client is shown - const selected_client = this.handle.client_info().current_client(); - const conversation = selected_client ? this.handle.private_conversations().find_conversation({ - name: selected_client.properties.client_nickname, - unique_id: selected_client.properties.client_unique_identifier, - client_id: selected_client.clientId() - }, { create: false, attach: false }) : undefined; - - const visibility = (selected_client && selected_client.clientId() !== this.handle.handle.clientId) ? "visible" : "hidden"; - if(this._button_conversation.style.visibility !== visibility) - this._button_conversation.style.visibility = visibility; - if(conversation) { - this._button_conversation.innerText = tr("Open conversation"); - } else { - this._button_conversation.innerText = tr("Start a conversation"); - } - } else if(mode === InfoFrameMode.MUSIC_BOT) { - //TODO? - } + count_tag.text(conversations.length + " " + tr("conversations")); } } - export enum FrameContent { - NONE, - PRIVATE_CHAT, - CHANNEL_CHAT, - CLIENT_INFO, - MUSIC_BOT + current_mode() : InfoFrameMode { + return this._mode; } - export class Frame { - readonly handle: ConnectionHandler; - private _info_frame: InfoFrame; - private _html_tag: JQuery; - private _container_info: JQuery; - private _container_chat: JQuery; - private _content_type: FrameContent; + set_mode(mode: InfoFrameMode) { + for(const mode in InfoFrameMode) + this._html_tag.removeClass("mode-" + InfoFrameMode[mode]); + this._html_tag.addClass("mode-" + mode); - private _conversations: PrivateConverations; - private _client_info: ClientInfo; - private _music_info: MusicInfo; - private _channel_conversations: channel.ConversationManager; + if(mode === InfoFrameMode.CLIENT_INFO && this._button_conversation) { + //Will be called every time a client is shown + const selected_client = this.handle.client_info().current_client(); + const conversation = selected_client ? this.handle.private_conversations().find_conversation({ + name: selected_client.properties.client_nickname, + unique_id: selected_client.properties.client_unique_identifier, + client_id: selected_client.clientId() + }, { create: false, attach: false }) : undefined; - constructor(handle: ConnectionHandler) { - this.handle = handle; + const visibility = (selected_client && selected_client.clientId() !== this.handle.handle.clientId) ? "visible" : "hidden"; + if(this._button_conversation.style.visibility !== visibility) + this._button_conversation.style.visibility = visibility; + if(conversation) { + this._button_conversation.innerText = tr("Open conversation"); + } else { + this._button_conversation.innerText = tr("Start a conversation"); + } + } else if(mode === InfoFrameMode.MUSIC_BOT) { + //TODO? + } + } +} - this._content_type = FrameContent.NONE; - this._info_frame = new InfoFrame(this); - this._conversations = new PrivateConverations(this); - this._channel_conversations = new channel.ConversationManager(this); - this._client_info = new ClientInfo(this); - this._music_info = new MusicInfo(this); +export enum FrameContent { + NONE, + PRIVATE_CHAT, + CHANNEL_CHAT, + CLIENT_INFO, + MUSIC_BOT +} - this._build_html_tag(); +export class Frame { + readonly handle: ConnectionHandler; + private _info_frame: InfoFrame; + private _html_tag: JQuery; + private _container_info: JQuery; + private _container_chat: JQuery; + private _content_type: FrameContent; + + private _conversations: PrivateConverations; + private _client_info: ClientInfo; + private _music_info: MusicInfo; + private _channel_conversations: ConversationManager; + + constructor(handle: ConnectionHandler) { + this.handle = handle; + + this._content_type = FrameContent.NONE; + this._info_frame = new InfoFrame(this); + this._conversations = new PrivateConverations(this); + this._channel_conversations = new ConversationManager(this); + this._client_info = new ClientInfo(this); + this._music_info = new MusicInfo(this); + + this._build_html_tag(); + this.show_channel_conversations(); + this.info_frame().update_chat_counter(); + } + + html_tag() : JQuery { return this._html_tag; } + info_frame() : InfoFrame { return this._info_frame; } + + content_type() : FrameContent { return this._content_type; } + + destroy() { + this._html_tag && this._html_tag.remove(); + this._html_tag = undefined; + + this._info_frame && this._info_frame.destroy(); + this._info_frame = undefined; + + this._conversations && this._conversations.destroy(); + this._conversations = undefined; + + this._client_info && this._client_info.destroy(); + this._client_info = undefined; + + this._music_info && this._music_info.destroy(); + this._music_info = undefined; + + this._channel_conversations && this._channel_conversations.destroy(); + this._channel_conversations = undefined; + + this._container_info && this._container_info.remove(); + this._container_info = undefined; + + this._container_chat && this._container_chat.remove(); + this._container_chat = undefined; + } + + private _build_html_tag() { + this._html_tag = $("#tmpl_frame_chat").renderTag(); + this._container_info = this._html_tag.find(".container-info"); + this._container_chat = this._html_tag.find(".container-chat"); + + this._info_frame.html_tag().appendTo(this._container_info); + } + + + private_conversations() : PrivateConverations { + return this._conversations; + } + + channel_conversations() : ConversationManager { + return this._channel_conversations; + } + + client_info() : ClientInfo { + return this._client_info; + } + + music_info() : MusicInfo { + return this._music_info; + } + + private _clear() { + this._content_type = FrameContent.NONE; + this._container_chat.children().detach(); + } + + show_private_conversations() { + if(this._content_type === FrameContent.PRIVATE_CHAT) + return; + this._clear(); + this._content_type = FrameContent.PRIVATE_CHAT; + this._container_chat.append(this._conversations.html_tag()); + this._conversations.on_show(); + this._info_frame.set_mode(InfoFrameMode.PRIVATE_CHAT); + } + + show_channel_conversations() { + if(this._content_type === FrameContent.CHANNEL_CHAT) + return; + + this._clear(); + this._content_type = FrameContent.CHANNEL_CHAT; + this._container_chat.append(this._channel_conversations.html_tag()); + this._channel_conversations.on_show(); + this._info_frame.set_mode(InfoFrameMode.CHANNEL_CHAT); + } + + show_client_info(client: ClientEntry) { + this._client_info.set_current_client(client); + this._info_frame.set_mode(InfoFrameMode.CLIENT_INFO); /* specially needs an update here to update the conversation button */ + + if(this._content_type === FrameContent.CLIENT_INFO) + return; + + this._client_info.previous_frame_content = this._content_type; + this._clear(); + this._content_type = FrameContent.CLIENT_INFO; + this._container_chat.append(this._client_info.html_tag()); + } + + show_music_player(client: MusicClientEntry) { + this._music_info.set_current_bot(client); + + if(this._content_type === FrameContent.MUSIC_BOT) + return; + + this._info_frame.set_mode(InfoFrameMode.MUSIC_BOT); + this._music_info.previous_frame_content = this._content_type; + this._clear(); + this._content_type = FrameContent.MUSIC_BOT; + this._container_chat.append(this._music_info.html_tag()); + } + + set_content(type: FrameContent) { + if(this._content_type === type) + return; + + if(type === FrameContent.CHANNEL_CHAT) this.show_channel_conversations(); - this.info_frame().update_chat_counter(); - } - - html_tag() : JQuery { return this._html_tag; } - info_frame() : InfoFrame { return this._info_frame; } - - content_type() : FrameContent { return this._content_type; } - - destroy() { - this._html_tag && this._html_tag.remove(); - this._html_tag = undefined; - - this._info_frame && this._info_frame.destroy(); - this._info_frame = undefined; - - this._conversations && this._conversations.destroy(); - this._conversations = undefined; - - this._client_info && this._client_info.destroy(); - this._client_info = undefined; - - this._music_info && this._music_info.destroy(); - this._music_info = undefined; - - this._channel_conversations && this._channel_conversations.destroy(); - this._channel_conversations = undefined; - - this._container_info && this._container_info.remove(); - this._container_info = undefined; - - this._container_chat && this._container_chat.remove(); - this._container_chat = undefined; - } - - private _build_html_tag() { - this._html_tag = $("#tmpl_frame_chat").renderTag(); - this._container_info = this._html_tag.find(".container-info"); - this._container_chat = this._html_tag.find(".container-chat"); - - this._info_frame.html_tag().appendTo(this._container_info); - } - - - private_conversations() : PrivateConverations { - return this._conversations; - } - - channel_conversations() : channel.ConversationManager { - return this._channel_conversations; - } - - client_info() : ClientInfo { - return this._client_info; - } - - music_info() : MusicInfo { - return this._music_info; - } - - private _clear() { + else if(type === FrameContent.PRIVATE_CHAT) + this.show_private_conversations(); + else { + this._clear(); this._content_type = FrameContent.NONE; - this._container_chat.children().detach(); - } - - show_private_conversations() { - if(this._content_type === FrameContent.PRIVATE_CHAT) - return; - this._clear(); - this._content_type = FrameContent.PRIVATE_CHAT; - this._container_chat.append(this._conversations.html_tag()); - this._conversations.on_show(); - this._info_frame.set_mode(InfoFrameMode.PRIVATE_CHAT); - } - - show_channel_conversations() { - if(this._content_type === FrameContent.CHANNEL_CHAT) - return; - - this._clear(); - this._content_type = FrameContent.CHANNEL_CHAT; - this._container_chat.append(this._channel_conversations.html_tag()); - this._channel_conversations.on_show(); - this._info_frame.set_mode(InfoFrameMode.CHANNEL_CHAT); - } - - show_client_info(client: ClientEntry) { - this._client_info.set_current_client(client); - this._info_frame.set_mode(InfoFrameMode.CLIENT_INFO); /* specially needs an update here to update the conversation button */ - - if(this._content_type === FrameContent.CLIENT_INFO) - return; - - this._client_info.previous_frame_content = this._content_type; - this._clear(); - this._content_type = FrameContent.CLIENT_INFO; - this._container_chat.append(this._client_info.html_tag()); - } - - show_music_player(client: MusicClientEntry) { - this._music_info.set_current_bot(client); - - if(this._content_type === FrameContent.MUSIC_BOT) - return; - - this._info_frame.set_mode(InfoFrameMode.MUSIC_BOT); - this._music_info.previous_frame_content = this._content_type; - this._clear(); - this._content_type = FrameContent.MUSIC_BOT; - this._container_chat.append(this._music_info.html_tag()); - } - - set_content(type: FrameContent) { - if(this._content_type === type) - return; - - if(type === FrameContent.CHANNEL_CHAT) - this.show_channel_conversations(); - else if(type === FrameContent.PRIVATE_CHAT) - this.show_private_conversations(); - else { - this._clear(); - this._content_type = FrameContent.NONE; - this._info_frame.set_mode(InfoFrameMode.NONE); - } + this._info_frame.set_mode(InfoFrameMode.NONE); } } } \ No newline at end of file diff --git a/shared/js/ui/frames/connection_handlers.ts b/shared/js/ui/frames/connection_handlers.ts index 4d9fb936..1decafc0 100644 --- a/shared/js/ui/frames/connection_handlers.ts +++ b/shared/js/ui/frames/connection_handlers.ts @@ -1,7 +1,13 @@ +import {ConnectionHandler, DisconnectReason} from "tc-shared/ConnectionHandler"; +import {Settings, settings} from "tc-shared/settings"; +import {control_bar} from "tc-shared/ui/frames/ControlBar"; +import * as top_menu from "./MenuBar"; -let server_connections: ServerConnectionManager; - -class ServerConnectionManager { +export let server_connections: ServerConnectionManager; +export function initialize(manager: ServerConnectionManager) { + server_connections = manager; +} +export class ServerConnectionManager { private connection_handlers: ConnectionHandler[] = []; private active_handler: ConnectionHandler | undefined; diff --git a/shared/js/ui/frames/hostbanner.ts b/shared/js/ui/frames/hostbanner.ts index 2b0466d1..9fa20334 100644 --- a/shared/js/ui/frames/hostbanner.ts +++ b/shared/js/ui/frames/hostbanner.ts @@ -1,4 +1,9 @@ -class Hostbanner { +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {settings, Settings} from "tc-shared/settings"; +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; + +export class Hostbanner { readonly html_tag: JQuery; readonly client: ConnectionHandler; diff --git a/shared/js/ui/frames/image_preview.ts b/shared/js/ui/frames/image_preview.ts index af46412f..0ebd02b3 100644 --- a/shared/js/ui/frames/image_preview.ts +++ b/shared/js/ui/frames/image_preview.ts @@ -1,81 +1,81 @@ -namespace image_preview { - let preview_overlay: JQuery; - let container_image: JQuery; - let button_open_in_browser: JQuery; +import * as loader from "tc-loader"; - export function preview_image(url: string, original_url: string) { - if(!preview_overlay) return; +let preview_overlay: JQuery; +let container_image: JQuery; +let button_open_in_browser: JQuery; - container_image.empty(); - $.spawn("img").attr({ - "src": url, - "title": original_url, - "x-original-src": original_url - }).appendTo(container_image); +export function preview_image(url: string, original_url: string) { + if(!preview_overlay) return; - preview_overlay.removeClass("hidden"); - button_open_in_browser.show(); + container_image.empty(); + $.spawn("img").attr({ + "src": url, + "title": original_url, + "x-original-src": original_url + }).appendTo(container_image); + + preview_overlay.removeClass("hidden"); + button_open_in_browser.show(); +} + +export function preview_image_tag(tag: JQuery) { + if(!preview_overlay) return; + + container_image.empty(); + container_image.append(tag); + + preview_overlay.removeClass("hidden"); + button_open_in_browser.hide(); +} + +export function current_url() { + const image_tag = container_image.find("img"); + return image_tag.attr("x-original-src") || image_tag.attr("src") || ""; +} + +export function close_preview() { + preview_overlay.addClass("hidden"); +} + +loader.register_task(loader.Stage.LOADED, { + priority: 0, + name: "image preview init", + function: async () => { + preview_overlay = $("#overlay-image-preview"); + container_image = preview_overlay.find(".container-image") as any; + + preview_overlay.find("img").on('click', event => event.preventDefault()); + preview_overlay.on('click', event => { + if(event.isDefaultPrevented()) return; + close_preview(); + }); + + preview_overlay.find(".button-close").on('click', event => { + event.preventDefault(); + close_preview(); + }); + + preview_overlay.find(".button-download").on('click', event => { + event.preventDefault(); + + const link = document.createElement('a'); + link.href = current_url(); + link.target = "_blank"; + + const findex = link.href.lastIndexOf("/") + 1; + link.download = link.href.substr(findex); + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }); + + button_open_in_browser = preview_overlay.find(".button-open-in-window"); + button_open_in_browser.on('click', event => { + event.preventDefault(); + + const win = window.open(current_url(), '_blank'); + win.focus(); + }); } - - export function preview_image_tag(tag: JQuery) { - if(!preview_overlay) return; - - container_image.empty(); - container_image.append(tag); - - preview_overlay.removeClass("hidden"); - button_open_in_browser.hide(); - } - - export function current_url() { - const image_tag = container_image.find("img"); - return image_tag.attr("x-original-src") || image_tag.attr("src") || ""; - } - - export function close_preview() { - preview_overlay.addClass("hidden"); - } - - loader.register_task(loader.Stage.LOADED, { - priority: 0, - name: "image preview init", - function: async () => { - preview_overlay = $("#overlay-image-preview"); - container_image = preview_overlay.find(".container-image") as any; - - preview_overlay.find("img").on('click', event => event.preventDefault()); - preview_overlay.on('click', event => { - if(event.isDefaultPrevented()) return; - close_preview(); - }); - - preview_overlay.find(".button-close").on('click', event => { - event.preventDefault(); - close_preview(); - }); - - preview_overlay.find(".button-download").on('click', event => { - event.preventDefault(); - - const link = document.createElement('a'); - link.href = current_url(); - link.target = "_blank"; - - const findex = link.href.lastIndexOf("/") + 1; - link.download = link.href.substr(findex); - - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - }); - - button_open_in_browser = preview_overlay.find(".button-open-in-window"); - button_open_in_browser.on('click', event => { - event.preventDefault(); - - const win = window.open(current_url(), '_blank'); - win.focus(); - }); - } - }); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/shared/js/ui/frames/server_log.ts b/shared/js/ui/frames/server_log.ts index d6e29f22..bfaaab6b 100644 --- a/shared/js/ui/frames/server_log.ts +++ b/shared/js/ui/frames/server_log.ts @@ -1,566 +1,563 @@ -namespace log { - export namespace server { - export enum Type { - CONNECTION_BEGIN = "connection_begin", - CONNECTION_HOSTNAME_RESOLVE = "connection_hostname_resolve", - CONNECTION_HOSTNAME_RESOLVE_ERROR = "connection_hostname_resolve_error", - CONNECTION_HOSTNAME_RESOLVED = "connection_hostname_resolved", - CONNECTION_LOGIN = "connection_login", - CONNECTION_CONNECTED = "connection_connected", - CONNECTION_FAILED = "connection_failed", +import {tra} from "tc-shared/i18n/localize"; +import {PermissionInfo} from "tc-shared/permission/PermissionManager"; +import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler"; +import * as htmltags from "tc-shared/ui/htmltags"; +import {bbcode_chat, format_time, formatMessage} from "tc-shared/ui/frames/chat"; +import {formatDate} from "tc-shared/MessageFormatter"; - DISCONNECTED = "disconnected", +export enum Type { + CONNECTION_BEGIN = "connection_begin", + CONNECTION_HOSTNAME_RESOLVE = "connection_hostname_resolve", + CONNECTION_HOSTNAME_RESOLVE_ERROR = "connection_hostname_resolve_error", + CONNECTION_HOSTNAME_RESOLVED = "connection_hostname_resolved", + CONNECTION_LOGIN = "connection_login", + CONNECTION_CONNECTED = "connection_connected", + CONNECTION_FAILED = "connection_failed", - CONNECTION_VOICE_SETUP_FAILED = "connection_voice_setup_failed", - CONNECTION_COMMAND_ERROR = "connection_command_error", + DISCONNECTED = "disconnected", - GLOBAL_MESSAGE = "global_message", + CONNECTION_VOICE_SETUP_FAILED = "connection_voice_setup_failed", + CONNECTION_COMMAND_ERROR = "connection_command_error", - SERVER_WELCOME_MESSAGE = "server_welcome_message", - SERVER_HOST_MESSAGE = "server_host_message", - SERVER_HOST_MESSAGE_DISCONNECT = "server_host_message_disconnect", + GLOBAL_MESSAGE = "global_message", - SERVER_CLOSED = "server_closed", - SERVER_BANNED = "server_banned", - SERVER_REQUIRES_PASSWORD = "server_requires_password", + SERVER_WELCOME_MESSAGE = "server_welcome_message", + SERVER_HOST_MESSAGE = "server_host_message", + SERVER_HOST_MESSAGE_DISCONNECT = "server_host_message_disconnect", - CLIENT_VIEW_ENTER = "client_view_enter", - CLIENT_VIEW_LEAVE = "client_view_leave", - CLIENT_VIEW_MOVE = "client_view_move", + SERVER_CLOSED = "server_closed", + SERVER_BANNED = "server_banned", + SERVER_REQUIRES_PASSWORD = "server_requires_password", - CLIENT_NICKNAME_CHANGED = "client_nickname_changed", - CLIENT_NICKNAME_CHANGE_FAILED = "client_nickname_change_failed", + CLIENT_VIEW_ENTER = "client_view_enter", + CLIENT_VIEW_LEAVE = "client_view_leave", + CLIENT_VIEW_MOVE = "client_view_move", - CLIENT_SERVER_GROUP_ADD = "client_server_group_add", - CLIENT_SERVER_GROUP_REMOVE = "client_server_group_remove", - CLIENT_CHANNEL_GROUP_CHANGE = "client_channel_group_change", + CLIENT_NICKNAME_CHANGED = "client_nickname_changed", + CLIENT_NICKNAME_CHANGE_FAILED = "client_nickname_change_failed", - CHANNEL_CREATE = "channel_create", - CHANNEL_DELETE = "channel_delete", + CLIENT_SERVER_GROUP_ADD = "client_server_group_add", + CLIENT_SERVER_GROUP_REMOVE = "client_server_group_remove", + CLIENT_CHANNEL_GROUP_CHANGE = "client_channel_group_change", - ERROR_CUSTOM = "error_custom", - ERROR_PERMISSION = "error_permission", + CHANNEL_CREATE = "channel_create", + CHANNEL_DELETE = "channel_delete", - RECONNECT_SCHEDULED = "reconnect_scheduled", - RECONNECT_EXECUTE = "reconnect_execute", - RECONNECT_CANCELED = "reconnect_canceled" - } + ERROR_CUSTOM = "error_custom", + ERROR_PERMISSION = "error_permission", - export namespace base { - export type Client = { - client_unique_id: string; - client_name: string; - client_id: number; - } - export type Channel = { - channel_id: number; - channel_name: string; - } - export type Server = { - server_name: string; - server_unique_id: string; - } - export type ServerAddress = { - server_hostname: string; - server_port: number; - } - } + RECONNECT_SCHEDULED = "reconnect_scheduled", + RECONNECT_EXECUTE = "reconnect_execute", + RECONNECT_CANCELED = "reconnect_canceled" +} - export namespace event { - export type GlobalMessage = { - sender: base.Client; - message: string; - } - export type ConnectBegin = { - address: base.ServerAddress; - client_nickname: string; - } - export type ErrorCustom = { - message: string; - } +export namespace base { + export type Client = { + client_unique_id: string; + client_name: string; + client_id: number; + } + export type Channel = { + channel_id: number; + channel_name: string; + } + export type Server = { + server_name: string; + server_unique_id: string; + } + export type ServerAddress = { + server_hostname: string; + server_port: number; + } +} - export type ReconnectScheduled = { - timeout: number; - } - - export type ReconnectCanceled = { } - export type ReconnectExecute = { } - - export type ErrorPermission = { - permission: PermissionInfo; - } - - export type WelcomeMessage = { - message: string; - } - - export type HostMessageDisconnect = { - message: string; - } - - export type ClientMove = { - channel_from?: base.Channel; - channel_from_own: boolean; - - channel_to?: base.Channel; - channel_to_own: boolean; - - client: base.Client; - client_own: boolean; - - invoker?: base.Client; - - message?: string; - reason: ViewReasonId; - } - - export type ClientEnter = { - channel_from?: base.Channel; - channel_to?: base.Channel; - - client: base.Client; - invoker?: base.Client; - - message?: string; - own_channel: boolean; - - reason: ViewReasonId; - ban_time?: number; - } - - export type ClientLeave = { - channel_from?: base.Channel; - channel_to?: base.Channel; - - client: base.Client; - invoker?: base.Client; - - message?: string; - own_channel: boolean; - - reason: ViewReasonId; - ban_time?: number; - } - - export type ChannelCreate = { - creator: base.Client; - channel: base.Channel; - - own_action: boolean; - } - - export type ChannelDelete = { - deleter: base.Client; - channel: base.Channel; - - own_action: boolean; - } - - export type ConnectionConnected = { - own_client: base.Client; - } - export type ConnectionFailed = {}; - export type ConnectionLogin = {} - export type ConnectionHostnameResolve = {}; - export type ConnectionHostnameResolved = { - address: base.ServerAddress; - } - export type ConnectionHostnameResolveError = { - message: string; - } - - export type ConnectionVoiceSetupFailed = { - reason: string; - reconnect_delay: number; /* if less or equal to 0 reconnect is prohibited */ - } - - export type ConnectionCommandError = { - error: any; - } - - export type ClientNicknameChanged = { - own_client: boolean; - - client: base.Client; - - old_name: string; - new_name: string; - } - - export type ClientNicknameChangeFailed = { - reason: string; - } - - export type ServerClosed = { - message: string; - } - - export type ServerRequiresPassword = {} - - export type ServerBanned = { - message: string; - time: number; - - invoker: base.Client; - } - } - - export type LogMessage = { - type: Type; - timestamp: number; - data: any; - } - - export interface TypeInfo { - "connection_begin" : event.ConnectBegin; - "global_message": event.GlobalMessage; - - "error_custom": event.ErrorCustom; - "error_permission": event.ErrorPermission; - - "connection_hostname_resolved": event.ConnectionHostnameResolved; - "connection_hostname_resolve": event.ConnectionHostnameResolve; - "connection_hostname_resolve_error": event.ConnectionHostnameResolveError; - "connection_failed": event.ConnectionFailed; - "connection_login": event.ConnectionLogin; - "connection_connected": event.ConnectionConnected; - "connection_voice_setup_failed": event.ConnectionVoiceSetupFailed; - "connection_command_error": event.ConnectionCommandError; - - "reconnect_scheduled": event.ReconnectScheduled; - "reconnect_canceled": event.ReconnectCanceled; - "reconnect_execute": event.ReconnectExecute; - - "server_welcome_message": event.WelcomeMessage; - "server_host_message": event.WelcomeMessage; - "server_host_message_disconnect": event.HostMessageDisconnect; - - "server_closed": event.ServerClosed; - "server_requires_password": event.ServerRequiresPassword; - "server_banned": event.ServerBanned; - - "client_view_enter": event.ClientEnter; - "client_view_move": event.ClientMove; - "client_view_leave": event.ClientLeave; - - "client_nickname_change_failed": event.ClientNicknameChangeFailed, - "client_nickname_changed": event.ClientNicknameChanged, - - "channel_create": event.ChannelCreate; - "channel_delete": event.ChannelDelete; - - "disconnected": any; - } - - export type MessageBuilderOptions = {}; - export type MessageBuilder = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | undefined; - - export const MessageBuilders: {[key: string]: MessageBuilder} = { - "error_custom": (data: event.ErrorCustom, options) => { - return [$.spawn("div").addClass("log-error").text(data.message)] - } - }; +export namespace event { + export type GlobalMessage = { + sender: base.Client; + message: string; + } + export type ConnectBegin = { + address: base.ServerAddress; + client_nickname: string; + } + export type ErrorCustom = { + message: string; } - export class ServerLog { - private readonly handle: ConnectionHandler; - private history_length: number = 100; + export type ReconnectScheduled = { + timeout: number; + } - private _log: server.LogMessage[] = []; - private _html_tag: JQuery; - private _log_container: JQuery; - private auto_follow: boolean; /* automatic scroll to bottom */ - private _ignore_event: number; /* after auto scroll we've triggered the scroll event. We want to prevent this so we capture the next event */ + export type ReconnectCanceled = { } + export type ReconnectExecute = { } - constructor(handle: ConnectionHandler) { - this.handle = handle; - this.auto_follow = true; + export type ErrorPermission = { + permission: PermissionInfo; + } - this._html_tag = $.spawn("div").addClass("container-log"); - this._log_container = $.spawn("div").addClass("container-messages"); - this._log_container.appendTo(this._html_tag); + export type WelcomeMessage = { + message: string; + } - this._html_tag.on('scroll', event => { - if(Date.now() - this._ignore_event < 100) { - this._ignore_event = 0; - return; - } + export type HostMessageDisconnect = { + message: string; + } - this.auto_follow = (this._html_tag[0].scrollTop + this._html_tag[0].clientHeight + this._html_tag[0].clientHeight * .125) > this._html_tag[0].scrollHeight; - }); - } + export type ClientMove = { + channel_from?: base.Channel; + channel_from_own: boolean; - log(type: T, data: server.TypeInfo[T]) { - const event = { - data: data, - timestamp: Date.now(), - type: type as any - }; + channel_to?: base.Channel; + channel_to_own: boolean; - this._log.push(event); - while(this._log.length > this.history_length) - this._log.pop_front(); + client: base.Client; + client_own: boolean; - this.append_log(event); - } + invoker?: base.Client; - html_tag() : JQuery { - return this._html_tag; - } + message?: string; + reason: ViewReasonId; + } - destroy() { - this._html_tag && this._html_tag.remove(); - this._html_tag = undefined; - this._log_container = undefined; + export type ClientEnter = { + channel_from?: base.Channel; + channel_to?: base.Channel; - this._log = undefined; - } + client: base.Client; + invoker?: base.Client; - private _scroll_task: number; + message?: string; + own_channel: boolean; - private append_log(message: server.LogMessage) { - let container = $.spawn("div").addClass("log-message"); + reason: ViewReasonId; + ban_time?: number; + } - /* build timestamp */ - { - const num = number => ('00' + number).substr(-2); - const date = new Date(message.timestamp); - $.spawn("div") - .addClass("timestamp") - .text("<" + num(date.getHours()) + ":" + num(date.getMinutes()) + ":" + num(date.getSeconds()) + ">") - .appendTo(container); + export type ClientLeave = { + channel_from?: base.Channel; + channel_to?: base.Channel; + + client: base.Client; + invoker?: base.Client; + + message?: string; + own_channel: boolean; + + reason: ViewReasonId; + ban_time?: number; + } + + export type ChannelCreate = { + creator: base.Client; + channel: base.Channel; + + own_action: boolean; + } + + export type ChannelDelete = { + deleter: base.Client; + channel: base.Channel; + + own_action: boolean; + } + + export type ConnectionConnected = { + own_client: base.Client; + } + export type ConnectionFailed = {}; + export type ConnectionLogin = {} + export type ConnectionHostnameResolve = {}; + export type ConnectionHostnameResolved = { + address: base.ServerAddress; + } + export type ConnectionHostnameResolveError = { + message: string; + } + + export type ConnectionVoiceSetupFailed = { + reason: string; + reconnect_delay: number; /* if less or equal to 0 reconnect is prohibited */ + } + + export type ConnectionCommandError = { + error: any; + } + + export type ClientNicknameChanged = { + own_client: boolean; + + client: base.Client; + + old_name: string; + new_name: string; + } + + export type ClientNicknameChangeFailed = { + reason: string; + } + + export type ServerClosed = { + message: string; + } + + export type ServerRequiresPassword = {} + + export type ServerBanned = { + message: string; + time: number; + + invoker: base.Client; + } +} + +export type LogMessage = { + type: Type; + timestamp: number; + data: any; +} + +export interface TypeInfo { + "connection_begin" : event.ConnectBegin; + "global_message": event.GlobalMessage; + + "error_custom": event.ErrorCustom; + "error_permission": event.ErrorPermission; + + "connection_hostname_resolved": event.ConnectionHostnameResolved; + "connection_hostname_resolve": event.ConnectionHostnameResolve; + "connection_hostname_resolve_error": event.ConnectionHostnameResolveError; + "connection_failed": event.ConnectionFailed; + "connection_login": event.ConnectionLogin; + "connection_connected": event.ConnectionConnected; + "connection_voice_setup_failed": event.ConnectionVoiceSetupFailed; + "connection_command_error": event.ConnectionCommandError; + + "reconnect_scheduled": event.ReconnectScheduled; + "reconnect_canceled": event.ReconnectCanceled; + "reconnect_execute": event.ReconnectExecute; + + "server_welcome_message": event.WelcomeMessage; + "server_host_message": event.WelcomeMessage; + "server_host_message_disconnect": event.HostMessageDisconnect; + + "server_closed": event.ServerClosed; + "server_requires_password": event.ServerRequiresPassword; + "server_banned": event.ServerBanned; + + "client_view_enter": event.ClientEnter; + "client_view_move": event.ClientMove; + "client_view_leave": event.ClientLeave; + + "client_nickname_change_failed": event.ClientNicknameChangeFailed, + "client_nickname_changed": event.ClientNicknameChanged, + + "channel_create": event.ChannelCreate; + "channel_delete": event.ChannelDelete; + + "disconnected": any; +} + +export type MessageBuilderOptions = {}; +export type MessageBuilder = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | undefined; + +export const MessageBuilders: {[key: string]: MessageBuilder} = { + "error_custom": (data: event.ErrorCustom, options) => { + return [$.spawn("div").addClass("log-error").text(data.message)] + } +}; + +export class ServerLog { + private readonly handle: ConnectionHandler; + private history_length: number = 100; + + private _log: LogMessage[] = []; + private _html_tag: JQuery; + private _log_container: JQuery; + private auto_follow: boolean; /* automatic scroll to bottom */ + private _ignore_event: number; /* after auto scroll we've triggered the scroll event. We want to prevent this so we capture the next event */ + + constructor(handle: ConnectionHandler) { + this.handle = handle; + this.auto_follow = true; + + this._html_tag = $.spawn("div").addClass("container-log"); + this._log_container = $.spawn("div").addClass("container-messages"); + this._log_container.appendTo(this._html_tag); + + this._html_tag.on('scroll', event => { + if(Date.now() - this._ignore_event < 100) { + this._ignore_event = 0; + return; } - /* build message data */ - { - const builder = server.MessageBuilders[message.type]; - if(!builder) { - MessageHelper.formatMessage(tr("missing log message builder {0}!"), message.type).forEach(e => e.addClass("log-error").appendTo(container)); - } else { - const elements = builder(message.data, {}); - if(!elements || elements.length == 0) - return; /* discard message */ - container.append(...elements); - } + this.auto_follow = (this._html_tag[0].scrollTop + this._html_tag[0].clientHeight + this._html_tag[0].clientHeight * .125) > this._html_tag[0].scrollHeight; + }); + } + + log(type: T, data: TypeInfo[T]) { + const event = { + data: data, + timestamp: Date.now(), + type: type as any + }; + + this._log.push(event); + while(this._log.length > this.history_length) + this._log.pop_front(); + + this.append_log(event); + } + + html_tag() : JQuery { + return this._html_tag; + } + + destroy() { + this._html_tag && this._html_tag.remove(); + this._html_tag = undefined; + this._log_container = undefined; + + this._log = undefined; + } + + private _scroll_task: number; + + private append_log(message: LogMessage) { + let container = $.spawn("div").addClass("log-message"); + + /* build timestamp */ + { + const num = number => ('00' + number).substr(-2); + const date = new Date(message.timestamp); + $.spawn("div") + .addClass("timestamp") + .text("<" + num(date.getHours()) + ":" + num(date.getMinutes()) + ":" + num(date.getSeconds()) + ">") + .appendTo(container); + } + + /* build message data */ + { + const builder = MessageBuilders[message.type]; + if(!builder) { + formatMessage(tr("missing log message builder {0}!"), message.type).forEach(e => e.addClass("log-error").appendTo(container)); + } else { + const elements = builder(message.data, {}); + if(!elements || elements.length == 0) + return; /* discard message */ + container.append(...elements); } - this._ignore_event = Date.now(); - this._log_container.append(container); + } + this._ignore_event = Date.now(); + this._log_container.append(container); - /* max history messages! */ - const messages = this._log_container.children(); - let index = 0; - while(messages.length - index > this.history_length) - index++; - const hide_elements = messages.filter(idx => idx < index); - hide_elements.hide(250, () => hide_elements.remove()); + /* max history messages! */ + const messages = this._log_container.children(); + let index = 0; + while(messages.length - index > this.history_length) + index++; + const hide_elements = messages.filter(idx => idx < index); + hide_elements.hide(250, () => hide_elements.remove()); - if(this.auto_follow) { - clearTimeout(this._scroll_task); + if(this.auto_follow) { + clearTimeout(this._scroll_task); - /* do not enforce a recalculate style here */ - this._scroll_task = setTimeout(() => { - this._html_tag.scrollTop(this._html_tag[0].scrollHeight); - this._scroll_task = 0; - }, 5) as any; - } + /* do not enforce a recalculate style here */ + this._scroll_task = setTimeout(() => { + this._html_tag.scrollTop(this._html_tag[0].scrollHeight); + this._scroll_task = 0; + }, 5) as any; } } } /* impl of the parsers */ -namespace log { - export namespace server { - namespace impl { - const client_tag = (client: base.Client, braces?: boolean) => htmltags.generate_client_object({ - client_unique_id: client.client_unique_id, - client_id: client.client_id, - client_name: client.client_name, - add_braces: braces - }); - const channel_tag = (channel: base.Channel, braces?: boolean) => htmltags.generate_channel_object({ - channel_display_name: channel.channel_name, - channel_name: channel.channel_name, - channel_id: channel.channel_id, - add_braces: braces - }); +const client_tag = (client: base.Client, braces?: boolean) => htmltags.generate_client_object({ + client_unique_id: client.client_unique_id, + client_id: client.client_id, + client_name: client.client_name, + add_braces: braces +}); +const channel_tag = (channel: base.Channel, braces?: boolean) => htmltags.generate_channel_object({ + channel_display_name: channel.channel_name, + channel_name: channel.channel_name, + channel_id: channel.channel_id, + add_braces: braces +}); - MessageBuilders["connection_begin"] = (data: event.ConnectBegin, options) => { - return MessageHelper.formatMessage(tr("Connecting to {0}{1}"), data.address.server_hostname, data.address.server_port == 9987 ? "" : (":" + data.address.server_port)); - }; +MessageBuilders["connection_begin"] = (data: event.ConnectBegin, options) => { + return formatMessage(tr("Connecting to {0}{1}"), data.address.server_hostname, data.address.server_port == 9987 ? "" : (":" + data.address.server_port)); +}; - MessageBuilders["connection_hostname_resolve"] = (data: event.ConnectionHostnameResolve, options) => MessageHelper.formatMessage(tr("Resolving hostname")); - MessageBuilders["connection_hostname_resolved"] = (data: event.ConnectionHostnameResolved, options) => MessageHelper.formatMessage(tr("Hostname resolved successfully to {0}:{1}"), data.address.server_hostname, data.address.server_port); - MessageBuilders["connection_hostname_resolve_error"] = (data: event.ConnectionHostnameResolveError, options) => MessageHelper.formatMessage(tr("Failed to resolve hostname. Connecting to given hostname. Error: {0}"), data.message); +MessageBuilders["connection_hostname_resolve"] = (data: event.ConnectionHostnameResolve, options) => formatMessage(tr("Resolving hostname")); +MessageBuilders["connection_hostname_resolved"] = (data: event.ConnectionHostnameResolved, options) => formatMessage(tr("Hostname resolved successfully to {0}:{1}"), data.address.server_hostname, data.address.server_port); +MessageBuilders["connection_hostname_resolve_error"] = (data: event.ConnectionHostnameResolveError, options) => formatMessage(tr("Failed to resolve hostname. Connecting to given hostname. Error: {0}"), data.message); - MessageBuilders["connection_login"] = (data: event.ConnectionLogin, options) => MessageHelper.formatMessage(tr("Logging in...")); - MessageBuilders["connection_failed"] = (data: event.ConnectionFailed, options) => MessageHelper.formatMessage(tr("Connect failed.")); - MessageBuilders["connection_connected"] = (data: event.ConnectionConnected, options) => MessageHelper.formatMessage(tr("Connected as {0}"), client_tag(data.own_client, true)); +MessageBuilders["connection_login"] = (data: event.ConnectionLogin, options) => formatMessage(tr("Logging in...")); +MessageBuilders["connection_failed"] = (data: event.ConnectionFailed, options) => formatMessage(tr("Connect failed.")); +MessageBuilders["connection_connected"] = (data: event.ConnectionConnected, options) => formatMessage(tr("Connected as {0}"), client_tag(data.own_client, true)); - MessageBuilders["connection_voice_setup_failed"] = (data: event.ConnectionVoiceSetupFailed, options) => { - return MessageHelper.formatMessage(tr("Failed to setup voice bridge: {0}. Allow reconnect: {1}"), data.reason, data.reconnect_delay > 0 ? tr("yes") : tr("no")); - }; +MessageBuilders["connection_voice_setup_failed"] = (data: event.ConnectionVoiceSetupFailed, options) => { + return formatMessage(tr("Failed to setup voice bridge: {0}. Allow reconnect: {1}"), data.reason, data.reconnect_delay > 0 ? tr("yes") : tr("no")); +}; - MessageBuilders["error_permission"] = (data: event.ErrorPermission, options) => { - return MessageHelper.formatMessage(tr("Insufficient client permissions. Failed on permission {0}"), data.permission ? data.permission.name : "unknown").map(e => e.addClass("log-error")); - }; +MessageBuilders["error_permission"] = (data: event.ErrorPermission, options) => { + return formatMessage(tr("Insufficient client permissions. Failed on permission {0}"), data.permission ? data.permission.name : "unknown").map(e => e.addClass("log-error")); +}; - MessageBuilders["client_view_enter"] = (data: event.ClientEnter, options) => { - if(data.reason == ViewReasonId.VREASON_SYSTEM) { - return undefined; - } if(data.reason == ViewReasonId.VREASON_USER_ACTION) { - /* client appeared */ - if(data.channel_from) { - return MessageHelper.formatMessage(data.own_channel ? tr("{0} appeared from {1} to your {2}") : tr("{0} appeared from {1} to {2}"), client_tag(data.client), channel_tag(data.channel_from), channel_tag(data.channel_to)); - } else { - return MessageHelper.formatMessage(data.own_channel ? tr("{0} connected to your channel {1}") : tr("{0} connected to channel {1}"), client_tag(data.client), channel_tag(data.channel_to)); - } - } else if(data.reason == ViewReasonId.VREASON_MOVED) { - if(data.channel_from) { - return MessageHelper.formatMessage(data.own_channel ? tr("{0} appeared from {1} to your channel {2}, moved by {3}") : tr("{0} appeared from {1} to {2}, moved by {3}"), - client_tag(data.client), - channel_tag(data.channel_from), - channel_tag(data.channel_to), - client_tag(data.invoker) - ); - } else { - return MessageHelper.formatMessage(data.own_channel ? tr("{0} appeared to your channel {1}, moved by {2}") : tr("{0} appeared to {1}, moved by {2}"), - client_tag(data.client), - channel_tag(data.channel_to), - client_tag(data.invoker) - ); - } - } else if(data.reason == ViewReasonId.VREASON_CHANNEL_KICK) { - if(data.channel_from) { - return MessageHelper.formatMessage(data.own_channel ? tr("{0} appeared from {1} to your channel {2}, kicked by {3}{4}") : tr("{0} appeared from {1} to {2}, kicked by {3}{4}"), - client_tag(data.client), - channel_tag(data.channel_from), - channel_tag(data.channel_to), - client_tag(data.invoker), - data.message ? (" (" + data.message + ")") : "" - ); - } else { - return MessageHelper.formatMessage(data.own_channel ? tr("{0} appeared to your channel {1}, kicked by {2}{3}") : tr("{0} appeared to {1}, kicked by {2}{3}"), - client_tag(data.client), - channel_tag(data.channel_to), - client_tag(data.invoker), - data.message ? (" (" + data.message + ")") : "" - ); - } - } - return [$.spawn("div").addClass("log-error").text("Invalid view enter reason id (" + data.message + ")")]; - }; - - MessageBuilders["client_view_move"] = (data: event.ClientMove, options) => { - if(data.reason == ViewReasonId.VREASON_MOVED) { - return MessageHelper.formatMessage(data.client_own ? tr("You was moved by {3} from channel {1} to {2}") : tr("{0} was moved from channel {1} to {2} by {3}"), - client_tag(data.client), - channel_tag(data.channel_from), - channel_tag(data.channel_to), - client_tag(data.invoker) - ); - } else if(data.reason == ViewReasonId.VREASON_USER_ACTION) { - return MessageHelper.formatMessage(data.client_own ? tr("You switched from channel {1} to {2}") : tr("{0} switched from channel {1} to {2}"), - client_tag(data.client), - channel_tag(data.channel_from), - channel_tag(data.channel_to) - ); - } else if(data.reason == ViewReasonId.VREASON_CHANNEL_KICK) { - return MessageHelper.formatMessage(data.client_own ? tr("You got kicked out of the channel {1} to channel {2} by {3}{4}") : tr("{0} got kicked from channel {1} to {2} by {3}{4}"), - client_tag(data.client), - channel_tag(data.channel_from), - channel_tag(data.channel_to), - client_tag(data.invoker), - data.message ? (" (" + data.message + ")") : "" - ); - } - return [$.spawn("div").addClass("log-error").text("Invalid view move reason id (" + data.reason + ")")]; - }; - - MessageBuilders["client_view_leave"] = (data: event.ClientLeave, options) => { - if(data.reason == ViewReasonId.VREASON_USER_ACTION) { - return MessageHelper.formatMessage(data.own_channel ? tr("{0} disappeared from your channel {1} to {2}") : tr("{0} disappeared from {1} to {2}"), client_tag(data.client), channel_tag(data.channel_from), channel_tag(data.channel_to)); - } else if(data.reason == ViewReasonId.VREASON_SERVER_LEFT) { - return MessageHelper.formatMessage(tr("{0} left the server{1}"), client_tag(data.client), data.message ? (" (" + data.message + ")") : ""); - } else if(data.reason == ViewReasonId.VREASON_SERVER_KICK) { - return MessageHelper.formatMessage(tr("{0} was kicked from the server by {1}.{2}"), client_tag(data.client), client_tag(data.invoker), data.message ? (" (" + data.message + ")") : ""); - } else if(data.reason == ViewReasonId.VREASON_CHANNEL_KICK) { - return MessageHelper.formatMessage(data.own_channel ? tr("{0} was kicked from your channel by {2}.{3}") : tr("{0} was kicked from channel {1} by {2}.{3}"), - client_tag(data.client), - channel_tag(data.channel_from), - client_tag(data.invoker), - data.message ? (" (" + data.message + ")") : "" - ); - } else if(data.reason == ViewReasonId.VREASON_BAN) { - let duration = "permanently"; - if(data.ban_time) - duration = "for " + formatDate(data.ban_time); - return MessageHelper.formatMessage(tr("{0} was banned {1} by {2}.{3}"), - client_tag(data.client), - duration, - client_tag(data.invoker), - data.message ? (" (" + data.message + ")") : "" - ); - } else if(data.reason == ViewReasonId.VREASON_TIMEOUT) { - return MessageHelper.formatMessage(tr("{0} timed out{1}"), client_tag(data.client), data.message ? (" (" + data.message + ")") : ""); - } else if(data.reason == ViewReasonId.VREASON_MOVED) { - return MessageHelper.formatMessage(data.own_channel ? tr("{0} disappeared from your channel {1} to {2}, moved by {3}") : tr("{0} disappeared from {1} to {2}, moved by {3}"), client_tag(data.client), channel_tag(data.channel_from), channel_tag(data.channel_to), client_tag(data.invoker)); - } - - return [$.spawn("div").addClass("log-error").text("Invalid view leave reason id (" + data.reason + ")")]; - }; - - MessageBuilders["server_welcome_message"] = (data: event.WelcomeMessage, options) => { - return MessageHelper.bbcode_chat("[color=green]" + data.message + "[/color]"); - }; - - MessageBuilders["server_host_message"] = (data: event.WelcomeMessage, options) => { - return MessageHelper.bbcode_chat("[color=green]" + data.message + "[/color]"); - }; - - MessageBuilders["client_nickname_changed"] = (data: event.ClientNicknameChanged, options) => { - if(data.own_client) { - return MessageHelper.formatMessage(tr("Nickname successfully changed.")); - } else { - return MessageHelper.formatMessage(tr("{0} changed his nickname from \"{1}\" to \"{2}\""), client_tag(data.client), data.old_name, data.new_name); - } - }; - - MessageBuilders["global_message"] = (data: event.GlobalMessage, options) => { - return []; /* we do not show global messages within log */ - }; - - MessageBuilders["disconnected"] = () => MessageHelper.formatMessage(tr("Disconnected from server")); - - MessageBuilders["reconnect_scheduled"] = (data: event.ReconnectScheduled, options) => { - return tra("Reconnecting in {0}.", MessageHelper.format_time(data.timeout, tr("now"))) - }; - - MessageBuilders["reconnect_canceled"] = (data: event.ReconnectCanceled, options) => { - return tra("Canceled reconnect.") - }; - - MessageBuilders["reconnect_execute"] = (data: event.ReconnectExecute, options) => { - return tra("Reconnecting...") - }; - - MessageBuilders["server_banned"] = (data: event.ServerBanned, options) => { - let result: JQuery[]; - - const time = data.time == 0 ? tr("ever") : MessageHelper.format_time(data.time * 1000, tr("one second")); - if(data.invoker.client_id > 0) { - if(data.message) - result = tra("You've been banned from the server by {0} for {1}. Reason: {2}", client_tag(data.invoker), time, data.message); - else - result = tra("You've been banned from the server by {0} for {1}.", client_tag(data.invoker), time); - } else { - if(data.message) - result = tra("You've been banned from the server for {0}. Reason: {1}", time, data.message); - else - result = tra("You've been banned from the server for {0}.", time); - } - - return result.map(e => e.addClass("log-error")); - }; +MessageBuilders["client_view_enter"] = (data: event.ClientEnter, options) => { + if(data.reason == ViewReasonId.VREASON_SYSTEM) { + return undefined; + } if(data.reason == ViewReasonId.VREASON_USER_ACTION) { + /* client appeared */ + if(data.channel_from) { + return formatMessage(data.own_channel ? tr("{0} appeared from {1} to your {2}") : tr("{0} appeared from {1} to {2}"), client_tag(data.client), channel_tag(data.channel_from), channel_tag(data.channel_to)); + } else { + return formatMessage(data.own_channel ? tr("{0} connected to your channel {1}") : tr("{0} connected to channel {1}"), client_tag(data.client), channel_tag(data.channel_to)); + } + } else if(data.reason == ViewReasonId.VREASON_MOVED) { + if(data.channel_from) { + return formatMessage(data.own_channel ? tr("{0} appeared from {1} to your channel {2}, moved by {3}") : tr("{0} appeared from {1} to {2}, moved by {3}"), + client_tag(data.client), + channel_tag(data.channel_from), + channel_tag(data.channel_to), + client_tag(data.invoker) + ); + } else { + return formatMessage(data.own_channel ? tr("{0} appeared to your channel {1}, moved by {2}") : tr("{0} appeared to {1}, moved by {2}"), + client_tag(data.client), + channel_tag(data.channel_to), + client_tag(data.invoker) + ); + } + } else if(data.reason == ViewReasonId.VREASON_CHANNEL_KICK) { + if(data.channel_from) { + return formatMessage(data.own_channel ? tr("{0} appeared from {1} to your channel {2}, kicked by {3}{4}") : tr("{0} appeared from {1} to {2}, kicked by {3}{4}"), + client_tag(data.client), + channel_tag(data.channel_from), + channel_tag(data.channel_to), + client_tag(data.invoker), + data.message ? (" (" + data.message + ")") : "" + ); + } else { + return formatMessage(data.own_channel ? tr("{0} appeared to your channel {1}, kicked by {2}{3}") : tr("{0} appeared to {1}, kicked by {2}{3}"), + client_tag(data.client), + channel_tag(data.channel_to), + client_tag(data.invoker), + data.message ? (" (" + data.message + ")") : "" + ); } } -} \ No newline at end of file + return [$.spawn("div").addClass("log-error").text("Invalid view enter reason id (" + data.message + ")")]; +}; + +MessageBuilders["client_view_move"] = (data: event.ClientMove, options) => { + if(data.reason == ViewReasonId.VREASON_MOVED) { + return formatMessage(data.client_own ? tr("You was moved by {3} from channel {1} to {2}") : tr("{0} was moved from channel {1} to {2} by {3}"), + client_tag(data.client), + channel_tag(data.channel_from), + channel_tag(data.channel_to), + client_tag(data.invoker) + ); + } else if(data.reason == ViewReasonId.VREASON_USER_ACTION) { + return formatMessage(data.client_own ? tr("You switched from channel {1} to {2}") : tr("{0} switched from channel {1} to {2}"), + client_tag(data.client), + channel_tag(data.channel_from), + channel_tag(data.channel_to) + ); + } else if(data.reason == ViewReasonId.VREASON_CHANNEL_KICK) { + return formatMessage(data.client_own ? tr("You got kicked out of the channel {1} to channel {2} by {3}{4}") : tr("{0} got kicked from channel {1} to {2} by {3}{4}"), + client_tag(data.client), + channel_tag(data.channel_from), + channel_tag(data.channel_to), + client_tag(data.invoker), + data.message ? (" (" + data.message + ")") : "" + ); + } + return [$.spawn("div").addClass("log-error").text("Invalid view move reason id (" + data.reason + ")")]; +}; + +MessageBuilders["client_view_leave"] = (data: event.ClientLeave, options) => { + if(data.reason == ViewReasonId.VREASON_USER_ACTION) { + return formatMessage(data.own_channel ? tr("{0} disappeared from your channel {1} to {2}") : tr("{0} disappeared from {1} to {2}"), client_tag(data.client), channel_tag(data.channel_from), channel_tag(data.channel_to)); + } else if(data.reason == ViewReasonId.VREASON_SERVER_LEFT) { + return formatMessage(tr("{0} left the server{1}"), client_tag(data.client), data.message ? (" (" + data.message + ")") : ""); + } else if(data.reason == ViewReasonId.VREASON_SERVER_KICK) { + return formatMessage(tr("{0} was kicked from the server by {1}.{2}"), client_tag(data.client), client_tag(data.invoker), data.message ? (" (" + data.message + ")") : ""); + } else if(data.reason == ViewReasonId.VREASON_CHANNEL_KICK) { + return formatMessage(data.own_channel ? tr("{0} was kicked from your channel by {2}.{3}") : tr("{0} was kicked from channel {1} by {2}.{3}"), + client_tag(data.client), + channel_tag(data.channel_from), + client_tag(data.invoker), + data.message ? (" (" + data.message + ")") : "" + ); + } else if(data.reason == ViewReasonId.VREASON_BAN) { + let duration = "permanently"; + if(data.ban_time) + duration = "for " + formatDate(data.ban_time); + return formatMessage(tr("{0} was banned {1} by {2}.{3}"), + client_tag(data.client), + duration, + client_tag(data.invoker), + data.message ? (" (" + data.message + ")") : "" + ); + } else if(data.reason == ViewReasonId.VREASON_TIMEOUT) { + return formatMessage(tr("{0} timed out{1}"), client_tag(data.client), data.message ? (" (" + data.message + ")") : ""); + } else if(data.reason == ViewReasonId.VREASON_MOVED) { + return formatMessage(data.own_channel ? tr("{0} disappeared from your channel {1} to {2}, moved by {3}") : tr("{0} disappeared from {1} to {2}, moved by {3}"), client_tag(data.client), channel_tag(data.channel_from), channel_tag(data.channel_to), client_tag(data.invoker)); + } + + return [$.spawn("div").addClass("log-error").text("Invalid view leave reason id (" + data.reason + ")")]; +}; + +MessageBuilders["server_welcome_message"] = (data: event.WelcomeMessage, options) => { + return bbcode_chat("[color=green]" + data.message + "[/color]"); +}; + +MessageBuilders["server_host_message"] = (data: event.WelcomeMessage, options) => { + return bbcode_chat("[color=green]" + data.message + "[/color]"); +}; + +MessageBuilders["client_nickname_changed"] = (data: event.ClientNicknameChanged, options) => { + if(data.own_client) { + return formatMessage(tr("Nickname successfully changed.")); + } else { + return formatMessage(tr("{0} changed his nickname from \"{1}\" to \"{2}\""), client_tag(data.client), data.old_name, data.new_name); + } +}; + +MessageBuilders["global_message"] = (data: event.GlobalMessage, options) => { + return []; /* we do not show global messages within log */ +}; + +MessageBuilders["disconnected"] = () => formatMessage(tr("Disconnected from server")); + +MessageBuilders["reconnect_scheduled"] = (data: event.ReconnectScheduled, options) => { + return tra("Reconnecting in {0}.", format_time(data.timeout, tr("now"))) +}; + +MessageBuilders["reconnect_canceled"] = (data: event.ReconnectCanceled, options) => { + return tra("Canceled reconnect.") +}; + +MessageBuilders["reconnect_execute"] = (data: event.ReconnectExecute, options) => { + return tra("Reconnecting...") +}; + +MessageBuilders["server_banned"] = (data: event.ServerBanned, options) => { + let result: JQuery[]; + + const time = data.time == 0 ? tr("ever") : format_time(data.time * 1000, tr("one second")); + if(data.invoker.client_id > 0) { + if(data.message) + result = tra("You've been banned from the server by {0} for {1}. Reason: {2}", client_tag(data.invoker), time, data.message); + else + result = tra("You've been banned from the server by {0} for {1}.", client_tag(data.invoker), time); + } else { + if(data.message) + result = tra("You've been banned from the server for {0}. Reason: {1}", time, data.message); + else + result = tra("You've been banned from the server for {0}.", time); + } + + return result.map(e => e.addClass("log-error")); +}; \ No newline at end of file diff --git a/shared/js/ui/frames/side/chat_box.ts b/shared/js/ui/frames/side/chat_box.ts index 863f9335..b8f2cc7d 100644 --- a/shared/js/ui/frames/side/chat_box.ts +++ b/shared/js/ui/frames/side/chat_box.ts @@ -1,267 +1,268 @@ -namespace chat { - declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; - declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; +import {Settings, settings} from "tc-shared/settings"; +import {helpers} from "tc-shared/ui/frames/side/chat_helper"; - export class ChatBox { - private _html_tag: JQuery; - private _html_input: JQuery; - private _enabled: boolean; - private __callback_text_changed; - private __callback_key_down; - private __callback_key_up; - private __callback_paste; +declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; +declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; - private _typing_timeout: number; /* ID when the next callback_typing will be called */ - private _typing_last_event: number; /* timestamp of the last typing event */ +export class ChatBox { + private _html_tag: JQuery; + private _html_input: JQuery; + private _enabled: boolean; + private __callback_text_changed; + private __callback_key_down; + private __callback_key_up; + private __callback_paste; - private _message_history: string[] = []; - private _message_history_length = 100; - private _message_history_index = 0; + private _typing_timeout: number; /* ID when the next callback_typing will be called */ + private _typing_last_event: number; /* timestamp of the last typing event */ - typing_interval: number = 2000; /* update frequency */ - callback_typing: () => any; - callback_text: (text: string) => any; + private _message_history: string[] = []; + private _message_history_length = 100; + private _message_history_index = 0; - constructor() { - this._enabled = true; - this.__callback_key_up = this._callback_key_up.bind(this); - this.__callback_key_down = this._callback_key_down.bind(this); - this.__callback_text_changed = this._callback_text_changed.bind(this); - this.__callback_paste = event => this._callback_paste(event); + typing_interval: number = 2000; /* update frequency */ + callback_typing: () => any; + callback_text: (text: string) => any; - this._build_html_tag(); - this._initialize_listener(); - } + constructor() { + this._enabled = true; + this.__callback_key_up = this._callback_key_up.bind(this); + this.__callback_key_down = this._callback_key_down.bind(this); + this.__callback_text_changed = this._callback_text_changed.bind(this); + this.__callback_paste = event => this._callback_paste(event); - html_tag() : JQuery { - return this._html_tag; - } + this._build_html_tag(); + this._initialize_listener(); + } - destroy() { - this._html_tag && this._html_tag.remove(); - this._html_tag = undefined; - this._html_input = undefined; + html_tag() : JQuery { + return this._html_tag; + } - clearTimeout(this._typing_timeout); + destroy() { + this._html_tag && this._html_tag.remove(); + this._html_tag = undefined; + this._html_input = undefined; - this.__callback_text_changed = undefined; - this.__callback_key_down = undefined; - this.__callback_paste = undefined; + clearTimeout(this._typing_timeout); - this.callback_text = undefined; - this.callback_typing = undefined; - } + this.__callback_text_changed = undefined; + this.__callback_key_down = undefined; + this.__callback_paste = undefined; - private _initialize_listener() { - this._html_input.on("cut paste drop keydown keyup", (event) => this.__callback_text_changed(event)); - this._html_input.on("change", this.__callback_text_changed); - this._html_input.on("keydown", this.__callback_key_down); - this._html_input.on("keyup", this.__callback_key_up); - this._html_input.on("paste", this.__callback_paste); - } + this.callback_text = undefined; + this.callback_typing = undefined; + } - private _build_html_tag() { - this._html_tag = $("#tmpl_frame_chat_chatbox").renderTag({ - emojy_support: settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES) - }); - this._html_input = this._html_tag.find(".textarea") as any; + private _initialize_listener() { + this._html_input.on("cut paste drop keydown keyup", (event) => this.__callback_text_changed(event)); + this._html_input.on("change", this.__callback_text_changed); + this._html_input.on("keydown", this.__callback_key_down); + this._html_input.on("keyup", this.__callback_key_up); + this._html_input.on("paste", this.__callback_paste); + } - const tag: JQuery & { lsxEmojiPicker(args: any); } = this._html_tag.find('.button-emoji') as any; - tag.lsxEmojiPicker({ - width: 300, - height: 400, - twemoji: typeof(window.twemoji) !== "undefined", - onSelect: emoji => this._html_input.html(this._html_input.html() + emoji.value), - closeOnSelect: false - }); - } + private _build_html_tag() { + this._html_tag = $("#tmpl_frame_chat_chatbox").renderTag({ + emojy_support: settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES) + }); + this._html_input = this._html_tag.find(".textarea") as any; - private _callback_text_changed(event: Event) { - if(event && event.defaultPrevented) + const tag: JQuery & { lsxEmojiPicker(args: any); } = this._html_tag.find('.button-emoji') as any; + tag.lsxEmojiPicker({ + width: 300, + height: 400, + twemoji: typeof(window.twemoji) !== "undefined", + onSelect: emoji => this._html_input.html(this._html_input.html() + emoji.value), + closeOnSelect: false + }); + } + + private _callback_text_changed(event: Event) { + if(event && event.defaultPrevented) + return; + + /* Auto resize */ + const text = this._html_input[0]; + text.style.height = "1em"; + text.style.height = text.scrollHeight + 'px'; + + if(!event || (event.type !== "keydown" && event.type !== "keyup" && event.type !== "change")) + return; + + this._typing_last_event = Date.now(); + if(this._typing_timeout) + return; + + const _trigger_typing = (last_time: number) => { + if(this._typing_last_event <= last_time) { + this._typing_timeout = 0; return; - - /* Auto resize */ - const text = this._html_input[0]; - text.style.height = "1em"; - text.style.height = text.scrollHeight + 'px'; - - if(!event || (event.type !== "keydown" && event.type !== "keyup" && event.type !== "change")) - return; - - this._typing_last_event = Date.now(); - if(this._typing_timeout) - return; - - const _trigger_typing = (last_time: number) => { - if(this._typing_last_event <= last_time) { - this._typing_timeout = 0; - return; - } - - try { - if(this.callback_typing) - this.callback_typing(); - } finally { - this._typing_timeout = setTimeout(_trigger_typing, this.typing_interval, this._typing_last_event); - } - }; - _trigger_typing(0); /* We def want that*/ - } - - private _text(element: HTMLElement) { - if(typeof(element) !== "object") - return element; - - if(element instanceof HTMLImageElement) - return element.alt || element.title; - if(element instanceof HTMLBRElement) { - return '\n'; } - if(element.childNodes.length > 0) - return [...element.childNodes].map(e => this._text(e as HTMLElement)).join(""); + try { + if(this.callback_typing) + this.callback_typing(); + } finally { + this._typing_timeout = setTimeout(_trigger_typing, this.typing_interval, this._typing_last_event); + } + }; + _trigger_typing(0); /* We def want that*/ + } - if(element.nodeType == Node.TEXT_NODE) - return element.textContent; - return typeof(element.innerText) === "string" ? element.innerText : ""; + private _text(element: HTMLElement) { + if(typeof(element) !== "object") + return element; + + if(element instanceof HTMLImageElement) + return element.alt || element.title; + if(element instanceof HTMLBRElement) { + return '\n'; } - private htmlEscape(message: string) : string { - const div = document.createElement('div'); - div.innerText = message; - message = div.innerHTML; - return message.replace(/ /g, ' '); - } - private _callback_paste(event: ClipboardEvent) { - const _event = (event).originalEvent as ClipboardEvent || event; - const clipboard = _event.clipboardData || (window).clipboardData; - if(!clipboard) return; + if(element.childNodes.length > 0) + return [...element.childNodes].map(e => this._text(e as HTMLElement)).join(""); + + if(element.nodeType == Node.TEXT_NODE) + return element.textContent; + return typeof(element.innerText) === "string" ? element.innerText : ""; + } + + private htmlEscape(message: string) : string { + const div = document.createElement('div'); + div.innerText = message; + message = div.innerHTML; + return message.replace(/ /g, ' '); + } + private _callback_paste(event: ClipboardEvent) { + const _event = (event).originalEvent as ClipboardEvent || event; + const clipboard = _event.clipboardData || (window).clipboardData; + if(!clipboard) return; - const raw_text = clipboard.getData('text/plain'); - const selection = window.getSelection(); - if (!selection.rangeCount) - return false; + const raw_text = clipboard.getData('text/plain'); + const selection = window.getSelection(); + if (!selection.rangeCount) + return false; - let html_xml = clipboard.getData('text/html'); - if(!html_xml) - html_xml = $.spawn("div").text(raw_text).html(); + let html_xml = clipboard.getData('text/html'); + if(!html_xml) + html_xml = $.spawn("div").text(raw_text).html(); - const parser = new DOMParser(); - const nodes = parser.parseFromString(html_xml, "text/html"); + const parser = new DOMParser(); + const nodes = parser.parseFromString(html_xml, "text/html"); - let data = this._text(nodes.body); + let data = this._text(nodes.body); - /* fix prefix & suffix new lines */ + /* fix prefix & suffix new lines */ + { + let prefix_length = 0, suffix_length = 0; { - let prefix_length = 0, suffix_length = 0; - { - for(let i = 0; i < raw_text.length; i++) - if(raw_text.charAt(i) === '\n') - prefix_length++; - else if(raw_text.charAt(i) !== '\r') - break; - for(let i = raw_text.length - 1; i >= 0; i++) - if(raw_text.charAt(i) === '\n') - suffix_length++; - else if(raw_text.charAt(i) !== '\r') - break; - } - - data = data.replace(/^[\n\r]+|[\n\r]+$/g, ''); - data = "\n".repeat(prefix_length) + data + "\n".repeat(suffix_length); + for(let i = 0; i < raw_text.length; i++) + if(raw_text.charAt(i) === '\n') + prefix_length++; + else if(raw_text.charAt(i) !== '\r') + break; + for(let i = raw_text.length - 1; i >= 0; i++) + if(raw_text.charAt(i) === '\n') + suffix_length++; + else if(raw_text.charAt(i) !== '\r') + break; } + + data = data.replace(/^[\n\r]+|[\n\r]+$/g, ''); + data = "\n".repeat(prefix_length) + data + "\n".repeat(suffix_length); + } + event.preventDefault(); + + selection.deleteFromDocument(); + document.execCommand('insertHTML', false, this.htmlEscape(data)); + } + + private test_message(message: string) : boolean { + message = message + .replace(/ /gi, "") + .replace(/
/gi, "") + .replace(/\n/gi, "") + .replace(//gi, ""); + return message.length > 0; + } + + private _callback_key_down(event: KeyboardEvent) { + if(event.key.toLowerCase() === "enter" && !event.shiftKey) { event.preventDefault(); - selection.deleteFromDocument(); - document.execCommand('insertHTML', false, this.htmlEscape(data)); - } - - private test_message(message: string) : boolean { - message = message - .replace(/ /gi, "") - .replace(/
/gi, "") - .replace(/\n/gi, "") - .replace(//gi, ""); - return message.length > 0; - } - - private _callback_key_down(event: KeyboardEvent) { - if(event.key.toLowerCase() === "enter" && !event.shiftKey) { - event.preventDefault(); - - /* deactivate chatbox when no callback? */ - let text = this._html_input[0].innerText as string; - if(!this.test_message(text)) - return; - - this._message_history.push(text); - this._message_history_index = this._message_history.length; - if(this._message_history.length > this._message_history_length) - this._message_history = this._message_history.slice(this._message_history.length - this._message_history_length); - - if(this.callback_text) { - this.callback_text(helpers.preprocess_chat_message(text)); - } - - if(this._typing_timeout) - clearTimeout(this._typing_timeout); - this._typing_timeout = 1; /* enforce no typing update while sending */ - this._html_input.text(""); - setTimeout(() => { - this.__callback_text_changed(); - this._typing_timeout = 0; /* enable text change listener again */ - }); - } else if(event.key.toLowerCase() === "arrowdown") { - //TODO: Test for at the last line within the box - if(this._message_history_index < 0) return; - if(this._message_history_index >= this._message_history.length) return; /* OOB, even with the empty message */ - - this._message_history_index++; - this._html_input[0].innerText = this._message_history[this._message_history_index] || ""; /* OOB just returns "undefined" */ - } else if(event.key.toLowerCase() === "arrowup") { - //TODO: Test for at the first line within the box - if(this._message_history_index <= 0) return; /* we cant go "down" */ - this._message_history_index--; - this._html_input[0].innerText = this._message_history[this._message_history_index]; - } else { - if(this._message_history_index >= 0) { - if(this._message_history_index >= this._message_history.length) { - if("" !== this._html_input[0].innerText) - this._message_history_index = -1; - } else if(this._message_history[this._message_history_index] !== this._html_input[0].innerText) - this._message_history_index = -1; - } - } - } - - private _callback_key_up(event: KeyboardEvent) { - if("" === this._html_input[0].innerText) - this._message_history_index = this._message_history.length; - } - - private _context_task: number; - set_enabled(flag: boolean) { - if(this._enabled === flag) + /* deactivate chatbox when no callback? */ + let text = this._html_input[0].innerText as string; + if(!this.test_message(text)) return; - if(!this._context_task) { - this._enabled = flag; - /* Allow the browser to asynchronously recalculate everything */ - this._context_task = setTimeout(() => { - this._context_task = undefined; - this._html_input.each((_, e) => { e.contentEditable = this._enabled ? "true" : "false"; }); - }); - this._html_tag.find('.button-emoji').toggleClass("disabled", !flag); + this._message_history.push(text); + this._message_history_index = this._message_history.length; + if(this._message_history.length > this._message_history_length) + this._message_history = this._message_history.slice(this._message_history.length - this._message_history_length); + + if(this.callback_text) { + this.callback_text(helpers.preprocess_chat_message(text)); + } + + if(this._typing_timeout) + clearTimeout(this._typing_timeout); + this._typing_timeout = 1; /* enforce no typing update while sending */ + this._html_input.text(""); + setTimeout(() => { + this.__callback_text_changed(); + this._typing_timeout = 0; /* enable text change listener again */ + }); + } else if(event.key.toLowerCase() === "arrowdown") { + //TODO: Test for at the last line within the box + if(this._message_history_index < 0) return; + if(this._message_history_index >= this._message_history.length) return; /* OOB, even with the empty message */ + + this._message_history_index++; + this._html_input[0].innerText = this._message_history[this._message_history_index] || ""; /* OOB just returns "undefined" */ + } else if(event.key.toLowerCase() === "arrowup") { + //TODO: Test for at the first line within the box + if(this._message_history_index <= 0) return; /* we cant go "down" */ + this._message_history_index--; + this._html_input[0].innerText = this._message_history[this._message_history_index]; + } else { + if(this._message_history_index >= 0) { + if(this._message_history_index >= this._message_history.length) { + if("" !== this._html_input[0].innerText) + this._message_history_index = -1; + } else if(this._message_history[this._message_history_index] !== this._html_input[0].innerText) + this._message_history_index = -1; } } + } - is_enabled() { - return this._enabled; - } + private _callback_key_up(event: KeyboardEvent) { + if("" === this._html_input[0].innerText) + this._message_history_index = this._message_history.length; + } - focus_input() { - this._html_input.focus(); + private _context_task: number; + set_enabled(flag: boolean) { + if(this._enabled === flag) + return; + + if(!this._context_task) { + this._enabled = flag; + /* Allow the browser to asynchronously recalculate everything */ + this._context_task = setTimeout(() => { + this._context_task = undefined; + this._html_input.each((_, e) => { e.contentEditable = this._enabled ? "true" : "false"; }); + }); + this._html_tag.find('.button-emoji').toggleClass("disabled", !flag); } } + + is_enabled() { + return this._enabled; + } + + focus_input() { + this._html_input.focus(); + } } \ No newline at end of file diff --git a/shared/js/ui/frames/side/chat_helper.ts b/shared/js/ui/frames/side/chat_helper.ts index c3e7396f..1522741c 100644 --- a/shared/js/ui/frames/side/chat_helper.ts +++ b/shared/js/ui/frames/side/chat_helper.ts @@ -1,423 +1,426 @@ -namespace chat { - export namespace helpers { - //https://regex101.com/r/YQbfcX/2 - //static readonly URL_REGEX = /^(?([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/(?(?:[^\s?]+)?)(?:\?(?\S+))?)?$/gm; - const URL_REGEX = /^(([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/((?:[^\s?]+)?)(?:\?(\S+))?)?$/gm; - function process_urls(message: string) : string { - const words = message.split(/[ \n]/); - for(let index = 0; index < words.length; index++) { - const flag_escaped = words[index].startsWith('!'); - const unescaped = flag_escaped ? words[index].substr(1) : words[index]; +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import {Settings, settings} from "tc-shared/settings"; - _try: - try { - const url = new URL(unescaped); - log.debug(LogCategory.GENERAL, tr("Chat message contains URL: %o"), url); - if(url.protocol !== 'http:' && url.protocol !== 'https:') - break _try; - if(flag_escaped) { - message = undefined; - words[index] = unescaped; - } else { - message = undefined; - words[index] = "[url=" + url.toString() + "]" + url.toString() + "[/url]"; - } - } catch(e) { /* word isn't an url */ } +declare const xbbcode; +export namespace helpers { + //https://regex101.com/r/YQbfcX/2 + //static readonly URL_REGEX = /^(?([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/(?(?:[^\s?]+)?)(?:\?(?\S+))?)?$/gm; + const URL_REGEX = /^(([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/((?:[^\s?]+)?)(?:\?(\S+))?)?$/gm; + function process_urls(message: string) : string { + const words = message.split(/[ \n]/); + for(let index = 0; index < words.length; index++) { + const flag_escaped = words[index].startsWith('!'); + const unescaped = flag_escaped ? words[index].substr(1) : words[index]; - if(unescaped.match(URL_REGEX)) { + _try: + try { + const url = new URL(unescaped); + log.debug(LogCategory.GENERAL, tr("Chat message contains URL: %o"), url); + if(url.protocol !== 'http:' && url.protocol !== 'https:') + break _try; if(flag_escaped) { message = undefined; words[index] = unescaped; } else { message = undefined; - words[index] = "[url=" + unescaped + "]" + unescaped + "[/url]"; + words[index] = "[url=" + url.toString() + "]" + url.toString() + "[/url]"; } + } catch(e) { /* word isn't an url */ } + + if(unescaped.match(URL_REGEX)) { + if(flag_escaped) { + message = undefined; + words[index] = unescaped; + } else { + message = undefined; + words[index] = "[url=" + unescaped + "]" + unescaped + "[/url]"; } } - - return message || words.join(" "); } - namespace md2bbc { - export type RemarkToken = { - type: string; - tight: boolean; - lines: number[]; - level: number; + return message || words.join(" "); + } - /* img */ - alt?: string; - src?: string; + namespace md2bbc { + export type RemarkToken = { + type: string; + tight: boolean; + lines: number[]; + level: number; - /* link */ - href?: string; + /* img */ + alt?: string; + src?: string; - /* table */ - align?: string; + /* link */ + href?: string; - /* code */ - params?: string; + /* table */ + align?: string; - content?: string; - hLevel?: number; - children?: RemarkToken[]; - } + /* code */ + params?: string; - export class Renderer { - private static renderers = { - "text": (renderer: Renderer, token: RemarkToken) => renderer.options().process_url ? process_urls(renderer.maybe_escape_bb(token.content)) : renderer.maybe_escape_bb(token.content), - "softbreak": () => "\n", - "hardbreak": () => "\n", + content?: string; + hLevel?: number; + children?: RemarkToken[]; + } - "paragraph_open": (renderer: Renderer, token: RemarkToken) => { - const last_line = !renderer.last_paragraph || !renderer.last_paragraph.lines ? 0 : renderer.last_paragraph.lines[1]; - const lines = token.lines[0] - last_line; - return [...new Array(lines)].map(e => "[br]").join(""); - }, - "paragraph_close": () => "", + export class Renderer { + private static renderers = { + "text": (renderer: Renderer, token: RemarkToken) => renderer.options().process_url ? process_urls(renderer.maybe_escape_bb(token.content)) : renderer.maybe_escape_bb(token.content), + "softbreak": () => "\n", + "hardbreak": () => "\n", - "strong_open": (renderer: Renderer, token: RemarkToken) => "[b]", - "strong_close": (renderer: Renderer, token: RemarkToken) => "[/b]", + "paragraph_open": (renderer: Renderer, token: RemarkToken) => { + const last_line = !renderer.last_paragraph || !renderer.last_paragraph.lines ? 0 : renderer.last_paragraph.lines[1]; + const lines = token.lines[0] - last_line; + return [...new Array(lines)].map(e => "[br]").join(""); + }, + "paragraph_close": () => "", - "em_open": (renderer: Renderer, token: RemarkToken) => "[i]", - "em_close": (renderer: Renderer, token: RemarkToken) => "[/i]", + "strong_open": (renderer: Renderer, token: RemarkToken) => "[b]", + "strong_close": (renderer: Renderer, token: RemarkToken) => "[/b]", - "del_open": () => "[s]", - "del_close": () => "[/s]", + "em_open": (renderer: Renderer, token: RemarkToken) => "[i]", + "em_close": (renderer: Renderer, token: RemarkToken) => "[/i]", - "sup": (renderer: Renderer, token: RemarkToken) => "[sup]" + renderer.maybe_escape_bb(token.content) + "[/sup]", - "sub": (renderer: Renderer, token: RemarkToken) => "[sub]" + renderer.maybe_escape_bb(token.content) + "[/sub]", + "del_open": () => "[s]", + "del_close": () => "[/s]", - "bullet_list_open": () => "[ul]", - "bullet_list_close": () => "[/ul]", + "sup": (renderer: Renderer, token: RemarkToken) => "[sup]" + renderer.maybe_escape_bb(token.content) + "[/sup]", + "sub": (renderer: Renderer, token: RemarkToken) => "[sub]" + renderer.maybe_escape_bb(token.content) + "[/sub]", - "ordered_list_open": () => "[ol]", - "ordered_list_close": () => "[/ol]", + "bullet_list_open": () => "[ul]", + "bullet_list_close": () => "[/ul]", - "list_item_open": () => "[li]", - "list_item_close": () => "[/li]", + "ordered_list_open": () => "[ol]", + "ordered_list_close": () => "[/ol]", - "table_open": () => "[table]", - "table_close": () => "[/table]", + "list_item_open": () => "[li]", + "list_item_close": () => "[/li]", - "thead_open": () => "", - "thead_close": () => "", + "table_open": () => "[table]", + "table_close": () => "[/table]", - "tbody_open": () => "", - "tbody_close": () => "", + "thead_open": () => "", + "thead_close": () => "", - "tr_open": () => "[tr]", - "tr_close": () => "[/tr]", + "tbody_open": () => "", + "tbody_close": () => "", - "th_open": (renderer: Renderer, token: RemarkToken) => "[th" + (token.align ? ("=" + token.align) : "") + "]", - "th_close": () => "[/th]", + "tr_open": () => "[tr]", + "tr_close": () => "[/tr]", - "td_open": () => "[td]", - "td_close": () => "[/td]", + "th_open": (renderer: Renderer, token: RemarkToken) => "[th" + (token.align ? ("=" + token.align) : "") + "]", + "th_close": () => "[/th]", - "link_open": (renderer: Renderer, token: RemarkToken) => "[url" + (token.href ? ("=" + token.href) : "") + "]", - "link_close": () => "[/url]", + "td_open": () => "[td]", + "td_close": () => "[/td]", - "image": (renderer: Renderer, token: RemarkToken) => "[img=" + (token.src) + "]" + (token.alt || token.src) + "[/img]", + "link_open": (renderer: Renderer, token: RemarkToken) => "[url" + (token.href ? ("=" + token.href) : "") + "]", + "link_close": () => "[/url]", - //footnote_ref + "image": (renderer: Renderer, token: RemarkToken) => "[img=" + (token.src) + "]" + (token.alt || token.src) + "[/img]", - //"content": "==Marked text==", - //mark_open - //mark_close + //footnote_ref - //++Inserted text++ - "ins_open": () => "[u]", - "ins_close": () => "[/u]", + //"content": "==Marked text==", + //mark_open + //mark_close - /* + //++Inserted text++ + "ins_open": () => "[u]", + "ins_close": () => "[/u]", + + /* ``` test [/code] test ``` - */ + */ - "code": (renderer: Renderer, token: RemarkToken) => "[i-code]" + xbbcode.escape(token.content) + "[/i-code]", - "fence": (renderer: Renderer, token: RemarkToken) => "[code" + (token.params ? ("=" + token.params) : "") + "]" + xbbcode.escape(token.content) + "[/code]", + "code": (renderer: Renderer, token: RemarkToken) => "[i-code]" + xbbcode.escape(token.content) + "[/i-code]", + "fence": (renderer: Renderer, token: RemarkToken) => "[code" + (token.params ? ("=" + token.params) : "") + "]" + xbbcode.escape(token.content) + "[/code]", - "heading_open": (renderer: Renderer, token: RemarkToken) => "[size=" + (9 - Math.min(4, token.hLevel)) + "]", - "heading_close": (renderer: Renderer, token: RemarkToken) => "[/size][hr]", + "heading_open": (renderer: Renderer, token: RemarkToken) => "[size=" + (9 - Math.min(4, token.hLevel)) + "]", + "heading_close": (renderer: Renderer, token: RemarkToken) => "[/size][hr]", - "hr": () => "[hr]", + "hr": () => "[hr]", - //> Experience real-time editing with Remarkable! - //blockquote_open, - //blockquote_close - }; + //> Experience real-time editing with Remarkable! + //blockquote_open, + //blockquote_close + }; - private _options; - last_paragraph: RemarkToken; + private _options; + last_paragraph: RemarkToken; - render(tokens: RemarkToken[], options: any, env: any) { - this.last_paragraph = undefined; - this._options = options; - let result = ''; + render(tokens: RemarkToken[], options: any, env: any) { + this.last_paragraph = undefined; + this._options = options; + let result = ''; - //TODO: Escape BB-Codes - for(let index = 0; index < tokens.length; index++) { - if (tokens[index].type === 'inline') { - result += this.render_inline(tokens[index].children, index); - } else { - result += this.render_token(tokens[index], index); - } - } - - this._options = undefined; - return result; - } - - private render_token(token: RemarkToken, index: number) { - log.debug(LogCategory.GENERAL, tr("Render Markdown token: %o"), token); - const renderer = Renderer.renderers[token.type]; - if(typeof(renderer) === "undefined") { - log.warn(LogCategory.CHAT, tr("Missing markdown to bbcode renderer for token %s: %o"), token.type, token); - return token.content || ""; - } - - const result = renderer(this, token, index); - if(token.type === "paragraph_open") this.last_paragraph = token; - return result; - } - - private render_inline(tokens: RemarkToken[], index: number) { - let result = ''; - - for(let index = 0; index < tokens.length; index++) { + //TODO: Escape BB-Codes + for(let index = 0; index < tokens.length; index++) { + if (tokens[index].type === 'inline') { + result += this.render_inline(tokens[index].children, index); + } else { result += this.render_token(tokens[index], index); } - - return result; } - options() : any { - return this._options; + this._options = undefined; + return result; + } + + private render_token(token: RemarkToken, index: number) { + log.debug(LogCategory.GENERAL, tr("Render Markdown token: %o"), token); + const renderer = Renderer.renderers[token.type]; + if(typeof(renderer) === "undefined") { + log.warn(LogCategory.CHAT, tr("Missing markdown to bbcode renderer for token %s: %o"), token.type, token); + return token.content || ""; } - maybe_escape_bb(text: string) { - if(this._options.escape_bb) - return xbbcode.escape(text); - return text; - } - } - } - - let _renderer: any; - function process_markdown(message: string, options: { - process_url?: boolean, - escape_bb?: boolean - }) : string { - if(typeof(window.remarkable) === "undefined") - return (options.process_url ? process_urls(message) : message); - - if(!_renderer) { - _renderer = new window.remarkable.Remarkable('full'); - _renderer.set({ - typographer: true - }); - _renderer.renderer = new md2bbc.Renderer(); - _renderer.inline.ruler.disable([ 'newline', 'autolink' ]); - } - _renderer.set({ - process_url: !!options.process_url, - escape_bb: !!options.escape_bb - }); - let result: string = _renderer.render(message); - if(result.endsWith("\n")) - result = result.substr(0, result.length - 1); - return result; - } - - export function preprocess_chat_message(message: string) : string { - const process_url = settings.static_global(Settings.KEY_CHAT_TAG_URLS); - const parse_markdown = settings.static_global(Settings.KEY_CHAT_ENABLE_MARKDOWN); - const escape_bb = !settings.static_global(Settings.KEY_CHAT_ENABLE_BBCODE); - - if(parse_markdown) - return process_markdown(message, { - process_url: process_url, - escape_bb: escape_bb - }); - - if(escape_bb) - message = xbbcode.escape(message); - return process_url ? process_urls(message) : message; - } - - export namespace history { - let _local_cache: Cache; - - async function get_cache() { - if(_local_cache) - return _local_cache; - - if(!('caches' in window)) - throw "missing cache extension!"; - - return (_local_cache = await caches.open('chat_history')); + const result = renderer(this, token, index); + if(token.type === "paragraph_open") this.last_paragraph = token; + return result; } - export async function load_history(key: string) : Promise { - const cache = await get_cache(); - const request = new Request("https://_local_cache/cache_request_" + key); - const cached_response = await cache.match(request); - if(!cached_response) - return undefined; + private render_inline(tokens: RemarkToken[], index: number) { + let result = ''; - return await cached_response.json(); - } - - export async function save_history(key: string, value: any) { - const cache = await get_cache(); - const request = new Request("https://_local_cache/cache_request_" + key); - const data = JSON.stringify(value); - - const new_headers = new Headers(); - new_headers.set("Content-type", "application/json"); - new_headers.set("Content-length", data.length.toString()); - - - await cache.put(request, new Response(data, { - headers: new_headers - })); - } - } - - export namespace date { - export function same_day(a: number | Date, b: number | Date) { - a = a instanceof Date ? a : new Date(a); - b = b instanceof Date ? b : new Date(b); - - if(a.getDate() !== b.getDate()) - return false; - if(a.getMonth() !== b.getMonth()) - return false; - return a.getFullYear() === b.getFullYear(); - } - } - } - - export namespace format { - export namespace date { - export enum ColloquialFormat { - YESTERDAY, - TODAY, - GENERAL - } - - export function date_format(date: Date, now: Date, ignore_settings?: boolean) : ColloquialFormat { - if(!ignore_settings && !settings.static_global(Settings.KEY_CHAT_COLLOQUIAL_TIMESTAMPS)) - return ColloquialFormat.GENERAL; - - let delta_day = now.getDate() - date.getDate(); - if(delta_day < 1) /* month change? */ - delta_day = date.getDate() - now.getDate(); - if(delta_day == 0) - return ColloquialFormat.TODAY; - else if(delta_day == 1) - return ColloquialFormat.YESTERDAY; - return ColloquialFormat.GENERAL; - } - - export function format_date_general(date: Date, hours?: boolean) : string { - return ('00' + date.getDate()).substr(-2) + "." - + ('00' + date.getMonth()).substr(-2) + "." - + date.getFullYear() + - (typeof(hours) === "undefined" || hours ? " at " - + ('00' + date.getHours()).substr(-2) + ":" - + ('00' + date.getMinutes()).substr(-2) - : ""); - } - - export function format_date_colloquial(date: Date, current_timestamp: Date) : { result: string; format: ColloquialFormat } { - const format = date_format(date, current_timestamp); - if(format == ColloquialFormat.GENERAL) { - return { - result: format_date_general(date), - format: format - }; - } else { - let hrs = date.getHours(); - let time = "AM"; - if(hrs > 12) { - hrs -= 12; - time = "PM"; - } - return { - result: (format == ColloquialFormat.YESTERDAY ? tr("Yesterday at") : tr("Today at")) + " " + hrs + ":" + date.getMinutes() + " " + time, - format: format - }; - } - } - - export function format_chat_time(date: Date) : { - result: string, - next_update: number /* in MS */ - } { - const timestamp = date.getTime(); - const current_timestamp = new Date(); - - const result = { - result: "", - next_update: 0 - }; - - if(settings.static_global(Settings.KEY_CHAT_FIXED_TIMESTAMPS)) { - const format = format_date_colloquial(date, current_timestamp); - result.result = format.result; - result.next_update = 0; /* TODO: Update on day change? */ - } else { - const delta = current_timestamp.getTime() - timestamp; - if(delta < 2000) { - result.result = "now"; - result.next_update = 2500 - delta; /* update after two seconds */ - } else if(delta < 30000) { /* 30 seconds */ - result.result = Math.floor(delta / 1000) + " " + tr("seconds ago"); - result.next_update = 1000; /* update every second */ - } else if(delta < 30 * 60 * 1000) { /* 30 minutes */ - if(delta < 120 * 1000) - result.result = tr("one minute ago"); - else - result.result = Math.floor(delta / (1000 * 60)) + " " + tr("minutes ago"); - result.next_update = 60000; /* updater after a minute */ - } else { - result.result = format_date_colloquial(date, current_timestamp).result; - result.next_update = 0; /* TODO: Update on day change? */ - } + for(let index = 0; index < tokens.length; index++) { + result += this.render_token(tokens[index], index); } return result; } - } - export namespace time { - export function format_online_time(secs: number) : string { - let years = Math.floor(secs / (60 * 60 * 24 * 365)); - let days = Math.floor(secs / (60 * 60 * 24)) % 365; - let hours = Math.floor(secs / (60 * 60)) % 24; - let minutes = Math.floor(secs / 60) % 60; - let seconds = Math.floor(secs % 60); - let result = ""; - if(years > 0) - result += years + " " + tr("years") + " "; - if(years > 0 || days > 0) - result += days + " " + tr("days") + " "; - if(years > 0 || days > 0 || hours > 0) - result += hours + " " + tr("hours") + " "; - if(years > 0 || days > 0 || hours > 0 || minutes > 0) - result += minutes + " " + tr("minutes") + " "; - if(years > 0 || days > 0 || hours > 0 || minutes > 0 || seconds > 0) - result += seconds + " " + tr("seconds") + " "; - else - result = tr("now") + " "; + options() : any { + return this._options; + } - return result.substr(0, result.length - 1); + maybe_escape_bb(text: string) { + if(this._options.escape_bb) + return xbbcode.escape(text); + return text; } } } + + let _renderer: any; + function process_markdown(message: string, options: { + process_url?: boolean, + escape_bb?: boolean + }) : string { + if(typeof(window.remarkable) === "undefined") + return (options.process_url ? process_urls(message) : message); + + if(!_renderer) { + _renderer = new window.remarkable.Remarkable('full'); + _renderer.set({ + typographer: true + }); + _renderer.renderer = new md2bbc.Renderer(); + _renderer.inline.ruler.disable([ 'newline', 'autolink' ]); + } + _renderer.set({ + process_url: !!options.process_url, + escape_bb: !!options.escape_bb + }); + let result: string = _renderer.render(message); + if(result.endsWith("\n")) + result = result.substr(0, result.length - 1); + return result; + } + + export function preprocess_chat_message(message: string) : string { + const process_url = settings.static_global(Settings.KEY_CHAT_TAG_URLS); + const parse_markdown = settings.static_global(Settings.KEY_CHAT_ENABLE_MARKDOWN); + const escape_bb = !settings.static_global(Settings.KEY_CHAT_ENABLE_BBCODE); + + if(parse_markdown) + return process_markdown(message, { + process_url: process_url, + escape_bb: escape_bb + }); + + if(escape_bb) + message = xbbcode.escape(message); + return process_url ? process_urls(message) : message; + } + + export namespace history { + let _local_cache: Cache; + + async function get_cache() { + if(_local_cache) + return _local_cache; + + if(!('caches' in window)) + throw "missing cache extension!"; + + return (_local_cache = await caches.open('chat_history')); + } + + export async function load_history(key: string) : Promise { + const cache = await get_cache(); + const request = new Request("https://_local_cache/cache_request_" + key); + const cached_response = await cache.match(request); + if(!cached_response) + return undefined; + + return await cached_response.json(); + } + + export async function save_history(key: string, value: any) { + const cache = await get_cache(); + const request = new Request("https://_local_cache/cache_request_" + key); + const data = JSON.stringify(value); + + const new_headers = new Headers(); + new_headers.set("Content-type", "application/json"); + new_headers.set("Content-length", data.length.toString()); + + + await cache.put(request, new Response(data, { + headers: new_headers + })); + } + } + + export namespace date { + export function same_day(a: number | Date, b: number | Date) { + a = a instanceof Date ? a : new Date(a); + b = b instanceof Date ? b : new Date(b); + + if(a.getDate() !== b.getDate()) + return false; + if(a.getMonth() !== b.getMonth()) + return false; + return a.getFullYear() === b.getFullYear(); + } + } +} + +export namespace format { + export namespace date { + export enum ColloquialFormat { + YESTERDAY, + TODAY, + GENERAL + } + + export function date_format(date: Date, now: Date, ignore_settings?: boolean) : ColloquialFormat { + if(!ignore_settings && !settings.static_global(Settings.KEY_CHAT_COLLOQUIAL_TIMESTAMPS)) + return ColloquialFormat.GENERAL; + + let delta_day = now.getDate() - date.getDate(); + if(delta_day < 1) /* month change? */ + delta_day = date.getDate() - now.getDate(); + if(delta_day == 0) + return ColloquialFormat.TODAY; + else if(delta_day == 1) + return ColloquialFormat.YESTERDAY; + return ColloquialFormat.GENERAL; + } + + export function format_date_general(date: Date, hours?: boolean) : string { + return ('00' + date.getDate()).substr(-2) + "." + + ('00' + date.getMonth()).substr(-2) + "." + + date.getFullYear() + + (typeof(hours) === "undefined" || hours ? " at " + + ('00' + date.getHours()).substr(-2) + ":" + + ('00' + date.getMinutes()).substr(-2) + : ""); + } + + export function format_date_colloquial(date: Date, current_timestamp: Date) : { result: string; format: ColloquialFormat } { + const format = date_format(date, current_timestamp); + if(format == ColloquialFormat.GENERAL) { + return { + result: format_date_general(date), + format: format + }; + } else { + let hrs = date.getHours(); + let time = "AM"; + if(hrs > 12) { + hrs -= 12; + time = "PM"; + } + return { + result: (format == ColloquialFormat.YESTERDAY ? tr("Yesterday at") : tr("Today at")) + " " + hrs + ":" + date.getMinutes() + " " + time, + format: format + }; + } + } + + export function format_chat_time(date: Date) : { + result: string, + next_update: number /* in MS */ + } { + const timestamp = date.getTime(); + const current_timestamp = new Date(); + + const result = { + result: "", + next_update: 0 + }; + + if(settings.static_global(Settings.KEY_CHAT_FIXED_TIMESTAMPS)) { + const format = format_date_colloquial(date, current_timestamp); + result.result = format.result; + result.next_update = 0; /* TODO: Update on day change? */ + } else { + const delta = current_timestamp.getTime() - timestamp; + if(delta < 2000) { + result.result = "now"; + result.next_update = 2500 - delta; /* update after two seconds */ + } else if(delta < 30000) { /* 30 seconds */ + result.result = Math.floor(delta / 1000) + " " + tr("seconds ago"); + result.next_update = 1000; /* update every second */ + } else if(delta < 30 * 60 * 1000) { /* 30 minutes */ + if(delta < 120 * 1000) + result.result = tr("one minute ago"); + else + result.result = Math.floor(delta / (1000 * 60)) + " " + tr("minutes ago"); + result.next_update = 60000; /* updater after a minute */ + } else { + result.result = format_date_colloquial(date, current_timestamp).result; + result.next_update = 0; /* TODO: Update on day change? */ + } + } + + return result; + } + } + export namespace time { + export function format_online_time(secs: number) : string { + let years = Math.floor(secs / (60 * 60 * 24 * 365)); + let days = Math.floor(secs / (60 * 60 * 24)) % 365; + let hours = Math.floor(secs / (60 * 60)) % 24; + let minutes = Math.floor(secs / 60) % 60; + let seconds = Math.floor(secs % 60); + + let result = ""; + if(years > 0) + result += years + " " + tr("years") + " "; + if(years > 0 || days > 0) + result += days + " " + tr("days") + " "; + if(years > 0 || days > 0 || hours > 0) + result += hours + " " + tr("hours") + " "; + if(years > 0 || days > 0 || hours > 0 || minutes > 0) + result += minutes + " " + tr("minutes") + " "; + if(years > 0 || days > 0 || hours > 0 || minutes > 0 || seconds > 0) + result += seconds + " " + tr("seconds") + " "; + else + result = tr("now") + " "; + + return result.substr(0, result.length - 1); + } + } } \ No newline at end of file diff --git a/shared/js/ui/frames/side/client_info.ts b/shared/js/ui/frames/side/client_info.ts index 9847b42b..3865ac51 100644 --- a/shared/js/ui/frames/side/client_info.ts +++ b/shared/js/ui/frames/side/client_info.ts @@ -1,273 +1,280 @@ -namespace chat { - declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; - declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; +import {GroupManager} from "tc-shared/permission/GroupManager"; +import {Frame, FrameContent} from "tc-shared/ui/frames/chat_frame"; +import {ClientEntry, LocalClientEntry} from "tc-shared/ui/client"; +import {openClientInfo} from "tc-shared/ui/modal/ModalClientInfo"; +import * as htmltags from "tc-shared/ui/htmltags"; +import * as image_preview from "../image_preview"; +import {format} from "tc-shared/ui/frames/side/chat_helper"; +import * as i18nc from "tc-shared/i18n/country"; - export class ClientInfo { - readonly handle: Frame; - private _html_tag: JQuery; - private _current_client: ClientEntry | undefined; - private _online_time_updater: number; - previous_frame_content: FrameContent; +declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; +declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; - constructor(handle: Frame) { - this.handle = handle; - this._build_html_tag(); - } +export class ClientInfo { + readonly handle: Frame; + private _html_tag: JQuery; + private _current_client: ClientEntry | undefined; + private _online_time_updater: number; + previous_frame_content: FrameContent; - html_tag() : JQuery { - return this._html_tag; - } + constructor(handle: Frame) { + this.handle = handle; + this._build_html_tag(); + } - destroy() { - clearInterval(this._online_time_updater); + html_tag() : JQuery { + return this._html_tag; + } - this._html_tag && this._html_tag.remove(); - this._html_tag = undefined; + destroy() { + clearInterval(this._online_time_updater); - this._current_client = undefined; - this.previous_frame_content = undefined; - } + this._html_tag && this._html_tag.remove(); + this._html_tag = undefined; - private _build_html_tag() { - this._html_tag = $("#tmpl_frame_chat_client_info").renderTag(); - this._html_tag.find(".button-close").on('click', () => { - if(this.previous_frame_content === FrameContent.CLIENT_INFO) - this.previous_frame_content = FrameContent.NONE; + this._current_client = undefined; + this.previous_frame_content = undefined; + } - this.handle.set_content(this.previous_frame_content); - }); - this._html_tag.find(".button-more").on('click', () => { - if(!this._current_client) - return; + private _build_html_tag() { + this._html_tag = $("#tmpl_frame_chat_client_info").renderTag(); + this._html_tag.find(".button-close").on('click', () => { + if(this.previous_frame_content === FrameContent.CLIENT_INFO) + this.previous_frame_content = FrameContent.NONE; - Modals.openClientInfo(this._current_client); - }); - this._html_tag.find('.container-avatar-edit').on('click', () => this.handle.handle.update_avatar()); - } - - current_client() : ClientEntry { - return this._current_client; - } - - set_current_client(client: ClientEntry | undefined, enforce?: boolean) { - if(client) client.updateClientVariables(); /* just to ensure */ - if(client === this._current_client && (typeof(enforce) === "undefined" || !enforce)) + this.handle.set_content(this.previous_frame_content); + }); + this._html_tag.find(".button-more").on('click', () => { + if(!this._current_client) return; - this._current_client = client; + openClientInfo(this._current_client); + }); + this._html_tag.find('.container-avatar-edit').on('click', () => this.handle.handle.update_avatar()); + } - /* updating the header */ - { - const client_name = this._html_tag.find(".client-name"); - client_name.children().remove(); - htmltags.generate_client_object({ - add_braces: false, - client_name: client ? client.clientNickName() : "undefined", - client_unique_id: client ? client.clientUid() : "", - client_id: client ? client.clientId() : 0 - }).appendTo(client_name); + current_client() : ClientEntry { + return this._current_client; + } - const client_description = this._html_tag.find(".client-description"); - client_description.text(client ? client.properties.client_description : "").toggle(!!client.properties.client_description); + set_current_client(client: ClientEntry | undefined, enforce?: boolean) { + if(client) client.updateClientVariables(); /* just to ensure */ + if(client === this._current_client && (typeof(enforce) === "undefined" || !enforce)) + return; - const is_local_entry = client instanceof LocalClientEntry; - const container_avatar = this._html_tag.find(".container-avatar"); - container_avatar.find(".avatar").remove(); - if(client) { - const avatar = this.handle.handle.fileManager.avatars.generate_chat_tag({id: client.clientId()}, client.clientUid()); - if(!is_local_entry) { - avatar.css("cursor", "pointer").on('click', event => { - image_preview.preview_image_tag(this.handle.handle.fileManager.avatars.generate_chat_tag({id: client.clientId()}, client.clientUid())); - }); + this._current_client = client; + + /* updating the header */ + { + const client_name = this._html_tag.find(".client-name"); + client_name.children().remove(); + htmltags.generate_client_object({ + add_braces: false, + client_name: client ? client.clientNickName() : "undefined", + client_unique_id: client ? client.clientUid() : "", + client_id: client ? client.clientId() : 0 + }).appendTo(client_name); + + const client_description = this._html_tag.find(".client-description"); + client_description.text(client ? client.properties.client_description : "").toggle(!!client.properties.client_description); + + const is_local_entry = client instanceof LocalClientEntry; + const container_avatar = this._html_tag.find(".container-avatar"); + container_avatar.find(".avatar").remove(); + if(client) { + const avatar = this.handle.handle.fileManager.avatars.generate_chat_tag({id: client.clientId()}, client.clientUid()); + if(!is_local_entry) { + avatar.css("cursor", "pointer").on('click', event => { + image_preview.preview_image_tag(this.handle.handle.fileManager.avatars.generate_chat_tag({id: client.clientId()}, client.clientUid())); + }); + } + avatar.appendTo(container_avatar); + } else + this.handle.handle.fileManager.avatars.generate_chat_tag(undefined, undefined).appendTo(container_avatar); + + container_avatar.toggleClass("editable", is_local_entry); + } + /* updating the info fields */ + { + const online_time = this._html_tag.find(".client-online-time"); + online_time.text(format.time.format_online_time(client ? client.calculateOnlineTime() : 0)); + if(this._online_time_updater) { + clearInterval(this._online_time_updater); + this._online_time_updater = 0; + } + if(client) { + this._online_time_updater = setInterval(() => { + const client = this._current_client; + if(!client) { + clearInterval(this._online_time_updater); + this._online_time_updater = undefined; + return; } - avatar.appendTo(container_avatar); - } else - this.handle.handle.fileManager.avatars.generate_chat_tag(undefined, undefined).appendTo(container_avatar); - container_avatar.toggleClass("editable", is_local_entry); - } - /* updating the info fields */ - { - const online_time = this._html_tag.find(".client-online-time"); - online_time.text(format.time.format_online_time(client ? client.calculateOnlineTime() : 0)); - if(this._online_time_updater) { - clearInterval(this._online_time_updater); - this._online_time_updater = 0; - } - if(client) { - this._online_time_updater = setInterval(() => { - const client = this._current_client; - if(!client) { - clearInterval(this._online_time_updater); - this._online_time_updater = undefined; - return; - } - - if(client.currentChannel()) /* If he has no channel then he might be disconnected */ - online_time.text(format.time.format_online_time(client.calculateOnlineTime())); - else { - online_time.text(online_time.text() + tr(" (left view)")); - clearInterval(this._online_time_updater); - } - }, 1000); - } - - const country = this._html_tag.find(".client-country"); - country.children().detach(); - const country_code = (client ? client.properties.client_country : undefined) || "xx"; - $.spawn("div").addClass("country flag-" + country_code.toLowerCase()).appendTo(country); - $.spawn("a").text(i18n.country_name(country_code.toUpperCase())).appendTo(country); - - - const version = this._html_tag.find(".client-version"); - version.children().detach(); - if(client) { - let platform = client.properties.client_platform; - if(platform.indexOf("Win32") != 0 && (client.properties.client_version.indexOf("Win64") != -1 || client.properties.client_version.indexOf("WOW64") != -1)) - platform = platform.replace("Win32", "Win64"); - $.spawn("a").attr("title", client.properties.client_version).text( - client.properties.client_version.split(" ")[0] + " on " + platform - ).appendTo(version); - } - - const volume = this._html_tag.find(".client-local-volume"); - volume.text((client && client.get_audio_handle() ? (client.get_audio_handle().get_volume() * 100) : -1).toFixed(0) + "%"); + if(client.currentChannel()) /* If he has no channel then he might be disconnected */ + online_time.text(format.time.format_online_time(client.calculateOnlineTime())); + else { + online_time.text(online_time.text() + tr(" (left view)")); + clearInterval(this._online_time_updater); + } + }, 1000); } - /* teaspeak forum */ - { - const container_forum = this._html_tag.find(".container-teaforo"); - if(client && client.properties.client_teaforo_id) { - container_forum.show(); + const country = this._html_tag.find(".client-country"); + country.children().detach(); + const country_code = (client ? client.properties.client_country : undefined) || "xx"; + $.spawn("div").addClass("country flag-" + country_code.toLowerCase()).appendTo(country); + $.spawn("a").text(i18nc.country_name(country_code.toUpperCase())).appendTo(country); - const container_data = container_forum.find(".client-teaforo-account"); - container_data.children().remove(); - let text = client.properties.client_teaforo_name; - if((client.properties.client_teaforo_flags & 0x01) > 0) - text += " (" + tr("Banned") + ")"; - if((client.properties.client_teaforo_flags & 0x02) > 0) - text += " (" + tr("Stuff") + ")"; - if((client.properties.client_teaforo_flags & 0x04) > 0) - text += " (" + tr("Premium") + ")"; + const version = this._html_tag.find(".client-version"); + version.children().detach(); + if(client) { + let platform = client.properties.client_platform; + if(platform.indexOf("Win32") != 0 && (client.properties.client_version.indexOf("Win64") != -1 || client.properties.client_version.indexOf("WOW64") != -1)) + platform = platform.replace("Win32", "Win64"); + $.spawn("a").attr("title", client.properties.client_version).text( + client.properties.client_version.split(" ")[0] + " on " + platform + ).appendTo(version); + } - $.spawn("a") - .attr("href", "https://forum.teaspeak.de/index.php?members/" + client.properties.client_teaforo_id) - .attr("target", "_blank") - .text(text) - .appendTo(container_data); + const volume = this._html_tag.find(".client-local-volume"); + volume.text((client && client.get_audio_handle() ? (client.get_audio_handle().get_volume() * 100) : -1).toFixed(0) + "%"); + } + + /* teaspeak forum */ + { + const container_forum = this._html_tag.find(".container-teaforo"); + if(client && client.properties.client_teaforo_id) { + container_forum.show(); + + const container_data = container_forum.find(".client-teaforo-account"); + container_data.children().remove(); + + let text = client.properties.client_teaforo_name; + if((client.properties.client_teaforo_flags & 0x01) > 0) + text += " (" + tr("Banned") + ")"; + if((client.properties.client_teaforo_flags & 0x02) > 0) + text += " (" + tr("Stuff") + ")"; + if((client.properties.client_teaforo_flags & 0x04) > 0) + text += " (" + tr("Premium") + ")"; + + $.spawn("a") + .attr("href", "https://forum.teaspeak.de/index.php?members/" + client.properties.client_teaforo_id) + .attr("target", "_blank") + .text(text) + .appendTo(container_data); + } else { + container_forum.hide(); + } + } + + /* update the client status */ + { + //TODO Implement client status! + const container_status = this._html_tag.find(".container-client-status"); + const container_status_entries = container_status.find(".client-status"); + container_status_entries.children().detach(); + if(client) { + if(client.properties.client_away) { + container_status_entries.append( + $.spawn("div").addClass("status-entry").append( + $.spawn("div").addClass("icon_em client-away"), + $.spawn("a").text(tr("Away")), + client.properties.client_away_message ? + $.spawn("a").addClass("away-message").text("(" + client.properties.client_away_message + ")") : + undefined + ) + ) + } + if(client.is_muted()) { + container_status_entries.append( + $.spawn("div").addClass("status-entry").append( + $.spawn("div").addClass("icon_em client-input_muted_local"), + $.spawn("a").text(tr("Client local muted")) + ) + ) + } + if(!client.properties.client_output_hardware) { + container_status_entries.append( + $.spawn("div").addClass("status-entry").append( + $.spawn("div").addClass("icon_em client-hardware_output_muted"), + $.spawn("a").text(tr("Speakers/Headphones disabled")) + ) + ) + } + if(!client.properties.client_input_hardware) { + container_status_entries.append( + $.spawn("div").addClass("status-entry").append( + $.spawn("div").addClass("icon_em client-hardware_input_muted"), + $.spawn("a").text(tr("Microphone disabled")) + ) + ) + } + if(client.properties.client_output_muted) { + container_status_entries.append( + $.spawn("div").addClass("status-entry").append( + $.spawn("div").addClass("icon_em client-output_muted"), + $.spawn("a").text(tr("Speakers/Headphones Muted")) + ) + ) + } + if(client.properties.client_input_muted) { + container_status_entries.append( + $.spawn("div").addClass("status-entry").append( + $.spawn("div").addClass("icon_em client-input_muted"), + $.spawn("a").text(tr("Microphone Muted")) + ) + ) + } + } + container_status.toggle(container_status_entries.children().length > 0); + } + /* update client server groups */ + { + const container_groups = this._html_tag.find(".client-group-server"); + container_groups.children().detach(); + if(client) { + const invalid_groups = []; + const groups = client.assignedServerGroupIds().map(group_id => { + const result = this.handle.handle.groups.serverGroup(group_id); + if(!result) + invalid_groups.push(group_id); + return result; + }).filter(e => !!e).sort(GroupManager.sorter()); + for(const invalid_id of invalid_groups) { + container_groups.append($.spawn("a").text("{" + tr("server group ") + invalid_groups + "}").attr("title", tr("Missing server group id!") + " (" + invalid_groups + ")")); + } + for(let group of groups) { + container_groups.append( + $.spawn("div").addClass("group-container") + .append( + this.handle.handle.fileManager.icons.generateTag(group.properties.iconid) + ).append( + $.spawn("a").text(group.name).attr("title", tr("Group id: ") + group.id) + ) + ); + } + } + } + /* update client channel group */ + { + const container_group = this._html_tag.find(".client-group-channel"); + container_group.children().detach(); + if(client) { + const group_id = client.assignedChannelGroup(); + let group = this.handle.handle.groups.channelGroup(group_id); + if(group) { + container_group.append( + $.spawn("div").addClass("group-container") + .append( + this.handle.handle.fileManager.icons.generateTag(group.properties.iconid) + ).append( + $.spawn("a").text(group.name).attr("title", tr("Group id: ") + group_id) + ) + ); } else { - container_forum.hide(); - } - } - - /* update the client status */ - { - //TODO Implement client status! - const container_status = this._html_tag.find(".container-client-status"); - const container_status_entries = container_status.find(".client-status"); - container_status_entries.children().detach(); - if(client) { - if(client.properties.client_away) { - container_status_entries.append( - $.spawn("div").addClass("status-entry").append( - $.spawn("div").addClass("icon_em client-away"), - $.spawn("a").text(tr("Away")), - client.properties.client_away_message ? - $.spawn("a").addClass("away-message").text("(" + client.properties.client_away_message + ")") : - undefined - ) - ) - } - if(client.is_muted()) { - container_status_entries.append( - $.spawn("div").addClass("status-entry").append( - $.spawn("div").addClass("icon_em client-input_muted_local"), - $.spawn("a").text(tr("Client local muted")) - ) - ) - } - if(!client.properties.client_output_hardware) { - container_status_entries.append( - $.spawn("div").addClass("status-entry").append( - $.spawn("div").addClass("icon_em client-hardware_output_muted"), - $.spawn("a").text(tr("Speakers/Headphones disabled")) - ) - ) - } - if(!client.properties.client_input_hardware) { - container_status_entries.append( - $.spawn("div").addClass("status-entry").append( - $.spawn("div").addClass("icon_em client-hardware_input_muted"), - $.spawn("a").text(tr("Microphone disabled")) - ) - ) - } - if(client.properties.client_output_muted) { - container_status_entries.append( - $.spawn("div").addClass("status-entry").append( - $.spawn("div").addClass("icon_em client-output_muted"), - $.spawn("a").text(tr("Speakers/Headphones Muted")) - ) - ) - } - if(client.properties.client_input_muted) { - container_status_entries.append( - $.spawn("div").addClass("status-entry").append( - $.spawn("div").addClass("icon_em client-input_muted"), - $.spawn("a").text(tr("Microphone Muted")) - ) - ) - } - } - container_status.toggle(container_status_entries.children().length > 0); - } - /* update client server groups */ - { - const container_groups = this._html_tag.find(".client-group-server"); - container_groups.children().detach(); - if(client) { - const invalid_groups = []; - const groups = client.assignedServerGroupIds().map(group_id => { - const result = this.handle.handle.groups.serverGroup(group_id); - if(!result) - invalid_groups.push(group_id); - return result; - }).filter(e => !!e).sort(GroupManager.sorter()); - for(const invalid_id of invalid_groups) { - container_groups.append($.spawn("a").text("{" + tr("server group ") + invalid_groups + "}").attr("title", tr("Missing server group id!") + " (" + invalid_groups + ")")); - } - for(let group of groups) { - container_groups.append( - $.spawn("div").addClass("group-container") - .append( - this.handle.handle.fileManager.icons.generateTag(group.properties.iconid) - ).append( - $.spawn("a").text(group.name).attr("title", tr("Group id: ") + group.id) - ) - ); - } - } - } - /* update client channel group */ - { - const container_group = this._html_tag.find(".client-group-channel"); - container_group.children().detach(); - if(client) { - const group_id = client.assignedChannelGroup(); - let group = this.handle.handle.groups.channelGroup(group_id); - if(group) { - container_group.append( - $.spawn("div").addClass("group-container") - .append( - this.handle.handle.fileManager.icons.generateTag(group.properties.iconid) - ).append( - $.spawn("a").text(group.name).attr("title", tr("Group id: ") + group_id) - ) - ); - } else { - container_group.append($.spawn("a").text(tr("Invalid channel group!")).attr("title", tr("Missing channel group id!") + " (" + group_id + ")")); - } + container_group.append($.spawn("a").text(tr("Invalid channel group!")).attr("title", tr("Missing channel group id!") + " (" + group_id + ")")); } } } diff --git a/shared/js/ui/frames/side/conversations.ts b/shared/js/ui/frames/side/conversations.ts index 3c45863a..18ec6a0c 100644 --- a/shared/js/ui/frames/side/conversations.ts +++ b/shared/js/ui/frames/side/conversations.ts @@ -1,615 +1,623 @@ -namespace chat { - declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; - declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; +import {settings, Settings} from "tc-shared/settings"; +import {format} from "tc-shared/ui/frames/side/chat_helper"; +import {bbcode_chat, formatMessage} from "tc-shared/ui/frames/chat"; +import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; +import {LogCategory} from "tc-shared/log"; +import PermissionType from "tc-shared/permission/PermissionType"; +import {ChatBox} from "tc-shared/ui/frames/side/chat_box"; +import {Frame, FrameContent} from "tc-shared/ui/frames/chat_frame"; +import {createErrorModal} from "tc-shared/ui/elements/Modal"; +import * as log from "tc-shared/log"; +import * as htmltags from "tc-shared/ui/htmltags"; - export namespace channel { - export type ViewEntry = { - html_element: JQuery; - update_timer?: number; +declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; +declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; + +export type ViewEntry = { + html_element: JQuery; + update_timer?: number; +} +export type MessageData = { + timestamp: number; + + message: string; + + sender_name: string; + sender_unique_id: string; + sender_database_id: number; +} +export type Message = MessageData & ViewEntry; + +export class Conversation { + readonly handle: ConversationManager; + readonly channel_id: number; + + private _flag_private: boolean; + + private _html_tag: JQuery; + private _container_messages: JQuery; + private _container_new_message: JQuery; + + private _container_no_permissions: JQuery; + private _container_no_permissions_shown: boolean = false; + + private _container_is_private: JQuery; + private _container_is_private_shown: boolean = false; + + private _container_no_support: JQuery; + private _container_no_support_shown: boolean = false; + + private _view_max_messages = 40; /* reset to 40 again as soon we tab out :) */ + private _view_older_messages: ViewEntry; + private _has_older_messages: boolean; /* undefined := not known | else flag */ + + private _view_entries: ViewEntry[] = []; + + private _last_messages: MessageData[] = []; + private _last_messages_timestamp: number = 0; + private _first_unread_message: Message; + private _first_unread_message_pointer: ViewEntry; + + private _scroll_position: number | undefined; /* undefined to follow bottom | position for special stuff */ + + constructor(handle: ConversationManager, channel_id: number) { + this.handle = handle; + this.channel_id = channel_id; + + this._build_html_tag(); + } + + html_tag() : JQuery { return this._html_tag; } + destroy() { + this._first_unread_message_pointer.html_element.detach(); + this._first_unread_message_pointer = undefined; + + this._view_older_messages.html_element.detach(); + this._view_older_messages = undefined; + + for(const view_entry of this._view_entries) { + view_entry.html_element.detach(); + clearTimeout(view_entry.update_timer); } - export type MessageData = { - timestamp: number; + this._view_entries = []; + } - message: string; + private _build_html_tag() { + this._html_tag = $("#tmpl_frame_chat_channel_messages").renderTag(); - sender_name: string; - sender_unique_id: string; - sender_database_id: number; + this._container_new_message = this._html_tag.find(".new-message"); + this._container_no_permissions = this._html_tag.find(".no-permissions").hide(); + this._container_is_private = this._html_tag.find(".private-conversation").hide(); + this._container_no_support = this._html_tag.find(".not-supported").hide(); + + this._container_messages = this._html_tag.find(".container-messages"); + this._container_messages.on('scroll', event => { + const exact_position = this._container_messages[0].scrollTop + this._container_messages[0].clientHeight; + const current_view = exact_position + this._container_messages[0].clientHeight * .125; + if(current_view > this._container_messages[0].scrollHeight) { + this._scroll_position = undefined; + } else { + this._scroll_position = this._container_messages[0].scrollTop; + } + + const will_visible = !!this._first_unread_message && this._first_unread_message_pointer.html_element[0].offsetTop > exact_position; + const is_visible = this._container_new_message[0].classList.contains("shown"); + if(!is_visible && will_visible) + this._container_new_message[0].classList.add("shown"); + + if(is_visible && !will_visible) + this._container_new_message[0].classList.remove("shown"); + + //This causes a Layout recalc (Forced reflow) + //this._container_new_message.toggleClass("shown",!!this._first_unread_message && this._first_unread_message_pointer.html_element[0].offsetTop > exact_position); + }); + + this._view_older_messages = this._generate_view_spacer(tr("Load older messages"), "old"); + this._first_unread_message_pointer = this._generate_view_spacer(tr("Unread messages"), "new"); + this._view_older_messages.html_element.appendTo(this._container_messages).on('click', event => { + this.fetch_older_messages(); + }); + + this._container_new_message.on('click', event => { + if(!this._first_unread_message) + return; + this._scroll_position = this._first_unread_message_pointer.html_element[0].offsetTop; + this.fix_scroll(true); + }); + this._container_messages.on('click', event => { + if(this._container_new_message.hasClass('shown')) + return; /* we have clicked, but no chance to see the unread message pointer */ + this._mark_read(); + }); + this.set_flag_private(false); + } + + is_unread() { return !!this._first_unread_message; } + + mark_read() { this._mark_read(); } + private _mark_read() { + if(this._first_unread_message) { + this._first_unread_message = undefined; + + const ctree = this.handle.handle.handle.channelTree; + if(ctree && ctree.tag_tree()) + ctree.tag_tree().find(".marker-text-unread[conversation='" + this.channel_id + "']").addClass("hidden"); } - export type Message = MessageData & ViewEntry; + this._first_unread_message_pointer.html_element.detach(); + } - export class Conversation { - readonly handle: ConversationManager; - readonly channel_id: number; + private _generate_view_message(data: MessageData) : Message { + const response = data as Message; + if(response.html_element) + return response; - private _flag_private: boolean; + const timestamp = new Date(data.timestamp); + let time = format.date.format_chat_time(timestamp); + response.html_element = $("#tmpl_frame_chat_channel_message").renderTag({ + timestamp: time.result, + client_name: htmltags.generate_client_object({ + add_braces: false, + client_name: data.sender_name, + client_unique_id: data.sender_unique_id, + client_id: 0 + }), + message: bbcode_chat(data.message), + avatar: this.handle.handle.handle.fileManager.avatars.generate_chat_tag({database_id: data.sender_database_id}, data.sender_unique_id) + }); - private _html_tag: JQuery; - private _container_messages: JQuery; - private _container_new_message: JQuery; + response.html_element.find(".button-delete").on('click', () => this.delete_message(data)); - private _container_no_permissions: JQuery; - private _container_no_permissions_shown: boolean = false; - - private _container_is_private: JQuery; - private _container_is_private_shown: boolean = false; - - private _container_no_support: JQuery; - private _container_no_support_shown: boolean = false; - - private _view_max_messages = 40; /* reset to 40 again as soon we tab out :) */ - private _view_older_messages: ViewEntry; - private _has_older_messages: boolean; /* undefined := not known | else flag */ - - private _view_entries: ViewEntry[] = []; - - private _last_messages: MessageData[] = []; - private _last_messages_timestamp: number = 0; - private _first_unread_message: Message; - private _first_unread_message_pointer: ViewEntry; - - private _scroll_position: number | undefined; /* undefined to follow bottom | position for special stuff */ - - constructor(handle: ConversationManager, channel_id: number) { - this.handle = handle; - this.channel_id = channel_id; - - this._build_html_tag(); - } - - html_tag() : JQuery { return this._html_tag; } - destroy() { - this._first_unread_message_pointer.html_element.detach(); - this._first_unread_message_pointer = undefined; - - this._view_older_messages.html_element.detach(); - this._view_older_messages = undefined; - - for(const view_entry of this._view_entries) { - view_entry.html_element.detach(); - clearTimeout(view_entry.update_timer); - } - this._view_entries = []; - } - - private _build_html_tag() { - this._html_tag = $("#tmpl_frame_chat_channel_messages").renderTag(); - - this._container_new_message = this._html_tag.find(".new-message"); - this._container_no_permissions = this._html_tag.find(".no-permissions").hide(); - this._container_is_private = this._html_tag.find(".private-conversation").hide(); - this._container_no_support = this._html_tag.find(".not-supported").hide(); - - this._container_messages = this._html_tag.find(".container-messages"); - this._container_messages.on('scroll', event => { - const exact_position = this._container_messages[0].scrollTop + this._container_messages[0].clientHeight; - const current_view = exact_position + this._container_messages[0].clientHeight * .125; - if(current_view > this._container_messages[0].scrollHeight) { - this._scroll_position = undefined; - } else { - this._scroll_position = this._container_messages[0].scrollTop; - } - - const will_visible = !!this._first_unread_message && this._first_unread_message_pointer.html_element[0].offsetTop > exact_position; - const is_visible = this._container_new_message[0].classList.contains("shown"); - if(!is_visible && will_visible) - this._container_new_message[0].classList.add("shown"); - - if(is_visible && !will_visible) - this._container_new_message[0].classList.remove("shown"); - - //This causes a Layout recalc (Forced reflow) - //this._container_new_message.toggleClass("shown",!!this._first_unread_message && this._first_unread_message_pointer.html_element[0].offsetTop > exact_position); - }); - - this._view_older_messages = this._generate_view_spacer(tr("Load older messages"), "old"); - this._first_unread_message_pointer = this._generate_view_spacer(tr("Unread messages"), "new"); - this._view_older_messages.html_element.appendTo(this._container_messages).on('click', event => { - this.fetch_older_messages(); - }); - - this._container_new_message.on('click', event => { - if(!this._first_unread_message) - return; - this._scroll_position = this._first_unread_message_pointer.html_element[0].offsetTop; - this.fix_scroll(true); - }); - this._container_messages.on('click', event => { - if(this._container_new_message.hasClass('shown')) - return; /* we have clicked, but no chance to see the unread message pointer */ - this._mark_read(); - }); - this.set_flag_private(false); - } - - is_unread() { return !!this._first_unread_message; } - - mark_read() { this._mark_read(); } - private _mark_read() { - if(this._first_unread_message) { - this._first_unread_message = undefined; - - const ctree = this.handle.handle.handle.channelTree; - if(ctree && ctree.tag_tree()) - ctree.tag_tree().find(".marker-text-unread[conversation='" + this.channel_id + "']").addClass("hidden"); - } - this._first_unread_message_pointer.html_element.detach(); - } - - private _generate_view_message(data: MessageData) : Message { - const response = data as Message; - if(response.html_element) - return response; - - const timestamp = new Date(data.timestamp); - let time = format.date.format_chat_time(timestamp); - response.html_element = $("#tmpl_frame_chat_channel_message").renderTag({ - timestamp: time.result, - client_name: htmltags.generate_client_object({ - add_braces: false, - client_name: data.sender_name, - client_unique_id: data.sender_unique_id, - client_id: 0 - }), - message: MessageHelper.bbcode_chat(data.message), - avatar: this.handle.handle.handle.fileManager.avatars.generate_chat_tag({database_id: data.sender_database_id}, data.sender_unique_id) - }); - - response.html_element.find(".button-delete").on('click', () => this.delete_message(data)); - - if(time.next_update > 0) { - const _updater = () => { - time = format.date.format_chat_time(timestamp); - response.html_element.find(".info .timestamp").text(time.result); - if(time.next_update > 0) - response.update_timer = setTimeout(_updater, time.next_update); - else - response.update_timer = 0; - }; + if(time.next_update > 0) { + const _updater = () => { + time = format.date.format_chat_time(timestamp); + response.html_element.find(".info .timestamp").text(time.result); + if(time.next_update > 0) response.update_timer = setTimeout(_updater, time.next_update); - } else { + else response.update_timer = 0; - } + }; + response.update_timer = setTimeout(_updater, time.next_update); + } else { + response.update_timer = 0; + } - return response; - } + return response; + } - private _generate_view_spacer(message: string, type: "date" | "new" | "old" | "error") : ViewEntry { - const tag = $("#tmpl_frame_chat_private_spacer").renderTag({ - message: message - }).addClass("type-" + type); - return { - html_element: tag, - update_timer: 0 - } - } + private _generate_view_spacer(message: string, type: "date" | "new" | "old" | "error") : ViewEntry { + const tag = $("#tmpl_frame_chat_private_spacer").renderTag({ + message: message + }).addClass("type-" + type); + return { + html_element: tag, + update_timer: 0 + } + } - last_messages_timestamp() : number { - return this._last_messages_timestamp; - } + last_messages_timestamp() : number { + return this._last_messages_timestamp; + } - fetch_last_messages() { - const fetch_count = this._view_max_messages - this._last_messages.length; - const fetch_timestamp_end = this._last_messages_timestamp + 1; /* we want newer messages then the last message we have */ + fetch_last_messages() { + const fetch_count = this._view_max_messages - this._last_messages.length; + const fetch_timestamp_end = this._last_messages_timestamp + 1; /* we want newer messages then the last message we have */ - //conversationhistory cid=1 [cpw=xxx] [timestamp_begin] [timestamp_end (0 := no end)] [message_count (default 25| max 100)] [-merge] - this.handle.handle.handle.serverConnection.send_command("conversationhistory", { - cid: this.channel_id, - timestamp_end: fetch_timestamp_end, - message_count: fetch_count - }, {flagset: ["merge"], process_result: false }).catch(error => { - this._view_older_messages.html_element.toggleClass('shown', false); - if(error instanceof CommandResult) { - if(error.id == ErrorID.CONVERSATION_MORE_DATA) { - if(typeof(this._has_older_messages) === "undefined") - this._has_older_messages = true; - this._view_older_messages.html_element.toggleClass('shown', true); - return; - } else if(error.id == ErrorID.PERMISSION_ERROR) { - this._container_no_permissions.show(); - this._container_no_permissions_shown = true; - } else if(error.id == ErrorID.CONVERSATION_IS_PRIVATE) { - this.set_flag_private(true); - } - /* - else if(error.id == ErrorID.NOT_IMPLEMENTED || error.id == ErrorID.COMMAND_NOT_FOUND) { - this._container_no_support.show(); - this._container_no_support_shown = true; - } - */ - } - //TODO log and handle! - log.error(LogCategory.CHAT, tr("Failed to fetch conversation history. %o"), error); - }).then(() => { - this.fix_scroll(true); - this.handle.update_chat_box(); - }); - } - - fetch_older_messages() { - this._view_older_messages.html_element.toggleClass('shown', false); - - const entry = this._view_entries.slice().reverse().find(e => 'timestamp' in e) as any as {timestamp: number}; - //conversationhistory cid=1 [cpw=xxx] [timestamp_begin] [timestamp_end (0 := no end)] [message_count (default 25| max 100)] [-merge] - this.handle.handle.handle.serverConnection.send_command("conversationhistory", { - cid: this.channel_id, - timestamp_begin: entry.timestamp - 1, - message_count: this._view_max_messages - }, {flagset: ["merge"]}).catch(error => { - this._view_older_messages.html_element.toggleClass('shown', false); - if(error instanceof CommandResult) { - if(error.id == ErrorID.CONVERSATION_MORE_DATA) { - this._view_older_messages.html_element.toggleClass('shown', true); - this.handle.update_chat_box(); - return; - } - } - //TODO log and handle! - log.error(LogCategory.CHAT, tr("Failed to fetch conversation history. %o"), error); - }).then(() => { - this.fix_scroll(true); - }); - } - - register_new_message(message: MessageData, update_view?: boolean) { - /* lets insert the message at the right index */ - let _new_message = false; - { - let spliced = false; - for(let index = 0; index < this._last_messages.length; index++) { - if(this._last_messages[index].timestamp < message.timestamp) { - this._last_messages.splice(index, 0, message); - spliced = true; - _new_message = index == 0; /* only set flag if this has been inserted at the front */ - break; - } else if(this._last_messages[index].timestamp == message.timestamp && this._last_messages[index].sender_database_id == message.sender_database_id) { - return; /* we already have that message */ - } - } - if(!spliced && this._last_messages.length < this._view_max_messages) { - this._last_messages.push(message); - } - this._last_messages_timestamp = this._last_messages[0].timestamp; - - while(this._last_messages.length > this._view_max_messages) { - if(this._last_messages[this._last_messages.length - 1] == this._first_unread_message) - break; - this._last_messages.pop(); - } - } - - /* message is within view */ - { - const entry = this._generate_view_message(message); - - let previous: ViewEntry; - for(let index = 0; index < this._view_entries.length; index++) { - const current_entry = this._view_entries[index]; - if(!('timestamp' in current_entry)) - continue; - - if((current_entry as Message).timestamp < message.timestamp) { - this._view_entries.splice(index, 0, entry); - previous = current_entry; - break; - } - } - if(!previous) - this._view_entries.push(entry); - - if(previous) - entry.html_element.insertAfter(previous.html_element); - else - entry.html_element.insertAfter(this._view_older_messages.html_element); /* last element is already the current element */ - - if(_new_message && (typeof(this._scroll_position) === "number" || this.handle.current_channel() !== this.channel_id || this.handle.handle.content_type() !== FrameContent.CHANNEL_CHAT)) { - if(typeof(this._first_unread_message) === "undefined") - this._first_unread_message = entry; - - this._first_unread_message_pointer.html_element.insertBefore(entry.html_element); - this._container_messages.trigger('scroll'); /* updates the new message stuff */ - } - if(typeof(update_view) !== "boolean" || update_view) - this.fix_scroll(true); - } - - /* update chat state */ - this._container_no_permissions.hide(); - this._container_no_permissions_shown = false; - if(update_view) this.handle.update_chat_box(); - } - - /* using a timeout here to not cause a force style recalculation */ - private _scroll_fix_timer: number; - private _scroll_animate: boolean; - - fix_scroll(animate: boolean) { - if(this._scroll_fix_timer) { - this._scroll_animate = this._scroll_animate && animate; - return; - } - - this._scroll_fix_timer = setTimeout(() => { - this._scroll_fix_timer = undefined; - - let offset; - if(this._first_unread_message) { - offset = this._first_unread_message.html_element[0].offsetTop; - } else if(typeof(this._scroll_position) !== "undefined") { - offset = this._scroll_position; - } else { - offset = this._container_messages[0].scrollHeight; - } - - if(this._scroll_animate) { - this._container_messages.stop(true).animate({ - scrollTop: offset - }, 'slow'); - } else { - this._container_messages.stop(true).scrollTop(offset); - } - }, 5); - } - - fix_view_size() { - this._view_older_messages.html_element.toggleClass('shown', !!this._has_older_messages); - - let count = 0; - for(let index = 0; index < this._view_entries.length; index++) { - if('timestamp' in this._view_entries[index]) - count++; - - if(count > this._view_max_messages) { - this._view_entries.splice(index, this._view_entries.length - index).forEach(e => { - clearTimeout(e.update_timer); - e.html_element.remove(); - }); + //conversationhistory cid=1 [cpw=xxx] [timestamp_begin] [timestamp_end (0 := no end)] [message_count (default 25| max 100)] [-merge] + this.handle.handle.handle.serverConnection.send_command("conversationhistory", { + cid: this.channel_id, + timestamp_end: fetch_timestamp_end, + message_count: fetch_count + }, {flagset: ["merge"], process_result: false }).catch(error => { + this._view_older_messages.html_element.toggleClass('shown', false); + if(error instanceof CommandResult) { + if(error.id == ErrorID.CONVERSATION_MORE_DATA) { + if(typeof(this._has_older_messages) === "undefined") this._has_older_messages = true; - this._view_older_messages.html_element.toggleClass('shown', true); - break; - } - } - } - - chat_available() : boolean { - return !this._container_no_permissions_shown && !this._container_is_private_shown && !this._container_no_support_shown; - } - - text_send_failed(error: CommandResult | any) { - log.warn(LogCategory.CHAT, "Failed to send text message! (%o)", error); - //TODO: Log if message send failed? - if(error instanceof CommandResult) { - if(error.id == ErrorID.PERMISSION_ERROR) { - //TODO: Split up between channel_text_message_send permission and no view permission - if(error.json["failed_permid"] == 0) { - this._container_no_permissions_shown = true; - this._container_no_permissions.show(); - this.handle.update_chat_box(); - } - } - } - } - - set_flag_private(flag: boolean) { - if(this._flag_private === flag) + this._view_older_messages.html_element.toggleClass('shown', true); return; - - this._flag_private = flag; - this.update_private_state(); - if(!flag) - this.fetch_last_messages(); + } else if(error.id == ErrorID.PERMISSION_ERROR) { + this._container_no_permissions.show(); + this._container_no_permissions_shown = true; + } else if(error.id == ErrorID.CONVERSATION_IS_PRIVATE) { + this.set_flag_private(true); + } + /* + else if(error.id == ErrorID.NOT_IMPLEMENTED || error.id == ErrorID.COMMAND_NOT_FOUND) { + this._container_no_support.show(); + this._container_no_support_shown = true; + } + */ } + //TODO log and handle! + log.error(LogCategory.CHAT, tr("Failed to fetch conversation history. %o"), error); + }).then(() => { + this.fix_scroll(true); + this.handle.update_chat_box(); + }); + } - update_private_state() { - if(!this._flag_private) { - this._container_is_private.hide(); - this._container_is_private_shown = false; - } else { - const client = this.handle.handle.handle.getClient(); - if(client && client.currentChannel() && client.currentChannel().channelId === this.channel_id) { - this._container_is_private_shown = false; - this._container_is_private.hide(); - } else { - this._container_is_private.show(); - this._container_is_private_shown = true; - } + fetch_older_messages() { + this._view_older_messages.html_element.toggleClass('shown', false); + + const entry = this._view_entries.slice().reverse().find(e => 'timestamp' in e) as any as {timestamp: number}; + //conversationhistory cid=1 [cpw=xxx] [timestamp_begin] [timestamp_end (0 := no end)] [message_count (default 25| max 100)] [-merge] + this.handle.handle.handle.serverConnection.send_command("conversationhistory", { + cid: this.channel_id, + timestamp_begin: entry.timestamp - 1, + message_count: this._view_max_messages + }, {flagset: ["merge"]}).catch(error => { + this._view_older_messages.html_element.toggleClass('shown', false); + if(error instanceof CommandResult) { + if(error.id == ErrorID.CONVERSATION_MORE_DATA) { + this._view_older_messages.html_element.toggleClass('shown', true); + this.handle.update_chat_box(); + return; } } + //TODO log and handle! + log.error(LogCategory.CHAT, tr("Failed to fetch conversation history. %o"), error); + }).then(() => { + this.fix_scroll(true); + }); + } - delete_message(message: MessageData) { - //TODO A lot of checks! - //conversationmessagedelete cid=2 timestamp_begin= timestamp_end= cldbid= limit=1 - this.handle.handle.handle.serverConnection.send_command('conversationmessagedelete', { - cid: this.channel_id, - cldbid: message.sender_database_id, - - timestamp_begin: message.timestamp - 1, - timestamp_end: message.timestamp + 1, - - limit: 1 - }).then(() => { - return; /* in general it gets deleted via notify */ - }).catch(error => { - log.error(LogCategory.CHAT, tr("Failed to delete conversation message for conversation %o: %o"), this.channel_id, error); - if(error instanceof CommandResult) - error = error.extra_message || error.message; - createErrorModal(tr("Failed to delete message"), MessageHelper.formatMessage(tr("Failed to delete conversation message{:br:}Error: {}"), error)).open(); - }); - log.debug(LogCategory.CLIENT, tr("Deleting text message %o"), message); - } - - delete_messages(begin: number, end: number, sender: number, limit: number) { - let count = 0; - for(const message of this._view_entries.slice()) { - if(!('sender_database_id' in message)) - continue; - - const cmsg = message as Message; - if(end != 0 && cmsg.timestamp > end) - continue; - if(begin != 0 && cmsg.timestamp < begin) - break; - - if(cmsg.sender_database_id !== sender) - continue; - - this._delete_message(message); - if(--count >= limit) - return; + register_new_message(message: MessageData, update_view?: boolean) { + /* lets insert the message at the right index */ + let _new_message = false; + { + let spliced = false; + for(let index = 0; index < this._last_messages.length; index++) { + if(this._last_messages[index].timestamp < message.timestamp) { + this._last_messages.splice(index, 0, message); + spliced = true; + _new_message = index == 0; /* only set flag if this has been inserted at the front */ + break; + } else if(this._last_messages[index].timestamp == message.timestamp && this._last_messages[index].sender_database_id == message.sender_database_id) { + return; /* we already have that message */ } - - //TODO remove in cache? (_last_messages) } + if(!spliced && this._last_messages.length < this._view_max_messages) { + this._last_messages.push(message); + } + this._last_messages_timestamp = this._last_messages[0].timestamp; - private _delete_message(message: Message) { - if('html_element' in message) { - const cmessage = message as Message; - cmessage.html_element.remove(); - clearTimeout(cmessage.update_timer); - this._view_entries.remove(message as any); - } - - this._last_messages.remove(message); + while(this._last_messages.length > this._view_max_messages) { + if(this._last_messages[this._last_messages.length - 1] == this._first_unread_message) + break; + this._last_messages.pop(); } } - export class ConversationManager { - readonly handle: Frame; + /* message is within view */ + { + const entry = this._generate_view_message(message); - private _html_tag: JQuery; - private _chat_box: ChatBox; + let previous: ViewEntry; + for(let index = 0; index < this._view_entries.length; index++) { + const current_entry = this._view_entries[index]; + if(!('timestamp' in current_entry)) + continue; - private _container_conversation: JQuery; - - private _conversations: Conversation[] = []; - private _current_conversation: Conversation | undefined; - - private _needed_listener = () => this.update_chat_box(); - - constructor(handle: Frame) { - this.handle = handle; - - this._chat_box = new ChatBox(); - this._build_html_tag(); - - this._chat_box.callback_text = text => { - if(!this._current_conversation) - return; - - const conv = this._current_conversation; - this.handle.handle.serverConnection.send_command("sendtextmessage", {targetmode: conv.channel_id == 0 ? 3 : 2, cid: conv.channel_id, msg: text}, {process_result: false}).catch(error => { - conv.text_send_failed(error); - }); - }; - this.update_chat_box(); - } - - initialize_needed_listener() { - this.handle.handle.permissions.register_needed_permission(PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND, this._needed_listener); - this.handle.handle.permissions.register_needed_permission(PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND, this._needed_listener); - } - - html_tag() : JQuery { return this._html_tag; } - destroy() { - if(this.handle.handle.permissions) - this.handle.handle.permissions.unregister_needed_permission(PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND, this._needed_listener); - this.handle.handle.permissions.unregister_needed_permission(PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND, this._needed_listener); - this._needed_listener = undefined; - - this._chat_box && this._chat_box.destroy(); - this._chat_box = undefined; - - this._html_tag && this._html_tag.remove(); - this._html_tag = undefined; - this._container_conversation = undefined; - - for(const conversation of this._conversations) - conversation.destroy(); - this._conversations = []; - this._current_conversation = undefined; - } - - update_chat_box() { - let flag = true; - flag = flag && !!this._current_conversation; /* test if we have a conversation */ - flag = flag && !!this.handle.handle.permissions; /* test if we got permissions to test with */ - flag = flag && this.handle.handle.permissions.neededPermission(this._current_conversation.channel_id == 0 ? PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND : PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND).granted(1); - flag = flag && this._current_conversation.chat_available(); - this._chat_box.set_enabled(flag); - } - - private _build_html_tag() { - this._html_tag = $("#tmpl_frame_chat_channel").renderTag({ - chatbox: this._chat_box.html_tag() - }); - this._container_conversation = this._html_tag.find(".container-chat"); - this._chat_box.html_tag().on('focus', event => { - if(this._current_conversation) - this._current_conversation.mark_read(); - }); - this.update_input_format_helper(); - } - - set_current_channel(channel_id: number, update_info_frame?: boolean) { - if(this._current_conversation && this._current_conversation.channel_id === channel_id) - return; - - let conversation = this.conversation(channel_id); - this._current_conversation = conversation; - - if(this._current_conversation) { - this._container_conversation.children().detach(); - this._container_conversation.append(conversation.html_tag()); - this._current_conversation.fix_view_size(); - this._current_conversation.fix_scroll(false); - this.update_chat_box(); + if((current_entry as Message).timestamp < message.timestamp) { + this._view_entries.splice(index, 0, entry); + previous = current_entry; + break; } - if(typeof(update_info_frame) === "undefined" || update_info_frame) - this.handle.info_frame().update_channel_text(); + } + if(!previous) + this._view_entries.push(entry); + + if(previous) + entry.html_element.insertAfter(previous.html_element); + else + entry.html_element.insertAfter(this._view_older_messages.html_element); /* last element is already the current element */ + + if(_new_message && (typeof(this._scroll_position) === "number" || this.handle.current_channel() !== this.channel_id || this.handle.handle.content_type() !== FrameContent.CHANNEL_CHAT)) { + if(typeof(this._first_unread_message) === "undefined") + this._first_unread_message = entry; + + this._first_unread_message_pointer.html_element.insertBefore(entry.html_element); + this._container_messages.trigger('scroll'); /* updates the new message stuff */ + } + if(typeof(update_view) !== "boolean" || update_view) + this.fix_scroll(true); + } + + /* update chat state */ + this._container_no_permissions.hide(); + this._container_no_permissions_shown = false; + if(update_view) this.handle.update_chat_box(); + } + + /* using a timeout here to not cause a force style recalculation */ + private _scroll_fix_timer: number; + private _scroll_animate: boolean; + + fix_scroll(animate: boolean) { + if(this._scroll_fix_timer) { + this._scroll_animate = this._scroll_animate && animate; + return; + } + + this._scroll_fix_timer = setTimeout(() => { + this._scroll_fix_timer = undefined; + + let offset; + if(this._first_unread_message) { + offset = this._first_unread_message.html_element[0].offsetTop; + } else if(typeof(this._scroll_position) !== "undefined") { + offset = this._scroll_position; + } else { + offset = this._container_messages[0].scrollHeight; } - current_channel() : number { return this._current_conversation ? this._current_conversation.channel_id : 0; } - - /* Used by notifychanneldeleted */ - delete_conversation(channel_id: number) { - const entry = this._conversations.find(e => e.channel_id === channel_id); - if(!entry) - return; - - this._conversations.remove(entry); - entry.html_tag().detach(); - entry.destroy(); + if(this._scroll_animate) { + this._container_messages.stop(true).animate({ + scrollTop: offset + }, 'slow'); + } else { + this._container_messages.stop(true).scrollTop(offset); } + }, 5); + } - reset() { - while(this._conversations.length > 0) - this.delete_conversation(this._conversations[0].channel_id); + fix_view_size() { + this._view_older_messages.html_element.toggleClass('shown', !!this._has_older_messages); + + let count = 0; + for(let index = 0; index < this._view_entries.length; index++) { + if('timestamp' in this._view_entries[index]) + count++; + + if(count > this._view_max_messages) { + this._view_entries.splice(index, this._view_entries.length - index).forEach(e => { + clearTimeout(e.update_timer); + e.html_element.remove(); + }); + this._has_older_messages = true; + this._view_older_messages.html_element.toggleClass('shown', true); + break; } + } + } - conversation(channel_id: number, create?: boolean) : Conversation { - let conversation = this._conversations.find(e => e.channel_id === channel_id); + chat_available() : boolean { + return !this._container_no_permissions_shown && !this._container_is_private_shown && !this._container_no_support_shown; + } - if(!conversation && channel_id >= 0 && (typeof (create) === "undefined" || create)) { - conversation = new Conversation(this, channel_id); - this._conversations.push(conversation); - conversation.fetch_last_messages(); - } - return conversation; - } - - on_show() { - if(this._current_conversation) - this._current_conversation.fix_scroll(false); - } - - update_input_format_helper() { - const tag = this._html_tag.find(".container-format-helper"); - if(settings.static_global(Settings.KEY_CHAT_ENABLE_MARKDOWN)) { - tag.removeClass("hidden").text(tr("*italic*, **bold**, ~~strikethrough~~, `code`, and more...")); - } else { - tag.addClass("hidden"); + text_send_failed(error: CommandResult | any) { + log.warn(LogCategory.CHAT, "Failed to send text message! (%o)", error); + //TODO: Log if message send failed? + if(error instanceof CommandResult) { + if(error.id == ErrorID.PERMISSION_ERROR) { + //TODO: Split up between channel_text_message_send permission and no view permission + if(error.json["failed_permid"] == 0) { + this._container_no_permissions_shown = true; + this._container_no_permissions.show(); + this.handle.update_chat_box(); } } } } + + set_flag_private(flag: boolean) { + if(this._flag_private === flag) + return; + + this._flag_private = flag; + this.update_private_state(); + if(!flag) + this.fetch_last_messages(); + } + + update_private_state() { + if(!this._flag_private) { + this._container_is_private.hide(); + this._container_is_private_shown = false; + } else { + const client = this.handle.handle.handle.getClient(); + if(client && client.currentChannel() && client.currentChannel().channelId === this.channel_id) { + this._container_is_private_shown = false; + this._container_is_private.hide(); + } else { + this._container_is_private.show(); + this._container_is_private_shown = true; + } + } + } + + delete_message(message: MessageData) { + //TODO A lot of checks! + //conversationmessagedelete cid=2 timestamp_begin= timestamp_end= cldbid= limit=1 + this.handle.handle.handle.serverConnection.send_command('conversationmessagedelete', { + cid: this.channel_id, + cldbid: message.sender_database_id, + + timestamp_begin: message.timestamp - 1, + timestamp_end: message.timestamp + 1, + + limit: 1 + }).then(() => { + return; /* in general it gets deleted via notify */ + }).catch(error => { + log.error(LogCategory.CHAT, tr("Failed to delete conversation message for conversation %o: %o"), this.channel_id, error); + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Failed to delete message"), formatMessage(tr("Failed to delete conversation message{:br:}Error: {}"), error)).open(); + }); + log.debug(LogCategory.CLIENT, tr("Deleting text message %o"), message); + } + + delete_messages(begin: number, end: number, sender: number, limit: number) { + let count = 0; + for(const message of this._view_entries.slice()) { + if(!('sender_database_id' in message)) + continue; + + const cmsg = message as Message; + if(end != 0 && cmsg.timestamp > end) + continue; + if(begin != 0 && cmsg.timestamp < begin) + break; + + if(cmsg.sender_database_id !== sender) + continue; + + this._delete_message(message); + if(--count >= limit) + return; + } + + //TODO remove in cache? (_last_messages) + } + + private _delete_message(message: Message) { + if('html_element' in message) { + const cmessage = message as Message; + cmessage.html_element.remove(); + clearTimeout(cmessage.update_timer); + this._view_entries.remove(message as any); + } + + this._last_messages.remove(message); + } +} + +export class ConversationManager { + readonly handle: Frame; + + private _html_tag: JQuery; + private _chat_box: ChatBox; + + private _container_conversation: JQuery; + + private _conversations: Conversation[] = []; + private _current_conversation: Conversation | undefined; + + private _needed_listener = () => this.update_chat_box(); + + constructor(handle: Frame) { + this.handle = handle; + + this._chat_box = new ChatBox(); + this._build_html_tag(); + + this._chat_box.callback_text = text => { + if(!this._current_conversation) + return; + + const conv = this._current_conversation; + this.handle.handle.serverConnection.send_command("sendtextmessage", {targetmode: conv.channel_id == 0 ? 3 : 2, cid: conv.channel_id, msg: text}, {process_result: false}).catch(error => { + conv.text_send_failed(error); + }); + }; + this.update_chat_box(); + } + + initialize_needed_listener() { + this.handle.handle.permissions.register_needed_permission(PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND, this._needed_listener); + this.handle.handle.permissions.register_needed_permission(PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND, this._needed_listener); + } + + html_tag() : JQuery { return this._html_tag; } + destroy() { + if(this.handle.handle.permissions) + this.handle.handle.permissions.unregister_needed_permission(PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND, this._needed_listener); + this.handle.handle.permissions.unregister_needed_permission(PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND, this._needed_listener); + this._needed_listener = undefined; + + this._chat_box && this._chat_box.destroy(); + this._chat_box = undefined; + + this._html_tag && this._html_tag.remove(); + this._html_tag = undefined; + this._container_conversation = undefined; + + for(const conversation of this._conversations) + conversation.destroy(); + this._conversations = []; + this._current_conversation = undefined; + } + + update_chat_box() { + let flag = true; + flag = flag && !!this._current_conversation; /* test if we have a conversation */ + flag = flag && !!this.handle.handle.permissions; /* test if we got permissions to test with */ + flag = flag && this.handle.handle.permissions.neededPermission(this._current_conversation.channel_id == 0 ? PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND : PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND).granted(1); + flag = flag && this._current_conversation.chat_available(); + this._chat_box.set_enabled(flag); + } + + private _build_html_tag() { + this._html_tag = $("#tmpl_frame_chat_channel").renderTag({ + chatbox: this._chat_box.html_tag() + }); + this._container_conversation = this._html_tag.find(".container-chat"); + this._chat_box.html_tag().on('focus', event => { + if(this._current_conversation) + this._current_conversation.mark_read(); + }); + this.update_input_format_helper(); + } + + set_current_channel(channel_id: number, update_info_frame?: boolean) { + if(this._current_conversation && this._current_conversation.channel_id === channel_id) + return; + + let conversation = this.conversation(channel_id); + this._current_conversation = conversation; + + if(this._current_conversation) { + this._container_conversation.children().detach(); + this._container_conversation.append(conversation.html_tag()); + this._current_conversation.fix_view_size(); + this._current_conversation.fix_scroll(false); + this.update_chat_box(); + } + if(typeof(update_info_frame) === "undefined" || update_info_frame) + this.handle.info_frame().update_channel_text(); + } + + current_channel() : number { return this._current_conversation ? this._current_conversation.channel_id : 0; } + + /* Used by notifychanneldeleted */ + delete_conversation(channel_id: number) { + const entry = this._conversations.find(e => e.channel_id === channel_id); + if(!entry) + return; + + this._conversations.remove(entry); + entry.html_tag().detach(); + entry.destroy(); + } + + reset() { + while(this._conversations.length > 0) + this.delete_conversation(this._conversations[0].channel_id); + } + + conversation(channel_id: number, create?: boolean) : Conversation { + let conversation = this._conversations.find(e => e.channel_id === channel_id); + + if(!conversation && channel_id >= 0 && (typeof (create) === "undefined" || create)) { + conversation = new Conversation(this, channel_id); + this._conversations.push(conversation); + conversation.fetch_last_messages(); + } + return conversation; + } + + on_show() { + if(this._current_conversation) + this._current_conversation.fix_scroll(false); + } + + update_input_format_helper() { + const tag = this._html_tag.find(".container-format-helper"); + if(settings.static_global(Settings.KEY_CHAT_ENABLE_MARKDOWN)) { + tag.removeClass("hidden").text(tr("*italic*, **bold**, ~~strikethrough~~, `code`, and more...")); + } else { + tag.addClass("hidden"); + } + } } \ No newline at end of file diff --git a/shared/js/ui/frames/side/music_info.ts b/shared/js/ui/frames/side/music_info.ts index 41cbebbc..4aafa4ed 100644 --- a/shared/js/ui/frames/side/music_info.ts +++ b/shared/js/ui/frames/side/music_info.ts @@ -1,849 +1,856 @@ -namespace chat { - import PlayerState = connection.voice.PlayerState; +import {Frame, FrameContent} from "tc-shared/ui/frames/chat_frame"; +import * as events from "tc-shared/events"; +import {MusicClientEntry, SongInfo} from "tc-shared/ui/client"; +import {voice} from "tc-shared/connection/ConnectionBase"; +import PlayerState = voice.PlayerState; +import {LogCategory} from "tc-shared/log"; +import {CommandResult, ErrorID, PlaylistSong} from "tc-shared/connection/ServerConnectionDeclaration"; +import {createErrorModal, createInputModal} from "tc-shared/ui/elements/Modal"; +import * as log from "tc-shared/log"; +import * as image_preview from "../image_preview"; - declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; - declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; +declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; +declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; - interface LoadedSongData { - description: string; - title: string; - url: string; +interface LoadedSongData { + description: string; + title: string; + url: string; - length: number; - thumbnail?: string; + length: number; + thumbnail?: string; - metadata: {[key: string]: string}; + metadata: {[key: string]: string}; +} + +export class MusicInfo { + readonly events: events.Registry; + readonly handle: Frame; + + private _html_tag: JQuery; + private _container_playlist: JQuery; + + private _current_bot: MusicClientEntry | undefined; + private update_song_info: number = 0; /* timestamp when we force update the info */ + private time_select: { + active: boolean, + max_time: number, + current_select_time: number, + current_player_time: number + } = { active: false, current_select_time: 0, max_time: 0, current_player_time: 0}; + private song_reorder: { + active: boolean, + song_id: number, + previous_entry: number, + html: JQuery, + mouse?: {x: number, y: number}, + indicator: JQuery + } = { active: false, song_id: 0, previous_entry: 0, html: undefined, indicator: $.spawn("div").addClass("reorder-indicator") }; + + previous_frame_content: FrameContent; + + constructor(handle: Frame) { + this.events = new events.Registry(); + this.handle = handle; + + this.events.enable_debug("music-info"); + this.initialize_listener(); + this._build_html_tag(); + + this.set_current_bot(undefined, true); } - export class MusicInfo { - readonly events: events.Registry; - readonly handle: Frame; + html_tag() : JQuery { + return this._html_tag; + } - private _html_tag: JQuery; - private _container_playlist: JQuery; + destroy() { + this.set_current_bot(undefined); + this.events.destory(); - private _current_bot: MusicClientEntry | undefined; - private update_song_info: number = 0; /* timestamp when we force update the info */ - private time_select: { - active: boolean, - max_time: number, - current_select_time: number, - current_player_time: number - } = { active: false, current_select_time: 0, max_time: 0, current_player_time: 0}; - private song_reorder: { - active: boolean, - song_id: number, - previous_entry: number, - html: JQuery, - mouse?: {x: number, y: number}, - indicator: JQuery - } = { active: false, song_id: 0, previous_entry: 0, html: undefined, indicator: $.spawn("div").addClass("reorder-indicator") }; + this._html_tag && this._html_tag.remove(); + this._html_tag = undefined; - previous_frame_content: FrameContent; + this._current_bot = undefined; + this.previous_frame_content = undefined; + } - constructor(handle: Frame) { - this.events = new events.Registry(); - this.handle = handle; + private format_time(value: number) { + if(value == 0) return "--:--:--"; - this.events.enable_debug("music-info"); - this.initialize_listener(); - this._build_html_tag(); + value /= 1000; - this.set_current_bot(undefined, true); + let hours = 0, minutes = 0; + while(value >= 60 * 60) { + hours++; + value -= 60 * 60; } - html_tag() : JQuery { - return this._html_tag; + while(value >= 60) { + minutes++; + value -= 60; } - destroy() { - this.set_current_bot(undefined); - this.events.destory(); + return ("0" + hours).substr(-2) + ":" + ("0" + minutes).substr(-2) + ":" + ("0" + value.toFixed(0)).substr(-2); + }; - this._html_tag && this._html_tag.remove(); - this._html_tag = undefined; + private _build_html_tag() { + this._html_tag = $("#tmpl_frame_chat_music_info").renderTag(); + this._container_playlist = this._html_tag.find(".container-playlist"); - this._current_bot = undefined; - this.previous_frame_content = undefined; - } + this._html_tag.find(".button-close").on('click', () => { + if(this.previous_frame_content === FrameContent.CLIENT_INFO) + this.previous_frame_content = FrameContent.NONE; - private format_time(value: number) { - if(value == 0) return "--:--:--"; + this.handle.set_content(this.previous_frame_content); + }); - value /= 1000; + this._html_tag.find(".button-reload-playlist").on('click', () => this.events.fire("action_playlist_reload")); + this._html_tag.find(".button-song-add").on('click', () => this.events.fire("action_song_add")); + this._html_tag.find(".thumbnail").on('click', event => { + const image = this._html_tag.find(".thumbnail img"); + const url = image.attr("x-thumbnail-url"); + if(!url) return; - let hours = 0, minutes = 0; - while(value >= 60 * 60) { - hours++; - value -= 60 * 60; - } + image_preview.preview_image(decodeURIComponent(url), decodeURIComponent(url)); + }); - while(value >= 60) { - minutes++; - value -= 60; - } + { + const button_play = this._html_tag.find(".control-buttons .button-play"); + const button_pause = this._html_tag.find(".control-buttons .button-pause"); - return ("0" + hours).substr(-2) + ":" + ("0" + minutes).substr(-2) + ":" + ("0" + value.toFixed(0)).substr(-2); - }; + button_play.on('click', () => this.events.fire("action_play")); + button_pause.on('click', () => this.events.fire("action_pause")); - private _build_html_tag() { - this._html_tag = $("#tmpl_frame_chat_music_info").renderTag(); - this._container_playlist = this._html_tag.find(".container-playlist"); + this.events.on(["bot_change", "bot_property_update"], event => { + if(event.type === "bot_property_update" && event.as<"bot_property_update">().properties.indexOf("player_state") == -1) return; - this._html_tag.find(".button-close").on('click', () => { - if(this.previous_frame_content === FrameContent.CLIENT_INFO) - this.previous_frame_content = FrameContent.NONE; - - this.handle.set_content(this.previous_frame_content); + button_play.toggleClass("hidden", this._current_bot === undefined || this._current_bot.properties.player_state < PlayerState.STOPPING); }); - this._html_tag.find(".button-reload-playlist").on('click', () => this.events.fire("action_playlist_reload")); - this._html_tag.find(".button-song-add").on('click', () => this.events.fire("action_song_add")); - this._html_tag.find(".thumbnail").on('click', event => { - const image = this._html_tag.find(".thumbnail img"); - const url = image.attr("x-thumbnail-url"); - if(!url) return; + this.events.on(["bot_change", "bot_property_update"], event => { + if(event.type === "bot_property_update" && event.as<"bot_property_update">().properties.indexOf("player_state") == -1) return; - image_preview.preview_image(decodeURIComponent(url), decodeURIComponent(url)); + button_pause.toggleClass("hidden", this._current_bot !== undefined && this._current_bot.properties.player_state >= PlayerState.STOPPING); }); - { - const button_play = this._html_tag.find(".control-buttons .button-play"); - const button_pause = this._html_tag.find(".control-buttons .button-pause"); + this._html_tag.find(".control-buttons .button-rewind").on('click', () => this.events.fire("action_rewind")); + this._html_tag.find(".control-buttons .button-forward").on('click', () => this.events.fire("action_forward")); + } - button_play.on('click', () => this.events.fire("action_play")); - button_pause.on('click', () => this.events.fire("action_pause")); + /* timeline updaters */ + { + const container = this._html_tag.find(".container-timeline"); - this.events.on(["bot_change", "bot_property_update"], event => { - if(event.type === "bot_property_update" && event.as<"bot_property_update">().properties.indexOf("player_state") == -1) return; + const timeline = container.find(".timeline"); + const indicator_playtime = container.find(".indicator-playtime"); + const indicator_buffered = container.find(".indicator-buffered"); + const thumb = container.find(".thumb"); - button_play.toggleClass("hidden", this._current_bot === undefined || this._current_bot.properties.player_state < PlayerState.STOPPING); - }); + const timestamp_current = container.find(".timestamps .current"); + const timestamp_max = container.find(".timestamps .max"); - this.events.on(["bot_change", "bot_property_update"], event => { - if(event.type === "bot_property_update" && event.as<"bot_property_update">().properties.indexOf("player_state") == -1) return; + thumb.on('mousedown', event => event.button === 0 && this.events.fire("playtime_move_begin")); - button_pause.toggleClass("hidden", this._current_bot !== undefined && this._current_bot.properties.player_state >= PlayerState.STOPPING); - }); + this.events.on(["bot_change", "player_song_change", "player_time_update", "playtime_move_end"], event => { + if(!this._current_bot) { + this.time_select.max_time = 0; + indicator_buffered.each((_, e) => { e.style.width = "0%"; }); + indicator_playtime.each((_, e) => { e.style.width = "0%"; }); + thumb.each((_, e) => { e.style.marginLeft = "0%"; }); - this._html_tag.find(".control-buttons .button-rewind").on('click', () => this.events.fire("action_rewind")); - this._html_tag.find(".control-buttons .button-forward").on('click', () => this.events.fire("action_forward")); - } + timestamp_current.text("--:--:--"); + timestamp_max.text("--:--:--"); + return; + } + if(event.type === "playtime_move_end" && !event.as<"playtime_move_end">().canceled) return; - /* timeline updaters */ - { - const container = this._html_tag.find(".container-timeline"); - - const timeline = container.find(".timeline"); - const indicator_playtime = container.find(".indicator-playtime"); - const indicator_buffered = container.find(".indicator-buffered"); - const thumb = container.find(".thumb"); - - const timestamp_current = container.find(".timestamps .current"); - const timestamp_max = container.find(".timestamps .max"); - - thumb.on('mousedown', event => event.button === 0 && this.events.fire("playtime_move_begin")); - - this.events.on(["bot_change", "player_song_change", "player_time_update", "playtime_move_end"], event => { - if(!this._current_bot) { - this.time_select.max_time = 0; - indicator_buffered.each((_, e) => { e.style.width = "0%"; }); - indicator_playtime.each((_, e) => { e.style.width = "0%"; }); - thumb.each((_, e) => { e.style.marginLeft = "0%"; }); - - timestamp_current.text("--:--:--"); - timestamp_max.text("--:--:--"); - return; - } - if(event.type === "playtime_move_end" && !event.as<"playtime_move_end">().canceled) return; - - const update_info = Date.now() > this.update_song_info; - this._current_bot.requestPlayerInfo(update_info ? 1000 : 60 * 1000).then(data => { - if(update_info) - this.display_song_info(data); - - let played, buffered; - if(event.type !== "player_time_update") { - played = data.player_replay_index; - buffered = data.player_buffered_index; - } else { - played = event.as<"player_time_update">().player_replay_index; - buffered = event.as<"player_time_update">().player_buffered_index; - } - - this.time_select.current_player_time = played; - this.time_select.max_time = data.player_max_index; - timestamp_max.text(data.player_max_index ? this.format_time(data.player_max_index) : "--:--:--"); - - if(this.time_select.active) - return; - - let wplayed, wbuffered; - if(data.player_max_index) { - wplayed = (played * 100 / data.player_max_index).toFixed(2) + "%"; - wbuffered = (buffered * 100 / data.player_max_index).toFixed(2) + "%"; - - timestamp_current.text(this.format_time(played)); - } else { - wplayed = "100%"; - wbuffered = "100%"; - - timestamp_current.text(this.format_time(played)); - } - - indicator_buffered.each((_, e) => { e.style.width = wbuffered; }); - indicator_playtime.each((_, e) => { e.style.width = wplayed; }); - thumb.each((_, e) => { e.style.marginLeft = wplayed; }); - }); - }); - - const move_callback = (event: MouseEvent) => { - const x_min = timeline.offset().left; - const x_max = x_min + timeline.width(); - - let current = event.pageX; - if(current < x_min) - current = x_min; - else if(current > x_max) - current = x_max; - - const percent = (current - x_min) / (x_max - x_min); - this.time_select.current_select_time = percent * this.time_select.max_time; - timestamp_current.text(this.format_time(this.time_select.current_select_time)); - - const w = (percent * 100).toFixed(2) + "%"; - indicator_playtime.each((_, e) => { e.style.width = w; }); - thumb.each((_, e) => { e.style.marginLeft = w; }); - }; - - const up_callback = (event: MouseEvent | FocusEvent) => { - if(event.type === "mouseup") - if((event as MouseEvent).button !== 0) return; - - this.events.fire("playtime_move_end", { - canceled: event.type !== "mouseup", - target_time: this.time_select.current_select_time - }); - }; - - this.events.on("playtime_move_begin", event => { - if(this.time_select.max_time <= 0) return; - - this.time_select.active = true; - indicator_buffered.each((_, e) => { e.style.width = "0"; }); - document.addEventListener("mousemove", move_callback); - document.addEventListener("mouseleave", up_callback); - document.addEventListener("blur", up_callback); - document.addEventListener("mouseup", up_callback); - document.body.style.userSelect = "none"; - }); - - this.events.on(["bot_change", "player_song_change", "playtime_move_end"], event => { - document.removeEventListener("mousemove", move_callback); - document.removeEventListener("mouseleave", up_callback); - document.removeEventListener("blur", up_callback); - document.removeEventListener("mouseup", up_callback); - document.body.style.userSelect = undefined; - this.time_select.active = false; - - if(event.type === "playtime_move_end") { - const data = event.as<"playtime_move_end">(); - if(data.canceled) return; - - const offset = data.target_time - this.time_select.current_player_time; - this.events.fire(offset > 0 ? "action_forward_ms" : "action_rewind_ms", {units: Math.abs(offset) }); - } - }); - } - - /* song info handlers */ - this.events.on(["bot_change", "player_song_change"], event => { - let song: SongInfo; - - /* update the player info so we dont get old data */ - if(this._current_bot) { - this.update_song_info = 0; - this._current_bot.requestPlayerInfo(1000).then(data => { + const update_info = Date.now() > this.update_song_info; + this._current_bot.requestPlayerInfo(update_info ? 1000 : 60 * 1000).then(data => { + if(update_info) this.display_song_info(data); - }).catch(error => { - log.warn(LogCategory.CLIENT, tr("Failed to update current song for side bar: %o"), error); - }); - } - if(event.type === "bot_change") { - song = undefined; - } else { - song = event.as<"player_song_change">().song; - } - this.display_song_info(song); - }); - } - - private display_song_info(song: SongInfo) { - if(song) { - if(!song.song_loaded) { - console.log("Awaiting a loaded song info."); - this.update_song_info = 0; - } else { - console.log("Song info loaded."); - this.update_song_info = Date.now() + 60 * 1000; - } - } - - if(!song) song = new SongInfo(); - - const container_thumbnail = this._html_tag.find(".player .container-thumbnail"); - const container_info = this._html_tag.find(".player .container-song-info"); - - container_thumbnail.find("img") - .attr("src", song.song_thumbnail || "img/music/no-thumbnail.png") - .attr("x-thumbnail-url", encodeURIComponent(song.song_thumbnail)) - .css("cursor", song.song_thumbnail ? "pointer" : null); - - if(song.song_id) - container_info.find(".song-name").text(song.song_title || song.song_url).attr("title", song.song_title || song.song_url); - else - container_info.find(".song-name").text(tr("No song selected")); - if(song.song_description) { - container_info.find(".song-description").removeClass("hidden").text(song.song_description).attr("title", song.song_description); - } else { - container_info.find(".song-description").addClass("hidden").text(tr("Song has no description")).attr("title", tr("Song has no description")); - } - } - - private initialize_listener() { - //Must come at first! - this.events.on("player_song_change", event => { - if(!this._current_bot) return; - - this._current_bot.requestPlayerInfo(0); /* enforce an info refresh */ - }); - - /* bot property listener */ - const callback_property = event => this.events.fire("bot_property_update", { properties: event.properties }); - const callback_time_update = event => this.events.fire("player_time_update", event); - const callback_song_change = event => this.events.fire("player_song_change", event); - this.events.on("bot_change", event => { - if(event.old) { - event.old.events.off(callback_property); - event.old.events.off(callback_time_update); - event.old.events.off(callback_song_change); - event.old.events.disconnect_all(this.events); - } - if(event.new) { - event.new.events.on("property_update", callback_property); - - event.new.events.on("music_status_update", callback_time_update); - event.new.events.on("music_song_change", callback_song_change); - - event.new.events.connect("playlist_song_add", this.events); - event.new.events.connect("playlist_song_remove", this.events); - event.new.events.connect("playlist_song_reorder", this.events); - event.new.events.connect("playlist_song_loaded", this.events); - } - }); - - /* basic player actions */ - { - const action_map = { - "action_play": 1, - "action_pause": 2, - "action_forward": 3, - "action_rewind": 4, - "action_forward_ms": 5, - "action_rewind_ms": 6 - }; - - this.events.on(Object.keys(action_map) as any, event => { - if(!this._current_bot) return; - - const action_id = action_map[event.type]; - if(typeof action_id === "undefined") { - log.warn(LogCategory.GENERAL, tr("Invalid music bot action event detected: %s. This should not happen!"), event.type); - return; + let played, buffered; + if(event.type !== "player_time_update") { + played = data.player_replay_index; + buffered = data.player_buffered_index; + } else { + played = event.as<"player_time_update">().player_replay_index; + buffered = event.as<"player_time_update">().player_buffered_index; } - const data = { - bot_id: this._current_bot.properties.client_database_id, - action: action_id, - units: event.units - }; - this.handle.handle.serverConnection.send_command("musicbotplayeraction", data).catch(error => { - if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) return; - log.error(LogCategory.CLIENT, tr("Failed to perform action %s on bot: %o"), event.type, error); - //TODO: Better error dialog - createErrorModal(tr("Failed to perform action."), tr("Failed to perform action for music bot.")).open(); - }); + this.time_select.current_player_time = played; + this.time_select.max_time = data.player_max_index; + timestamp_max.text(data.player_max_index ? this.format_time(data.player_max_index) : "--:--:--"); + + if(this.time_select.active) + return; + + let wplayed, wbuffered; + if(data.player_max_index) { + wplayed = (played * 100 / data.player_max_index).toFixed(2) + "%"; + wbuffered = (buffered * 100 / data.player_max_index).toFixed(2) + "%"; + + timestamp_current.text(this.format_time(played)); + } else { + wplayed = "100%"; + wbuffered = "100%"; + + timestamp_current.text(this.format_time(played)); + } + + indicator_buffered.each((_, e) => { e.style.width = wbuffered; }); + indicator_playtime.each((_, e) => { e.style.width = wplayed; }); + thumb.each((_, e) => { e.style.marginLeft = wplayed; }); + }); + }); + + const move_callback = (event: MouseEvent) => { + const x_min = timeline.offset().left; + const x_max = x_min + timeline.width(); + + let current = event.pageX; + if(current < x_min) + current = x_min; + else if(current > x_max) + current = x_max; + + const percent = (current - x_min) / (x_max - x_min); + this.time_select.current_select_time = percent * this.time_select.max_time; + timestamp_current.text(this.format_time(this.time_select.current_select_time)); + + const w = (percent * 100).toFixed(2) + "%"; + indicator_playtime.each((_, e) => { e.style.width = w; }); + thumb.each((_, e) => { e.style.marginLeft = w; }); + }; + + const up_callback = (event: MouseEvent | FocusEvent) => { + if(event.type === "mouseup") + if((event as MouseEvent).button !== 0) return; + + this.events.fire("playtime_move_end", { + canceled: event.type !== "mouseup", + target_time: this.time_select.current_select_time + }); + }; + + this.events.on("playtime_move_begin", event => { + if(this.time_select.max_time <= 0) return; + + this.time_select.active = true; + indicator_buffered.each((_, e) => { e.style.width = "0"; }); + document.addEventListener("mousemove", move_callback); + document.addEventListener("mouseleave", up_callback); + document.addEventListener("blur", up_callback); + document.addEventListener("mouseup", up_callback); + document.body.style.userSelect = "none"; + }); + + this.events.on(["bot_change", "player_song_change", "playtime_move_end"], event => { + document.removeEventListener("mousemove", move_callback); + document.removeEventListener("mouseleave", up_callback); + document.removeEventListener("blur", up_callback); + document.removeEventListener("mouseup", up_callback); + document.body.style.userSelect = undefined; + this.time_select.active = false; + + if(event.type === "playtime_move_end") { + const data = event.as<"playtime_move_end">(); + if(data.canceled) return; + + const offset = data.target_time - this.time_select.current_player_time; + this.events.fire(offset > 0 ? "action_forward_ms" : "action_rewind_ms", {units: Math.abs(offset) }); + } + }); + } + + /* song info handlers */ + this.events.on(["bot_change", "player_song_change"], event => { + let song: SongInfo; + + /* update the player info so we dont get old data */ + if(this._current_bot) { + this.update_song_info = 0; + this._current_bot.requestPlayerInfo(1000).then(data => { + this.display_song_info(data); + }).catch(error => { + log.warn(LogCategory.CLIENT, tr("Failed to update current song for side bar: %o"), error); }); } - this.events.on("action_song_set", event => { + if(event.type === "bot_change") { + song = undefined; + } else { + song = event.as<"player_song_change">().song; + } + this.display_song_info(song); + }); + } + + private display_song_info(song: SongInfo) { + if(song) { + if(!song.song_loaded) { + console.log("Awaiting a loaded song info."); + this.update_song_info = 0; + } else { + console.log("Song info loaded."); + this.update_song_info = Date.now() + 60 * 1000; + } + } + + if(!song) song = new SongInfo(); + + const container_thumbnail = this._html_tag.find(".player .container-thumbnail"); + const container_info = this._html_tag.find(".player .container-song-info"); + + container_thumbnail.find("img") + .attr("src", song.song_thumbnail || "img/music/no-thumbnail.png") + .attr("x-thumbnail-url", encodeURIComponent(song.song_thumbnail)) + .css("cursor", song.song_thumbnail ? "pointer" : null); + + if(song.song_id) + container_info.find(".song-name").text(song.song_title || song.song_url).attr("title", song.song_title || song.song_url); + else + container_info.find(".song-name").text(tr("No song selected")); + if(song.song_description) { + container_info.find(".song-description").removeClass("hidden").text(song.song_description).attr("title", song.song_description); + } else { + container_info.find(".song-description").addClass("hidden").text(tr("Song has no description")).attr("title", tr("Song has no description")); + } + } + + private initialize_listener() { + //Must come at first! + this.events.on("player_song_change", event => { + if(!this._current_bot) return; + + this._current_bot.requestPlayerInfo(0); /* enforce an info refresh */ + }); + + /* bot property listener */ + const callback_property = event => this.events.fire("bot_property_update", { properties: event.properties }); + const callback_time_update = event => this.events.fire("player_time_update", event); + const callback_song_change = event => this.events.fire("player_song_change", event); + this.events.on("bot_change", event => { + if(event.old) { + event.old.events.off(callback_property); + event.old.events.off(callback_time_update); + event.old.events.off(callback_song_change); + event.old.events.disconnect_all(this.events); + } + if(event.new) { + event.new.events.on("property_update", callback_property); + + event.new.events.on("music_status_update", callback_time_update); + event.new.events.on("music_song_change", callback_song_change); + + event.new.events.connect("playlist_song_add", this.events); + event.new.events.connect("playlist_song_remove", this.events); + event.new.events.connect("playlist_song_reorder", this.events); + event.new.events.connect("playlist_song_loaded", this.events); + } + }); + + /* basic player actions */ + { + const action_map = { + "action_play": 1, + "action_pause": 2, + "action_forward": 3, + "action_rewind": 4, + "action_forward_ms": 5, + "action_rewind_ms": 6 + }; + + this.events.on(Object.keys(action_map) as any, event => { if(!this._current_bot) return; + const action_id = action_map[event.type]; + if(typeof action_id === "undefined") { + log.warn(LogCategory.GENERAL, tr("Invalid music bot action event detected: %s. This should not happen!"), event.type); + return; + } + const data = { + bot_id: this._current_bot.properties.client_database_id, + action: action_id, + units: event.units + }; + this.handle.handle.serverConnection.send_command("musicbotplayeraction", data).catch(error => { + if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) return; + + log.error(LogCategory.CLIENT, tr("Failed to perform action %s on bot: %o"), event.type, error); + //TODO: Better error dialog + createErrorModal(tr("Failed to perform action."), tr("Failed to perform action for music bot.")).open(); + }); + }); + } + + this.events.on("action_song_set", event => { + if(!this._current_bot) return; + + const connection = this.handle.handle.serverConnection; + if(!connection || !connection.connected()) return; + + connection.send_command("playlistsongsetcurrent", { + playlist_id: this._current_bot.properties.client_playlist_id, + song_id: event.song_id + }).catch(error => { + if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) return; + + log.error(LogCategory.CLIENT, tr("Failed to set current song on bot: %o"), event.type, error); + //TODO: Better error dialog + createErrorModal(tr("Failed to set song."), tr("Failed to set current replaying song.")).open(); + }) + }); + + this.events.on("action_song_add", () => { + if(!this._current_bot) return; + + createInputModal(tr("Enter song URL"), tr("Please enter the target song URL"), text => { + try { + new URL(text); + return true; + } catch(error) { + return false; + } + }, result => { + if(!result || !this._current_bot) return; + const connection = this.handle.handle.serverConnection; - if(!connection || !connection.connected()) return; - - connection.send_command("playlistsongsetcurrent", { + connection.send_command("playlistsongadd", { playlist_id: this._current_bot.properties.client_playlist_id, - song_id: event.song_id + url: result }).catch(error => { if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) return; - log.error(LogCategory.CLIENT, tr("Failed to set current song on bot: %o"), event.type, error); - //TODO: Better error dialog - createErrorModal(tr("Failed to set song."), tr("Failed to set current replaying song.")).open(); - }) + log.error(LogCategory.CLIENT, tr("Failed to add song to bot playlist: %o"), error); + + //TODO: Better error description + createErrorModal(tr("Failed to insert song"), tr("Failed to append song to the playlist.")).open(); + }); + }).open(); + }); + + this.events.on("action_song_delete", event => { + if(!this._current_bot) return; + + const connection = this.handle.handle.serverConnection; + if(!connection || !connection.connected()) return; + + connection.send_command("playlistsongremove", { + playlist_id: this._current_bot.properties.client_playlist_id, + song_id: event.song_id + }).catch(error => { + if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) return; + + log.error(LogCategory.CLIENT, tr("Failed to delete song from bot playlist: %o"), error); + + //TODO: Better error description + createErrorModal(tr("Failed to delete song"), tr("Failed to remove song from the playlist.")).open(); + }); + }); + + /* bot subscription */ + this.events.on("bot_change", () => { + const connection = this.handle.handle.serverConnection; + if(!connection || !connection.connected()) return; + + const bot_id = this._current_bot ? this._current_bot.properties.client_database_id : 0; + this.handle.handle.serverConnection.send_command("musicbotsetsubscription", { bot_id: bot_id }).catch(error => { + log.warn(LogCategory.CLIENT, tr("Failed to subscribe to displayed bot within the side bar: %o"), error); + }); + }); + + /* playlist stuff */ + this.events.on(["bot_change", "action_playlist_reload"], event => { + this.playlist_subscribe(true); + this.update_playlist(); + }); + + this.events.on("playlist_song_add", event => { + const animation = typeof event.insert_effect === "boolean" ? event.insert_effect : true; + const html_entry = this.build_playlist_entry(event.song, animation); + const playlist = this._container_playlist.find(".playlist"); + const previous = playlist.find(".entry[song-id=" + event.song.song_previous_song_id + "]"); + + if(previous.length) + html_entry.insertAfter(previous); + else + html_entry.appendTo(playlist); + if(event.song.song_loaded) + this.events.fire("playlist_song_loaded", { + html_entry: html_entry, + metadata: event.song.song_metadata, + success: true, + song_id: event.song.song_id + }); + if(animation) + setTimeout(() => html_entry.addClass("shown"), 50); + }); + + this.events.on("playlist_song_remove", event => { + const playlist = this._container_playlist.find(".playlist"); + const song = playlist.find(".entry[song-id=" + event.song_id + "]"); + song.addClass("deleted"); + setTimeout(() => song.remove(), 5000); /* to play some animations */ + }); + + this.events.on("playlist_song_reorder", event => { + const playlist = this._container_playlist.find(".playlist"); + const entry = playlist.find(".entry[song-id=" + event.song_id + "]"); + if(!entry) return; + + console.log(event); + const previous = playlist.find(".entry[song-id=" + event.previous_song_id + "]"); + if(previous.length) { + entry.insertAfter(previous); + } else { + entry.insertBefore(playlist.find(".entry")[0]); + } + }); + + this.events.on("playlist_song_loaded", event => { + const entry = event.html_entry || this._container_playlist.find(".playlist .entry[song-id=" + event.song_id + "]"); + + const thumbnail = entry.find(".container-thumbnail img"); + const name = entry.find(".name"); + const description = entry.find(".description"); + const length = entry.find(".length"); + + if(event.success) { + let meta: LoadedSongData; + try { + meta = JSON.parse(event.metadata); + } catch(error) { + log.warn(LogCategory.CLIENT, tr("Failed to decode song metadata")); + meta = { + description: "", + title: "", + metadata: {}, + length: 0, + url: entry.attr("song-url") + } + } + + if(!meta.title && meta.description) { + meta.title = meta.description.split("\n")[0]; + meta.description = meta.description.split("\n").slice(1).join("\n"); + } + meta.title = meta.title || meta.url; + + name.text(meta.title); + description.text(meta.description); + length.text(this.format_time(meta.length || 0)); + if(meta.thumbnail) { + thumbnail.attr("src", meta.thumbnail) + .attr("x-thumbnail-url", encodeURIComponent(meta.thumbnail)); + } + } else { + name.text(tr("failed to load ") + entry.attr("song-url")).attr("title", tr("failed to load ") + entry.attr("song-url")); + description.text(event.error_msg || tr("unknown error")).attr("title", event.error_msg || tr("unknown error")); + } + }); + + /* song reorder */ + { + const move_callback = (event: MouseEvent) => { + if(!this.song_reorder.html) return; + + this.song_reorder.html.each((_, e) => { + e.style.left = (event.pageX - this.song_reorder.mouse.x) + "px"; + e.style.top = (event.pageY - this.song_reorder.mouse.y) + "px"; + }); + + const entries = this._container_playlist.find(".playlist .entry"); + let before: HTMLElement; + for(const entry of entries) { + const off = $(entry).offset().top; + if(off > event.pageY) { + this.song_reorder.indicator.insertBefore(entry); + this.song_reorder.previous_entry = before ? parseInt(before.attributes.getNamedItem("song-id").value) : 0; + return; + } + + before = entry; + } + this.song_reorder.indicator.insertAfter(entries.last()); + this.song_reorder.previous_entry = before ? parseInt(before.attributes.getNamedItem("song-id").value) : 0; + }; + + const up_callback = (event: MouseEvent | FocusEvent) => { + if(event.type === "mouseup") + if((event as MouseEvent).button !== 0) return; + + this.events.fire("reorder_end", { + canceled: event.type !== "mouseup", + song_id: this.song_reorder.song_id, + entry: this.song_reorder.html, + previous_entry: this.song_reorder.previous_entry + }); + }; + + this.events.on("reorder_begin", event => { + this.song_reorder.song_id = event.song_id; + this.song_reorder.html = event.entry; + + const width = this.song_reorder.html.width() + "px"; + this.song_reorder.html.each((_, e) => { e.style.width = width; }); + this.song_reorder.active = true; + this.song_reorder.html.addClass("reordering"); + + document.addEventListener("mousemove", move_callback); + document.addEventListener("mouseleave", up_callback); + document.addEventListener("blur", up_callback); + document.addEventListener("mouseup", up_callback); + document.body.style.userSelect = "none"; }); - this.events.on("action_song_add", () => { - if(!this._current_bot) return; + this.events.on(["bot_change", "playlist_song_remove", "reorder_end"], event => { + if(event.type === "playlist_song_remove" && event.as<"playlist_song_remove">().song_id !== this.song_reorder.song_id) return; - createInputModal(tr("Enter song URL"), tr("Please enter the target song URL"), text => { - try { - new URL(text); - return true; - } catch(error) { - return false; - } - }, result => { - if(!result || !this._current_bot) return; + document.removeEventListener("mousemove", move_callback); + document.removeEventListener("mouseleave", up_callback); + document.removeEventListener("blur", up_callback); + document.removeEventListener("mouseup", up_callback); + document.body.style.userSelect = undefined; + + this.song_reorder.active = false; + this.song_reorder.indicator.remove(); + if(this.song_reorder.html) { + this.song_reorder.html.each((_, e) => { + e.style.width = null; + e.style.left = null; + e.style.top = null; + }); + this.song_reorder.html.removeClass("reordering"); + } + + if(event.type === "reorder_end") { + const data = event.as<"reorder_end">(); + if(data.canceled) return; const connection = this.handle.handle.serverConnection; - connection.send_command("playlistsongadd", { + if(!connection || !connection.connected()) return; + if(!this._current_bot) return; + + connection.send_command("playlistsongreorder", { playlist_id: this._current_bot.properties.client_playlist_id, - url: result + song_id: data.song_id, + song_previous_song_id: data.previous_entry }).catch(error => { if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) return; log.error(LogCategory.CLIENT, tr("Failed to add song to bot playlist: %o"), error); //TODO: Better error description - createErrorModal(tr("Failed to insert song"), tr("Failed to append song to the playlist.")).open(); + createErrorModal(tr("Failed to reorder song"), tr("Failed to reorder song within the playlist.")).open(); }); - }).open(); - }); - - this.events.on("action_song_delete", event => { - if(!this._current_bot) return; - - const connection = this.handle.handle.serverConnection; - if(!connection || !connection.connected()) return; - - connection.send_command("playlistsongremove", { - playlist_id: this._current_bot.properties.client_playlist_id, - song_id: event.song_id - }).catch(error => { - if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) return; - - log.error(LogCategory.CLIENT, tr("Failed to delete song from bot playlist: %o"), error); - - //TODO: Better error description - createErrorModal(tr("Failed to delete song"), tr("Failed to remove song from the playlist.")).open(); - }); - }); - - /* bot subscription */ - this.events.on("bot_change", () => { - const connection = this.handle.handle.serverConnection; - if(!connection || !connection.connected()) return; - - const bot_id = this._current_bot ? this._current_bot.properties.client_database_id : 0; - this.handle.handle.serverConnection.send_command("musicbotsetsubscription", { bot_id: bot_id }).catch(error => { - log.warn(LogCategory.CLIENT, tr("Failed to subscribe to displayed bot within the side bar: %o"), error); - }); - }); - - /* playlist stuff */ - this.events.on(["bot_change", "action_playlist_reload"], event => { - this.playlist_subscribe(true); - this.update_playlist(); - }); - - this.events.on("playlist_song_add", event => { - const animation = typeof event.insert_effect === "boolean" ? event.insert_effect : true; - const html_entry = this.build_playlist_entry(event.song, animation); - const playlist = this._container_playlist.find(".playlist"); - const previous = playlist.find(".entry[song-id=" + event.song.song_previous_song_id + "]"); - - if(previous.length) - html_entry.insertAfter(previous); - else - html_entry.appendTo(playlist); - if(event.song.song_loaded) - this.events.fire("playlist_song_loaded", { - html_entry: html_entry, - metadata: event.song.song_metadata, - success: true, - song_id: event.song.song_id - }); - if(animation) - setTimeout(() => html_entry.addClass("shown"), 50); - }); - - this.events.on("playlist_song_remove", event => { - const playlist = this._container_playlist.find(".playlist"); - const song = playlist.find(".entry[song-id=" + event.song_id + "]"); - song.addClass("deleted"); - setTimeout(() => song.remove(), 5000); /* to play some animations */ - }); - - this.events.on("playlist_song_reorder", event => { - const playlist = this._container_playlist.find(".playlist"); - const entry = playlist.find(".entry[song-id=" + event.song_id + "]"); - if(!entry) return; - - console.log(event); - const previous = playlist.find(".entry[song-id=" + event.previous_song_id + "]"); - if(previous.length) { - entry.insertAfter(previous); - } else { - entry.insertBefore(playlist.find(".entry")[0]); + console.log("Reorder to %d", data.previous_entry); } }); - this.events.on("playlist_song_loaded", event => { - const entry = event.html_entry || this._container_playlist.find(".playlist .entry[song-id=" + event.song_id + "]"); - - const thumbnail = entry.find(".container-thumbnail img"); - const name = entry.find(".name"); - const description = entry.find(".description"); - const length = entry.find(".length"); - - if(event.success) { - let meta: LoadedSongData; - try { - meta = JSON.parse(event.metadata); - } catch(error) { - log.warn(LogCategory.CLIENT, tr("Failed to decode song metadata")); - meta = { - description: "", - title: "", - metadata: {}, - length: 0, - url: entry.attr("song-url") - } - } - - if(!meta.title && meta.description) { - meta.title = meta.description.split("\n")[0]; - meta.description = meta.description.split("\n").slice(1).join("\n"); - } - meta.title = meta.title || meta.url; - - name.text(meta.title); - description.text(meta.description); - length.text(this.format_time(meta.length || 0)); - if(meta.thumbnail) { - thumbnail.attr("src", meta.thumbnail) - .attr("x-thumbnail-url", encodeURIComponent(meta.thumbnail)); - } - } else { - name.text(tr("failed to load ") + entry.attr("song-url")).attr("title", tr("failed to load ") + entry.attr("song-url")); - description.text(event.error_msg || tr("unknown error")).attr("title", event.error_msg || tr("unknown error")); + this.events.on(["bot_change", "player_song_change"], event => { + if(!this._current_bot) { + this._html_tag.find(".playlist .current-song").removeClass("current-song"); + return; } - }); - /* song reorder */ - { - const move_callback = (event: MouseEvent) => { - if(!this.song_reorder.html) return; - - this.song_reorder.html.each((_, e) => { - e.style.left = (event.pageX - this.song_reorder.mouse.x) + "px"; - e.style.top = (event.pageY - this.song_reorder.mouse.y) + "px"; - }); - - const entries = this._container_playlist.find(".playlist .entry"); - let before: HTMLElement; - for(const entry of entries) { - const off = $(entry).offset().top; - if(off > event.pageY) { - this.song_reorder.indicator.insertBefore(entry); - this.song_reorder.previous_entry = before ? parseInt(before.attributes.getNamedItem("song-id").value) : 0; - return; - } - - before = entry; - } - this.song_reorder.indicator.insertAfter(entries.last()); - this.song_reorder.previous_entry = before ? parseInt(before.attributes.getNamedItem("song-id").value) : 0; - }; - - const up_callback = (event: MouseEvent | FocusEvent) => { - if(event.type === "mouseup") - if((event as MouseEvent).button !== 0) return; - - this.events.fire("reorder_end", { - canceled: event.type !== "mouseup", - song_id: this.song_reorder.song_id, - entry: this.song_reorder.html, - previous_entry: this.song_reorder.previous_entry - }); - }; - - this.events.on("reorder_begin", event => { - this.song_reorder.song_id = event.song_id; - this.song_reorder.html = event.entry; - - const width = this.song_reorder.html.width() + "px"; - this.song_reorder.html.each((_, e) => { e.style.width = width; }); - this.song_reorder.active = true; - this.song_reorder.html.addClass("reordering"); - - document.addEventListener("mousemove", move_callback); - document.addEventListener("mouseleave", up_callback); - document.addEventListener("blur", up_callback); - document.addEventListener("mouseup", up_callback); - document.body.style.userSelect = "none"; - }); - - this.events.on(["bot_change", "playlist_song_remove", "reorder_end"], event => { - if(event.type === "playlist_song_remove" && event.as<"playlist_song_remove">().song_id !== this.song_reorder.song_id) return; - - document.removeEventListener("mousemove", move_callback); - document.removeEventListener("mouseleave", up_callback); - document.removeEventListener("blur", up_callback); - document.removeEventListener("mouseup", up_callback); - document.body.style.userSelect = undefined; - - this.song_reorder.active = false; - this.song_reorder.indicator.remove(); - if(this.song_reorder.html) { - this.song_reorder.html.each((_, e) => { - e.style.width = null; - e.style.left = null; - e.style.top = null; - }); - this.song_reorder.html.removeClass("reordering"); - } - - if(event.type === "reorder_end") { - const data = event.as<"reorder_end">(); - if(data.canceled) return; - - const connection = this.handle.handle.serverConnection; - if(!connection || !connection.connected()) return; - if(!this._current_bot) return; - - connection.send_command("playlistsongreorder", { - playlist_id: this._current_bot.properties.client_playlist_id, - song_id: data.song_id, - song_previous_song_id: data.previous_entry - }).catch(error => { - if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) return; - - log.error(LogCategory.CLIENT, tr("Failed to add song to bot playlist: %o"), error); - - //TODO: Better error description - createErrorModal(tr("Failed to reorder song"), tr("Failed to reorder song within the playlist.")).open(); - }); - console.log("Reorder to %d", data.previous_entry); - } - }); - - this.events.on(["bot_change", "player_song_change"], event => { - if(!this._current_bot) { - this._html_tag.find(".playlist .current-song").removeClass("current-song"); - return; - } - - this._current_bot.requestPlayerInfo(1000).then(data => { - const song_id = data ? data.song_id : 0; - this._html_tag.find(".playlist .current-song").removeClass("current-song"); - this._html_tag.find(".playlist .entry[song-id=" + song_id + "]").addClass("current-song"); - }); - }); - } - } - - set_current_bot(client: MusicClientEntry | undefined, enforce?: boolean) { - if(client) client.updateClientVariables(); /* just to ensure */ - if(client === this._current_bot && (typeof(enforce) === "undefined" || !enforce)) - return; - - const old = this._current_bot; - this._current_bot = client; - this.events.fire("bot_change", { - new: client, - old: old - }); - } - - current_bot() : MusicClientEntry | undefined { - return this._current_bot; - } - - private sort_songs(data: PlaylistSong[]) { - const result = []; - - let appendable: PlaylistSong[] = []; - for(const song of data) { - if(song.song_id == 0 || data.findIndex(e => e.song_id === song.song_previous_song_id) == -1) - result.push(song); - else - appendable.push(song); - } - - let iters; - while (appendable.length) { - do { - iters = 0; - const left: PlaylistSong[] = []; - for(const song of appendable) { - const index = data.findIndex(e => e.song_id === song.song_previous_song_id); - if(index == -1) { - left.push(song); - continue; - } - - result.splice(index + 1, 0, song); - iters++; - } - appendable = left; - } while(iters > 0); - - if(appendable.length) - result.push(appendable.pop_front()); - } - - return result; - } - - /* playlist stuff */ - update_playlist() { - this.playlist_subscribe(true); - - this._container_playlist.find(".overlay").toggleClass("hidden", true); - const playlist = this._container_playlist.find(".playlist"); - playlist.empty(); - - if(!this.handle.handle.serverConnection || !this.handle.handle.serverConnection.connected() || !this._current_bot) { - this._container_playlist.find(".overlay-empty").removeClass("hidden"); - return; - } - - const overlay_loading = this._container_playlist.find(".overlay-loading"); - overlay_loading.removeClass("hidden"); - - this._current_bot.updateClientVariables(true).catch(error => { - log.warn(LogCategory.CLIENT, tr("Failed to update music bot variables: %o"), error); - }).then(() => { - this.handle.handle.serverConnection.command_helper.request_playlist_songs(this._current_bot.properties.client_playlist_id).then(songs => { - this.playlist_subscribe(false); /* we're allowed to see the playlist */ - if(!songs) { - this._container_playlist.find(".overlay-empty").removeClass("hidden"); - return; - } - - for(const song of this.sort_songs(songs)) - this.events.fire("playlist_song_add", { song: song, insert_effect: false }); - }).catch(error => { - if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) { - this._container_playlist.find(".overlay-no-permissions").removeClass("hidden"); - return; - } - log.error(LogCategory.CLIENT, tr("Failed to load bot playlist: %o"), error); - this._container_playlist.find(".overlay.overlay-error").removeClass("hidden"); - }).then(() => { - overlay_loading.addClass("hidden"); + this._current_bot.requestPlayerInfo(1000).then(data => { + const song_id = data ? data.song_id : 0; + this._html_tag.find(".playlist .current-song").removeClass("current-song"); + this._html_tag.find(".playlist .entry[song-id=" + song_id + "]").addClass("current-song"); }); }); } - - private _playlist_subscribed = false; - private playlist_subscribe(unsubscribe: boolean) { - if(!this.handle.handle.serverConnection) return; - - if(unsubscribe || !this._current_bot) { - if(!this._playlist_subscribed) return; - this._playlist_subscribed = false; - - this.handle.handle.serverConnection.send_command("playlistsetsubscription", {playlist_id: 0}).catch(error => { - log.warn(LogCategory.CLIENT, tr("Failed to unsubscribe from last playlist: %o"), error); - }); - } else { - this.handle.handle.serverConnection.send_command("playlistsetsubscription", { - playlist_id: this._current_bot.properties.client_playlist_id - }).then(() => this._playlist_subscribed = true).catch(error => { - log.warn(LogCategory.CLIENT, tr("Failed to subscribe to bots playlist: %o"), error); - }); - } - } - - private build_playlist_entry(data: PlaylistSong, insert_effect: boolean) : JQuery { - const tag = $("#tmpl_frame_music_playlist_entry").renderTag(); - tag.attr({ - "song-id": data.song_id, - "song-url": data.song_url - }); - - const thumbnail = tag.find(".container-thumbnail img"); - const name = tag.find(".name"); - const description = tag.find(".description"); - const length = tag.find(".length"); - - tag.find(".button-delete").on('click', () => this.events.fire("action_song_delete", { song_id: data.song_id })); - tag.find(".container-thumbnail").on('click', event => { - const target = tag.find(".container-thumbnail img"); - const url = target.attr("x-thumbnail-url"); - if(!url) return; - - image_preview.preview_image(decodeURIComponent(url), decodeURIComponent(url)); - }); - tag.on('dblclick', event => this.events.fire("action_song_set", { song_id: data.song_id })); - name.text(tr("loading...")); - description.text(data.song_url); - - tag.on('mousedown', event => { - if(event.button !== 0) return; - - this.song_reorder.mouse = { - x: event.pageX, - y: event.pageY - }; - - const baseOff = tag.offset(); - const off = { x: event.pageX - baseOff.left, y: event.pageY - baseOff.top }; - const move_listener = (event: MouseEvent) => { - const distance = Math.pow(event.pageX - this.song_reorder.mouse.x, 2) + Math.pow(event.pageY - this.song_reorder.mouse.y, 2); - if(distance < 50) return; - - document.removeEventListener("blur", up_listener); - document.removeEventListener("mouseup", up_listener); - document.removeEventListener("mousemove", move_listener); - - this.song_reorder.mouse = off; - this.events.fire("reorder_begin", { - entry: tag, - song_id: data.song_id - }); - }; - - const up_listener = event => { - if(event.type === "mouseup" && event.button !== 0) return; - - document.removeEventListener("blur", up_listener); - document.removeEventListener("mouseup", up_listener); - document.removeEventListener("mousemove", move_listener); - }; - - document.addEventListener("blur", up_listener); - document.addEventListener("mouseup", up_listener); - document.addEventListener("mousemove", move_listener); - }); - - if(this._current_bot) { - this._current_bot.requestPlayerInfo(60 * 1000).then(pdata => { - if(pdata.song_id === data.song_id) - tag.addClass("current-song"); - }); - } - - if(insert_effect) { - tag.removeClass("shown"); - tag.addClass("animation"); - } - return tag; - } + } + + set_current_bot(client: MusicClientEntry | undefined, enforce?: boolean) { + if(client) client.updateClientVariables(); /* just to ensure */ + if(client === this._current_bot && (typeof(enforce) === "undefined" || !enforce)) + return; + + const old = this._current_bot; + this._current_bot = client; + this.events.fire("bot_change", { + new: client, + old: old + }); + } + + current_bot() : MusicClientEntry | undefined { + return this._current_bot; + } + + private sort_songs(data: PlaylistSong[]) { + const result = []; + + let appendable: PlaylistSong[] = []; + for(const song of data) { + if(song.song_id == 0 || data.findIndex(e => e.song_id === song.song_previous_song_id) == -1) + result.push(song); + else + appendable.push(song); + } + + let iters; + while (appendable.length) { + do { + iters = 0; + const left: PlaylistSong[] = []; + for(const song of appendable) { + const index = data.findIndex(e => e.song_id === song.song_previous_song_id); + if(index == -1) { + left.push(song); + continue; + } + + result.splice(index + 1, 0, song); + iters++; + } + appendable = left; + } while(iters > 0); + + if(appendable.length) + result.push(appendable.pop_front()); + } + + return result; + } + + /* playlist stuff */ + update_playlist() { + this.playlist_subscribe(true); + + this._container_playlist.find(".overlay").toggleClass("hidden", true); + const playlist = this._container_playlist.find(".playlist"); + playlist.empty(); + + if(!this.handle.handle.serverConnection || !this.handle.handle.serverConnection.connected() || !this._current_bot) { + this._container_playlist.find(".overlay-empty").removeClass("hidden"); + return; + } + + const overlay_loading = this._container_playlist.find(".overlay-loading"); + overlay_loading.removeClass("hidden"); + + this._current_bot.updateClientVariables(true).catch(error => { + log.warn(LogCategory.CLIENT, tr("Failed to update music bot variables: %o"), error); + }).then(() => { + this.handle.handle.serverConnection.command_helper.request_playlist_songs(this._current_bot.properties.client_playlist_id).then(songs => { + this.playlist_subscribe(false); /* we're allowed to see the playlist */ + if(!songs) { + this._container_playlist.find(".overlay-empty").removeClass("hidden"); + return; + } + + for(const song of this.sort_songs(songs)) + this.events.fire("playlist_song_add", { song: song, insert_effect: false }); + }).catch(error => { + if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) { + this._container_playlist.find(".overlay-no-permissions").removeClass("hidden"); + return; + } + log.error(LogCategory.CLIENT, tr("Failed to load bot playlist: %o"), error); + this._container_playlist.find(".overlay.overlay-error").removeClass("hidden"); + }).then(() => { + overlay_loading.addClass("hidden"); + }); + }); + } + + private _playlist_subscribed = false; + private playlist_subscribe(unsubscribe: boolean) { + if(!this.handle.handle.serverConnection) return; + + if(unsubscribe || !this._current_bot) { + if(!this._playlist_subscribed) return; + this._playlist_subscribed = false; + + this.handle.handle.serverConnection.send_command("playlistsetsubscription", {playlist_id: 0}).catch(error => { + log.warn(LogCategory.CLIENT, tr("Failed to unsubscribe from last playlist: %o"), error); + }); + } else { + this.handle.handle.serverConnection.send_command("playlistsetsubscription", { + playlist_id: this._current_bot.properties.client_playlist_id + }).then(() => this._playlist_subscribed = true).catch(error => { + log.warn(LogCategory.CLIENT, tr("Failed to subscribe to bots playlist: %o"), error); + }); + } + } + + private build_playlist_entry(data: PlaylistSong, insert_effect: boolean) : JQuery { + const tag = $("#tmpl_frame_music_playlist_entry").renderTag(); + tag.attr({ + "song-id": data.song_id, + "song-url": data.song_url + }); + + const thumbnail = tag.find(".container-thumbnail img"); + const name = tag.find(".name"); + const description = tag.find(".description"); + const length = tag.find(".length"); + + tag.find(".button-delete").on('click', () => this.events.fire("action_song_delete", { song_id: data.song_id })); + tag.find(".container-thumbnail").on('click', event => { + const target = tag.find(".container-thumbnail img"); + const url = target.attr("x-thumbnail-url"); + if(!url) return; + + image_preview.preview_image(decodeURIComponent(url), decodeURIComponent(url)); + }); + tag.on('dblclick', event => this.events.fire("action_song_set", { song_id: data.song_id })); + name.text(tr("loading...")); + description.text(data.song_url); + + tag.on('mousedown', event => { + if(event.button !== 0) return; + + this.song_reorder.mouse = { + x: event.pageX, + y: event.pageY + }; + + const baseOff = tag.offset(); + const off = { x: event.pageX - baseOff.left, y: event.pageY - baseOff.top }; + const move_listener = (event: MouseEvent) => { + const distance = Math.pow(event.pageX - this.song_reorder.mouse.x, 2) + Math.pow(event.pageY - this.song_reorder.mouse.y, 2); + if(distance < 50) return; + + document.removeEventListener("blur", up_listener); + document.removeEventListener("mouseup", up_listener); + document.removeEventListener("mousemove", move_listener); + + this.song_reorder.mouse = off; + this.events.fire("reorder_begin", { + entry: tag, + song_id: data.song_id + }); + }; + + const up_listener = event => { + if(event.type === "mouseup" && event.button !== 0) return; + + document.removeEventListener("blur", up_listener); + document.removeEventListener("mouseup", up_listener); + document.removeEventListener("mousemove", move_listener); + }; + + document.addEventListener("blur", up_listener); + document.addEventListener("mouseup", up_listener); + document.addEventListener("mousemove", move_listener); + }); + + if(this._current_bot) { + this._current_bot.requestPlayerInfo(60 * 1000).then(pdata => { + if(pdata.song_id === data.song_id) + tag.addClass("current-song"); + }); + } + + if(insert_effect) { + tag.removeClass("shown"); + tag.addClass("animation"); + } + return tag; } } \ No newline at end of file diff --git a/shared/js/ui/frames/side/private_conversations.ts b/shared/js/ui/frames/side/private_conversations.ts index f85ffe7c..b702d07c 100644 --- a/shared/js/ui/frames/side/private_conversations.ts +++ b/shared/js/ui/frames/side/private_conversations.ts @@ -1,896 +1,904 @@ /* the bar on the right with the chats (Channel & Client) */ -namespace chat { - declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; - declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; +import {settings, Settings} from "tc-shared/settings"; +import {LogCategory} from "tc-shared/log"; +import {format, helpers} from "tc-shared/ui/frames/side/chat_helper"; +import {bbcode_chat} from "tc-shared/ui/frames/chat"; +import {Frame} from "tc-shared/ui/frames/chat_frame"; +import {ChatBox} from "tc-shared/ui/frames/side/chat_box"; +import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; +import * as log from "tc-shared/log"; +import * as htmltags from "tc-shared/ui/htmltags"; - export type PrivateConversationViewEntry = { - html_tag: JQuery; +declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; +declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; + +export type PrivateConversationViewEntry = { + html_tag: JQuery; +} + +export type PrivateConversationMessageData = { + message_id: string; + message: string; + sender: "self" | "partner"; + + sender_name: string; + sender_unique_id: string; + sender_client_id: number; + + timestamp: number; +}; + +export type PrivateConversationViewMessage = PrivateConversationMessageData & PrivateConversationViewEntry & { + time_update_id: number; +}; +export type PrivateConversationViewSpacer = PrivateConversationViewEntry; + +export enum PrivateConversationState { + OPEN, + CLOSED, + DISCONNECTED, + DISCONNECTED_SELF, +} + +export type DisplayedMessage = { + timestamp: number; + + message: PrivateConversationViewMessage | PrivateConversationViewEntry; + message_type: "spacer" | "message"; + + /* structure as following + 1. time pointer + 2. unread + 3. message + */ + tag_message: JQuery; + tag_unread: PrivateConversationViewSpacer | undefined; + tag_timepointer: PrivateConversationViewSpacer | undefined; +} + +export class PrivateConveration { + readonly handle: PrivateConverations; + private _html_entry_tag: JQuery; + private _message_history: PrivateConversationMessageData[] = []; + + private _callback_message: (text: string) => any; + + private _state: PrivateConversationState; + + private _last_message_updater_id: number; + private _last_typing: number = 0; + private _typing_timeout: number = 4000; + private _typing_timeout_task: number; + + _scroll_position: number | undefined; /* undefined to follow bottom | position for special stuff */ + _html_message_container: JQuery; /* only set when this chat is selected! */ + + client_unique_id: string; + client_id: number; + client_name: string; + + private _displayed_messages: DisplayedMessage[] = []; + private _displayed_messages_length: number = 500; + private _spacer_unread_message: DisplayedMessage; + + constructor(handle: PrivateConverations, client_unique_id: string, client_name: string, client_id: number) { + this.handle = handle; + this.client_name = client_name; + this.client_unique_id = client_unique_id; + this.client_id = client_id; + this._state = PrivateConversationState.OPEN; + + this._build_entry_tag(); + this.set_unread_flag(false); + + this.load_history(); } - export type PrivateConversationMessageData = { - message_id: string; - message: string; - sender: "self" | "partner"; + private history_key() { return this.handle.handle.handle.channelTree.server.properties.virtualserver_unique_identifier + "_" + this.client_unique_id; } + private load_history() { + helpers.history.load_history(this.history_key()).then((data: PrivateConversationMessageData[]) => { + if(!data) return; - sender_name: string; - sender_unique_id: string; - sender_client_id: number; - - timestamp: number; - }; - - export type PrivateConversationViewMessage = PrivateConversationMessageData & PrivateConversationViewEntry & { - time_update_id: number; - }; - export type PrivateConversationViewSpacer = PrivateConversationViewEntry; - - export enum PrivateConversationState { - OPEN, - CLOSED, - DISCONNECTED, - DISCONNECTED_SELF, - } - - export type DisplayedMessage = { - timestamp: number; - - message: PrivateConversationViewMessage | PrivateConversationViewEntry; - message_type: "spacer" | "message"; - - /* structure as following - 1. time pointer - 2. unread - 3. message - */ - tag_message: JQuery; - tag_unread: PrivateConversationViewSpacer | undefined; - tag_timepointer: PrivateConversationViewSpacer | undefined; - } - - export class PrivateConveration { - readonly handle: PrivateConverations; - private _html_entry_tag: JQuery; - private _message_history: PrivateConversationMessageData[] = []; - - private _callback_message: (text: string) => any; - - private _state: PrivateConversationState; - - private _last_message_updater_id: number; - private _last_typing: number = 0; - private _typing_timeout: number = 4000; - private _typing_timeout_task: number; - - _scroll_position: number | undefined; /* undefined to follow bottom | position for special stuff */ - _html_message_container: JQuery; /* only set when this chat is selected! */ - - client_unique_id: string; - client_id: number; - client_name: string; - - private _displayed_messages: DisplayedMessage[] = []; - private _displayed_messages_length: number = 500; - private _spacer_unread_message: DisplayedMessage; - - constructor(handle: PrivateConverations, client_unique_id: string, client_name: string, client_id: number) { - this.handle = handle; - this.client_name = client_name; - this.client_unique_id = client_unique_id; - this.client_id = client_id; - this._state = PrivateConversationState.OPEN; - - this._build_entry_tag(); - this.set_unread_flag(false); - - this.load_history(); - } - - private history_key() { return this.handle.handle.handle.channelTree.server.properties.virtualserver_unique_identifier + "_" + this.client_unique_id; } - private load_history() { - helpers.history.load_history(this.history_key()).then((data: PrivateConversationMessageData[]) => { - if(!data) return; - - const flag_unread = !!this._spacer_unread_message; - for(const message of data.slice(data.length > this._displayed_messages_length ? data.length - this._displayed_messages_length : 0)) { - this.append_message(message.message, { - type: message.sender, - name: message.sender_name, - unique_id: message.sender_unique_id, - client_id: message.sender_client_id - }, new Date(message.timestamp), false); - } - - if(!flag_unread) - this.set_unread_flag(false); - - this.fix_scroll(false); - this.save_history(); - }).catch(error => { - log.warn(LogCategory.CHAT, tr("Failed to load private conversation history for user %s on server %s: %o"), - this.client_unique_id, this.handle.handle.handle.channelTree.server.properties.virtualserver_unique_identifier, error); - }) - } - - private save_history() { - helpers.history.save_history(this.history_key(), this._message_history).catch(error => { - log.warn(LogCategory.CHAT, tr("Failed to save private conversation history for user %s on server %s: %o"), - this.client_unique_id, this.handle.handle.handle.channelTree.server.properties.virtualserver_unique_identifier, error); - }); - } - - entry_tag() : JQuery { - return this._html_entry_tag; - } - - destroy() { - this._html_message_container = undefined; /* we do not own this container */ - - this.clear_messages(false); - - this._html_entry_tag && this._html_entry_tag.remove(); - this._html_entry_tag = undefined; - - this._message_history = undefined; - if(this._typing_timeout_task) - clearTimeout(this._typing_timeout_task); - } - - private _2d_flat(array: T[][]) : T[] { - const result = []; - for(const a of array) - result.push(...a.filter(e => typeof(e) !== "undefined")); - return result; - } - - messages_tags() : JQuery[] { - return this._2d_flat(this._displayed_messages.slice().reverse().map(e => [ - e.tag_timepointer ? e.tag_timepointer.html_tag : undefined, - e.tag_unread ? e.tag_unread.html_tag : undefined, - e.tag_message - ])); - } - - append_message(message: string, sender: { - type: "self" | "partner"; - name: string; - unique_id: string; - client_id: number; - }, timestamp?: Date, save_history?: boolean) { - const message_date = timestamp || new Date(); - const message_timestamp = message_date.getTime(); - - const packed_message = { - message: message, - sender: sender.type, - sender_name: sender.name, - sender_client_id: sender.client_id, - sender_unique_id: sender.unique_id, - timestamp: message_date.getTime(), - message_id: 'undefined' - }; - - /* first of all register message in message history */ - { - let index = 0; - for(;index < this._message_history.length; index++) { - if(this._message_history[index].timestamp > message_timestamp) - continue; - this._message_history.splice(index, 0, packed_message); - break; - } - - if(index > 100) - return; /* message is too old to be displayed */ - - if(index >= this._message_history.length) - this._message_history.push(packed_message); - - while(this._message_history.length > 100) - this._message_history.pop(); + const flag_unread = !!this._spacer_unread_message; + for(const message of data.slice(data.length > this._displayed_messages_length ? data.length - this._displayed_messages_length : 0)) { + this.append_message(message.message, { + type: message.sender, + name: message.sender_name, + unique_id: message.sender_unique_id, + client_id: message.sender_client_id + }, new Date(message.timestamp), false); } - if(sender.type === "partner") { - clearTimeout(this._typing_timeout_task); - this._typing_timeout_task = 0; + if(!flag_unread) + this.set_unread_flag(false); - if(this.typing_active()) { - this._last_typing = 0; - this.typing_expired(); - } else { - this._update_message_timestamp(); - } + this.fix_scroll(false); + this.save_history(); + }).catch(error => { + log.warn(LogCategory.CHAT, tr("Failed to load private conversation history for user %s on server %s: %o"), + this.client_unique_id, this.handle.handle.handle.channelTree.server.properties.virtualserver_unique_identifier, error); + }) + } + + private save_history() { + helpers.history.save_history(this.history_key(), this._message_history).catch(error => { + log.warn(LogCategory.CHAT, tr("Failed to save private conversation history for user %s on server %s: %o"), + this.client_unique_id, this.handle.handle.handle.channelTree.server.properties.virtualserver_unique_identifier, error); + }); + } + + entry_tag() : JQuery { + return this._html_entry_tag; + } + + destroy() { + this._html_message_container = undefined; /* we do not own this container */ + + this.clear_messages(false); + + this._html_entry_tag && this._html_entry_tag.remove(); + this._html_entry_tag = undefined; + + this._message_history = undefined; + if(this._typing_timeout_task) + clearTimeout(this._typing_timeout_task); + } + + private _2d_flat(array: T[][]) : T[] { + const result = []; + for(const a of array) + result.push(...a.filter(e => typeof(e) !== "undefined")); + return result; + } + + messages_tags() : JQuery[] { + return this._2d_flat(this._displayed_messages.slice().reverse().map(e => [ + e.tag_timepointer ? e.tag_timepointer.html_tag : undefined, + e.tag_unread ? e.tag_unread.html_tag : undefined, + e.tag_message + ])); + } + + append_message(message: string, sender: { + type: "self" | "partner"; + name: string; + unique_id: string; + client_id: number; + }, timestamp?: Date, save_history?: boolean) { + const message_date = timestamp || new Date(); + const message_timestamp = message_date.getTime(); + + const packed_message = { + message: message, + sender: sender.type, + sender_name: sender.name, + sender_client_id: sender.client_id, + sender_unique_id: sender.unique_id, + timestamp: message_date.getTime(), + message_id: 'undefined' + }; + + /* first of all register message in message history */ + { + let index = 0; + for(;index < this._message_history.length; index++) { + if(this._message_history[index].timestamp > message_timestamp) + continue; + this._message_history.splice(index, 0, packed_message); + break; + } + + if(index > 100) + return; /* message is too old to be displayed */ + + if(index >= this._message_history.length) + this._message_history.push(packed_message); + + while(this._message_history.length > 100) + this._message_history.pop(); + } + + if(sender.type === "partner") { + clearTimeout(this._typing_timeout_task); + this._typing_timeout_task = 0; + + if(this.typing_active()) { + this._last_typing = 0; + this.typing_expired(); } else { this._update_message_timestamp(); } - - if(typeof(save_history) !== "boolean" || save_history) - this.save_history(); - - /* insert in view */ - { - const basic_view_entry = this._build_message(packed_message); - - this._register_displayed_message({ - timestamp: basic_view_entry.timestamp, - message: basic_view_entry, - message_type: "message", - tag_message: basic_view_entry.html_tag, - tag_timepointer: undefined, - tag_unread: undefined - }, true); - } - } - - private _displayed_message_first_tag(message: DisplayedMessage) { - const tp = message.tag_timepointer ? message.tag_timepointer.html_tag : undefined; - const tu = message.tag_unread ? message.tag_unread.html_tag : undefined; - return tp || tu || message.tag_message; - } - - private _destroy_displayed_message(message: DisplayedMessage, update_pointers: boolean) { - if(update_pointers) { - const index = this._displayed_messages.indexOf(message); - if(index != -1 && index > 0) { - const next = this._displayed_messages[index - 1]; - if(!next.tag_timepointer && message.tag_timepointer) { - next.tag_timepointer = message.tag_timepointer; - message.tag_timepointer = undefined; - } - if(!next.tag_unread && message.tag_unread) { - this._spacer_unread_message = next; - next.tag_unread = message.tag_unread; - message.tag_unread = undefined; - } - } - - if(message == this._spacer_unread_message) - this._spacer_unread_message = undefined; - } - - this._displayed_messages.remove(message); - if(message.tag_timepointer) - this._destroy_view_entry(message.tag_timepointer); - - if(message.tag_unread) - this._destroy_view_entry(message.tag_unread); - - this._destroy_view_entry(message.message); - } - - clear_messages(save?: boolean) { - this._message_history = []; - while(this._displayed_messages.length > 0) { - this._destroy_displayed_message(this._displayed_messages[0], false); - } - - this._spacer_unread_message = undefined; - - this._update_message_timestamp(); - if(save) - this.save_history(); - } - - fix_scroll(animate: boolean) { - if(!this._html_message_container) - return; - - let offset; - if(this._spacer_unread_message) { - offset = this._displayed_message_first_tag(this._spacer_unread_message)[0].offsetTop; - } else if(typeof(this._scroll_position) !== "undefined") { - offset = this._scroll_position; - } else { - offset = this._html_message_container[0].scrollHeight; - } - if(animate) { - this._html_message_container.stop(true).animate({ - scrollTop: offset - }, 'slow'); - } else { - this._html_message_container.stop(true).scrollTop(offset); - } - } - - private _update_message_timestamp() { - if(this._last_message_updater_id) - clearTimeout(this._last_message_updater_id); - - if(!this._html_entry_tag) - return; /* we got deleted, not need for updates */ - - if(this.typing_active()) { - this._html_entry_tag.find(".last-message").text(tr("currently typing...")); - return; - } - - const last_message = this._message_history[0]; - if(!last_message) { - this._html_entry_tag.find(".last-message").text(tr("no history")); - return; - } - - const timestamp = new Date(last_message.timestamp); - let time = format.date.format_chat_time(timestamp); - this._html_entry_tag.find(".last-message").text(time.result); - - if(time.next_update > 0) { - this._last_message_updater_id = setTimeout(() => this._update_message_timestamp(), time.next_update); - } else { - this._last_message_updater_id = 0; - } - } - - private _destroy_message(message: PrivateConversationViewMessage) { - if(message.time_update_id) - clearTimeout(message.time_update_id); - } - - private _build_message(message: PrivateConversationMessageData) : PrivateConversationViewMessage { - const result = message as PrivateConversationViewMessage; - if(result.html_tag) - return result; - - const timestamp = new Date(message.timestamp); - let time = format.date.format_chat_time(timestamp); - result.html_tag = $("#tmpl_frame_chat_private_message").renderTag({ - timestamp: time.result, - message_id: message.message_id, - client_name: htmltags.generate_client_object({ - add_braces: false, - client_name: message.sender_name, - client_unique_id: message.sender_unique_id, - client_id: message.sender_client_id - }), - message: MessageHelper.bbcode_chat(message.message), - avatar: this.handle.handle.handle.fileManager.avatars.generate_chat_tag({id: message.sender_client_id}, message.sender_unique_id) - }); - if(time.next_update > 0) { - const _updater = () => { - time = format.date.format_chat_time(timestamp); - result.html_tag.find(".info .timestamp").text(time.result); - if(time.next_update > 0) - result.time_update_id = setTimeout(_updater, time.next_update); - else - result.time_update_id = 0; - }; - result.time_update_id = setTimeout(_updater, time.next_update); - } else { - result.time_update_id = 0; - } - - return result; - } - - private _build_spacer(message: string, type: "date" | "new" | "disconnect" | "disconnect_self" | "reconnect" | "closed" | "error") : PrivateConversationViewSpacer { - const tag = $("#tmpl_frame_chat_private_spacer").renderTag({ - message: message - }).addClass("type-" + type); - return { - html_tag: tag - } - } - - private _register_displayed_message(message: DisplayedMessage, update_new: boolean) { - const message_date = new Date(message.timestamp); - - /* before := older message; after := newer message */ - let entry_before: DisplayedMessage, entry_after: DisplayedMessage; - let index = 0; - for(;index < this._displayed_messages.length; index++) { - if(this._displayed_messages[index].timestamp > message.timestamp) - continue; - - entry_after = index > 0 ? this._displayed_messages[index - 1] : undefined; - entry_before = this._displayed_messages[index]; - this._displayed_messages.splice(index, 0, message); - break; - } - if(index >= this._displayed_messages_length) { - return; /* message is out of view region */ - } - - if(index >= this._displayed_messages.length) { - entry_before = undefined; - entry_after = this._displayed_messages.last(); - this._displayed_messages.push(message); - } - - while(this._displayed_messages.length > this._displayed_messages_length) - this._destroy_displayed_message(this._displayed_messages.last(), true); - - const flag_new_message = update_new && index == 0 && (message.message_type === "spacer" || (message.message).sender === "partner"); - - /* Timeline for before - now */ - { - let append_pointer = false; - - if(entry_before) { - if(!helpers.date.same_day(message.timestamp, entry_before.timestamp)) { - append_pointer = true; - } - } else { - append_pointer = true; - } - if(append_pointer) { - const diff = format.date.date_format(message_date, new Date()); - if(diff == format.date.ColloquialFormat.YESTERDAY) - message.tag_timepointer = this._build_spacer(tr("Yesterday"), "date"); - else if(diff == format.date.ColloquialFormat.TODAY) - message.tag_timepointer = this._build_spacer(tr("Today"), "date"); - else if(diff == format.date.ColloquialFormat.GENERAL) - message.tag_timepointer = this._build_spacer(format.date.format_date_general(message_date, false), "date"); - } - } - - /* Timeline not and after */ - { - if(entry_after) { - if(helpers.date.same_day(message_date, entry_after.timestamp)) { - if(entry_after.tag_timepointer) { - this._destroy_view_entry(entry_after.tag_timepointer); - entry_after.tag_timepointer = undefined; - } - } else if(!entry_after.tag_timepointer) { - const diff = format.date.date_format(new Date(entry_after.timestamp), new Date()); - if(diff == format.date.ColloquialFormat.YESTERDAY) - entry_after.tag_timepointer = this._build_spacer(tr("Yesterday"), "date"); - else if(diff == format.date.ColloquialFormat.TODAY) - entry_after.tag_timepointer = this._build_spacer(tr("Today"), "date"); - else if(diff == format.date.ColloquialFormat.GENERAL) - entry_after.tag_timepointer = this._build_spacer(format.date.format_date_general(message_date, false), "date"); - - entry_after.tag_timepointer.html_tag.insertBefore(entry_after.tag_message); - } - } - } - - /* new message flag */ - if(flag_new_message) { - if(!this._spacer_unread_message) { - this._spacer_unread_message = message; - message.tag_unread = this._build_spacer(tr("Unread messages"), "new"); - - this.set_unread_flag(true); - } - } - - if(this._html_message_container) { - if(entry_before) { - message.tag_message.insertAfter(entry_before.tag_message); - } else if(entry_after) { - message.tag_message.insertBefore(this._displayed_message_first_tag(entry_after)); - } else { - this._html_message_container.append(message.tag_message); - } - - /* first time pointer */ - if(message.tag_timepointer) - message.tag_timepointer.html_tag.insertBefore(message.tag_message); - - /* the unread */ - if(message.tag_unread) - message.tag_unread.html_tag.insertBefore(message.tag_message); - } - - this.fix_scroll(true); - } - - private _destroy_view_entry(entry: PrivateConversationViewEntry) { - if(!entry.html_tag) - return; - entry.html_tag.remove(); - if('sender' in entry) - this._destroy_message(entry); - } - - private _build_entry_tag() { - this._html_entry_tag = $("#tmpl_frame_chat_private_entry").renderTag({ - client_name: this.client_name, - last_time: tr("error no timestamp"), - avatar: this.handle.handle.handle.fileManager.avatars.generate_chat_tag({id: this.client_id}, this.client_unique_id) - }); - this._html_entry_tag.on('click', event => { - if(event.isDefaultPrevented()) - return; - - this.handle.set_selected_conversation(this); - }); - this._html_entry_tag.find('.button-close').on('click', event => { - event.preventDefault(); - this.close_conversation(); - }); + } else { this._update_message_timestamp(); } - update_avatar() { - const container = this._html_entry_tag.find(".container-avatar"); - container.find(".avatar").remove(); - container.append(this.handle.handle.handle.fileManager.avatars.generate_chat_tag({id: this.client_id}, this.client_unique_id)); - } + if(typeof(save_history) !== "boolean" || save_history) + this.save_history(); - close_conversation() { - this.handle.delete_conversation(this, true); - } + /* insert in view */ + { + const basic_view_entry = this._build_message(packed_message); - set_client_name(name: string) { - if(this.client_name === name) - return; - this.client_name = name; - this._html_entry_tag.find(".client-name").text(name); - } - - set_unread_flag(flag: boolean, update_chat_counter?: boolean) { - /* unread message pointer */ - if(flag != (typeof(this._spacer_unread_message) !== "undefined")) { - if(flag) { - if(this._displayed_messages.length > 0) /* without messages we cant be unread */ - return; - - if(!this._spacer_unread_message) { - this._spacer_unread_message = this._displayed_messages[0]; - this._spacer_unread_message.tag_unread = this._build_spacer(tr("Unread messages"), "new"); - this._spacer_unread_message.tag_unread.html_tag.insertBefore(this._spacer_unread_message.tag_message); - } - } else { - const ctree = this.handle.handle.handle.channelTree; - if(ctree && ctree.tag_tree() && this.client_id) - ctree.tag_tree().find(".marker-text-unread[private-conversation='" + this.client_id + "']").addClass("hidden"); - - if(this._spacer_unread_message) { - this._destroy_view_entry(this._spacer_unread_message.tag_unread); - this._spacer_unread_message.tag_unread = undefined; - this._spacer_unread_message = undefined; - } - } - } - - /* general notify */ - this._html_entry_tag.toggleClass("unread", flag); - if(typeof(update_chat_counter) !== "boolean" || update_chat_counter) - this.handle.handle.info_frame().update_chat_counter(); - } - - is_unread() : boolean { return !!this._spacer_unread_message; } - - private _append_state_change(state: "disconnect" | "disconnect_self" | "reconnect" | "closed") { - let message; - if(state == "closed") - message = tr("Your chat partner has closed the conversation"); - else if(state == "reconnect") - message = this._state === PrivateConversationState.DISCONNECTED_SELF ?tr("You've reconnected to the server") : tr("Your chat partner has reconnected"); - else if(state === "disconnect") - message = tr("Your chat partner has disconnected"); - else - message = tr("You've disconnected from the server"); - - const spacer = this._build_spacer(message, state); this._register_displayed_message({ - timestamp: Date.now(), - message: spacer, - message_type: "spacer", - tag_message: spacer.html_tag, - tag_timepointer: undefined, - tag_unread: undefined - }, state === "disconnect"); - } - - state() : PrivateConversationState { - return this._state; - } - - set_state(state: PrivateConversationState) { - if(this._state == state) - return; - - if(state == PrivateConversationState.DISCONNECTED) { - this._append_state_change("disconnect"); - this.client_id = 0; - } else if(state == PrivateConversationState.OPEN && this._state != PrivateConversationState.CLOSED) - this._append_state_change("reconnect"); - else if(state == PrivateConversationState.CLOSED) - this._append_state_change("closed"); - else if(state == PrivateConversationState.DISCONNECTED_SELF) - this._append_state_change("disconnect_self"); - - this._state = state; - } - - set_text_callback(callback: (text: string) => any, update_enabled_state?: boolean) { - this._callback_message = callback; - if(typeof (update_enabled_state) !== "boolean" || update_enabled_state) - this.handle.update_chatbox_state(); - } - - chat_enabled() { - return typeof(this._callback_message) !== "undefined" && (this._state == PrivateConversationState.OPEN || this._state == PrivateConversationState.CLOSED); - } - - append_error(message: string, date?: number) { - const spacer = this._build_spacer(message, "error"); - this._register_displayed_message({ - timestamp: date || Date.now(), - message: spacer, - message_type: "spacer", - tag_message: spacer.html_tag, + timestamp: basic_view_entry.timestamp, + message: basic_view_entry, + message_type: "message", + tag_message: basic_view_entry.html_tag, tag_timepointer: undefined, tag_unread: undefined }, true); } + } - call_message(message: string) { - if(this._callback_message) - this._callback_message(message); - else { - log.warn(LogCategory.CHAT, tr("Dropping conversation message for client %o because of no message callback."), { - client_name: this.client_name, - client_id: this.client_id, - client_unique_id: this.client_unique_id - }); + private _displayed_message_first_tag(message: DisplayedMessage) { + const tp = message.tag_timepointer ? message.tag_timepointer.html_tag : undefined; + const tu = message.tag_unread ? message.tag_unread.html_tag : undefined; + return tp || tu || message.tag_message; + } + + private _destroy_displayed_message(message: DisplayedMessage, update_pointers: boolean) { + if(update_pointers) { + const index = this._displayed_messages.indexOf(message); + if(index != -1 && index > 0) { + const next = this._displayed_messages[index - 1]; + if(!next.tag_timepointer && message.tag_timepointer) { + next.tag_timepointer = message.tag_timepointer; + message.tag_timepointer = undefined; + } + if(!next.tag_unread && message.tag_unread) { + this._spacer_unread_message = next; + next.tag_unread = message.tag_unread; + message.tag_unread = undefined; + } } + + if(message == this._spacer_unread_message) + this._spacer_unread_message = undefined; } - private typing_expired() { - this._update_message_timestamp(); - if(this.handle.current_conversation() === this) - this.handle.update_typing_state(); + this._displayed_messages.remove(message); + if(message.tag_timepointer) + this._destroy_view_entry(message.tag_timepointer); + + if(message.tag_unread) + this._destroy_view_entry(message.tag_unread); + + this._destroy_view_entry(message.message); + } + + clear_messages(save?: boolean) { + this._message_history = []; + while(this._displayed_messages.length > 0) { + this._destroy_displayed_message(this._displayed_messages[0], false); } - trigger_typing() { - let _new = Date.now() - this._last_typing > this._typing_timeout; - this._last_typing = Date.now(); + this._spacer_unread_message = undefined; - if(this._typing_timeout_task) - clearTimeout(this._typing_timeout_task); + this._update_message_timestamp(); + if(save) + this.save_history(); + } - if(_new) - this._update_message_timestamp(); - if(this.handle.current_conversation() === this) - this.handle.update_typing_state(); + fix_scroll(animate: boolean) { + if(!this._html_message_container) + return; - this._typing_timeout_task = setTimeout(() => this.typing_expired(), this._typing_timeout); + let offset; + if(this._spacer_unread_message) { + offset = this._displayed_message_first_tag(this._spacer_unread_message)[0].offsetTop; + } else if(typeof(this._scroll_position) !== "undefined") { + offset = this._scroll_position; + } else { + offset = this._html_message_container[0].scrollHeight; } - - typing_active() { - return Date.now() - this._last_typing < this._typing_timeout; + if(animate) { + this._html_message_container.stop(true).animate({ + scrollTop: offset + }, 'slow'); + } else { + this._html_message_container.stop(true).scrollTop(offset); } } - export class PrivateConverations { - readonly handle: Frame; - private _chat_box: ChatBox; - private _html_tag: JQuery; + private _update_message_timestamp() { + if(this._last_message_updater_id) + clearTimeout(this._last_message_updater_id); - private _container_conversation: JQuery; - private _container_conversation_messages: JQuery; - private _container_conversation_list: JQuery; - private _container_typing: JQuery; + if(!this._html_entry_tag) + return; /* we got deleted, not need for updates */ - private _html_no_chats: JQuery; - private _conversations: PrivateConveration[] = []; + if(this.typing_active()) { + this._html_entry_tag.find(".last-message").text(tr("currently typing...")); + return; + } - private _current_conversation: PrivateConveration = undefined; - private _select_read_timer: number; + const last_message = this._message_history[0]; + if(!last_message) { + this._html_entry_tag.find(".last-message").text(tr("no history")); + return; + } - constructor(handle: Frame) { - this.handle = handle; - this._chat_box = new ChatBox(); - this._build_html_tag(); + const timestamp = new Date(last_message.timestamp); + let time = format.date.format_chat_time(timestamp); + this._html_entry_tag.find(".last-message").text(time.result); - this.update_chatbox_state(); - this.update_typing_state(); - this._chat_box.callback_text = message => { - if(!this._current_conversation) { - log.warn(LogCategory.CHAT, tr("Dropping conversation message because of no active conversation.")); - return; - } - this._current_conversation.call_message(message); + if(time.next_update > 0) { + this._last_message_updater_id = setTimeout(() => this._update_message_timestamp(), time.next_update); + } else { + this._last_message_updater_id = 0; + } + } + + private _destroy_message(message: PrivateConversationViewMessage) { + if(message.time_update_id) + clearTimeout(message.time_update_id); + } + + private _build_message(message: PrivateConversationMessageData) : PrivateConversationViewMessage { + const result = message as PrivateConversationViewMessage; + if(result.html_tag) + return result; + + const timestamp = new Date(message.timestamp); + let time = format.date.format_chat_time(timestamp); + result.html_tag = $("#tmpl_frame_chat_private_message").renderTag({ + timestamp: time.result, + message_id: message.message_id, + client_name: htmltags.generate_client_object({ + add_braces: false, + client_name: message.sender_name, + client_unique_id: message.sender_unique_id, + client_id: message.sender_client_id + }), + message: bbcode_chat(message.message), + avatar: this.handle.handle.handle.fileManager.avatars.generate_chat_tag({id: message.sender_client_id}, message.sender_unique_id) + }); + if(time.next_update > 0) { + const _updater = () => { + time = format.date.format_chat_time(timestamp); + result.html_tag.find(".info .timestamp").text(time.result); + if(time.next_update > 0) + result.time_update_id = setTimeout(_updater, time.next_update); + else + result.time_update_id = 0; }; + result.time_update_id = setTimeout(_updater, time.next_update); + } else { + result.time_update_id = 0; + } - this._chat_box.callback_typing = () => { - if(!this._current_conversation) { - log.warn(LogCategory.CHAT, tr("Dropping conversation typing action because of no active conversation.")); - return; + return result; + } + + private _build_spacer(message: string, type: "date" | "new" | "disconnect" | "disconnect_self" | "reconnect" | "closed" | "error") : PrivateConversationViewSpacer { + const tag = $("#tmpl_frame_chat_private_spacer").renderTag({ + message: message + }).addClass("type-" + type); + return { + html_tag: tag + } + } + + private _register_displayed_message(message: DisplayedMessage, update_new: boolean) { + const message_date = new Date(message.timestamp); + + /* before := older message; after := newer message */ + let entry_before: DisplayedMessage, entry_after: DisplayedMessage; + let index = 0; + for(;index < this._displayed_messages.length; index++) { + if(this._displayed_messages[index].timestamp > message.timestamp) + continue; + + entry_after = index > 0 ? this._displayed_messages[index - 1] : undefined; + entry_before = this._displayed_messages[index]; + this._displayed_messages.splice(index, 0, message); + break; + } + if(index >= this._displayed_messages_length) { + return; /* message is out of view region */ + } + + if(index >= this._displayed_messages.length) { + entry_before = undefined; + entry_after = this._displayed_messages.last(); + this._displayed_messages.push(message); + } + + while(this._displayed_messages.length > this._displayed_messages_length) + this._destroy_displayed_message(this._displayed_messages.last(), true); + + const flag_new_message = update_new && index == 0 && (message.message_type === "spacer" || (message.message).sender === "partner"); + + /* Timeline for before - now */ + { + let append_pointer = false; + + if(entry_before) { + if(!helpers.date.same_day(message.timestamp, entry_before.timestamp)) { + append_pointer = true; } - - const connection = this.handle.handle.serverConnection; - if(!connection || !connection.connected()) - return; - - connection.send_command("clientchatcomposing", { - clid: this._current_conversation.client_id - }); + } else { + append_pointer = true; + } + if(append_pointer) { + const diff = format.date.date_format(message_date, new Date()); + if(diff == format.date.ColloquialFormat.YESTERDAY) + message.tag_timepointer = this._build_spacer(tr("Yesterday"), "date"); + else if(diff == format.date.ColloquialFormat.TODAY) + message.tag_timepointer = this._build_spacer(tr("Today"), "date"); + else if(diff == format.date.ColloquialFormat.GENERAL) + message.tag_timepointer = this._build_spacer(format.date.format_date_general(message_date, false), "date"); } } - clear_client_ids() { - this._conversations.forEach(e => { - e.client_id = 0; - e.set_state(PrivateConversationState.DISCONNECTED_SELF); - }); + /* Timeline not and after */ + { + if(entry_after) { + if(helpers.date.same_day(message_date, entry_after.timestamp)) { + if(entry_after.tag_timepointer) { + this._destroy_view_entry(entry_after.tag_timepointer); + entry_after.tag_timepointer = undefined; + } + } else if(!entry_after.tag_timepointer) { + const diff = format.date.date_format(new Date(entry_after.timestamp), new Date()); + if(diff == format.date.ColloquialFormat.YESTERDAY) + entry_after.tag_timepointer = this._build_spacer(tr("Yesterday"), "date"); + else if(diff == format.date.ColloquialFormat.TODAY) + entry_after.tag_timepointer = this._build_spacer(tr("Today"), "date"); + else if(diff == format.date.ColloquialFormat.GENERAL) + entry_after.tag_timepointer = this._build_spacer(format.date.format_date_general(message_date, false), "date"); + + entry_after.tag_timepointer.html_tag.insertBefore(entry_after.tag_message); + } + } } - html_tag() : JQuery { return this._html_tag; } - destroy() { - this._chat_box && this._chat_box.destroy(); - this._chat_box = undefined; + /* new message flag */ + if(flag_new_message) { + if(!this._spacer_unread_message) { + this._spacer_unread_message = message; + message.tag_unread = this._build_spacer(tr("Unread messages"), "new"); - for(const conversation of this._conversations) - conversation.destroy(); - this._conversations = []; - this._current_conversation = undefined; + this.set_unread_flag(true); + } + } + if(this._html_message_container) { + if(entry_before) { + message.tag_message.insertAfter(entry_before.tag_message); + } else if(entry_after) { + message.tag_message.insertBefore(this._displayed_message_first_tag(entry_after)); + } else { + this._html_message_container.append(message.tag_message); + } + + /* first time pointer */ + if(message.tag_timepointer) + message.tag_timepointer.html_tag.insertBefore(message.tag_message); + + /* the unread */ + if(message.tag_unread) + message.tag_unread.html_tag.insertBefore(message.tag_message); + } + + this.fix_scroll(true); + } + + private _destroy_view_entry(entry: PrivateConversationViewEntry) { + if(!entry.html_tag) + return; + entry.html_tag.remove(); + if('sender' in entry) + this._destroy_message(entry); + } + + private _build_entry_tag() { + this._html_entry_tag = $("#tmpl_frame_chat_private_entry").renderTag({ + client_name: this.client_name, + last_time: tr("error no timestamp"), + avatar: this.handle.handle.handle.fileManager.avatars.generate_chat_tag({id: this.client_id}, this.client_unique_id) + }); + this._html_entry_tag.on('click', event => { + if(event.isDefaultPrevented()) + return; + + this.handle.set_selected_conversation(this); + }); + this._html_entry_tag.find('.button-close').on('click', event => { + event.preventDefault(); + this.close_conversation(); + }); + this._update_message_timestamp(); + } + + update_avatar() { + const container = this._html_entry_tag.find(".container-avatar"); + container.find(".avatar").remove(); + container.append(this.handle.handle.handle.fileManager.avatars.generate_chat_tag({id: this.client_id}, this.client_unique_id)); + } + + close_conversation() { + this.handle.delete_conversation(this, true); + } + + set_client_name(name: string) { + if(this.client_name === name) + return; + this.client_name = name; + this._html_entry_tag.find(".client-name").text(name); + } + + set_unread_flag(flag: boolean, update_chat_counter?: boolean) { + /* unread message pointer */ + if(flag != (typeof(this._spacer_unread_message) !== "undefined")) { + if(flag) { + if(this._displayed_messages.length > 0) /* without messages we cant be unread */ + return; + + if(!this._spacer_unread_message) { + this._spacer_unread_message = this._displayed_messages[0]; + this._spacer_unread_message.tag_unread = this._build_spacer(tr("Unread messages"), "new"); + this._spacer_unread_message.tag_unread.html_tag.insertBefore(this._spacer_unread_message.tag_message); + } + } else { + const ctree = this.handle.handle.handle.channelTree; + if(ctree && ctree.tag_tree() && this.client_id) + ctree.tag_tree().find(".marker-text-unread[private-conversation='" + this.client_id + "']").addClass("hidden"); + + if(this._spacer_unread_message) { + this._destroy_view_entry(this._spacer_unread_message.tag_unread); + this._spacer_unread_message.tag_unread = undefined; + this._spacer_unread_message = undefined; + } + } + } + + /* general notify */ + this._html_entry_tag.toggleClass("unread", flag); + if(typeof(update_chat_counter) !== "boolean" || update_chat_counter) + this.handle.handle.info_frame().update_chat_counter(); + } + + is_unread() : boolean { return !!this._spacer_unread_message; } + + private _append_state_change(state: "disconnect" | "disconnect_self" | "reconnect" | "closed") { + let message; + if(state == "closed") + message = tr("Your chat partner has closed the conversation"); + else if(state == "reconnect") + message = this._state === PrivateConversationState.DISCONNECTED_SELF ?tr("You've reconnected to the server") : tr("Your chat partner has reconnected"); + else if(state === "disconnect") + message = tr("Your chat partner has disconnected"); + else + message = tr("You've disconnected from the server"); + + const spacer = this._build_spacer(message, state); + this._register_displayed_message({ + timestamp: Date.now(), + message: spacer, + message_type: "spacer", + tag_message: spacer.html_tag, + tag_timepointer: undefined, + tag_unread: undefined + }, state === "disconnect"); + } + + state() : PrivateConversationState { + return this._state; + } + + set_state(state: PrivateConversationState) { + if(this._state == state) + return; + + if(state == PrivateConversationState.DISCONNECTED) { + this._append_state_change("disconnect"); + this.client_id = 0; + } else if(state == PrivateConversationState.OPEN && this._state != PrivateConversationState.CLOSED) + this._append_state_change("reconnect"); + else if(state == PrivateConversationState.CLOSED) + this._append_state_change("closed"); + else if(state == PrivateConversationState.DISCONNECTED_SELF) + this._append_state_change("disconnect_self"); + + this._state = state; + } + + set_text_callback(callback: (text: string) => any, update_enabled_state?: boolean) { + this._callback_message = callback; + if(typeof (update_enabled_state) !== "boolean" || update_enabled_state) + this.handle.update_chatbox_state(); + } + + chat_enabled() { + return typeof(this._callback_message) !== "undefined" && (this._state == PrivateConversationState.OPEN || this._state == PrivateConversationState.CLOSED); + } + + append_error(message: string, date?: number) { + const spacer = this._build_spacer(message, "error"); + this._register_displayed_message({ + timestamp: date || Date.now(), + message: spacer, + message_type: "spacer", + tag_message: spacer.html_tag, + tag_timepointer: undefined, + tag_unread: undefined + }, true); + } + + call_message(message: string) { + if(this._callback_message) + this._callback_message(message); + else { + log.warn(LogCategory.CHAT, tr("Dropping conversation message for client %o because of no message callback."), { + client_name: this.client_name, + client_id: this.client_id, + client_unique_id: this.client_unique_id + }); + } + } + + private typing_expired() { + this._update_message_timestamp(); + if(this.handle.current_conversation() === this) + this.handle.update_typing_state(); + } + + trigger_typing() { + let _new = Date.now() - this._last_typing > this._typing_timeout; + this._last_typing = Date.now(); + + if(this._typing_timeout_task) + clearTimeout(this._typing_timeout_task); + + if(_new) + this._update_message_timestamp(); + if(this.handle.current_conversation() === this) + this.handle.update_typing_state(); + + this._typing_timeout_task = setTimeout(() => this.typing_expired(), this._typing_timeout); + } + + typing_active() { + return Date.now() - this._last_typing < this._typing_timeout; + } +} + +export class PrivateConverations { + readonly handle: Frame; + private _chat_box: ChatBox; + private _html_tag: JQuery; + + private _container_conversation: JQuery; + private _container_conversation_messages: JQuery; + private _container_conversation_list: JQuery; + private _container_typing: JQuery; + + private _html_no_chats: JQuery; + private _conversations: PrivateConveration[] = []; + + private _current_conversation: PrivateConveration = undefined; + private _select_read_timer: number; + + constructor(handle: Frame) { + this.handle = handle; + this._chat_box = new ChatBox(); + this._build_html_tag(); + + this.update_chatbox_state(); + this.update_typing_state(); + this._chat_box.callback_text = message => { + if(!this._current_conversation) { + log.warn(LogCategory.CHAT, tr("Dropping conversation message because of no active conversation.")); + return; + } + this._current_conversation.call_message(message); + }; + + this._chat_box.callback_typing = () => { + if(!this._current_conversation) { + log.warn(LogCategory.CHAT, tr("Dropping conversation typing action because of no active conversation.")); + return; + } + + const connection = this.handle.handle.serverConnection; + if(!connection || !connection.connected()) + return; + + connection.send_command("clientchatcomposing", { + clid: this._current_conversation.client_id + }); + } + } + + clear_client_ids() { + this._conversations.forEach(e => { + e.client_id = 0; + e.set_state(PrivateConversationState.DISCONNECTED_SELF); + }); + } + + html_tag() : JQuery { return this._html_tag; } + destroy() { + this._chat_box && this._chat_box.destroy(); + this._chat_box = undefined; + + for(const conversation of this._conversations) + conversation.destroy(); + this._conversations = []; + this._current_conversation = undefined; + + clearTimeout(this._select_read_timer); + + this._html_tag && this._html_tag.remove(); + this._html_tag = undefined; + + } + + current_conversation() : PrivateConveration | undefined { return this._current_conversation; } + + conversations() : PrivateConveration[] { return this._conversations; } + create_conversation(client_uid: string, client_name: string, client_id: number) : PrivateConveration { + const conv = new PrivateConveration(this, client_uid, client_name, client_id); + this._conversations.push(conv); + this._html_no_chats.hide(); + + this._container_conversation_list.append(conv.entry_tag()); + this.handle.info_frame().update_chat_counter(); + return conv; + } + delete_conversation(conv: PrivateConveration, update_chat_couner?: boolean) { + if(!this._conversations.remove(conv)) + return; + //TODO: May animate? + conv.destroy(); + conv.clear_messages(false); + this._html_no_chats.toggle(this._conversations.length == 0); + if(conv === this._current_conversation) + this.set_selected_conversation(undefined); + if(update_chat_couner || typeof(update_chat_couner) !== "boolean") + this.handle.info_frame().update_chat_counter(); + } + find_conversation(partner: { name: string; unique_id: string; client_id: number }, mode: { create: boolean, attach: boolean }) : PrivateConveration | undefined { + for(const conversation of this.conversations()) + if(conversation.client_id == partner.client_id && (!partner.unique_id || conversation.client_unique_id == partner.unique_id)) { + if(conversation.state() != PrivateConversationState.OPEN) + conversation.set_state(PrivateConversationState.OPEN); + return conversation; + } + + let conv: PrivateConveration; + if(mode.attach) { + for(const conversation of this.conversations()) + if(conversation.client_unique_id == partner.unique_id && conversation.state() != PrivateConversationState.OPEN) { + conversation.set_state(PrivateConversationState.OPEN); + conversation.client_id = partner.client_id; + conversation.set_client_name(partner.name); + + conv = conversation; + break; + } + } + + if(mode.create && !conv) { + conv = this.create_conversation(partner.unique_id, partner.name, partner.client_id); + conv.client_id = partner.client_id; + conv.set_client_name(partner.name); + } + + if(conv) { + conv.set_text_callback(message => { + log.debug(LogCategory.CLIENT, tr("Sending text message %s to %o"), message, partner); + this.handle.handle.serverConnection.send_command("sendtextmessage", {"targetmode": 1, "target": partner.client_id, "msg": message}).catch(error => { + if(error instanceof CommandResult) { + if(error.id == ErrorID.CLIENT_INVALID_ID) { + conv.set_state(PrivateConversationState.DISCONNECTED); + conv.set_text_callback(undefined); + } else if(error.id == ErrorID.PERMISSION_ERROR) { + /* may notify for no permissions? */ + } else { + conv.append_error(tr("Failed to send message: ") + (error.extra_message || error.message)); + } + } else { + conv.append_error(tr("Failed to send message. Lookup the console for more details")); + log.error(LogCategory.CHAT, tr("Failed to send conversation message: %o"), error); + } + }); + }); + } + return conv; + } + + clear_conversations() { + while(this._conversations.length > 0) + this.delete_conversation(this._conversations[0], false); + this.handle.info_frame().update_chat_counter(); + } + + set_selected_conversation(conv: PrivateConveration | undefined) { + if(conv === this._current_conversation) + return; + + if(this._select_read_timer) clearTimeout(this._select_read_timer); - this._html_tag && this._html_tag.remove(); - this._html_tag = undefined; + if(this._current_conversation) + this._current_conversation._html_message_container = undefined; - } - - current_conversation() : PrivateConveration | undefined { return this._current_conversation; } - - conversations() : PrivateConveration[] { return this._conversations; } - create_conversation(client_uid: string, client_name: string, client_id: number) : PrivateConveration { - const conv = new PrivateConveration(this, client_uid, client_name, client_id); - this._conversations.push(conv); - this._html_no_chats.hide(); - - this._container_conversation_list.append(conv.entry_tag()); - this.handle.info_frame().update_chat_counter(); - return conv; - } - delete_conversation(conv: PrivateConveration, update_chat_couner?: boolean) { - if(!this._conversations.remove(conv)) - return; - //TODO: May animate? - conv.destroy(); - conv.clear_messages(false); - this._html_no_chats.toggle(this._conversations.length == 0); - if(conv === this._current_conversation) - this.set_selected_conversation(undefined); - if(update_chat_couner || typeof(update_chat_couner) !== "boolean") - this.handle.info_frame().update_chat_counter(); - } - find_conversation(partner: { name: string; unique_id: string; client_id: number }, mode: { create: boolean, attach: boolean }) : PrivateConveration | undefined { - for(const conversation of this.conversations()) - if(conversation.client_id == partner.client_id && (!partner.unique_id || conversation.client_unique_id == partner.unique_id)) { - if(conversation.state() != PrivateConversationState.OPEN) - conversation.set_state(PrivateConversationState.OPEN); - return conversation; - } - - let conv: PrivateConveration; - if(mode.attach) { - for(const conversation of this.conversations()) - if(conversation.client_unique_id == partner.unique_id && conversation.state() != PrivateConversationState.OPEN) { - conversation.set_state(PrivateConversationState.OPEN); - conversation.client_id = partner.client_id; - conversation.set_client_name(partner.name); - - conv = conversation; - break; - } - } - - if(mode.create && !conv) { - conv = this.create_conversation(partner.unique_id, partner.name, partner.client_id); - conv.client_id = partner.client_id; - conv.set_client_name(partner.name); - } - - if(conv) { - conv.set_text_callback(message => { - log.debug(LogCategory.CLIENT, tr("Sending text message %s to %o"), message, partner); - this.handle.handle.serverConnection.send_command("sendtextmessage", {"targetmode": 1, "target": partner.client_id, "msg": message}).catch(error => { - if(error instanceof CommandResult) { - if(error.id == ErrorID.CLIENT_INVALID_ID) { - conv.set_state(PrivateConversationState.DISCONNECTED); - conv.set_text_callback(undefined); - } else if(error.id == ErrorID.PERMISSION_ERROR) { - /* may notify for no permissions? */ - } else { - conv.append_error(tr("Failed to send message: ") + (error.extra_message || error.message)); - } - } else { - conv.append_error(tr("Failed to send message. Lookup the console for more details")); - log.error(LogCategory.CHAT, tr("Failed to send conversation message: %o", error)); - } - }); - }); - } - return conv; - } - - clear_conversations() { - while(this._conversations.length > 0) - this.delete_conversation(this._conversations[0], false); - this.handle.info_frame().update_chat_counter(); - } - - set_selected_conversation(conv: PrivateConveration | undefined) { - if(conv === this._current_conversation) - return; - - if(this._select_read_timer) - clearTimeout(this._select_read_timer); - - if(this._current_conversation) - this._current_conversation._html_message_container = undefined; - - this._container_conversation_list.find(".selected").removeClass("selected"); - this._container_conversation_messages.children().detach(); - this._current_conversation = conv; - if(!this._current_conversation) { - this.update_chatbox_state(); - return; - } - - this._current_conversation._html_message_container = this._container_conversation_messages; - const messages = this._current_conversation.messages_tags(); - /* TODO: Check if the messages are empty and display "No messages" */ - this._container_conversation_messages.append(...messages); - - if(this._current_conversation.is_unread() && false) { - this._select_read_timer = setTimeout(() => { - this._current_conversation.set_unread_flag(false, true); - }, 20 * 1000); /* Lets guess you've read the new messages within 5 seconds */ - } - this._current_conversation.fix_scroll(false); - this._current_conversation.entry_tag().addClass("selected"); + this._container_conversation_list.find(".selected").removeClass("selected"); + this._container_conversation_messages.children().detach(); + this._current_conversation = conv; + if(!this._current_conversation) { this.update_chatbox_state(); + return; } - update_chatbox_state() { - this._chat_box.set_enabled(!!this._current_conversation && this._current_conversation.chat_enabled()); + this._current_conversation._html_message_container = this._container_conversation_messages; + const messages = this._current_conversation.messages_tags(); + /* TODO: Check if the messages are empty and display "No messages" */ + this._container_conversation_messages.append(...messages); + + if(this._current_conversation.is_unread() && false) { + this._select_read_timer = setTimeout(() => { + this._current_conversation.set_unread_flag(false, true); + }, 20 * 1000); /* Lets guess you've read the new messages within 5 seconds */ } + this._current_conversation.fix_scroll(false); + this._current_conversation.entry_tag().addClass("selected"); + this.update_chatbox_state(); + } - update_typing_state() { - this._container_typing.toggleClass("hidden", !this._current_conversation || !this._current_conversation.typing_active()); - } + update_chatbox_state() { + this._chat_box.set_enabled(!!this._current_conversation && this._current_conversation.chat_enabled()); + } - private _build_html_tag() { - this._html_tag = $("#tmpl_frame_chat_private").renderTag({ - chatbox: this._chat_box.html_tag() - }).dividerfy(); - this._container_conversation = this._html_tag.find(".conversation"); - this._container_conversation.on('click', event => { /* lets think if a user clicks within that field that he has read the messages */ - if(this._current_conversation) - this._current_conversation.set_unread_flag(false, true); /* only updates everything if the state changes */ - }); + update_typing_state() { + this._container_typing.toggleClass("hidden", !this._current_conversation || !this._current_conversation.typing_active()); + } - this._container_conversation_messages = this._container_conversation.find(".messages"); - this._container_conversation_messages.on('scroll', event => { - if(!this._current_conversation) - return; - - const current_view = this._container_conversation_messages[0].scrollTop + this._container_conversation_messages[0].clientHeight + this._container_conversation_messages[0].clientHeight * .125; - if(current_view > this._container_conversation_messages[0].scrollHeight) - this._current_conversation._scroll_position = undefined; - else - this._current_conversation._scroll_position = this._container_conversation_messages[0].scrollTop; - }); - - this._container_conversation_list = this._html_tag.find(".conversation-list"); - this._html_no_chats = this._container_conversation_list.find(".no-chats"); - this._container_typing = this._html_tag.find(".container-typing"); - this.update_input_format_helper(); - } - - try_input_focus() { - this._chat_box.focus_input(); - } - - on_show() { + private _build_html_tag() { + this._html_tag = $("#tmpl_frame_chat_private").renderTag({ + chatbox: this._chat_box.html_tag() + }).dividerfy(); + this._container_conversation = this._html_tag.find(".conversation"); + this._container_conversation.on('click', event => { /* lets think if a user clicks within that field that he has read the messages */ if(this._current_conversation) - this._current_conversation.fix_scroll(false); - } + this._current_conversation.set_unread_flag(false, true); /* only updates everything if the state changes */ + }); - update_input_format_helper() { - const tag = this._html_tag.find(".container-format-helper"); - if(settings.static_global(Settings.KEY_CHAT_ENABLE_MARKDOWN)) { - tag.removeClass("hidden").text(tr("*italic*, **bold**, ~~strikethrough~~, `code`, and more...")); - } else { - tag.addClass("hidden"); - } + this._container_conversation_messages = this._container_conversation.find(".messages"); + this._container_conversation_messages.on('scroll', event => { + if(!this._current_conversation) + return; + + const current_view = this._container_conversation_messages[0].scrollTop + this._container_conversation_messages[0].clientHeight + this._container_conversation_messages[0].clientHeight * .125; + if(current_view > this._container_conversation_messages[0].scrollHeight) + this._current_conversation._scroll_position = undefined; + else + this._current_conversation._scroll_position = this._container_conversation_messages[0].scrollTop; + }); + + this._container_conversation_list = this._html_tag.find(".conversation-list"); + this._html_no_chats = this._container_conversation_list.find(".no-chats"); + this._container_typing = this._html_tag.find(".container-typing"); + this.update_input_format_helper(); + } + + try_input_focus() { + this._chat_box.focus_input(); + } + + on_show() { + if(this._current_conversation) + this._current_conversation.fix_scroll(false); + } + + update_input_format_helper() { + const tag = this._html_tag.find(".container-format-helper"); + if(settings.static_global(Settings.KEY_CHAT_ENABLE_MARKDOWN)) { + tag.removeClass("hidden").text(tr("*italic*, **bold**, ~~strikethrough~~, `code`, and more...")); + } else { + tag.addClass("hidden"); } } } \ No newline at end of file diff --git a/shared/js/ui/htmltags.ts b/shared/js/ui/htmltags.ts index b208043f..c9180c0a 100644 --- a/shared/js/ui/htmltags.ts +++ b/shared/js/ui/htmltags.ts @@ -1,253 +1,236 @@ -namespace htmltags { - let mouse_coordinates: {x: number, y: number} = {x: 0, y: 0}; +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import {ChannelEntry} from "tc-shared/ui/channel"; +import {ClientEntry} from "tc-shared/ui/client"; +import {htmlEscape} from "tc-shared/ui/frames/chat"; +import {server_connections} from "tc-shared/ui/frames/connection_handlers"; + +let mouse_coordinates: {x: number, y: number} = {x: 0, y: 0}; + +function initialize() { + document.addEventListener('mousemove', event => { + mouse_coordinates.x = event.pageX; + mouse_coordinates.y = event.pageY; + }); +} +initialize(); + +export interface ClientProperties { + client_id: number, + client_unique_id: string, + client_name: string, + add_braces?: boolean, + client_database_id?: number; /* not yet used */ +} + +export interface ChannelProperties { + channel_id: number, + channel_name: string, + channel_display_name?: string, + add_braces?: boolean +} + +/* required for the bbcodes */ +function generate_client_open(properties: ClientProperties) : string { + let result = ""; + + /* build the opening tag:
*/ + result = result + "
"; + return result; +} + +export function generate_client(properties: ClientProperties) : string { + let result = generate_client_open(properties); + /* content */ + { + if(properties.add_braces) + result = result + "\""; + + result = result + htmlEscape(properties.client_name || "undefined").join(" "); + if(properties.add_braces) + result = result + "\""; + } + + /* close tag */ + { + result += "
"; + } + return result; +} + +export function generate_client_object(properties: ClientProperties) : JQuery { + return $(this.generate_client(properties)); +} + +/* required for the bbcodes */ +function generate_channel_open(properties: ChannelProperties) : string { + let result = ""; + + /* build the opening tag:
*/ + result = result + "
"; + return result; +} + +export function generate_channel(properties: ChannelProperties) : string { + let result = generate_channel_open(properties); + /* content */ + { + if(properties.add_braces) + result = result + "\""; + result = result + htmlEscape(properties.channel_display_name || properties.channel_name || "undefined").join(" "); + if(properties.add_braces) + result = result + "\""; + } + + /* close tag */ + { + result += "
"; + } + return result; +} + +export function generate_channel_object(properties: ChannelProperties) : JQuery { + return $(this.generate_channel(properties)); +} + + +export namespace callbacks { + export function callback_context_client(element: JQuery) { + const client_id = parseInt(element.attr("client-id") || "0"); + const client_unique_id = decodeURIComponent(element.attr("client-unique-id") || ""); + /* we ignore the name, we cant find clients by name because the name is too volatile*/ + + let client: ClientEntry; + + const current_connection = server_connections.active_connection_handler(); + if(current_connection && current_connection.channelTree) { + if(!client && client_id) { + client = current_connection.channelTree.findClient(client_id); + if(client && (client_unique_id && client.properties.client_unique_identifier != client_unique_id)) { + client = undefined; /* client id dosn't match anymore, lets search for the unique id */ + } + } + if(!client && client_unique_id) + client = current_connection.channelTree.find_client_by_unique_id(client_unique_id); + + if(!client) { + if(current_connection.channelTree.server.properties.virtualserver_unique_identifier === client_unique_id) { + current_connection.channelTree.server.spawnContextMenu(mouse_coordinates.x, mouse_coordinates.y); + return; + } + } + } + if(!client) { + + /* we may should open a "offline" menu? */ + log.debug(LogCategory.GENERAL, "Failed to resolve client from html tag. Client id: %o, Client unique id: %o, Client name: %o", + client_id, + client_unique_id, + decodeURIComponent(element.attr("client-name")) + ); + return false; + } + + client.showContextMenu(mouse_coordinates.x, mouse_coordinates.y); + return false; + } + + export function callback_context_channel(element: JQuery) { + const channel_id = parseInt(element.attr("channel-id") || "0"); + + const current_connection = server_connections.active_connection_handler(); + let channel: ChannelEntry; + if(current_connection && current_connection.channelTree) { + channel = current_connection.channelTree.findChannel(channel_id); + } + + if(!channel) + return false; + + channel.showContextMenu(mouse_coordinates.x, mouse_coordinates.y); + return false; + } +} + +declare const xbbcode; +namespace bbcodes { + /* the = because we sometimes get that */ + //const url_client_regex = /?client:\/\/(?[0-9]+)\/(?[a-zA-Z0-9+=#]+)~(?(?:[^%]|%[0-9A-Fa-f]{2})+)$/g; + const url_client_regex = /client:\/\/([0-9]+)\/([a-zA-Z0-9+=/#]+)~((?:[^%]|%[0-9A-Fa-f]{2})+)$/g; /* IDK which browsers already support group naming */ + const url_channel_regex = /channel:\/\/([0-9]+)~((?:[^%]|%[0-9A-Fa-f]{2})+)$/g; function initialize() { - document.addEventListener('mousemove', event => { - mouse_coordinates.x = event.pageX; - mouse_coordinates.y = event.pageY; + const origin_url = xbbcode.register.find_parser('url'); + xbbcode.register.register_parser({ + tag: 'url', + build_html_tag_open(layer): string { + if(layer.options) { + if(layer.options.match(url_channel_regex)) { + const groups = url_channel_regex.exec(layer.options); + + return generate_channel_open({ + add_braces: false, + channel_id: parseInt(groups[1]), + channel_name: decodeURIComponent(groups[2]) + }); + } else if(layer.options.match(url_client_regex)) { + const groups = url_client_regex.exec(layer.options); + + return generate_client_open({ + add_braces: false, + client_id: parseInt(groups[1]), + client_unique_id: groups[2], + client_name: decodeURIComponent(groups[3]) + }); + } + } + return origin_url.build_html_tag_open(layer); + }, + build_html_tag_close(layer): string { + if(layer.options) { + if(layer.options.match(url_client_regex)) + return "
"; + if(layer.options.match(url_channel_regex)) + return "
"; + } + return origin_url.build_html_tag_close(layer); + } }); } initialize(); - - export interface ClientProperties { - client_id: number, - client_unique_id: string, - client_name: string, - add_braces?: boolean, - client_database_id?: number; /* not yet used */ - } - - export interface ChannelProperties { - channel_id: number, - channel_name: string, - channel_display_name?: string, - add_braces?: boolean - } - - /* required for the bbcodes */ - function generate_client_open(properties: ClientProperties) : string { - let result = ""; - - /* build the opening tag:
*/ - result = result + "
"; - return result; - } - - export function generate_client(properties: ClientProperties) : string { - let result = generate_client_open(properties); - /* content */ - { - if(properties.add_braces) - result = result + "\""; - - result = result + MessageHelper.htmlEscape(properties.client_name || "undefined").join(" "); - if(properties.add_braces) - result = result + "\""; - } - - /* close tag */ - { - result += "
"; - } - return result; - } - - export function generate_client_object(properties: ClientProperties) : JQuery { - return $(this.generate_client(properties)); - } - - /* required for the bbcodes */ - function generate_channel_open(properties: ChannelProperties) : string { - let result = ""; - - /* build the opening tag:
*/ - result = result + "
"; - return result; - } - - export function generate_channel(properties: ChannelProperties) : string { - let result = generate_channel_open(properties); - /* content */ - { - if(properties.add_braces) - result = result + "\""; - result = result + MessageHelper.htmlEscape(properties.channel_display_name || properties.channel_name || "undefined").join(" "); - if(properties.add_braces) - result = result + "\""; - } - - /* close tag */ - { - result += "
"; - } - return result; - } - - export function generate_channel_object(properties: ChannelProperties) : JQuery { - return $(this.generate_channel(properties)); - } - - - export namespace callbacks { - export function callback_context_client(element: JQuery) { - const client_id = parseInt(element.attr("client-id") || "0"); - const client_unique_id = decodeURIComponent(element.attr("client-unique-id") || ""); - /* we ignore the name, we cant find clients by name because the name is too volatile*/ - - let client: ClientEntry; - - const current_connection = server_connections.active_connection_handler(); - if(current_connection && current_connection.channelTree) { - if(!client && client_id) { - client = current_connection.channelTree.findClient(client_id); - if(client && (client_unique_id && client.properties.client_unique_identifier != client_unique_id)) { - client = undefined; /* client id dosn't match anymore, lets search for the unique id */ - } - } - if(!client && client_unique_id) - client = current_connection.channelTree.find_client_by_unique_id(client_unique_id); - - if(!client) { - if(current_connection.channelTree.server.properties.virtualserver_unique_identifier === client_unique_id) { - current_connection.channelTree.server.spawnContextMenu(mouse_coordinates.x, mouse_coordinates.y); - return; - } - } - } - if(!client) { - - /* we may should open a "offline" menu? */ - log.debug(LogCategory.GENERAL, "Failed to resolve client from html tag. Client id: %o, Client unique id: %o, Client name: %o", - client_id, - client_unique_id, - decodeURIComponent(element.attr("client-name")) - ); - return false; - } - - client.showContextMenu(mouse_coordinates.x, mouse_coordinates.y); - return false; - } - - export function callback_context_channel(element: JQuery) { - const channel_id = parseInt(element.attr("channel-id") || "0"); - - const current_connection = server_connections.active_connection_handler(); - let channel: ChannelEntry; - if(current_connection && current_connection.channelTree) { - channel = current_connection.channelTree.findChannel(channel_id); - } - - if(!channel) - return false; - - channel.showContextMenu(mouse_coordinates.x, mouse_coordinates.y); - return false; - } - } - - namespace bbcodes { - /* the = because we sometimes get that */ - //const url_client_regex = /?client:\/\/(?[0-9]+)\/(?[a-zA-Z0-9+=#]+)~(?(?:[^%]|%[0-9A-Fa-f]{2})+)$/g; - const url_client_regex = /client:\/\/([0-9]+)\/([a-zA-Z0-9+=/#]+)~((?:[^%]|%[0-9A-Fa-f]{2})+)$/g; /* IDK which browsers already support group naming */ - const url_channel_regex = /channel:\/\/([0-9]+)~((?:[^%]|%[0-9A-Fa-f]{2})+)$/g; - - function initialize() { - const origin_url = xbbcode.register.find_parser('url'); - xbbcode.register.register_parser({ - tag: 'url', - build_html_tag_open(layer): string { - if(layer.options) { - if(layer.options.match(url_channel_regex)) { - const groups = url_channel_regex.exec(layer.options); - - return generate_channel_open({ - add_braces: false, - channel_id: parseInt(groups[1]), - channel_name: decodeURIComponent(groups[2]) - }); - } else if(layer.options.match(url_client_regex)) { - const groups = url_client_regex.exec(layer.options); - - return generate_client_open({ - add_braces: false, - client_id: parseInt(groups[1]), - client_unique_id: groups[2], - client_name: decodeURIComponent(groups[3]) - }); - } - } - return origin_url.build_html_tag_open(layer); - }, - build_html_tag_close(layer): string { - if(layer.options) { - if(layer.options.match(url_client_regex)) - return "
"; - if(layer.options.match(url_channel_regex)) - return "
"; - } - return origin_url.build_html_tag_close(layer); - } - }) - /* - "img": { - openTag: function(params,content) { - let myUrl; - - if (!params) { - myUrl = content.replace(/<.*?>/g,""); - } else { - myUrl = params.substr(1); - } - - urlPattern.lastIndex = 0; - if ( !urlPattern.test( myUrl ) ) { - myUrl = "#"; - } - - return ''; - }, - closeTag: function(params,content) { - return ''; - } - }, - */ - } - initialize(); - } } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalAbout.ts b/shared/js/ui/modal/ModalAbout.ts index 39207f1c..693c5068 100644 --- a/shared/js/ui/modal/ModalAbout.ts +++ b/shared/js/ui/modal/ModalAbout.ts @@ -1,55 +1,54 @@ -/// -/// -/// +import {createModal} from "tc-shared/ui/elements/Modal"; +import * as loader from "tc-loader"; +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; -namespace Modals { - function format_date(date: number) { - const d = new Date(date); +function format_date(date: number) { + const d = new Date(date); - return ('00' + d.getDay()).substr(-2) + "." + ('00' + d.getMonth()).substr(-2) + "." + d.getFullYear() + " - " + ('00' + d.getHours()).substr(-2) + ":" + ('00' + d.getMinutes()).substr(-2); - } + return ('00' + d.getDay()).substr(-2) + "." + ('00' + d.getMonth()).substr(-2) + "." + d.getFullYear() + " - " + ('00' + d.getHours()).substr(-2) + ":" + ('00' + d.getMinutes()).substr(-2); +} - export function spawnAbout() { - const app_version = (() => { - const version_node = document.getElementById("app_version"); - if(!version_node) return undefined; +export function spawnAbout() { + const app_version = (() => { + const version_node = document.getElementById("app_version"); + if(!version_node) return undefined; - const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined; - if(!version) return undefined; + const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined; + if(!version) return undefined; - if(version == "unknown" || version.replace(/0+/, "").length == 0) - return undefined; + if(version == "unknown" || version.replace(/0+/, "").length == 0) + return undefined; - return version; - })(); + return version; + })(); - const connectModal = createModal({ - header: tr("About"), - body: () => { - let tag = $("#tmpl_about").renderTag({ - client: !app.is_web(), + const connectModal = createModal({ + header: tr("About"), + body: () => { + let tag = $("#tmpl_about").renderTag({ + client: loader.version().type !== "web", - version_client: app.is_web() ? app_version || "in-dev" : "loading...", - version_ui: app_version || "in-dev", + version_client: loader.version().type === "web" ? app_version || "in-dev" : "loading...", + version_ui: app_version || "in-dev", - version_timestamp: !!app_version ? format_date(Date.now()) : "--" - }); - return tag; - }, - footer: null, - - width: "60em" - }); - connectModal.htmlTag.find(".modal-body").addClass("modal-about"); - connectModal.open(); - - if(!app.is_web()) { - (window as any).native.client_version().then(version => { - connectModal.htmlTag.find(".version-client").text(version); - }).catch(error => { - log.error(LogCategory.GENERAL, tr("Failed to load client version: %o"), error); - connectModal.htmlTag.find(".version-client").text("unknown"); + version_timestamp: !!app_version ? format_date(Date.now()) : "--" }); - } + return tag; + }, + footer: null, + + width: "60em" + }); + connectModal.htmlTag.find(".modal-body").addClass("modal-about"); + connectModal.open(); + + if(loader.version().type !== "web") { + (window as any).native.client_version().then(version => { + connectModal.htmlTag.find(".version-client").text(version); + }).catch(error => { + log.error(LogCategory.GENERAL, tr("Failed to load client version: %o"), error); + connectModal.htmlTag.find(".version-client").text("unknown"); + }); } } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalAvatar.ts b/shared/js/ui/modal/ModalAvatar.ts index c5fcac24..9a9fdf71 100644 --- a/shared/js/ui/modal/ModalAvatar.ts +++ b/shared/js/ui/modal/ModalAvatar.ts @@ -1,74 +1,72 @@ -/// -/// -/// +//TODO: Test if we could render this image and not only the browser by knowing the type. +import {createErrorModal, createModal} from "tc-shared/ui/elements/Modal"; +import {tra} from "tc-shared/i18n/localize"; +import {arrayBufferBase64} from "tc-shared/utils/buffers"; -namespace Modals { - //TODO: Test if we could render this image and not only the browser by knowing the type. - 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({}); - } +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 => { + console.log("Files: %o", input_node.files); + + const read_file = (file: File) => new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = error => reject(error); + + reader.readAsDataURL(file); }); - let _data_submitted = false; - let _current_avatar; + (async () => { + const data = await read_file(input_node.files[0]); - 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) + if(!data.startsWith("data:image/")) { + console.error(tr("Failed to load file %s: Invalid data media type (%o)"), input_node.files[0].name, data); + createErrorModal(tr("Icon upload failed"), tra("Failed to select avatar {}.
File is not an image", input_node.files[0].name)).open(); return; - _data_submitted = true; - modal.close(); - callback_data(null); - }); + } + const semi = data.indexOf(';'); + const type = data.substring(11, semi); + console.log(tr("Given image has type %s"), type); - 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 => { - console.log("Files: %o", input_node.files); - - const read_file = (file: File) => new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result as string); - reader.onerror = error => reject(error); - - reader.readAsDataURL(file); - }); - - (async () => { - const data = await read_file(input_node.files[0]); - - if(!data.startsWith("data:image/")) { - console.error(tr("Failed to load file %s: Invalid data media type (%o)"), input_node.files[0].name, data); - createErrorModal(tr("Icon upload failed"), tra("Failed to select avatar {}.
File is not an image", input_node.files[0].name)).open(); - return; - } - const semi = data.indexOf(';'); - const type = data.substring(11, semi); - console.log(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(); - } + set_avatar(data.substr(semi + 8 /* 8 bytes := base64, */), type); + })(); + }); + set_avatar(undefined); + modal.close_listener.push(() => !_data_submitted && callback_data(undefined)); + modal.open(); } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalAvatarList.ts b/shared/js/ui/modal/ModalAvatarList.ts index 021430f2..1131c40e 100644 --- a/shared/js/ui/modal/ModalAvatarList.ts +++ b/shared/js/ui/modal/ModalAvatarList.ts @@ -1,162 +1,166 @@ -/// -/// -/// +import {createErrorModal, createModal} from "tc-shared/ui/elements/Modal"; +import {LogCategory} from "tc-shared/log"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {base64_encode_ab} from "tc-shared/utils/buffers"; +import {media_image_type} from "tc-shared/FileManager"; +import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; +import {ClientEntry} from "tc-shared/ui/client"; +import * as log from "tc-shared/log"; -namespace Modals { - const avatar_to_uid = (id: string) => { - const buffer = new Uint8Array(id.length / 2); - for(let index = 0; index < id.length; index += 2) { - const upper_nibble = id.charCodeAt(index) - 97; - const lower_nibble = id.charCodeAt(index + 1) - 97; - buffer[index / 2] = (upper_nibble << 4) | lower_nibble; +const avatar_to_uid = (id: string) => { + const buffer = new Uint8Array(id.length / 2); + for(let index = 0; index < id.length; index += 2) { + const upper_nibble = id.charCodeAt(index) - 97; + const lower_nibble = id.charCodeAt(index + 1) - 97; + buffer[index / 2] = (upper_nibble << 4) | lower_nibble; + } + return base64_encode_ab(buffer); +}; + +export const human_file_size = (size: number) => { + if(size < 1000) + return size + "B"; + const exp = Math.floor(Math.log2(size) / 10); + return (size / Math.pow(1024, exp)).toFixed(2) + 'KMGTPE'.charAt(exp - 1) + "iB"; +}; + +declare const moment; +export function spawnAvatarList(client: ConnectionHandler) { + const modal = createModal({ + header: tr("Avatars"), + footer: undefined, + body: () => { + const template = $("#tmpl_avatar_list").renderTag({}); + + return template; } - return base64_encode_ab(buffer); - }; + }); - export const human_file_size = (size: number) => { - if(size < 1000) - return size + "B"; - const exp = Math.floor(Math.log2(size) / 10); - return (size / Math.pow(1024, exp)).toFixed(2) + 'KMGTPE'.charAt(exp - 1) + "iB"; - }; + let callback_download: () => any; + let callback_delete: () => any; - export function spawnAvatarList(client: ConnectionHandler) { - const modal = createModal({ - header: tr("Avatars"), - footer: undefined, - body: () => { - const template = $("#tmpl_avatar_list").renderTag({}); + const button_download = modal.htmlTag.find(".button-download"); + const button_delete = modal.htmlTag.find(".button-delete"); + const container_list = modal.htmlTag.find(".container-list .list-entries-container"); + const list_entries = container_list.find(".list-entries"); + const container_info = modal.htmlTag.find(".container-info"); + const overlay_no_user = container_info.find(".disabled-overlay").show(); - return template; + const set_selected_avatar = (unique_id: string, avatar_id: string, size: number) => { + button_download.prop("disabled", true); + callback_download = undefined; + if(!unique_id) { + overlay_no_user.show(); + return; + } + + const tag_username = container_info.find(".property-username"); + const tag_unique_id = container_info.find(".property-unique-id"); + const tag_avatar_id = container_info.find(".property-avatar-id"); + const container_avatar = container_info.find(".container-image"); + const tag_image_bytes = container_info.find(".property-image-size"); + const tag_image_width = container_info.find(".property-image-width").val(tr("loading...")); + const tag_image_height = container_info.find(".property-image-height").val(tr("loading...")); + const tag_image_type = container_info.find(".property-image-type").val(tr("loading...")); + + tag_username.val("unknown"); + tag_unique_id.val(unique_id); + tag_avatar_id.val(avatar_id); + tag_image_bytes.val(size); + + container_avatar.empty().append(client.fileManager.avatars.generate_tag(avatar_id, undefined, { + callback_image: image => { + tag_image_width.val(image[0].naturalWidth + 'px'); + tag_image_height.val(image[0].naturalHeight + 'px'); + }, + callback_avatar: avatar => { + tag_image_type.val(media_image_type(avatar.type)); + button_download.prop("disabled", false); + + callback_download = () => { + const element = $.spawn("a") + .text("download") + .attr("href", avatar.url) + .attr("download", "avatar-" + unique_id + "." + media_image_type(avatar.type, true)) + .css("display", "none") + .appendTo($("body")); + element[0].click(); + element.remove(); + }; } - }); + })); - let callback_download: () => any; - let callback_delete: () => any; - - const button_download = modal.htmlTag.find(".button-download"); - const button_delete = modal.htmlTag.find(".button-delete"); - const container_list = modal.htmlTag.find(".container-list .list-entries-container"); - const list_entries = container_list.find(".list-entries"); - const container_info = modal.htmlTag.find(".container-info"); - const overlay_no_user = container_info.find(".disabled-overlay").show(); - - const set_selected_avatar = (unique_id: string, avatar_id: string, size: number) => { - button_download.prop("disabled", true); - callback_download = undefined; - if(!unique_id) { - overlay_no_user.show(); - return; - } - - const tag_username = container_info.find(".property-username"); - const tag_unique_id = container_info.find(".property-unique-id"); - const tag_avatar_id = container_info.find(".property-avatar-id"); - const container_avatar = container_info.find(".container-image"); - const tag_image_bytes = container_info.find(".property-image-size"); - const tag_image_width = container_info.find(".property-image-width").val(tr("loading...")); - const tag_image_height = container_info.find(".property-image-height").val(tr("loading...")); - const tag_image_type = container_info.find(".property-image-type").val(tr("loading...")); - - tag_username.val("unknown"); - tag_unique_id.val(unique_id); - tag_avatar_id.val(avatar_id); - tag_image_bytes.val(size); - - container_avatar.empty().append(client.fileManager.avatars.generate_tag(avatar_id, undefined, { - callback_image: image => { - tag_image_width.val(image[0].naturalWidth + 'px'); - tag_image_height.val(image[0].naturalHeight + 'px'); - }, - callback_avatar: avatar => { - tag_image_type.val(media_image_type(avatar.type)); - button_download.prop("disabled", false); - - callback_download = () => { - const element = $.spawn("a") - .text("download") - .attr("href", avatar.url) - .attr("download", "avatar-" + unique_id + "." + media_image_type(avatar.type, true)) - .css("display", "none") - .appendTo($("body")); - element[0].click(); - element.remove(); - }; + callback_delete = () => { + spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this avatar?"), result => { + if(result) { + createErrorModal(tr("Not implemented"), tr("Avatar delete hasn't implemented yet")).open(); + //TODO Implement avatar delete } - })); - - callback_delete = () => { - spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this avatar?"), result => { - if(result) { - createErrorModal(tr("Not implemented"), tr("Avatar delete hasn't implemented yet")).open(); - //TODO Implement avatar delete - } - }); - }; - overlay_no_user.hide(); - }; - set_selected_avatar(undefined, undefined, 0); - - const update_avatar_list = () => { - const template_entry = $("#tmpl_avatar_list-list_entry"); - list_entries.empty(); - - client.fileManager.requestFileList("/").then(files => { - const username_resolve: {[unique_id: string]:((name:string) => any)[]} = {}; - for(const entry of files) { - const avatar_id = entry.name.substr('avatar_'.length); - const unique_id = avatar_to_uid(avatar_id); - - const tag = template_entry.renderTag({ - username: 'loading', - unique_id: unique_id, - size: human_file_size(entry.size), - timestamp: moment(entry.datetime * 1000).format('YY-MM-DD HH:mm') - }); - - (username_resolve[unique_id] || (username_resolve[unique_id] = [])).push(name => { - const tag_username = tag.find(".column-username").empty(); - if(name) { - tag_username.append(ClientEntry.chatTag(0, name, unique_id, false)); - } else { - tag_username.text("unknown"); - } - }); - list_entries.append(tag); - - tag.on('click', () => { - list_entries.find('.selected').removeClass('selected'); - tag.addClass('selected'); - - set_selected_avatar(unique_id, avatar_id, entry.size); - }); - } - - if(container_list.hasScrollBar()) - container_list.addClass("scrollbar"); - - client.serverConnection.command_helper.info_from_uid(...Object.keys(username_resolve)).then(result => { - for(const info of result) { - username_resolve[info.client_unique_id].forEach(e => e(info.client_nickname)); - delete username_resolve[info.client_unique_id]; - } - for(const uid of Object.keys(username_resolve)) { - (username_resolve[uid] || []).forEach(e => e(undefined)); - } - }).catch(error => { - log.error(LogCategory.GENERAL, tr("Failed to fetch usernames from avatar names. Error: %o"), error); - createErrorModal(tr("Failed to fetch usernames"), tr("Failed to fetch usernames related to their avatar names"), undefined).open(); - }) - }).catch(error => { - //TODO: Display no perms error - log.error(LogCategory.GENERAL, tr("Failed to receive avatar list. Error: %o"), error); - createErrorModal(tr("Failed to list avatars"), tr("Failed to receive avatar list."), undefined).open(); }); }; + overlay_no_user.hide(); + }; + set_selected_avatar(undefined, undefined, 0); - button_download.on('click', () => (callback_download || (() => {}))()); - button_delete.on('click', () => (callback_delete || (() => {}))()); - setTimeout(() => update_avatar_list(), 250); - modal.open(); - } + const update_avatar_list = () => { + const template_entry = $("#tmpl_avatar_list-list_entry"); + list_entries.empty(); + + client.fileManager.requestFileList("/").then(files => { + const username_resolve: {[unique_id: string]:((name:string) => any)[]} = {}; + for(const entry of files) { + const avatar_id = entry.name.substr('avatar_'.length); + const unique_id = avatar_to_uid(avatar_id); + + const tag = template_entry.renderTag({ + username: 'loading', + unique_id: unique_id, + size: human_file_size(entry.size), + timestamp: moment(entry.datetime * 1000).format('YY-MM-DD HH:mm') + }); + + (username_resolve[unique_id] || (username_resolve[unique_id] = [])).push(name => { + const tag_username = tag.find(".column-username").empty(); + if(name) { + tag_username.append(ClientEntry.chatTag(0, name, unique_id, false)); + } else { + tag_username.text("unknown"); + } + }); + list_entries.append(tag); + + tag.on('click', () => { + list_entries.find('.selected').removeClass('selected'); + tag.addClass('selected'); + + set_selected_avatar(unique_id, avatar_id, entry.size); + }); + } + + if(container_list.hasScrollBar()) + container_list.addClass("scrollbar"); + + client.serverConnection.command_helper.info_from_uid(...Object.keys(username_resolve)).then(result => { + for(const info of result) { + username_resolve[info.client_unique_id].forEach(e => e(info.client_nickname)); + delete username_resolve[info.client_unique_id]; + } + for(const uid of Object.keys(username_resolve)) { + (username_resolve[uid] || []).forEach(e => e(undefined)); + } + }).catch(error => { + log.error(LogCategory.GENERAL, tr("Failed to fetch usernames from avatar names. Error: %o"), error); + createErrorModal(tr("Failed to fetch usernames"), tr("Failed to fetch usernames related to their avatar names"), undefined).open(); + }) + }).catch(error => { + //TODO: Display no perms error + log.error(LogCategory.GENERAL, tr("Failed to receive avatar list. Error: %o"), error); + createErrorModal(tr("Failed to list avatars"), tr("Failed to receive avatar list."), undefined).open(); + }); + }; + + button_download.on('click', () => (callback_download || (() => {}))()); + button_delete.on('click', () => (callback_delete || (() => {}))()); + setTimeout(() => update_avatar_list(), 250); + modal.open(); } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalBanClient.ts b/shared/js/ui/modal/ModalBanClient.ts index bb7b0627..a479a68d 100644 --- a/shared/js/ui/modal/ModalBanClient.ts +++ b/shared/js/ui/modal/ModalBanClient.ts @@ -2,179 +2,183 @@ /// /// -namespace Modals { - export type BanEntry = { - name?: string; - unique_id: string; - } - export function spawnBanClient(client: ConnectionHandler, entries: BanEntry | BanEntry[], callback: (data: { - length: number, - reason: string, - no_name: boolean, - no_ip: boolean, - no_hwid: boolean - }) => void) { - const max_ban_time = client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).value; +import PermissionType from "tc-shared/permission/PermissionType"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {createModal} from "tc-shared/ui/elements/Modal"; +import {duration_data} from "tc-shared/ui/modal/ModalBanList"; +import * as tooltip from "tc-shared/ui/elements/Tooltip"; - const permission_criteria_hwid = client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_HWID).granted(1); - const permission_criteria_ip = client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_IP).granted(1); - const permission_criteria_name = client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_NAME).granted(1); +export type BanEntry = { + name?: string; + unique_id: string; +} +export function spawnBanClient(client: ConnectionHandler, entries: BanEntry | BanEntry[], callback: (data: { + length: number, + reason: string, + no_name: boolean, + no_ip: boolean, + no_hwid: boolean +}) => void) { + const max_ban_time = client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).value; - const modal = createModal({ - header: Array.isArray(entries) ? tr("Ban clients") : tr("Ban client"), - body: function () { - let template = $("#tmpl_client_ban").renderTag({entries: entries}); + const permission_criteria_hwid = client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_HWID).granted(1); + const permission_criteria_ip = client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_IP).granted(1); + const permission_criteria_name = client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_NAME).granted(1); - let update_duration; - let update_button_ok; - const button_ok = template.find(".button-apply"); - const button_cancel = template.find(".button-cancel"); + const modal = createModal({ + header: Array.isArray(entries) ? tr("Ban clients") : tr("Ban client"), + body: function () { + let template = $("#tmpl_client_ban").renderTag({entries: entries}); - const input_duration_value = template.find(".container-duration input").on('change keyup', () => update_duration()); - const input_duration_type = template.find(".container-duration select").on('change keyup', () => update_duration()); + let update_duration; + let update_button_ok; + const button_ok = template.find(".button-apply"); + const button_cancel = template.find(".button-cancel"); - const container_reason = template.find(".container-reason"); + const input_duration_value = template.find(".container-duration input").on('change keyup', () => update_duration()); + const input_duration_type = template.find(".container-duration select").on('change keyup', () => update_duration()); - const criteria_nickname = template.find(".criteria.nickname input") - .prop('checked', permission_criteria_name).prop("disabled", !permission_criteria_name) - .firstParent(".checkbox").toggleClass("disabled", !permission_criteria_name); + const container_reason = template.find(".container-reason"); - const criteria_ip_address = template.find(".criteria.ip-address input") - .prop('checked', permission_criteria_ip).prop("disabled", !permission_criteria_ip) - .firstParent(".checkbox").toggleClass("disabled", !permission_criteria_ip); + const criteria_nickname = template.find(".criteria.nickname input") + .prop('checked', permission_criteria_name).prop("disabled", !permission_criteria_name) + .firstParent(".checkbox").toggleClass("disabled", !permission_criteria_name); - const criteria_hardware_id = template.find(".criteria.hardware-id input") - .prop('checked', permission_criteria_hwid).prop("disabled", !permission_criteria_hwid) - .firstParent(".checkbox").toggleClass("disabled", !permission_criteria_hwid); + const criteria_ip_address = template.find(".criteria.ip-address input") + .prop('checked', permission_criteria_ip).prop("disabled", !permission_criteria_ip) + .firstParent(".checkbox").toggleClass("disabled", !permission_criteria_ip); - /* duration input handler */ - { - const tooltip_duration_max = template.find(".tooltip-max-time a.max"); + const criteria_hardware_id = template.find(".criteria.hardware-id input") + .prop('checked', permission_criteria_hwid).prop("disabled", !permission_criteria_hwid) + .firstParent(".checkbox").toggleClass("disabled", !permission_criteria_hwid); - update_duration = () => { - const type = input_duration_type.val() as string; - const value = parseInt(input_duration_value.val() as string); - const disabled = input_duration_type.prop("disabled"); + /* duration input handler */ + { + const tooltip_duration_max = template.find(".tooltip-max-time a.max"); - input_duration_value.prop("disabled", type === "perm" || disabled).firstParent(".input-boxed").toggleClass("disabled", type === "perm" || disabled); - if(type !== "perm") { - if(input_duration_value.attr("x-saved-value")) { - input_duration_value.val(parseInt(input_duration_value.attr("x-saved-value"))); - input_duration_value.attr("x-saved-value", null); - } + update_duration = () => { + const type = input_duration_type.val() as string; + const value = parseInt(input_duration_value.val() as string); + const disabled = input_duration_type.prop("disabled"); - const selected_option = input_duration_type.find("option[value='" + type + "']"); - const max = parseInt(selected_option.attr("duration-max")); + input_duration_value.prop("disabled", type === "perm" || disabled).firstParent(".input-boxed").toggleClass("disabled", type === "perm" || disabled); + if(type !== "perm") { + if(input_duration_value.attr("x-saved-value")) { + input_duration_value.val(parseInt(input_duration_value.attr("x-saved-value"))); + input_duration_value.attr("x-saved-value", null); + } - input_duration_value.attr("max", max); - if((value > max && max != -1) || value < 1) { - input_duration_value.firstParent(".input-boxed").addClass("is-invalid"); - } else { - input_duration_value.firstParent(".input-boxed").removeClass("is-invalid"); - } + const selected_option = input_duration_type.find("option[value='" + type + "']"); + const max = parseInt(selected_option.attr("duration-max")); - if(max != -1) - tooltip_duration_max.html(tr("You're allowed to ban a maximum of ") + "" + max + " " + duration_data[type][max == 1 ? "1-text" : "text"] + ""); - else - tooltip_duration_max.html(tr("You're allowed to ban permanent.")); + input_duration_value.attr("max", max); + if((value > max && max != -1) || value < 1) { + input_duration_value.firstParent(".input-boxed").addClass("is-invalid"); } else { - if(value && !Number.isNaN(value)) - input_duration_value.attr("x-saved-value", value); - input_duration_value.attr("placeholder", tr("for ever")).val(null); + input_duration_value.firstParent(".input-boxed").removeClass("is-invalid"); + } + + if(max != -1) + tooltip_duration_max.html(tr("You're allowed to ban a maximum of ") + "" + max + " " + duration_data[type][max == 1 ? "1-text" : "text"] + ""); + else tooltip_duration_max.html(tr("You're allowed to ban permanent.")); - } - update_button_ok && update_button_ok(); - }; + } else { + if(value && !Number.isNaN(value)) + input_duration_value.attr("x-saved-value", value); + input_duration_value.attr("placeholder", tr("for ever")).val(null); + tooltip_duration_max.html(tr("You're allowed to ban permanent.")); + } + update_button_ok && update_button_ok(); + }; - /* initialize ban time */ - Promise.resolve(max_ban_time).catch(error => { /* TODO: Error handling? */ return 0; }).then(max_time => { - let unlimited = max_time == 0 || max_time == -1; - if(unlimited || typeof(max_time) === "undefined") max_time = 0; + /* initialize ban time */ + Promise.resolve(max_ban_time).catch(error => { /* TODO: Error handling? */ return 0; }).then(max_time => { + let unlimited = max_time == 0 || max_time == -1; + if(unlimited || typeof(max_time) === "undefined") max_time = 0; - for(const value of Object.keys(duration_data)) { - input_duration_type.find("option[value='" + value + "']") - .prop("disabled", !unlimited && max_time >= duration_data[value].scale) - .attr("duration-scale", duration_data[value].scale) - .attr("duration-max", unlimited ? -1 : Math.floor(max_time / duration_data[value].scale)); - } - - input_duration_type.find("option[value='perm']") - .prop("disabled", !unlimited) - .attr("duration-scale", 0) - .attr("duration-max", -1); - update_duration(); - }); + for(const value of Object.keys(duration_data)) { + input_duration_type.find("option[value='" + value + "']") + .prop("disabled", !unlimited && max_time >= duration_data[value].scale) + .attr("duration-scale", duration_data[value].scale) + .attr("duration-max", unlimited ? -1 : Math.floor(max_time / duration_data[value].scale)); + } + input_duration_type.find("option[value='perm']") + .prop("disabled", !unlimited) + .attr("duration-scale", 0) + .attr("duration-max", -1); update_duration(); - } + }); - /* ban reason */ - { - const input = container_reason.find("textarea"); + update_duration(); + } - const insert_tag = (open: string, close: string) => { - if(input.prop("disabled")) - return; + /* ban reason */ + { + const input = container_reason.find("textarea"); - const node = input[0] as HTMLTextAreaElement; - if (node.selectionStart || node.selectionStart == 0) { - const startPos = node.selectionStart; - const endPos = node.selectionEnd; - node.value = node.value.substring(0, startPos) + open + node.value.substring(startPos, endPos) + close + node.value.substring(endPos); - node.selectionEnd = endPos + open.length; - node.selectionStart = node.selectionEnd; - } else { - node.value += open + close; - node.selectionEnd = node.value.length - close.length; - node.selectionStart = node.selectionEnd; - } + const insert_tag = (open: string, close: string) => { + if(input.prop("disabled")) + return; - input.focus().trigger('change'); - }; + const node = input[0] as HTMLTextAreaElement; + if (node.selectionStart || node.selectionStart == 0) { + const startPos = node.selectionStart; + const endPos = node.selectionEnd; + node.value = node.value.substring(0, startPos) + open + node.value.substring(startPos, endPos) + close + node.value.substring(endPos); + node.selectionEnd = endPos + open.length; + node.selectionStart = node.selectionEnd; + } else { + node.value += open + close; + node.selectionEnd = node.value.length - close.length; + node.selectionStart = node.selectionEnd; + } - container_reason.find(".button-bold").on('click', () => insert_tag('[b]', '[/b]')); - container_reason.find(".button-italic").on('click', () => insert_tag('[i]', '[/i]')); - container_reason.find(".button-underline").on('click', () => insert_tag('[u]', '[/u]')); - container_reason.find(".button-color input").on('change', event => { - insert_tag('[color=' + (event.target as HTMLInputElement).value + ']', '[/color]') + input.focus().trigger('change'); + }; + + container_reason.find(".button-bold").on('click', () => insert_tag('[b]', '[/b]')); + container_reason.find(".button-italic").on('click', () => insert_tag('[i]', '[/i]')); + container_reason.find(".button-underline").on('click', () => insert_tag('[u]', '[/u]')); + container_reason.find(".button-color input").on('change', event => { + insert_tag('[color=' + (event.target as HTMLInputElement).value + ']', '[/color]') + }); + } + + /* buttons */ + { + button_cancel.on('click', event => modal.close()); + button_ok.on('click', event => { + const duration = input_duration_type.val() === "perm" ? 0 : (1000 * parseInt(input_duration_type.find("option[value='" + input_duration_type.val() + "']").attr("duration-scale")) * parseInt(input_duration_value.val() as string)); + + modal.close(); + callback({ + length: Math.floor(duration / 1000), + reason: container_reason.find("textarea").val() as string, + + no_hwid: !criteria_hardware_id.find("input").prop("checked"), + no_ip: !criteria_ip_address.find("input").prop("checked"), + no_name: !criteria_nickname.find("input").prop("checked") }); - } + }); - /* buttons */ - { - button_cancel.on('click', event => modal.close()); - button_ok.on('click', event => { - const duration = input_duration_type.val() === "perm" ? 0 : (1000 * parseInt(input_duration_type.find("option[value='" + input_duration_type.val() + "']").attr("duration-scale")) * parseInt(input_duration_value.val() as string)); + const inputs = template.find(".input-boxed"); + update_button_ok = () => { + const invalid = [...inputs].find(e => $(e).hasClass("is-invalid")); + button_ok.prop('disabled', !!invalid); + }; + update_button_ok(); + } - modal.close(); - callback({ - length: Math.floor(duration / 1000), - reason: container_reason.find("textarea").val() as string, + tooltip.initialize(template); + return template.children(); + }, + footer: null, - no_hwid: !criteria_hardware_id.find("input").prop("checked"), - no_ip: !criteria_ip_address.find("input").prop("checked"), - no_name: !criteria_nickname.find("input").prop("checked") - }); - }); + min_width: "10em", + width: "30em" + }); + modal.open(); - const inputs = template.find(".input-boxed"); - update_button_ok = () => { - const invalid = [...inputs].find(e => $(e).hasClass("is-invalid")); - button_ok.prop('disabled', !!invalid); - }; - update_button_ok(); - } - - tooltip(template); - return template.children(); - }, - footer: null, - - min_width: "10em", - width: "30em" - }); - modal.open(); - - modal.htmlTag.find(".modal-body").addClass("modal-ban-client"); - } + modal.htmlTag.find(".modal-body").addClass("modal-ban-client"); } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalBanList.ts b/shared/js/ui/modal/ModalBanList.ts index e5460964..20f1cc31 100644 --- a/shared/js/ui/modal/ModalBanList.ts +++ b/shared/js/ui/modal/ModalBanList.ts @@ -1,921 +1,924 @@ -/// -/// -/// -/// +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {createErrorModal, createInfoModal, createModal, Modal} from "tc-shared/ui/elements/Modal"; +import {SingleCommandHandler} from "tc-shared/connection/ConnectionBase"; +import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; +import PermissionType from "tc-shared/permission/PermissionType"; +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; +import * as tooltip from "tc-shared/ui/elements/Tooltip"; +import * as htmltags from "tc-shared/ui/htmltags"; +import {format_time, formatMessage} from "tc-shared/ui/frames/chat"; -namespace Modals { - export function openBanList(client: ConnectionHandler) { - let modal: Modal; +export function openBanList(client: ConnectionHandler) { + let modal: Modal; - let _callback_bans; - let _callback_triggers; - const single_ban_handler: connection.SingleCommandHandler = { - command: "notifybanlist", - function: command => { - const json = command.arguments; + let _callback_bans; + let _callback_triggers; + const single_ban_handler: SingleCommandHandler = { + command: "notifybanlist", + function: command => { + const json = command.arguments; - let bans: BanEntry[] = []; - for(const entry of json) { - bans.push({ - server_id: parseInt(entry["sid"]), - banid: parseInt(entry["banid"]), - ip: entry["ip"], - name: entry["name"], - unique_id: entry["uid"], - hardware_id: entry["hwid"], + let bans: BanEntry[] = []; + for(const entry of json) { + bans.push({ + server_id: parseInt(entry["sid"]), + banid: parseInt(entry["banid"]), + ip: entry["ip"], + name: entry["name"], + unique_id: entry["uid"], + hardware_id: entry["hwid"], - timestamp_created: (parseInt(entry["created"]) * 1000), - timestamp_expire: (parseInt(entry["duration"]) > 0 ? parseInt(entry["created"]) * 1000 + parseInt(entry["duration"]) * 1000 : 0), + timestamp_created: (parseInt(entry["created"]) * 1000), + timestamp_expire: (parseInt(entry["duration"]) > 0 ? parseInt(entry["created"]) * 1000 + parseInt(entry["duration"]) * 1000 : 0), - invoker_name: entry["invokername"], - invoker_database_id: parseInt(entry["invokercldbid"]), - invoker_unique_id: entry["invokeruid"], - reason: entry["reason"], + invoker_name: entry["invokername"], + invoker_database_id: parseInt(entry["invokercldbid"]), + invoker_unique_id: entry["invokeruid"], + reason: entry["reason"], - enforcements: parseInt(entry["enforcements"]), - flag_own: entry["invokeruid"] == client.getClient().properties.client_unique_identifier - }); - } - - _callback_bans(bans); - return false; /* do not remove me */ - } - }; - const single_trigger_handler: connection.SingleCommandHandler = { - command: "notifybantriggerlist", - function: command => { - //TODO: Test the server id in the response? - const json = command.arguments; - - let triggers: TriggerEntry[] = []; - for(const entry of json) { - triggers.push({ - unique_id: entry["client_unique_identifier"], - client_nickname: entry["client_nickname"], - hardware_id: entry["client_hardware_identifier"], - connection_ip: entry["connection_client_ip"], - - timestamp: parseInt(entry["timestamp"]) - }); - } - - _callback_triggers(triggers); - return false; /* do not remove me */ - } - }; - - const controller: BanListController = { - request_list(_callback): Promise { - _callback_bans = _callback; - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - cleanup(); - reject("timeout"); - }, 2500); - - const cleanup = () => { - clearTimeout(timeout); - _callback_bans = undefined; - }; - - Promise.all([ - client.serverConnection.send_command("banlist", { sid: 0 }, {process_result: false}).catch(error => { - //TODO: May lookup for permissions - }), - client.serverConnection.send_command("banlist").catch(async error => { - if(error instanceof CommandResult) - if(error.id === ErrorID.EMPTY_RESULT) - return; - throw error; - }) - ]).then(() => { - if(_callback_bans) resolve(); - cleanup(); - }).catch(error => { - if(_callback_bans) reject(error); - cleanup(); - }); + enforcements: parseInt(entry["enforcements"]), + flag_own: entry["invokeruid"] == client.getClient().properties.client_unique_identifier }); - }, - request_trigger_list(ban, _callback): Promise { - _callback_triggers = _callback; - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - cleanup(); - reject("timeout"); - }, 2500); + } - const cleanup = () => { - clearTimeout(timeout); - _callback_triggers = undefined; - }; + _callback_bans(bans); + return false; /* do not remove me */ + } + }; + const single_trigger_handler: SingleCommandHandler = { + command: "notifybantriggerlist", + function: command => { + //TODO: Test the server id in the response? + const json = command.arguments; - const data = {banid: ban.ban_id}; - if(typeof ban.server_id !== "undefined") - data["sid"] = ban.server_id; - client.serverConnection.send_command("bantriggerlist", data).catch(async error => { + let triggers: TriggerEntry[] = []; + for(const entry of json) { + triggers.push({ + unique_id: entry["client_unique_identifier"], + client_nickname: entry["client_nickname"], + hardware_id: entry["client_hardware_identifier"], + connection_ip: entry["connection_client_ip"], + + timestamp: parseInt(entry["timestamp"]) + }); + } + + _callback_triggers(triggers); + return false; /* do not remove me */ + } + }; + + const controller: BanListController = { + request_list(_callback): Promise { + _callback_bans = _callback; + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + cleanup(); + reject("timeout"); + }, 2500); + + const cleanup = () => { + clearTimeout(timeout); + _callback_bans = undefined; + }; + + Promise.all([ + client.serverConnection.send_command("banlist", { sid: 0 }, {process_result: false}).catch(error => { + //TODO: May lookup for permissions + }), + client.serverConnection.send_command("banlist").catch(async error => { if(error instanceof CommandResult) if(error.id === ErrorID.EMPTY_RESULT) return; throw error; - }).then(() => { - if(_callback_triggers) resolve(); - cleanup(); - }).catch(error => { - if(_callback_triggers) reject(error); - cleanup(); - }); + }) + ]).then(() => { + if(_callback_bans) resolve(); + cleanup(); + }).catch(error => { + if(_callback_bans) reject(error); + cleanup(); }); - }, - async max_bantime(): Promise { - const value = client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).value || 0; - return value == -2 ? 0 : value; - }, - async permission_add(): Promise { - return [ - client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_CREATE).granted(1), - client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_CREATE_GLOBAL).granted(1) - ]; - }, - async permission_edit(): Promise { - return [ - client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_EDIT).granted(1), - client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_EDIT_GLOBAL).granted(1) && false - ]; - }, - add_ban(entry: BanEntry): Promise { - const data = {}; + }); + }, + request_trigger_list(ban, _callback): Promise { + _callback_triggers = _callback; + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + cleanup(); + reject("timeout"); + }, 2500); - if(entry.ip) data["ip"] = entry.ip; - if(entry.name) data["name"] = entry.name; - if(entry.unique_id) data["uid"] = entry.unique_id; - if(entry.hardware_id) data["hwid"] = entry.hardware_id; - if(entry.reason) data["banreason"] = entry.reason; - if(entry.timestamp_expire) data["time"] = Math.floor((entry.timestamp_expire - entry.timestamp_created) / 1000); - if(typeof(entry.server_id) === "number") data["sid"] = entry.server_id; - - return client.serverConnection.send_command("banadd", data).then(e => { if(!e.success) throw e; }); - }, - edit_ban(data: any): Promise { - return client.serverConnection.send_command("banedit", data).then(e => { if(!e.success) throw e; }); - }, - delete_ban(entry_id, server_id): Promise { - const data = { - banid: entry_id + const cleanup = () => { + clearTimeout(timeout); + _callback_triggers = undefined; }; - if(typeof(server_id) === "number") - data["sid"] = server_id; - return client.serverConnection.send_command("bandel", data).then(e => { if(!e.success) throw e; }); - } - }; - - modal = createModal({ - header: tr("Server Banlist"), - body: () => generate_dom(controller), - footer: null, - - width: '60em' - }); - - client.serverConnection.command_handler_boss().register_single_handler(single_ban_handler); - client.serverConnection.command_handler_boss().register_single_handler(single_trigger_handler); - modal.close_listener.push(() => { - client.serverConnection.command_handler_boss().remove_single_handler(single_ban_handler); - client.serverConnection.command_handler_boss().remove_single_handler(single_trigger_handler); - }); - - //TODO: Test without dividerfy! - modal.htmlTag.dividerfy(); - modal.htmlTag.find(".modal-body").addClass("modal-ban-list"); - modal.open(); - } - - interface BanEntry { - server_id: number; - banid: number; - - name?: string; - name_type?: number; - - unique_id?: string; - ip?: string; - hardware_id?: string; - - reason: string; - invoker_name: string; - invoker_unique_id?: string; - invoker_database_id?: number; - - timestamp_created: number; - timestamp_expire: number; - - enforcements: number; - - flag_own?: boolean; - } - - interface TriggerEntry { - unique_id?: string; - client_nickname?: string; - hardware_id?: string; - connection_ip: string; - - timestamp: number; - } - - interface BanListController { - request_list(callback_bans: (entries: BanEntry[]) => any) : Promise; - request_trigger_list(ban: {ban_id: number, server_id: number | undefined}, callback_triggers: (entries: TriggerEntry[]) => any) : Promise; - - max_bantime() : Promise; - permission_edit() : Promise; - permission_add() : Promise; - - add_ban(entry: BanEntry) : Promise; - edit_ban(data: any) : Promise; - delete_ban(entry_id: number, server_id: number | undefined) : Promise; - } - - //Note: This object must be sorted (from shortest to longest)! - export const duration_data = { - "sec": { - "text": tr("Seconds"), - "1-text": tr("Second"), - - scale: 1 + const data = {banid: ban.ban_id}; + if(typeof ban.server_id !== "undefined") + data["sid"] = ban.server_id; + client.serverConnection.send_command("bantriggerlist", data).catch(async error => { + if(error instanceof CommandResult) + if(error.id === ErrorID.EMPTY_RESULT) + return; + throw error; + }).then(() => { + if(_callback_triggers) resolve(); + cleanup(); + }).catch(error => { + if(_callback_triggers) reject(error); + cleanup(); + }); + }); }, - "min": { - "text": tr("Minutes"), - "1-text": tr("Minute"), - - scale: 60 + async max_bantime(): Promise { + const value = client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).value || 0; + return value == -2 ? 0 : value; }, - "hours": { - "text": tr("Hours"), - "1-text": tr("Hour"), - - scale: 3600 + async permission_add(): Promise { + return [ + client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_CREATE).granted(1), + client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_CREATE_GLOBAL).granted(1) + ]; }, - "days": { - "text": tr("Days"), - "1-text": tr("Day"), - - scale: 86400 + async permission_edit(): Promise { + return [ + client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_EDIT).granted(1), + client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_EDIT_GLOBAL).granted(1) && false + ]; }, + add_ban(entry: BanEntry): Promise { + const data = {}; + + if(entry.ip) data["ip"] = entry.ip; + if(entry.name) data["name"] = entry.name; + if(entry.unique_id) data["uid"] = entry.unique_id; + if(entry.hardware_id) data["hwid"] = entry.hardware_id; + if(entry.reason) data["banreason"] = entry.reason; + if(entry.timestamp_expire) data["time"] = Math.floor((entry.timestamp_expire - entry.timestamp_created) / 1000); + if(typeof(entry.server_id) === "number") data["sid"] = entry.server_id; + + return client.serverConnection.send_command("banadd", data).then(e => { if(!e.success) throw e; }); + }, + edit_ban(data: any): Promise { + return client.serverConnection.send_command("banedit", data).then(e => { if(!e.success) throw e; }); + }, + delete_ban(entry_id, server_id): Promise { + const data = { + banid: entry_id + }; + if(typeof(server_id) === "number") + data["sid"] = server_id; + + return client.serverConnection.send_command("bandel", data).then(e => { if(!e.success) throw e; }); + } }; - function generate_dom(controller: BanListController) : JQuery { - const template = $("#tmpl_ban_list").renderTag(); - - let callback_ban_filter: ((text: string, flag_own: boolean, highlight_own: boolean) => boolean)[] = []; - let callback_trigger_filter: ((text: string) => boolean)[] = []; - let selected_ban: BanEntry | undefined; - let update_edit_window: (switch_to: boolean) => any; - let update_ban_filter: () => any; - let update_trigger_filter: () => any; - - const container_ban = template.find(".container-banlist"); - const container_ban_entries = container_ban.find(".container-list .body"); - const container_ban_entries_empty = container_ban.find(".container-list .container-empty"); - const container_ban_entries_error = container_ban.find(".container-list .container-error"); - - const container_trigger = template.find(".container-triggerlist").hide(); - const container_trigger_entries = container_trigger.find(".container-list .body"); - const container_trigger_entries_empty = container_trigger.find(".container-list .container-empty"); - const container_trigger_entries_error = container_trigger.find(".container-list .container-error"); - - const button_apply = template.find(".button-apply"); - let button_apply_state = [false, false]; /* first index is add; second index is edit */ - let update_category_inputs: (() => any)[] = [undefined, undefined]; - let button_apply_state_index = 1; - - const category_add = template.find(".left .head .category-add"); - const category_edit = template.find(".left .head .category-edit"); - const container_add = template.find(".left .container-add"); - const container_add_no_permissions = template.find(".left .container-add .container-no-permissions"); - const container_edit = template.find(".left .container-edit"); - - const seperator_top = template.find(".container-seperator .top"); - - /* [local; global] */ - let permission_edit: boolean[] = [false, false], permission_add: boolean[] = [false, false]; - - container_add_no_permissions.hide(); - controller.permission_add().then(result => permission_add = result).catch(error => { - log.error(LogCategory.CLIENT, tr("Failed to query ban add permissions: %o"), error); - }).then(() => { - if(permission_add[0] !== permission_add[1]) { - const input_global = container_add.find(".group-global input"); - input_global.prop("checked", permission_add[1]).prop("disabled", true).firstParent(".checkbox").addClass("disabled"); - } else if(!permission_add[0]) - container_add_no_permissions.show(); - }); - - controller.permission_edit().then(result => permission_edit = result).catch(error => { - log.error(LogCategory.CLIENT, tr("Failed to query ban edit permissions: %o"), error); - }).then(() => { - if(selected_ban) update_edit_window(false); - }); - - /* category switch */ - { - category_add.on('click', event => { - container_add.removeClass("hidden"); - category_add.addClass("selected"); - - container_edit.addClass("hidden"); - category_edit.removeClass("selected"); - - seperator_top.css({opacity: 1}); - - button_apply_state_index = 0; - button_apply.prop("disabled", !button_apply_state[0]).text(tr("Add ban")); - update_category_inputs[button_apply_state_index](); - }); - - category_edit.on('click', event => { - if(!selected_ban) return; - - container_add.addClass("hidden"); - category_add.removeClass("selected"); - - container_edit.removeClass("hidden"); - category_edit.addClass("selected"); - - seperator_top.css({opacity: 0}); - - button_apply_state_index = 1; - button_apply.prop("disabled", !button_apply_state[1]).text(tr("Save ban")); - update_category_inputs[button_apply_state_index](); - }); - } - - const build_ban_entry = (entry: BanEntry, selected: boolean) => { - let button_delete; - const tag = $.spawn("div").addClass("entry" + (entry.server_id > 0 ? "" : " global") + (selected ? " selected" : "")).append( - $.spawn("div").addClass("column column-key").append( - entry.name ? $.spawn("div").append(entry.name) : undefined, - entry.ip ? $.spawn("div").append(entry.ip) : undefined, - entry.unique_id ? $.spawn("div").append(entry.unique_id) : undefined, - entry.hardware_id ? $.spawn("div").append(entry.hardware_id) : undefined - ), - $.spawn("div").addClass("column column-reason").text(entry.reason), - $.spawn("div").addClass("column column-expires").text(entry.timestamp_expire ? moment(entry.timestamp_expire).format('DD.MM.YYYY hh:mm') : tr("Never")), - $.spawn("div").addClass("column column-delete").append( - button_delete = $.spawn("div").addClass("button-delete").append( - $.spawn("div").addClass("icon_em client-delete") - ) - ) - ); - - tag.on('click', event => { - if(selected_ban === entry || event.isDefaultPrevented()) return; - selected_ban = entry; - - container_ban_entries.find(".entry.selected").removeClass("selected"); - tag.addClass("selected"); - - update_edit_window(true); - }); - - button_delete.on('click', event => { - event.preventDefault(); - - controller.delete_ban(entry.banid, entry.server_id).then(() => { - tag.css({opacity: 1}).animate({opacity: 0}, 250, () => tag.animate({"max-height": 0}, 250, () => tag.remove())); - if(entry === selected_ban) { - selected_ban = undefined; - update_edit_window(false); - } - }).catch(error => { - log.error(LogCategory.CLIENT, tr("Failed to delete ban: %o"), error); - if(error instanceof CommandResult) - error = error.id === ErrorID.PERMISSION_ERROR ? "no permissions" : error.extra_message || error.message; - createErrorModal(tr("Failed to delete ban"), MessageHelper.formatMessage(tr("Failed to delete ban. {:br:}Error: {}"), error)).open(); - }); - }); - - if(selected) { - selected_ban = entry; - update_edit_window(false); - } - - const lower_mesh = - (entry.reason || "").toLowerCase() + " " + - (entry.unique_id || "").toLowerCase() + " " + - (entry.name || "").toLowerCase() + " " + - (entry.ip || "").toLowerCase() + " " + - (entry.hardware_id || "").toLowerCase(); - callback_ban_filter.push((text, flag_own, highlight_own) => { - if(text && lower_mesh.indexOf(text) == -1) { - tag.hide(); - return false; - } - - if(flag_own && !entry.flag_own) { - tag.hide(); - return false; - } - - tag.show().toggleClass( - "highlight", - highlight_own && - entry.flag_own - ); - return true; - }); - - return tag; - }; - - const update_banlist = (selected_ban?: number) => { - callback_ban_filter = []; - - container_ban_entries.find(".entry").remove(); - container_ban_entries_error.hide(); - container_ban_entries_empty.show().find("a").text(tr("Loading...")); - - let bans = []; - controller.request_list(_bans => bans.push(..._bans)).then(() => { - if(bans.length) { - container_ban_entries.append(...bans.map(e => build_ban_entry(e, e.banid === selected_ban))); - container_ban_entries_empty.hide(); - } else { - container_ban_entries_empty.find("a").text(tr("No bans registered")); - } - update_ban_filter(); - }).catch(error => { - log.info(LogCategory.CLIENT, tr("Failed to update ban list: %o"), error); - if(error instanceof CommandResult) - error = error.id === ErrorID.PERMISSION_ERROR ? tr("no permissions") : error.extra_message || error.message; - container_ban_entries_error.show().find("a").text(tr("Failed to receive banlist: ") + error); - container_ban_entries_empty.hide(); - }); - }; - - const build_trigger_entry = (entry: TriggerEntry) => { - const spawn_key_value = (key, value, reason) => { - return $.spawn("div").addClass("property").toggleClass("highlighted", reason).append( - $.spawn("div").addClass("key").text(key + ": "), - $.spawn("div").addClass("value").text(value) - ); - }; - - let cause_name = !!selected_ban.name && !!entry.client_nickname.match(selected_ban.name); - let cause_uid = !cause_name && !!selected_ban.unique_id && selected_ban.unique_id.toLowerCase() === (entry.unique_id || "").toLowerCase(); - let cause_ip = !cause_uid && !!selected_ban.ip && selected_ban.ip.toLowerCase() === (entry.connection_ip || "").toLowerCase(); - let cause_hwid = !cause_ip && !!selected_ban.hardware_id && selected_ban.hardware_id.toLowerCase() === (entry.hardware_id || "").toLowerCase(); - - /* we guess that IP is the cause because we dont see the IP and there is no other reason */ - if(!cause_name && !cause_uid && !cause_ip && !cause_hwid && entry.connection_ip === "hidden") - cause_ip = true; - - const time_str = moment(entry.timestamp).format('DD.MM.YYYY hh:mm'); - - const tag = $.spawn("div").addClass("entry").append( - $.spawn("div").addClass("column column-properties").append( - entry.client_nickname ? spawn_key_value(tr("Nickname"), entry.client_nickname, cause_name) : undefined, - entry.connection_ip ? spawn_key_value(tr("IP"), entry.connection_ip, cause_ip) : undefined, - entry.unique_id ? spawn_key_value(tr("Unique ID"), entry.unique_id, cause_uid) : undefined, - entry.hardware_id ? spawn_key_value(tr("Hardware ID"), entry.hardware_id, cause_hwid) : undefined - ), - $.spawn("div").addClass("column column-timestamp").text(time_str) - ); - - const lower_mesh = - (entry.unique_id || "").toLowerCase() + " " + - (entry.client_nickname || "").toLowerCase() + " " + - (entry.connection_ip || "").toLowerCase() + " " + - (entry.hardware_id || "").toLowerCase() + " " + - time_str + " " + - entry.timestamp; - callback_trigger_filter.push(text => { - if(text && lower_mesh.indexOf(text) == -1) { - tag.hide(); - return false; - } - - tag.show(); - return true; - }); - - return tag; - }; - - const update_triggerlist = () => { - callback_trigger_filter = []; - - container_trigger_entries.find(".entry").remove(); - container_trigger_entries_error.hide(); - container_trigger_entries_empty.show().find("a").text(tr("Loading...")); - - let triggers = []; - controller.request_trigger_list({ - ban_id: selected_ban.banid, - server_id: selected_ban.server_id - }, _triggers => triggers.push(..._triggers)).then(() => { - if(triggers.length) { - container_trigger_entries.append(...triggers.sort((a, b) => b.timestamp - a.timestamp).map(e => build_trigger_entry(e))); - container_trigger_entries_empty.hide(); - } else { - container_trigger_entries_empty.find("a").text(tr("No triggers logged")); - } - - update_trigger_filter(); - }).catch(error => { - log.info(LogCategory.CLIENT, tr("Failed to update trigger list: %o"), error); - if(error instanceof CommandResult) - error = error.id === ErrorID.PERMISSION_ERROR ? tr("no permissions") : error.extra_message || error.message; - container_trigger_entries_error.show().find("a").text(tr("Failed to receive trigger list: ") + error); - container_trigger_entries_empty.hide(); - }); - }; - - const show_triggerlist = () => { - container_trigger.show(); - }; - - /* general input field rules */ - const initialize_fields = (tag: JQuery, index: number) => { - const input_name = tag.find(".group-name input").on('change keyup', () => update_category_inputs[index]()); - const input_ip = tag.find(".group-ip input").on('change keyup', () => update_category_inputs[index]()); - const input_uid = tag.find(".group-unique-id input").on('change keyup', () => update_category_inputs[index]()); - const input_hwid = tag.find(".group-hwid input").on('change keyup', () => update_category_inputs[index]()); - const input_reason = tag.find(".group-reason textarea").on('change keyup', () => update_category_inputs[index]()); - //const input_global = tag.find(".group-global input"); - const input_duration_value = tag.find(".group-duration input").on('change keyup', () => update_category_inputs[index]()); - const input_duration_type = tag.find(".group-duration select").on('change keyup', () => update_category_inputs[index]()); - const tooltip_duration_max = tag.find(".tooltip-max-time a.max"); - - update_category_inputs[index] = () => { - let _criteria_set = false; - let _input_invalid = false; - - { - //TODO: Check if in regex mode or not - const value = input_name.val() as string || ""; - if(value.length > 255) { - _input_invalid = true; - input_name.firstParent(".input-boxed").addClass("is-invalid"); - } else { - _criteria_set = _criteria_set || !!value; - input_name.firstParent(".input-boxed").removeClass("is-invalid"); - } - } - - { - //TODO: Check if in regex mode or not - const value = input_ip.val() as string || ""; - if(value.length > 255) { - _input_invalid = true; - input_ip.firstParent(".input-boxed").addClass("is-invalid"); - } else { - _criteria_set = _criteria_set || !!value; - input_ip.firstParent(".input-boxed").removeClass("is-invalid"); - } - } - - { - const value = input_uid.val() as string || ""; - try { - if(value && atob(value).length != 20) throw ""; - - _criteria_set = _criteria_set || !!value; - input_uid.firstParent(".input-boxed").removeClass("is-invalid"); - } catch(e) { - _input_invalid = true; - input_uid.firstParent(".input-boxed").addClass("is-invalid"); - } - } - - { - const value = input_hwid.val() as string || ""; - if(value.length > 255) { - _input_invalid = true; - input_hwid.firstParent(".input-boxed").addClass("is-invalid"); - } else { - _criteria_set = _criteria_set || !!value; - input_hwid.firstParent(".input-boxed").removeClass("is-invalid"); - } - } - - { - const value = input_reason.val() as string || ""; - if(value.length > 512) { - _input_invalid = true; - input_reason.firstParent(".input-boxed").addClass("is-invalid"); - } else { - input_reason.firstParent(".input-boxed").removeClass("is-invalid"); - } - } - - { - const type = input_duration_type.val() as string; - const value = parseInt(input_duration_value.val() as string); - const disabled = input_duration_type.prop("disabled"); - - input_duration_value.prop("disabled", type === "perm" || disabled).firstParent(".input-boxed").toggleClass("disabled", type === "perm" || disabled); - if(type !== "perm") { - if(input_duration_value.attr("x-saved-value")) { - input_duration_value.val(parseInt(input_duration_value.attr("x-saved-value"))); - input_duration_value.attr("x-saved-value", null); - } - - const selected_option = input_duration_type.find("option[value='" + type + "']"); - const max = parseInt(selected_option.attr("duration-max")); - - input_duration_value.attr("max", max); - if((value > max && max != -1) || value < 1) { - _input_invalid = true; - input_duration_value.firstParent(".input-boxed").addClass("is-invalid"); - } else { - input_duration_value.firstParent(".input-boxed").removeClass("is-invalid"); - } - - if(max != -1) - tooltip_duration_max.html(tr("You're allowed to ban a maximum of ") + "" + max + " " + duration_data[type][max == 1 ? "1-text" : "text"] + ""); - else - tooltip_duration_max.html(tr("You're allowed to ban permanent.")); - } else { - if(value && !Number.isNaN(value)) - input_duration_value.attr("x-saved-value", value); - input_duration_value.attr("placeholder", tr("for ever")).val(null); - tooltip_duration_max.html(tr("You're allowed to ban permanent.")); - } - } - - button_apply.prop("disabled", !(button_apply_state[button_apply_state_index] = _criteria_set && !_input_invalid)); - }; - - /* initialize ban time */ - controller.max_bantime().catch(error => { /* TODO: Error handling? */ return 0; }).then(max_time => { - let unlimited = max_time == 0 || max_time == -1; - if(unlimited) max_time = 0; - - for(const value of Object.keys(duration_data)) { - input_duration_type.find("option[value='" + value + "']") - .prop("disabled", !unlimited && max_time >= duration_data[value].scale) - .attr("duration-scale", duration_data[value].scale) - .attr("duration-max", unlimited ? -1 : Math.floor(max_time / duration_data[value].scale)); - } - - input_duration_type.find("option[value='perm']") - .prop("disabled", !unlimited) - .attr("duration-scale", 0) - .attr("duration-max", -1); - }); - }; - initialize_fields(container_add, 0); - initialize_fields(container_edit, 1); - - /* the edit "handler" */ - { - const tag = container_edit; - const input_name = tag.find(".group-name input"); - const input_ip = tag.find(".group-ip input"); - const input_interpret = tag.find(".group-interpret select"); - const input_uid = tag.find(".group-unique-id input"); - const input_hwid = tag.find(".group-hwid input"); - const input_reason = tag.find(".group-reason textarea"); - const input_global = tag.find(".group-global input"); - const input_duration_value = tag.find(".group-duration input"); - const input_duration_type = tag.find(".group-duration select"); - const tooltip_duration_detailed = tag.find(".tooltip-max-time a.detailed"); - - const label_enforcement_count = tag.find(".group-enforcements .value a"); - const button_enforcement_list = tag.find(".button-enforcement-list"); - - const container_creator = tag.find(".group-creator .value"); - - update_edit_window = (switch_to: boolean) => { - category_edit.toggleClass("disabled", !selected_ban); - - const editable = selected_ban && selected_ban.server_id === 0 ? permission_edit[1] : permission_edit[0]; - - input_name.val(selected_ban ? selected_ban.name : null).prop("disabled", !editable).firstParent(".input-boxed").toggleClass("disabled", !editable); - input_ip.val(selected_ban ? selected_ban.ip : null).prop("disabled", !editable).firstParent(".input-boxed").toggleClass("disabled", !editable); - input_uid.val(selected_ban ? selected_ban.unique_id : null).prop("disabled", !editable).firstParent(".input-boxed").toggleClass("disabled", !editable); - input_hwid.val(selected_ban ? selected_ban.hardware_id : null).prop("disabled", !editable).firstParent(".input-boxed").toggleClass("disabled", !editable); - input_reason.val(selected_ban ? selected_ban.reason : null).prop("disabled", !editable).firstParent(".input-boxed").toggleClass("disabled", !editable); - - input_interpret.find("option").eq(selected_ban && typeof(selected_ban.name_type) === "number" ? selected_ban.name_type : 2).prop("selected", true).prop("disabled", !editable).firstParent(".input-boxed").toggleClass("disabled", !editable); - label_enforcement_count.text((selected_ban ? selected_ban.enforcements : 0) || 0); - button_enforcement_list.prop("disabled", !selected_ban || selected_ban.enforcements == 0); - - input_global.prop("checked", selected_ban && selected_ban.server_id == 0); - - input_duration_type.prop("disabled", !editable).firstParent(".input-boxed").toggleClass("disabled", !editable); - input_duration_value.prop("disabled", !editable).firstParent(".input-boxed").toggleClass("disabled", !editable); - - if(selected_ban) { - if(selected_ban.timestamp_expire > selected_ban.timestamp_created) { - const duration = Math.ceil((selected_ban.timestamp_expire - selected_ban.timestamp_created) / 1000); - - const periods = Object.keys(duration_data); - let index; - for(index = 0; index < periods.length; index++) { - if(duration_data[periods[index]].scale > duration + 1 || ((duration + 1) % duration_data[periods[index]].scale) > 1.9) - break; - } - if(index > 0) index--; - input_duration_type.find("option[value='" + periods[index] + "']").prop("selected", true); - input_duration_value.val(Math.ceil(duration / duration_data[periods[index]].scale)); - tooltip_duration_detailed.text($.spawn("div").append(...MessageHelper.formatMessage(tr("The ban lasts for exact {}."), MessageHelper.format_time(duration * 1000, "never"))).text()); - } else { - tooltip_duration_detailed.text(tr("The ban is forever.")); - input_duration_value.attr("placeholder", tr("for ever")).val(null).prop('disabled', true); - input_duration_type.find("option[value='perm']").prop("selected", true); - } - } - - container_creator.empty(); - if(selected_ban) { - container_creator.append( - htmltags.generate_client_object({ - client_id: 0, - client_unique_id: selected_ban.invoker_unique_id, - client_name: selected_ban.invoker_name, - add_braces: false - }) - ); - } - - if(switch_to) - category_edit.trigger('click'); - }; - - button_apply.on('click', event => { - if (!button_apply_state[1] || button_apply_state_index != 1) return; - - const data = {banid: selected_ban.banid}; - - if(input_ip.val() != selected_ban.ip) - data["ip"] = input_ip.val(); - - if(input_name.val() != selected_ban.name) - data["name"] = input_name.val(); - - if(input_uid.val() != selected_ban.unique_id) - data["uid"] = input_uid.val(); - - if(input_hwid.val() != selected_ban.hardware_id) - data["hwid"] = input_hwid.val(); - - if(input_reason.val() != selected_ban.reason) - data["banreason"] = input_reason.val(); - - if(input_reason.val() != selected_ban.reason) - data["reason"] = input_reason.val(); - - const duration = input_duration_type.val() === "perm" ? 0 : (1000 * parseInt(input_duration_type.find("option[value='" + input_duration_type.val() + "']").attr("duration-scale")) * parseInt(input_duration_value.val() as string)); - if(selected_ban.timestamp_expire > 0 ? (selected_ban.timestamp_expire - selected_ban.timestamp_created != duration) : duration != 0) - data["time"] = Math.floor(duration / 1000); - - controller.edit_ban(data).then(() => { - update_banlist(selected_ban ? selected_ban.banid : undefined); - - selected_ban = undefined; - update_edit_window(false); - - createInfoModal(tr("Ban successfully edited"), tr("Your ban has been successfully edited.")).open(); - }).catch(error => { - log.error(LogCategory.CLIENT, tr("Failed to edited ban: %o"), error); - if(error instanceof CommandResult) - error = error.id === ErrorID.PERMISSION_ERROR ? "no permissions" : error.extra_message || error.message; - createErrorModal(tr("Failed to edited ban"), MessageHelper.formatMessage(tr("Failed to edited ban. {:br:}Error: {}"), error)).open(); - }); - }); - - button_enforcement_list.on('click', () => { - update_triggerlist(); - show_triggerlist(); - }); - } - - /* the create "handler" */ - { - const tag = container_add; - const input_name = tag.find(".group-name input"); - const input_ip = tag.find(".group-ip input"); - const input_interpret = tag.find(".group-interpret select"); - const input_uid = tag.find(".group-unique-id input"); - const input_hwid = tag.find(".group-hwid input"); - const input_reason = tag.find(".group-reason textarea"); - const input_global = tag.find(".group-global input"); - const input_duration_value = tag.find(".group-duration input"); - const input_duration_type = tag.find(".group-duration select"); - - button_apply.on('click', event => { - if(!button_apply_state[0] || button_apply_state_index != 0) return; - - const data: BanEntry = { - banid: 0, - enforcements: 0, - } as any; - - if(input_global.prop('checked')) - data.server_id = 0; - - if(input_ip.val()) - data.ip = input_ip.val() as any; - - if(input_name.val()) - data.name = input_name.val() as any; - - if(input_uid.val()) - data.unique_id = input_uid.val() as any; - - if(input_hwid.val()) - data.hardware_id = input_hwid.val() as any; - - if(input_reason.val()) - data.reason = input_reason.val() as any; - - data.timestamp_created = Date.now(); - - data.timestamp_expire = input_duration_type.val() === "perm" ? 0 : (data.timestamp_created + 1000 * parseInt(input_duration_type.find("option[value='" + input_duration_type.val() + "']").attr("duration-scale")) * parseInt(input_duration_value.val() as string)); - //TODO: input_interpret (Currently not supported by TeaSpeak) - - controller.add_ban(data).then(() => { - input_name.val(null); - input_ip.val(null); - input_uid.val(null); - input_hwid.val(null); - input_reason.val(null); - input_duration_value.val(1); - update_banlist(); - - createInfoModal(tr("Ban successfully added"), tr("Your ban has been successfully added.")).open(); - }).catch(error => { - log.error(LogCategory.CLIENT, tr("Failed to add ban: %o"), error); - if(error instanceof CommandResult) - error = error.id === ErrorID.PERMISSION_ERROR ? "no permissions" : error.extra_message || error.message; - createErrorModal(tr("Failed to add ban"), MessageHelper.formatMessage(tr("Failed to add ban. {:br:}Error: {}"), error)).open(); - }); - }); - } - - /* the banlist filter */ - { - const input_filter = container_ban.find(".container-filter input").on('change keyup', () => update_ban_filter()); - const option_show_own = container_ban.find(".option-show-own").on('change keyup', () => update_ban_filter()); - const option_hightlight_own = container_ban.find(".option-highlight-own").on('change keyup', () => update_ban_filter()); - - update_ban_filter = () => { - const text = (input_filter.val() as string || "").toLowerCase(); - const flag_show_own = option_show_own.prop('checked'); - const flag_hightlight_own = option_hightlight_own.prop('checked'); - - let count = 0; - for(const entry of callback_ban_filter) - if(entry(text, flag_show_own, flag_hightlight_own)) - count++; - if(callback_ban_filter.length != 0) { - if(count > 0) - container_ban_entries_empty.hide(); - else - container_ban_entries_empty.show().find("a").text(tr("No bans found")); - } - }; - } - - /* the trigger list filter */ - { - const input_filter = container_trigger.find(".container-filter input").on('change keyup', () => update_trigger_filter()); - const option_hightlight_cause = container_trigger.find(".option-highlight-cause").on('change keyup', () => update_trigger_filter()); - const button_close = container_trigger.find(".container-close"); - - update_trigger_filter = () => { - const text = (input_filter.val() as string || "").toLowerCase(); - - let count = 0; - for(const entry of callback_trigger_filter) - if(entry(text)) - count++; - if(callback_trigger_filter.length != 0) { - if(count > 0) - container_trigger_entries_empty.hide(); - else - container_trigger_entries_empty.show().find("a").text(tr("No trigger events found")); - } - container_trigger.find(".container-list").toggleClass('highlight', option_hightlight_cause.prop('checked')); - }; - - button_close.on('click', () => container_trigger.hide()); - } - - template.find(".button-refresh-banlist").on('click', event => update_banlist(selected_ban ? selected_ban.banid : undefined)); - template.find(".button-refresh-triggerlist").on('click', event => update_triggerlist()); - - /* initialize */ - category_add.trigger('click'); - update_edit_window(false); - update_banlist(); - - tooltip(template); - return template.children(); - } + modal = createModal({ + header: tr("Server Banlist"), + body: () => generate_dom(controller), + footer: null, + + width: '60em' + }); + + client.serverConnection.command_handler_boss().register_single_handler(single_ban_handler); + client.serverConnection.command_handler_boss().register_single_handler(single_trigger_handler); + modal.close_listener.push(() => { + client.serverConnection.command_handler_boss().remove_single_handler(single_ban_handler); + client.serverConnection.command_handler_boss().remove_single_handler(single_trigger_handler); + }); + + //TODO: Test without dividerfy! + modal.htmlTag.dividerfy(); + modal.htmlTag.find(".modal-body").addClass("modal-ban-list"); + modal.open(); } -//container-triggerlist \ No newline at end of file +interface BanEntry { + server_id: number; + banid: number; + + name?: string; + name_type?: number; + + unique_id?: string; + ip?: string; + hardware_id?: string; + + reason: string; + invoker_name: string; + invoker_unique_id?: string; + invoker_database_id?: number; + + timestamp_created: number; + timestamp_expire: number; + + enforcements: number; + + flag_own?: boolean; +} + +interface TriggerEntry { + unique_id?: string; + client_nickname?: string; + hardware_id?: string; + connection_ip: string; + + timestamp: number; +} + +interface BanListController { + request_list(callback_bans: (entries: BanEntry[]) => any) : Promise; + request_trigger_list(ban: {ban_id: number, server_id: number | undefined}, callback_triggers: (entries: TriggerEntry[]) => any) : Promise; + + max_bantime() : Promise; + permission_edit() : Promise; + permission_add() : Promise; + + add_ban(entry: BanEntry) : Promise; + edit_ban(data: any) : Promise; + delete_ban(entry_id: number, server_id: number | undefined) : Promise; +} + +//Note: This object must be sorted (from shortest to longest)! +export const duration_data = { + "sec": { + "text": tr("Seconds"), + "1-text": tr("Second"), + + scale: 1 + }, + "min": { + "text": tr("Minutes"), + "1-text": tr("Minute"), + + scale: 60 + }, + "hours": { + "text": tr("Hours"), + "1-text": tr("Hour"), + + scale: 3600 + }, + "days": { + "text": tr("Days"), + "1-text": tr("Day"), + + scale: 86400 + }, +}; + +declare const moment; +function generate_dom(controller: BanListController) : JQuery { + const template = $("#tmpl_ban_list").renderTag(); + + let callback_ban_filter: ((text: string, flag_own: boolean, highlight_own: boolean) => boolean)[] = []; + let callback_trigger_filter: ((text: string) => boolean)[] = []; + let selected_ban: BanEntry | undefined; + let update_edit_window: (switch_to: boolean) => any; + let update_ban_filter: () => any; + let update_trigger_filter: () => any; + + const container_ban = template.find(".container-banlist"); + const container_ban_entries = container_ban.find(".container-list .body"); + const container_ban_entries_empty = container_ban.find(".container-list .container-empty"); + const container_ban_entries_error = container_ban.find(".container-list .container-error"); + + const container_trigger = template.find(".container-triggerlist").hide(); + const container_trigger_entries = container_trigger.find(".container-list .body"); + const container_trigger_entries_empty = container_trigger.find(".container-list .container-empty"); + const container_trigger_entries_error = container_trigger.find(".container-list .container-error"); + + const button_apply = template.find(".button-apply"); + let button_apply_state = [false, false]; /* first index is add; second index is edit */ + let update_category_inputs: (() => any)[] = [undefined, undefined]; + let button_apply_state_index = 1; + + const category_add = template.find(".left .head .category-add"); + const category_edit = template.find(".left .head .category-edit"); + const container_add = template.find(".left .container-add"); + const container_add_no_permissions = template.find(".left .container-add .container-no-permissions"); + const container_edit = template.find(".left .container-edit"); + + const seperator_top = template.find(".container-seperator .top"); + + /* [local; global] */ + let permission_edit: boolean[] = [false, false], permission_add: boolean[] = [false, false]; + + container_add_no_permissions.hide(); + controller.permission_add().then(result => permission_add = result).catch(error => { + log.error(LogCategory.CLIENT, tr("Failed to query ban add permissions: %o"), error); + }).then(() => { + if(permission_add[0] !== permission_add[1]) { + const input_global = container_add.find(".group-global input"); + input_global.prop("checked", permission_add[1]).prop("disabled", true).firstParent(".checkbox").addClass("disabled"); + } else if(!permission_add[0]) + container_add_no_permissions.show(); + }); + + controller.permission_edit().then(result => permission_edit = result).catch(error => { + log.error(LogCategory.CLIENT, tr("Failed to query ban edit permissions: %o"), error); + }).then(() => { + if(selected_ban) update_edit_window(false); + }); + + /* category switch */ + { + category_add.on('click', event => { + container_add.removeClass("hidden"); + category_add.addClass("selected"); + + container_edit.addClass("hidden"); + category_edit.removeClass("selected"); + + seperator_top.css({opacity: 1}); + + button_apply_state_index = 0; + button_apply.prop("disabled", !button_apply_state[0]).text(tr("Add ban")); + update_category_inputs[button_apply_state_index](); + }); + + category_edit.on('click', event => { + if(!selected_ban) return; + + container_add.addClass("hidden"); + category_add.removeClass("selected"); + + container_edit.removeClass("hidden"); + category_edit.addClass("selected"); + + seperator_top.css({opacity: 0}); + + button_apply_state_index = 1; + button_apply.prop("disabled", !button_apply_state[1]).text(tr("Save ban")); + update_category_inputs[button_apply_state_index](); + }); + } + + const build_ban_entry = (entry: BanEntry, selected: boolean) => { + let button_delete; + const tag = $.spawn("div").addClass("entry" + (entry.server_id > 0 ? "" : " global") + (selected ? " selected" : "")).append( + $.spawn("div").addClass("column column-key").append( + entry.name ? $.spawn("div").append(entry.name) : undefined, + entry.ip ? $.spawn("div").append(entry.ip) : undefined, + entry.unique_id ? $.spawn("div").append(entry.unique_id) : undefined, + entry.hardware_id ? $.spawn("div").append(entry.hardware_id) : undefined + ), + $.spawn("div").addClass("column column-reason").text(entry.reason), + $.spawn("div").addClass("column column-expires").text(entry.timestamp_expire ? moment(entry.timestamp_expire).format('DD.MM.YYYY hh:mm') : tr("Never")), + $.spawn("div").addClass("column column-delete").append( + button_delete = $.spawn("div").addClass("button-delete").append( + $.spawn("div").addClass("icon_em client-delete") + ) + ) + ); + + tag.on('click', event => { + if(selected_ban === entry || event.isDefaultPrevented()) return; + selected_ban = entry; + + container_ban_entries.find(".entry.selected").removeClass("selected"); + tag.addClass("selected"); + + update_edit_window(true); + }); + + button_delete.on('click', event => { + event.preventDefault(); + + controller.delete_ban(entry.banid, entry.server_id).then(() => { + tag.css({opacity: 1}).animate({opacity: 0}, 250, () => tag.animate({"max-height": 0}, 250, () => tag.remove())); + if(entry === selected_ban) { + selected_ban = undefined; + update_edit_window(false); + } + }).catch(error => { + log.error(LogCategory.CLIENT, tr("Failed to delete ban: %o"), error); + if(error instanceof CommandResult) + error = error.id === ErrorID.PERMISSION_ERROR ? "no permissions" : error.extra_message || error.message; + createErrorModal(tr("Failed to delete ban"), formatMessage(tr("Failed to delete ban. {:br:}Error: {}"), error)).open(); + }); + }); + + if(selected) { + selected_ban = entry; + update_edit_window(false); + } + + const lower_mesh = + (entry.reason || "").toLowerCase() + " " + + (entry.unique_id || "").toLowerCase() + " " + + (entry.name || "").toLowerCase() + " " + + (entry.ip || "").toLowerCase() + " " + + (entry.hardware_id || "").toLowerCase(); + callback_ban_filter.push((text, flag_own, highlight_own) => { + if(text && lower_mesh.indexOf(text) == -1) { + tag.hide(); + return false; + } + + if(flag_own && !entry.flag_own) { + tag.hide(); + return false; + } + + tag.show().toggleClass( + "highlight", + highlight_own && + entry.flag_own + ); + return true; + }); + + return tag; + }; + + const update_banlist = (selected_ban?: number) => { + callback_ban_filter = []; + + container_ban_entries.find(".entry").remove(); + container_ban_entries_error.hide(); + container_ban_entries_empty.show().find("a").text(tr("Loading...")); + + let bans = []; + controller.request_list(_bans => bans.push(..._bans)).then(() => { + if(bans.length) { + container_ban_entries.append(...bans.map(e => build_ban_entry(e, e.banid === selected_ban))); + container_ban_entries_empty.hide(); + } else { + container_ban_entries_empty.find("a").text(tr("No bans registered")); + } + update_ban_filter(); + }).catch(error => { + log.info(LogCategory.CLIENT, tr("Failed to update ban list: %o"), error); + if(error instanceof CommandResult) + error = error.id === ErrorID.PERMISSION_ERROR ? tr("no permissions") : error.extra_message || error.message; + container_ban_entries_error.show().find("a").text(tr("Failed to receive banlist: ") + error); + container_ban_entries_empty.hide(); + }); + }; + + const build_trigger_entry = (entry: TriggerEntry) => { + const spawn_key_value = (key, value, reason) => { + return $.spawn("div").addClass("property").toggleClass("highlighted", reason).append( + $.spawn("div").addClass("key").text(key + ": "), + $.spawn("div").addClass("value").text(value) + ); + }; + + let cause_name = !!selected_ban.name && !!entry.client_nickname.match(selected_ban.name); + let cause_uid = !cause_name && !!selected_ban.unique_id && selected_ban.unique_id.toLowerCase() === (entry.unique_id || "").toLowerCase(); + let cause_ip = !cause_uid && !!selected_ban.ip && selected_ban.ip.toLowerCase() === (entry.connection_ip || "").toLowerCase(); + let cause_hwid = !cause_ip && !!selected_ban.hardware_id && selected_ban.hardware_id.toLowerCase() === (entry.hardware_id || "").toLowerCase(); + + /* we guess that IP is the cause because we dont see the IP and there is no other reason */ + if(!cause_name && !cause_uid && !cause_ip && !cause_hwid && entry.connection_ip === "hidden") + cause_ip = true; + + const time_str = moment(entry.timestamp).format('DD.MM.YYYY hh:mm'); + + const tag = $.spawn("div").addClass("entry").append( + $.spawn("div").addClass("column column-properties").append( + entry.client_nickname ? spawn_key_value(tr("Nickname"), entry.client_nickname, cause_name) : undefined, + entry.connection_ip ? spawn_key_value(tr("IP"), entry.connection_ip, cause_ip) : undefined, + entry.unique_id ? spawn_key_value(tr("Unique ID"), entry.unique_id, cause_uid) : undefined, + entry.hardware_id ? spawn_key_value(tr("Hardware ID"), entry.hardware_id, cause_hwid) : undefined + ), + $.spawn("div").addClass("column column-timestamp").text(time_str) + ); + + const lower_mesh = + (entry.unique_id || "").toLowerCase() + " " + + (entry.client_nickname || "").toLowerCase() + " " + + (entry.connection_ip || "").toLowerCase() + " " + + (entry.hardware_id || "").toLowerCase() + " " + + time_str + " " + + entry.timestamp; + callback_trigger_filter.push(text => { + if(text && lower_mesh.indexOf(text) == -1) { + tag.hide(); + return false; + } + + tag.show(); + return true; + }); + + return tag; + }; + + const update_triggerlist = () => { + callback_trigger_filter = []; + + container_trigger_entries.find(".entry").remove(); + container_trigger_entries_error.hide(); + container_trigger_entries_empty.show().find("a").text(tr("Loading...")); + + let triggers = []; + controller.request_trigger_list({ + ban_id: selected_ban.banid, + server_id: selected_ban.server_id + }, _triggers => triggers.push(..._triggers)).then(() => { + if(triggers.length) { + container_trigger_entries.append(...triggers.sort((a, b) => b.timestamp - a.timestamp).map(e => build_trigger_entry(e))); + container_trigger_entries_empty.hide(); + } else { + container_trigger_entries_empty.find("a").text(tr("No triggers logged")); + } + + update_trigger_filter(); + }).catch(error => { + log.info(LogCategory.CLIENT, tr("Failed to update trigger list: %o"), error); + if(error instanceof CommandResult) + error = error.id === ErrorID.PERMISSION_ERROR ? tr("no permissions") : error.extra_message || error.message; + container_trigger_entries_error.show().find("a").text(tr("Failed to receive trigger list: ") + error); + container_trigger_entries_empty.hide(); + }); + }; + + const show_triggerlist = () => { + container_trigger.show(); + }; + + /* general input field rules */ + const initialize_fields = (tag: JQuery, index: number) => { + const input_name = tag.find(".group-name input").on('change keyup', () => update_category_inputs[index]()); + const input_ip = tag.find(".group-ip input").on('change keyup', () => update_category_inputs[index]()); + const input_uid = tag.find(".group-unique-id input").on('change keyup', () => update_category_inputs[index]()); + const input_hwid = tag.find(".group-hwid input").on('change keyup', () => update_category_inputs[index]()); + const input_reason = tag.find(".group-reason textarea").on('change keyup', () => update_category_inputs[index]()); + //const input_global = tag.find(".group-global input"); + const input_duration_value = tag.find(".group-duration input").on('change keyup', () => update_category_inputs[index]()); + const input_duration_type = tag.find(".group-duration select").on('change keyup', () => update_category_inputs[index]()); + const tooltip_duration_max = tag.find(".tooltip-max-time a.max"); + + update_category_inputs[index] = () => { + let _criteria_set = false; + let _input_invalid = false; + + { + //TODO: Check if in regex mode or not + const value = input_name.val() as string || ""; + if(value.length > 255) { + _input_invalid = true; + input_name.firstParent(".input-boxed").addClass("is-invalid"); + } else { + _criteria_set = _criteria_set || !!value; + input_name.firstParent(".input-boxed").removeClass("is-invalid"); + } + } + + { + //TODO: Check if in regex mode or not + const value = input_ip.val() as string || ""; + if(value.length > 255) { + _input_invalid = true; + input_ip.firstParent(".input-boxed").addClass("is-invalid"); + } else { + _criteria_set = _criteria_set || !!value; + input_ip.firstParent(".input-boxed").removeClass("is-invalid"); + } + } + + { + const value = input_uid.val() as string || ""; + try { + if(value && atob(value).length != 20) throw ""; + + _criteria_set = _criteria_set || !!value; + input_uid.firstParent(".input-boxed").removeClass("is-invalid"); + } catch(e) { + _input_invalid = true; + input_uid.firstParent(".input-boxed").addClass("is-invalid"); + } + } + + { + const value = input_hwid.val() as string || ""; + if(value.length > 255) { + _input_invalid = true; + input_hwid.firstParent(".input-boxed").addClass("is-invalid"); + } else { + _criteria_set = _criteria_set || !!value; + input_hwid.firstParent(".input-boxed").removeClass("is-invalid"); + } + } + + { + const value = input_reason.val() as string || ""; + if(value.length > 512) { + _input_invalid = true; + input_reason.firstParent(".input-boxed").addClass("is-invalid"); + } else { + input_reason.firstParent(".input-boxed").removeClass("is-invalid"); + } + } + + { + const type = input_duration_type.val() as string; + const value = parseInt(input_duration_value.val() as string); + const disabled = input_duration_type.prop("disabled"); + + input_duration_value.prop("disabled", type === "perm" || disabled).firstParent(".input-boxed").toggleClass("disabled", type === "perm" || disabled); + if(type !== "perm") { + if(input_duration_value.attr("x-saved-value")) { + input_duration_value.val(parseInt(input_duration_value.attr("x-saved-value"))); + input_duration_value.attr("x-saved-value", null); + } + + const selected_option = input_duration_type.find("option[value='" + type + "']"); + const max = parseInt(selected_option.attr("duration-max")); + + input_duration_value.attr("max", max); + if((value > max && max != -1) || value < 1) { + _input_invalid = true; + input_duration_value.firstParent(".input-boxed").addClass("is-invalid"); + } else { + input_duration_value.firstParent(".input-boxed").removeClass("is-invalid"); + } + + if(max != -1) + tooltip_duration_max.html(tr("You're allowed to ban a maximum of ") + "" + max + " " + duration_data[type][max == 1 ? "1-text" : "text"] + ""); + else + tooltip_duration_max.html(tr("You're allowed to ban permanent.")); + } else { + if(value && !Number.isNaN(value)) + input_duration_value.attr("x-saved-value", value); + input_duration_value.attr("placeholder", tr("for ever")).val(null); + tooltip_duration_max.html(tr("You're allowed to ban permanent.")); + } + } + + button_apply.prop("disabled", !(button_apply_state[button_apply_state_index] = _criteria_set && !_input_invalid)); + }; + + /* initialize ban time */ + controller.max_bantime().catch(error => { /* TODO: Error handling? */ return 0; }).then(max_time => { + let unlimited = max_time == 0 || max_time == -1; + if(unlimited) max_time = 0; + + for(const value of Object.keys(duration_data)) { + input_duration_type.find("option[value='" + value + "']") + .prop("disabled", !unlimited && max_time >= duration_data[value].scale) + .attr("duration-scale", duration_data[value].scale) + .attr("duration-max", unlimited ? -1 : Math.floor(max_time / duration_data[value].scale)); + } + + input_duration_type.find("option[value='perm']") + .prop("disabled", !unlimited) + .attr("duration-scale", 0) + .attr("duration-max", -1); + }); + }; + initialize_fields(container_add, 0); + initialize_fields(container_edit, 1); + + /* the edit "handler" */ + { + const tag = container_edit; + const input_name = tag.find(".group-name input"); + const input_ip = tag.find(".group-ip input"); + const input_interpret = tag.find(".group-interpret select"); + const input_uid = tag.find(".group-unique-id input"); + const input_hwid = tag.find(".group-hwid input"); + const input_reason = tag.find(".group-reason textarea"); + const input_global = tag.find(".group-global input"); + const input_duration_value = tag.find(".group-duration input"); + const input_duration_type = tag.find(".group-duration select"); + const tooltip_duration_detailed = tag.find(".tooltip-max-time a.detailed"); + + const label_enforcement_count = tag.find(".group-enforcements .value a"); + const button_enforcement_list = tag.find(".button-enforcement-list"); + + const container_creator = tag.find(".group-creator .value"); + + update_edit_window = (switch_to: boolean) => { + category_edit.toggleClass("disabled", !selected_ban); + + const editable = selected_ban && selected_ban.server_id === 0 ? permission_edit[1] : permission_edit[0]; + + input_name.val(selected_ban ? selected_ban.name : null).prop("disabled", !editable).firstParent(".input-boxed").toggleClass("disabled", !editable); + input_ip.val(selected_ban ? selected_ban.ip : null).prop("disabled", !editable).firstParent(".input-boxed").toggleClass("disabled", !editable); + input_uid.val(selected_ban ? selected_ban.unique_id : null).prop("disabled", !editable).firstParent(".input-boxed").toggleClass("disabled", !editable); + input_hwid.val(selected_ban ? selected_ban.hardware_id : null).prop("disabled", !editable).firstParent(".input-boxed").toggleClass("disabled", !editable); + input_reason.val(selected_ban ? selected_ban.reason : null).prop("disabled", !editable).firstParent(".input-boxed").toggleClass("disabled", !editable); + + input_interpret.find("option").eq(selected_ban && typeof(selected_ban.name_type) === "number" ? selected_ban.name_type : 2).prop("selected", true).prop("disabled", !editable).firstParent(".input-boxed").toggleClass("disabled", !editable); + label_enforcement_count.text((selected_ban ? selected_ban.enforcements : 0) || 0); + button_enforcement_list.prop("disabled", !selected_ban || selected_ban.enforcements == 0); + + input_global.prop("checked", selected_ban && selected_ban.server_id == 0); + + input_duration_type.prop("disabled", !editable).firstParent(".input-boxed").toggleClass("disabled", !editable); + input_duration_value.prop("disabled", !editable).firstParent(".input-boxed").toggleClass("disabled", !editable); + + if(selected_ban) { + if(selected_ban.timestamp_expire > selected_ban.timestamp_created) { + const duration = Math.ceil((selected_ban.timestamp_expire - selected_ban.timestamp_created) / 1000); + + const periods = Object.keys(duration_data); + let index; + for(index = 0; index < periods.length; index++) { + if(duration_data[periods[index]].scale > duration + 1 || ((duration + 1) % duration_data[periods[index]].scale) > 1.9) + break; + } + if(index > 0) index--; + input_duration_type.find("option[value='" + periods[index] + "']").prop("selected", true); + input_duration_value.val(Math.ceil(duration / duration_data[periods[index]].scale)); + tooltip_duration_detailed.text($.spawn("div").append(...formatMessage(tr("The ban lasts for exact {}."), format_time(duration * 1000, "never"))).text()); + } else { + tooltip_duration_detailed.text(tr("The ban is forever.")); + input_duration_value.attr("placeholder", tr("for ever")).val(null).prop('disabled', true); + input_duration_type.find("option[value='perm']").prop("selected", true); + } + } + + container_creator.empty(); + if(selected_ban) { + container_creator.append( + htmltags.generate_client_object({ + client_id: 0, + client_unique_id: selected_ban.invoker_unique_id, + client_name: selected_ban.invoker_name, + add_braces: false + }) + ); + } + + if(switch_to) + category_edit.trigger('click'); + }; + + button_apply.on('click', event => { + if (!button_apply_state[1] || button_apply_state_index != 1) return; + + const data = {banid: selected_ban.banid}; + + if(input_ip.val() != selected_ban.ip) + data["ip"] = input_ip.val(); + + if(input_name.val() != selected_ban.name) + data["name"] = input_name.val(); + + if(input_uid.val() != selected_ban.unique_id) + data["uid"] = input_uid.val(); + + if(input_hwid.val() != selected_ban.hardware_id) + data["hwid"] = input_hwid.val(); + + if(input_reason.val() != selected_ban.reason) + data["banreason"] = input_reason.val(); + + if(input_reason.val() != selected_ban.reason) + data["reason"] = input_reason.val(); + + const duration = input_duration_type.val() === "perm" ? 0 : (1000 * parseInt(input_duration_type.find("option[value='" + input_duration_type.val() + "']").attr("duration-scale")) * parseInt(input_duration_value.val() as string)); + if(selected_ban.timestamp_expire > 0 ? (selected_ban.timestamp_expire - selected_ban.timestamp_created != duration) : duration != 0) + data["time"] = Math.floor(duration / 1000); + + controller.edit_ban(data).then(() => { + update_banlist(selected_ban ? selected_ban.banid : undefined); + + selected_ban = undefined; + update_edit_window(false); + + createInfoModal(tr("Ban successfully edited"), tr("Your ban has been successfully edited.")).open(); + }).catch(error => { + log.error(LogCategory.CLIENT, tr("Failed to edited ban: %o"), error); + if(error instanceof CommandResult) + error = error.id === ErrorID.PERMISSION_ERROR ? "no permissions" : error.extra_message || error.message; + createErrorModal(tr("Failed to edited ban"), formatMessage(tr("Failed to edited ban. {:br:}Error: {}"), error)).open(); + }); + }); + + button_enforcement_list.on('click', () => { + update_triggerlist(); + show_triggerlist(); + }); + } + + /* the create "handler" */ + { + const tag = container_add; + const input_name = tag.find(".group-name input"); + const input_ip = tag.find(".group-ip input"); + const input_interpret = tag.find(".group-interpret select"); + const input_uid = tag.find(".group-unique-id input"); + const input_hwid = tag.find(".group-hwid input"); + const input_reason = tag.find(".group-reason textarea"); + const input_global = tag.find(".group-global input"); + const input_duration_value = tag.find(".group-duration input"); + const input_duration_type = tag.find(".group-duration select"); + + button_apply.on('click', event => { + if(!button_apply_state[0] || button_apply_state_index != 0) return; + + const data: BanEntry = { + banid: 0, + enforcements: 0, + } as any; + + if(input_global.prop('checked')) + data.server_id = 0; + + if(input_ip.val()) + data.ip = input_ip.val() as any; + + if(input_name.val()) + data.name = input_name.val() as any; + + if(input_uid.val()) + data.unique_id = input_uid.val() as any; + + if(input_hwid.val()) + data.hardware_id = input_hwid.val() as any; + + if(input_reason.val()) + data.reason = input_reason.val() as any; + + data.timestamp_created = Date.now(); + + data.timestamp_expire = input_duration_type.val() === "perm" ? 0 : (data.timestamp_created + 1000 * parseInt(input_duration_type.find("option[value='" + input_duration_type.val() + "']").attr("duration-scale")) * parseInt(input_duration_value.val() as string)); + //TODO: input_interpret (Currently not supported by TeaSpeak) + + controller.add_ban(data).then(() => { + input_name.val(null); + input_ip.val(null); + input_uid.val(null); + input_hwid.val(null); + input_reason.val(null); + input_duration_value.val(1); + update_banlist(); + + createInfoModal(tr("Ban successfully added"), tr("Your ban has been successfully added.")).open(); + }).catch(error => { + log.error(LogCategory.CLIENT, tr("Failed to add ban: %o"), error); + if(error instanceof CommandResult) + error = error.id === ErrorID.PERMISSION_ERROR ? "no permissions" : error.extra_message || error.message; + createErrorModal(tr("Failed to add ban"), formatMessage(tr("Failed to add ban. {:br:}Error: {}"), error)).open(); + }); + }); + } + + /* the banlist filter */ + { + const input_filter = container_ban.find(".container-filter input").on('change keyup', () => update_ban_filter()); + const option_show_own = container_ban.find(".option-show-own").on('change keyup', () => update_ban_filter()); + const option_hightlight_own = container_ban.find(".option-highlight-own").on('change keyup', () => update_ban_filter()); + + update_ban_filter = () => { + const text = (input_filter.val() as string || "").toLowerCase(); + const flag_show_own = option_show_own.prop('checked'); + const flag_hightlight_own = option_hightlight_own.prop('checked'); + + let count = 0; + for(const entry of callback_ban_filter) + if(entry(text, flag_show_own, flag_hightlight_own)) + count++; + if(callback_ban_filter.length != 0) { + if(count > 0) + container_ban_entries_empty.hide(); + else + container_ban_entries_empty.show().find("a").text(tr("No bans found")); + } + }; + } + + /* the trigger list filter */ + { + const input_filter = container_trigger.find(".container-filter input").on('change keyup', () => update_trigger_filter()); + const option_hightlight_cause = container_trigger.find(".option-highlight-cause").on('change keyup', () => update_trigger_filter()); + const button_close = container_trigger.find(".container-close"); + + update_trigger_filter = () => { + const text = (input_filter.val() as string || "").toLowerCase(); + + let count = 0; + for(const entry of callback_trigger_filter) + if(entry(text)) + count++; + if(callback_trigger_filter.length != 0) { + if(count > 0) + container_trigger_entries_empty.hide(); + else + container_trigger_entries_empty.show().find("a").text(tr("No trigger events found")); + } + container_trigger.find(".container-list").toggleClass('highlight', option_hightlight_cause.prop('checked')); + }; + + button_close.on('click', () => container_trigger.hide()); + } + + template.find(".button-refresh-banlist").on('click', event => update_banlist(selected_ban ? selected_ban.banid : undefined)); + template.find(".button-refresh-triggerlist").on('click', event => update_triggerlist()); + + /* initialize */ + category_add.trigger('click'); + update_edit_window(false); + update_banlist(); + + tooltip.initialize(template); + return template.children(); +} \ No newline at end of file diff --git a/shared/js/ui/modal/ModalBookmarks.ts b/shared/js/ui/modal/ModalBookmarks.ts index 950b3658..3f9fe5cd 100644 --- a/shared/js/ui/modal/ModalBookmarks.ts +++ b/shared/js/ui/modal/ModalBookmarks.ts @@ -1,368 +1,383 @@ -/// -/// -/// +import {createInputModal, createModal, Modal} from "tc-shared/ui/elements/Modal"; +import { + Bookmark, + bookmarks, + BookmarkType, boorkmak_connect, create_bookmark, create_bookmark_directory, + delete_bookmark, + DirectoryBookmark, + save_bookmark +} from "tc-shared/bookmarks"; +import {connection_log, Regex} from "tc-shared/ui/modal/ModalConnect"; +import {IconManager} from "tc-shared/FileManager"; +import {profiles} from "tc-shared/profiles/ConnectionProfile"; +import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; +import {Settings, settings} from "tc-shared/settings"; +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; +import * as i18nc from "tc-shared/i18n/country"; +import {formatMessage} from "tc-shared/ui/frames/chat"; +import {control_bar} from "tc-shared/ui/frames/ControlBar"; +import * as top_menu from "../frames/MenuBar"; -namespace Modals { - export function spawnBookmarkModal() { - let modal: Modal; - modal = createModal({ - header: tr("Manage bookmarks"), - body: () => { - let template = $("#tmpl_manage_bookmarks").renderTag({ }); - let selected_bookmark: bookmarks.Bookmark | bookmarks.DirectoryBookmark | undefined; +export function spawnBookmarkModal() { + let modal: Modal; + modal = createModal({ + header: tr("Manage bookmarks"), + body: () => { + let template = $("#tmpl_manage_bookmarks").renderTag({ }); + let selected_bookmark: Bookmark | DirectoryBookmark | undefined; - const button_delete = template.find(".button-delete"); - const button_add_folder = template.find(".button-add-folder"); - const button_add_bookmark = template.find(".button-add-bookmark"); + const button_delete = template.find(".button-delete"); + const button_add_folder = template.find(".button-add-folder"); + const button_add_bookmark = template.find(".button-add-bookmark"); - const button_connect = template.find(".button-connect"); - const button_connect_tab = template.find(".button-connect-tab"); + const button_connect = template.find(".button-connect"); + const button_connect_tab = template.find(".button-connect-tab"); - const label_bookmark_name = template.find(".header .container-name"); - const label_server_address = template.find(".header .container-address"); + const label_bookmark_name = template.find(".header .container-name"); + const label_server_address = template.find(".header .container-address"); - const input_bookmark_name = template.find(".input-bookmark-name"); - const input_connect_profile = template.find(".input-connect-profile"); + const input_bookmark_name = template.find(".input-bookmark-name"); + const input_connect_profile = template.find(".input-connect-profile"); - const input_server_address = template.find(".input-server-address"); - const input_server_password = template.find(".input-server-password"); + const input_server_address = template.find(".input-server-address"); + const input_server_password = template.find(".input-server-password"); - const label_server_name = template.find(".server-name"); - const label_server_region = template.find(".server-region"); - const label_last_ping = template.find(".server-ping"); - const label_client_count = template.find(".server-client-count"); - const label_connection_count = template.find(".server-connection-count"); + const label_server_name = template.find(".server-name"); + const label_server_region = template.find(".server-region"); + const label_last_ping = template.find(".server-ping"); + const label_client_count = template.find(".server-client-count"); + const label_connection_count = template.find(".server-connection-count"); - const update_buttons = () => { - button_delete.prop("disabled", !selected_bookmark); - button_connect.prop("disabled", !selected_bookmark || selected_bookmark.type !== bookmarks.BookmarkType.ENTRY); - button_connect_tab.prop("disabled", !selected_bookmark || selected_bookmark.type !== bookmarks.BookmarkType.ENTRY); - }; + const update_buttons = () => { + button_delete.prop("disabled", !selected_bookmark); + button_connect.prop("disabled", !selected_bookmark || selected_bookmark.type !== BookmarkType.ENTRY); + button_connect_tab.prop("disabled", !selected_bookmark || selected_bookmark.type !== BookmarkType.ENTRY); + }; - const update_connect_info = () => { - if(selected_bookmark && selected_bookmark.type === bookmarks.BookmarkType.ENTRY) { - const entry = selected_bookmark as bookmarks.Bookmark; + const update_connect_info = () => { + if(selected_bookmark && selected_bookmark.type === BookmarkType.ENTRY) { + const entry = selected_bookmark as Bookmark; - const history = connection_log.history().find(e => e.address.hostname === entry.server_properties.server_address && e.address.port === entry.server_properties.server_port); - if(history) { - label_server_name.text(history.name); - label_server_region.empty().append( - $.spawn("div").addClass("country flag-" + history.country.toLowerCase()), - $.spawn("div").text(i18n.country_name(history.country, tr("Global"))) - ); - label_client_count.text(history.clients_online + "/" + history.clients_total); - label_connection_count.empty().append( - ...MessageHelper.formatMessage(tr("You've connected {} times"), $.spawn("div").addClass("connect-count").text(history.total_connection)) - ); - } else { - label_server_name.text(tr("Unknown")); - label_server_region.empty().text(tr("Unknown")); - label_client_count.text(tr("Unknown")); - label_connection_count.empty().append( - ...MessageHelper.formatMessage(tr("You {} connected to that server address"), $.spawn("div").addClass("connect-never").text("never")) - ); - } - label_last_ping.text(tr("Average ping isn't yet supported")); - } else { - label_server_name.text("--"); - label_server_region.text("--"); - label_last_ping.text("--"); - label_client_count.text("--"); - label_connection_count.text("--"); - } - }; - - const update_selected = () => { - input_bookmark_name.prop("disabled", !selected_bookmark); - input_connect_profile.prop("disabled", !selected_bookmark || selected_bookmark.type !== bookmarks.BookmarkType.ENTRY); - input_server_address.prop("disabled", !selected_bookmark || selected_bookmark.type !== bookmarks.BookmarkType.ENTRY); - input_server_password.prop("disabled", !selected_bookmark || selected_bookmark.type !== bookmarks.BookmarkType.ENTRY); - - if(selected_bookmark) { - input_bookmark_name.val(selected_bookmark.display_name); - label_bookmark_name.text(selected_bookmark.display_name); - } - - if(selected_bookmark && selected_bookmark.type === bookmarks.BookmarkType.ENTRY) { - const entry = selected_bookmark as bookmarks.Bookmark; - - const address = entry.server_properties.server_address + (entry.server_properties.server_port == 9987 ? "" : (" " + entry.server_properties.server_port)); - label_server_address.text(address); - input_server_address.val(address); - - let profile = input_connect_profile.find("option[value='" + entry.connect_profile + "']"); - if(profile.length == 0) - profile = input_connect_profile.find("option[value=default]"); - profile.prop("selected", true); - - input_server_password.val(entry.server_properties.server_password_hash || entry.server_properties.server_password ? "WolverinDEV" : ""); - } else { - input_server_password.val(""); - input_server_address.val(""); - input_connect_profile.find("option[value='no-value']").prop('selected', true); - label_server_address.text(" "); - } - - update_connect_info(); - }; - - const container_bookmarks = template.find(".container-bookmarks"); - const update_bookmark_list = (_current_selected: string) => { - container_bookmarks.empty(); - selected_bookmark = undefined; - update_selected(); - - const hide_links: boolean[] = []; - const build_entry = (entry: bookmarks.Bookmark | bookmarks.DirectoryBookmark, sibling_data: {first: boolean; last: boolean;}, index: number) => { - let container = $.spawn("div") - .addClass(entry.type === bookmarks.BookmarkType.ENTRY ? "bookmark" : "directory") - .addClass(index > 0 ? "linked" : "") - .addClass(sibling_data.first ? "link-start" : ""); - for (let i = 0; i < index; i++) { - container.append( - $.spawn("div") - .addClass("link") - .addClass(i + 1 === index ? " connected" : "") - .addClass(hide_links[i + 1] ? "hidden" : "") - ); - } - - if (entry.type === bookmarks.BookmarkType.ENTRY) { - const bookmark = entry as bookmarks.Bookmark; - container.append( - bookmark.last_icon_id ? - IconManager.generate_tag(IconManager.load_cached_icon(bookmark.last_icon_id || 0), {animate: false}) : - $.spawn("div").addClass("icon-container icon_em") - ); - } else { - container.append( - $.spawn("div").addClass("icon-container icon_em client-folder") - ); - } - - container.append( - $.spawn("div").addClass("name").attr("title", entry.display_name).text(entry.display_name) + const history = connection_log.history().find(e => e.address.hostname === entry.server_properties.server_address && e.address.port === entry.server_properties.server_port); + if(history) { + label_server_name.text(history.name); + label_server_region.empty().append( + $.spawn("div").addClass("country flag-" + history.country.toLowerCase()), + $.spawn("div").text(i18nc.country_name(history.country, tr("Global"))) ); + label_client_count.text(history.clients_online + "/" + history.clients_total); + label_connection_count.empty().append( + ...formatMessage(tr("You've connected {} times"), $.spawn("div").addClass("connect-count").text(history.total_connection)) + ); + } else { + label_server_name.text(tr("Unknown")); + label_server_region.empty().text(tr("Unknown")); + label_client_count.text(tr("Unknown")); + label_connection_count.empty().append( + ...formatMessage(tr("You {} connected to that server address"), $.spawn("div").addClass("connect-never").text("never")) + ); + } + label_last_ping.text(tr("Average ping isn't yet supported")); + } else { + label_server_name.text("--"); + label_server_region.text("--"); + label_last_ping.text("--"); + label_client_count.text("--"); + label_connection_count.text("--"); + } + }; - container.appendTo(container_bookmarks); - container.on('click', event => { - if(selected_bookmark === entry) - return; + const update_selected = () => { + input_bookmark_name.prop("disabled", !selected_bookmark); + input_connect_profile.prop("disabled", !selected_bookmark || selected_bookmark.type !== BookmarkType.ENTRY); + input_server_address.prop("disabled", !selected_bookmark || selected_bookmark.type !== BookmarkType.ENTRY); + input_server_password.prop("disabled", !selected_bookmark || selected_bookmark.type !== BookmarkType.ENTRY); - selected_bookmark = entry; - container_bookmarks.find(".selected").removeClass("selected"); - container.addClass("selected"); - update_buttons(); - update_selected(); - }); - if(entry.unique_id === _current_selected) - container.trigger('click'); + if(selected_bookmark) { + input_bookmark_name.val(selected_bookmark.display_name); + label_bookmark_name.text(selected_bookmark.display_name); + } - hide_links.push(sibling_data.last); - let cindex = 0; - const children = (entry as bookmarks.DirectoryBookmark).content || []; - for (const child of children) - build_entry(child, {first: cindex++ == 0, last: cindex == children.length}, index + 1); - hide_links.pop(); - }; + if(selected_bookmark && selected_bookmark.type === BookmarkType.ENTRY) { + const entry = selected_bookmark as Bookmark; + const address = entry.server_properties.server_address + (entry.server_properties.server_port == 9987 ? "" : (" " + entry.server_properties.server_port)); + label_server_address.text(address); + input_server_address.val(address); + + let profile = input_connect_profile.find("option[value='" + entry.connect_profile + "']"); + if(profile.length == 0) + profile = input_connect_profile.find("option[value=default]"); + profile.prop("selected", true); + + input_server_password.val(entry.server_properties.server_password_hash || entry.server_properties.server_password ? "WolverinDEV" : ""); + } else { + input_server_password.val(""); + input_server_address.val(""); + input_connect_profile.find("option[value='no-value']").prop('selected', true); + label_server_address.text(" "); + } + + update_connect_info(); + }; + + const container_bookmarks = template.find(".container-bookmarks"); + const update_bookmark_list = (_current_selected: string) => { + container_bookmarks.empty(); + selected_bookmark = undefined; + update_selected(); + + const hide_links: boolean[] = []; + const build_entry = (entry: Bookmark | DirectoryBookmark, sibling_data: {first: boolean; last: boolean;}, index: number) => { + let container = $.spawn("div") + .addClass(entry.type === BookmarkType.ENTRY ? "bookmark" : "directory") + .addClass(index > 0 ? "linked" : "") + .addClass(sibling_data.first ? "link-start" : ""); + for (let i = 0; i < index; i++) { + container.append( + $.spawn("div") + .addClass("link") + .addClass(i + 1 === index ? " connected" : "") + .addClass(hide_links[i + 1] ? "hidden" : "") + ); + } + + if (entry.type === BookmarkType.ENTRY) { + const bookmark = entry as Bookmark; + container.append( + bookmark.last_icon_id ? + IconManager.generate_tag(IconManager.load_cached_icon(bookmark.last_icon_id || 0), {animate: false}) : + $.spawn("div").addClass("icon-container icon_em") + ); + } else { + container.append( + $.spawn("div").addClass("icon-container icon_em client-folder") + ); + } + + container.append( + $.spawn("div").addClass("name").attr("title", entry.display_name).text(entry.display_name) + ); + + container.appendTo(container_bookmarks); + container.on('click', event => { + if(selected_bookmark === entry) + return; + + selected_bookmark = entry; + container_bookmarks.find(".selected").removeClass("selected"); + container.addClass("selected"); + update_buttons(); + update_selected(); + }); + if(entry.unique_id === _current_selected) + container.trigger('click'); + + hide_links.push(sibling_data.last); let cindex = 0; - const children = bookmarks.bookmarks().content; - for (const bookmark of children) - build_entry(bookmark, {first: cindex++ == 0, last: cindex == children.length}, 0); + const children = (entry as DirectoryBookmark).content || []; + for (const child of children) + build_entry(child, {first: cindex++ == 0, last: cindex == children.length}, index + 1); + hide_links.pop(); }; - /* generate profile list */ - { + let cindex = 0; + const children = bookmarks().content; + for (const bookmark of children) + build_entry(bookmark, {first: cindex++ == 0, last: cindex == children.length}, 0); + }; + + /* generate profile list */ + { + input_connect_profile.append( + $.spawn("option") + .attr("value", "no-value") + .text("") + .css("display", "none") + ); + for(const profile of profiles()) { input_connect_profile.append( $.spawn("option") - .attr("value", "no-value") - .text("") - .css("display", "none") + .attr("value", profile.id) + .text(profile.profile_name) ); - for(const profile of profiles.profiles()) { - input_connect_profile.append( - $.spawn("option") - .attr("value", profile.id) - .text(profile.profile_name) - ); + } + } + + /* buttons */ + { + button_delete.on('click', event => { + if(!selected_bookmark) return; + + if(selected_bookmark.type === BookmarkType.DIRECTORY && (selected_bookmark as DirectoryBookmark).content.length > 0) { + spawnYesNo(tr("Are you sure"), tr("Do you really want to delete this non empty directory?"), answer => { + if(answer) { + delete_bookmark(selected_bookmark); + save_bookmark(selected_bookmark); + update_bookmark_list(undefined); + } + }); + } else { + delete_bookmark(selected_bookmark); + save_bookmark(selected_bookmark); + update_bookmark_list(undefined); } - } - - /* buttons */ - { - button_delete.on('click', event => { - if(!selected_bookmark) return; - - if(selected_bookmark.type === bookmarks.BookmarkType.DIRECTORY && (selected_bookmark as bookmarks.DirectoryBookmark).content.length > 0) { - Modals.spawnYesNo(tr("Are you sure"), tr("Do you really want to delete this non empty directory?"), answer => { - if(answer) { - bookmarks.delete_bookmark(selected_bookmark); - bookmarks.save_bookmark(selected_bookmark); - update_bookmark_list(undefined); - } - }); - } else { - bookmarks.delete_bookmark(selected_bookmark); - bookmarks.save_bookmark(selected_bookmark); - update_bookmark_list(undefined); - } - }); - - button_add_folder.on('click', event => { - createInputModal(tr("Enter a folder name"), tr("Enter the folder name"), text => { - return true; - }, result => { - if(result) { - const mark = bookmarks.create_bookmark_directory( - selected_bookmark ? - selected_bookmark.type === bookmarks.BookmarkType.DIRECTORY ? - selected_bookmark as bookmarks.DirectoryBookmark : - selected_bookmark.parent : - bookmarks.bookmarks(), - result as string - ); - bookmarks.save_bookmark(mark); - update_bookmark_list(mark.unique_id); - } - }).open(); - }); - - button_add_bookmark.on('click', event => { - createInputModal(tr("Enter a bookmark name"), tr("Enter the bookmark name"), text => { - return true; - }, result => { - if(result) { - const mark = bookmarks.create_bookmark(result as string, - selected_bookmark ? - selected_bookmark.type === bookmarks.BookmarkType.DIRECTORY ? - selected_bookmark as bookmarks.DirectoryBookmark : - selected_bookmark.parent : - bookmarks.bookmarks(), { - server_password: "", - server_port: 9987, - server_address: "", - server_password_hash: "" - }, ""); - bookmarks.save_bookmark(mark); - update_bookmark_list(mark.unique_id); - } - }).open(); - }); - - button_connect_tab.on('click', event => { - bookmarks.boorkmak_connect(selected_bookmark as bookmarks.Bookmark, true); - modal.close(); - }).toggle(!settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION)); - - button_connect.on('click', event => { - bookmarks.boorkmak_connect(selected_bookmark as bookmarks.Bookmark, false); - modal.close(); - }); - } - - /* inputs */ - { - input_bookmark_name.on('change keydown', event => { - const name = input_bookmark_name.val() as string; - const valid = name.length > 3; - input_bookmark_name.firstParent(".input-boxed").toggleClass("is-invalid", !valid); - - if(event.type === "change" && valid) { - selected_bookmark.display_name = name; - label_bookmark_name.text(name); - } - }); - - input_server_address.on('change keydown', event => { - const address = input_server_address.val() as string; - const valid = !!address.match(Regex.IP_V4) || !!address.match(Regex.IP_V6) || !!address.match(Regex.DOMAIN); - input_server_address.firstParent(".input-boxed").toggleClass("is-invalid", !valid); - - if(valid) { - const entry = selected_bookmark as bookmarks.Bookmark; - let _v6_end = address.indexOf(']'); - let idx = address.lastIndexOf(':'); - if(idx != -1 && idx > _v6_end) { - entry.server_properties.server_port = parseInt(address.substr(idx + 1)); - entry.server_properties.server_address = address.substr(0, idx); - } else { - entry.server_properties.server_address = address; - entry.server_properties.server_port = 9987; - } - - label_server_address.text(entry.server_properties.server_address + (entry.server_properties.server_port == 9987 ? "" : (" " + entry.server_properties.server_port))); - update_connect_info(); - } - }); - - input_connect_profile.on('change', event => { - const id = input_connect_profile.val() as string; - const profile = profiles.profiles().find(e => e.id === id); - if(profile) { - (selected_bookmark as bookmarks.Bookmark).connect_profile = id; - } else { - log.warn(LogCategory.GENERAL, tr("Failed to change connect profile for profile %s to %s"), selected_bookmark.unique_id, id); - } - }) - } - - /* Arrow key navigation for the bookmark list */ - { - let _focused = false; - let _focus_listener; - let _key_listener; - - _focus_listener = event => { - _focused = false; - let element = event.target as HTMLElement; - while(element) { - if(element === container_bookmarks[0]) { - _focused = true; - break; - } - element = element.parentNode as HTMLElement; - } - }; - - _key_listener = event => { - if(!_focused) return; - - if(event.key.toLowerCase() === "arrowdown") { - container_bookmarks.find(".selected").next().trigger('click'); - } else if(event.key.toLowerCase() === "arrowup") { - container_bookmarks.find(".selected").prev().trigger('click'); - } - }; - - document.addEventListener('click', _focus_listener); - document.addEventListener('keydown', _key_listener); - modal.close_listener.push(() => { - document.removeEventListener('click', _focus_listener); - document.removeEventListener('keydown', _key_listener); - }) - } - - - update_bookmark_list(undefined); - update_buttons(); - - template.find(".container-bookmarks").on('keydown', event => { - console.error(event.key); }); - template.find(".button-close").on('click', event => modal.close()); - return template.children(); - }, - footer: undefined, - width: "40em" - }); - modal.htmlTag.dividerfy().find(".modal-body").addClass("modal-bookmarks"); - modal.close_listener.push(() => { - control_bar.update_bookmarks(); - top_menu.rebuild_bookmarks(); - }); + button_add_folder.on('click', event => { + createInputModal(tr("Enter a folder name"), tr("Enter the folder name"), text => { + return true; + }, result => { + if(result) { + const mark = create_bookmark_directory( + selected_bookmark ? + selected_bookmark.type === BookmarkType.DIRECTORY ? + selected_bookmark as DirectoryBookmark : + selected_bookmark.parent : + bookmarks(), + result as string + ); + save_bookmark(mark); + update_bookmark_list(mark.unique_id); + } + }).open(); + }); - modal.open(); - } + button_add_bookmark.on('click', event => { + createInputModal(tr("Enter a bookmark name"), tr("Enter the bookmark name"), text => { + return true; + }, result => { + if(result) { + const mark = create_bookmark(result as string, + selected_bookmark ? + selected_bookmark.type === BookmarkType.DIRECTORY ? + selected_bookmark as DirectoryBookmark : + selected_bookmark.parent : + bookmarks(), { + server_password: "", + server_port: 9987, + server_address: "", + server_password_hash: "" + }, ""); + save_bookmark(mark); + update_bookmark_list(mark.unique_id); + } + }).open(); + }); + + button_connect_tab.on('click', event => { + boorkmak_connect(selected_bookmark as Bookmark, true); + modal.close(); + }).toggle(!settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION)); + + button_connect.on('click', event => { + boorkmak_connect(selected_bookmark as Bookmark, false); + modal.close(); + }); + } + + /* inputs */ + { + input_bookmark_name.on('change keydown', event => { + const name = input_bookmark_name.val() as string; + const valid = name.length > 3; + input_bookmark_name.firstParent(".input-boxed").toggleClass("is-invalid", !valid); + + if(event.type === "change" && valid) { + selected_bookmark.display_name = name; + label_bookmark_name.text(name); + } + }); + + input_server_address.on('change keydown', event => { + const address = input_server_address.val() as string; + const valid = !!address.match(Regex.IP_V4) || !!address.match(Regex.IP_V6) || !!address.match(Regex.DOMAIN); + input_server_address.firstParent(".input-boxed").toggleClass("is-invalid", !valid); + + if(valid) { + const entry = selected_bookmark as Bookmark; + let _v6_end = address.indexOf(']'); + let idx = address.lastIndexOf(':'); + if(idx != -1 && idx > _v6_end) { + entry.server_properties.server_port = parseInt(address.substr(idx + 1)); + entry.server_properties.server_address = address.substr(0, idx); + } else { + entry.server_properties.server_address = address; + entry.server_properties.server_port = 9987; + } + + label_server_address.text(entry.server_properties.server_address + (entry.server_properties.server_port == 9987 ? "" : (" " + entry.server_properties.server_port))); + update_connect_info(); + } + }); + + input_connect_profile.on('change', event => { + const id = input_connect_profile.val() as string; + const profile = profiles().find(e => e.id === id); + if(profile) { + (selected_bookmark as Bookmark).connect_profile = id; + } else { + log.warn(LogCategory.GENERAL, tr("Failed to change connect profile for profile %s to %s"), selected_bookmark.unique_id, id); + } + }) + } + + /* Arrow key navigation for the bookmark list */ + { + let _focused = false; + let _focus_listener; + let _key_listener; + + _focus_listener = event => { + _focused = false; + let element = event.target as HTMLElement; + while(element) { + if(element === container_bookmarks[0]) { + _focused = true; + break; + } + element = element.parentNode as HTMLElement; + } + }; + + _key_listener = event => { + if(!_focused) return; + + if(event.key.toLowerCase() === "arrowdown") { + container_bookmarks.find(".selected").next().trigger('click'); + } else if(event.key.toLowerCase() === "arrowup") { + container_bookmarks.find(".selected").prev().trigger('click'); + } + }; + + document.addEventListener('click', _focus_listener); + document.addEventListener('keydown', _key_listener); + modal.close_listener.push(() => { + document.removeEventListener('click', _focus_listener); + document.removeEventListener('keydown', _key_listener); + }) + } + + + update_bookmark_list(undefined); + update_buttons(); + + template.find(".container-bookmarks").on('keydown', event => { + console.error(event.key); + }); + template.find(".button-close").on('click', event => modal.close()); + return template.children(); + }, + footer: undefined, + width: "40em" + }); + + modal.htmlTag.dividerfy().find(".modal-body").addClass("modal-bookmarks"); + modal.close_listener.push(() => { + control_bar.update_bookmarks(); + top_menu.rebuild_bookmarks(); + }); + + modal.open(); } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalChangeLatency.ts b/shared/js/ui/modal/ModalChangeLatency.ts index 3b8dca95..d279e276 100644 --- a/shared/js/ui/modal/ModalChangeLatency.ts +++ b/shared/js/ui/modal/ModalChangeLatency.ts @@ -1,114 +1,115 @@ -/// -/// -/// +import {createModal, Modal} from "tc-shared/ui/elements/Modal"; +import {ClientEntry} from "tc-shared/ui/client"; +import {voice} from "tc-shared/connection/ConnectionBase"; +import LatencySettings = voice.LatencySettings; +import {Slider, sliderfy} from "tc-shared/ui/elements/Slider"; +import * as htmltags from "tc-shared/ui/htmltags"; -namespace Modals { - let modal: Modal; - export function spawnChangeLatency(client: ClientEntry, current: connection.voice.LatencySettings, reset: () => connection.voice.LatencySettings, apply: (settings: connection.voice.LatencySettings) => any, callback_flush?: () => any) { - if(modal) modal.close(); +let modal: Modal; +export function spawnChangeLatency(client: ClientEntry, current: LatencySettings, reset: () => LatencySettings, apply: (settings: LatencySettings) => any, callback_flush?: () => any) { + if(modal) modal.close(); - const begin = Object.assign({}, current); - current = Object.assign({}, current); + const begin = Object.assign({}, current); + current = Object.assign({}, current); - modal = createModal({ - header: tr("Change playback latency"), - body: function () { - let tag = $("#tmpl_change_latency").renderTag({ - client: htmltags.generate_client_object({ - add_braces: false, - client_name: client.clientNickName(), - client_unique_id: client.properties.client_unique_identifier, - client_id: client.clientId() - }), + modal = createModal({ + header: tr("Change playback latency"), + body: function () { + let tag = $("#tmpl_change_latency").renderTag({ + client: htmltags.generate_client_object({ + add_braces: false, + client_name: client.clientNickName(), + client_unique_id: client.properties.client_unique_identifier, + client_id: client.clientId() + }), - have_flush: (typeof(callback_flush) === "function") + have_flush: (typeof(callback_flush) === "function") + }); + + const update_value = () => { + const valid = current.min_buffer < current.max_buffer; + + modal.htmlTag.find(".modal-body").toggleClass("modal-red", !valid); + modal.htmlTag.find(".modal-body").toggleClass("modal-green", valid); + + if(!valid) + return; + + apply(current); + }; + + let slider_min: Slider, slider_max: Slider; + { + const container = tag.find(".container-min"); + const tag_value = container.find(".value"); + + const slider_tag = container.find(".container-slider"); + slider_min = sliderfy(slider_tag, { + initial_value: current.min_buffer, + step: 20, + max_value: 1000, + min_value: 0, + + unit: 'ms' }); - - const update_value = () => { - const valid = current.min_buffer < current.max_buffer; - - modal.htmlTag.find(".modal-body").toggleClass("modal-red", !valid); - modal.htmlTag.find(".modal-body").toggleClass("modal-green", valid); - - if(!valid) - return; - - apply(current); - }; - - let slider_min: Slider, slider_max: Slider; - { - const container = tag.find(".container-min"); - const tag_value = container.find(".value"); - - const slider_tag = container.find(".container-slider"); - slider_min = sliderfy(slider_tag, { - initial_value: current.min_buffer, - step: 20, - max_value: 1000, - min_value: 0, - - unit: 'ms' - }); - slider_tag.on('change', event => { - current.min_buffer = parseInt(slider_tag.attr("value")); - tag_value.text(current.min_buffer + "ms"); - update_value(); - }); - + slider_tag.on('change', event => { + current.min_buffer = parseInt(slider_tag.attr("value")); tag_value.text(current.min_buffer + "ms"); - } + update_value(); + }); - { - const container = tag.find(".container-max"); - const tag_value = container.find(".value"); + tag_value.text(current.min_buffer + "ms"); + } - const slider_tag = container.find(".container-slider"); - slider_max = sliderfy(slider_tag, { - initial_value: current.max_buffer, - step: 20, - max_value: 1020, - min_value: 20, + { + const container = tag.find(".container-max"); + const tag_value = container.find(".value"); - unit: 'ms' - }); + const slider_tag = container.find(".container-slider"); + slider_max = sliderfy(slider_tag, { + initial_value: current.max_buffer, + step: 20, + max_value: 1020, + min_value: 20, - slider_tag.on('change', event => { - current.max_buffer = parseInt(slider_tag.attr("value")); - tag_value.text(current.max_buffer + "ms"); - update_value(); - }); + unit: 'ms' + }); + slider_tag.on('change', event => { + current.max_buffer = parseInt(slider_tag.attr("value")); tag_value.text(current.max_buffer + "ms"); - } - setTimeout(update_value, 0); - - tag.find(".button-close").on('click', event => { - modal.close(); + update_value(); }); - tag.find(".button-cancel").on('click', event => { - apply(begin); - modal.close(); - }); + tag_value.text(current.max_buffer + "ms"); + } + setTimeout(update_value, 0); - tag.find(".button-reset").on('click', event => { - current = Object.assign({}, reset()); - slider_max.value(current.max_buffer); - slider_min.value(current.min_buffer); - }); + tag.find(".button-close").on('click', event => { + modal.close(); + }); - tag.find(".button-flush").on('click', event => callback_flush()); + tag.find(".button-cancel").on('click', event => { + apply(begin); + modal.close(); + }); - return tag.children(); - }, - footer: null, + tag.find(".button-reset").on('click', event => { + current = Object.assign({}, reset()); + slider_max.value(current.max_buffer); + slider_min.value(current.min_buffer); + }); - width: 600 - }); + tag.find(".button-flush").on('click', event => callback_flush()); - modal.close_listener.push(() => modal = undefined); - modal.open(); - modal.htmlTag.find(".modal-body").addClass("modal-latency"); - } + return tag.children(); + }, + footer: null, + + width: 600 + }); + + modal.close_listener.push(() => modal = undefined); + modal.open(); + modal.htmlTag.find(".modal-body").addClass("modal-latency"); } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalChangeVolume.ts b/shared/js/ui/modal/ModalChangeVolume.ts index b9b0778a..8271705e 100644 --- a/shared/js/ui/modal/ModalChangeVolume.ts +++ b/shared/js/ui/modal/ModalChangeVolume.ts @@ -1,77 +1,76 @@ -/// -/// -/// +//TODO: Use the max limit! -namespace Modals { - //TODO: Use the max limit! +import {sliderfy} from "tc-shared/ui/elements/Slider"; +import {createModal, Modal} from "tc-shared/ui/elements/Modal"; +import {ClientEntry} from "tc-shared/ui/client"; +import * as htmltags from "tc-shared/ui/htmltags"; - let modal: Modal; - export function spawnChangeVolume(client: ClientEntry, local: boolean, current: number, max: number | undefined, callback: (number) => void) { - if(modal) modal.close(); +let modal: Modal; +export function spawnChangeVolume(client: ClientEntry, local: boolean, current: number, max: number | undefined, callback: (number) => void) { + if(modal) modal.close(); - let new_value: number; - modal = createModal({ - header: local ? tr("Change local volume") : tr("Change remote volume"), - body: function () { - let tag = $("#tmpl_change_volume").renderTag({ - client: htmltags.generate_client_object({ - add_braces: false, - client_name: client.clientNickName(), - client_unique_id: client.properties.client_unique_identifier, - client_id: client.clientId() - }), - local: local - }); + let new_value: number; + modal = createModal({ + header: local ? tr("Change local volume") : tr("Change remote volume"), + body: function () { + let tag = $("#tmpl_change_volume").renderTag({ + client: htmltags.generate_client_object({ + add_braces: false, + client_name: client.clientNickName(), + client_unique_id: client.properties.client_unique_identifier, + client_id: client.clientId() + }), + local: local + }); - const container_value = tag.find(".info .value"); - const set_value = value => { - const number = value > 100 ? value - 100 : 100 - value; - container_value.html((value == 100 ? "±" : value > 100 ? "+" : "-") + number + "%"); + const container_value = tag.find(".info .value"); + const set_value = value => { + const number = value > 100 ? value - 100 : 100 - value; + container_value.html((value == 100 ? "±" : value > 100 ? "+" : "-") + number + "%"); - new_value = value / 100; - if(local) callback(new_value); - }; - set_value(current * 100); + new_value = value / 100; + if(local) callback(new_value); + }; + set_value(current * 100); - const slider_tag = tag.find(".container-slider"); - const slider = sliderfy(slider_tag, { - initial_value: current * 100, - step: 1, - max_value: 200, - min_value: 0, + const slider_tag = tag.find(".container-slider"); + const slider = sliderfy(slider_tag, { + initial_value: current * 100, + step: 1, + max_value: 200, + min_value: 0, - unit: '%' - }); - slider_tag.on('change', event => set_value(parseInt(slider_tag.attr("value")))); + unit: '%' + }); + slider_tag.on('change', event => set_value(parseInt(slider_tag.attr("value")))); - tag.find(".button-save").on('click', event => { - if(typeof(new_value) !== "undefined") callback(new_value); - modal.close(); - }); + tag.find(".button-save").on('click', event => { + if(typeof(new_value) !== "undefined") callback(new_value); + modal.close(); + }); - tag.find(".button-cancel").on('click', event => { - callback(current); - modal.close(); - }); + tag.find(".button-cancel").on('click', event => { + callback(current); + modal.close(); + }); - tag.find(".button-reset").on('click', event => { - slider.value(100); - }); + tag.find(".button-reset").on('click', event => { + slider.value(100); + }); - tag.find(".button-apply").on('click', event => { - callback(new_value); - new_value = undefined; - }); + tag.find(".button-apply").on('click', event => { + callback(new_value); + new_value = undefined; + }); - return tag.children(); - }, - footer: null, + return tag.children(); + }, + footer: null, - width: 600 - }); + width: 600 + }); - modal.close_listener.push(() => modal = undefined); - modal.open(); - modal.htmlTag.find(".modal-body").addClass("modal-volume"); - } + modal.close_listener.push(() => modal = undefined); + modal.open(); + modal.htmlTag.find(".modal-body").addClass("modal-volume"); } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalChannelInfo.ts b/shared/js/ui/modal/ModalChannelInfo.ts index db91989f..7433eea0 100644 --- a/shared/js/ui/modal/ModalChannelInfo.ts +++ b/shared/js/ui/modal/ModalChannelInfo.ts @@ -1,152 +1,157 @@ -namespace Modals { - export function openChannelInfo(channel: ChannelEntry) { - let modal: Modal; +import {createInfoModal, createModal, Modal} from "tc-shared/ui/elements/Modal"; +import {ChannelEntry} from "tc-shared/ui/channel"; +import {copy_to_clipboard} from "tc-shared/utils/helpers"; +import * as tooltip from "tc-shared/ui/elements/Tooltip"; +import {formatMessage} from "tc-shared/ui/frames/chat"; - modal = createModal({ - header: tr("Channel information: ") + channel.channelName(), - body: () => { - const template = $("#tmpl_channel_info").renderTag(); +export function openChannelInfo(channel: ChannelEntry) { + let modal: Modal; - const update_values = (container) => { + modal = createModal({ + header: tr("Channel information: ") + channel.channelName(), + body: () => { + const template = $("#tmpl_channel_info").renderTag(); - apply_channel_description(container.find(".container-description"), channel); - apply_general(container, channel); - }; + const update_values = (container) => { - template.find(".button-copy").on('click', event => { - copy_to_clipboard(channel.properties.channel_description); - createInfoModal(tr("Description copied"), tr("The channel description has been copied to your clipboard!")).open(); - }); + apply_channel_description(container.find(".container-description"), channel); + apply_general(container, channel); + }; - const button_update = template.find(".button-update"); - button_update.on('click', event => update_values(modal.htmlTag)); + template.find(".button-copy").on('click', event => { + copy_to_clipboard(channel.properties.channel_description); + createInfoModal(tr("Description copied"), tr("The channel description has been copied to your clipboard!")).open(); + }); - update_values(template); - tooltip(template); - return template.children(); - }, - footer: null, - width: "65em" - }); - modal.htmlTag.find(".button-close").on('click', event => modal.close()); - modal.htmlTag.find(".modal-body").addClass("modal-channel-info"); - modal.open(); + const button_update = template.find(".button-update"); + button_update.on('click', event => update_values(modal.htmlTag)); + + update_values(template); + tooltip.initialize(template); + return template.children(); + }, + footer: null, + width: "65em" + }); + modal.htmlTag.find(".button-close").on('click', event => modal.close()); + modal.htmlTag.find(".modal-body").addClass("modal-channel-info"); + modal.open(); +} + +declare const xbbcode; +function apply_channel_description(container: JQuery, channel: ChannelEntry) { + const container_value = container.find(".value"); + const container_no_value = container.find(".no-value"); + + channel.getChannelDescription().then(description => { + if(description) { + const result = xbbcode.parse(description, {}); + container_value[0].innerHTML = result.build_html(); + container_no_value.hide(); + container_value.show(); + } else { + container_no_value.text(tr("Channel has no description")); + } + }); + + container_value.hide(); + container_no_value.text(tr("loading...")).show(); +} + +const codec_names = [ + tr("Speex Narrowband"), + tr("Speex Wideband"), + tr("Speex Ultra-Wideband"), + tr("CELT Mono"), + tr("Opus Voice"), + tr("Opus Music") +]; + +function apply_general(container: JQuery, channel: ChannelEntry) { + /* channel type */ + { + const tag = container.find(".channel-type .value").empty(); + if(channel.properties.channel_flag_permanent) + tag.text(tr("Permanent")); + else if(channel.properties.channel_flag_semi_permanent) + tag.text(tr("Semi permanent")); + else + //TODO: Channel delete delay! + tag.text(tr("Temporary")); } - function apply_channel_description(container: JQuery, channel: ChannelEntry) { - const container_value = container.find(".value"); - const container_no_value = container.find(".no-value"); - - channel.getChannelDescription().then(description => { - if(description) { - const result = xbbcode.parse(description, {}); - container_value[0].innerHTML = result.build_html(); - container_no_value.hide(); - container_value.show(); - } else { - container_no_value.text(tr("Channel has no description")); - } - }); - - container_value.hide(); - container_no_value.text(tr("loading...")).show(); + /* chat mode */ + { + const tag = container.find(".chat-mode .value").empty(); + if(channel.properties.channel_flag_conversation_private || channel.properties.channel_flag_password) { + tag.text(tr("Private")); + } else { + if(channel.properties.channel_conversation_history_length == -1) + tag.text(tr("Public; Semi permanent message saving")); + else if(channel.properties.channel_conversation_history_length == 0) + tag.text(tr("Public; Permanent message saving")); + else + tag.append(formatMessage(tr("Public; Saving last {} messages"), channel.properties.channel_conversation_history_length)); + } } - const codec_names = [ - tr("Speex Narrowband"), - tr("Speex Wideband"), - tr("Speex Ultra-Wideband"), - tr("CELT Mono"), - tr("Opus Voice"), - tr("Opus Music") - ]; + /* current clients */ + { + const tag = container.find(".current-clients .value").empty(); - function apply_general(container: JQuery, channel: ChannelEntry) { - /* channel type */ - { - const tag = container.find(".channel-type .value").empty(); - if(channel.properties.channel_flag_permanent) - tag.text(tr("Permanent")); - else if(channel.properties.channel_flag_semi_permanent) - tag.text(tr("Semi permanent")); - else - //TODO: Channel delete delay! - tag.text(tr("Temporary")); - } - - /* chat mode */ - { - const tag = container.find(".chat-mode .value").empty(); - if(channel.properties.channel_flag_conversation_private || channel.properties.channel_flag_password) { - tag.text(tr("Private")); - } else { - if(channel.properties.channel_conversation_history_length == -1) - tag.text(tr("Public; Semi permanent message saving")); - else if(channel.properties.channel_conversation_history_length == 0) - tag.text(tr("Public; Permanent message saving")); - else - tag.append(MessageHelper.formatMessage(tr("Public; Saving last {} messages"), channel.properties.channel_conversation_history_length)); + if(channel.flag_subscribed) { + const current = channel.clients().length; + let channel_limit = tr("Unlimited"); + if(!channel.properties.channel_flag_maxclients_unlimited) + channel_limit = "" + channel.properties.channel_maxclients; + else if(!channel.properties.channel_flag_maxfamilyclients_unlimited) { + if(channel.properties.channel_maxfamilyclients >= 0) + channel_limit = "" + channel.properties.channel_maxfamilyclients; } + + tag.text(current + " / " + channel_limit); + } else { + tag.text(tr("Channel not subscribed")); } + } - /* current clients */ - { - const tag = container.find(".current-clients .value").empty(); + /* audio codec */ + { + const tag = container.find(".audio-codec .value").empty(); + tag.text((codec_names[channel.properties.channel_codec] || tr("Unknown")) + " (" + channel.properties.channel_codec_quality + ")") + } - if(channel.flag_subscribed) { - const current = channel.clients().length; - let channel_limit = tr("Unlimited"); - if(!channel.properties.channel_flag_maxclients_unlimited) - channel_limit = "" + channel.properties.channel_maxclients; - else if(!channel.properties.channel_flag_maxfamilyclients_unlimited) { - if(channel.properties.channel_maxfamilyclients >= 0) - channel_limit = "" + channel.properties.channel_maxfamilyclients; - } + /* audio encrypted */ + { + const tag = container.find(".audio-encrypted .value").empty(); + const mode = channel.channelTree.server.properties.virtualserver_codec_encryption_mode; + let appendix; + if(mode == 1) + appendix = tr("Overridden by the server with Unencrypted!"); + else if(mode == 2) + appendix = tr("Overridden by the server with Encrypted!"); - tag.text(current + " / " + channel_limit); - } else { - tag.text(tr("Channel not subscribed")); - } - } + tag.html((channel.properties.channel_codec_is_unencrypted ? tr("Unencrypted") : tr("Encrypted")) + (appendix ? "
" + appendix : "")) + } - /* audio codec */ - { - const tag = container.find(".audio-codec .value").empty(); - tag.text((codec_names[channel.properties.channel_codec] || tr("Unknown")) + " (" + channel.properties.channel_codec_quality + ")") - } + /* flag password */ + { + const tag = container.find(".flag-password .value").empty(); + if(channel.properties.channel_flag_password) + tag.text(tr("Yes")); + else + tag.text(tr("No")); + } - /* audio encrypted */ - { - const tag = container.find(".audio-encrypted .value").empty(); - const mode = channel.channelTree.server.properties.virtualserver_codec_encryption_mode; - let appendix; - if(mode == 1) - appendix = tr("Overridden by the server with Unencrypted!"); - else if(mode == 2) - appendix = tr("Overridden by the server with Encrypted!"); - - tag.html((channel.properties.channel_codec_is_unencrypted ? tr("Unencrypted") : tr("Encrypted")) + (appendix ? "
" + appendix : "")) - } - - /* flag password */ - { - const tag = container.find(".flag-password .value").empty(); - if(channel.properties.channel_flag_password) - tag.text(tr("Yes")); - else - tag.text(tr("No")); - } - - /* topic */ - { - const container_tag = container.find(".topic"); - const tag = container_tag.find(".value").empty(); - if(channel.properties.channel_topic) { - container_tag.show(); - tag.text(channel.properties.channel_topic); - } else { - container_tag.hide(); - } + /* topic */ + { + const container_tag = container.find(".topic"); + const tag = container_tag.find(".value").empty(); + if(channel.properties.channel_topic) { + container_tag.show(); + tag.text(channel.properties.channel_topic); + } else { + container_tag.hide(); } } } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalClientInfo.ts b/shared/js/ui/modal/ModalClientInfo.ts index 482a42d5..b2249283 100644 --- a/shared/js/ui/modal/ModalClientInfo.ts +++ b/shared/js/ui/modal/ModalClientInfo.ts @@ -1,512 +1,519 @@ -namespace Modals { - type InfoUpdateCallback = (info: ClientConnectionInfo) => any; - export function openClientInfo(client: ClientEntry) { - let modal: Modal; - let update_callbacks: InfoUpdateCallback[] = []; +import {ClientConnectionInfo, ClientEntry} from "tc-shared/ui/client"; +import PermissionType from "tc-shared/permission/PermissionType"; +import {createInfoModal, createModal, Modal} from "tc-shared/ui/elements/Modal"; +import {copy_to_clipboard} from "tc-shared/utils/helpers"; +import * as i18nc from "tc-shared/i18n/country"; +import * as tooltip from "tc-shared/ui/elements/Tooltip"; +import {format_number, network} from "tc-shared/ui/frames/chat"; - modal = createModal({ - header: tr("Profile Information: ") + client.clientNickName(), - body: () => { - const template = $("#tmpl_client_info").renderTag(); +type InfoUpdateCallback = (info: ClientConnectionInfo) => any; +export function openClientInfo(client: ClientEntry) { + let modal: Modal; + let update_callbacks: InfoUpdateCallback[] = []; - /* the tab functionality */ - { - const container_tabs = template.find(".container-categories"); - container_tabs.find(".categories .entry").on('click', event => { - const entry = $(event.target); + modal = createModal({ + header: tr("Profile Information: ") + client.clientNickName(), + body: () => { + const template = $("#tmpl_client_info").renderTag(); - container_tabs.find(".bodies > .body").addClass("hidden"); - container_tabs.find(".categories > .selected").removeClass("selected"); + /* the tab functionality */ + { + const container_tabs = template.find(".container-categories"); + container_tabs.find(".categories .entry").on('click', event => { + const entry = $(event.target); - entry.addClass("selected"); - container_tabs.find(".bodies > .body." + entry.attr("container")).removeClass("hidden"); - }); + container_tabs.find(".bodies > .body").addClass("hidden"); + container_tabs.find(".categories > .selected").removeClass("selected"); - container_tabs.find(".entry").first().trigger('click'); - } + entry.addClass("selected"); + container_tabs.find(".bodies > .body." + entry.attr("container")).removeClass("hidden"); + }); - apply_static_info(client, template, modal, update_callbacks); - apply_client_status(client, template, modal, update_callbacks); - apply_basic_info(client, template.find(".container-basic"), modal, update_callbacks); - apply_groups(client, template.find(".container-groups"), modal, update_callbacks); - apply_packets(client, template.find(".container-packets"), modal, update_callbacks); + container_tabs.find(".entry").first().trigger('click'); + } - tooltip(template); - return template.children(); - }, - footer: null, + apply_static_info(client, template, modal, update_callbacks); + apply_client_status(client, template, modal, update_callbacks); + apply_basic_info(client, template.find(".container-basic"), modal, update_callbacks); + apply_groups(client, template.find(".container-groups"), modal, update_callbacks); + apply_packets(client, template.find(".container-packets"), modal, update_callbacks); - width: '60em' + tooltip.initialize(template); + return template.children(); + }, + footer: null, + + width: '60em' + }); + + const updater = setInterval(() => { + client.request_connection_info().then(info => update_callbacks.forEach(e => e(info))); + }, 1000); + + modal.htmlTag.find(".modal-body").addClass("modal-client-info"); + modal.open(); + modal.close_listener.push(() => clearInterval(updater)); +} + +const TIME_SECOND = 1000; +const TIME_MINUTE = 60 * TIME_SECOND; +const TIME_HOUR = 60 * TIME_MINUTE; +const TIME_DAY = 24 * TIME_HOUR; +const TIME_WEEK = 7 * TIME_DAY; + +function format_time(time: number, default_value: string) { + let result = ""; + if(time > TIME_WEEK) { + const amount = Math.floor(time / TIME_WEEK); + result += " " + amount + " " + (amount > 1 ? tr("Weeks") : tr("Week")); + time -= amount * TIME_WEEK; + } + + if(time > TIME_DAY) { + const amount = Math.floor(time / TIME_DAY); + result += " " + amount + " " + (amount > 1 ? tr("Days") : tr("Day")); + time -= amount * TIME_DAY; + } + + if(time > TIME_HOUR) { + const amount = Math.floor(time / TIME_HOUR); + result += " " + amount + " " + (amount > 1 ? tr("Hours") : tr("Hour")); + time -= amount * TIME_HOUR; + } + + if(time > TIME_MINUTE) { + const amount = Math.floor(time / TIME_MINUTE); + result += " " + amount + " " + (amount > 1 ? tr("Minutes") : tr("Minute")); + time -= amount * TIME_MINUTE; + } + + if(time > TIME_SECOND) { + const amount = Math.floor(time / TIME_SECOND); + result += " " + amount + " " + (amount > 1 ? tr("Seconds") : tr("Second")); + time -= amount * TIME_SECOND; + } + + return result.length > 0 ? result.substring(1) : default_value; +} + +function apply_static_info(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) { + tag.find(".container-avatar").append( + client.channelTree.client.fileManager.avatars.generate_chat_tag({database_id: client.properties.client_database_id, id: client.clientId()}, client.properties.client_unique_identifier) + ); + + tag.find(".container-name").append( + client.createChatTag() + ); + + tag.find(".client-description").text( + client.properties.client_description + ); +} + +function apply_client_status(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) { + tag.find(".status-output-disabled").toggle(!client.properties.client_output_hardware); + tag.find(".status-input-disabled").toggle(!client.properties.client_input_hardware); + + tag.find(".status-output-muted").toggle(client.properties.client_output_muted); + tag.find(".status-input-muted").toggle(client.properties.client_input_muted); + + + tag.find(".status-away").toggle(client.properties.client_away); + if(client.properties.client_away_message) { + tag.find(".container-away-message").show().find("a").text(client.properties.client_away_message); + } else { + tag.find(".container-away-message").hide(); + } +} + +declare const moment; +function apply_basic_info(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) { + /* Unique ID */ + { + const container = tag.find(".property-unique-id"); + + container.find(".value a").text(client.clientUid()); + container.find(".value-dbid").text(client.properties.client_database_id); + + container.find(".button-copy").on('click', event => { + copy_to_clipboard(client.clientUid()); + createInfoModal(tr("Unique ID copied"), tr("The unique id has been copied to your clipboard!")).open(); + }); + } + + /* TeaForo */ + { + const container = tag.find(".property-teaforo .value").empty(); + + if(client.properties.client_teaforo_id) { + container.children().remove(); + + let text = client.properties.client_teaforo_name; + if((client.properties.client_teaforo_flags & 0x01) > 0) + text += " (" + tr("Banned") + ")"; + if((client.properties.client_teaforo_flags & 0x02) > 0) + text += " (" + tr("Stuff") + ")"; + if((client.properties.client_teaforo_flags & 0x04) > 0) + text += " (" + tr("Premium") + ")"; + + $.spawn("a") + .attr("href", "https://forum.teaspeak.de/index.php?members/" + client.properties.client_teaforo_id) + .attr("target", "_blank") + .text(text) + .appendTo(container); + } else { + container.append($.spawn("a").text(tr("Not connected"))); + } + } + + /* Version */ + { + const container = tag.find(".property-version"); + + let version_full = client.properties.client_version; + let version = version_full.substring(0, version_full.indexOf(" ")); + + container.find(".value").empty().append( + $.spawn("a").attr("title", version_full).text(version), + $.spawn("a").addClass("a-on").text("on"), + $.spawn("a").text(client.properties.client_platform) + ); + + const container_timestamp = container.find(".container-tooltip"); + + let timestamp = -1; + version_full.replace(/\[build: ?([0-9]+)]/gmi, (group, ts) => { + timestamp = parseInt(ts); + return ""; + }); + if(timestamp > 0) { + container_timestamp.find(".value-timestamp").text(moment(timestamp * 1000).format('MMMM Do YYYY, h:mm:ss a')); + container_timestamp.show(); + } else { + container_timestamp.hide(); + } + } + + /* Country */ + { + const container = tag.find(".property-country"); + container.find(".value").empty().append( + $.spawn("div").addClass("country flag-" + client.properties.client_country.toLowerCase()), + $.spawn("a").text(i18nc.country_name(client.properties.client_country, tr("Unknown"))) + ); + } + + /* IP Address */ + { + const container = tag.find(".property-ip"); + const value = container.find(".value a"); + value.text(tr("loading...")); + + container.find(".button-copy").on('click', event => { + copy_to_clipboard(value.text()); + createInfoModal(tr("Client IP copied"), tr("The client IP has been copied to your clipboard!")).open(); }); - const updater = setInterval(() => { - client.request_connection_info().then(info => update_callbacks.forEach(e => e(info))); - }, 1000); - - modal.htmlTag.find(".modal-body").addClass("modal-client-info"); - modal.open(); - modal.close_listener.push(() => clearInterval(updater)); + callbacks.push(info => { + value.text(info.connection_client_ip ? (info.connection_client_ip + ":" + info.connection_client_port) : tr("Hidden")); + }); } - const TIME_SECOND = 1000; - const TIME_MINUTE = 60 * TIME_SECOND; - const TIME_HOUR = 60 * TIME_MINUTE; - const TIME_DAY = 24 * TIME_HOUR; - const TIME_WEEK = 7 * TIME_DAY; + /* first connected */ + { + const container = tag.find(".property-first-connected"); - function format_time(time: number, default_value: string) { - let result = ""; - if(time > TIME_WEEK) { - const amount = Math.floor(time / TIME_WEEK); - result += " " + amount + " " + (amount > 1 ? tr("Weeks") : tr("Week")); - time -= amount * TIME_WEEK; - } - - if(time > TIME_DAY) { - const amount = Math.floor(time / TIME_DAY); - result += " " + amount + " " + (amount > 1 ? tr("Days") : tr("Day")); - time -= amount * TIME_DAY; - } - - if(time > TIME_HOUR) { - const amount = Math.floor(time / TIME_HOUR); - result += " " + amount + " " + (amount > 1 ? tr("Hours") : tr("Hour")); - time -= amount * TIME_HOUR; - } - - if(time > TIME_MINUTE) { - const amount = Math.floor(time / TIME_MINUTE); - result += " " + amount + " " + (amount > 1 ? tr("Minutes") : tr("Minute")); - time -= amount * TIME_MINUTE; - } - - if(time > TIME_SECOND) { - const amount = Math.floor(time / TIME_SECOND); - result += " " + amount + " " + (amount > 1 ? tr("Seconds") : tr("Second")); - time -= amount * TIME_SECOND; - } - - return result.length > 0 ? result.substring(1) : default_value; + container.find(".value a").text(tr("loading...")); + client.updateClientVariables().then(() => { + container.find(".value a").text(moment(client.properties.client_created * 1000).format('MMMM Do YYYY, h:mm:ss a')); + }).catch(error => { + container.find(".value a").text(tr("error")); + }); } - function apply_static_info(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) { - tag.find(".container-avatar").append( - client.channelTree.client.fileManager.avatars.generate_chat_tag({database_id: client.properties.client_database_id, id: client.clientId()}, client.properties.client_unique_identifier) - ); + /* connect count */ + { + const container = tag.find(".property-connect-count"); - tag.find(".container-name").append( - client.createChatTag() - ); - - tag.find(".client-description").text( - client.properties.client_description - ); + container.find(".value a").text(tr("loading...")); + client.updateClientVariables().then(() => { + container.find(".value a").text(client.properties.client_totalconnections); + }).catch(error => { + container.find(".value a").text(tr("error")); + }); } - function apply_client_status(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) { - tag.find(".status-output-disabled").toggle(!client.properties.client_output_hardware); - tag.find(".status-input-disabled").toggle(!client.properties.client_input_hardware); + /* Online since */ + { + const container = tag.find(".property-online-since"); - tag.find(".status-output-muted").toggle(client.properties.client_output_muted); - tag.find(".status-input-muted").toggle(client.properties.client_input_muted); - - - tag.find(".status-away").toggle(client.properties.client_away); - if(client.properties.client_away_message) { - tag.find(".container-away-message").show().find("a").text(client.properties.client_away_message); - } else { - tag.find(".container-away-message").hide(); - } - } - - function apply_basic_info(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) { - /* Unique ID */ - { - const container = tag.find(".property-unique-id"); - - container.find(".value a").text(client.clientUid()); - container.find(".value-dbid").text(client.properties.client_database_id); - - container.find(".button-copy").on('click', event => { - copy_to_clipboard(client.clientUid()); - createInfoModal(tr("Unique ID copied"), tr("The unique id has been copied to your clipboard!")).open(); - }); - } - - /* TeaForo */ - { - const container = tag.find(".property-teaforo .value").empty(); - - if(client.properties.client_teaforo_id) { - container.children().remove(); - - let text = client.properties.client_teaforo_name; - if((client.properties.client_teaforo_flags & 0x01) > 0) - text += " (" + tr("Banned") + ")"; - if((client.properties.client_teaforo_flags & 0x02) > 0) - text += " (" + tr("Stuff") + ")"; - if((client.properties.client_teaforo_flags & 0x04) > 0) - text += " (" + tr("Premium") + ")"; - - $.spawn("a") - .attr("href", "https://forum.teaspeak.de/index.php?members/" + client.properties.client_teaforo_id) - .attr("target", "_blank") - .text(text) - .appendTo(container); - } else { - container.append($.spawn("a").text(tr("Not connected"))); - } - } - - /* Version */ - { - const container = tag.find(".property-version"); - - let version_full = client.properties.client_version; - let version = version_full.substring(0, version_full.indexOf(" ")); - - container.find(".value").empty().append( - $.spawn("a").attr("title", version_full).text(version), - $.spawn("a").addClass("a-on").text("on"), - $.spawn("a").text(client.properties.client_platform) - ); - - const container_timestamp = container.find(".container-tooltip"); - - let timestamp = -1; - version_full.replace(/\[build: ?([0-9]+)]/gmi, (group, ts) => { - timestamp = parseInt(ts); - return ""; - }); - if(timestamp > 0) { - container_timestamp.find(".value-timestamp").text(moment(timestamp * 1000).format('MMMM Do YYYY, h:mm:ss a')); - container_timestamp.show(); - } else { - container_timestamp.hide(); - } - } - - /* Country */ - { - const container = tag.find(".property-country"); - container.find(".value").empty().append( - $.spawn("div").addClass("country flag-" + client.properties.client_country.toLowerCase()), - $.spawn("a").text(i18n.country_name(client.properties.client_country, tr("Unknown"))) - ); - } - - /* IP Address */ - { - const container = tag.find(".property-ip"); - const value = container.find(".value a"); - value.text(tr("loading...")); - - container.find(".button-copy").on('click', event => { - copy_to_clipboard(value.text()); - createInfoModal(tr("Client IP copied"), tr("The client IP has been copied to your clipboard!")).open(); - }); - - callbacks.push(info => { - value.text(info.connection_client_ip ? (info.connection_client_ip + ":" + info.connection_client_port) : tr("Hidden")); - }); - } - - /* first connected */ - { - const container = tag.find(".property-first-connected"); - - container.find(".value a").text(tr("loading...")); - client.updateClientVariables().then(() => { - container.find(".value a").text(moment(client.properties.client_created * 1000).format('MMMM Do YYYY, h:mm:ss a')); - }).catch(error => { - container.find(".value a").text(tr("error")); - }); - } - - /* connect count */ - { - const container = tag.find(".property-connect-count"); - - container.find(".value a").text(tr("loading...")); - client.updateClientVariables().then(() => { - container.find(".value a").text(client.properties.client_totalconnections); - }).catch(error => { - container.find(".value a").text(tr("error")); - }); - } - - /* Online since */ - { - const container = tag.find(".property-online-since"); - - const node = container.find(".value a")[0]; - if(node) { - const update = () => { - node.innerText = format_time(client.calculateOnlineTime() * 1000, tr("0 Seconds")); - }; - - callbacks.push(update); /* keep it in sync with all other updates. Else it looks wired */ - update(); - } - } - - /* Idle time */ - { - const container = tag.find(".property-idle-time"); - const node = container.find(".value a")[0]; - if(node) { - callbacks.push(info => { - node.innerText = format_time(info.connection_idle_time, tr("Currently active")); - }); - node.innerText = tr("loading..."); - } - } - - /* ping */ - { - const container = tag.find(".property-ping"); - const node = container.find(".value a")[0]; - - if(node) { - callbacks.push(info => { - if(info.connection_ping >= 0) - node.innerText = info.connection_ping.toFixed(0) + "ms ± " + info.connection_ping_deviation.toFixed(2) + "ms"; - else if(info.connection_ping == -1 && info.connection_ping_deviation == -1) - node.innerText = tr("Not calculated"); - else - node.innerText = tr("loading..."); - }); - node.innerText = tr("loading..."); - } - } - } - - function apply_groups(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) { - /* server groups */ - { - const container_entries = tag.find(".entries"); - const container_empty = tag.find(".container-default-groups"); - - const update_groups = () => { - container_entries.empty(); - container_empty.show(); - - for(const group_id of client.assignedServerGroupIds()) { - if(group_id == client.channelTree.server.properties.virtualserver_default_server_group) - continue; - - const group = client.channelTree.client.groups.serverGroup(group_id); - if(!group) continue; //This shall never happen! - - container_empty.hide(); - container_entries.append($.spawn("div").addClass("entry").append( - client.channelTree.client.fileManager.icons.generateTag(group.properties.iconid), - $.spawn("a").addClass("name").text(group.name + " (" + group.id + ")"), - $.spawn("div").addClass("button-delete").append( - $.spawn("div").addClass("icon_em client-delete").attr("title", tr("Delete group")).on('click', event => { - client.channelTree.client.serverConnection.send_command("servergroupdelclient", { - sgid: group.id, - cldbid: client.properties.client_database_id - }).then(result => update_groups()); - }) - ).toggleClass("visible", - client.channelTree.client.permissions.neededPermission(PermissionType.I_SERVER_GROUP_MEMBER_REMOVE_POWER).granted(group.requiredMemberRemovePower) || - client.clientId() == client.channelTree.client.getClientId() && client.channelTree.client.permissions.neededPermission(PermissionType.I_SERVER_GROUP_SELF_REMOVE_POWER).granted(group.requiredMemberRemovePower) - ) - )) - } + const node = container.find(".value a")[0]; + if(node) { + const update = () => { + node.innerText = format_time(client.calculateOnlineTime() * 1000, tr("0 Seconds")); }; - tag.find(".button-group-add").on('click', () => client.open_assignment_modal()); - - update_groups(); + callbacks.push(update); /* keep it in sync with all other updates. Else it looks wired */ + update(); } } - function apply_packets(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) { - - /* Packet Loss */ - { - const container = tag.find(".statistic-packet-loss"); - const node_downstream = container.find(".downstream .value")[0]; - const node_upstream = container.find(".upstream .value")[0]; - - if(node_downstream) { - callbacks.push(info => { - node_downstream.innerText = info.connection_server2client_packetloss_control < 0 ? tr("Not calculated") : (info.connection_server2client_packetloss_control || 0).toFixed(); - }); - node_downstream.innerText = tr("loading..."); - } - - if(node_upstream) { - callbacks.push(info => { - node_upstream.innerText = info.connection_client2server_packetloss_total < 0 ? tr("Not calculated") : (info.connection_client2server_packetloss_total || 0).toFixed(); - }); - node_upstream.innerText = tr("loading..."); - } - } - - /* Packets transmitted */ - { - const container = tag.find(".statistic-transmitted-packets"); - const node_downstream = container.find(".downstream .value")[0]; - const node_upstream = container.find(".upstream .value")[0]; - - if(node_downstream) { - callbacks.push(info => { - let packets = 0; - packets += info.connection_packets_received_speech > 0 ? info.connection_packets_received_speech : 0; - packets += info.connection_packets_received_control > 0 ? info.connection_packets_received_control : 0; - packets += info.connection_packets_received_keepalive > 0 ? info.connection_packets_received_keepalive : 0; - if(packets == 0 && info.connection_packets_received_keepalive == -1) - node_downstream.innerText = tr("Not calculated"); - else - node_downstream.innerText = MessageHelper.format_number(packets, {unit: "Packets"}); - }); - node_downstream.innerText = tr("loading..."); - } - - if(node_upstream) { - callbacks.push(info => { - let packets = 0; - packets += info.connection_packets_sent_speech > 0 ? info.connection_packets_sent_speech : 0; - packets += info.connection_packets_sent_control > 0 ? info.connection_packets_sent_control : 0; - packets += info.connection_packets_sent_keepalive > 0 ? info.connection_packets_sent_keepalive : 0; - if(packets == 0 && info.connection_packets_sent_keepalive == -1) - node_upstream.innerText = tr("Not calculated"); - else - node_upstream.innerText = MessageHelper.format_number(packets, {unit: "Packets"}); - }); - node_upstream.innerText = tr("loading..."); - } - } - - /* Bytes transmitted */ - { - const container = tag.find(".statistic-transmitted-bytes"); - const node_downstream = container.find(".downstream .value")[0]; - const node_upstream = container.find(".upstream .value")[0]; - - if(node_downstream) { - callbacks.push(info => { - let bytes = 0; - bytes += info.connection_bytes_received_speech > 0 ? info.connection_bytes_received_speech : 0; - bytes += info.connection_bytes_received_control > 0 ? info.connection_bytes_received_control : 0; - bytes += info.connection_bytes_received_keepalive > 0 ? info.connection_bytes_received_keepalive : 0; - if(bytes == 0 && info.connection_bytes_received_keepalive == -1) - node_downstream.innerText = tr("Not calculated"); - else - node_downstream.innerText = MessageHelper.network.format_bytes(bytes); - }); - node_downstream.innerText = tr("loading..."); - } - - if(node_upstream) { - callbacks.push(info => { - let bytes = 0; - bytes += info.connection_bytes_sent_speech > 0 ? info.connection_bytes_sent_speech : 0; - bytes += info.connection_bytes_sent_control > 0 ? info.connection_bytes_sent_control : 0; - bytes += info.connection_bytes_sent_keepalive > 0 ? info.connection_bytes_sent_keepalive : 0; - if(bytes == 0 && info.connection_bytes_sent_keepalive == -1) - node_upstream.innerText = tr("Not calculated"); - else - node_upstream.innerText = MessageHelper.network.format_bytes(bytes); - }); - node_upstream.innerText = tr("loading..."); - } - } - - /* Bandwidth second */ - { - const container = tag.find(".statistic-bandwidth-second"); - const node_downstream = container.find(".downstream .value")[0]; - const node_upstream = container.find(".upstream .value")[0]; - - if(node_downstream) { - callbacks.push(info => { - let bytes = 0; - bytes += info.connection_bandwidth_received_last_second_speech > 0 ? info.connection_bandwidth_received_last_second_speech : 0; - bytes += info.connection_bandwidth_received_last_second_control > 0 ? info.connection_bandwidth_received_last_second_control : 0; - bytes += info.connection_bandwidth_received_last_second_keepalive > 0 ? info.connection_bandwidth_received_last_second_keepalive : 0; - if(bytes == 0 && info.connection_bandwidth_received_last_second_keepalive == -1) - node_downstream.innerText = tr("Not calculated"); - else - node_downstream.innerText = MessageHelper.network.format_bytes(bytes, {time: "s"}); + /* Idle time */ + { + const container = tag.find(".property-idle-time"); + const node = container.find(".value a")[0]; + if(node) { + callbacks.push(info => { + node.innerText = format_time(info.connection_idle_time, tr("Currently active")); }); - node_downstream.innerText = tr("loading..."); - } + node.innerText = tr("loading..."); + } + } - if(node_upstream) { - callbacks.push(info => { - let bytes = 0; - bytes += info.connection_bandwidth_sent_last_second_speech > 0 ? info.connection_bandwidth_sent_last_second_speech : 0; - bytes += info.connection_bandwidth_sent_last_second_control > 0 ? info.connection_bandwidth_sent_last_second_control : 0; - bytes += info.connection_bandwidth_sent_last_second_keepalive > 0 ? info.connection_bandwidth_sent_last_second_keepalive : 0; - if(bytes == 0 && info.connection_bandwidth_sent_last_second_keepalive == -1) - node_upstream.innerText = tr("Not calculated"); - else - node_upstream.innerText = MessageHelper.network.format_bytes(bytes, {time: "s"}); - }); - node_upstream.innerText = tr("loading..."); + /* ping */ + { + const container = tag.find(".property-ping"); + const node = container.find(".value a")[0]; + + if(node) { + callbacks.push(info => { + if(info.connection_ping >= 0) + node.innerText = info.connection_ping.toFixed(0) + "ms ± " + info.connection_ping_deviation.toFixed(2) + "ms"; + else if(info.connection_ping == -1 && info.connection_ping_deviation == -1) + node.innerText = tr("Not calculated"); + else + node.innerText = tr("loading..."); + }); + node.innerText = tr("loading..."); + } + } +} + +function apply_groups(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) { + /* server groups */ + { + const container_entries = tag.find(".entries"); + const container_empty = tag.find(".container-default-groups"); + + const update_groups = () => { + container_entries.empty(); + container_empty.show(); + + for(const group_id of client.assignedServerGroupIds()) { + if(group_id == client.channelTree.server.properties.virtualserver_default_server_group) + continue; + + const group = client.channelTree.client.groups.serverGroup(group_id); + if(!group) continue; //This shall never happen! + + container_empty.hide(); + container_entries.append($.spawn("div").addClass("entry").append( + client.channelTree.client.fileManager.icons.generateTag(group.properties.iconid), + $.spawn("a").addClass("name").text(group.name + " (" + group.id + ")"), + $.spawn("div").addClass("button-delete").append( + $.spawn("div").addClass("icon_em client-delete").attr("title", tr("Delete group")).on('click', event => { + client.channelTree.client.serverConnection.send_command("servergroupdelclient", { + sgid: group.id, + cldbid: client.properties.client_database_id + }).then(result => update_groups()); + }) + ).toggleClass("visible", + client.channelTree.client.permissions.neededPermission(PermissionType.I_SERVER_GROUP_MEMBER_REMOVE_POWER).granted(group.requiredMemberRemovePower) || + client.clientId() == client.channelTree.client.getClientId() && client.channelTree.client.permissions.neededPermission(PermissionType.I_SERVER_GROUP_SELF_REMOVE_POWER).granted(group.requiredMemberRemovePower) + ) + )) } + }; + + tag.find(".button-group-add").on('click', () => client.open_assignment_modal()); + + update_groups(); + } +} + +function apply_packets(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) { + + /* Packet Loss */ + { + const container = tag.find(".statistic-packet-loss"); + const node_downstream = container.find(".downstream .value")[0]; + const node_upstream = container.find(".upstream .value")[0]; + + if(node_downstream) { + callbacks.push(info => { + node_downstream.innerText = info.connection_server2client_packetloss_control < 0 ? tr("Not calculated") : (info.connection_server2client_packetloss_control || 0).toFixed(); + }); + node_downstream.innerText = tr("loading..."); } - /* Bandwidth minute */ - { - const container = tag.find(".statistic-bandwidth-minute"); - const node_downstream = container.find(".downstream .value")[0]; - const node_upstream = container.find(".upstream .value")[0]; + if(node_upstream) { + callbacks.push(info => { + node_upstream.innerText = info.connection_client2server_packetloss_total < 0 ? tr("Not calculated") : (info.connection_client2server_packetloss_total || 0).toFixed(); + }); + node_upstream.innerText = tr("loading..."); + } + } - if(node_downstream) { - callbacks.push(info => { - let bytes = 0; - bytes += info.connection_bandwidth_received_last_minute_speech > 0 ? info.connection_bandwidth_received_last_minute_speech : 0; - bytes += info.connection_bandwidth_received_last_minute_control > 0 ? info.connection_bandwidth_received_last_minute_control : 0; - bytes += info.connection_bandwidth_received_last_minute_keepalive > 0 ? info.connection_bandwidth_received_last_minute_keepalive : 0; - if(bytes == 0 && info.connection_bandwidth_received_last_minute_keepalive == -1) - node_downstream.innerText = tr("Not calculated"); - else - node_downstream.innerText = MessageHelper.network.format_bytes(bytes, {time: "s"}); - }); - node_downstream.innerText = tr("loading..."); - } + /* Packets transmitted */ + { + const container = tag.find(".statistic-transmitted-packets"); + const node_downstream = container.find(".downstream .value")[0]; + const node_upstream = container.find(".upstream .value")[0]; - if(node_upstream) { - callbacks.push(info => { - let bytes = 0; - bytes += info.connection_bandwidth_sent_last_minute_speech > 0 ? info.connection_bandwidth_sent_last_minute_speech : 0; - bytes += info.connection_bandwidth_sent_last_minute_control > 0 ? info.connection_bandwidth_sent_last_minute_control : 0; - bytes += info.connection_bandwidth_sent_last_minute_keepalive > 0 ? info.connection_bandwidth_sent_last_minute_keepalive : 0; - if(bytes == 0 && info.connection_bandwidth_sent_last_minute_keepalive == -1) - node_upstream.innerText = tr("Not calculated"); - else - node_upstream.innerText = MessageHelper.network.format_bytes(bytes, {time: "s"}); - }); - node_upstream.innerText = tr("loading..."); - } + if(node_downstream) { + callbacks.push(info => { + let packets = 0; + packets += info.connection_packets_received_speech > 0 ? info.connection_packets_received_speech : 0; + packets += info.connection_packets_received_control > 0 ? info.connection_packets_received_control : 0; + packets += info.connection_packets_received_keepalive > 0 ? info.connection_packets_received_keepalive : 0; + if(packets == 0 && info.connection_packets_received_keepalive == -1) + node_downstream.innerText = tr("Not calculated"); + else + node_downstream.innerText = format_number(packets, {unit: "Packets"}); + }); + node_downstream.innerText = tr("loading..."); } - /* quota */ - { - const container = tag.find(".statistic-quota"); - const node_downstream = container.find(".downstream .value")[0]; - const node_upstream = container.find(".upstream .value")[0]; + if(node_upstream) { + callbacks.push(info => { + let packets = 0; + packets += info.connection_packets_sent_speech > 0 ? info.connection_packets_sent_speech : 0; + packets += info.connection_packets_sent_control > 0 ? info.connection_packets_sent_control : 0; + packets += info.connection_packets_sent_keepalive > 0 ? info.connection_packets_sent_keepalive : 0; + if(packets == 0 && info.connection_packets_sent_keepalive == -1) + node_upstream.innerText = tr("Not calculated"); + else + node_upstream.innerText = format_number(packets, {unit: "Packets"}); + }); + node_upstream.innerText = tr("loading..."); + } + } - if(node_downstream) { - client.updateClientVariables().then(info => { - //TODO: Test for own client info and if so then show the max quota (needed permission) - node_downstream.innerText = MessageHelper.network.format_bytes(client.properties.client_month_bytes_downloaded, {exact: false}); - }); - node_downstream.innerText = tr("loading..."); - } + /* Bytes transmitted */ + { + const container = tag.find(".statistic-transmitted-bytes"); + const node_downstream = container.find(".downstream .value")[0]; + const node_upstream = container.find(".upstream .value")[0]; - if(node_upstream) { - client.updateClientVariables().then(info => { - //TODO: Test for own client info and if so then show the max quota (needed permission) - node_upstream.innerText = MessageHelper.network.format_bytes(client.properties.client_month_bytes_uploaded, {exact: false}); - }); - node_upstream.innerText = tr("loading..."); - } + if(node_downstream) { + callbacks.push(info => { + let bytes = 0; + bytes += info.connection_bytes_received_speech > 0 ? info.connection_bytes_received_speech : 0; + bytes += info.connection_bytes_received_control > 0 ? info.connection_bytes_received_control : 0; + bytes += info.connection_bytes_received_keepalive > 0 ? info.connection_bytes_received_keepalive : 0; + if(bytes == 0 && info.connection_bytes_received_keepalive == -1) + node_downstream.innerText = tr("Not calculated"); + else + node_downstream.innerText = network.format_bytes(bytes); + }); + node_downstream.innerText = tr("loading..."); + } + + if(node_upstream) { + callbacks.push(info => { + let bytes = 0; + bytes += info.connection_bytes_sent_speech > 0 ? info.connection_bytes_sent_speech : 0; + bytes += info.connection_bytes_sent_control > 0 ? info.connection_bytes_sent_control : 0; + bytes += info.connection_bytes_sent_keepalive > 0 ? info.connection_bytes_sent_keepalive : 0; + if(bytes == 0 && info.connection_bytes_sent_keepalive == -1) + node_upstream.innerText = tr("Not calculated"); + else + node_upstream.innerText = network.format_bytes(bytes); + }); + node_upstream.innerText = tr("loading..."); + } + } + + /* Bandwidth second */ + { + const container = tag.find(".statistic-bandwidth-second"); + const node_downstream = container.find(".downstream .value")[0]; + const node_upstream = container.find(".upstream .value")[0]; + + if(node_downstream) { + callbacks.push(info => { + let bytes = 0; + bytes += info.connection_bandwidth_received_last_second_speech > 0 ? info.connection_bandwidth_received_last_second_speech : 0; + bytes += info.connection_bandwidth_received_last_second_control > 0 ? info.connection_bandwidth_received_last_second_control : 0; + bytes += info.connection_bandwidth_received_last_second_keepalive > 0 ? info.connection_bandwidth_received_last_second_keepalive : 0; + if(bytes == 0 && info.connection_bandwidth_received_last_second_keepalive == -1) + node_downstream.innerText = tr("Not calculated"); + else + node_downstream.innerText = network.format_bytes(bytes, {time: "s"}); + }); + node_downstream.innerText = tr("loading..."); + } + + if(node_upstream) { + callbacks.push(info => { + let bytes = 0; + bytes += info.connection_bandwidth_sent_last_second_speech > 0 ? info.connection_bandwidth_sent_last_second_speech : 0; + bytes += info.connection_bandwidth_sent_last_second_control > 0 ? info.connection_bandwidth_sent_last_second_control : 0; + bytes += info.connection_bandwidth_sent_last_second_keepalive > 0 ? info.connection_bandwidth_sent_last_second_keepalive : 0; + if(bytes == 0 && info.connection_bandwidth_sent_last_second_keepalive == -1) + node_upstream.innerText = tr("Not calculated"); + else + node_upstream.innerText = network.format_bytes(bytes, {time: "s"}); + }); + node_upstream.innerText = tr("loading..."); + } + } + + /* Bandwidth minute */ + { + const container = tag.find(".statistic-bandwidth-minute"); + const node_downstream = container.find(".downstream .value")[0]; + const node_upstream = container.find(".upstream .value")[0]; + + if(node_downstream) { + callbacks.push(info => { + let bytes = 0; + bytes += info.connection_bandwidth_received_last_minute_speech > 0 ? info.connection_bandwidth_received_last_minute_speech : 0; + bytes += info.connection_bandwidth_received_last_minute_control > 0 ? info.connection_bandwidth_received_last_minute_control : 0; + bytes += info.connection_bandwidth_received_last_minute_keepalive > 0 ? info.connection_bandwidth_received_last_minute_keepalive : 0; + if(bytes == 0 && info.connection_bandwidth_received_last_minute_keepalive == -1) + node_downstream.innerText = tr("Not calculated"); + else + node_downstream.innerText = network.format_bytes(bytes, {time: "s"}); + }); + node_downstream.innerText = tr("loading..."); + } + + if(node_upstream) { + callbacks.push(info => { + let bytes = 0; + bytes += info.connection_bandwidth_sent_last_minute_speech > 0 ? info.connection_bandwidth_sent_last_minute_speech : 0; + bytes += info.connection_bandwidth_sent_last_minute_control > 0 ? info.connection_bandwidth_sent_last_minute_control : 0; + bytes += info.connection_bandwidth_sent_last_minute_keepalive > 0 ? info.connection_bandwidth_sent_last_minute_keepalive : 0; + if(bytes == 0 && info.connection_bandwidth_sent_last_minute_keepalive == -1) + node_upstream.innerText = tr("Not calculated"); + else + node_upstream.innerText = network.format_bytes(bytes, {time: "s"}); + }); + node_upstream.innerText = tr("loading..."); + } + } + + /* quota */ + { + const container = tag.find(".statistic-quota"); + const node_downstream = container.find(".downstream .value")[0]; + const node_upstream = container.find(".upstream .value")[0]; + + if(node_downstream) { + client.updateClientVariables().then(info => { + //TODO: Test for own client info and if so then show the max quota (needed permission) + node_downstream.innerText = network.format_bytes(client.properties.client_month_bytes_downloaded, {exact: false}); + }); + node_downstream.innerText = tr("loading..."); + } + + if(node_upstream) { + client.updateClientVariables().then(info => { + //TODO: Test for own client info and if so then show the max quota (needed permission) + node_upstream.innerText = network.format_bytes(client.properties.client_month_bytes_uploaded, {exact: false}); + }); + node_upstream.innerText = tr("loading..."); } } } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalConnect.ts b/shared/js/ui/modal/ModalConnect.ts index cefdf403..ba5f9ff1 100644 --- a/shared/js/ui/modal/ModalConnect.ts +++ b/shared/js/ui/modal/ModalConnect.ts @@ -1,7 +1,17 @@ -/// +import {Settings, settings} from "tc-shared/settings"; +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; +import * as loader from "tc-loader"; +import {createModal} from "tc-shared/ui/elements/Modal"; +import {ConnectionProfile, default_profile, find_profile, profiles} from "tc-shared/profiles/ConnectionProfile"; +import {KeyCode} from "tc-shared/PPTListener"; +import {IconManager} from "tc-shared/FileManager"; +import * as i18nc from "tc-shared/i18n/country"; +import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings"; +import {server_connections} from "tc-shared/ui/frames/connection_handlers"; //FIXME: Move this shit out of this file! -namespace connection_log { +export namespace connection_log { //TODO: Save password data export type ConnectionData = { name: string; @@ -91,168 +101,150 @@ namespace connection_log { }); } -namespace Modals { - export function spawnConnectModal(options: { - default_connect_new_tab?: boolean /* default false */ - }, defaultHost: { url: string, enforce: boolean} = { url: "ts.TeaSpeak.de", enforce: false}, connect_profile?: { profile: profiles.ConnectionProfile, enforce: boolean}) { - let selected_profile: profiles.ConnectionProfile; +declare const native_client; +export function spawnConnectModal(options: { + default_connect_new_tab?: boolean /* default false */ +}, defaultHost: { url: string, enforce: boolean} = { url: "ts.TeaSpeak.de", enforce: false}, connect_profile?: { profile: ConnectionProfile, enforce: boolean}) { + let selected_profile: ConnectionProfile; - const random_id = (() => { - const array = new Uint32Array(10); - window.crypto.getRandomValues(array); - return array.join(""); - })(); + const random_id = (() => { + const array = new Uint32Array(10); + window.crypto.getRandomValues(array); + return array.join(""); + })(); - const modal = createModal({ - header: tr("Connect to a server"), - body: $("#tmpl_connect").renderTag({ - client: native_client, - forum_path: settings.static("forum_path"), - password_id: random_id, - multi_tab: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION), - default_connect_new_tab: typeof(options.default_connect_new_tab) === "boolean" && options.default_connect_new_tab - }), - footer: () => undefined, - min_width: "28em" + const modal = createModal({ + header: tr("Connect to a server"), + body: $("#tmpl_connect").renderTag({ + client: native_client, + forum_path: settings.static("forum_path"), + password_id: random_id, + multi_tab: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION), + default_connect_new_tab: typeof(options.default_connect_new_tab) === "boolean" && options.default_connect_new_tab + }), + footer: () => undefined, + min_width: "28em" + }); + + modal.htmlTag.find(".modal-body").addClass("modal-connect"); + + /* server list toggle */ + { + const container_last_servers = modal.htmlTag.find(".container-last-servers"); + const button = modal.htmlTag.find(".button-toggle-last-servers"); + const set_show = shown => { + container_last_servers.toggleClass('shown', shown); + button.find(".arrow").toggleClass('down', shown).toggleClass('up', !shown); + settings.changeGlobal("connect_show_last_servers", shown); + }; + button.on('click', event => { + set_show(!container_last_servers.hasClass("shown")); }); + set_show(settings.static_global("connect_show_last_servers", false)); + } - modal.htmlTag.find(".modal-body").addClass("modal-connect"); + const apply = (header, body, footer) => { + const container_last_server_body = modal.htmlTag.find(".container-last-servers .table .body"); + const container_empty = container_last_server_body.find(".body-empty"); + let current_connect_data: connection_log.ConnectionEntry; - /* server list toggle */ - { - const container_last_servers = modal.htmlTag.find(".container-last-servers"); - const button = modal.htmlTag.find(".button-toggle-last-servers"); - const set_show = shown => { - container_last_servers.toggleClass('shown', shown); - button.find(".arrow").toggleClass('down', shown).toggleClass('up', !shown); - settings.changeGlobal("connect_show_last_servers", shown); - }; - button.on('click', event => { - set_show(!container_last_servers.hasClass("shown")); - }); - set_show(settings.static_global("connect_show_last_servers", false)); - } + const button_connect = footer.find(".button-connect"); + const button_connect_tab = footer.find(".button-connect-new-tab"); + const button_manage = body.find(".button-manage-profiles"); - const apply = (header, body, footer) => { - const container_last_server_body = modal.htmlTag.find(".container-last-servers .table .body"); - const container_empty = container_last_server_body.find(".body-empty"); - let current_connect_data: connection_log.ConnectionEntry; + const input_profile = body.find(".container-select-profile select"); + const input_address = body.find(".container-address input"); + const input_nickname = body.find(".container-nickname input"); + const input_password = body.find(".container-password input"); - const button_connect = footer.find(".button-connect"); - const button_connect_tab = footer.find(".button-connect-new-tab"); - const button_manage = body.find(".button-manage-profiles"); - - const input_profile = body.find(".container-select-profile select"); - const input_address = body.find(".container-address input"); - const input_nickname = body.find(".container-nickname input"); - const input_password = body.find(".container-password input"); - - let updateFields = (reset_current_data: boolean) => { - if(reset_current_data) { - current_connect_data = undefined; - container_last_server_body.find(".selected").removeClass("selected"); - } - - let address = input_address.val().toString(); - settings.changeGlobal(Settings.KEY_CONNECT_ADDRESS, address); - let flag_address = !!address.match(Regex.IP_V4) || !!address.match(Regex.IP_V6) || !!address.match(Regex.DOMAIN); - - let nickname = input_nickname.val().toString(); - if(nickname) - settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, nickname); - else - nickname = input_nickname.attr("placeholder") || ""; - let flag_nickname = nickname.length >= 3 && nickname.length <= 32; - - input_address.attr('pattern', flag_address ? null : '^[a]{1000}$').toggleClass('is-invalid', !flag_address); - input_nickname.attr('pattern', flag_nickname ? null : '^[a]{1000}$').toggleClass('is-invalid', !flag_nickname); - - const flag_disabled = !flag_nickname || !flag_address || !selected_profile || !selected_profile.valid(); - button_connect.prop("disabled", flag_disabled); - button_connect_tab.prop("disabled", flag_disabled); - }; - - input_address.val(defaultHost.enforce ? defaultHost.url : settings.static_global(Settings.KEY_CONNECT_ADDRESS, defaultHost.url)); - input_address - .on("keyup", () => updateFields(true)) - .on('keydown', event => { - if(event.keyCode == KeyCode.KEY_ENTER && !event.shiftKey) - button_connect.trigger('click'); - }); - button_manage.on('click', event => { - const modal = Modals.spawnSettingsModal("identity-profiles"); - modal.close_listener.push(() => { - input_profile.trigger('change'); - }); - return true; - }); - - /* Connect Profiles */ - { - for(const profile of profiles.profiles()) { - input_profile.append( - $.spawn("option").text(profile.profile_name).val(profile.id) - ); - } - - input_profile.on('change', event => { - selected_profile = profiles.find_profile(input_profile.val() as string) || profiles.default_profile(); - { - settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, undefined); - input_nickname - .attr('placeholder', selected_profile.connect_username() || "Another TeaSpeak user") - .val(""); - } - - settings.changeGlobal(Settings.KEY_CONNECT_PROFILE, selected_profile.id); - input_profile.toggleClass("is-invalid", !selected_profile || !selected_profile.valid()); - updateFields(true); - }); - input_profile.val(connect_profile && connect_profile.profile ? - connect_profile.profile.id : - settings.static_global(Settings.KEY_CONNECT_PROFILE, "default") - ).trigger('change'); + let updateFields = (reset_current_data: boolean) => { + if(reset_current_data) { + current_connect_data = undefined; + container_last_server_body.find(".selected").removeClass("selected"); } - const last_nickname = settings.static_global(Settings.KEY_CONNECT_USERNAME, undefined); - if(last_nickname) /* restore */ - settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, last_nickname); + let address = input_address.val().toString(); + settings.changeGlobal(Settings.KEY_CONNECT_ADDRESS, address); + let flag_address = !!address.match(Regex.IP_V4) || !!address.match(Regex.IP_V6) || !!address.match(Regex.DOMAIN); - input_nickname.val(last_nickname); - input_nickname.on("keyup", () => updateFields(true)); - setTimeout(() => updateFields(false), 100); + let nickname = input_nickname.val().toString(); + if(nickname) + settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, nickname); + else + nickname = input_nickname.attr("placeholder") || ""; + let flag_nickname = nickname.length >= 3 && nickname.length <= 32; - const server_address = () => { - let address = input_address.val().toString(); - if(address.match(Regex.IP_V6) && !address.startsWith("[")) - return "[" + address + "]"; - return address; - }; - button_connect.on('click', event => { - modal.close(); + input_address.attr('pattern', flag_address ? null : '^[a]{1000}$').toggleClass('is-invalid', !flag_address); + input_nickname.attr('pattern', flag_nickname ? null : '^[a]{1000}$').toggleClass('is-invalid', !flag_nickname); - const connection = server_connections.active_connection_handler(); - if(connection) { - connection.startConnection( - current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(), - selected_profile, - true, - { - nickname: input_nickname.val().toString() || input_nickname.attr("placeholder"), - password: (current_connect_data && current_connect_data.password_hash) ? {password: current_connect_data.password_hash, hashed: true} : {password: input_password.val().toString(), hashed: false} - } - ); - } else { - button_connect_tab.trigger('click'); - } + const flag_disabled = !flag_nickname || !flag_address || !selected_profile || !selected_profile.valid(); + button_connect.prop("disabled", flag_disabled); + button_connect_tab.prop("disabled", flag_disabled); + }; + + input_address.val(defaultHost.enforce ? defaultHost.url : settings.static_global(Settings.KEY_CONNECT_ADDRESS, defaultHost.url)); + input_address + .on("keyup", () => updateFields(true)) + .on('keydown', event => { + if(event.keyCode == KeyCode.KEY_ENTER && !event.shiftKey) + button_connect.trigger('click'); }); - button_connect_tab.on('click', event => { - modal.close(); + button_manage.on('click', event => { + const modal = spawnSettingsModal("identity-profiles"); + modal.close_listener.push(() => { + input_profile.trigger('change'); + }); + return true; + }); - const connection = server_connections.spawn_server_connection_handler(); - server_connections.set_active_connection_handler(connection); + /* Connect Profiles */ + { + for(const profile of profiles()) { + input_profile.append( + $.spawn("option").text(profile.profile_name).val(profile.id) + ); + } + + input_profile.on('change', event => { + selected_profile = find_profile(input_profile.val() as string) || default_profile(); + { + settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, undefined); + input_nickname + .attr('placeholder', selected_profile.connect_username() || "Another TeaSpeak user") + .val(""); + } + + settings.changeGlobal(Settings.KEY_CONNECT_PROFILE, selected_profile.id); + input_profile.toggleClass("is-invalid", !selected_profile || !selected_profile.valid()); + updateFields(true); + }); + input_profile.val(connect_profile && connect_profile.profile ? + connect_profile.profile.id : + settings.static_global(Settings.KEY_CONNECT_PROFILE, "default") + ).trigger('change'); + } + + const last_nickname = settings.static_global(Settings.KEY_CONNECT_USERNAME, undefined); + if(last_nickname) /* restore */ + settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, last_nickname); + + input_nickname.val(last_nickname); + input_nickname.on("keyup", () => updateFields(true)); + setTimeout(() => updateFields(false), 100); + + const server_address = () => { + let address = input_address.val().toString(); + if(address.match(Regex.IP_V6) && !address.startsWith("[")) + return "[" + address + "]"; + return address; + }; + button_connect.on('click', event => { + modal.close(); + + const connection = server_connections.active_connection_handler(); + if(connection) { connection.startConnection( - current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(), + current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(), selected_profile, true, { @@ -260,72 +252,89 @@ namespace Modals { password: (current_connect_data && current_connect_data.password_hash) ? {password: current_connect_data.password_hash, hashed: true} : {password: input_password.val().toString(), hashed: false} } ); - }); - - - /* connect history show */ - { - for(const entry of connection_log.history().slice(0, 10)) { - $.spawn("div").addClass("row").append( - $.spawn("div").addClass("column delete").append($.spawn("div").addClass("icon_em client-delete")).on('click', event => { - event.preventDefault(); - - const row = $(event.target).parents('.row'); - row.hide(250, () => { - row.detach(); - }); - connection_log.delete_entry(entry.address); - container_empty.toggle(container_last_server_body.children().length > 1); - }) - ).append( - $.spawn("div").addClass("column name").append([ - IconManager.generate_tag(IconManager.load_cached_icon(entry.icon_id)), - $.spawn("a").text(entry.name) - ]) - ).append( - $.spawn("div").addClass("column address").text(entry.address.hostname + (entry.address.port != 9987 ? (":" + entry.address.port) : "")) - ).append( - $.spawn("div").addClass("column password").text(entry.flag_password ? tr("Yes") : tr("No")) - ).append( - $.spawn("div").addClass("column country-name").append([ - $.spawn("div").addClass("country flag-" + entry.country.toLowerCase()), - $.spawn("a").text(i18n.country_name(entry.country, tr("Global"))) - ]) - ).append( - $.spawn("div").addClass("column clients").text(entry.clients_online + "/" + entry.clients_total) - ).append( - $.spawn("div").addClass("column connections").text(entry.total_connection + "") - ).on('click', event => { - if(event.isDefaultPrevented()) - return; - - event.preventDefault(); - current_connect_data = entry; - container_last_server_body.find(".selected").removeClass("selected"); - $(event.target).parent('.row').addClass('selected'); - - input_address.val(entry.address.hostname + (entry.address.port != 9987 ? (":" + entry.address.port) : "")); - input_password.val(entry.flag_password && entry.password_hash ? "WolverinDEV Yeahr!" : "").trigger('change'); - }).on('dblclick', event => { - current_connect_data = entry; - button_connect.trigger('click'); - }).appendTo(container_last_server_body); - container_empty.toggle(false); - } + } else { + button_connect_tab.trigger('click'); } - }; - apply(modal.htmlTag, modal.htmlTag, modal.htmlTag); + }); + button_connect_tab.on('click', event => { + modal.close(); - modal.open(); - return; - } + const connection = server_connections.spawn_server_connection_handler(); + server_connections.set_active_connection_handler(connection); + connection.startConnection( + current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(), + selected_profile, + true, + { + nickname: input_nickname.val().toString() || input_nickname.attr("placeholder"), + password: (current_connect_data && current_connect_data.password_hash) ? {password: current_connect_data.password_hash, hashed: true} : {password: input_password.val().toString(), hashed: false} + } + ); + }); - 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*$/, + + /* connect history show */ + { + for(const entry of connection_log.history().slice(0, 10)) { + $.spawn("div").addClass("row").append( + $.spawn("div").addClass("column delete").append($.spawn("div").addClass("icon_em client-delete")).on('click', event => { + event.preventDefault(); + + const row = $(event.target).parents('.row'); + row.hide(250, () => { + row.detach(); + }); + connection_log.delete_entry(entry.address); + container_empty.toggle(container_last_server_body.children().length > 1); + }) + ).append( + $.spawn("div").addClass("column name").append([ + IconManager.generate_tag(IconManager.load_cached_icon(entry.icon_id)), + $.spawn("a").text(entry.name) + ]) + ).append( + $.spawn("div").addClass("column address").text(entry.address.hostname + (entry.address.port != 9987 ? (":" + entry.address.port) : "")) + ).append( + $.spawn("div").addClass("column password").text(entry.flag_password ? tr("Yes") : tr("No")) + ).append( + $.spawn("div").addClass("column country-name").append([ + $.spawn("div").addClass("country flag-" + entry.country.toLowerCase()), + $.spawn("a").text(i18nc.country_name(entry.country, tr("Global"))) + ]) + ).append( + $.spawn("div").addClass("column clients").text(entry.clients_online + "/" + entry.clients_total) + ).append( + $.spawn("div").addClass("column connections").text(entry.total_connection + "") + ).on('click', event => { + if(event.isDefaultPrevented()) + return; + + event.preventDefault(); + current_connect_data = entry; + container_last_server_body.find(".selected").removeClass("selected"); + $(event.target).parent('.row').addClass('selected'); + + input_address.val(entry.address.hostname + (entry.address.port != 9987 ? (":" + entry.address.port) : "")); + input_password.val(entry.flag_password && entry.password_hash ? "WolverinDEV Yeahr!" : "").trigger('change'); + }).on('dblclick', event => { + current_connect_data = entry; + button_connect.trigger('click'); + }).appendTo(container_last_server_body); + container_empty.toggle(false); + } + } }; -} \ No newline at end of file + apply(modal.htmlTag, modal.htmlTag, modal.htmlTag); + + modal.open(); + return; +} + +export const Regex = { + //DOMAIN<:port> + DOMAIN: /^(localhost|((([a-zA-Z0-9_-]{0,63}\.){0,253})?[a-zA-Z0-9_-]{0,63}\.[a-zA-Z]{2,64}))(|:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[0-5]?[0-9]{1,46}))$/, + //IP<:port> + IP_V4: /(^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(|:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[0-5]?[0-9]{1,4}))$/, + IP_V6: /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/, + IP: /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$|^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/, +}; \ No newline at end of file diff --git a/shared/js/ui/modal/ModalCreateChannel.ts b/shared/js/ui/modal/ModalCreateChannel.ts index 5f7be3bc..a5be35d3 100644 --- a/shared/js/ui/modal/ModalCreateChannel.ts +++ b/shared/js/ui/modal/ModalCreateChannel.ts @@ -1,711 +1,720 @@ -/// +import PermissionType from "tc-shared/permission/PermissionType"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {ChannelEntry, ChannelProperties} from "tc-shared/ui/channel"; +import {PermissionManager, PermissionValue} from "tc-shared/permission/PermissionManager"; +import {LogCategory} from "tc-shared/log"; +import {createModal} from "tc-shared/ui/elements/Modal"; +import * as log from "tc-shared/log"; +import {Settings, settings} from "tc-shared/settings"; +import * as tooltip from "tc-shared/ui/elements/Tooltip"; +import {spawnIconSelect} from "tc-shared/ui/modal/ModalIconSelect"; +import {hashPassword} from "tc-shared/utils/helpers"; +import {sliderfy} from "tc-shared/ui/elements/Slider"; -namespace Modals { - export function createChannelModal(connection: ConnectionHandler, channel: ChannelEntry | undefined, parent: ChannelEntry | undefined, permissions: PermissionManager, callback: (properties?: ChannelProperties, permissions?: PermissionValue[]) => any) { - let properties: ChannelProperties = { } as ChannelProperties; //The changes properties - const modal = createModal({ - header: channel ? tr("Edit channel") : tr("Create channel"), - body: () => { - const render_properties = {}; - Object.assign(render_properties, channel ? channel.properties : { - channel_flag_maxfamilyclients_unlimited: true, - channel_flag_maxclients_unlimited: true, - }); - render_properties["channel_icon_tab"] = connection.fileManager.icons.generateTag(channel ? channel.properties.channel_icon_id : 0); - render_properties["channel_icon_general"] = connection.fileManager.icons.generateTag(channel ? channel.properties.channel_icon_id : 0); - render_properties["create"] = !channel; - - let template = $("#tmpl_channel_edit").renderTag(render_properties); - - /* the tab functionality */ - { - const container_tabs = template.find(".container-advanced"); - container_tabs.find(".categories .entry").on('click', event => { - const entry = $(event.target); - - container_tabs.find(".bodies > .body").addClass("hidden"); - container_tabs.find(".categories > .selected").removeClass("selected"); - - entry.addClass("selected"); - container_tabs.find(".bodies > .body." + entry.attr("container")).removeClass("hidden"); - }); - - container_tabs.find(".entry").first().trigger('click'); - } - - /* Advanced/normal switch */ - { - const input = template.find(".input-advanced-mode"); - const container_mode = template.find(".mode-container"); - const container_advanced = container_mode.find(".container-advanced"); - const container_simple = container_mode.find(".container-simple"); - input.on('change', event => { - const advanced = input.prop("checked"); - settings.changeGlobal(Settings.KEY_CHANNEL_EDIT_ADVANCED, advanced); - - container_mode.css("overflow", "hidden"); - container_advanced.show().toggleClass("hidden", !advanced); - container_simple.show().toggleClass("hidden", advanced); - - setTimeout(() => { - container_advanced.toggle(advanced); - container_simple.toggle(!advanced); - container_mode.css("overflow", "visible"); - }, 300); - }).prop("checked", settings.static_global(Settings.KEY_CHANNEL_EDIT_ADVANCED)).trigger('change'); - } - - return template.tabify().children(); /* the "render" div */ - }, - footer: null, - width: 500 - }); - modal.htmlTag.find(".modal-body").addClass("modal-channel modal-blue"); - - - applyGeneralListener(connection, properties, modal.htmlTag.find(".container-general"), modal.htmlTag.find(".button_ok"), channel); - applyStandardListener(connection, properties, modal.htmlTag.find(".container-standard"), modal.htmlTag.find(".container-simple"), parent, channel); - applyPermissionListener(connection, properties, modal.htmlTag.find(".container-permissions"), modal.htmlTag.find(".button_ok"), permissions, channel); - applyAudioListener(connection, properties, modal.htmlTag.find(".container-audio"), modal.htmlTag.find(".container-simple"), channel); - applyAdvancedListener(connection, properties, modal.htmlTag.find(".container-misc"), modal.htmlTag.find(".button_ok"), channel); - - let updated: PermissionValue[] = []; - modal.htmlTag.find(".button_ok").click(() => { - modal.htmlTag.find(".container-permissions").find("input[permission]").each((index, _element) => { - let element = $(_element); - if(element.val() == element.attr("original-value")) return; - let permission = permissions.resolveInfo(element.attr("permission")); - if(!permission) { - log.error(LogCategory.PERMISSIONS, tr("Failed to resolve channel permission for name %o"), element.attr("permission")); - element.prop("disabled", true); - return; - } - - updated.push(new PermissionValue(permission, element.val())); +export function createChannelModal(connection: ConnectionHandler, channel: ChannelEntry | undefined, parent: ChannelEntry | undefined, permissions: PermissionManager, callback: (properties?: ChannelProperties, permissions?: PermissionValue[]) => any) { + let properties: ChannelProperties = { } as ChannelProperties; //The changes properties + const modal = createModal({ + header: channel ? tr("Edit channel") : tr("Create channel"), + body: () => { + const render_properties = {}; + Object.assign(render_properties, channel ? channel.properties : { + channel_flag_maxfamilyclients_unlimited: true, + channel_flag_maxclients_unlimited: true, }); - console.log(tr("Updated permissions %o"), updated); - }).click(() => { - modal.close(); - for(const key of Object.keys(channel ? channel.properties : {})) - if(channel.properties[key] == properties[key]) - delete properties[key]; - callback(properties, updated); //First may create the channel - }); + render_properties["channel_icon_tab"] = connection.fileManager.icons.generateTag(channel ? channel.properties.channel_icon_id : 0); + render_properties["channel_icon_general"] = connection.fileManager.icons.generateTag(channel ? channel.properties.channel_icon_id : 0); + render_properties["create"] = !channel; - tooltip(modal.htmlTag); - modal.htmlTag.find(".button_cancel").click(() => { - modal.close(); - callback(); - }); + let template = $("#tmpl_channel_edit").renderTag(render_properties); - modal.open(); - if(!channel) - modal.htmlTag.find(".channel_name").focus(); + /* the tab functionality */ + { + const container_tabs = template.find(".container-advanced"); + container_tabs.find(".categories .entry").on('click', event => { + const entry = $(event.target); + + container_tabs.find(".bodies > .body").addClass("hidden"); + container_tabs.find(".categories > .selected").removeClass("selected"); + + entry.addClass("selected"); + container_tabs.find(".bodies > .body." + entry.attr("container")).removeClass("hidden"); + }); + + container_tabs.find(".entry").first().trigger('click'); + } + + /* Advanced/normal switch */ + { + const input = template.find(".input-advanced-mode"); + const container_mode = template.find(".mode-container"); + const container_advanced = container_mode.find(".container-advanced"); + const container_simple = container_mode.find(".container-simple"); + input.on('change', event => { + const advanced = input.prop("checked"); + settings.changeGlobal(Settings.KEY_CHANNEL_EDIT_ADVANCED, advanced); + + container_mode.css("overflow", "hidden"); + container_advanced.show().toggleClass("hidden", !advanced); + container_simple.show().toggleClass("hidden", advanced); + + setTimeout(() => { + container_advanced.toggle(advanced); + container_simple.toggle(!advanced); + container_mode.css("overflow", "visible"); + }, 300); + }).prop("checked", settings.static_global(Settings.KEY_CHANNEL_EDIT_ADVANCED)).trigger('change'); + } + + return template.tabify().children(); /* the "render" div */ + }, + footer: null, + width: 500 + }); + modal.htmlTag.find(".modal-body").addClass("modal-channel modal-blue"); + + + applyGeneralListener(connection, properties, modal.htmlTag.find(".container-general"), modal.htmlTag.find(".button_ok"), channel); + applyStandardListener(connection, properties, modal.htmlTag.find(".container-standard"), modal.htmlTag.find(".container-simple"), parent, channel); + applyPermissionListener(connection, properties, modal.htmlTag.find(".container-permissions"), modal.htmlTag.find(".button_ok"), permissions, channel); + applyAudioListener(connection, properties, modal.htmlTag.find(".container-audio"), modal.htmlTag.find(".container-simple"), channel); + applyAdvancedListener(connection, properties, modal.htmlTag.find(".container-misc"), modal.htmlTag.find(".button_ok"), channel); + + let updated: PermissionValue[] = []; + modal.htmlTag.find(".button_ok").click(() => { + modal.htmlTag.find(".container-permissions").find("input[permission]").each((index, _element) => { + let element = $(_element); + if(element.val() == element.attr("original-value")) return; + let permission = permissions.resolveInfo(element.attr("permission")); + if(!permission) { + log.error(LogCategory.PERMISSIONS, tr("Failed to resolve channel permission for name %o"), element.attr("permission")); + element.prop("disabled", true); + return; + } + + updated.push(new PermissionValue(permission, element.val())); + }); + console.log(tr("Updated permissions %o"), updated); + }).click(() => { + modal.close(); + for(const key of Object.keys(channel ? channel.properties : {})) + if(channel.properties[key] == properties[key]) + delete properties[key]; + callback(properties, updated); //First may create the channel + }); + + tooltip.initialize(modal.htmlTag); + modal.htmlTag.find(".button_cancel").click(() => { + modal.close(); + callback(); + }); + + modal.open(); + if(!channel) + modal.htmlTag.find(".channel_name").focus(); +} + +function applyGeneralListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, channel: ChannelEntry | undefined) { + let updateButton = () => { + const status = tag.find(".input_error").length != 0; + console.log("Disabled: %o", status); + button.prop("disabled", status); + }; + + { + const channel_name = tag.find(".channel_name"); + tag.find(".channel_name").on('change keyup', function (this: HTMLInputElement) { + properties.channel_name = this.value; + + channel_name.toggleClass("input_error", this.value.length < 1 || this.value.length > 40); + updateButton(); + }).prop("disabled", channel && !connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_NAME).granted(1)); } - function applyGeneralListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, channel: ChannelEntry | undefined) { - let updateButton = () => { - const status = tag.find(".input_error").length != 0; - console.log("Disabled: %o", status); - button.prop("disabled", status); - }; - - { - const channel_name = tag.find(".channel_name"); - tag.find(".channel_name").on('change keyup', function (this: HTMLInputElement) { - properties.channel_name = this.value; - - channel_name.toggleClass("input_error", this.value.length < 1 || this.value.length > 40); - updateButton(); - }).prop("disabled", channel && !connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_NAME).granted(1)); - } - - tag.find(".button-select-icon").on('click', event => { - Modals.spawnIconSelect(connection, id => { - const icon_node = tag.find(".icon-preview"); - icon_node.children().remove(); - icon_node.append(connection.fileManager.icons.generateTag(id)); - - console.log("Selected icon ID: %d", id); - properties.channel_icon_id = id; - }, channel ? channel.properties.channel_icon_id : 0); - }); - - tag.find(".button-icon-remove").on('click', event => { + tag.find(".button-select-icon").on('click', event => { + spawnIconSelect(connection, id => { const icon_node = tag.find(".icon-preview"); icon_node.children().remove(); - icon_node.append(connection.fileManager.icons.generateTag(0)); + icon_node.append(connection.fileManager.icons.generateTag(id)); - console.log("Remove channel icon"); - properties.channel_icon_id = 0; - }); + console.log("Selected icon ID: %d", id); + properties.channel_icon_id = id; + }, channel ? channel.properties.channel_icon_id : 0); + }); - { - const channel_password = tag.find(".channel_password"); - tag.find(".channel_password").change(function (this: HTMLInputElement) { - properties.channel_flag_password = this.value.length != 0; - if(properties.channel_flag_password) - helpers.hashPassword(this.value).then(pass => properties.channel_password = pass); + tag.find(".button-icon-remove").on('click', event => { + const icon_node = tag.find(".icon-preview"); + icon_node.children().remove(); + icon_node.append(connection.fileManager.icons.generateTag(0)); - channel_password.removeClass("input_error"); - if(!properties.channel_flag_password) - if(connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_FORCE_PASSWORD).granted(1)) - channel_password.addClass("input_error"); - updateButton(); - }).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_PASSWORD : PermissionType.B_CHANNEL_MODIFY_PASSWORD).granted(1)); - } + console.log("Remove channel icon"); + properties.channel_icon_id = 0; + }); - tag.find(".channel_topic").change(function (this: HTMLInputElement) { - properties.channel_topic = this.value; - }).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_TOPIC : PermissionType.B_CHANNEL_MODIFY_TOPIC).granted(1)); + { + const channel_password = tag.find(".channel_password"); + tag.find(".channel_password").change(function (this: HTMLInputElement) { + properties.channel_flag_password = this.value.length != 0; + if(properties.channel_flag_password) + hashPassword(this.value).then(pass => properties.channel_password = pass); - { - const container = tag.find(".container-description"); - const input = container.find("textarea"); - - const insert_tag = (open: string, close: string) => { - if(input.prop("disabled")) - return; - - const node = input[0] as HTMLTextAreaElement; - if (node.selectionStart || node.selectionStart == 0) { - const startPos = node.selectionStart; - const endPos = node.selectionEnd; - node.value = node.value.substring(0, startPos) + open + node.value.substring(startPos, endPos) + close + node.value.substring(endPos); - node.selectionEnd = endPos + open.length; - node.selectionStart = node.selectionEnd; - } else { - node.value += open + close; - node.selectionEnd = node.value.length - close.length; - node.selectionStart = node.selectionEnd; - } - - input.focus().trigger('change'); - }; - - input.on('change', event => { - console.log(tr("Channel description edited: %o"), input.val()); - properties.channel_description = input.val() as string; - }); - - container.find(".button-bold").on('click', () => insert_tag('[b]', '[/b]')); - container.find(".button-italic").on('click', () => insert_tag('[i]', '[/i]')); - container.find(".button-underline").on('click', () => insert_tag('[u]', '[/u]')); - container.find(".button-color input").on('change', event => { - insert_tag('[color=' + (event.target as HTMLInputElement).value + ']', '[/color]') - }) - } - tag.find(".channel_description").change(function (this: HTMLInputElement) { - properties.channel_description = this.value; - }).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_DESCRIPTION : PermissionType.B_CHANNEL_MODIFY_DESCRIPTION).granted(1)); - - if(!channel) { - setTimeout(() => { - tag.find(".channel_name").trigger("change"); - tag.find(".channel_password").trigger('change'); - }, 0); - } + channel_password.removeClass("input_error"); + if(!properties.channel_flag_password) + if(connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_FORCE_PASSWORD).granted(1)) + channel_password.addClass("input_error"); + updateButton(); + }).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_PASSWORD : PermissionType.B_CHANNEL_MODIFY_PASSWORD).granted(1)); } - function applyStandardListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, simple: JQuery, parent: ChannelEntry, channel: ChannelEntry) { - /* Channel type */ - { - const input_advanced_type = tag.find("input[name='channel_type']"); + tag.find(".channel_topic").change(function (this: HTMLInputElement) { + properties.channel_topic = this.value; + }).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_TOPIC : PermissionType.B_CHANNEL_MODIFY_TOPIC).granted(1)); - let _in_update = false; - const update_simple_type = () => { - if(_in_update) - return; + { + const container = tag.find(".container-description"); + const input = container.find("textarea"); - let type; - if(properties.channel_flag_default || (typeof(properties.channel_flag_default) === "undefined" && channel && channel.properties.channel_flag_default)) - type = "def"; - else if(properties.channel_flag_permanent || (typeof(properties.channel_flag_permanent) === "undefined" && channel && channel.properties.channel_flag_permanent)) - type = "perm"; - else if(properties.channel_flag_semi_permanent || (typeof(properties.channel_flag_semi_permanent) === "undefined" && channel && channel.properties.channel_flag_semi_permanent)) - type = "semi"; - else - type = "temp"; + const insert_tag = (open: string, close: string) => { + if(input.prop("disabled")) + return; - simple.find("option[name='channel-type'][value='" + type + "']").prop("selected", true); - }; - - input_advanced_type.on('change', event => { - const value = [...input_advanced_type as JQuery].find(e => e.checked).value; - switch(value) { - case "semi": - properties.channel_flag_permanent = false; - properties.channel_flag_semi_permanent = true; - break; - case "perm": - properties.channel_flag_permanent = true; - properties.channel_flag_semi_permanent = false; - break; - default: - properties.channel_flag_permanent = false; - properties.channel_flag_semi_permanent = false; - break; - } - update_simple_type(); - }); - - const permission_temp = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_TEMPORARY : PermissionType.B_CHANNEL_MODIFY_MAKE_TEMPORARY).granted(1); - const permission_semi = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_SEMI_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_SEMI_PERMANENT).granted(1); - const permission_perm = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1); - const permission_default = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1) && - connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_DEFAULT : PermissionType.B_CHANNEL_MODIFY_MAKE_DEFAULT).granted(1); - - /* advanced type listeners */ - const container_types = tag.find(".container-channel-type"); - const tag_type_temp = container_types.find(".type-temp"); - const tag_type_semi = container_types.find(".type-semi"); - const tag_type_perm = container_types.find(".type-perm"); - const select_default = tag.find(".input-flag-default"); - - { - select_default.on('change', event => { - const node = select_default[0] as HTMLInputElement; - properties.channel_flag_default = node.checked; - - if(node.checked) - tag_type_perm.find("input").prop("checked", true); - - tag_type_temp - .toggleClass("disabled", node.checked || !permission_temp) - .find("input").prop("disabled", node.checked || !permission_temp); - - tag_type_semi - .toggleClass("disabled", node.checked || !permission_semi) - .find("input").prop("disabled", node.checked || !permission_semi); - - tag_type_perm - .toggleClass("disabled", node.checked || !permission_perm) - .find("input").prop("disabled", node.checked || !permission_perm); - - update_simple_type(); - }).prop("disabled", !permission_default).trigger('change').parent().toggleClass("disabled", !permission_default); - } - - /* simple */ - { - simple.find("option[name='channel-type'][value='def']").prop("disabled", !permission_default); - simple.find("option[name='channel-type'][value='perm']").prop("disabled", !permission_perm); - simple.find("option[name='channel-type'][value='semi']").prop("disabled", !permission_semi); - simple.find("option[name='channel-type'][value='temp']").prop("disabled", !permission_temp); - - simple.find("select[name='channel-type']").on('change', event => { - try { - _in_update = true; - switch ((event.target as HTMLSelectElement).value) { - case "temp": - properties.channel_flag_permanent = false; - properties.channel_flag_semi_permanent = false; - properties.channel_flag_default = false; - select_default.prop("checked", false).trigger('change'); - tag_type_temp.trigger('click'); - break; - case "semi": - properties.channel_flag_permanent = false; - properties.channel_flag_semi_permanent = true; - properties.channel_flag_default = false; - select_default.prop("checked", false).trigger('change'); - tag_type_semi.trigger('click'); - break; - case "perm": - properties.channel_flag_permanent = true; - properties.channel_flag_semi_permanent = false; - properties.channel_flag_default = false; - select_default.prop("checked", false).trigger('change'); - tag_type_perm.trigger('click'); - break; - case "def": - properties.channel_flag_permanent = true; - properties.channel_flag_semi_permanent = false; - properties.channel_flag_default = true; - select_default.prop("checked", true).trigger('change'); - break; - } - } finally { - _in_update = false; - /* We dont need to update the simple type because we changed the advanced part to the just changed simple part */ - //update_simple_type(); - } - }); - } - - /* init */ - setTimeout(() => { - if(!channel) { - if(permission_perm) - tag_type_perm.find("input").trigger('click'); - else if(permission_semi) - tag_type_semi.find("input").trigger('click'); - else - tag_type_temp.find("input").trigger('click'); - } else { - if(channel.properties.channel_flag_permanent) - tag_type_perm.find("input").trigger('click'); - else if(channel.properties.channel_flag_semi_permanent) - tag_type_semi.find("input").trigger('click'); - else - tag_type_temp.find("input").trigger('click'); - } - }, 0); - } - - /* Talk power */ - { - const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_NEEDED_TALK_POWER : PermissionType.B_CHANNEL_MODIFY_NEEDED_TALK_POWER).granted(1); - const input_advanced = tag.find("input[name='talk_power']").prop("disabled", !permission); - const input_simple = simple.find("input[name='talk_power']").prop("disabled", !permission); - - input_advanced.on('change', event => { - properties.channel_needed_talk_power = parseInt(input_advanced.val() as string); - input_simple.val(input_advanced.val()); - }); - - input_simple.on('change', event => { - properties.channel_needed_talk_power = parseInt(input_simple.val() as string); - input_advanced.val(input_simple.val()); - }); - } - - /* Channel order */ - { - const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_SORTORDER : PermissionType.B_CHANNEL_MODIFY_SORTORDER).granted(1); - - const advanced_order_id = tag.find(".order_id").prop("disabled", !permission) as JQuery; - const simple_order_id = simple.find(".order_id").prop("disabled", !permission) as JQuery; - - for(let previous_channel of (parent ? parent.children() : connection.channelTree.rootChannel())) { - let selected = channel && channel.properties.channel_order == previous_channel.channelId; - $.spawn("option").attr("channelId", previous_channel.channelId.toString()).prop("selected", selected).text(previous_channel.channelName()).appendTo(advanced_order_id); - $.spawn("option").attr("channelId", previous_channel.channelId.toString()).prop("selected", selected).text(previous_channel.channelName()).appendTo(simple_order_id); - } - - advanced_order_id.on('change', event => { - simple_order_id[0].selectedIndex = advanced_order_id[0].selectedIndex; - const selected = $(advanced_order_id[0].options.item(advanced_order_id[0].selectedIndex)); - properties.channel_order = parseInt(selected.attr("channelId")); - }); - - simple_order_id.on('change', event => { - advanced_order_id[0].selectedIndex = simple_order_id[0].selectedIndex; - const selected = $(simple_order_id[0].options.item(simple_order_id[0].selectedIndex)); - properties.channel_order = parseInt(selected.attr("channelId")); - }); - } - - - /* Advanced only */ - { - const container_max_users = tag.find(".container-max-users"); - - const container_unlimited = container_max_users.find(".container-unlimited"); - const container_limited = container_max_users.find(".container-limited"); - - const input_unlimited = container_unlimited.find("input[value='unlimited']"); - const input_limited = container_limited.find("input[value='limited']"); - const input_limit = container_limited.find(".channel_maxclients"); - - const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1); - - if(!permission) { - input_unlimited.prop("disabled", true); - input_limited.prop("disabled", true); - input_limit.prop("disabled", true); - - container_limited.addClass("disabled"); - container_unlimited.addClass("disabled"); + const node = input[0] as HTMLTextAreaElement; + if (node.selectionStart || node.selectionStart == 0) { + const startPos = node.selectionStart; + const endPos = node.selectionEnd; + node.value = node.value.substring(0, startPos) + open + node.value.substring(startPos, endPos) + close + node.value.substring(endPos); + node.selectionEnd = endPos + open.length; + node.selectionStart = node.selectionEnd; } else { - container_max_users.find("input[name='max_users']").on('change', event => { - const node = event.target as HTMLInputElement; - console.log(tr("Channel max user mode: %o"), node.value); - - const flag = node.value === "unlimited"; - input_limit - .prop("disabled", flag) - .parent().toggleClass("disabled", flag); - properties.channel_flag_maxclients_unlimited = flag; - }); - - input_limit.on('change', event => { - properties.channel_maxclients = parseInt(input_limit.val() as string); - console.log(tr("Changed max user limit to %o"), properties.channel_maxclients); - }); - - setTimeout(() => container_max_users.find("input:checked").trigger('change'), 100); + node.value += open + close; + node.selectionEnd = node.value.length - close.length; + node.selectionStart = node.selectionEnd; } - } - { - const container_max_users = tag.find(".container-max-family-users"); - - const container_unlimited = container_max_users.find(".container-unlimited"); - const container_inherited = container_max_users.find(".container-inherited"); - const container_limited = container_max_users.find(".container-limited"); - - const input_unlimited = container_unlimited.find("input[value='unlimited']"); - const input_inherited = container_inherited.find("input[value='inherited']"); - const input_limited = container_limited.find("input[value='limited']"); - const input_limit = container_limited.find(".channel_maxfamilyclients"); - - const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1); - - if(!permission) { - input_unlimited.prop("disabled", true); - input_inherited.prop("disabled", true); - input_limited.prop("disabled", true); - input_limit.prop("disabled", true); - - container_limited.addClass("disabled"); - container_unlimited.addClass("disabled"); - container_inherited.addClass("disabled"); - } else { - container_max_users.find("input[name='max_family_users']").on('change', event => { - const node = event.target as HTMLInputElement; - console.log(tr("Channel max family user mode: %o"), node.value); - - const flag_unlimited = node.value === "unlimited"; - const flag_inherited = node.value === "inherited"; - input_limit - .prop("disabled", flag_unlimited || flag_inherited) - .parent().toggleClass("disabled", flag_unlimited || flag_inherited); - properties.channel_flag_maxfamilyclients_unlimited = flag_unlimited; - properties.channel_flag_maxfamilyclients_inherited = flag_inherited; - }); - - input_limit.on('change', event => { - properties.channel_maxfamilyclients = parseInt(input_limit.val() as string); - console.log(tr("Changed max family user limit to %o"), properties.channel_maxfamilyclients); - }); - - setTimeout(() => container_max_users.find("input:checked").trigger('change'), 100); - } - } - } - - function applyPermissionListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, permissions: PermissionManager, channel?: ChannelEntry) { - let apply_permissions = (channel_permissions: PermissionValue[]) => { - log.trace(LogCategory.CHANNEL, tr("Received channel permissions: %o"), channel_permissions); - - let required_power = -2; - for(let cperm of channel_permissions) - if(cperm.type.name == PermissionType.I_CHANNEL_NEEDED_MODIFY_POWER) { - required_power = cperm.value; - break; - } - - tag.find("input[permission]").each((index, _element) => { - let element = $(_element); - element.attr("original-value", 0); - element.val(0); - - let permission = permissions.resolveInfo(element.attr("permission")); - if(!permission) { - log.error(LogCategory.PERMISSIONS, tr("Failed to resolve channel permission for name %o"), element.attr("permission")); - element.prop("disabled", true); - return; - } - - for(let cperm of channel_permissions) - if(cperm.type == permission) { - element.val(cperm.value); - element.attr("original-value", cperm.value); - return; - } - }); - - const permission = permissions.neededPermission(PermissionType.I_CHANNEL_PERMISSION_MODIFY_POWER).granted(required_power, false); - tag.find("input[permission]").prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission); //No permissions + input.focus().trigger('change'); }; - if(channel) { - permissions.requestChannelPermissions(channel.getChannelId()).then(apply_permissions).catch((error) => { - tag.find("input[permission]").prop("disabled", true); - console.log("Failed to receive channel permissions (%o)", error); - }); - } else apply_permissions([]); + input.on('change', event => { + console.log(tr("Channel description edited: %o"), input.val()); + properties.channel_description = input.val() as string; + }); + + container.find(".button-bold").on('click', () => insert_tag('[b]', '[/b]')); + container.find(".button-italic").on('click', () => insert_tag('[i]', '[/i]')); + container.find(".button-underline").on('click', () => insert_tag('[u]', '[/u]')); + container.find(".button-color input").on('change', event => { + insert_tag('[color=' + (event.target as HTMLInputElement).value + ']', '[/color]') + }) } + tag.find(".channel_description").change(function (this: HTMLInputElement) { + properties.channel_description = this.value; + }).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_DESCRIPTION : PermissionType.B_CHANNEL_MODIFY_DESCRIPTION).granted(1)); - function applyAudioListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, simple: JQuery, channel?: ChannelEntry) { - const bandwidth_mapping = [ - /* SPEEX narrow */ [2.49, 2.69, 2.93, 3.17, 3.17, 3.56, 3.56, 4.05, 4.05, 4.44, 5.22], - /* SPEEX wide */ [2.69, 2.93, 3.17, 3.42, 3.76, 4.25, 4.74, 5.13, 5.62, 6.40, 7.37], - /* SPEEX ultra */ [2.73, 3.12, 3.37, 3.61, 4.00, 4.49, 4.93, 5.32, 5.81, 6.59, 7.57], - /* CELT */ [6.10, 6.10, 7.08, 7.08, 7.08, 8.06, 8.06, 8.06, 8.06, 10.01, 13.92], + if(!channel) { + setTimeout(() => { + tag.find(".channel_name").trigger("change"); + tag.find(".channel_password").trigger('change'); + }, 0); + } +} - /* Opus Voice */ [2.73, 3.22, 3.71, 4.20, 4.74, 5.22, 5.71, 6.20, 6.74, 7.23, 7.71], - /* Opus Music */ [3.08, 3.96, 4.83, 5.71, 6.59, 7.47, 8.35, 9.23, 10.11, 10.99, 11.87] - ]; +function applyStandardListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, simple: JQuery, parent: ChannelEntry, channel: ChannelEntry) { + /* Channel type */ + { + const input_advanced_type = tag.find("input[name='channel_type']"); - let update_template = () => { - let codec = properties.channel_codec; - if(!codec && channel) - codec = channel.properties.channel_codec; - if(!codec) return; + let _in_update = false; + const update_simple_type = () => { + if(_in_update) + return; - let quality = properties.channel_codec_quality; - if(!quality && channel) - quality = channel.properties.channel_codec_quality; - if(!quality) return; - - let template_name = "custom"; - - { - if(codec == 4 && quality == 4) - template_name = "voice_mobile"; - else if(codec == 4 && quality == 6) - template_name = "voice_desktop"; - else if(codec == 5 && quality == 6) - template_name = "music"; - } - tag.find("input[name='voice_template'][value='" + template_name + "']").prop("checked", true); - simple.find("option[name='voice_template'][value='" + template_name + "']").prop("selected", true); - - let bandwidth; - if(codec < 0 || codec > bandwidth_mapping.length) - bandwidth = 0; + let type; + if(properties.channel_flag_default || (typeof(properties.channel_flag_default) === "undefined" && channel && channel.properties.channel_flag_default)) + type = "def"; + else if(properties.channel_flag_permanent || (typeof(properties.channel_flag_permanent) === "undefined" && channel && channel.properties.channel_flag_permanent)) + type = "perm"; + else if(properties.channel_flag_semi_permanent || (typeof(properties.channel_flag_semi_permanent) === "undefined" && channel && channel.properties.channel_flag_semi_permanent)) + type = "semi"; else - bandwidth = bandwidth_mapping[codec][quality] || 0; /* OOB access results in undefined, but is allowed */ - tag.find(".container-needed-bandwidth").text(bandwidth.toFixed(2) + " KiB/s"); + type = "temp"; + + simple.find("option[name='channel-type'][value='" + type + "']").prop("selected", true); }; - let change_codec = codec => { - if(properties.channel_codec == codec) return; - - tag.find(".voice_codec option").prop("selected", false).eq(codec).prop("selected", true); - properties.channel_codec = codec; - update_template(); - }; - - const container_quality = tag.find(".container-quality"); - const slider_quality = sliderfy(container_quality.find(".container-slider"), { - initial_value: properties.channel_codec_quality || 6, - unit: "", - min_value: 1, - max_value: 10, - step: 1, - value_field: container_quality.find(".container-value") - }); - - let change_quality = (quality: number) => { - if(properties.channel_codec_quality == quality) return; - - properties.channel_codec_quality = quality; - slider_quality.value(quality); - update_template(); - }; - - container_quality.find(".container-slider").on('change', event => { - properties.channel_codec_quality = slider_quality.value(); - update_template(); - }); - - tag.find("input[name='voice_template']").change(function (this: HTMLInputElement) { - switch(this.value) { - case "custom": + input_advanced_type.on('change', event => { + const value = [...input_advanced_type as JQuery].find(e => e.checked).value; + switch(value) { + case "semi": + properties.channel_flag_permanent = false; + properties.channel_flag_semi_permanent = true; break; - case "music": - change_codec(5); - change_quality(6); + case "perm": + properties.channel_flag_permanent = true; + properties.channel_flag_semi_permanent = false; break; - case "voice_desktop": - change_codec(4); - change_quality(6); - break; - case "voice_mobile": - change_codec(4); - change_quality(4); + default: + properties.channel_flag_permanent = false; + properties.channel_flag_semi_permanent = false; break; } + update_simple_type(); }); - simple.find("select[name='voice_template']").change(function (this: HTMLInputElement) { - switch(this.value) { - case "custom": - break; - case "music": - change_codec(5); - change_quality(6); - break; - case "voice_desktop": - change_codec(4); - change_quality(6); - break; - case "voice_mobile": - change_codec(4); - change_quality(4); - break; - } - }); + const permission_temp = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_TEMPORARY : PermissionType.B_CHANNEL_MODIFY_MAKE_TEMPORARY).granted(1); + const permission_semi = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_SEMI_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_SEMI_PERMANENT).granted(1); + const permission_perm = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1); + const permission_default = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1) && + connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_DEFAULT : PermissionType.B_CHANNEL_MODIFY_MAKE_DEFAULT).granted(1); - /* disable not granted templates */ - { - tag.find("input[name='voice_template'][value='voice_mobile']") - .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1)); - simple.find("option[name='voice_template'][value='voice_mobile']") - .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1)); - - tag.find("input[name='voice_template'][value=\"voice_desktop\"]") - .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1)); - simple.find("option[name='voice_template'][value=\"voice_desktop\"]") - .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1)); - - tag.find("input[name='voice_template'][value=\"music\"]") - .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1)); - simple.find("option[name='voice_template'][value=\"music\"]") - .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1)); - } - - let codecs = tag.find(".voice_codec option"); - codecs.eq(0).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX8).granted(1)); - codecs.eq(1).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX16).granted(1)); - codecs.eq(2).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX32).granted(1)); - codecs.eq(3).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_CELTMONO48).granted(1)); - codecs.eq(4).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1)); - codecs.eq(5).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1)); - tag.find(".voice_codec").change(function (this: HTMLSelectElement) { - if($(this.item(this.selectedIndex)).prop("disabled")) return false; - - change_codec(this.selectedIndex); - }); - - if(!channel) { - change_codec(4); - change_quality(6); - } else { - change_codec(channel.properties.channel_codec); - change_quality(channel.properties.channel_codec_quality); - } - update_template(); - } - - function applyAdvancedListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, channel?: ChannelEntry) { - tag.find(".channel_name_phonetic").change(function (this: HTMLInputElement) { - properties.channel_topic = this.value; - }); + /* advanced type listeners */ + const container_types = tag.find(".container-channel-type"); + const tag_type_temp = container_types.find(".type-temp"); + const tag_type_semi = container_types.find(".type-semi"); + const tag_type_perm = container_types.find(".type-perm"); + const select_default = tag.find(".input-flag-default"); { - const permission = connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_TEMP_DELETE_DELAY).granted(1); - tag.find(".channel_delete_delay").change(function (this: HTMLInputElement) { - properties.channel_delete_delay = parseInt(this.value); - }).prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission); + select_default.on('change', event => { + const node = select_default[0] as HTMLInputElement; + properties.channel_flag_default = node.checked; + + if(node.checked) + tag_type_perm.find("input").prop("checked", true); + + tag_type_temp + .toggleClass("disabled", node.checked || !permission_temp) + .find("input").prop("disabled", node.checked || !permission_temp); + + tag_type_semi + .toggleClass("disabled", node.checked || !permission_semi) + .find("input").prop("disabled", node.checked || !permission_semi); + + tag_type_perm + .toggleClass("disabled", node.checked || !permission_perm) + .find("input").prop("disabled", node.checked || !permission_perm); + + update_simple_type(); + }).prop("disabled", !permission_default).trigger('change').parent().toggleClass("disabled", !permission_default); } + /* simple */ { - tag.find(".button-delete-max").on('click', event => { - const power = connection.permissions.neededPermission(PermissionType.I_CHANNEL_CREATE_MODIFY_WITH_TEMP_DELETE_DELAY).value; - let value = power == -2 ? 0 : power == -1 ? (7 * 24 * 60 * 60) : power; - tag.find(".channel_delete_delay").val(value).trigger('change'); + simple.find("option[name='channel-type'][value='def']").prop("disabled", !permission_default); + simple.find("option[name='channel-type'][value='perm']").prop("disabled", !permission_perm); + simple.find("option[name='channel-type'][value='semi']").prop("disabled", !permission_semi); + simple.find("option[name='channel-type'][value='temp']").prop("disabled", !permission_temp); + + simple.find("select[name='channel-type']").on('change', event => { + try { + _in_update = true; + switch ((event.target as HTMLSelectElement).value) { + case "temp": + properties.channel_flag_permanent = false; + properties.channel_flag_semi_permanent = false; + properties.channel_flag_default = false; + select_default.prop("checked", false).trigger('change'); + tag_type_temp.trigger('click'); + break; + case "semi": + properties.channel_flag_permanent = false; + properties.channel_flag_semi_permanent = true; + properties.channel_flag_default = false; + select_default.prop("checked", false).trigger('change'); + tag_type_semi.trigger('click'); + break; + case "perm": + properties.channel_flag_permanent = true; + properties.channel_flag_semi_permanent = false; + properties.channel_flag_default = false; + select_default.prop("checked", false).trigger('change'); + tag_type_perm.trigger('click'); + break; + case "def": + properties.channel_flag_permanent = true; + properties.channel_flag_semi_permanent = false; + properties.channel_flag_default = true; + select_default.prop("checked", true).trigger('change'); + break; + } + } finally { + _in_update = false; + /* We dont need to update the simple type because we changed the advanced part to the just changed simple part */ + //update_simple_type(); + } }); } - { - const permission = connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_MAKE_CODEC_ENCRYPTED).granted(1); - tag.find(".channel_codec_is_unencrypted").change(function (this: HTMLInputElement) { - properties.channel_codec_is_unencrypted = parseInt(this.value) == 0; - }).prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission); + /* init */ + setTimeout(() => { + if(!channel) { + if(permission_perm) + tag_type_perm.find("input").trigger('click'); + else if(permission_semi) + tag_type_semi.find("input").trigger('click'); + else + tag_type_temp.find("input").trigger('click'); + } else { + if(channel.properties.channel_flag_permanent) + tag_type_perm.find("input").trigger('click'); + else if(channel.properties.channel_flag_semi_permanent) + tag_type_semi.find("input").trigger('click'); + else + tag_type_temp.find("input").trigger('click'); + } + }, 0); + } + + /* Talk power */ + { + const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_NEEDED_TALK_POWER : PermissionType.B_CHANNEL_MODIFY_NEEDED_TALK_POWER).granted(1); + const input_advanced = tag.find("input[name='talk_power']").prop("disabled", !permission); + const input_simple = simple.find("input[name='talk_power']").prop("disabled", !permission); + + input_advanced.on('change', event => { + properties.channel_needed_talk_power = parseInt(input_advanced.val() as string); + input_simple.val(input_advanced.val()); + }); + + input_simple.on('change', event => { + properties.channel_needed_talk_power = parseInt(input_simple.val() as string); + input_advanced.val(input_simple.val()); + }); + } + + /* Channel order */ + { + const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_SORTORDER : PermissionType.B_CHANNEL_MODIFY_SORTORDER).granted(1); + + const advanced_order_id = tag.find(".order_id").prop("disabled", !permission) as JQuery; + const simple_order_id = simple.find(".order_id").prop("disabled", !permission) as JQuery; + + for(let previous_channel of (parent ? parent.children() : connection.channelTree.rootChannel())) { + let selected = channel && channel.properties.channel_order == previous_channel.channelId; + $.spawn("option").attr("channelId", previous_channel.channelId.toString()).prop("selected", selected).text(previous_channel.channelName()).appendTo(advanced_order_id); + $.spawn("option").attr("channelId", previous_channel.channelId.toString()).prop("selected", selected).text(previous_channel.channelName()).appendTo(simple_order_id); } + + advanced_order_id.on('change', event => { + simple_order_id[0].selectedIndex = advanced_order_id[0].selectedIndex; + const selected = $(advanced_order_id[0].options.item(advanced_order_id[0].selectedIndex)); + properties.channel_order = parseInt(selected.attr("channelId")); + }); + + simple_order_id.on('change', event => { + advanced_order_id[0].selectedIndex = simple_order_id[0].selectedIndex; + const selected = $(simple_order_id[0].options.item(simple_order_id[0].selectedIndex)); + properties.channel_order = parseInt(selected.attr("channelId")); + }); + } + + + /* Advanced only */ + { + const container_max_users = tag.find(".container-max-users"); + + const container_unlimited = container_max_users.find(".container-unlimited"); + const container_limited = container_max_users.find(".container-limited"); + + const input_unlimited = container_unlimited.find("input[value='unlimited']"); + const input_limited = container_limited.find("input[value='limited']"); + const input_limit = container_limited.find(".channel_maxclients"); + + const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1); + + if(!permission) { + input_unlimited.prop("disabled", true); + input_limited.prop("disabled", true); + input_limit.prop("disabled", true); + + container_limited.addClass("disabled"); + container_unlimited.addClass("disabled"); + } else { + container_max_users.find("input[name='max_users']").on('change', event => { + const node = event.target as HTMLInputElement; + console.log(tr("Channel max user mode: %o"), node.value); + + const flag = node.value === "unlimited"; + input_limit + .prop("disabled", flag) + .parent().toggleClass("disabled", flag); + properties.channel_flag_maxclients_unlimited = flag; + }); + + input_limit.on('change', event => { + properties.channel_maxclients = parseInt(input_limit.val() as string); + console.log(tr("Changed max user limit to %o"), properties.channel_maxclients); + }); + + setTimeout(() => container_max_users.find("input:checked").trigger('change'), 100); + } + } + + { + const container_max_users = tag.find(".container-max-family-users"); + + const container_unlimited = container_max_users.find(".container-unlimited"); + const container_inherited = container_max_users.find(".container-inherited"); + const container_limited = container_max_users.find(".container-limited"); + + const input_unlimited = container_unlimited.find("input[value='unlimited']"); + const input_inherited = container_inherited.find("input[value='inherited']"); + const input_limited = container_limited.find("input[value='limited']"); + const input_limit = container_limited.find(".channel_maxfamilyclients"); + + const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1); + + if(!permission) { + input_unlimited.prop("disabled", true); + input_inherited.prop("disabled", true); + input_limited.prop("disabled", true); + input_limit.prop("disabled", true); + + container_limited.addClass("disabled"); + container_unlimited.addClass("disabled"); + container_inherited.addClass("disabled"); + } else { + container_max_users.find("input[name='max_family_users']").on('change', event => { + const node = event.target as HTMLInputElement; + console.log(tr("Channel max family user mode: %o"), node.value); + + const flag_unlimited = node.value === "unlimited"; + const flag_inherited = node.value === "inherited"; + input_limit + .prop("disabled", flag_unlimited || flag_inherited) + .parent().toggleClass("disabled", flag_unlimited || flag_inherited); + properties.channel_flag_maxfamilyclients_unlimited = flag_unlimited; + properties.channel_flag_maxfamilyclients_inherited = flag_inherited; + }); + + input_limit.on('change', event => { + properties.channel_maxfamilyclients = parseInt(input_limit.val() as string); + console.log(tr("Changed max family user limit to %o"), properties.channel_maxfamilyclients); + }); + + setTimeout(() => container_max_users.find("input:checked").trigger('change'), 100); + } + } +} + +function applyPermissionListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, permissions: PermissionManager, channel?: ChannelEntry) { + let apply_permissions = (channel_permissions: PermissionValue[]) => { + log.trace(LogCategory.CHANNEL, tr("Received channel permissions: %o"), channel_permissions); + + let required_power = -2; + for(let cperm of channel_permissions) + if(cperm.type.name == PermissionType.I_CHANNEL_NEEDED_MODIFY_POWER) { + required_power = cperm.value; + break; + } + + tag.find("input[permission]").each((index, _element) => { + let element = $(_element); + element.attr("original-value", 0); + element.val(0); + + let permission = permissions.resolveInfo(element.attr("permission")); + if(!permission) { + log.error(LogCategory.PERMISSIONS, tr("Failed to resolve channel permission for name %o"), element.attr("permission")); + element.prop("disabled", true); + return; + } + + for(let cperm of channel_permissions) + if(cperm.type == permission) { + element.val(cperm.value); + element.attr("original-value", cperm.value); + return; + } + }); + + const permission = permissions.neededPermission(PermissionType.I_CHANNEL_PERMISSION_MODIFY_POWER).granted(required_power, false); + tag.find("input[permission]").prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission); //No permissions + }; + + if(channel) { + permissions.requestChannelPermissions(channel.getChannelId()).then(apply_permissions).catch((error) => { + tag.find("input[permission]").prop("disabled", true); + console.log("Failed to receive channel permissions (%o)", error); + }); + } else apply_permissions([]); +} + +function applyAudioListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, simple: JQuery, channel?: ChannelEntry) { + const bandwidth_mapping = [ + /* SPEEX narrow */ [2.49, 2.69, 2.93, 3.17, 3.17, 3.56, 3.56, 4.05, 4.05, 4.44, 5.22], + /* SPEEX wide */ [2.69, 2.93, 3.17, 3.42, 3.76, 4.25, 4.74, 5.13, 5.62, 6.40, 7.37], + /* SPEEX ultra */ [2.73, 3.12, 3.37, 3.61, 4.00, 4.49, 4.93, 5.32, 5.81, 6.59, 7.57], + /* CELT */ [6.10, 6.10, 7.08, 7.08, 7.08, 8.06, 8.06, 8.06, 8.06, 10.01, 13.92], + + /* Opus Voice */ [2.73, 3.22, 3.71, 4.20, 4.74, 5.22, 5.71, 6.20, 6.74, 7.23, 7.71], + /* Opus Music */ [3.08, 3.96, 4.83, 5.71, 6.59, 7.47, 8.35, 9.23, 10.11, 10.99, 11.87] + ]; + + let update_template = () => { + let codec = properties.channel_codec; + if(!codec && channel) + codec = channel.properties.channel_codec; + if(!codec) return; + + let quality = properties.channel_codec_quality; + if(!quality && channel) + quality = channel.properties.channel_codec_quality; + if(!quality) return; + + let template_name = "custom"; + + { + if(codec == 4 && quality == 4) + template_name = "voice_mobile"; + else if(codec == 4 && quality == 6) + template_name = "voice_desktop"; + else if(codec == 5 && quality == 6) + template_name = "music"; + } + tag.find("input[name='voice_template'][value='" + template_name + "']").prop("checked", true); + simple.find("option[name='voice_template'][value='" + template_name + "']").prop("selected", true); + + let bandwidth; + if(codec < 0 || codec > bandwidth_mapping.length) + bandwidth = 0; + else + bandwidth = bandwidth_mapping[codec][quality] || 0; /* OOB access results in undefined, but is allowed */ + tag.find(".container-needed-bandwidth").text(bandwidth.toFixed(2) + " KiB/s"); + }; + + let change_codec = codec => { + if(properties.channel_codec == codec) return; + + tag.find(".voice_codec option").prop("selected", false).eq(codec).prop("selected", true); + properties.channel_codec = codec; + update_template(); + }; + + const container_quality = tag.find(".container-quality"); + const slider_quality = sliderfy(container_quality.find(".container-slider"), { + initial_value: properties.channel_codec_quality || 6, + unit: "", + min_value: 1, + max_value: 10, + step: 1, + value_field: container_quality.find(".container-value") + }); + + let change_quality = (quality: number) => { + if(properties.channel_codec_quality == quality) return; + + properties.channel_codec_quality = quality; + slider_quality.value(quality); + update_template(); + }; + + container_quality.find(".container-slider").on('change', event => { + properties.channel_codec_quality = slider_quality.value(); + update_template(); + }); + + tag.find("input[name='voice_template']").change(function (this: HTMLInputElement) { + switch(this.value) { + case "custom": + break; + case "music": + change_codec(5); + change_quality(6); + break; + case "voice_desktop": + change_codec(4); + change_quality(6); + break; + case "voice_mobile": + change_codec(4); + change_quality(4); + break; + } + }); + + simple.find("select[name='voice_template']").change(function (this: HTMLInputElement) { + switch(this.value) { + case "custom": + break; + case "music": + change_codec(5); + change_quality(6); + break; + case "voice_desktop": + change_codec(4); + change_quality(6); + break; + case "voice_mobile": + change_codec(4); + change_quality(4); + break; + } + }); + + /* disable not granted templates */ + { + tag.find("input[name='voice_template'][value='voice_mobile']") + .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1)); + simple.find("option[name='voice_template'][value='voice_mobile']") + .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1)); + + tag.find("input[name='voice_template'][value=\"voice_desktop\"]") + .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1)); + simple.find("option[name='voice_template'][value=\"voice_desktop\"]") + .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1)); + + tag.find("input[name='voice_template'][value=\"music\"]") + .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1)); + simple.find("option[name='voice_template'][value=\"music\"]") + .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1)); + } + + let codecs = tag.find(".voice_codec option"); + codecs.eq(0).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX8).granted(1)); + codecs.eq(1).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX16).granted(1)); + codecs.eq(2).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX32).granted(1)); + codecs.eq(3).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_CELTMONO48).granted(1)); + codecs.eq(4).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1)); + codecs.eq(5).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1)); + tag.find(".voice_codec").change(function (this: HTMLSelectElement) { + if($(this.item(this.selectedIndex)).prop("disabled")) return false; + + change_codec(this.selectedIndex); + }); + + if(!channel) { + change_codec(4); + change_quality(6); + } else { + change_codec(channel.properties.channel_codec); + change_quality(channel.properties.channel_codec_quality); + } + update_template(); +} + +function applyAdvancedListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, channel?: ChannelEntry) { + tag.find(".channel_name_phonetic").change(function (this: HTMLInputElement) { + properties.channel_topic = this.value; + }); + + { + const permission = connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_TEMP_DELETE_DELAY).granted(1); + tag.find(".channel_delete_delay").change(function (this: HTMLInputElement) { + properties.channel_delete_delay = parseInt(this.value); + }).prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission); + } + + { + tag.find(".button-delete-max").on('click', event => { + const power = connection.permissions.neededPermission(PermissionType.I_CHANNEL_CREATE_MODIFY_WITH_TEMP_DELETE_DELAY).value; + let value = power == -2 ? 0 : power == -1 ? (7 * 24 * 60 * 60) : power; + tag.find(".channel_delete_delay").val(value).trigger('change'); + }); + } + + { + const permission = connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_MAKE_CODEC_ENCRYPTED).granted(1); + tag.find(".channel_codec_is_unencrypted").change(function (this: HTMLInputElement) { + properties.channel_codec_is_unencrypted = parseInt(this.value) == 0; + }).prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission); } } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalGroupAssignment.ts b/shared/js/ui/modal/ModalGroupAssignment.ts index 52deb24d..4efff2c9 100644 --- a/shared/js/ui/modal/ModalGroupAssignment.ts +++ b/shared/js/ui/modal/ModalGroupAssignment.ts @@ -1,81 +1,85 @@ -namespace Modals { - let current_modal: Modal; - export function createServerGroupAssignmentModal(client: ClientEntry, callback: (groups: number[], flag: boolean) => Promise) { - if(current_modal) - current_modal.close(); +import {LogCategory} from "tc-shared/log"; +import {createModal, Modal} from "tc-shared/ui/elements/Modal"; +import * as log from "tc-shared/log"; +import {ClientEntry} from "tc-shared/ui/client"; +import {GroupManager, GroupType} from "tc-shared/permission/GroupManager"; +import PermissionType from "tc-shared/permission/PermissionType"; - current_modal = createModal({ - header: tr("Server Groups"), - body: () => { - let tag: any = {}; - let groups = tag["groups"] = []; +let current_modal: Modal; +export function createServerGroupAssignmentModal(client: ClientEntry, callback: (groups: number[], flag: boolean) => Promise) { + if(current_modal) + current_modal.close(); - tag["client"] = client.createChatTag(); + current_modal = createModal({ + header: tr("Server Groups"), + body: () => { + let tag: any = {}; + let groups = tag["groups"] = []; - const _groups = client.channelTree.client.groups.serverGroups.sort(GroupManager.sorter()); + 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] = client.channelTree.client.fileManager.icons.generateTag(group.properties.iconid); + groups.push(entry); + } + + let template = $("#tmpl_server_group_assignment").renderTag(tag); + + const update_groups = () => { 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] = client.channelTree.client.fileManager.icons.generateTag(group.properties.iconid); - groups.push(entry); + template.find("input[group-id='" + group.id + "']").prop("checked", client.groupAssigned(group)); } + }; - let template = $("#tmpl_server_group_assignment").renderTag(tag); + template.find(".group-entry input").each((_idx, _entry) => { + let entry = $(_entry); - const update_groups = () => { - for(let group of _groups) { - template.find("input[group-id='" + group.id + "']").prop("checked", client.groupAssigned(group)); + entry.on('change', event => { + let group_id = parseInt(entry.attr("group-id")); + let group = client.channelTree.client.groups.serverGroup(group_id); + if(!group) { + console.warn(tr("Could not resolve target group!")); + return false; } - }; + + let target = entry.prop("checked"); + callback([group.id], target).catch(e => { log.warn(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; - entry.on('change', event => { - let group_id = parseInt(entry.attr("group-id")); - let group = client.channelTree.client.groups.serverGroup(group_id); - if(!group) { - console.warn(tr("Could not resolve target group!")); - return false; - } - - let target = entry.prop("checked"); - callback([group.id], target).catch(e => { log.warn(LogCategory.GENERAL, tr("Failed to change group assignment: %o"), e)}).then(update_groups); - }); + group_ids.push(parseInt(entry.attr("group-id"))); }); - template.find(".button-close").on('click', () => current_modal.close()); - template.find(".button-remove-all").on('click', () => { - const group_ids = []; + callback(group_ids, false).catch(e => { log.warn(LogCategory.GENERAL, tr("Failed to remove all group assignments: %o"), e)}).then(update_groups); - 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"))); - }); + update_groups(); + return template; + }, + footer: null, + min_width: "10em" - callback(group_ids, false).catch(e => { log.warn(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(); - } + }); + current_modal.htmlTag.find(".modal-body").addClass("modal-server-group-assignments"); + current_modal.close_listener.push(() => current_modal = undefined); + current_modal.open(); } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalIconSelect.ts b/shared/js/ui/modal/ModalIconSelect.ts index 42911e61..78cee825 100644 --- a/shared/js/ui/modal/ModalIconSelect.ts +++ b/shared/js/ui/modal/ModalIconSelect.ts @@ -1,539 +1,518 @@ -/// -/// -/// +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import PermissionType from "tc-shared/permission/PermissionType"; +import {createErrorModal, createModal} from "tc-shared/ui/elements/Modal"; +import {FileEntry, spawn_upload_transfer, UploadKey} from "tc-shared/FileManager"; +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; +import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; +import {tra} from "tc-shared/i18n/localize"; +import {arrayBufferBase64} from "tc-shared/utils/buffers"; +import {Settings, settings} from "tc-shared/settings"; +import * as crc32 from "tc-shared/crypto/crc32"; -namespace Modals { - export function spawnIconSelect(client: ConnectionHandler, callback_icon?: (id: number) => any, selected_icon?: number) { - selected_icon = selected_icon || 0; - let allow_manage = client.permissions.neededPermission(PermissionType.B_ICON_MANAGE).granted(1); +export function spawnIconSelect(client: ConnectionHandler, callback_icon?: (id: number) => any, selected_icon?: number) { + selected_icon = selected_icon || 0; + let allow_manage = client.permissions.neededPermission(PermissionType.B_ICON_MANAGE).granted(1); - const modal = createModal({ - header: tr("Icons"), - footer: undefined, - body: () => { - return $("#tmpl_icon_select").renderTag({ - enable_select: !!callback_icon, + const modal = createModal({ + header: tr("Icons"), + footer: undefined, + body: () => { + return $("#tmpl_icon_select").renderTag({ + enable_select: !!callback_icon, - enable_upload: allow_manage, - enable_delete: allow_manage + enable_upload: allow_manage, + enable_delete: allow_manage + }); + }, + + min_width: "20em" + }); + + modal.htmlTag.find(".modal-body").addClass("modal-icon-select"); + + const button_select = modal.htmlTag.find(".button-select"); + const button_delete = modal.htmlTag.find(".button-delete").prop("disabled", true); + const button_upload = modal.htmlTag.find(".button-upload").prop("disabled", !allow_manage); + + const container_loading = modal.htmlTag.find(".container-loading").hide(); + const container_no_permissions = modal.htmlTag.find(".container-no-permissions").hide(); + const container_error = modal.htmlTag.find(".container-error").hide(); + + const selected_container = modal.htmlTag.find(".selected-item-container"); + + const container_icons = modal.htmlTag.find(".container-icons"); + const container_icons_remote = container_icons.find(".container-icons-remote"); + const container_icons_local = container_icons.find(".container-icons-local"); + + const update_local_icons = (icons: number[]) => { + container_icons_local.empty(); + + for(const icon_id of icons) { + const tag = client.fileManager.icons.generateTag(icon_id, {animate: false}).attr('title', "Icon " + icon_id); + if(callback_icon) { + tag.on('click', event => { + container_icons.find(".selected").removeClass("selected"); + tag.addClass("selected"); + + selected_container.empty().append(tag.clone()); + selected_icon = icon_id; + button_select.prop("disabled", false); }); - }, - - min_width: "20em" - }); - - modal.htmlTag.find(".modal-body").addClass("modal-icon-select"); - - const button_select = modal.htmlTag.find(".button-select"); - const button_delete = modal.htmlTag.find(".button-delete").prop("disabled", true); - const button_upload = modal.htmlTag.find(".button-upload").prop("disabled", !allow_manage); - - const container_loading = modal.htmlTag.find(".container-loading").hide(); - const container_no_permissions = modal.htmlTag.find(".container-no-permissions").hide(); - const container_error = modal.htmlTag.find(".container-error").hide(); - - const selected_container = modal.htmlTag.find(".selected-item-container"); - - const container_icons = modal.htmlTag.find(".container-icons"); - const container_icons_remote = container_icons.find(".container-icons-remote"); - const container_icons_local = container_icons.find(".container-icons-local"); - - const update_local_icons = (icons: number[]) => { - container_icons_local.empty(); - - for(const icon_id of icons) { - const tag = client.fileManager.icons.generateTag(icon_id, {animate: false}).attr('title', "Icon " + icon_id); - if(callback_icon) { - tag.on('click', event => { - container_icons.find(".selected").removeClass("selected"); - tag.addClass("selected"); - - selected_container.empty().append(tag.clone()); - selected_icon = icon_id; - button_select.prop("disabled", false); - }); - tag.on('dblclick', event => { - callback_icon(icon_id); - modal.close(); - }); - if(icon_id == selected_icon) - tag.trigger('click'); - } - tag.appendTo(container_icons_local); + tag.on('dblclick', event => { + callback_icon(icon_id); + modal.close(); + }); + if(icon_id == selected_icon) + tag.trigger('click'); } - }; - - const update_remote_icons = () => { - container_no_permissions.hide(); - container_error.hide(); - container_loading.show(); - const display_remote_error = (error?: string) => { - if(typeof(error) === "string") { - container_error.find(".error-message").text(error); - container_error.show(); - } else { - container_error.hide(); - } - }; - - client.fileManager.requestFileList("/icons").then(icons => { - const container_icons_remote_parent = container_icons_remote.parent(); - container_icons_remote.detach().empty(); - - const chunk_size = 50; - const icon_chunks: FileEntry[][] = []; - let index = 0; - while(icons.length > index) { - icon_chunks.push(icons.slice(index, index + chunk_size)); - index += chunk_size; - } - - const process_next_chunk = () => { - const chunk = icon_chunks.pop_front(); - if(!chunk) return; - - for(const icon of chunk) { - const icon_id = parseInt(icon.name.substr("icon_".length)); - if(Number.isNaN(icon_id)) { - log.warn(LogCategory.GENERAL, tr("Received an unparsable icon within icon list (%o)"), icon); - continue; - } - const tag = client.fileManager.icons.generateTag(icon_id, {animate: false}).attr('title', "Icon " + icon_id); - if(callback_icon || allow_manage) { - tag.on('click', event => { - container_icons.find(".selected").removeClass("selected"); - tag.addClass("selected"); - - selected_container.empty().append(tag.clone()); - selected_icon = icon_id; - button_select.prop("disabled", false); - button_delete.prop("disabled", !allow_manage); - }); - tag.on('dblclick', event => { - if(!callback_icon) - return; - - callback_icon(icon_id); - modal.close(); - }); - if(icon_id == selected_icon) - tag.trigger('click'); - } - tag.appendTo(container_icons_remote); - } - setTimeout(process_next_chunk, 100); - }; - process_next_chunk(); - - container_icons_remote_parent.append(container_icons_remote); - container_error.hide(); - container_loading.hide(); - container_no_permissions.hide(); - }).catch(error => { - if(error instanceof CommandResult && error.id == ErrorID.PERMISSION_ERROR) { - container_no_permissions.show(); - } else { - log.error(LogCategory.GENERAL, tr("Failed to fetch icon list. Error: %o"), error); - display_remote_error(tr("Failed to fetch icon list")); - } - container_loading.hide(); - }); - }; - - button_delete.on('click', event => { - if(!selected_icon) - return; - - const selected = modal.htmlTag.find(".selected"); - if(selected.length != 1) - console.warn(tr("UI selected icon length does not equal with 1! (%o)"), selected.length); - - if(selected_icon < 1000) return; /* we cant delete local icons */ - - client.fileManager.icons.delete_icon(selected_icon).then(() => { - selected.detach(); - }).catch(error => { - if(error instanceof CommandResult && error.id == ErrorID.PERMISSION_ERROR) - return; - console.warn(tr("Failed to delete icon %d: %o"), selected_icon, error); - - error = error instanceof CommandResult ? error.extra_message || error.message : error; - - createErrorModal(tr("Failed to delete icon"), tra("Failed to delete icon.
Error: ", error)).open(); - }); - }); - - button_upload.on('click', event => spawnIconUpload(client)); - - update_local_icons([100, 200, 300, 500, 600]); - update_remote_icons(); - modal.htmlTag.find('.button-reload').on('click', () => update_remote_icons()); - button_select.prop("disabled", true).on('click', () => { - if(callback_icon) callback_icon(selected_icon); - modal.close(); - }); - modal.htmlTag.find(".button-select-no-icon").on('click', () => { - if(callback_icon) callback_icon(0); - modal.close(); - }); - modal.open(); - } - - interface UploadingIcon { - file: File; - state: "loading" | "valid" | "error"; - upload_state: "unset" | "uploading" | "uploaded" | "error"; - - html_tag?: JQuery; - image_element?: () => HTMLImageElement; - - loader: Promise; - - upload_icon: () => () => Promise; - upload_html_tag?: JQuery; - - icon_id: string; - } - - function handle_icon_upload(file: File, client: ConnectionHandler) : UploadingIcon { - const icon = {} as UploadingIcon; - icon.file = file; - icon.upload_state = "unset"; - - const file_too_big = () => { - console.error(tr("Failed to load file %s: File is too big!"), file.name); - createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.
The given file is too big!", file.name)).open(); - icon.state = "error"; - }; - if(file.size > 1024 * 1024 * 512) { - file_too_big(); - } else if((file.size | 0) <= 0) { - console.error(tr("Failed to load file %s: Your browser does not support file sizes!"), file.name); - createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.
Your browser does not support file sizes!", file.name)).open(); - icon.state = "error"; - return; - } else { - icon.state = "loading"; - icon.loader = (async () => { - const reader = new FileReader(); - - try { - await new Promise((resolve, reject) => { - reader.onload = resolve; - reader.onerror = reject; - reader.readAsDataURL(file); - }); - } catch(error) { - console.log("Image failed to load (%o)", error); - console.error(tr("Failed to load file %s: Image failed to load"), file.name); - createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.
Failed to load image", file.name)).open(); - icon.state = "error"; - return; - } - - const result = reader.result as string; - if(typeof(result) !== "string") { - console.error(tr("Failed to load file %s: Result is not an media string (%o)"), file.name, result); - createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.
Result is not an media string", file.name)).open(); - icon.state = "error"; - return; - } - - - /* get the CRC32 sum */ - { - if(!result.startsWith("data:image/")) { - console.error(tr("Failed to load file %s: Invalid data media type (%o)"), file.name, result); - createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.
File is not an image", file.name)).open(); - icon.state = "error"; - return; - } - const semi = result.indexOf(';'); - const type = result.substring(11, semi); - console.log(tr("Given image has type %s"), type); - if(!result.substr(semi + 1).startsWith("base64,")) { - console.error(tr("Failed to load file %s: Mimetype isnt base64 encoded (%o)"), file.name, result.substr(semi + 1)); - createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.
Decoder returned unknown result", file.name)).open(); - icon.state = "error"; - return; - } - - const crc = new Crc32(); - crc.update(arrayBufferBase64(result.substr(semi + 8))); - icon.icon_id = crc.digest(10); - } - - - const image = document.createElement("img"); - try { - await new Promise((resolve, reject) => { - image.onload = resolve; - image.onerror = reject; - image.src = result; - }); - } catch(error) { - console.log("Image failed to load (%o)", error); - console.error(tr("Failed to load file %s: Image failed to load"), file.name); - createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.{:br:}Failed to load image", file.name)).open(); - icon.state = "error"; - } - - const width_error = message => { - console.error(tr("Failed to load file %s: Invalid bounds: %s"), file.name, message); - createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.{:br:}Image is too large ({})", file.name, message)).open(); - icon.state = "error"; - }; - - if(!result.startsWith("data:image/svg+xml")) { - if(image.naturalWidth > 32 && image.naturalHeight > 32) { - width_error("width and height (max 32px). Given: " + image.naturalWidth + "x" + image.naturalHeight); - return; - } - if(image.naturalWidth > 32) { - width_error("width (max 32px)"); - return; - } - if(image.naturalHeight > 32) { - width_error("height (max 32px)"); - return; - } - } - console.log("Image loaded (%dx%d) %s (%s)", image.naturalWidth, image.naturalHeight, image.name, icon.icon_id); - icon.image_element = () => { - const image = document.createElement("img"); - image.src = result; - return image; - }; - icon.state = "valid"; - })(); - - icon.upload_icon = () => { - const create_progress_bar = () => { - const html = $.spawn("div").addClass("progress"); - const indicator = $.spawn("div").addClass("progress-bar bg-success progress-bar-striped progress-bar-animated"); - const message = $.spawn("div").addClass("progress-message"); - const set_value = value => { - indicator.stop(true, false).animate({width: value + "%"}, 250); - if(value === 100) - setTimeout(() => indicator.removeClass("progress-bar-striped progress-bar-animated"), 900) - }; - - return { - html_tag: html.append(indicator).append(message), - set_value: set_value, - set_message: msg => message.text(msg), - set_error: (msg: string) => { - let index = msg.lastIndexOf(':'); - message.text(index == -1 ? msg : msg.substring(index + 1)); - message.attr('title', msg); - set_value(100); - indicator.removeClass("bg-success").addClass("bg-danger"); - } - } - }; - - const container_image = $.spawn("div").addClass("container-icon"); - const bar = create_progress_bar(); - - const set_error = message => { - bar.set_value(100); - bar.set_message(tr("error: ") + message); - }; - - const html_tag = $.spawn("div") - .addClass("upload-entry") - .append(container_image) - .append(bar.html_tag); - - icon.upload_html_tag = html_tag; - - let icon_added = false; - if(icon.image_element) { - container_image.append(icon.image_element()); - icon_added = true; - } - - - bar.set_value(0); - bar.set_value(tr("waiting")); - - return async () => { - const time_begin = Date.now(); - - if(icon.state === "loading") { - bar.set_message(tr("Awaiting local processing")); - await icon.loader; - // @ts-ignore Could happen because the loader function updates the state - if(icon.state !== "valid") { - set_error(tr("local processing failed")); - icon.upload_state = "error"; - return; - } - } else if(icon.state === "error") { - set_error(tr("local processing error")); - icon.upload_state = "error"; - return; - } - if(!icon_added) - container_image.append(icon.image_element()); - - bar.set_value(25); - bar.set_message(tr("initializing")); - - let upload_key: transfer.UploadKey; - try { - upload_key = await client.fileManager.upload_file({ - channel: undefined, - channel_password: undefined, - name: '/icon_' + icon.icon_id, - overwrite: false, - path: '', - size: icon.file.size - }) - } catch(error) { - if(error instanceof CommandResult && error.id == ErrorID.FILE_ALREADY_EXISTS) { - if(!settings.static_global(Settings.KEY_DISABLE_COSMETIC_SLOWDOWN, false)) - await new Promise(resolve => setTimeout(resolve, 500 + Math.floor(Math.random() * 500))); - bar.set_message(tr("icon already exists")); - bar.set_value(100); - icon.upload_state = "uploaded"; - return; - } - console.error(tr("Failed to initialize upload: %o"), error); - bar.set_error(tr("failed to initialize upload")); - icon.upload_state = "error"; - return; - } - bar.set_value(50); - bar.set_message(tr("uploading")); - - const connection = transfer.spawn_upload_transfer(upload_key); - try { - await connection.put_data(icon.file) - } catch(error) { - console.error(tr("Icon upload failed for icon %s: %o"), icon.file.name, error); - if(typeof(error) === "string") - bar.set_error(tr("upload failed: ") + error); - else if(typeof(error.message) === "string") - bar.set_error(tr("upload failed: ") + error.message); - else - bar.set_error(tr("upload failed")); - icon.upload_state = "error"; - return; - } - - const time_end = Date.now(); - if(!settings.static_global(Settings.KEY_DISABLE_COSMETIC_SLOWDOWN, false)) - await new Promise(resolve => setTimeout(resolve, Math.max(0, 1000 - (time_end - time_begin)))); - bar.set_value(100); - bar.set_message(tr("upload completed")); - icon.upload_state = "uploaded"; - }; - }; + tag.appendTo(container_icons_local); } + }; - return icon; - } - - export function spawnIconUpload(client: ConnectionHandler) { - const modal = createModal({ - header: tr("Upload Icons"), - footer: undefined, - body: () => $("#tmpl_icon_upload").renderTag(), - closeable: false, - - min_width: "20em" - }); - modal.htmlTag.find(".modal-body").addClass("modal-icon-upload"); - - const button_upload = modal.htmlTag.find(".button-upload"); - const button_delete = modal.htmlTag.find(".button-remove").prop("disabled", true); - const button_add = modal.htmlTag.find(".button-add"); - const button_upload_abort = modal.htmlTag.find(".button-upload-abort"); - const input_file = modal.htmlTag.find(".input-file-upload") as JQuery; - const container_icons = modal.htmlTag.find(".container-icons"); - - let selected_icon: UploadingIcon; - let icons: UploadingIcon[] = []; - - const update_upload_button = () => { - const icon_count = icons.filter(e => e.state === "valid").length; - button_upload.empty(); - tra("Upload icons ({})", icon_count).forEach(e => e.appendTo(button_upload)); - button_upload.prop("disabled", icon_count == 0); + const update_remote_icons = () => { + container_no_permissions.hide(); + container_error.hide(); + container_loading.show(); + const display_remote_error = (error?: string) => { + if(typeof(error) === "string") { + container_error.find(".error-message").text(error); + container_error.show(); + } else { + container_error.hide(); + } }; - update_upload_button(); - const add_icon = (icon: UploadingIcon) => { - icons.push(icon); - icon.loader.then(e => { - if(icon.state === "valid") { - const image = icon.image_element(); - const element = $.spawn("div") - .addClass("icon-container") - .append(image); - container_icons.append(icon.html_tag = element); + client.fileManager.requestFileList("/icons").then(icons => { + const container_icons_remote_parent = container_icons_remote.parent(); + container_icons_remote.detach().empty(); - element.on('click', event => { - container_icons.find(".selected").removeClass("selected"); - element.addClass("selected"); + const chunk_size = 50; + const icon_chunks: FileEntry[][] = []; + let index = 0; + while(icons.length > index) { + icon_chunks.push(icons.slice(index, index + chunk_size)); + index += chunk_size; + } - selected_icon = icon; - button_delete.prop("disabled", false); - }); + const process_next_chunk = () => { + const chunk = icon_chunks.pop_front(); + if(!chunk) return; - update_upload_button(); - } - }); - }; - button_delete.on('click', event => { - if(!selected_icon) - return; - icons = icons.filter(e => e !== selected_icon); - if(selected_icon.html_tag) - selected_icon.html_tag.detach(); - button_delete.prop("disabled", true); - update_upload_button(); - }); - - button_add.on('click', event => input_file.click()); - input_file.on('change', event => { - if(input_file[0].files.length > 0) { - for(let index = 0; index < input_file[0].files.length; index++) { - const file = input_file[0].files.item(index); - { - let duplicate = false; - - for(const icon of icons) - if(icon.file.name === file.name && icon.file.lastModified === file.lastModified && icon.state !== "error") { - duplicate = true; - break; - } - if(duplicate) - continue; + for(const icon of chunk) { + const icon_id = parseInt(icon.name.substr("icon_".length)); + if(Number.isNaN(icon_id)) { + log.warn(LogCategory.GENERAL, tr("Received an unparsable icon within icon list (%o)"), icon); + continue; } + const tag = client.fileManager.icons.generateTag(icon_id, {animate: false}).attr('title', "Icon " + icon_id); + if(callback_icon || allow_manage) { + tag.on('click', event => { + container_icons.find(".selected").removeClass("selected"); + tag.addClass("selected"); - add_icon(handle_icon_upload(file, client)); + selected_container.empty().append(tag.clone()); + selected_icon = icon_id; + button_select.prop("disabled", false); + button_delete.prop("disabled", !allow_manage); + }); + tag.on('dblclick', event => { + if(!callback_icon) + return; + + callback_icon(icon_id); + modal.close(); + }); + if(icon_id == selected_icon) + tag.trigger('click'); + } + tag.appendTo(container_icons_remote); + } + setTimeout(process_next_chunk, 100); + }; + process_next_chunk(); + + container_icons_remote_parent.append(container_icons_remote); + container_error.hide(); + container_loading.hide(); + container_no_permissions.hide(); + }).catch(error => { + if(error instanceof CommandResult && error.id == ErrorID.PERMISSION_ERROR) { + container_no_permissions.show(); + } else { + log.error(LogCategory.GENERAL, tr("Failed to fetch icon list. Error: %o"), error); + display_remote_error(tr("Failed to fetch icon list")); + } + container_loading.hide(); + }); + }; + + button_delete.on('click', event => { + if(!selected_icon) + return; + + const selected = modal.htmlTag.find(".selected"); + if(selected.length != 1) + console.warn(tr("UI selected icon length does not equal with 1! (%o)"), selected.length); + + if(selected_icon < 1000) return; /* we cant delete local icons */ + + client.fileManager.icons.delete_icon(selected_icon).then(() => { + selected.detach(); + }).catch(error => { + if(error instanceof CommandResult && error.id == ErrorID.PERMISSION_ERROR) + return; + console.warn(tr("Failed to delete icon %d: %o"), selected_icon, error); + + error = error instanceof CommandResult ? error.extra_message || error.message : error; + + createErrorModal(tr("Failed to delete icon"), tra("Failed to delete icon.
Error: ", error)).open(); + }); + }); + + button_upload.on('click', event => spawnIconUpload(client)); + + update_local_icons([100, 200, 300, 500, 600]); + update_remote_icons(); + modal.htmlTag.find('.button-reload').on('click', () => update_remote_icons()); + button_select.prop("disabled", true).on('click', () => { + if(callback_icon) callback_icon(selected_icon); + modal.close(); + }); + modal.htmlTag.find(".button-select-no-icon").on('click', () => { + if(callback_icon) callback_icon(0); + modal.close(); + }); + modal.open(); +} + +interface UploadingIcon { + file: File; + state: "loading" | "valid" | "error"; + upload_state: "unset" | "uploading" | "uploaded" | "error"; + + html_tag?: JQuery; + image_element?: () => HTMLImageElement; + + loader: Promise; + + upload_icon: () => () => Promise; + upload_html_tag?: JQuery; + + icon_id: string; +} + +function handle_icon_upload(file: File, client: ConnectionHandler) : UploadingIcon { + const icon = {} as UploadingIcon; + icon.file = file; + icon.upload_state = "unset"; + + const file_too_big = () => { + console.error(tr("Failed to load file %s: File is too big!"), file.name); + createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.
The given file is too big!", file.name)).open(); + icon.state = "error"; + }; + if(file.size > 1024 * 1024 * 512) { + file_too_big(); + } else if((file.size | 0) <= 0) { + console.error(tr("Failed to load file %s: Your browser does not support file sizes!"), file.name); + createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.
Your browser does not support file sizes!", file.name)).open(); + icon.state = "error"; + return; + } else { + icon.state = "loading"; + icon.loader = (async () => { + const reader = new FileReader(); + + try { + await new Promise((resolve, reject) => { + reader.onload = resolve; + reader.onerror = reject; + reader.readAsDataURL(file); + }); + } catch(error) { + console.log("Image failed to load (%o)", error); + console.error(tr("Failed to load file %s: Image failed to load"), file.name); + createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.
Failed to load image", file.name)).open(); + icon.state = "error"; + return; + } + + const result = reader.result as string; + if(typeof(result) !== "string") { + console.error(tr("Failed to load file %s: Result is not an media string (%o)"), file.name, result); + createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.
Result is not an media string", file.name)).open(); + icon.state = "error"; + return; + } + + + /* get the CRC32 sum */ + { + if(!result.startsWith("data:image/")) { + console.error(tr("Failed to load file %s: Invalid data media type (%o)"), file.name, result); + createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.
File is not an image", file.name)).open(); + icon.state = "error"; + return; + } + const semi = result.indexOf(';'); + const type = result.substring(11, semi); + console.log(tr("Given image has type %s"), type); + if(!result.substr(semi + 1).startsWith("base64,")) { + console.error(tr("Failed to load file %s: Mimetype isnt base64 encoded (%o)"), file.name, result.substr(semi + 1)); + createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.
Decoder returned unknown result", file.name)).open(); + icon.state = "error"; + return; + } + + const crc = new crc32.Crc32(); + crc.update(arrayBufferBase64(result.substr(semi + 8))); + icon.icon_id = crc.digest(10); + } + + + const image = document.createElement("img"); + try { + await new Promise((resolve, reject) => { + image.onload = resolve; + image.onerror = reject; + image.src = result; + }); + } catch(error) { + console.log("Image failed to load (%o)", error); + console.error(tr("Failed to load file %s: Image failed to load"), file.name); + createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.{:br:}Failed to load image", file.name)).open(); + icon.state = "error"; + } + + const width_error = message => { + console.error(tr("Failed to load file %s: Invalid bounds: %s"), file.name, message); + createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.{:br:}Image is too large ({})", file.name, message)).open(); + icon.state = "error"; + }; + + if(!result.startsWith("data:image/svg+xml")) { + if(image.naturalWidth > 32 && image.naturalHeight > 32) { + width_error("width and height (max 32px). Given: " + image.naturalWidth + "x" + image.naturalHeight); + return; + } + if(image.naturalWidth > 32) { + width_error("width (max 32px)"); + return; + } + if(image.naturalHeight > 32) { + width_error("height (max 32px)"); + return; } } + console.log("Image loaded (%dx%d) %s (%s)", image.naturalWidth, image.naturalHeight, image.name, icon.icon_id); + icon.image_element = () => { + const image = document.createElement("img"); + image.src = result; + return image; + }; + icon.state = "valid"; + })(); + + icon.upload_icon = () => { + const create_progress_bar = () => { + const html = $.spawn("div").addClass("progress"); + const indicator = $.spawn("div").addClass("progress-bar bg-success progress-bar-striped progress-bar-animated"); + const message = $.spawn("div").addClass("progress-message"); + const set_value = value => { + indicator.stop(true, false).animate({width: value + "%"}, 250); + if(value === 100) + setTimeout(() => indicator.removeClass("progress-bar-striped progress-bar-animated"), 900) + }; + + return { + html_tag: html.append(indicator).append(message), + set_value: set_value, + set_message: msg => message.text(msg), + set_error: (msg: string) => { + let index = msg.lastIndexOf(':'); + message.text(index == -1 ? msg : msg.substring(index + 1)); + message.attr('title', msg); + set_value(100); + indicator.removeClass("bg-success").addClass("bg-danger"); + } + } + }; + + const container_image = $.spawn("div").addClass("container-icon"); + const bar = create_progress_bar(); + + const set_error = message => { + bar.set_value(100); + bar.set_message(tr("error: ") + message); + }; + + const html_tag = $.spawn("div") + .addClass("upload-entry") + .append(container_image) + .append(bar.html_tag); + + icon.upload_html_tag = html_tag; + + let icon_added = false; + if(icon.image_element) { + container_image.append(icon.image_element()); + icon_added = true; + } + + + bar.set_value(0); + bar.set_value(tr("waiting")); + + return async () => { + const time_begin = Date.now(); + + if(icon.state === "loading") { + bar.set_message(tr("Awaiting local processing")); + await icon.loader; + // @ts-ignore Could happen because the loader function updates the state + if(icon.state !== "valid") { + set_error(tr("local processing failed")); + icon.upload_state = "error"; + return; + } + } else if(icon.state === "error") { + set_error(tr("local processing error")); + icon.upload_state = "error"; + return; + } + if(!icon_added) + container_image.append(icon.image_element()); + + bar.set_value(25); + bar.set_message(tr("initializing")); + + let upload_key: UploadKey; + try { + upload_key = await client.fileManager.upload_file({ + channel: undefined, + channel_password: undefined, + name: '/icon_' + icon.icon_id, + overwrite: false, + path: '', + size: icon.file.size + }) + } catch(error) { + if(error instanceof CommandResult && error.id == ErrorID.FILE_ALREADY_EXISTS) { + if(!settings.static_global(Settings.KEY_DISABLE_COSMETIC_SLOWDOWN, false)) + await new Promise(resolve => setTimeout(resolve, 500 + Math.floor(Math.random() * 500))); + bar.set_message(tr("icon already exists")); + bar.set_value(100); + icon.upload_state = "uploaded"; + return; + } + console.error(tr("Failed to initialize upload: %o"), error); + bar.set_error(tr("failed to initialize upload")); + icon.upload_state = "error"; + return; + } + bar.set_value(50); + bar.set_message(tr("uploading")); + + const connection = spawn_upload_transfer(upload_key); + try { + await connection.put_data(icon.file) + } catch(error) { + console.error(tr("Icon upload failed for icon %s: %o"), icon.file.name, error); + if(typeof(error) === "string") + bar.set_error(tr("upload failed: ") + error); + else if(typeof(error.message) === "string") + bar.set_error(tr("upload failed: ") + error.message); + else + bar.set_error(tr("upload failed")); + icon.upload_state = "error"; + return; + } + + const time_end = Date.now(); + if(!settings.static_global(Settings.KEY_DISABLE_COSMETIC_SLOWDOWN, false)) + await new Promise(resolve => setTimeout(resolve, Math.max(0, 1000 - (time_end - time_begin)))); + bar.set_value(100); + bar.set_message(tr("upload completed")); + icon.upload_state = "uploaded"; + }; + }; + } + + return icon; +} + +export function spawnIconUpload(client: ConnectionHandler) { + const modal = createModal({ + header: tr("Upload Icons"), + footer: undefined, + body: () => $("#tmpl_icon_upload").renderTag(), + closeable: false, + + min_width: "20em" + }); + modal.htmlTag.find(".modal-body").addClass("modal-icon-upload"); + + const button_upload = modal.htmlTag.find(".button-upload"); + const button_delete = modal.htmlTag.find(".button-remove").prop("disabled", true); + const button_add = modal.htmlTag.find(".button-add"); + const button_upload_abort = modal.htmlTag.find(".button-upload-abort"); + const input_file = modal.htmlTag.find(".input-file-upload") as JQuery; + const container_icons = modal.htmlTag.find(".container-icons"); + + let selected_icon: UploadingIcon; + let icons: UploadingIcon[] = []; + + const update_upload_button = () => { + const icon_count = icons.filter(e => e.state === "valid").length; + button_upload.empty(); + tra("Upload icons ({})", icon_count).forEach(e => e.appendTo(button_upload)); + button_upload.prop("disabled", icon_count == 0); + }; + update_upload_button(); + + const add_icon = (icon: UploadingIcon) => { + icons.push(icon); + icon.loader.then(e => { + if(icon.state === "valid") { + const image = icon.image_element(); + const element = $.spawn("div") + .addClass("icon-container") + .append(image); + container_icons.append(icon.html_tag = element); + + element.on('click', event => { + container_icons.find(".selected").removeClass("selected"); + element.addClass("selected"); + + selected_icon = icon; + button_delete.prop("disabled", false); + }); + + update_upload_button(); + } }); + }; + button_delete.on('click', event => { + if(!selected_icon) + return; + icons = icons.filter(e => e !== selected_icon); + if(selected_icon.html_tag) + selected_icon.html_tag.detach(); + button_delete.prop("disabled", true); + update_upload_button(); + }); - container_icons.on('dragover', ((event: DragEvent) => { - event.stopPropagation(); - event.preventDefault(); - event.dataTransfer.dropEffect = 'copy'; - }) as any); - container_icons.on('drop', ((event: DragEvent) => { - event.stopPropagation(); - event.preventDefault(); - - for(let index = 0; index < event.dataTransfer.files.length; index++) { - const file = event.dataTransfer.files.item(index); + button_add.on('click', event => input_file.click()); + input_file.on('change', event => { + if(input_file[0].files.length > 0) { + for(let index = 0; index < input_file[0].files.length; index++) { + const file = input_file[0].files.item(index); { let duplicate = false; for(const icon of icons) - if(icon.file === file && icon.state !== "error") { + if(icon.file.name === file.name && icon.file.lastModified === file.lastModified && icon.state !== "error") { duplicate = true; break; } @@ -543,110 +522,137 @@ namespace Modals { add_icon(handle_icon_upload(file, client)); } - }) as any); - - /* upload process */ - { - const container_upload = modal.htmlTag.find(".container-upload"); - const container_error = container_upload.find(".container-error"); - const container_success = container_upload.find(".container-success"); - const container_process = container_upload.find(".container-process"); - const container_info = container_upload.find(".container-info"); - const container_statistics = container_upload.find(".uploaded-statistics"); - - const show_critical_error = message => { - container_error.find(".error-message").text(message); - container_error.removeClass("hidden"); - }; - - const finish_upload = () => { - icons = icons.filter(e => { - if(e.upload_state === "uploaded") { - e.html_tag.detach(); - return false; - } - return true; - }); - update_upload_button(); - button_upload.prop("disabled", false); - button_upload.prop("disabled", false); - container_upload.hide(); - container_error.addClass("hidden"); - container_error.addClass("hidden"); - modal.set_closeable(true); - }; - - - const execute_upload = async () => { - if(!client || !client.fileManager) { - show_critical_error(tr("Invalid client handle")); - return; - } - if(!client.connected) { - show_critical_error(tr("Not connected")); - return; - } - - let invoke_count = 0; - let succeed_count = 0; - let failed_count = 0; - - const uploads = icons.filter(e => e.state !== "error"); - - const executes: {icon: UploadingIcon, task: () => Promise}[] = []; - for(const icon of uploads) { - executes.push({ - icon: icon, - task: icon.upload_icon() - }); - - if(!icon.upload_html_tag) - continue; /* TODO: error? */ - icon.upload_html_tag.appendTo(container_process); - } - - const update_state = () => container_statistics.text(invoke_count + " | " + succeed_count + " | " + failed_count); - for(const execute of executes) { - invoke_count++; - update_state(); - try { - await execute.task(); - if(execute.icon.upload_state !== "uploaded") - throw "failed"; - succeed_count++; - } catch(error) { - failed_count++; - } - update_state(); - } - container_info.css({opacity: 1}).animate({opacity: 0}, 250, () => container_info.css({opacity: undefined}).hide()); - container_success.find(".message").html( - "Total icons: " + invoke_count + "
" + - "Succeeded icons: " + succeed_count + "
" + - "Failed icons: " + failed_count - ); - - container_success.removeClass("hidden"); - }; - - button_upload.on('click', event => { - modal.set_closeable(false); - button_upload.prop("disabled", true); - button_delete.prop("disabled", true); - button_add.prop("disabled", true); - container_process.empty(); - container_upload.show(); - execute_upload(); - }); - - button_upload_abort.on('click', event => finish_upload()); - - container_error.addClass("hidden"); - container_success.addClass("hidden"); - container_upload.hide(); } + }); - modal.open(); - modal.set_closeable(true); + container_icons.on('dragover', ((event: DragEvent) => { + event.stopPropagation(); + event.preventDefault(); + event.dataTransfer.dropEffect = 'copy'; + }) as any); + container_icons.on('drop', ((event: DragEvent) => { + event.stopPropagation(); + event.preventDefault(); + + for(let index = 0; index < event.dataTransfer.files.length; index++) { + const file = event.dataTransfer.files.item(index); + { + let duplicate = false; + + for(const icon of icons) + if(icon.file === file && icon.state !== "error") { + duplicate = true; + break; + } + if(duplicate) + continue; + } + + add_icon(handle_icon_upload(file, client)); + } + }) as any); + + /* upload process */ + { + const container_upload = modal.htmlTag.find(".container-upload"); + const container_error = container_upload.find(".container-error"); + const container_success = container_upload.find(".container-success"); + const container_process = container_upload.find(".container-process"); + const container_info = container_upload.find(".container-info"); + const container_statistics = container_upload.find(".uploaded-statistics"); + + const show_critical_error = message => { + container_error.find(".error-message").text(message); + container_error.removeClass("hidden"); + }; + + const finish_upload = () => { + icons = icons.filter(e => { + if(e.upload_state === "uploaded") { + e.html_tag.detach(); + return false; + } + return true; + }); + update_upload_button(); + button_upload.prop("disabled", false); + button_upload.prop("disabled", false); + container_upload.hide(); + container_error.addClass("hidden"); + container_error.addClass("hidden"); + modal.set_closeable(true); + }; + + + const execute_upload = async () => { + if(!client || !client.fileManager) { + show_critical_error(tr("Invalid client handle")); + return; + } + if(!client.connected) { + show_critical_error(tr("Not connected")); + return; + } + + let invoke_count = 0; + let succeed_count = 0; + let failed_count = 0; + + const uploads = icons.filter(e => e.state !== "error"); + + const executes: {icon: UploadingIcon, task: () => Promise}[] = []; + for(const icon of uploads) { + executes.push({ + icon: icon, + task: icon.upload_icon() + }); + + if(!icon.upload_html_tag) + continue; /* TODO: error? */ + icon.upload_html_tag.appendTo(container_process); + } + + const update_state = () => container_statistics.text(invoke_count + " | " + succeed_count + " | " + failed_count); + for(const execute of executes) { + invoke_count++; + update_state(); + try { + await execute.task(); + if(execute.icon.upload_state !== "uploaded") + throw "failed"; + succeed_count++; + } catch(error) { + failed_count++; + } + update_state(); + } + container_info.css({opacity: 1}).animate({opacity: 0}, 250, () => container_info.css({opacity: undefined}).hide()); + container_success.find(".message").html( + "Total icons: " + invoke_count + "
" + + "Succeeded icons: " + succeed_count + "
" + + "Failed icons: " + failed_count + ); + + container_success.removeClass("hidden"); + }; + + button_upload.on('click', event => { + modal.set_closeable(false); + button_upload.prop("disabled", true); + button_delete.prop("disabled", true); + button_add.prop("disabled", true); + container_process.empty(); + container_upload.show(); + execute_upload(); + }); + + button_upload_abort.on('click', event => finish_upload()); + + container_error.addClass("hidden"); + container_success.addClass("hidden"); + container_upload.hide(); } + + modal.open(); + modal.set_closeable(true); } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalIdentity.ts b/shared/js/ui/modal/ModalIdentity.ts index 1c5f9296..1edab781 100644 --- a/shared/js/ui/modal/ModalIdentity.ts +++ b/shared/js/ui/modal/ModalIdentity.ts @@ -1,224 +1,227 @@ -namespace Modals { - export function spawnTeamSpeakIdentityImprove(identity: profiles.identities.TeaSpeakIdentity, name: string): Modal { - let modal: Modal; - let elapsed_timer: NodeJS.Timer; +import {createErrorModal, createInfoModal, createModal, Modal} from "tc-shared/ui/elements/Modal"; +import {TeaSpeakIdentity} from "tc-shared/profiles/identities/TeamSpeakIdentity"; +import * as tooltip from "tc-shared/ui/elements/Tooltip"; +import {formatMessage} from "tc-shared/ui/frames/chat"; - modal = createModal({ - header: tr("Improve identity"), - body: () => { - let template = $("#tmpl_settings-teamspeak_improve").renderTag({ - identity_name: name - }); - template = $.spawn("div").append(template); +export function spawnTeamSpeakIdentityImprove(identity: TeaSpeakIdentity, name: string): Modal { + let modal: Modal; + let elapsed_timer: NodeJS.Timer; - let active; - const button_start_stop = template.find(".button-start-stop"); - const button_close = template.find(".button-close"); - const input_current_level = template.find(".identity-level input"); - const input_target_level = template.find(".identity-target-level input"); - const input_threads = template.find(".threads input"); - const input_hash_rate = template.find(".hash-rate input"); - const input_elapsed = template.find(".time-elapsed input"); + modal = createModal({ + header: tr("Improve identity"), + body: () => { + let template = $("#tmpl_settings-teamspeak_improve").renderTag({ + identity_name: name + }); + template = $.spawn("div").append(template); - button_close.on('click', event => { - if (active) - button_start_stop.trigger('click'); + let active; + const button_start_stop = template.find(".button-start-stop"); + const button_close = template.find(".button-close"); + const input_current_level = template.find(".identity-level input"); + const input_target_level = template.find(".identity-target-level input"); + const input_threads = template.find(".threads input"); + const input_hash_rate = template.find(".hash-rate input"); + const input_elapsed = template.find(".time-elapsed input"); - if (modal.shown) - modal.close(); - }); + button_close.on('click', event => { + if (active) + button_start_stop.trigger('click'); - button_start_stop.on('click', event => { - button_start_stop - .toggleClass('btn-success', active) - .toggleClass('btn-danger', !active) - .text(active ? tr("Start") : tr("Stop")); - - input_threads.prop("disabled", !active); - input_target_level.prop("disabled", !active); - if (active) { - input_hash_rate.val(0); - clearInterval(elapsed_timer); - active = false; - return; - } - active = true; - input_hash_rate.val("nan"); - - const threads = parseInt(input_threads.val() as string); - const target_level = parseInt(input_target_level.val() as string); - if (target_level == 0) { - identity.improve_level(-1, threads, () => active, current_level => { - input_current_level.val(current_level); - }, hash_rate => { - input_hash_rate.val(hash_rate); - }).catch(error => { - console.error(error); - createErrorModal(tr("Failed to improve identity"), tr("Failed to improve identity.
Error:") + error).open(); - if (active) - button_start_stop.trigger('click'); - }); - } else { - identity.improve_level(target_level, threads, () => active, current_level => { - input_current_level.val(current_level); - }, hash_rate => { - input_hash_rate.val(hash_rate); - }).then(success => { - if (success) { - identity.level().then(level => { - input_current_level.val(level); - createInfoModal(tr("Identity successfully improved"), MessageHelper.formatMessage(tr("Identity successfully improved to level {}"), level)).open(); - }).catch(error => { - input_current_level.val("error: " + error); - }); - } - if (active) - button_start_stop.trigger('click'); - }).catch(error => { - console.error(error); - createErrorModal(tr("Failed to improve identity"), tr("Failed to improve identity.
Error:") + error).open(); - if (active) - button_start_stop.trigger('click'); - }); - } - - const begin = Date.now(); - elapsed_timer = setInterval(() => { - const time = (Date.now() - begin) / 1000; - let seconds = Math.floor(time % 60).toString(); - let minutes = Math.floor(time / 60).toString(); - - if (seconds.length < 2) - seconds = "0" + seconds; - - if (minutes.length < 2) - minutes = "0" + minutes; - - input_elapsed.val(minutes + ":" + seconds); - }, 1000); - }); - - - template.find(".identity-unique-id input").val(identity.uid()); - identity.level().then(level => { - input_current_level.val(level); - }).catch(error => { - input_current_level.val("error: " + error); - }); - tooltip(template); - return template.children(); - }, - footer: undefined, - width: 750 - }); - - modal.htmlTag.find(".modal-body").addClass("modal-identity-improve modal-green"); - modal.close_listener.push(() => modal.htmlTag.find(".button-close").trigger('click')); - modal.open(); - return modal; - } - - export function spawnTeamSpeakIdentityImport(callback: (identity: profiles.identities.TeaSpeakIdentity) => any): Modal { - let modal: Modal; - let selected_type: string; - let identities: {[key: string]: profiles.identities.TeaSpeakIdentity} = {}; - - modal = createModal({ - header: tr("Import identity"), - body: () => { - let template = $("#tmpl_settings-teamspeak_import").renderTag(); - - const button_import = template.find(".button-import"); - const button_file_select = template.find(".button-load-file"); - - const container_status = template.find(".container-status"); - const input_text = template.find(".input-identity-text"); - const input_file = template.find(".file-selector"); - - const set_status = (message: string | undefined, type: "error" | "loading" | "success") => { - container_status.toggleClass("hidden", !message); - if(message) { - container_status.toggleClass("error", type === "error"); - container_status.toggleClass("loading", type === "loading"); - container_status.find("a").text(message); - } - }; - - button_file_select.on('click', event => input_file.trigger('click')); - - template.find("input[name='type']").on('change', event => { - const type = (event.target as HTMLInputElement).value; - - button_file_select.prop("disabled", type !== "file"); - input_text.prop("disabled", type !== "text"); - - selected_type = type; - button_import.prop("disabled", !identities[type]); - }); - template.find("input[name='type'][value='file']").prop("checked", true).trigger("change"); - - const import_identity = (data: string, ini: boolean) => { - set_status(tr("Parsing identity"), "loading"); - profiles.identities.TeaSpeakIdentity.import_ts(data, ini).then(identity => { - identities[selected_type] = identity; - set_status("Identity parsed successfully.", "success"); - button_import.prop("disabled", false); - template.find(".success").show(); - }).catch(error => { - set_status(tr("Failed to parse identity: ") + error, "error"); - }); - }; - - /* file select button */ - input_file.on('change', event => { - const element = event.target as HTMLInputElement; - const file_reader = new FileReader(); - - set_status(tr("Loading file"), "loading"); - file_reader.onload = function () { - import_identity(file_reader.result as string, true); - }; - - file_reader.onerror = ev => { - console.error(tr("Failed to read give identity file: %o"), ev); - set_status(tr("Failed to read the identity file."), "error"); - return; - }; - - if (element.files && element.files.length > 0) - file_reader.readAsText(element.files[0]); - }); - - input_text.on('change keyup', event => { - const text = input_text.val() as string; - if(!text) { - set_status("", "success"); - return; - } - - if(text.indexOf('V') == -1) { - set_status(tr("Invalid identity string"), "error"); - return; - } - - import_identity(text, false); - }); - - button_import.on('click', event => { + if (modal.shown) modal.close(); - callback(identities[selected_type]); + }); + + button_start_stop.on('click', event => { + button_start_stop + .toggleClass('btn-success', active) + .toggleClass('btn-danger', !active) + .text(active ? tr("Start") : tr("Stop")); + + input_threads.prop("disabled", !active); + input_target_level.prop("disabled", !active); + if (active) { + input_hash_rate.val(0); + clearInterval(elapsed_timer); + active = false; + return; + } + active = true; + input_hash_rate.val("nan"); + + const threads = parseInt(input_threads.val() as string); + const target_level = parseInt(input_target_level.val() as string); + if (target_level == 0) { + identity.improve_level(-1, threads, () => active, current_level => { + input_current_level.val(current_level); + }, hash_rate => { + input_hash_rate.val(hash_rate); + }).catch(error => { + console.error(error); + createErrorModal(tr("Failed to improve identity"), tr("Failed to improve identity.
Error:") + error).open(); + if (active) + button_start_stop.trigger('click'); + }); + } else { + identity.improve_level(target_level, threads, () => active, current_level => { + input_current_level.val(current_level); + }, hash_rate => { + input_hash_rate.val(hash_rate); + }).then(success => { + if (success) { + identity.level().then(level => { + input_current_level.val(level); + createInfoModal(tr("Identity successfully improved"), formatMessage(tr("Identity successfully improved to level {}"), level)).open(); + }).catch(error => { + input_current_level.val("error: " + error); + }); + } + if (active) + button_start_stop.trigger('click'); + }).catch(error => { + console.error(error); + createErrorModal(tr("Failed to improve identity"), tr("Failed to improve identity.
Error:") + error).open(); + if (active) + button_start_stop.trigger('click'); + }); + } + + const begin = Date.now(); + elapsed_timer = setInterval(() => { + const time = (Date.now() - begin) / 1000; + let seconds = Math.floor(time % 60).toString(); + let minutes = Math.floor(time / 60).toString(); + + if (seconds.length < 2) + seconds = "0" + seconds; + + if (minutes.length < 2) + minutes = "0" + minutes; + + input_elapsed.val(minutes + ":" + seconds); + }, 1000); + }); + + + template.find(".identity-unique-id input").val(identity.uid()); + identity.level().then(level => { + input_current_level.val(level); + }).catch(error => { + input_current_level.val("error: " + error); + }); + tooltip.initialize(template); + return template.children(); + }, + footer: undefined, + width: 750 + }); + + modal.htmlTag.find(".modal-body").addClass("modal-identity-improve modal-green"); + modal.close_listener.push(() => modal.htmlTag.find(".button-close").trigger('click')); + modal.open(); + return modal; +} + +export function spawnTeamSpeakIdentityImport(callback: (identity: TeaSpeakIdentity) => any): Modal { + let modal: Modal; + let selected_type: string; + let identities: {[key: string]: TeaSpeakIdentity} = {}; + + modal = createModal({ + header: tr("Import identity"), + body: () => { + let template = $("#tmpl_settings-teamspeak_import").renderTag(); + + const button_import = template.find(".button-import"); + const button_file_select = template.find(".button-load-file"); + + const container_status = template.find(".container-status"); + const input_text = template.find(".input-identity-text"); + const input_file = template.find(".file-selector"); + + const set_status = (message: string | undefined, type: "error" | "loading" | "success") => { + container_status.toggleClass("hidden", !message); + if(message) { + container_status.toggleClass("error", type === "error"); + container_status.toggleClass("loading", type === "loading"); + container_status.find("a").text(message); + } + }; + + button_file_select.on('click', event => input_file.trigger('click')); + + template.find("input[name='type']").on('change', event => { + const type = (event.target as HTMLInputElement).value; + + button_file_select.prop("disabled", type !== "file"); + input_text.prop("disabled", type !== "text"); + + selected_type = type; + button_import.prop("disabled", !identities[type]); + }); + template.find("input[name='type'][value='file']").prop("checked", true).trigger("change"); + + const import_identity = (data: string, ini: boolean) => { + set_status(tr("Parsing identity"), "loading"); + TeaSpeakIdentity.import_ts(data, ini).then(identity => { + identities[selected_type] = identity; + set_status("Identity parsed successfully.", "success"); + button_import.prop("disabled", false); + template.find(".success").show(); + }).catch(error => { + set_status(tr("Failed to parse identity: ") + error, "error"); }); + }; - set_status("", "success"); - button_import.prop("disabled", true); - return template.children(); - }, - footer: undefined, - width: 750 - }); + /* file select button */ + input_file.on('change', event => { + const element = event.target as HTMLInputElement; + const file_reader = new FileReader(); - modal.htmlTag.find(".modal-body").addClass("modal-identity-import modal-green"); - modal.open(); - return modal; - } + set_status(tr("Loading file"), "loading"); + file_reader.onload = function () { + import_identity(file_reader.result as string, true); + }; + + file_reader.onerror = ev => { + console.error(tr("Failed to read give identity file: %o"), ev); + set_status(tr("Failed to read the identity file."), "error"); + return; + }; + + if (element.files && element.files.length > 0) + file_reader.readAsText(element.files[0]); + }); + + input_text.on('change keyup', event => { + const text = input_text.val() as string; + if(!text) { + set_status("", "success"); + return; + } + + if(text.indexOf('V') == -1) { + set_status(tr("Invalid identity string"), "error"); + return; + } + + import_identity(text, false); + }); + + button_import.on('click', event => { + modal.close(); + callback(identities[selected_type]); + }); + + set_status("", "success"); + button_import.prop("disabled", true); + return template.children(); + }, + footer: undefined, + width: 750 + }); + + modal.htmlTag.find(".modal-body").addClass("modal-identity-import modal-green"); + modal.open(); + return modal; } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalInvite.ts b/shared/js/ui/modal/ModalInvite.ts index 45a7acaa..b0a17db7 100644 --- a/shared/js/ui/modal/ModalInvite.ts +++ b/shared/js/ui/modal/ModalInvite.ts @@ -1,217 +1,216 @@ -/// -/// -/// +import {settings, Settings} from "tc-shared/settings"; +import {createModal, Modal} from "tc-shared/ui/elements/Modal"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {ServerAddress} from "tc-shared/ui/server"; -namespace Modals { - type URLGeneratorSettings = { - flag_direct: boolean, - flag_resolved: boolean - } +type URLGeneratorSettings = { + flag_direct: boolean, + flag_resolved: boolean +} - const DefaultGeneratorSettings: URLGeneratorSettings = { - flag_direct: true, - flag_resolved: false - }; +const DefaultGeneratorSettings: URLGeneratorSettings = { + flag_direct: true, + flag_resolved: false +}; - type URLGenerator = { - generate: (properties: { - address: ServerAddress, - resolved_address: ServerAddress - } & URLGeneratorSettings) => string; +type URLGenerator = { + generate: (properties: { + address: ServerAddress, + resolved_address: ServerAddress + } & URLGeneratorSettings) => string; - setting_available: (key: keyof URLGeneratorSettings) => boolean; - }; + setting_available: (key: keyof URLGeneratorSettings) => boolean; +}; - const build_url = (base, params) => { - if(Object.keys(params).length == 0) - return base; +const build_url = (base, params) => { + if(Object.keys(params).length == 0) + return base; - return base + "?" + Object.keys(params) - .map(e => e + "=" + encodeURIComponent(params[e])) - .join("&"); - }; + return base + "?" + Object.keys(params) + .map(e => e + "=" + encodeURIComponent(params[e])) + .join("&"); +}; - //TODO: Server password - const url_generators: {[key: string]:URLGenerator} = { - "tea-web": { - generate: properties => { - const address = properties.resolved_address ? properties.resolved_address : properties.address; - const address_str = address.host + (address.port === 9987 ? "" : address.port); - const parameter = "connect_default=" + (properties.flag_direct ? 1 : 0) + "&connect_address=" + encodeURIComponent(address_str); - - let pathbase = ""; - if(document.location.protocol !== 'https:') { - /* - * Seems to be a test environment or the TeaClient for localhost where we dont have to use https. - */ - pathbase = "https://web.teaspeak.de/"; - } else if(document.location.hostname === "localhost" || document.location.host.startsWith("127.")) { - pathbase = "https://web.teaspeak.de/"; - } else { - pathbase = document.location.origin + document.location.pathname; - } - return pathbase + "?" + parameter; - }, - setting_available: setting => { - return { - flag_direct: true, - flag_resolved: true - }[setting] || false; - } - }, - "tea-client": { - generate: properties => { - const address = properties.resolved_address ? properties.resolved_address : properties.address; - - - let parameters = { - connect_default: properties.flag_direct ? 1 : 0 - }; - - if(address.port != 9987) - parameters["port"] = address.port; - - return build_url("teaclient://" + address.host + "/", parameters); - }, - setting_available: setting => { - return { - flag_direct: true, - flag_resolved: true - }[setting] || false; - } - }, - "teamspeak": { - generate: properties => { - const address = properties.resolved_address ? properties.resolved_address : properties.address; - - let parameters = {}; - if(address.port != 9987) - parameters["port"] = address.port; +//TODO: Server password +const url_generators: {[key: string]:URLGenerator} = { + "tea-web": { + generate: properties => { + const address = properties.resolved_address ? properties.resolved_address : properties.address; + const address_str = address.host + (address.port === 9987 ? "" : address.port); + const parameter = "connect_default=" + (properties.flag_direct ? 1 : 0) + "&connect_address=" + encodeURIComponent(address_str); + let pathbase = ""; + if(document.location.protocol !== 'https:') { /* - ts3server://? - port=9987 - nickname=UserNickname - password=serverPassword - channel=MyDefaultChannel - cid=channelID - channelpassword=defaultChannelPassword - token=TokenKey - addbookmark=MyBookMarkLabel - */ - return build_url("ts3server://" + address.host + "/", parameters); - }, - setting_available: setting => { - return { - flag_direct: false, - flag_resolved: true - }[setting] || false; + * Seems to be a test environment or the TeaClient for localhost where we dont have to use https. + */ + pathbase = "https://web.teaspeak.de/"; + } else if(document.location.hostname === "localhost" || document.location.host.startsWith("127.")) { + pathbase = "https://web.teaspeak.de/"; + } else { + pathbase = document.location.origin + document.location.pathname; } + return pathbase + "?" + parameter; + }, + setting_available: setting => { + return { + flag_direct: true, + flag_resolved: true + }[setting] || false; } + }, + "tea-client": { + generate: properties => { + const address = properties.resolved_address ? properties.resolved_address : properties.address; + + + let parameters = { + connect_default: properties.flag_direct ? 1 : 0 + }; + + if(address.port != 9987) + parameters["port"] = address.port; + + return build_url("teaclient://" + address.host + "/", parameters); + }, + setting_available: setting => { + return { + flag_direct: true, + flag_resolved: true + }[setting] || false; + } + }, + "teamspeak": { + generate: properties => { + const address = properties.resolved_address ? properties.resolved_address : properties.address; + + let parameters = {}; + if(address.port != 9987) + parameters["port"] = address.port; + + /* + ts3server://? + port=9987 + nickname=UserNickname + password=serverPassword + channel=MyDefaultChannel + cid=channelID + channelpassword=defaultChannelPassword + token=TokenKey + addbookmark=MyBookMarkLabel + */ + return build_url("ts3server://" + address.host + "/", parameters); + }, + setting_available: setting => { + return { + flag_direct: false, + flag_resolved: true + }[setting] || false; + } + } +}; + +export function spawnInviteEditor(connection: ConnectionHandler) { + let modal: Modal; + modal = createModal({ + header: tr("Invite URL creator"), + body: () => { + let template = $("#tmpl_invite").renderTag(); + + template.find(".button-close").on('click', event => modal.close()); + return template; + }, + footer: undefined, + min_width: "20em", + width: "50em" + }); + + modal.htmlTag.find(".modal-body").addClass("modal-invite"); + + const button_copy = modal.htmlTag.find(".button-copy"); + const input_type = modal.htmlTag.find(".property-type select"); + const label_output = modal.htmlTag.find(".text-output"); + + const invite_settings = [ + { + key: "flag_direct", + node: modal.htmlTag.find(".flag-direct-connect input"), + value: node => node.prop('checked'), + set_value: (node, value) => node.prop('checked', value == "1"), + disable: (node, flag) => node.prop('disabled', flag) + .firstParent('.checkbox').toggleClass('disabled', flag) + }, + + { + key: "flag_resolved", + node: modal.htmlTag.find(".flag-resolved-address input"), + value: node => node.prop('checked'), + set_value: (node, value) => node.prop('checked', value == "1"), + disable: (node, flag) => node.prop('disabled', flag) + .firstParent('.checkbox').toggleClass('disabled', flag) + } + ]; + + const update_buttons = () => { + const generator = url_generators[input_type.val() as string]; + if(!generator) { + for(const s of invite_settings) + s.disable(s.node, true); + return; + } + + for(const s of invite_settings) + s.disable(s.node, !generator.setting_available(s.key as any)); }; - export function spawnInviteEditor(connection: ConnectionHandler) { - let modal: Modal; - modal = createModal({ - header: tr("Invite URL creator"), - body: () => { - let template = $("#tmpl_invite").renderTag(); - - template.find(".button-close").on('click', event => modal.close()); - return template; - }, - footer: undefined, - min_width: "20em", - width: "50em" - }); - - modal.htmlTag.find(".modal-body").addClass("modal-invite"); - - const button_copy = modal.htmlTag.find(".button-copy"); - const input_type = modal.htmlTag.find(".property-type select"); - const label_output = modal.htmlTag.find(".text-output"); - - const invite_settings = [ - { - key: "flag_direct", - node: modal.htmlTag.find(".flag-direct-connect input"), - value: node => node.prop('checked'), - set_value: (node, value) => node.prop('checked', value == "1"), - disable: (node, flag) => node.prop('disabled', flag) - .firstParent('.checkbox').toggleClass('disabled', flag) - }, - - { - key: "flag_resolved", - node: modal.htmlTag.find(".flag-resolved-address input"), - value: node => node.prop('checked'), - set_value: (node, value) => node.prop('checked', value == "1"), - disable: (node, flag) => node.prop('disabled', flag) - .firstParent('.checkbox').toggleClass('disabled', flag) - } - ]; - - const update_buttons = () => { - const generator = url_generators[input_type.val() as string]; - if(!generator) { - for(const s of invite_settings) - s.disable(s.node, true); - return; - } - - for(const s of invite_settings) - s.disable(s.node, !generator.setting_available(s.key as any)); - }; - - const update_link = () => { - const generator = url_generators[input_type.val() as string]; - if(!generator) { - button_copy.prop('disabled', true); - label_output.text(tr("Missing link generator")); - return; - } - button_copy.prop('disabled', false); - - const properties = { - address: connection.channelTree.server.remote_address, - resolved_address: connection.channelTree.client.serverConnection.remote_address() - }; - for(const s of invite_settings) - properties[s.key] = s.value(s.node); - - label_output.text(generator.generate(properties as any)); - }; - - - for(const s of invite_settings) { - s.node.on('change keyup', () => { - settings.changeGlobal(Settings.FN_INVITE_LINK_SETTING(s.key), s.value(s.node)); - update_link() - }); - - s.set_value(s.node, settings.global(Settings.FN_INVITE_LINK_SETTING(s.key), DefaultGeneratorSettings[s.key])); + const update_link = () => { + const generator = url_generators[input_type.val() as string]; + if(!generator) { + button_copy.prop('disabled', true); + label_output.text(tr("Missing link generator")); + return; } + button_copy.prop('disabled', false); - input_type.on('change', () => { - settings.changeGlobal(Settings.KEY_LAST_INVITE_LINK_TYPE, input_type.val()); - update_buttons(); - update_link(); - }).val(settings.global(Settings.KEY_LAST_INVITE_LINK_TYPE)); + const properties = { + address: connection.channelTree.server.remote_address, + resolved_address: connection.channelTree.client.serverConnection.remote_address() + }; + for(const s of invite_settings) + properties[s.key] = s.value(s.node); - button_copy.on('click', event => { - label_output.select(); - document.execCommand('copy'); + label_output.text(generator.generate(properties as any)); + }; + + + for(const s of invite_settings) { + s.node.on('change keyup', () => { + settings.changeGlobal(Settings.FN_INVITE_LINK_SETTING(s.key), s.value(s.node)); + update_link() }); - update_buttons(); - update_link(); - modal.open(); + s.set_value(s.node, settings.global(Settings.FN_INVITE_LINK_SETTING(s.key), DefaultGeneratorSettings[s.key])); } + + input_type.on('change', () => { + settings.changeGlobal(Settings.KEY_LAST_INVITE_LINK_TYPE, input_type.val()); + update_buttons(); + update_link(); + }).val(settings.global(Settings.KEY_LAST_INVITE_LINK_TYPE)); + + button_copy.on('click', event => { + label_output.select(); + document.execCommand('copy'); + }); + + update_buttons(); + update_link(); + modal.open(); } /* - - - - */ \ No newline at end of file + + + +*/ \ No newline at end of file diff --git a/shared/js/ui/modal/ModalKeySelect.ts b/shared/js/ui/modal/ModalKeySelect.ts index 8ad60b2d..5bc0cce7 100644 --- a/shared/js/ui/modal/ModalKeySelect.ts +++ b/shared/js/ui/modal/ModalKeySelect.ts @@ -1,50 +1,53 @@ -namespace Modals { - export function spawnKeySelect(callback: (key?: ppt.KeyEvent) => void) { - let modal = createModal({ - header: tr("Select a key"), - body: () => $("#tmpl_key_select").renderTag().children(), - footer: null, +import {createModal} from "tc-shared/ui/elements/Modal"; +import {EventType, key_description, KeyEvent} from "tc-shared/PPTListener"; +import * as loader from "tc-loader"; +import * as ppt from "tc-backend/ppt"; - width: "", - closeable: false - }); +export function spawnKeySelect(callback: (key?: KeyEvent) => void) { + let modal = createModal({ + header: tr("Select a key"), + body: () => $("#tmpl_key_select").renderTag().children(), + footer: null, - const container_key = modal.htmlTag.find(".container-key a"); - const button_save = modal.htmlTag.find(".button-save"); - const button_cancel = modal.htmlTag.find(".button-cancel"); + width: "", + closeable: false + }); - let current_key_age: number; - let last_key: ppt.KeyEvent; - let current_key: ppt.KeyEvent; - const listener = (event: ppt.KeyEvent) => { - if(event.type === ppt.EventType.KEY_PRESS) { - //console.log(tr("Key select got key press for %o"), event); - last_key = current_key; - current_key = event; - current_key_age = Date.now(); + const container_key = modal.htmlTag.find(".container-key a"); + const button_save = modal.htmlTag.find(".button-save"); + const button_cancel = modal.htmlTag.find(".button-cancel"); - container_key.text(ppt.key_description(event)); - button_save.prop("disabled", false); - } - }; + let current_key_age: number; + let last_key: KeyEvent; + let current_key: KeyEvent; + const listener = (event: KeyEvent) => { + if(event.type === EventType.KEY_PRESS) { + //console.log(tr("Key select got key press for %o"), event); + last_key = current_key; + current_key = event; + current_key_age = Date.now(); + + container_key.text(key_description(event)); + button_save.prop("disabled", false); + } + }; - button_save.on('click', () => { - if(!app.is_web()) { - /* Because pressing the close button is also a mouse action */ - if(current_key_age + 1000 > Date.now() && current_key.key_code == "MOUSE2") - current_key = last_key; - } + button_save.on('click', () => { + if(loader.version().type !== "web") { + /* Because pressing the close button is also a mouse action */ + if(current_key_age + 1000 > Date.now() && current_key.key_code == "MOUSE2") + current_key = last_key; + } - callback(current_key); - modal.close(); - }).prop("disabled", true); - button_cancel.on('click', () => modal.close()); + callback(current_key); + modal.close(); + }).prop("disabled", true); + button_cancel.on('click', () => modal.close()); - ppt.register_key_listener(listener); - modal.close_listener.push(() => ppt.unregister_key_listener(listener)); + ppt.register_key_listener(listener); + modal.close_listener.push(() => ppt.unregister_key_listener(listener)); - modal.htmlTag.find(".modal-body").addClass("modal-keyselect modal-green"); - modal.open(); - } + modal.htmlTag.find(".modal-body").addClass("modal-keyselect modal-green"); + modal.open(); } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalMusicManage.ts b/shared/js/ui/modal/ModalMusicManage.ts index 1736ca05..2c47375a 100644 --- a/shared/js/ui/modal/ModalMusicManage.ts +++ b/shared/js/ui/modal/ModalMusicManage.ts @@ -1,1774 +1,1449 @@ -/// -/// -/// +import {createErrorModal, createModal} from "tc-shared/ui/elements/Modal"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {MusicClientEntry} from "tc-shared/ui/client"; +import {Registry} from "tc-shared/events"; +import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; +import {tra} from "tc-shared/i18n/localize"; +import * as tooltip from "tc-shared/ui/elements/Tooltip"; +import { modal } from "tc-shared/events"; +import * as i18nc from "tc-shared/i18n/country"; +import {find} from "tc-shared/permission/PermissionManager"; +import ServerGroup = find.ServerGroup; +import * as htmltags from "tc-shared/ui/htmltags"; -namespace Modals { - export function openMusicManage(client: ConnectionHandler, bot: MusicClientEntry) { - const ev_registry = new events.Registry(); - ev_registry.enable_debug("music-manage"); - //dummy_controller(ev_registry); - permission_controller(ev_registry, bot, client); +export function openMusicManage(client: ConnectionHandler, bot: MusicClientEntry) { + const ev_registry = new Registry(); + ev_registry.enable_debug("music-manage"); + //dummy_controller(ev_registry); + permission_controller(ev_registry, bot, client); - let modal = createModal({ - header: tr(tr("Playlist Manage")), - body: () => build_modal(ev_registry), - footer: null, + let modal = createModal({ + header: tr(tr("Playlist Manage")), + body: () => build_modal(ev_registry), + footer: null, - min_width: "35em", - closeable: true - }); - modal.htmlTag.find(".modal-body").addClass("modal-music-manage"); + min_width: "35em", + closeable: true + }); + modal.htmlTag.find(".modal-body").addClass("modal-music-manage"); - /* "controller" */ - { + /* "controller" */ + { - } - - modal.open(); } - function permission_controller(event_registry: events.Registry, bot: MusicClientEntry, client: ConnectionHandler) { - const error_msg = error => { - if(error instanceof CommandResult) { - if(error.id === ErrorID.PERMISSION_ERROR) { - const permission = client.permissions.resolveInfo(error.json["failed_permid"]); - return tr("failed on permission ") + (permission ? permission.name : tr("unknown")); - } - return error.extra_message || error.message; - } else if(typeof error === "string") - return error; - else - return tr("command error"); - }; + modal.open(); +} - { - event_registry.on("query_playlist_status", event => { - const playlist_id = bot.properties.client_playlist_id; - client.serverConnection.command_helper.request_playlist_info(playlist_id).then(result => { - event_registry.fire("playlist_status", { - status: "success", - data: { - replay_mode: result.playlist_replay_mode, - finished: result.playlist_flag_finished, - delete_played: result.playlist_flag_delete_played, - notify_song_change: bot.properties.client_flag_notify_song_change, - max_size: result.playlist_max_songs - } - }); - }).catch(error => { - event_registry.fire("playlist_status", { - status: "error", - error_msg: error_msg(error) - }); - log.error(LogCategory.CLIENT, tr("Failed to query playlist info for playlist %d: %o"), playlist_id, error); - }); - }); +function permission_controller(event_registry: Registry, bot: MusicClientEntry, client: ConnectionHandler) { + const error_msg = error => { + if(error instanceof CommandResult) { + if(error.id === ErrorID.PERMISSION_ERROR) { + const permission = client.permissions.resolveInfo(error.json["failed_permid"]); + return tr("failed on permission ") + (permission ? permission.name : tr("unknown")); + } + return error.extra_message || error.message; + } else if(typeof error === "string") + return error; + else + return tr("command error"); + }; - event_registry.on("set_playlist_status", event => { - const playlist_id = bot.properties.client_playlist_id; - const property_map = { - "replay_mode": "playlist_replay_mode", - "finished": "playlist_flag_finished", - "delete_played": "playlist_flag_delete_played", - "max_size": "playlist_max_songs" - }; - - Promise.resolve().then(() => { - if(event.key === "notify_song_change") { - return client.serverConnection.send_command("clientedit", { - clid: bot.clientId(), - client_flag_notify_song_change: event.value - }); - } else { - const property = property_map[event.key]; - if(!property) return Promise.reject(tr("unknown property")); - - const data = { - playlist_id: playlist_id - }; - data[property] = event.value; - return client.serverConnection.send_command("playlistedit", data); + { + event_registry.on("query_playlist_status", event => { + const playlist_id = bot.properties.client_playlist_id; + client.serverConnection.command_helper.request_playlist_info(playlist_id).then(result => { + event_registry.fire("playlist_status", { + status: "success", + data: { + replay_mode: result.playlist_replay_mode, + finished: result.playlist_flag_finished, + delete_played: result.playlist_flag_delete_played, + notify_song_change: bot.properties.client_flag_notify_song_change, + max_size: result.playlist_max_songs } - }).then(() => { - event_registry.fire("set_playlist_status_result", { - status: "success", - key: event.key, - value: event.value - }); - }).catch(error => { - event_registry.fire("set_playlist_status_result", { - status: "error", - key: event.key, - error_msg: error_msg(error) - }); - log.error(LogCategory.CLIENT, tr("Failed to change playlist status %s for playlist %d: %o"), event.key, playlist_id, error); }); + }).catch(error => { + event_registry.fire("playlist_status", { + status: "error", + error_msg: error_msg(error) + }); + log.error(LogCategory.CLIENT, tr("Failed to query playlist info for playlist %d: %o"), playlist_id, error); }); + }); - event_registry.on("query_bot_status", event => { - setTimeout(() => { - event_registry.fire("bot_status", { - status: "success", - data: { - channel_commander: bot.properties.client_is_channel_commander, - volume: bot.properties.player_volume, - description: bot.properties.client_description, - default_country_code: ( - !bot.channelTree ? undefined : - !bot.channelTree.server ? undefined : bot.channelTree.server.properties.virtualserver_country_code) || "DE", - country_code: bot.properties.client_country, - name: bot.properties.client_nickname, - priority_speaker: bot.properties.client_is_priority_speaker, + event_registry.on("set_playlist_status", event => { + const playlist_id = bot.properties.client_playlist_id; + const property_map = { + "replay_mode": "playlist_replay_mode", + "finished": "playlist_flag_finished", + "delete_played": "playlist_flag_delete_played", + "max_size": "playlist_max_songs" + }; - bot_type: bot.properties.client_bot_type, - client_platform: bot.properties.client_platform, - client_version: bot.properties.client_version, - uptime_mode: bot.properties.client_uptime_mode - } + Promise.resolve().then(() => { + if(event.key === "notify_song_change") { + return client.serverConnection.send_command("clientedit", { + clid: bot.clientId(), + client_flag_notify_song_change: event.value }); - }, 0); - }); - - event_registry.on("set_bot_status", event => { - const property_map = { - "channel_commander": "client_is_channel_commander", - "volume": "player_volume", - "description": "client_description", - "country_code": "client_country", - "name": "client_nickname", - "priority_speaker": "client_is_priority_speaker", - - "bot_type": "client_bot_type", - "client_platform": "client_platform", - "client_version": "client_version", - "uptime_mode": "client_uptime_mode" - }; - - - Promise.resolve().then(() => { + } else { const property = property_map[event.key]; if(!property) return Promise.reject(tr("unknown property")); const data = { - clid: bot.clientId() + playlist_id: playlist_id }; data[property] = event.value; - return client.serverConnection.send_command("clientedit", data); - }).then(() => { - event_registry.fire("set_bot_status_result", { - status: "success", - key: event.key, - value: event.value - }); - }).catch(error => { - event_registry.fire("set_bot_status_result", { - status: "error", - key: event.key, - error_msg: error_msg(error) - }); - log.error(LogCategory.CLIENT, tr("Failed to change bot setting %s: %o"), event.key, error); + return client.serverConnection.send_command("playlistedit", data); + } + }).then(() => { + event_registry.fire("set_playlist_status_result", { + status: "success", + key: event.key, + value: event.value }); - }); - } - - /* permissions */ - { - event_registry.on("query_general_permissions", event => { - const playlist_id = bot.properties.client_playlist_id; - client.permissions.requestPlaylistPermissions(playlist_id).then(result => { - const permissions = {}; - for(const permission of result) - if(permission.hasValue()) - permissions[permission.type.name] = permission.value; - event_registry.fire("general_permissions", { - status: "success", - permissions: permissions - }); - }).catch(error => { - event_registry.fire("general_permissions", { - status: "error", - error_msg: error_msg(error) - }); - log.error(LogCategory.CLIENT, tr("Failed to query playlist general permissions for playlist %d: %o"), playlist_id, error); + }).catch(error => { + event_registry.fire("set_playlist_status_result", { + status: "error", + key: event.key, + error_msg: error_msg(error) }); + log.error(LogCategory.CLIENT, tr("Failed to change playlist status %s for playlist %d: %o"), event.key, playlist_id, error); }); - - event_registry.on("set_general_permission", event => { - const playlist_id = bot.properties.client_playlist_id; - - client.serverConnection.send_command("playlistaddperm", { - playlist_id: playlist_id, - permsid: event.key, - permvalue: event.value, - permskip: false, - permnegated: false - }).then(() => { - event_registry.fire("set_general_permission_result", { - key: event.key, - status: "success", - value: event.value - }); - }).catch(error => { - event_registry.fire("set_general_permission_result", { - status: "error", - key: event.key, - error_msg: error_msg(error) - }); - log.error(LogCategory.CLIENT, tr("Failed to set playlist general permissions for playlist %d and permission %d: %o"), playlist_id, event.key, error); - }); - }); - - event_registry.on("query_client_permissions", event => { - const playlist_id = bot.properties.client_playlist_id; - const client_id = event.client_database_id; - client.permissions.requestPlaylistClientPermissions(playlist_id, client_id).then(result => { - const permissions = {}; - for(const permission of result) - if(permission.hasValue()) - permissions[permission.type.name] = permission.value; - event_registry.fire("client_permissions", { - status: "success", - client_database_id: event.client_database_id, - permissions: permissions - }); - }).catch(error => { - event_registry.fire("client_permissions", { - status: "error", - client_database_id: event.client_database_id, - error_msg: error_msg(error) - }); - log.error(LogCategory.CLIENT, tr("Failed to query playlist client permissions for playlist %d and client %d: %o"), playlist_id, client_id, error); - }); - }); - - event_registry.on("set_client_permission", event => { - const playlist_id = bot.properties.client_playlist_id; - const client_id = event.client_database_id; - - client.serverConnection.send_command("playlistclientaddperm", { - playlist_id: playlist_id, - cldbid: client_id, - - permsid: event.key, - permvalue: event.value, - permskip: false, - permnegated: false - }).then(() => { - event_registry.fire("set_client_permission_result", { - key: event.key, - status: "success", - client_database_id: client_id, - value: event.value - }); - }).catch(error => { - event_registry.fire("set_client_permission_result", { - status: "error", - key: event.key, - client_database_id: client_id, - error_msg: error_msg(error) - }); - log.error(LogCategory.CLIENT, tr("Failed to set playlist client permissions for playlist %d, permission %d and client id %d: %o"), playlist_id, event.key, client_id, error); - }); - }); - - event_registry.on("query_special_clients", event => { - const playlist_id = bot.properties.client_playlist_id; - client.serverConnection.command_helper.request_playlist_client_list(playlist_id).then(clients => { - return client.serverConnection.command_helper.info_from_cldbid(...clients); - }).then(clients => { - event_registry.fire("special_client_list", { - status: "success", - clients: clients.map(e => { - return { - name: e.client_nickname, - unique_id: e.client_unique_id, - database_id: e.client_database_id - } - }) - }); - }).catch(error => { - event_registry.fire("special_client_list", { - status: "error", - error_msg: error_msg(error) - }); - log.error(LogCategory.CLIENT, tr("Failed to query special client list for playlist %d: %o"), playlist_id, error); - }) - }); - - event_registry.on("search_client", event => { - if(!event.text) return; - - const text = event.text; - Promise.resolve().then(() => { - let is_uuid = false; - try { - is_uuid = atob(text).length === 32; - } catch(e) {} - if(is_uuid) { - return client.serverConnection.command_helper.info_from_uid(text); - } else if(text.match(/^[0-9]{1,7}$/) && !isNaN(parseInt(text))) { - return client.serverConnection.command_helper.info_from_cldbid(parseInt(text)); - } else { - //TODO: Database name lookup? - return Promise.reject("no results"); - } - }).then(result => { - if(result.length) { - const client = result[0]; - event_registry.fire("search_client_result", { - status: "success", - client: { - name: client.client_nickname, - unique_id: client.client_unique_id, - database_id: client.client_database_id - } - }); - } else { - event_registry.fire("search_client_result", { - status: "empty" - }); - } - }).catch(error => { - event_registry.fire("search_client_result", { - status: "error", - error_msg: error_msg(error) - }); - log.error(LogCategory.CLIENT, tr("Failed to lookup search text \"%s\": %o"), text, error); - }); - }); - - event_registry.on("query_group_permissions", event => { - client.permissions.find_permission(event.permission_name).then(result => { - let groups = []; - for(const e of result) { - if(e.type !== "server_group") continue; - - const group = client.groups.serverGroup((e as permissions.find.ServerGroup).group_id); - if(!group) continue; - - groups.push({ - name: group.name, - value: e.value, - id: group.id - }); - } - - event_registry.fire("group_permissions", { - status: "success", - groups: groups, - permission_name: event.permission_name - }); - }).catch(error => { - event_registry.fire("group_permissions", { - status: "error", - error_msg: error_msg(error), - permission_name: event.permission_name - }); - log.error(LogCategory.CLIENT, tr("Failed to execute permfind for permission %s: %o"), event.permission_name, error); - }); - }); - } - } - - function dummy_controller(event_registry: events.Registry) { - /* settings */ - { - event_registry.on("query_bot_status", event => { - setTimeout(() => { - event_registry.fire("bot_status", { - status: "success", - data: { - name: "Another TeaSpeak bot", - country_code: "DE", - default_country_code: "GB", - channel_commander: false, - description: "Hello World", - priority_speaker: true, - volume: 66, - - uptime_mode: 0, - client_version: "Version", - client_platform: "Platform", - bot_type: 0 - } - }) - }); - }); - - - event_registry.on("query_playlist_status", event => { - setTimeout(() => { - event_registry.fire("playlist_status", { - status: "success", - data: { - max_size: 55, - notify_song_change: true, - delete_played: false, - finished: false, - replay_mode: 2 - } - }) - }); - }); - } - - /* permissions */ - { - event_registry.on("query_special_clients", event => { - setTimeout(() => { - event_registry.fire("special_client_list", { - status: "success", - clients: [{ - name: "WolverinDEV", - database_id: 1, - unique_id: "abd" - }, { - name: "WolverinDEV 2", - database_id: 2, - unique_id: "abd1" - }, { - name: "WolverinDEV 3", - database_id: 3, - unique_id: "abd1" - }] - }); - }, 0); - }); - - event_registry.on("query_group_permissions", event => { - setTimeout(() => { - event_registry.fire("group_permissions", { - status: "success", - groups: [{ - value: 20, - name: "Server Admin p:20", - id: 0 - }, { - value: 10, - name: "Server Mod p:10", - id: 0 - }], - permission_name: event.permission_name - }); - }, 0); - }); - - event_registry.on("query_general_permissions", event => { - setTimeout(() => { - event_registry.fire("general_permissions", { - status: "success", - permissions: { - i_playlist_song_needed_add_power: 77 - } - }) - }, 0); - }); - - event_registry.on("set_general_permission", event => { - setTimeout(() => { - event_registry.fire("set_general_permission_result", { - key: event.key, - value: event.value, - status: "success" - }); - }); - }); - - event_registry.on("query_client_permissions", event => { - setTimeout(() => { - event_registry.fire("client_permissions", { - client_database_id: event.client_database_id, - status: "success", - permissions: { - i_playlist_song_needed_add_power: 77 - } - }) - }, 500); - }); - - event_registry.on("set_client_permission", event => { - setTimeout(() => { - event_registry.fire("set_client_permission_result", { - key: event.key, - client_database_id: event.client_database_id, - status: "success", - value: event.value - }) - }, 500); - }); - } - } - - - function build_modal(event_registry: events.Registry) : JQuery { - const tag = $("#tmpl_music_manage").renderTag(); - - const container_settings = tag.find(".body > .category-settings"); - build_settings_container(event_registry, container_settings); - - const container_permissions = tag.find(".body > .category-permissions"); - build_permission_container(event_registry, container_permissions); - - /* general switch */ - { - let shown_container: "settings" | "permissions"; - - const header = tag.find(".header"); - - const category_permissions = header.find(".category-permissions"); - event_registry.on("show_container", data => { - category_permissions.toggleClass("selected", data.container === "permissions"); - container_permissions.toggleClass("hidden", data.container !== "permissions"); - }); - category_permissions.on('click', event => { - if(shown_container === "permissions") return; - event_registry.fire("show_container", { container: "permissions" }); - }); - - const category_settings = header.find(".category-settings"); - event_registry.on("show_container", data => { - category_settings.toggleClass("selected", data.container === "settings"); - container_settings.toggleClass("hidden", data.container !== "settings"); - }); - category_settings.on('click', event => { - if(shown_container === "settings") return; - event_registry.fire("show_container", { container: "settings" }); - }); - - event_registry.on("show_container", data => shown_container = data.container); - } - - /* input length fix */ - tag.find("input[maxlength]").on("input", event => { - const input = event.target as HTMLInputElement; - const max = parseInt(input.getAttribute("maxlength")); - const text = input.value; - if(!isNaN(max) && text && text.length > max) - //input.value = text.substr(text.length - max); - input.value = text.substr(0, max); }); - /* initialize */ - event_registry.fire("show_container", { container: "settings" }); - return tag.children(); + event_registry.on("query_bot_status", event => { + setTimeout(() => { + event_registry.fire("bot_status", { + status: "success", + data: { + channel_commander: bot.properties.client_is_channel_commander, + volume: bot.properties.player_volume, + description: bot.properties.client_description, + default_country_code: ( + !bot.channelTree ? undefined : + !bot.channelTree.server ? undefined : bot.channelTree.server.properties.virtualserver_country_code) || "DE", + country_code: bot.properties.client_country, + name: bot.properties.client_nickname, + priority_speaker: bot.properties.client_is_priority_speaker, + + bot_type: bot.properties.client_bot_type, + client_platform: bot.properties.client_platform, + client_version: bot.properties.client_version, + uptime_mode: bot.properties.client_uptime_mode + } + }); + }, 0); + }); + + event_registry.on("set_bot_status", event => { + const property_map = { + "channel_commander": "client_is_channel_commander", + "volume": "player_volume", + "description": "client_description", + "country_code": "client_country", + "name": "client_nickname", + "priority_speaker": "client_is_priority_speaker", + + "bot_type": "client_bot_type", + "client_platform": "client_platform", + "client_version": "client_version", + "uptime_mode": "client_uptime_mode" + }; + + + Promise.resolve().then(() => { + const property = property_map[event.key]; + if(!property) return Promise.reject(tr("unknown property")); + + const data = { + clid: bot.clientId() + }; + data[property] = event.value; + return client.serverConnection.send_command("clientedit", data); + }).then(() => { + event_registry.fire("set_bot_status_result", { + status: "success", + key: event.key, + value: event.value + }); + }).catch(error => { + event_registry.fire("set_bot_status_result", { + status: "error", + key: event.key, + error_msg: error_msg(error) + }); + log.error(LogCategory.CLIENT, tr("Failed to change bot setting %s: %o"), event.key, error); + }); + }); } - function build_settings_container(event_registry: events.Registry, tag: JQuery) { - const show_change_error = (header, message) => { - createErrorModal(tr("Failed to change value"), header + "
" + message).open(); - }; - - /* music bot settings */ - { - const container = tag.find(".settings-bot"); - - /* bot name */ - { - const input = container.find(".option-bot-name"); - let last_value = undefined; - - event_registry.on("query_bot_status", event => { - last_value = undefined; - input - .prop("disabled", true) - .val(null) - .attr("placeholder", tr("loading...")); + /* permissions */ + { + event_registry.on("query_general_permissions", event => { + const playlist_id = bot.properties.client_playlist_id; + client.permissions.requestPlaylistPermissions(playlist_id).then(result => { + const permissions = {}; + for(const permission of result) + if(permission.hasValue()) + permissions[permission.type.name] = permission.value; + event_registry.fire("general_permissions", { + status: "success", + permissions: permissions }); - - event_registry.on("bot_status", event => { - if(event.status === "error") - input - .prop("disabled", true) - .val(null) - .attr("placeholder", event.error_msg || tr("error while loading")); - else - input - .prop("disabled", false) - .attr("placeholder", null) - .val(last_value = event.data.name); + }).catch(error => { + event_registry.fire("general_permissions", { + status: "error", + error_msg: error_msg(error) }); - - event_registry.on("set_bot_status_result", event => { - if(event.key !== "name") return; - - if(event.status !== "success") - show_change_error(tr("Failed to set bot name"), event.error_msg || tr("timeout")); - else - last_value = event.value; - - input - .prop("disabled", false) - .attr("placeholder", null) - .val(last_value); - }); - - input.on("keyup", event => event.key === "Enter" && input.trigger("focusout")); - input.on("focusout", event => { - const value = input.val() as string; - if(value === last_value) return; - if(!value) { - input.val(last_value); - return; - } - - input - .prop("disabled", true) - .val(null) - .attr("placeholder", tr("applying...")); - - event_registry.fire("set_bot_status", { - key: "name", - value: value - }); - }); - } - - /* country flag */ - { - const input = container.find(".option-bot-country"); - const flag = container.find(".container-country .country"); - let last_value = undefined, fallback_country = undefined; - - const update_country_code = input => { - input = input || fallback_country || "ts"; - flag.each((_, e) => { - for(const [index, klass] of e.classList.entries()) - if(klass.startsWith("flag-")) - e.classList.remove(klass); - }); - flag.addClass("flag-" + input.toLowerCase()); - flag.attr("title", i18n.country_name(input, tr("Unknown country"))); - }; - - event_registry.on("query_bot_status", event => { - last_value = undefined; - input - .prop("disabled", true) - .val(null) - .attr("placeholder", "..."); - update_country_code("ts"); - }); - - event_registry.on("bot_status", event => { - if(event.status === "error") - input - .prop("disabled", true) - .val(null) - .attr("placeholder", "err"); - else { - input - .prop("disabled", false) - .attr("placeholder", null) - .val(last_value = event.data.country_code); - fallback_country = event.data.default_country_code; - } - update_country_code(last_value); - }); - - event_registry.on("set_bot_status_result", event => { - if(event.key !== "country_code") return; - - if(event.status !== "success") - show_change_error(tr("Failed to set bots country"), event.error_msg || tr("timeout")); - else - last_value = event.value; - - input - .prop("disabled", false) - .attr("placeholder", null) - .val(last_value); - update_country_code(last_value); - }); - - input.on("input", () => { - update_country_code(input.val()); - input.firstParent(".input-boxed").removeClass("is-invalid"); - }); - - input.on("keyup", event => event.key === "Enter" && input.trigger("focusout")); - input.on("focusout", event => { - const value = input.val() as string; - if(value === last_value) return; - if(value && value.length != 2) { - input.firstParent(".input-boxed").addClass("is-invalid"); - return; - } - - input - .prop("disabled", true) - .val(null) - .attr("placeholder", "..."); - - event_registry.fire("set_bot_status", { - key: "country_code", - value: value - }); - }); - } - - /* flag channel commander */ - { - const input = container.find(".option-channel-commander") as JQuery; - const label = input.parents("label"); - - let last_value = undefined; - - event_registry.on("query_bot_status", event => { - last_value = undefined; - - label.addClass("disabled"); - input - .prop("checked", false) - .prop("disabled", true); - }); - - event_registry.on("bot_status", event => { - if(event.status === "error") { - label.addClass("disabled"); - input - .prop("checked", false) - .prop("disabled", true); - } else { - label.removeClass("disabled"); - input - .prop("checked", last_value = event.data.channel_commander) - .prop("disabled", false); - } - }); - - event_registry.on("set_bot_status_result", event => { - if(event.key !== "channel_commander") return; - - if(event.status !== "success") - show_change_error(tr("Failed to change channel commander state"), event.error_msg || tr("timeout")); - else - last_value = event.value; - - label.removeClass("disabled"); - input - .prop("checked", last_value) - .prop("disabled", false); - }); - - input.on("change", event => { - label.addClass("disabled"); - input.prop("disabled", true); - event_registry.fire("set_bot_status", { - key: "channel_commander", - value: input.prop("checked") - }); - }); - } - - /* flag priority speaker */ - { - const input = container.find(".option-priority-speaker") as JQuery; - const label = input.parents("label"); - - let last_value = undefined; - - event_registry.on("query_bot_status", event => { - last_value = undefined; - - label.addClass("disabled"); - input - .prop("checked", false) - .prop("disabled", true); - }); - - event_registry.on("bot_status", event => { - if(event.status === "error") { - label.addClass("disabled"); - input - .prop("checked", false) - .prop("disabled", true); - } else { - label.removeClass("disabled"); - input - .prop("checked", last_value = event.data.priority_speaker) - .prop("disabled", false); - } - }); - - event_registry.on("set_bot_status_result", event => { - if(event.key !== "priority_speaker") return; - - if(event.status !== "success") - show_change_error(tr("Failed to change priority speaker state"), event.error_msg || tr("timeout")); - else - last_value = event.value; - - label.removeClass("disabled"); - input - .prop("checked", last_value) - .prop("disabled", false); - }); - - input.on("change", event => { - label.addClass("disabled"); - input.prop("disabled", true); - event_registry.fire("set_bot_status", { - key: "priority_speaker", - value: input.prop("checked") - }); - }); - } - - /* status load timeout */ - { - let timeout; - event_registry.on("query_bot_status", event => { - timeout = setTimeout(() => { - event_registry.fire("bot_status", { - status: "error", - error_msg: tr("load timeout") - }); - }, 5000); - }); - - event_registry.on("bot_status", event => clearTimeout(timeout)); - } - - /* set status timeout */ - { - let timeouts: {[key: string]:any} = {}; - event_registry.on("set_bot_status", event => { - clearTimeout(timeouts[event.key]); - timeouts[event.key] = setTimeout(() => { - event_registry.fire("set_bot_status_result", { - status: "timeout", - key: event.key, - }); - }, 5000); - }); - - event_registry.on("set_bot_status_result", event => { - clearTimeout(timeouts[event.key]); - delete timeouts[event.key]; - }); - } - } - - /* music bot settings */ - { - const container = tag.find(".settings-playlist"); - - /* playlist replay mode */ - { - const input = container.find(".option-replay-mode") as JQuery; - let last_value = undefined; - - const update_value = text => { - if(text) { - input.prop("disabled", true).addClass("disabled"); - input.val("-1"); - input.find("option[value=-1]").text(text); - } else if(last_value >= 0 && last_value <= 3) { - input - .prop("disabled", false) - .removeClass("disabled"); - input.val(last_value); - } else { - update_value(tr("invalid value")); - } - }; - - event_registry.on("query_playlist_status", event => { - last_value = undefined; - update_value(tr("loading...")); - }); - - event_registry.on("playlist_status", event => { - if(event.status === "error") { - update_value(event.error_msg || tr("error while loading")); - } else { - last_value = event.data.replay_mode; - update_value(undefined); - } - }); - - event_registry.on("set_playlist_status_result", event => { - if(event.key !== "replay_mode") return; - - if(event.status !== "success") - show_change_error(tr("Failed to change replay mode"), event.error_msg || tr("timeout")); - else - last_value = event.value; - update_value(undefined); - }); - - input.on("keyup", event => event.key === "Enter" && input.trigger("focusout")); - input.on("change", event => { - const value = parseInt(input.val() as string); - console.log(value); - if(isNaN(value)) return; - - update_value(tr("applying...")); - event_registry.fire("set_playlist_status", { - key: "replay_mode", - value: value - }); - }); - } - - /* playlist max size */ - { - const input = container.find(".container-max-playlist-size input"); - let last_value = undefined; - - event_registry.on("query_playlist_status", event => { - last_value = undefined; - input - .prop("disabled", true) - .val(null) - .attr("placeholder", tr("loading...")) - .firstParent(".input-boxed").addClass("disabled"); - }); - - event_registry.on("playlist_status", event => { - if(event.status === "error") - input - .prop("disabled", true) - .val(null) - .attr("placeholder", event.error_msg || tr("error while loading")) - .firstParent(".input-boxed").addClass("disabled"); - else - input - .prop("disabled", false) - .attr("placeholder", null) - .val((last_value = event.data.max_size).toString()) - .firstParent(".input-boxed").removeClass("disabled"); - }); - - event_registry.on("set_playlist_status_result", event => { - if(event.key !== "max_size") return; - - if(event.status !== "success") - show_change_error(tr("Failed to change max playlist size"), event.error_msg || tr("timeout")); - else - last_value = event.value; - - input - .prop("disabled", false) - .attr("placeholder", null) - .val(last_value) - .firstParent(".input-boxed").removeClass("disabled"); - }); - - input.on("input", event => input.parentsUntil(".input-boxed").removeClass("is-invalid")); - input.on("keyup", event => event.key === "Enter" && input.trigger("focusout")); - input.on("focusout", event => { - const value = input.val() as string; - if(value === last_value) return; - if(value === "") { - input.val(last_value); - return; - } - if(isNaN(parseInt(value))) { - input.parentsUntil(".input-boxed").addClass("is-invalid"); - return; - } - - input - .prop("disabled", true) - .val(null) - .attr("placeholder", tr("applying...")) - .firstParent(".input-boxed").addClass("disabled"); - - event_registry.fire("set_playlist_status", { - key: "max_size", - value: parseInt(value) - }); - }); - } - - /* flag delete played */ - { - const input = container.find(".option-delete-played-songs") as JQuery; - const label = input.parents("label"); - - let last_value = undefined; - - event_registry.on("query_playlist_status", event => { - last_value = undefined; - - label.addClass("disabled"); - input - .prop("checked", false) - .prop("disabled", true); - }); - - event_registry.on("playlist_status", event => { - if(event.status === "error") { - label.addClass("disabled"); - input - .prop("checked", false) - .prop("disabled", true); - } else { - label.removeClass("disabled"); - input - .prop("checked", last_value = event.data.delete_played) - .prop("disabled", false); - } - }); - - event_registry.on("set_playlist_status_result", event => { - if(event.key !== "delete_played") return; - - if(event.status !== "success") - show_change_error(tr("Failed to change delete state"), event.error_msg || tr("timeout")); - else - last_value = event.value; - - label.removeClass("disabled"); - input - .prop("checked", last_value) - .prop("disabled", false); - }); - - input.on("change", event => { - label.addClass("disabled"); - input.prop("disabled", true); - event_registry.fire("set_playlist_status", { - key: "delete_played", - value: input.prop("checked") - }); - }); - } - - /* flag notify song change */ - { - const input = container.find(".option-notify-songs-change") as JQuery; - const label = input.parents("label"); - - let last_value = undefined; - - event_registry.on("query_playlist_status", event => { - last_value = undefined; - - label.addClass("disabled"); - input - .prop("checked", false) - .prop("disabled", true); - }); - - event_registry.on("playlist_status", event => { - if(event.status === "error") { - label.addClass("disabled"); - input - .prop("checked", false) - .prop("disabled", true); - } else { - label.removeClass("disabled"); - input - .prop("checked", last_value = event.data.notify_song_change) - .prop("disabled", false); - } - }); - - event_registry.on("set_playlist_status_result", event => { - if(event.key !== "notify_song_change") return; - - if(event.status !== "success") - show_change_error(tr("Failed to change notify state"), event.error_msg || tr("timeout")); - else - last_value = event.value; - - label.removeClass("disabled"); - input - .prop("checked", last_value) - .prop("disabled", false); - }); - - input.on("change", event => { - label.addClass("disabled"); - input.prop("disabled", true); - event_registry.fire("set_playlist_status", { - key: "notify_song_change", - value: input.prop("checked") - }); - }); - } - - /* status load timeout */ - { - let timeout; - event_registry.on("query_playlist_status", event => { - timeout = setTimeout(() => { - event_registry.fire("playlist_status", { - status: "error", - error_msg: tr("load timeout") - }); - }, 5000); - }); - - event_registry.on("playlist_status", event => clearTimeout(timeout)); - } - - /* set status timeout */ - { - let timeouts: {[key: string]:any} = {}; - event_registry.on("set_playlist_status", event => { - clearTimeout(timeouts[event.key]); - timeouts[event.key] = setTimeout(() => { - event_registry.fire("set_playlist_status_result", { - status: "timeout", - key: event.key, - }); - }, 5000); - }); - - event_registry.on("set_playlist_status_result", event => { - clearTimeout(timeouts[event.key]); - delete timeouts[event.key]; - }); - } - } - - /* reload button */ - { - const button = tag.find(".button-reload"); - let timeout; - - event_registry.on(["query_bot_status", "query_playlist_status"], event => { - button.prop("disabled", true); - - clearTimeout(timeout); - timeout = setTimeout(() => { - button.prop("disabled", false); - }, 1000); + log.error(LogCategory.CLIENT, tr("Failed to query playlist general permissions for playlist %d: %o"), playlist_id, error); }); + }); - button.on("click", event => { - event_registry.fire("query_bot_status"); - event_registry.fire("query_playlist_status"); + event_registry.on("set_general_permission", event => { + const playlist_id = bot.properties.client_playlist_id; + + client.serverConnection.send_command("playlistaddperm", { + playlist_id: playlist_id, + permsid: event.key, + permvalue: event.value, + permskip: false, + permnegated: false + }).then(() => { + event_registry.fire("set_general_permission_result", { + key: event.key, + status: "success", + value: event.value + }); + }).catch(error => { + event_registry.fire("set_general_permission_result", { + status: "error", + key: event.key, + error_msg: error_msg(error) + }); + log.error(LogCategory.CLIENT, tr("Failed to set playlist general permissions for playlist %d and permission %d: %o"), playlist_id, event.key, error); }); - } + }); - tooltip(tag); - - /* initialize on show */ - { - let initialized = false; - event_registry.on("show_container", event => { - if(event.container !== "settings" || initialized) return; - initialized = true; - - event_registry.fire("query_bot_status"); - event_registry.fire("query_playlist_status"); + event_registry.on("query_client_permissions", event => { + const playlist_id = bot.properties.client_playlist_id; + const client_id = event.client_database_id; + client.permissions.requestPlaylistClientPermissions(playlist_id, client_id).then(result => { + const permissions = {}; + for(const permission of result) + if(permission.hasValue()) + permissions[permission.type.name] = permission.value; + event_registry.fire("client_permissions", { + status: "success", + client_database_id: event.client_database_id, + permissions: permissions + }); + }).catch(error => { + event_registry.fire("client_permissions", { + status: "error", + client_database_id: event.client_database_id, + error_msg: error_msg(error) + }); + log.error(LogCategory.CLIENT, tr("Failed to query playlist client permissions for playlist %d and client %d: %o"), playlist_id, client_id, error); }); - } + }); + + event_registry.on("set_client_permission", event => { + const playlist_id = bot.properties.client_playlist_id; + const client_id = event.client_database_id; + + client.serverConnection.send_command("playlistclientaddperm", { + playlist_id: playlist_id, + cldbid: client_id, + + permsid: event.key, + permvalue: event.value, + permskip: false, + permnegated: false + }).then(() => { + event_registry.fire("set_client_permission_result", { + key: event.key, + status: "success", + client_database_id: client_id, + value: event.value + }); + }).catch(error => { + event_registry.fire("set_client_permission_result", { + status: "error", + key: event.key, + client_database_id: client_id, + error_msg: error_msg(error) + }); + log.error(LogCategory.CLIENT, tr("Failed to set playlist client permissions for playlist %d, permission %d and client id %d: %o"), playlist_id, event.key, client_id, error); + }); + }); + + event_registry.on("query_special_clients", event => { + const playlist_id = bot.properties.client_playlist_id; + client.serverConnection.command_helper.request_playlist_client_list(playlist_id).then(clients => { + return client.serverConnection.command_helper.info_from_cldbid(...clients); + }).then(clients => { + event_registry.fire("special_client_list", { + status: "success", + clients: clients.map(e => { + return { + name: e.client_nickname, + unique_id: e.client_unique_id, + database_id: e.client_database_id + } + }) + }); + }).catch(error => { + event_registry.fire("special_client_list", { + status: "error", + error_msg: error_msg(error) + }); + log.error(LogCategory.CLIENT, tr("Failed to query special client list for playlist %d: %o"), playlist_id, error); + }) + }); + + event_registry.on("search_client", event => { + if(!event.text) return; + + const text = event.text; + Promise.resolve().then(() => { + let is_uuid = false; + try { + is_uuid = atob(text).length === 32; + } catch(e) {} + if(is_uuid) { + return client.serverConnection.command_helper.info_from_uid(text); + } else if(text.match(/^[0-9]{1,7}$/) && !isNaN(parseInt(text))) { + return client.serverConnection.command_helper.info_from_cldbid(parseInt(text)); + } else { + //TODO: Database name lookup? + return Promise.reject("no results"); + } + }).then(result => { + if(result.length) { + const client = result[0]; + event_registry.fire("search_client_result", { + status: "success", + client: { + name: client.client_nickname, + unique_id: client.client_unique_id, + database_id: client.client_database_id + } + }); + } else { + event_registry.fire("search_client_result", { + status: "empty" + }); + } + }).catch(error => { + event_registry.fire("search_client_result", { + status: "error", + error_msg: error_msg(error) + }); + log.error(LogCategory.CLIENT, tr("Failed to lookup search text \"%s\": %o"), text, error); + }); + }); + + event_registry.on("query_group_permissions", event => { + client.permissions.find_permission(event.permission_name).then(result => { + let groups = []; + for(const e of result) { + if(e.type !== "server_group") continue; + + const group = client.groups.serverGroup((e as ServerGroup).group_id); + if(!group) continue; + + groups.push({ + name: group.name, + value: e.value, + id: group.id + }); + } + + event_registry.fire("group_permissions", { + status: "success", + groups: groups, + permission_name: event.permission_name + }); + }).catch(error => { + event_registry.fire("group_permissions", { + status: "error", + error_msg: error_msg(error), + permission_name: event.permission_name + }); + log.error(LogCategory.CLIENT, tr("Failed to execute permfind for permission %s: %o"), event.permission_name, error); + }); + }); + } +} + +function dummy_controller(event_registry: Registry) { + /* settings */ + { + event_registry.on("query_bot_status", event => { + setTimeout(() => { + event_registry.fire("bot_status", { + status: "success", + data: { + name: "Another TeaSpeak bot", + country_code: "DE", + default_country_code: "GB", + channel_commander: false, + description: "Hello World", + priority_speaker: true, + volume: 66, + + uptime_mode: 0, + client_version: "Version", + client_platform: "Platform", + bot_type: 0 + } + }) + }); + }); + + + event_registry.on("query_playlist_status", event => { + setTimeout(() => { + event_registry.fire("playlist_status", { + status: "success", + data: { + max_size: 55, + notify_song_change: true, + delete_played: false, + finished: false, + replay_mode: 2 + } + }) + }); + }); } - function build_permission_container(event_registry: events.Registry, tag: JQuery) { - /* client search mechanism */ - { - const container = tag.find(".table-head .column-client-specific .client-select"); - let list_shown = false; - - /* search list show/hide */ - { - const button_list_clients = container.find(".button-list-clients"); - button_list_clients.on('click', event => - event_registry.fire(list_shown ? "hide_client_list" : "show_client_list")); - - event_registry.on("show_client_list", () => { - list_shown = true; - button_list_clients.text(tr("Hide clients")); + /* permissions */ + { + event_registry.on("query_special_clients", event => { + setTimeout(() => { + event_registry.fire("special_client_list", { + status: "success", + clients: [{ + name: "WolverinDEV", + database_id: 1, + unique_id: "abd" + }, { + name: "WolverinDEV 2", + database_id: 2, + unique_id: "abd1" + }, { + name: "WolverinDEV 3", + database_id: 3, + unique_id: "abd1" + }] }); + }, 0); + }); - event_registry.on("hide_client_list", () => { - list_shown = false; - button_list_clients.text(tr("List clients")); + event_registry.on("query_group_permissions", event => { + setTimeout(() => { + event_registry.fire("group_permissions", { + status: "success", + groups: [{ + value: 20, + name: "Server Admin p:20", + id: 0 + }, { + value: 10, + name: "Server Mod p:10", + id: 0 + }], + permission_name: event.permission_name }); - } + }, 0); + }); - /* the search box */ - { - const input_search = container.find(".input-search"); - const button_search = container.find(".button-search"); - - let search_timeout; - let last_query; - input_search.on('keyup', event => { - const text = input_search.val() as string; - if(text === last_query) return; - - if(text) - event_registry.fire("filter_client_list", { filter: text }); - else - event_registry.fire("filter_client_list", { filter: undefined }); - - input_search.toggleClass("is-invalid", !list_shown && text === last_query); - if(!list_shown) { - button_search.prop("disabled", !text || !!search_timeout); - } else { - last_query = text; + event_registry.on("query_general_permissions", event => { + setTimeout(() => { + event_registry.fire("general_permissions", { + status: "success", + permissions: { + i_playlist_song_needed_add_power: 77 } + }) + }, 0); + }); + + event_registry.on("set_general_permission", event => { + setTimeout(() => { + event_registry.fire("set_general_permission_result", { + key: event.key, + value: event.value, + status: "success" }); + }); + }); - input_search.on('keydown', event => { - if(event.key === "Enter" && !list_shown && !button_search.prop("disabled")) - button_search.trigger("click"); - }); - - event_registry.on("show_client_list", () => { - button_search.prop("disabled", true); - input_search.attr("placeholder", tr("Search client list")); - }); - - event_registry.on("hide_client_list", () => { - button_search.prop("disabled", !input_search.val() || !!search_timeout); - input_search.attr("placeholder", tr("Client uid or database id")); - }); - - button_search.on("click", event => { - button_search.prop("disabled", true); - - input_search.blur(); - const text = input_search.val() as string; - last_query = text; - event_registry.fire("search_client", { - text: text - }); - search_timeout = setTimeout(() => event_registry.fire("search_client_result", { - status: "timeout" - }), 5000); - }); - - event_registry.on("search_client_result", event => { - clearTimeout(search_timeout); - search_timeout = 0; - - button_search.prop("disabled", !input_search.val()); - if(event.status === "timeout") { - createErrorModal(tr("Client search failed"), tr("Failed to perform client search.
Search resulted in a timeout.")).open(); - return; - } else if(event.status === "error" || event.status === "empty") { - //TODO: Display the error somehow? - input_search.addClass("is-invalid"); - return; - } else { - event_registry.fire("special_client_set", { - client: event.client - }); + event_registry.on("query_client_permissions", event => { + setTimeout(() => { + event_registry.fire("client_permissions", { + client_database_id: event.client_database_id, + status: "success", + permissions: { + i_playlist_song_needed_add_power: 77 } - }); - } + }) + }, 500); + }); - /* the client list */ - { - const container = tag.find(".overlay-client-list"); - - event_registry.on("show_client_list", () => container.removeClass("hidden")); - event_registry.on("hide_client_list", () => container.addClass("hidden")); - - const button_refresh = container.find(".button-clientlist-refresh"); - - const container_entries = container.find(".container-client-list"); - event_registry.on("special_client_list", data => { - button_refresh.prop("disabled", false); - container.find(".overlay").addClass("hidden"); - if(data.status === "error-permission") { - const overlay = container.find(".overlay-query-error-permissions"); - overlay.find("a").text(tr("Insufficient permissions")); - overlay.removeClass("hidden"); - } else if(data.status === "success") { - container_entries.find(".client").remove(); /* clear */ - - if(!data.clients.length) { - const overlay = container.find(".overlay-empty-list"); - overlay.removeClass("hidden"); - } else { - for(const client of data.clients) { - const tag = $.spawn("div").addClass("client").append( - htmltags.generate_client_object({ - add_braces: false, - client_id: 0, - client_database_id: client.database_id, - client_name: client.name, - client_unique_id: client.unique_id - }) - ); - tag.on('dblclick', event => event_registry.fire("special_client_set", { client: client })); - tag.attr("x-filter", client.database_id + "_" + client.name + "_" + client.unique_id); - container_entries.append(tag); - } - } - } else { - const overlay = container.find(".overlay-query-error"); - overlay.find("a").text(data.error_msg ? data.error_msg : tr("query failed")); - overlay.removeClass("hidden"); - } - }); - - /* refresh button */ - button_refresh.on('click', event => { - button_refresh.prop("disabled", true); - event_registry.fire("query_special_clients"); - }); + event_registry.on("set_client_permission", event => { + setTimeout(() => { + event_registry.fire("set_client_permission_result", { + key: event.key, + client_database_id: event.client_database_id, + status: "success", + value: event.value + }) + }, 500); + }); + } +} - /* special client list query timeout handler */ - { - let query_timeout; - event_registry.on("query_special_clients", event => { - query_timeout = setTimeout(() => { - event_registry.fire("special_client_list", { - status: "error", - error_msg: tr("Query timeout") - }); - }, 5000); - }); +function build_modal(event_registry: Registry) : JQuery { + const tag = $("#tmpl_music_manage").renderTag(); - event_registry.on("special_client_list", event => clearTimeout(query_timeout)); - } + const container_settings = tag.find(".body > .category-settings"); + build_settings_container(event_registry, container_settings); - /* first time client list show */ - { - let shown; - event_registry.on('show_client_list', event => { - if(shown) return; - shown = true; + const container_permissions = tag.find(".body > .category-permissions"); + build_permission_container(event_registry, container_permissions); - event_registry.fire("query_special_clients"); - }); - } + /* general switch */ + { + let shown_container: "settings" | "permissions"; - /* the client list filter */ - { - let filter; + const header = tag.find(".header"); - const overlay = container.find(".overlay-filter-no-result"); - const update_filter = () => { - let shown = 0, hidden = 0; - container_entries.find(".client").each(function () { - const text = this.getAttribute("x-filter"); - if(!filter || text.toLowerCase().indexOf(filter) != -1) { - this.classList.remove("hidden"); - shown++; - } else { - this.classList.add("hidden"); - hidden++; - } - }); - if(shown == 0 && hidden == 0) return; - overlay.toggleClass("hidden", shown != 0); - }; + const category_permissions = header.find(".category-permissions"); + event_registry.on("show_container", data => { + category_permissions.toggleClass("selected", data.container === "permissions"); + container_permissions.toggleClass("hidden", data.container !== "permissions"); + }); + category_permissions.on('click', event => { + if(shown_container === "permissions") return; + event_registry.fire("show_container", { container: "permissions" }); + }); - event_registry.on("special_client_list", event => update_filter()); - event_registry.on("filter_client_list", event => { - filter = (event.filter || "").toLowerCase(); - update_filter(); - }); - } - } + const category_settings = header.find(".category-settings"); + event_registry.on("show_container", data => { + category_settings.toggleClass("selected", data.container === "settings"); + container_settings.toggleClass("hidden", data.container !== "settings"); + }); + category_settings.on('click', event => { + if(shown_container === "settings") return; + event_registry.fire("show_container", { container: "settings" }); + }); - event_registry.on("special_client_set", event => { - container.toggleClass("hidden", !!event.client); - event_registry.fire("hide_client_list"); - }); - } + event_registry.on("show_container", data => shown_container = data.container); + } - /* the client info */ + /* input length fix */ + tag.find("input[maxlength]").on("input", event => { + const input = event.target as HTMLInputElement; + const max = parseInt(input.getAttribute("maxlength")); + const text = input.value; + if(!isNaN(max) && text && text.length > max) + //input.value = text.substr(text.length - max); + input.value = text.substr(0, max); + }); + + /* initialize */ + event_registry.fire("show_container", { container: "settings" }); + return tag.children(); +} + +function build_settings_container(event_registry: Registry, tag: JQuery) { + const show_change_error = (header, message) => { + createErrorModal(tr("Failed to change value"), header + "
" + message).open(); + }; + + /* music bot settings */ + { + const container = tag.find(".settings-bot"); + + /* bot name */ { - const container = tag.find(".table-head .column-client-specific .client-info"); + const input = container.find(".option-bot-name"); + let last_value = undefined; - container.find(".button-client-deselect").on("click", event => { - event_registry.fire("special_client_set", { client: undefined }); + event_registry.on("query_bot_status", event => { + last_value = undefined; + input + .prop("disabled", true) + .val(null) + .attr("placeholder", tr("loading...")); }); - event_registry.on("special_client_set", event => { - container.toggleClass("hidden", !event.client); - - const client_container = container.find(".container-selected-client"); - client_container.find(".htmltag-client").remove(); - if(event.client) { - client_container.append(htmltags.generate_client_object({ - client_unique_id: event.client.unique_id, - client_name: event.client.name, - client_id: 0, - client_database_id: event.client.database_id, - add_braces: false - })); - } - }); - } - - const power_needed_map = { - i_client_music_rename_power: "i_client_music_needed_rename_power", - i_client_music_modify_power: "i_client_music_needed_modify_power", - i_client_music_delete_power: "i_client_music_needed_delete_power", - i_playlist_view_power: "i_playlist_needed_view_power", - i_playlist_modify_power: "i_playlist_needed_modify_power", - i_playlist_permission_modify_power: "i_playlist_needed_permission_modify_power", - i_playlist_song_add_power: "i_playlist_song_needed_add_power", - i_playlist_song_move_power: "i_playlist_song_needed_move_power", - i_playlist_song_remove_power: "i_playlist_song_needed_remove_power", - b_virtualserver_playlist_permission_list: "b_virtualserver_playlist_permission_list" - }; - const needed_power_map = Object.entries(power_needed_map).reduce((ret, entry) => { - const [key, value] = entry; - ret[value] = key; - return ret; - }, {}); - - /* general permissions */ - { - /* permission input functionality */ - { - tag.find(".general-permission").each((_, _e) => { - const elem = $(_e) as JQuery; - - const permission_name = elem.attr("x-permission"); - if(!permission_name) return; - - const input = elem.find("input"); - input.attr("maxlength", 6); - - let last_sync_value = undefined; - - event_registry.on("query_general_permissions", event => { - input.prop("disabled", true).val(null); - input.attr("placeholder", tr("loading...")); - }); - - event_registry.on("general_permissions", event => { - input.prop("disabled", true).val(null); - if(event.status === "timeout") { - input.attr("placeholder", tr("load timeout")); - } else if(event.status === "success") { - input.prop("disabled", false); //TODO: Check permissions? - input.attr("placeholder", null); - const value = event.permissions ? event.permissions[permission_name] || 0 : 0; - last_sync_value = value; - input.val(value); - } else { - input.attr("placeholder", event.error_msg || tr("load error")); - } - }); - - event_registry.on("set_general_permission_result", event => { - if(event.key !== permission_name) return; - - input.prop("disabled", false); //TODO: Check permissions? - input.attr("placeholder", null); - if(event.status === "success") { - input.val(event.value); - last_sync_value = event.value; - } else if(event.status === "error") { - if(typeof last_sync_value === "number") input.val(last_sync_value); - createErrorModal(tr("Failed to change permission"), tra("Failed to change permission:{:br:}{}", event.error_msg)).open(); - } - }); - - input.on("focusout", event => { - if(input.prop("disabled")) return; - - const value = parseInt(input.val() as string); - if(value === last_sync_value) return; - - input.prop("disabled", true).val(null); - input.attr("placeholder", tr("applying...")); - event_registry.fire("set_general_permission", { - key: permission_name, - value: value || 0 - }); - }); - input.on("keyup", event => event.key === "Enter" && input.blur()); - }); - } - - /* the tooltip functionality */ - { - tag.find(".general-permission").each((_, _e) => { - const elem = $(_e) as JQuery; - - const permission_name = elem.attr("x-permission"); - if(!permission_name) return; - - const required_power = needed_power_map[permission_name]; - if(!required_power) return; - - let last_sync_value = undefined; - let current_tag: JQuery; - - let loading = false; - let query_result: { - status: "error" | "timeout" | "success" - groups?: { - name: string, - value: number, - id: number - }[], - error_msg?: string - }; - - event_registry.on("general_permissions", event => { - if(event.status === "success") - last_sync_value = event.permissions ? event.permissions[permission_name] || 0 : 0; - }); - - event_registry.on("set_general_permission_result", event => { - if(event.key !== permission_name) return; - - if(event.status === "success") - last_sync_value = event.value; - }); - - event_registry.on("refresh_permissions", event => { - query_result = undefined; /* require for the next time */ - }); - - const show_query_result = () => { - if(!current_tag) return; - - const container_groups = current_tag.find(".container-groups"); - container_groups.children().remove(); - current_tag.find(".container-status").addClass("hidden"); - - if(loading) { - current_tag.find(".status-loading").removeClass("hidden"); - } else if(!query_result || query_result.status === "error") { - current_tag - .find(".status-error").removeClass("hidden") - .text((query_result ? query_result.error_msg : "") || tr("failed to query data")); - } else if(query_result.status === "timeout") { - current_tag - .find(".status-error").removeClass("hidden") - .text(tr("timeout while loading")); - } else { - let count = 0; - for(const group of (query_result.groups || [])) { - if(group.value !== -1 && group.value < last_sync_value) continue; - - count++; - container_groups.append($.spawn("div").addClass("group").text( - " - " + group.name + " (" + group.id + ")" - )); - } - - if(count === 0) current_tag.find(".status-no-groups").removeClass("hidden"); - } - }; - - tooltip.initialize(elem, { - on_show(tag: JQuery) { - current_tag = tag; - - if(!query_result && !loading) { - event_registry.fire("query_group_permissions", { - permission_name: required_power - }); - loading = true; - } - show_query_result(); - }, - on_hide(tag: JQuery) { - current_tag = undefined; - } - }); - - event_registry.on("group_permissions", event => { - if(event.permission_name !== required_power) return; - - loading = false; - query_result = event; - show_query_result(); - }); - }); - - - /* refresh mechanism */ - { - event_registry.on("refresh_permissions", event => event_registry.fire("query_general_permissions")); - } - } - - /* permission set timeout */ - { - let permission_timers: {[key: string]:any} = {}; - event_registry.on("set_general_permission", event => { - if(permission_timers[event.key]) - clearTimeout(permission_timers[event.key]); - permission_timers[event.key] = setTimeout(() => { - event_registry.fire("set_general_permission_result", { - key: event.key, - status: "error", - error_msg: tr("controller timeout") - }); - }, 5000); - }); - - event_registry.on("set_general_permission_result", event => { - clearTimeout(permission_timers[event.key]); - delete permission_timers[event.key]; - }); - } - - /* group query timeout */ - { - let timers: {[key: string]:any} = {}; - event_registry.on("query_group_permissions", event => { - if(timers[event.permission_name]) - clearTimeout(timers[event.permission_name]); - timers[event.permission_name] = setTimeout(() => { - event_registry.fire("group_permissions", { - permission_name: event.permission_name, - status: "timeout" - }); - }, 5000); - }); - - event_registry.on("group_permissions", event => { - clearTimeout(timers[event.permission_name]); - delete timers[event.permission_name]; - }); - } - - /* query timeout */ - { - let query_timeout; - event_registry.on("query_general_permissions", event => { - clearTimeout(query_timeout); - query_timeout = setTimeout(() => { - event_registry.fire("general_permissions", { - status: "timeout" - }); - }, 5000); - }); - - event_registry.on("general_permissions", event => clearTimeout(query_timeout)); - } - - /* refresh button */ - { - const button = tag.find(".button-permission-refresh"); - let refresh_timer; - - let loading_client_permissions = false; - let loading_general_permissions = false; - - const update_button = () => - button.prop("disabled", refresh_timer || loading_client_permissions || loading_general_permissions); - - event_registry.on("query_general_permissions", event => { - loading_general_permissions = true; - update_button(); - }); - - event_registry.on("general_permissions", event => { - loading_general_permissions = false; - update_button(); - }); - - event_registry.on("query_client_permissions", event => { - loading_client_permissions = true; - update_button(); - }); - - event_registry.on("client_permissions", event => { - loading_client_permissions = false; - update_button(); - }); - - button.on('click', event => { - event_registry.fire("refresh_permissions"); - - /* allow refreshes only every second */ - refresh_timer = setTimeout(() => { - refresh_timer = undefined; - update_button(); - }, 1000); - }); - } - } - - /* client specific permissions */ - { - const container = tag.find(".column-client-specific"); - - let client_database_id = 0; - let needed_permissions: {[key: string]:number} = {}; - - /* needed permissions updater */ - { - event_registry.on("general_permissions", event => { - if(event.status !== "success") return; - - needed_permissions = event.permissions; - }); - - event_registry.on("set_general_permission_result", event => { - if (event.status !== "success") return; - - needed_permissions[event.key] = event.value; - }); - } - - event_registry.on("special_client_set", event => { - client_database_id = event.client ? event.client.database_id : 0; - container.find(".client-permission").toggleClass("hidden", !event.client); - - if(client_database_id) - event_registry.fire("query_client_permissions", { client_database_id: client_database_id }); + event_registry.on("bot_status", event => { + if(event.status === "error") + input + .prop("disabled", true) + .val(null) + .attr("placeholder", event.error_msg || tr("error while loading")); + else + input + .prop("disabled", false) + .attr("placeholder", null) + .val(last_value = event.data.name); }); - const enabled_class = "client-apply"; - const disabled_class = "client-delete"; + event_registry.on("set_bot_status_result", event => { + if(event.key !== "name") return; - container.find(".client-permission").each((_, _e) => { - const elem = $(_e); + if(event.status !== "success") + show_change_error(tr("Failed to set bot name"), event.error_msg || tr("timeout")); + else + last_value = event.value; - const input = elem.find("input"); - const status_indicator = elem.find(".icon_em"); + input + .prop("disabled", false) + .attr("placeholder", null) + .val(last_value); + }); - const permission_name = elem.attr("x-permission") as string; - const permission_needed_name = power_needed_map[permission_name]; - - let last_sync_value = undefined; - let hide_indicator = false; - - if(typeof permission_needed_name !== "string") { - log.warn(LogCategory.GENERAL, tr("Missing permission needed mapping for %s"), permission_name); + input.on("keyup", event => event.key === "Enter" && input.trigger("focusout")); + input.on("focusout", event => { + const value = input.val() as string; + if(value === last_value) return; + if(!value) { + input.val(last_value); return; } - const update_indicator = () => { - const value = parseInt(input.val() as string); - const needed = typeof needed_permissions[permission_needed_name] === "number" ? needed_permissions[permission_needed_name] : 0; - const flag = value == -1 ? true : isNaN(value) || value == 0 ? false : value >= needed; + input + .prop("disabled", true) + .val(null) + .attr("placeholder", tr("applying...")); - status_indicator.toggle(!hide_indicator); - status_indicator.toggleClass(enabled_class, flag).toggleClass(disabled_class, !flag); + event_registry.fire("set_bot_status", { + key: "name", + value: value + }); + }); + } + + /* country flag */ + { + const input = container.find(".option-bot-country"); + const flag = container.find(".container-country .country"); + let last_value = undefined, fallback_country = undefined; + + const update_country_code = input => { + input = input || fallback_country || "ts"; + flag.each((_, e) => { + for(const [index, klass] of e.classList.entries()) + if(klass.startsWith("flag-")) + e.classList.remove(klass); + }); + flag.addClass("flag-" + input.toLowerCase()); + flag.attr("title", i18nc.country_name(input, tr("Unknown country"))); + }; + + event_registry.on("query_bot_status", event => { + last_value = undefined; + input + .prop("disabled", true) + .val(null) + .attr("placeholder", "..."); + update_country_code("ts"); + }); + + event_registry.on("bot_status", event => { + if(event.status === "error") + input + .prop("disabled", true) + .val(null) + .attr("placeholder", "err"); + else { + input + .prop("disabled", false) + .attr("placeholder", null) + .val(last_value = event.data.country_code); + fallback_country = event.data.default_country_code; + } + update_country_code(last_value); + }); + + event_registry.on("set_bot_status_result", event => { + if(event.key !== "country_code") return; + + if(event.status !== "success") + show_change_error(tr("Failed to set bots country"), event.error_msg || tr("timeout")); + else + last_value = event.value; + + input + .prop("disabled", false) + .attr("placeholder", null) + .val(last_value); + update_country_code(last_value); + }); + + input.on("input", () => { + update_country_code(input.val()); + input.firstParent(".input-boxed").removeClass("is-invalid"); + }); + + input.on("keyup", event => event.key === "Enter" && input.trigger("focusout")); + input.on("focusout", event => { + const value = input.val() as string; + if(value === last_value) return; + if(value && value.length != 2) { + input.firstParent(".input-boxed").addClass("is-invalid"); + return; + } + + input + .prop("disabled", true) + .val(null) + .attr("placeholder", "..."); + + event_registry.fire("set_bot_status", { + key: "country_code", + value: value + }); + }); + } + + /* flag channel commander */ + { + const input = container.find(".option-channel-commander") as JQuery; + const label = input.parents("label"); + + let last_value = undefined; + + event_registry.on("query_bot_status", event => { + last_value = undefined; + + label.addClass("disabled"); + input + .prop("checked", false) + .prop("disabled", true); + }); + + event_registry.on("bot_status", event => { + if(event.status === "error") { + label.addClass("disabled"); + input + .prop("checked", false) + .prop("disabled", true); + } else { + label.removeClass("disabled"); + input + .prop("checked", last_value = event.data.channel_commander) + .prop("disabled", false); + } + }); + + event_registry.on("set_bot_status_result", event => { + if(event.key !== "channel_commander") return; + + if(event.status !== "success") + show_change_error(tr("Failed to change channel commander state"), event.error_msg || tr("timeout")); + else + last_value = event.value; + + label.removeClass("disabled"); + input + .prop("checked", last_value) + .prop("disabled", false); + }); + + input.on("change", event => { + label.addClass("disabled"); + input.prop("disabled", true); + event_registry.fire("set_bot_status", { + key: "channel_commander", + value: input.prop("checked") + }); + }); + } + + /* flag priority speaker */ + { + const input = container.find(".option-priority-speaker") as JQuery; + const label = input.parents("label"); + + let last_value = undefined; + + event_registry.on("query_bot_status", event => { + last_value = undefined; + + label.addClass("disabled"); + input + .prop("checked", false) + .prop("disabled", true); + }); + + event_registry.on("bot_status", event => { + if(event.status === "error") { + label.addClass("disabled"); + input + .prop("checked", false) + .prop("disabled", true); + } else { + label.removeClass("disabled"); + input + .prop("checked", last_value = event.data.priority_speaker) + .prop("disabled", false); + } + }); + + event_registry.on("set_bot_status_result", event => { + if(event.key !== "priority_speaker") return; + + if(event.status !== "success") + show_change_error(tr("Failed to change priority speaker state"), event.error_msg || tr("timeout")); + else + last_value = event.value; + + label.removeClass("disabled"); + input + .prop("checked", last_value) + .prop("disabled", false); + }); + + input.on("change", event => { + label.addClass("disabled"); + input.prop("disabled", true); + event_registry.fire("set_bot_status", { + key: "priority_speaker", + value: input.prop("checked") + }); + }); + } + + /* status load timeout */ + { + let timeout; + event_registry.on("query_bot_status", event => { + timeout = setTimeout(() => { + event_registry.fire("bot_status", { + status: "error", + error_msg: tr("load timeout") + }); + }, 5000); + }); + + event_registry.on("bot_status", event => clearTimeout(timeout)); + } + + /* set status timeout */ + { + let timeouts: {[key: string]:any} = {}; + event_registry.on("set_bot_status", event => { + clearTimeout(timeouts[event.key]); + timeouts[event.key] = setTimeout(() => { + event_registry.fire("set_bot_status_result", { + status: "timeout", + key: event.key, + }); + }, 5000); + }); + + event_registry.on("set_bot_status_result", event => { + clearTimeout(timeouts[event.key]); + delete timeouts[event.key]; + }); + } + } + + /* music bot settings */ + { + const container = tag.find(".settings-playlist"); + + /* playlist replay mode */ + { + const input = container.find(".option-replay-mode") as JQuery; + let last_value = undefined; + + const update_value = text => { + if(text) { + input.prop("disabled", true).addClass("disabled"); + input.val("-1"); + input.find("option[value=-1]").text(text); + } else if(last_value >= 0 && last_value <= 3) { + input + .prop("disabled", false) + .removeClass("disabled"); + input.val(last_value); + } else { + update_value(tr("invalid value")); + } + }; + + event_registry.on("query_playlist_status", event => { + last_value = undefined; + update_value(tr("loading...")); + }); + + event_registry.on("playlist_status", event => { + if(event.status === "error") { + update_value(event.error_msg || tr("error while loading")); + } else { + last_value = event.data.replay_mode; + update_value(undefined); + } + }); + + event_registry.on("set_playlist_status_result", event => { + if(event.key !== "replay_mode") return; + + if(event.status !== "success") + show_change_error(tr("Failed to change replay mode"), event.error_msg || tr("timeout")); + else + last_value = event.value; + update_value(undefined); + }); + + input.on("keyup", event => event.key === "Enter" && input.trigger("focusout")); + input.on("change", event => { + const value = parseInt(input.val() as string); + console.log(value); + if(isNaN(value)) return; + + update_value(tr("applying...")); + event_registry.fire("set_playlist_status", { + key: "replay_mode", + value: value + }); + }); + } + + /* playlist max size */ + { + const input = container.find(".container-max-playlist-size input"); + let last_value = undefined; + + event_registry.on("query_playlist_status", event => { + last_value = undefined; + input + .prop("disabled", true) + .val(null) + .attr("placeholder", tr("loading...")) + .firstParent(".input-boxed").addClass("disabled"); + }); + + event_registry.on("playlist_status", event => { + if(event.status === "error") + input + .prop("disabled", true) + .val(null) + .attr("placeholder", event.error_msg || tr("error while loading")) + .firstParent(".input-boxed").addClass("disabled"); + else + input + .prop("disabled", false) + .attr("placeholder", null) + .val((last_value = event.data.max_size).toString()) + .firstParent(".input-boxed").removeClass("disabled"); + }); + + event_registry.on("set_playlist_status_result", event => { + if(event.key !== "max_size") return; + + if(event.status !== "success") + show_change_error(tr("Failed to change max playlist size"), event.error_msg || tr("timeout")); + else + last_value = event.value; + + input + .prop("disabled", false) + .attr("placeholder", null) + .val(last_value) + .firstParent(".input-boxed").removeClass("disabled"); + }); + + input.on("input", event => input.parentsUntil(".input-boxed").removeClass("is-invalid")); + input.on("keyup", event => event.key === "Enter" && input.trigger("focusout")); + input.on("focusout", event => { + const value = input.val() as string; + if(value === last_value) return; + if(value === "") { + input.val(last_value); + return; + } + if(isNaN(parseInt(value))) { + input.parentsUntil(".input-boxed").addClass("is-invalid"); + return; + } + + input + .prop("disabled", true) + .val(null) + .attr("placeholder", tr("applying...")) + .firstParent(".input-boxed").addClass("disabled"); + + event_registry.fire("set_playlist_status", { + key: "max_size", + value: parseInt(value) + }); + }); + } + + /* flag delete played */ + { + const input = container.find(".option-delete-played-songs") as JQuery; + const label = input.parents("label"); + + let last_value = undefined; + + event_registry.on("query_playlist_status", event => { + last_value = undefined; + + label.addClass("disabled"); + input + .prop("checked", false) + .prop("disabled", true); + }); + + event_registry.on("playlist_status", event => { + if(event.status === "error") { + label.addClass("disabled"); + input + .prop("checked", false) + .prop("disabled", true); + } else { + label.removeClass("disabled"); + input + .prop("checked", last_value = event.data.delete_played) + .prop("disabled", false); + } + }); + + event_registry.on("set_playlist_status_result", event => { + if(event.key !== "delete_played") return; + + if(event.status !== "success") + show_change_error(tr("Failed to change delete state"), event.error_msg || tr("timeout")); + else + last_value = event.value; + + label.removeClass("disabled"); + input + .prop("checked", last_value) + .prop("disabled", false); + }); + + input.on("change", event => { + label.addClass("disabled"); + input.prop("disabled", true); + event_registry.fire("set_playlist_status", { + key: "delete_played", + value: input.prop("checked") + }); + }); + } + + /* flag notify song change */ + { + const input = container.find(".option-notify-songs-change") as JQuery; + const label = input.parents("label"); + + let last_value = undefined; + + event_registry.on("query_playlist_status", event => { + last_value = undefined; + + label.addClass("disabled"); + input + .prop("checked", false) + .prop("disabled", true); + }); + + event_registry.on("playlist_status", event => { + if(event.status === "error") { + label.addClass("disabled"); + input + .prop("checked", false) + .prop("disabled", true); + } else { + label.removeClass("disabled"); + input + .prop("checked", last_value = event.data.notify_song_change) + .prop("disabled", false); + } + }); + + event_registry.on("set_playlist_status_result", event => { + if(event.key !== "notify_song_change") return; + + if(event.status !== "success") + show_change_error(tr("Failed to change notify state"), event.error_msg || tr("timeout")); + else + last_value = event.value; + + label.removeClass("disabled"); + input + .prop("checked", last_value) + .prop("disabled", false); + }); + + input.on("change", event => { + label.addClass("disabled"); + input.prop("disabled", true); + event_registry.fire("set_playlist_status", { + key: "notify_song_change", + value: input.prop("checked") + }); + }); + } + + /* status load timeout */ + { + let timeout; + event_registry.on("query_playlist_status", event => { + timeout = setTimeout(() => { + event_registry.fire("playlist_status", { + status: "error", + error_msg: tr("load timeout") + }); + }, 5000); + }); + + event_registry.on("playlist_status", event => clearTimeout(timeout)); + } + + /* set status timeout */ + { + let timeouts: {[key: string]:any} = {}; + event_registry.on("set_playlist_status", event => { + clearTimeout(timeouts[event.key]); + timeouts[event.key] = setTimeout(() => { + event_registry.fire("set_playlist_status_result", { + status: "timeout", + key: event.key, + }); + }, 5000); + }); + + event_registry.on("set_playlist_status_result", event => { + clearTimeout(timeouts[event.key]); + delete timeouts[event.key]; + }); + } + } + + /* reload button */ + { + const button = tag.find(".button-reload"); + let timeout; + + event_registry.on(["query_bot_status", "query_playlist_status"], event => { + button.prop("disabled", true); + + clearTimeout(timeout); + timeout = setTimeout(() => { + button.prop("disabled", false); + }, 1000); + }); + + button.on("click", event => { + event_registry.fire("query_bot_status"); + event_registry.fire("query_playlist_status"); + }); + } + + tooltip.initialize(tag); + + /* initialize on show */ + { + let initialized = false; + event_registry.on("show_container", event => { + if(event.container !== "settings" || initialized) return; + initialized = true; + + event_registry.fire("query_bot_status"); + event_registry.fire("query_playlist_status"); + }); + } +} + +function build_permission_container(event_registry: Registry, tag: JQuery) { + /* client search mechanism */ + { + const container = tag.find(".table-head .column-client-specific .client-select"); + let list_shown = false; + + /* search list show/hide */ + { + const button_list_clients = container.find(".button-list-clients"); + button_list_clients.on('click', event => + event_registry.fire(list_shown ? "hide_client_list" : "show_client_list")); + + event_registry.on("show_client_list", () => { + list_shown = true; + button_list_clients.text(tr("Hide clients")); + }); + + event_registry.on("hide_client_list", () => { + list_shown = false; + button_list_clients.text(tr("List clients")); + }); + } + + /* the search box */ + { + const input_search = container.find(".input-search"); + const button_search = container.find(".button-search"); + + let search_timeout; + let last_query; + input_search.on('keyup', event => { + const text = input_search.val() as string; + if(text === last_query) return; + + if(text) + event_registry.fire("filter_client_list", { filter: text }); + else + event_registry.fire("filter_client_list", { filter: undefined }); + + input_search.toggleClass("is-invalid", !list_shown && text === last_query); + if(!list_shown) { + button_search.prop("disabled", !text || !!search_timeout); + } else { + last_query = text; + } + }); + + input_search.on('keydown', event => { + if(event.key === "Enter" && !list_shown && !button_search.prop("disabled")) + button_search.trigger("click"); + }); + + event_registry.on("show_client_list", () => { + button_search.prop("disabled", true); + input_search.attr("placeholder", tr("Search client list")); + }); + + event_registry.on("hide_client_list", () => { + button_search.prop("disabled", !input_search.val() || !!search_timeout); + input_search.attr("placeholder", tr("Client uid or database id")); + }); + + button_search.on("click", event => { + button_search.prop("disabled", true); + + input_search.blur(); + const text = input_search.val() as string; + last_query = text; + event_registry.fire("search_client", { + text: text + }); + search_timeout = setTimeout(() => event_registry.fire("search_client_result", { + status: "timeout" + }), 5000); + }); + + event_registry.on("search_client_result", event => { + clearTimeout(search_timeout); + search_timeout = 0; + + button_search.prop("disabled", !input_search.val()); + if(event.status === "timeout") { + createErrorModal(tr("Client search failed"), tr("Failed to perform client search.
Search resulted in a timeout.")).open(); + return; + } else if(event.status === "error" || event.status === "empty") { + //TODO: Display the error somehow? + input_search.addClass("is-invalid"); + return; + } else { + event_registry.fire("special_client_set", { + client: event.client + }); + } + }); + } + + /* the client list */ + { + const container = tag.find(".overlay-client-list"); + + event_registry.on("show_client_list", () => container.removeClass("hidden")); + event_registry.on("hide_client_list", () => container.addClass("hidden")); + + const button_refresh = container.find(".button-clientlist-refresh"); + + const container_entries = container.find(".container-client-list"); + event_registry.on("special_client_list", data => { + button_refresh.prop("disabled", false); + container.find(".overlay").addClass("hidden"); + if(data.status === "error-permission") { + const overlay = container.find(".overlay-query-error-permissions"); + overlay.find("a").text(tr("Insufficient permissions")); + overlay.removeClass("hidden"); + } else if(data.status === "success") { + container_entries.find(".client").remove(); /* clear */ + + if(!data.clients.length) { + const overlay = container.find(".overlay-empty-list"); + overlay.removeClass("hidden"); + } else { + for(const client of data.clients) { + const tag = $.spawn("div").addClass("client").append( + htmltags.generate_client_object({ + add_braces: false, + client_id: 0, + client_database_id: client.database_id, + client_name: client.name, + client_unique_id: client.unique_id + }) + ); + tag.on('dblclick', event => event_registry.fire("special_client_set", { client: client })); + tag.attr("x-filter", client.database_id + "_" + client.name + "_" + client.unique_id); + container_entries.append(tag); + } + } + } else { + const overlay = container.find(".overlay-query-error"); + overlay.find("a").text(data.error_msg ? data.error_msg : tr("query failed")); + overlay.removeClass("hidden"); + } + }); + + /* refresh button */ + button_refresh.on('click', event => { + button_refresh.prop("disabled", true); + event_registry.fire("query_special_clients"); + }); + + + /* special client list query timeout handler */ + { + let query_timeout; + event_registry.on("query_special_clients", event => { + query_timeout = setTimeout(() => { + event_registry.fire("special_client_list", { + status: "error", + error_msg: tr("Query timeout") + }); + }, 5000); + }); + + event_registry.on("special_client_list", event => clearTimeout(query_timeout)); + } + + /* first time client list show */ + { + let shown; + event_registry.on('show_client_list', event => { + if(shown) return; + shown = true; + + event_registry.fire("query_special_clients"); + }); + } + + /* the client list filter */ + { + let filter; + + const overlay = container.find(".overlay-filter-no-result"); + const update_filter = () => { + let shown = 0, hidden = 0; + container_entries.find(".client").each(function () { + const text = this.getAttribute("x-filter"); + if(!filter || text.toLowerCase().indexOf(filter) != -1) { + this.classList.remove("hidden"); + shown++; + } else { + this.classList.add("hidden"); + hidden++; + } + }); + if(shown == 0 && hidden == 0) return; + overlay.toggleClass("hidden", shown != 0); }; - event_registry.on("special_client_set", event => { - last_sync_value = undefined; + event_registry.on("special_client_list", event => update_filter()); + event_registry.on("filter_client_list", event => { + filter = (event.filter || "").toLowerCase(); + update_filter(); }); - event_registry.on("general_permissions", event => update_indicator()); - event_registry.on("set_general_permission_result", event => { - if(event.key !== permission_needed_name) return; - if(event.status !== "success") return; + } + } - update_indicator(); - }); + event_registry.on("special_client_set", event => { + container.toggleClass("hidden", !!event.client); + event_registry.fire("hide_client_list"); + }); + } - /* loading the permission */ - event_registry.on("query_client_permissions", event => { - if(event.client_database_id !== client_database_id) return; + /* the client info */ + { + const container = tag.find(".table-head .column-client-specific .client-info"); - last_sync_value = undefined; - hide_indicator = true; + container.find(".button-client-deselect").on("click", event => { + event_registry.fire("special_client_set", { client: undefined }); + }); + + event_registry.on("special_client_set", event => { + container.toggleClass("hidden", !event.client); + + const client_container = container.find(".container-selected-client"); + client_container.find(".htmltag-client").remove(); + if(event.client) { + client_container.append(htmltags.generate_client_object({ + client_unique_id: event.client.unique_id, + client_name: event.client.name, + client_id: 0, + client_database_id: event.client.database_id, + add_braces: false + })); + } + }); + } + + const power_needed_map = { + i_client_music_rename_power: "i_client_music_needed_rename_power", + i_client_music_modify_power: "i_client_music_needed_modify_power", + i_client_music_delete_power: "i_client_music_needed_delete_power", + i_playlist_view_power: "i_playlist_needed_view_power", + i_playlist_modify_power: "i_playlist_needed_modify_power", + i_playlist_permission_modify_power: "i_playlist_needed_permission_modify_power", + i_playlist_song_add_power: "i_playlist_song_needed_add_power", + i_playlist_song_move_power: "i_playlist_song_needed_move_power", + i_playlist_song_remove_power: "i_playlist_song_needed_remove_power", + b_virtualserver_playlist_permission_list: "b_virtualserver_playlist_permission_list" + }; + const needed_power_map = Object.entries(power_needed_map).reduce((ret, entry) => { + const [key, value] = entry; + ret[value] = key; + return ret; + }, {}); + + /* general permissions */ + { + /* permission input functionality */ + { + tag.find(".general-permission").each((_, _e) => { + const elem = $(_e) as JQuery; + + const permission_name = elem.attr("x-permission"); + if(!permission_name) return; + + const input = elem.find("input"); + input.attr("maxlength", 6); + + let last_sync_value = undefined; + + event_registry.on("query_general_permissions", event => { input.prop("disabled", true).val(null); input.attr("placeholder", tr("loading...")); - update_indicator(); }); - event_registry.on('client_permissions', event => { - if(event.client_database_id !== client_database_id) return; - - hide_indicator = false; + event_registry.on("general_permissions", event => { input.prop("disabled", true).val(null); if(event.status === "timeout") { input.attr("placeholder", tr("load timeout")); @@ -1781,32 +1456,9 @@ namespace Modals { } else { input.attr("placeholder", event.error_msg || tr("load error")); } - update_indicator(); }); - /* permission editing */ - input.attr("maxlength", 6); - input.on("focusout", event => { - if(!client_database_id) return; - - const value = parseInt(input.val() as string); - if(value === last_sync_value) return; - - input.prop("disabled", true).val(null); - input.attr("placeholder", tr("applying...")); - event_registry.fire("set_client_permission", { - client_database_id: client_database_id, - key: permission_name, - value: value || 0 - }); - hide_indicator = true; - update_indicator(); - }); - - input.on("change", () => update_indicator()); - input.on("keyup", event => event.key === "Enter" && input.blur()); - - event_registry.on("set_client_permission_result", event => { + event_registry.on("set_general_permission_result", event => { if(event.key !== permission_name) return; input.prop("disabled", false); //TODO: Check permissions? @@ -1818,79 +1470,436 @@ namespace Modals { if(typeof last_sync_value === "number") input.val(last_sync_value); createErrorModal(tr("Failed to change permission"), tra("Failed to change permission:{:br:}{}", event.error_msg)).open(); } - hide_indicator = false; - update_indicator(); }); + + input.on("focusout", event => { + if(input.prop("disabled")) return; + + const value = parseInt(input.val() as string); + if(value === last_sync_value) return; + + input.prop("disabled", true).val(null); + input.attr("placeholder", tr("applying...")); + event_registry.fire("set_general_permission", { + key: permission_name, + value: value || 0 + }); + }); + input.on("keyup", event => event.key === "Enter" && input.blur()); }); - - /* client permission query timeout */ - { - let timeout: {[key: number]: any} = {}; - event_registry.on("query_client_permissions", event => { - if(timeout[event.client_database_id]) - clearTimeout(timeout[event.client_database_id]); - timeout[event.client_database_id] = setTimeout(() => { - event_registry.fire("client_permissions", { - status: "timeout", - client_database_id: event.client_database_id - }); - }, 5000); - }); - - event_registry.on("client_permissions", event => { - clearTimeout(timeout[event.client_database_id]); - }); - } - - /* client permission set timeout */ - { - let timeout: {[key: string]: any} = {}; - event_registry.on("set_client_permission", event => { - const key = event.client_database_id + "_" + event.key; - if(timeout[key]) - clearTimeout(timeout[key]); - - timeout[key] = setTimeout(() => { - event_registry.fire("set_client_permission_result", { - key: event.key, - status: "error", - client_database_id: event.client_database_id, - error_msg: tr("timeout") - }); - }, 5000); - }); - - event_registry.on("set_client_permission_result", event => { - const key = event.client_database_id + "_" + event.key; - if(timeout[key]) { - clearTimeout(timeout[key]); - delete timeout[key]; - } - }); - } - - event_registry.on("refresh_permissions", event => { - if(client_database_id) - event_registry.fire("query_client_permissions", { client_database_id: client_database_id }); - }); - tooltip(container); } - /* a title attribute for permission column */ - tag.find(".table-body .column-permission a").each(function () { - this.setAttribute("title", this.textContent); - }); - - /* initialize on show */ + /* the tooltip functionality */ { - let initialized = false; - event_registry.on("show_container", event => { - if(event.container !== "permissions" || initialized) return; - initialized = true; + tag.find(".general-permission").each((_, _e) => { + const elem = $(_e) as JQuery; - event_registry.fire("special_client_set", { client: undefined }); - event_registry.fire("query_general_permissions", {}); + const permission_name = elem.attr("x-permission"); + if(!permission_name) return; + + const required_power = needed_power_map[permission_name]; + if(!required_power) return; + + let last_sync_value = undefined; + let current_tag: JQuery; + + let loading = false; + let query_result: { + status: "error" | "timeout" | "success" + groups?: { + name: string, + value: number, + id: number + }[], + error_msg?: string + }; + + event_registry.on("general_permissions", event => { + if(event.status === "success") + last_sync_value = event.permissions ? event.permissions[permission_name] || 0 : 0; + }); + + event_registry.on("set_general_permission_result", event => { + if(event.key !== permission_name) return; + + if(event.status === "success") + last_sync_value = event.value; + }); + + event_registry.on("refresh_permissions", event => { + query_result = undefined; /* require for the next time */ + }); + + const show_query_result = () => { + if(!current_tag) return; + + const container_groups = current_tag.find(".container-groups"); + container_groups.children().remove(); + current_tag.find(".container-status").addClass("hidden"); + + if(loading) { + current_tag.find(".status-loading").removeClass("hidden"); + } else if(!query_result || query_result.status === "error") { + current_tag + .find(".status-error").removeClass("hidden") + .text((query_result ? query_result.error_msg : "") || tr("failed to query data")); + } else if(query_result.status === "timeout") { + current_tag + .find(".status-error").removeClass("hidden") + .text(tr("timeout while loading")); + } else { + let count = 0; + for(const group of (query_result.groups || [])) { + if(group.value !== -1 && group.value < last_sync_value) continue; + + count++; + container_groups.append($.spawn("div").addClass("group").text( + " - " + group.name + " (" + group.id + ")" + )); + } + + if(count === 0) current_tag.find(".status-no-groups").removeClass("hidden"); + } + }; + + tooltip.initialize(elem, { + on_show(tag: JQuery) { + current_tag = tag; + + if(!query_result && !loading) { + event_registry.fire("query_group_permissions", { + permission_name: required_power + }); + loading = true; + } + show_query_result(); + }, + on_hide(tag: JQuery) { + current_tag = undefined; + } + }); + + event_registry.on("group_permissions", event => { + if(event.permission_name !== required_power) return; + + loading = false; + query_result = event; + show_query_result(); + }); + }); + + + /* refresh mechanism */ + { + event_registry.on("refresh_permissions", event => event_registry.fire("query_general_permissions")); + } + } + + /* permission set timeout */ + { + let permission_timers: {[key: string]:any} = {}; + event_registry.on("set_general_permission", event => { + if(permission_timers[event.key]) + clearTimeout(permission_timers[event.key]); + permission_timers[event.key] = setTimeout(() => { + event_registry.fire("set_general_permission_result", { + key: event.key, + status: "error", + error_msg: tr("controller timeout") + }); + }, 5000); + }); + + event_registry.on("set_general_permission_result", event => { + clearTimeout(permission_timers[event.key]); + delete permission_timers[event.key]; + }); + } + + /* group query timeout */ + { + let timers: {[key: string]:any} = {}; + event_registry.on("query_group_permissions", event => { + if(timers[event.permission_name]) + clearTimeout(timers[event.permission_name]); + timers[event.permission_name] = setTimeout(() => { + event_registry.fire("group_permissions", { + permission_name: event.permission_name, + status: "timeout" + }); + }, 5000); + }); + + event_registry.on("group_permissions", event => { + clearTimeout(timers[event.permission_name]); + delete timers[event.permission_name]; + }); + } + + /* query timeout */ + { + let query_timeout; + event_registry.on("query_general_permissions", event => { + clearTimeout(query_timeout); + query_timeout = setTimeout(() => { + event_registry.fire("general_permissions", { + status: "timeout" + }); + }, 5000); + }); + + event_registry.on("general_permissions", event => clearTimeout(query_timeout)); + } + + /* refresh button */ + { + const button = tag.find(".button-permission-refresh"); + let refresh_timer; + + let loading_client_permissions = false; + let loading_general_permissions = false; + + const update_button = () => + button.prop("disabled", refresh_timer || loading_client_permissions || loading_general_permissions); + + event_registry.on("query_general_permissions", event => { + loading_general_permissions = true; + update_button(); + }); + + event_registry.on("general_permissions", event => { + loading_general_permissions = false; + update_button(); + }); + + event_registry.on("query_client_permissions", event => { + loading_client_permissions = true; + update_button(); + }); + + event_registry.on("client_permissions", event => { + loading_client_permissions = false; + update_button(); + }); + + button.on('click', event => { + event_registry.fire("refresh_permissions"); + + /* allow refreshes only every second */ + refresh_timer = setTimeout(() => { + refresh_timer = undefined; + update_button(); + }, 1000); }); } } + + /* client specific permissions */ + { + const container = tag.find(".column-client-specific"); + + let client_database_id = 0; + let needed_permissions: {[key: string]:number} = {}; + + /* needed permissions updater */ + { + event_registry.on("general_permissions", event => { + if(event.status !== "success") return; + + needed_permissions = event.permissions; + }); + + event_registry.on("set_general_permission_result", event => { + if (event.status !== "success") return; + + needed_permissions[event.key] = event.value; + }); + } + + event_registry.on("special_client_set", event => { + client_database_id = event.client ? event.client.database_id : 0; + container.find(".client-permission").toggleClass("hidden", !event.client); + + if(client_database_id) + event_registry.fire("query_client_permissions", { client_database_id: client_database_id }); + }); + + const enabled_class = "client-apply"; + const disabled_class = "client-delete"; + + container.find(".client-permission").each((_, _e) => { + const elem = $(_e); + + const input = elem.find("input"); + const status_indicator = elem.find(".icon_em"); + + const permission_name = elem.attr("x-permission") as string; + const permission_needed_name = power_needed_map[permission_name]; + + let last_sync_value = undefined; + let hide_indicator = false; + + if(typeof permission_needed_name !== "string") { + log.warn(LogCategory.GENERAL, tr("Missing permission needed mapping for %s"), permission_name); + return; + } + + const update_indicator = () => { + const value = parseInt(input.val() as string); + const needed = typeof needed_permissions[permission_needed_name] === "number" ? needed_permissions[permission_needed_name] : 0; + const flag = value == -1 ? true : isNaN(value) || value == 0 ? false : value >= needed; + + status_indicator.toggle(!hide_indicator); + status_indicator.toggleClass(enabled_class, flag).toggleClass(disabled_class, !flag); + }; + + event_registry.on("special_client_set", event => { + last_sync_value = undefined; + }); + event_registry.on("general_permissions", event => update_indicator()); + event_registry.on("set_general_permission_result", event => { + if(event.key !== permission_needed_name) return; + if(event.status !== "success") return; + + update_indicator(); + }); + + /* loading the permission */ + event_registry.on("query_client_permissions", event => { + if(event.client_database_id !== client_database_id) return; + + last_sync_value = undefined; + hide_indicator = true; + input.prop("disabled", true).val(null); + input.attr("placeholder", tr("loading...")); + update_indicator(); + }); + + event_registry.on('client_permissions', event => { + if(event.client_database_id !== client_database_id) return; + + hide_indicator = false; + input.prop("disabled", true).val(null); + if(event.status === "timeout") { + input.attr("placeholder", tr("load timeout")); + } else if(event.status === "success") { + input.prop("disabled", false); //TODO: Check permissions? + input.attr("placeholder", null); + const value = event.permissions ? event.permissions[permission_name] || 0 : 0; + last_sync_value = value; + input.val(value); + } else { + input.attr("placeholder", event.error_msg || tr("load error")); + } + update_indicator(); + }); + + /* permission editing */ + input.attr("maxlength", 6); + input.on("focusout", event => { + if(!client_database_id) return; + + const value = parseInt(input.val() as string); + if(value === last_sync_value) return; + + input.prop("disabled", true).val(null); + input.attr("placeholder", tr("applying...")); + event_registry.fire("set_client_permission", { + client_database_id: client_database_id, + key: permission_name, + value: value || 0 + }); + hide_indicator = true; + update_indicator(); + }); + + input.on("change", () => update_indicator()); + input.on("keyup", event => event.key === "Enter" && input.blur()); + + event_registry.on("set_client_permission_result", event => { + if(event.key !== permission_name) return; + + input.prop("disabled", false); //TODO: Check permissions? + input.attr("placeholder", null); + if(event.status === "success") { + input.val(event.value); + last_sync_value = event.value; + } else if(event.status === "error") { + if(typeof last_sync_value === "number") input.val(last_sync_value); + createErrorModal(tr("Failed to change permission"), tra("Failed to change permission:{:br:}{}", event.error_msg)).open(); + } + hide_indicator = false; + update_indicator(); + }); + }); + + /* client permission query timeout */ + { + let timeout: {[key: number]: any} = {}; + event_registry.on("query_client_permissions", event => { + if(timeout[event.client_database_id]) + clearTimeout(timeout[event.client_database_id]); + timeout[event.client_database_id] = setTimeout(() => { + event_registry.fire("client_permissions", { + status: "timeout", + client_database_id: event.client_database_id + }); + }, 5000); + }); + + event_registry.on("client_permissions", event => { + clearTimeout(timeout[event.client_database_id]); + }); + } + + /* client permission set timeout */ + { + let timeout: {[key: string]: any} = {}; + event_registry.on("set_client_permission", event => { + const key = event.client_database_id + "_" + event.key; + if(timeout[key]) + clearTimeout(timeout[key]); + + timeout[key] = setTimeout(() => { + event_registry.fire("set_client_permission_result", { + key: event.key, + status: "error", + client_database_id: event.client_database_id, + error_msg: tr("timeout") + }); + }, 5000); + }); + + event_registry.on("set_client_permission_result", event => { + const key = event.client_database_id + "_" + event.key; + if(timeout[key]) { + clearTimeout(timeout[key]); + delete timeout[key]; + } + }); + } + + event_registry.on("refresh_permissions", event => { + if(client_database_id) + event_registry.fire("query_client_permissions", { client_database_id: client_database_id }); + }); + tooltip.initialize(container); + } + + /* a title attribute for permission column */ + tag.find(".table-body .column-permission a").each(function () { + this.setAttribute("title", this.textContent); + }); + + /* initialize on show */ + { + let initialized = false; + event_registry.on("show_container", event => { + if(event.container !== "permissions" || initialized) return; + initialized = true; + + event_registry.fire("special_client_set", { client: undefined }); + event_registry.fire("query_general_permissions", {}); + }); + } } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalNewcomer.ts b/shared/js/ui/modal/ModalNewcomer.ts index 20f414a8..b537573c 100644 --- a/shared/js/ui/modal/ModalNewcomer.ts +++ b/shared/js/ui/modal/ModalNewcomer.ts @@ -1,434 +1,437 @@ -/// -/// -/// +import {createModal, Modal} from "tc-shared/ui/elements/Modal"; +import {tra} from "tc-shared/i18n/localize"; +import {Registry} from "tc-shared/events"; +import * as loader from "tc-loader"; +import { modal as emodal } from "tc-shared/events"; +import {modal_settings} from "tc-shared/ui/modal/ModalSettings"; +import {profiles} from "tc-shared/profiles/ConnectionProfile"; +import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; -namespace Modals { - const next_step: {[key: string]:string} = { - "welcome": "microphone", - //"microphone": app.is_web() ? "identity" : "speaker", /* speaker setup only for the native client! */ - "microphone": "identity", - "speaker": "identity", - "identity": "finish" - }; - const last_step: {[key: string]:string} = (() => { - const result = {}; - for(const key of Object.keys(next_step)) - if(!result[next_step[key]]) - result[next_step[key]] = key; - return result; - })(); +const next_step: {[key: string]:string} = { + "welcome": "microphone", + //"microphone": app.is_web() ? "identity" : "speaker", /* speaker setup only for the native client! */ + "microphone": "identity", + "speaker": "identity", + "identity": "finish" +}; +const last_step: {[key: string]:string} = (() => { + const result = {}; + for(const key of Object.keys(next_step)) + if(!result[next_step[key]]) + result[next_step[key]] = key; + return result; +})(); - export function openModalNewcomer() : Modal { - let modal = createModal({ - header: tra("Welcome to the {}", app.is_web() ? "TeaSpeak - Web client" : "TeaSpeak - Client"), - body: () => $("#tmpl_newcomer").renderTag({ - is_web: app.is_web() - }).children(), - footer: null, +export function openModalNewcomer() : Modal { + let modal = createModal({ + header: tra("Welcome to the {}", loader.version().type === "web" ? "TeaSpeak - Web client" : "TeaSpeak - Client"), + body: () => $("#tmpl_newcomer").renderTag({ + is_web: loader.version().type === "web" + }).children(), + footer: null, - width: "", - closeable: false - }); + width: "", + closeable: false + }); - const event_registry = new events.Registry(); - event_registry.enable_debug("newcomer"); + const event_registry = new Registry(); + event_registry.enable_debug("newcomer"); - modal.htmlTag.find(".modal-body").addClass("modal-newcomer"); + modal.htmlTag.find(".modal-body").addClass("modal-newcomer"); - initializeBasicFunctionality(modal.htmlTag, event_registry); - initializeStepWelcome(modal.htmlTag.find(".container-body .step.step-welcome"), event_registry); - initializeStepIdentity(modal.htmlTag.find(".container-body .step.step-identity"), event_registry); - initializeStepMicrophone(modal.htmlTag.find(".container-body .step.step-microphone"), event_registry, modal); - initializeStepFinish(modal.htmlTag.find(".container-body .step.step-finish"), event_registry); + initializeBasicFunctionality(modal.htmlTag, event_registry); + initializeStepWelcome(modal.htmlTag.find(".container-body .step.step-welcome"), event_registry); + initializeStepIdentity(modal.htmlTag.find(".container-body .step.step-identity"), event_registry); + initializeStepMicrophone(modal.htmlTag.find(".container-body .step.step-microphone"), event_registry, modal); + initializeStepFinish(modal.htmlTag.find(".container-body .step.step-finish"), event_registry); - event_registry.on("exit_guide", event => { - if(event.ask_yesno) - Modals.spawnYesNo(tr("Are you sure?"), tr("Do you really want to skip the basic setup guide?"), result => { - if(result) - event_registry.fire("exit_guide", {ask_yesno: false}); - }); - else - modal.close(); - }); - - event_registry.fire("show_step", {step: "welcome"}); - modal.open(); - event_registry.fire_async("modal-shown"); - return modal; - } - - function initializeBasicFunctionality(tag: JQuery, event_registry: events.Registry) { - const container_header = tag.find(".container-header"); - const tag_body = tag.find(".container-body .body"); - - /* step navigation */ - event_registry.on("show_step", event => { - tag_body.find(".step").addClass("hidden"); - container_header.find(".step").addClass("hidden"); - - tag_body.find(".step.step-" + event.step).removeClass("hidden"); - container_header.find(".step.step-" + event.step).removeClass("hidden"); - }); - - /* button controller */ - { - const buttons = tag.find(".buttons"); - const button_last_step = buttons.find(".button-last-step"); - const button_next_step = buttons.find(".button-next-step"); - - button_last_step.on('click', event => { - if(last_step[current_step]) - event_registry.fire("show_step", { step: last_step[current_step] as any }); - else - event_registry.fire("exit_guide", {ask_yesno: true}); - }); - - let current_step; - button_next_step.on('click', event => { - if(next_step[current_step]) - event_registry.fire("show_step", { step: next_step[current_step] as any }); - else + event_registry.on("exit_guide", event => { + if(event.ask_yesno) + spawnYesNo(tr("Are you sure?"), tr("Do you really want to skip the basic setup guide?"), result => { + if(result) event_registry.fire("exit_guide", {ask_yesno: false}); }); + else + modal.close(); + }); - event_registry.on("show_step", event => { - current_step = event.step; - button_next_step.text(next_step[current_step] ? tr("Next step") : tr("Finish guide")); - button_last_step.text(last_step[current_step] ? tr("Last step") : tr("Skip guide")); - }); + event_registry.fire("show_step", {step: "welcome"}); + modal.open(); + event_registry.fire_async("modal-shown"); + return modal; +} - event_registry.on("show_step", event => button_next_step.prop("disabled", true)); - event_registry.on("show_step", event => button_last_step.prop("disabled", true)); +function initializeBasicFunctionality(tag: JQuery, event_registry: Registry) { + const container_header = tag.find(".container-header"); + const tag_body = tag.find(".container-body .body"); - event_registry.on("step-status", event => button_next_step.prop("disabled", !event.next_button)); - event_registry.on("step-status", event => button_last_step.prop("disabled", !event.previous_button)); - } - } + /* step navigation */ + event_registry.on("show_step", event => { + tag_body.find(".step").addClass("hidden"); + container_header.find(".step").addClass("hidden"); - function initializeStepWelcome(tag: JQuery, event_registry: events.Registry) { - event_registry.on("show_step", e => { - if(e.step !== "welcome") return; + tag_body.find(".step.step-" + event.step).removeClass("hidden"); + container_header.find(".step.step-" + event.step).removeClass("hidden"); + }); - event_registry.fire_async("step-status", { next_button: true, previous_button: true }); + /* button controller */ + { + const buttons = tag.find(".buttons"); + const button_last_step = buttons.find(".button-last-step"); + const button_next_step = buttons.find(".button-next-step"); + + button_last_step.on('click', event => { + if(last_step[current_step]) + event_registry.fire("show_step", { step: last_step[current_step] as any }); + else + event_registry.fire("exit_guide", {ask_yesno: true}); }); - } - function initializeStepFinish(tag: JQuery, event_registry: events.Registry) { - event_registry.on("show_step", e => { - if(e.step !== "finish") return; - - event_registry.fire_async("step-status", {next_button: true, previous_button: true }); + let current_step; + button_next_step.on('click', event => { + if(next_step[current_step]) + event_registry.fire("show_step", { step: next_step[current_step] as any }); + else + event_registry.fire("exit_guide", {ask_yesno: false}); }); + + event_registry.on("show_step", event => { + current_step = event.step; + button_next_step.text(next_step[current_step] ? tr("Next step") : tr("Finish guide")); + button_last_step.text(last_step[current_step] ? tr("Last step") : tr("Skip guide")); + }); + + event_registry.on("show_step", event => button_next_step.prop("disabled", true)); + event_registry.on("show_step", event => button_last_step.prop("disabled", true)); + + event_registry.on("step-status", event => button_next_step.prop("disabled", !event.next_button)); + event_registry.on("step-status", event => button_last_step.prop("disabled", !event.previous_button)); } +} - function initializeStepIdentity(tag: JQuery, event_registry: events.Registry) { - const profile_events = new events.Registry(); - profile_events.enable_debug("settings-identity"); - modal_settings.initialize_identity_profiles_controller(profile_events); - modal_settings.initialize_identity_profiles_view(tag, profile_events, { forum_setuppable: false }); +function initializeStepWelcome(tag: JQuery, event_registry: Registry) { + event_registry.on("show_step", e => { + if(e.step !== "welcome") return; - let step_shown = false; - let help_animation_done = false; - const profiles_valid = () => profiles.profiles().findIndex(e => e.valid()) !== -1; - const update_step_status = () => { - event_registry.fire_async("step-status", { next_button: help_animation_done && profiles_valid(), previous_button: help_animation_done }); + event_registry.fire_async("step-status", { next_button: true, previous_button: true }); + }); +} + +function initializeStepFinish(tag: JQuery, event_registry: Registry) { + event_registry.on("show_step", e => { + if(e.step !== "finish") return; + + event_registry.fire_async("step-status", {next_button: true, previous_button: true }); + }); +} + +function initializeStepIdentity(tag: JQuery, event_registry: Registry) { + const profile_events = new Registry(); + profile_events.enable_debug("settings-identity"); + modal_settings.initialize_identity_profiles_controller(profile_events); + modal_settings.initialize_identity_profiles_view(tag, profile_events, { forum_setuppable: false }); + + let step_shown = false; + let help_animation_done = false; + const profiles_valid = () => profiles().findIndex(e => e.valid()) !== -1; + const update_step_status = () => { + event_registry.fire_async("step-status", { next_button: help_animation_done && profiles_valid(), previous_button: help_animation_done }); + }; + profile_events.on("query-profile-validity-result", event => step_shown && event.status === "success" && event.valid && update_step_status()); + event_registry.on("show_step", e => { + step_shown = e.step === "identity"; + if(!step_shown) return; + + update_step_status(); + }); + + /* the help sequence */ + { + const container = tag.find(".container-settings-identity-profile"); + const container_help_text = tag.find(".container-help-text"); + + const container_profile_list = tag.find(".highlight-profile-list"); + const container_profile_settings = tag.find(".highlight-profile-settings"); + const container_identity_settings = tag.find(".highlight-identity-settings"); + + let is_first_show = true; + + event_registry.on("show_step", event => { + if(!is_first_show || event.step !== "identity") return; + is_first_show = false; + + container.addClass("help-shown"); + + + const text = tr( + "After you've successfully set upped your microphone,\n" + + "lets setup some profiles and identities!\n" + + "\n" + + "Connect profiles determine, how your're authenticating yourself with the server.\n" + + "So basically they're your identity.\n" + + "In the following I'll guid you thru the options and GUI elements.\n" + + "\n" + + "To continue click anywhere on the screen." + ); + set_help_text(text); + $("body").one('mousedown', event => show_profile_list_help()); + }); + + const set_help_text = text => { + container_help_text.empty(); + text.split("\n").forEach(e => container_help_text.append(e == "" ? $.spawn("br") : $.spawn("a").text(e))); }; - profile_events.on("query-profile-validity-result", event => step_shown && event.status === "success" && event.valid && update_step_status()); - event_registry.on("show_step", e => { - step_shown = e.step === "identity"; - if(!step_shown) return; + const show_profile_list_help = () => { + container.find(".highlighted").removeClass("highlighted"); + container_profile_list.addClass("highlighted"); + + const update_position = () => { + const font_size = parseFloat(getComputedStyle(container_help_text[0]).fontSize); + + const offset = container_profile_list.offset(); + const abs = container.offset(); + + container_help_text.css({ + top: offset.top - abs.top, + left: ((offset.left - abs.left) + container_profile_list.outerWidth() + font_size) + "px", + right: "1em", + bottom: "1em" + }); + }; + update_position(); + container_help_text.off('resize').on('resize', update_position); + + const text = tr( + "You could have as many connect profiles as you want.\n" + + "All created profiles will be listed here.\n" + + "\n" + + "To create a new profile just simply click the blue button \"Create profile\" and enter a profile name.\n" + + "If you want to delete a profile you've to select that profile and click the delete button.\n" + + "\n" + + "By default we're using the \"default\" profile\n" + + "to connect to any server. o change the default profile\n" + + "just select the new profile and press the \"select as default\" button.\n" + + "\n" + + "To continue click anywhere on the screen." + ); + set_help_text(text); + $("body").one('mousedown', event => show_profile_settings_help()); + }; + + const show_profile_settings_help = () => { + container.find(".highlighted").removeClass("highlighted"); + container_profile_settings.addClass("highlighted"); + + const update_position = () => { + const font_size = parseFloat(getComputedStyle(container_help_text[0]).fontSize); + const container_settings_offset = container_profile_settings.offset(); + const right = container_profile_settings.outerWidth() + font_size * 2; + container_help_text.css({ + top: container_settings_offset.top - container.offset().top, + left: "1em", + right: right + "px", + bottom: "1em" + }); + }; + set_help_text(tr( + "In the upper left, you'll find the profile settings for the selected profile.\n" + + "You could give each profile an individual name. You could also specify the default connect nickname here.\n" + + "\n" + + "The last option \"Identity Type\" determines on what your identity is based on.\n" + + "TeaSpeak has two possibilities to identify yourself:\n" + + "1. Identify yourself by your TeaSpeak forum account\n" + + "2. Identify by an own generated cryptographic identity\n" + + "The second methods is also known as a TeamSpeak 3 identity.\n" + + "\n" + + "To continue click anywhere on the screen." + )); + update_position(); + container_help_text.off('resize').on('resize', update_position); + + $("body").one('mousedown', event => show_identity_settings_help()); + }; + + const show_identity_settings_help = () => { + container.find(".highlighted").removeClass("highlighted"); + container_identity_settings.addClass("highlighted"); + + const update_position = () => { + const font_size = parseFloat(getComputedStyle(container_help_text[0]).fontSize); + const container_identity_offset = container_identity_settings.offset(); + const right = container_profile_settings.outerWidth() + font_size * 2; + container_help_text.css({ + top: container_identity_offset.top - container.offset().top, + left: "1em", + right: right + "px", + bottom: "1em" + }); + }; + set_help_text(tr( + "When selecting an identify type, some corresponding will pop up in the highlighted area.\n" + + "\n" + + "But don't worry, we've already generated\n" + + "a cryptographic identity for you!\n" + + "So you don't have to change anything before you start." + )); + update_position(); + container_help_text.off('resize').on('resize', update_position); + + $("body").one('mousedown', event => hide_help()); + }; + + const hide_help = () => { + container.find(".highlighted").removeClass("highlighted"); + container.addClass("hide-help"); + setTimeout(() => container.removeClass("help-shown"), 1000); + container_help_text.off('resize'); + + help_animation_done = true; update_step_status(); - }); - - /* the help sequence */ - { - const container = tag.find(".container-settings-identity-profile"); - const container_help_text = tag.find(".container-help-text"); - - const container_profile_list = tag.find(".highlight-profile-list"); - const container_profile_settings = tag.find(".highlight-profile-settings"); - const container_identity_settings = tag.find(".highlight-identity-settings"); - - let is_first_show = true; - - event_registry.on("show_step", event => { - if(!is_first_show || event.step !== "identity") return; - is_first_show = false; - - container.addClass("help-shown"); - - - const text = tr( - "After you've successfully set upped your microphone,\n" + - "lets setup some profiles and identities!\n" + - "\n" + - "Connect profiles determine, how your're authenticating yourself with the server.\n" + - "So basically they're your identity.\n" + - "In the following I'll guid you thru the options and GUI elements.\n" + - "\n" + - "To continue click anywhere on the screen." - ); - set_help_text(text); - $("body").one('mousedown', event => show_profile_list_help()); - }); - - const set_help_text = text => { - container_help_text.empty(); - text.split("\n").forEach(e => container_help_text.append(e == "" ? $.spawn("br") : $.spawn("a").text(e))); - }; - - const show_profile_list_help = () => { - container.find(".highlighted").removeClass("highlighted"); - container_profile_list.addClass("highlighted"); - - const update_position = () => { - const font_size = parseFloat(getComputedStyle(container_help_text[0]).fontSize); - - const offset = container_profile_list.offset(); - const abs = container.offset(); - - container_help_text.css({ - top: offset.top - abs.top, - left: ((offset.left - abs.left) + container_profile_list.outerWidth() + font_size) + "px", - right: "1em", - bottom: "1em" - }); - }; - update_position(); - container_help_text.off('resize').on('resize', update_position); - - const text = tr( - "You could have as many connect profiles as you want.\n" + - "All created profiles will be listed here.\n" + - "\n" + - "To create a new profile just simply click the blue button \"Create profile\" and enter a profile name.\n" + - "If you want to delete a profile you've to select that profile and click the delete button.\n" + - "\n" + - "By default we're using the \"default\" profile\n" + - "to connect to any server. o change the default profile\n" + - "just select the new profile and press the \"select as default\" button.\n" + - "\n" + - "To continue click anywhere on the screen." - ); - set_help_text(text); - $("body").one('mousedown', event => show_profile_settings_help()); - }; - - const show_profile_settings_help = () => { - container.find(".highlighted").removeClass("highlighted"); - container_profile_settings.addClass("highlighted"); - - const update_position = () => { - const font_size = parseFloat(getComputedStyle(container_help_text[0]).fontSize); - const container_settings_offset = container_profile_settings.offset(); - const right = container_profile_settings.outerWidth() + font_size * 2; - container_help_text.css({ - top: container_settings_offset.top - container.offset().top, - left: "1em", - right: right + "px", - bottom: "1em" - }); - }; - set_help_text(tr( - "In the upper left, you'll find the profile settings for the selected profile.\n" + - "You could give each profile an individual name. You could also specify the default connect nickname here.\n" + - "\n" + - "The last option \"Identity Type\" determines on what your identity is based on.\n" + - "TeaSpeak has two possibilities to identify yourself:\n" + - "1. Identify yourself by your TeaSpeak forum account\n" + - "2. Identify by an own generated cryptographic identity\n" + - "The second methods is also known as a TeamSpeak 3 identity.\n" + - "\n" + - "To continue click anywhere on the screen." - )); - update_position(); - container_help_text.off('resize').on('resize', update_position); - - $("body").one('mousedown', event => show_identity_settings_help()); - }; - - const show_identity_settings_help = () => { - container.find(".highlighted").removeClass("highlighted"); - container_identity_settings.addClass("highlighted"); - - const update_position = () => { - const font_size = parseFloat(getComputedStyle(container_help_text[0]).fontSize); - const container_identity_offset = container_identity_settings.offset(); - const right = container_profile_settings.outerWidth() + font_size * 2; - container_help_text.css({ - top: container_identity_offset.top - container.offset().top, - left: "1em", - right: right + "px", - bottom: "1em" - }); - }; - set_help_text(tr( - "When selecting an identify type, some corresponding will pop up in the highlighted area.\n" + - "\n" + - "But don't worry, we've already generated\n" + - "a cryptographic identity for you!\n" + - "So you don't have to change anything before you start." - )); - update_position(); - container_help_text.off('resize').on('resize', update_position); - - $("body").one('mousedown', event => hide_help()); - }; - - const hide_help = () => { - container.find(".highlighted").removeClass("highlighted"); - container.addClass("hide-help"); - setTimeout(() => container.removeClass("help-shown"), 1000); - container_help_text.off('resize'); - - help_animation_done = true; - update_step_status(); - }; - } + }; } +} - function initializeStepMicrophone(tag: JQuery, event_registry: events.Registry, modal: Modal) { - const microphone_events = new events.Registry(); - //microphone_events.enable_debug("settings-microphone"); - modal_settings.initialize_audio_microphone_controller(microphone_events); - modal_settings.initialize_audio_microphone_view(tag, microphone_events); - modal.close_listener.push(() => microphone_events.fire_async("deinitialize")); +function initializeStepMicrophone(tag: JQuery, event_registry: Registry, modal: Modal) { + const microphone_events = new Registry(); + //microphone_events.enable_debug("settings-microphone"); + modal_settings.initialize_audio_microphone_controller(microphone_events); + modal_settings.initialize_audio_microphone_view(tag, microphone_events); + modal.close_listener.push(() => microphone_events.fire_async("deinitialize")); - let help_animation_done = false; - const update_step_status = () => event_registry.fire_async("step-status", { next_button: help_animation_done, previous_button: help_animation_done }); - event_registry.on("show_step", e => { - if(e.step !== "microphone") return; + let help_animation_done = false; + const update_step_status = () => event_registry.fire_async("step-status", { next_button: help_animation_done, previous_button: help_animation_done }); + event_registry.on("show_step", e => { + if(e.step !== "microphone") return; - update_step_status(); + update_step_status(); + }); + + /* the help sequence */ + { + const container = tag.find(".container-settings-audio-microphone"); + const container_help_text = tag.find(".container-help-text"); + + const container_profile_list = tag.find(".highlight-microphone-list"); + const container_profile_settings = tag.find(".highlight-microphone-settings"); + + let is_first_show = true; + event_registry.on("show_step", event => { + if(!is_first_show || event.step !== "microphone") return; + is_first_show = false; + + container.addClass("help-shown"); + const text = tr( + "Firstly we need to setup a microphone.\n" + + "Let me guide you thru the basic UI elements.\n" + + "\n" + + "To continue click anywhere on the screen." + ); + set_help_text(text); + $("body").one('mousedown', event => show_microphone_list_help()); }); - /* the help sequence */ - { - const container = tag.find(".container-settings-audio-microphone"); - const container_help_text = tag.find(".container-help-text"); + const set_help_text = text => { + container_help_text.empty(); + text.split("\n").forEach(e => container_help_text.append(e == "" ? $.spawn("br") : $.spawn("a").text(e))); + }; - const container_profile_list = tag.find(".highlight-microphone-list"); - const container_profile_settings = tag.find(".highlight-microphone-settings"); + const show_microphone_list_help = () => { + container.find(".highlighted").removeClass("highlighted"); + container_profile_list.addClass("highlighted"); - let is_first_show = true; - event_registry.on("show_step", event => { - if(!is_first_show || event.step !== "microphone") return; - is_first_show = false; + const update_position = () => { + const font_size = parseFloat(getComputedStyle(container_help_text[0]).fontSize); - container.addClass("help-shown"); - const text = tr( - "Firstly we need to setup a microphone.\n" + - "Let me guide you thru the basic UI elements.\n" + - "\n" + - "To continue click anywhere on the screen." - ); - set_help_text(text); - $("body").one('mousedown', event => show_microphone_list_help()); - }); + const offset = container_profile_list.offset(); + const abs = container.offset(); - const set_help_text = text => { - container_help_text.empty(); - text.split("\n").forEach(e => container_help_text.append(e == "" ? $.spawn("br") : $.spawn("a").text(e))); + container_help_text.css({ + top: offset.top - abs.top, + left: ((offset.left - abs.left) + container_profile_list.outerWidth() + font_size) + "px", + right: "1em", + bottom: "1em" + }); + }; + update_position(); + container_help_text.off('resize').on('resize', update_position); + + const text = tr( + "All your available microphones are listed within this box.\n" + + "\n" + + "The currently selected microphone\n" + + "is marked with a green checkmark. To change the selected microphone\n" + + "just click on the new one.\n" + + "\n" + + "To continue click anywhere on the screen." + ); + set_help_text(text); + $("body").one('mousedown', event => show_microphone_settings_help()); + }; + + const show_microphone_settings_help = () => { + container.find(".highlighted").removeClass("highlighted"); + container_profile_settings.addClass("highlighted"); + + const update_position = () => { + const font_size = parseFloat(getComputedStyle(container_help_text[0]).fontSize); + const container_settings_offset = container_profile_settings.offset(); + const right = container_profile_settings.outerWidth() + font_size * 2; + container_help_text.css({ + top: container_settings_offset.top - container.offset().top, + left: "1em", + right: right + "px", + bottom: "1em" + }); }; - const show_microphone_list_help = () => { - container.find(".highlighted").removeClass("highlighted"); - container_profile_list.addClass("highlighted"); - - const update_position = () => { - const font_size = parseFloat(getComputedStyle(container_help_text[0]).fontSize); - - const offset = container_profile_list.offset(); - const abs = container.offset(); - - container_help_text.css({ - top: offset.top - abs.top, - left: ((offset.left - abs.left) + container_profile_list.outerWidth() + font_size) + "px", - right: "1em", - bottom: "1em" - }); - }; - update_position(); - container_help_text.off('resize').on('resize', update_position); - - const text = tr( - "All your available microphones are listed within this box.\n" + - "\n" + - "The currently selected microphone\n" + - "is marked with a green checkmark. To change the selected microphone\n" + - "just click on the new one.\n" + - "\n" + - "To continue click anywhere on the screen." - ); - set_help_text(text); - $("body").one('mousedown', event => show_microphone_settings_help()); - }; - - const show_microphone_settings_help = () => { - container.find(".highlighted").removeClass("highlighted"); - container_profile_settings.addClass("highlighted"); - - const update_position = () => { - const font_size = parseFloat(getComputedStyle(container_help_text[0]).fontSize); - const container_settings_offset = container_profile_settings.offset(); - const right = container_profile_settings.outerWidth() + font_size * 2; - container_help_text.css({ - top: container_settings_offset.top - container.offset().top, - left: "1em", - right: right + "px", - bottom: "1em" - }); - }; - - container_help_text.empty(); - container_help_text.append($.spawn("div").addClass("help-microphone-settings").append( - $.spawn("a").text(tr("On the right side you'll find all microphone settings.")), - $.spawn("br"), - $.spawn("a").text("TeaSpeak has three voice activity detection types:"), - $.spawn("ol").append( - $.spawn("li").addClass("vad-type").append( - $.spawn("a").addClass("title").text(tr("Push to Talk")), - $.spawn("a").addClass("description").html(tr( - "To transmit audio data you'll have to
" + - "press a key. The key could be selected " + - "via the button right to the radio button." - )) - ), - $.spawn("li").addClass("vad-type").append( - $.spawn("a").addClass("title").text(tr("Voice activity detection")), - $.spawn("a").addClass("description").html(tr( - "In this mode, TeaSpeak will continuously analyze your microphone input. " + - "If the audio level is grater than a certain threshold, " + - "the audio will be transmitted. " + - "The threshold is changeable via the \"Sensitivity Settings\" slider." - )) - ), - $.spawn("li").addClass("vad-type").append( - $.spawn("a").addClass("title").html(tr("Always active")), - $.spawn("a").addClass("description").text(tr( - "Continuously transmit any audio data.\n" - )) - ) + container_help_text.empty(); + container_help_text.append($.spawn("div").addClass("help-microphone-settings").append( + $.spawn("a").text(tr("On the right side you'll find all microphone settings.")), + $.spawn("br"), + $.spawn("a").text("TeaSpeak has three voice activity detection types:"), + $.spawn("ol").append( + $.spawn("li").addClass("vad-type").append( + $.spawn("a").addClass("title").text(tr("Push to Talk")), + $.spawn("a").addClass("description").html(tr( + "To transmit audio data you'll have to
" + + "press a key. The key could be selected " + + "via the button right to the radio button." + )) ), - $.spawn("br"), - $.spawn("a").text(tr("Now you're ready to configure your microphone. Just click anywhere on the screen.")) - )); - update_position(); - container_help_text.off('resize').on('resize', update_position); + $.spawn("li").addClass("vad-type").append( + $.spawn("a").addClass("title").text(tr("Voice activity detection")), + $.spawn("a").addClass("description").html(tr( + "In this mode, TeaSpeak will continuously analyze your microphone input. " + + "If the audio level is grater than a certain threshold, " + + "the audio will be transmitted. " + + "The threshold is changeable via the \"Sensitivity Settings\" slider." + )) + ), + $.spawn("li").addClass("vad-type").append( + $.spawn("a").addClass("title").html(tr("Always active")), + $.spawn("a").addClass("description").text(tr( + "Continuously transmit any audio data.\n" + )) + ) + ), + $.spawn("br"), + $.spawn("a").text(tr("Now you're ready to configure your microphone. Just click anywhere on the screen.")) + )); + update_position(); + container_help_text.off('resize').on('resize', update_position); - $("body").one('mousedown', event => hide_help()); - }; + $("body").one('mousedown', event => hide_help()); + }; - const hide_help = () => { - container.find(".highlighted").removeClass("highlighted"); - container.addClass("hide-help"); - setTimeout(() => container.removeClass("help-shown"), 1000); - container_help_text.off('resize'); + const hide_help = () => { + container.find(".highlighted").removeClass("highlighted"); + container.addClass("hide-help"); + setTimeout(() => container.removeClass("help-shown"), 1000); + container_help_text.off('resize'); - help_animation_done = true; - update_step_status(); - }; - } + help_animation_done = true; + update_step_status(); + }; } } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalPlaylistEdit.ts b/shared/js/ui/modal/ModalPlaylistEdit.ts index 69ec39c5..35c8533f 100644 --- a/shared/js/ui/modal/ModalPlaylistEdit.ts +++ b/shared/js/ui/modal/ModalPlaylistEdit.ts @@ -1,360 +1,360 @@ -/// -/// -/// +import {CommandResult, Playlist, PlaylistSong} from "tc-shared/connection/ServerConnectionDeclaration"; +import {createErrorModal, createModal, Modal} from "tc-shared/ui/elements/Modal"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; +import PermissionType from "tc-shared/permission/PermissionType"; -namespace Modals { - export function spawnPlaylistSongInfo(song: PlaylistSong) { - let modal: Modal; +export function spawnPlaylistSongInfo(song: PlaylistSong) { + let modal: Modal; - modal = createModal({ - header: tr("Song info"), - body: () => { - try { - (song).metadata = JSON.parse(song.song_metadata); - } catch(e) {} + modal = createModal({ + header: tr("Song info"), + body: () => { + try { + (song).metadata = JSON.parse(song.song_metadata); + } catch(e) {} - let template = $("#tmpl_playlist_edit-song_info").renderTag(song); - template = $.spawn("div").append(template); - const text_area = template.find(".property-metadata-raw textarea"); + let template = $("#tmpl_playlist_edit-song_info").renderTag(song); + template = $.spawn("div").append(template); + const text_area = template.find(".property-metadata-raw textarea"); - template.find(".toggle-metadata").on('click', event => { - if(text_area.is(":visible")) { - template.find(".toggle-metadata").text("show"); - } else { - template.find(".toggle-metadata").text("hide"); - } - text_area.slideToggle({duration: 250}); - }); - text_area.hide(); - - return template; - }, - footer: undefined, - width: 750 - }); - - modal.open(); - } - - export function spawnSongAdd(playlist: Playlist, callback_add: (url: string, loader: string) => any) { - let modal: Modal; - - modal = createModal({ - header: tr("Add a song"), - body: () => { - let template = $("#tmpl_playlist_edit-song_add").renderTag(); - template = $.spawn("div").append(template); - - const url = template.find(".property-url .value"); - const url_loader = template.find(".property-loader .value"); - const button_add = template.find(".container-buttons .button-add"); - const button_cancel = template.find(".container-buttons .button-cancel"); - - url.on('change keyup', event => { - button_add.prop("disabled", url.val().toString().length == 0); - }).trigger('change'); - - button_cancel.on('click', event => modal.close()); - button_add.on('click', event => { - callback_add(url.val() as string, url_loader.val() as string); - modal.close(); - }); - return template; - }, - footer: undefined, - width: 750 - }); - - modal.open(); - } - - export function spawnPlaylistEdit(client: ConnectionHandler, playlist: Playlist) { - { - createErrorModal(tr("Not implemented"), tr("Playlist editing hasn't yet been implemented")).open(); - return; - } - - let modal: Modal; - let changed_properties = {}; - let changed_permissions = {}; - let callback_permission_update: () => any; - - const update_save = () => { - const save_button = modal.htmlTag.find(".buttons .button-save"); - save_button.prop("disabled", (Object.keys(changed_properties).length + Object.keys(changed_permissions).length) == 0); - }; - - modal = createModal({ - header: tr("Edit playlist"), - body: () => { - let template = $("#tmpl_playlist_edit").renderTag().tabify(); - - callback_permission_update = apply_permissions(template, client, playlist, (key, value) => { - console.log("Change permission %o => %o", key, value); - changed_permissions[key] = value; - update_save(); - }); - const callback_song_id = apply_songs(template, client, playlist); - apply_properties(template, client, playlist, (key, value) => { - console.log("Change property %o => %o", key, value); - changed_properties[key] = value; - update_save(); - }, callback_song_id); - - template.find(".buttons .button-save").on('click', event => { - if(Object.keys(changed_properties).length != 0) { - changed_properties["playlist_id"] = playlist.playlist_id; - client.serverConnection.send_command("playlistedit", changed_properties).then(() => { - changed_properties = {}; - update_save(); - }).catch(error => { - if(error instanceof CommandResult) - error = error.extra_message || error.message; - createErrorModal(tr("Failed to change properties."), tr("Failed to change playlist properties.
Error: ") + error).open(); - }); - } - if(Object.keys(changed_permissions).length != 0) { - const array: any[] = []; - - for(const permission_key of Object.keys(changed_permissions)) { - array.push({ - permvalue: changed_permissions[permission_key], - permnegated: false, - permskip: false, - permsid: permission_key - }); - } - - array[0]["playlist_id"] = playlist.playlist_id; - client.serverConnection.send_command("playlistaddperm", array).then(() => { - changed_permissions = {}; - update_save(); - }).catch(error => { - if(error instanceof CommandResult) - error = error.extra_message || error.message; - createErrorModal(tr("Failed to change permission."), tr("Failed to change playlist permissions.
Error: ") + error).open(); - }); - } - }); - - template.find(".buttons .button-close").on('click', event => { - if((Object.keys(changed_properties).length + Object.keys(changed_permissions).length) != 0) { - spawnYesNo(tr("Are you sure?"), tr("Do you really want to discard all your changes?"), result => { - if(result) - modal.close(); - }); - return; - } - modal.close(); - }); - return template; - }, - footer: undefined, - width: 750 - }); - update_save(); - - modal.open(); - return modal; - } - - function apply_songs(tag: JQuery, client: ConnectionHandler, playlist: Playlist) { - const owns_playlist = playlist.playlist_owner_dbid == client.getClient().properties.client_database_id; - const song_tag = tag.find(".container-songs"); - - let replaying_song_id: number = 0; - let selected_song: PlaylistSong; - - const set_song_info = (text: string) => { - const tag = song_tag.find(".info-message"); - if(text && text.length > 0) { - tag.text(text).show(); - } else - tag.hide(); - }; - - const set_current_song = (id: number) => { - /* this method shall enforce an update */ - replaying_song_id = id; - update_songs(); - }; - - const update_songs = () => { - set_song_info(tr("loading song list")); - client.serverConnection.command_helper.request_playlist_songs(playlist.playlist_id).then(result => { - const entries_tag = song_tag.find(".song-list-entries"); - const entry_template = $("#tmpl_playlist_edit-song_entry"); - entries_tag.empty(); - - for(const song of result) { - const rendered = entry_template.renderTag(song); - - rendered.find(".button-info").on('click', event => { - spawnPlaylistSongInfo(song); - }); - - const button_delete = rendered.find(".button-delete"); - if(!owns_playlist && !client.permissions.neededPermission(PermissionType.I_PLAYLIST_SONG_REMOVE_POWER).granted(playlist.needed_power_song_remove)) - button_delete.detach(); - else - button_delete.on('click', event => { - client.serverConnection.send_command("playlistsongremove", { - playlist_id: playlist.playlist_id, - song_id: song.song_id - }).then(() => { - rendered.slideToggle({duration: 250, done(animation: JQuery.Promise, jumpedToEnd: boolean): void { - rendered.detach(); - }}); - rendered.hide(250); - }).catch(error => { - if(error instanceof CommandResult) - error = error.extra_message || error.message; - createErrorModal(tr("Failed to remove song."), tr("Failed to remove song/url from the playlist.
Error: ") + error).open(); - }); - }); - - if(song.song_id == replaying_song_id) - rendered.addClass("playing"); - - rendered.on('click', event => { - selected_song = song; - entries_tag.find(".selected").removeClass("selected"); - rendered.addClass("selected"); - }); - - entries_tag.append(rendered); + template.find(".toggle-metadata").on('click', event => { + if(text_area.is(":visible")) { + template.find(".toggle-metadata").text("show"); + } else { + template.find(".toggle-metadata").text("hide"); } - - const entry_container = song_tag.find(".song-list-entries-container"); - if(entry_container.hasScrollBar()) - entry_container.addClass("scrollbar"); - - set_song_info("displaying " + result.length + " songs"); - }).catch(error => { - console.error(error); - set_song_info(tr("failed to load song list")); - //TODO improve error handling! + text_area.slideToggle({duration: 250}); }); - }; + text_area.hide(); - song_tag.find(".button-refresh").on('click', event => update_songs()); - song_tag.find(".button-song-add").on('click', event => { - spawnSongAdd(playlist, (url, loader) => { - //playlist_id invoker previous url - client.serverConnection.send_command("playlistsongadd", { - playlist_id: playlist.playlist_id, - invoker: loader, - url: url - }).then(() => { - update_songs(); - }).catch(error => { - if(error instanceof CommandResult) - error = error.extra_message || error.message; - createErrorModal(tr("Failed to add song."), tr("Failed to add song/url to the playlist.
Error: ") + error).open(); - }); + return template; + }, + footer: undefined, + width: 750 + }); + + modal.open(); +} + +export function spawnSongAdd(playlist: Playlist, callback_add: (url: string, loader: string) => any) { + let modal: Modal; + + modal = createModal({ + header: tr("Add a song"), + body: () => { + let template = $("#tmpl_playlist_edit-song_add").renderTag(); + template = $.spawn("div").append(template); + + const url = template.find(".property-url .value"); + const url_loader = template.find(".property-loader .value"); + const button_add = template.find(".container-buttons .button-add"); + const button_cancel = template.find(".container-buttons .button-cancel"); + + url.on('change keyup', event => { + button_add.prop("disabled", url.val().toString().length == 0); + }).trigger('change'); + + button_cancel.on('click', event => modal.close()); + button_add.on('click', event => { + callback_add(url.val() as string, url_loader.val() as string); + modal.close(); }); - }).prop("disabled", !owns_playlist && !client.permissions.neededPermission(PermissionType.I_PLAYLIST_SONG_ADD_POWER).granted(playlist.needed_power_song_add)); - /* setTimeout(update_songs, 100); */ /* We dont have to call that here because it will get called over set_current_song when we received the current song id */ + return template; + }, + footer: undefined, + width: 750 + }); - return set_current_song; + modal.open(); +} + +export function spawnPlaylistEdit(client: ConnectionHandler, playlist: Playlist) { + { + createErrorModal(tr("Not implemented"), tr("Playlist editing hasn't yet been implemented")).open(); + return; } - function apply_permissions(tag: JQuery, client: ConnectionHandler, playlist: Playlist, change_permission: (key: string, value: number) => any) { - const owns_playlist = playlist.playlist_owner_dbid == client.getClient().properties.client_database_id; - const permission_tag = tag.find(".container-permissions"); - const nopermission_tag = tag.find(".container-no-permissions"); + let modal: Modal; + let changed_properties = {}; + let changed_permissions = {}; + let callback_permission_update: () => any; - const update_permissions = () => { - if(!client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_PLAYLIST_PERMISSION_LIST).granted(1)) { - nopermission_tag.show(); - permission_tag.hide(); - } else { - nopermission_tag.hide(); - permission_tag.show(); + const update_save = () => { + const save_button = modal.htmlTag.find(".buttons .button-save"); + save_button.prop("disabled", (Object.keys(changed_properties).length + Object.keys(changed_permissions).length) == 0); + }; - permission_tag.find(".permission input").prop("disabled", true); - client.permissions.requestPlaylistPermissions(playlist.playlist_id).then(permissions => { - permission_tag.find(".permission input") - .val(0) - .prop("disabled", !owns_playlist && !client.permissions.neededPermission(PermissionType.I_PLAYLIST_PERMISSION_MODIFY_POWER).granted(playlist.needed_power_permission_modify)); + modal = createModal({ + header: tr("Edit playlist"), + body: () => { + let template = $("#tmpl_playlist_edit").renderTag().tabify(); - for(const permission of permissions) { - const tag = permission_tag.find(".permission[permission='" + permission.type.name + "']"); - if(permission.value != -2) - tag.find("input").val(permission.value); + callback_permission_update = apply_permissions(template, client, playlist, (key, value) => { + console.log("Change permission %o => %o", key, value); + changed_permissions[key] = value; + update_save(); + }); + const callback_song_id = apply_songs(template, client, playlist); + apply_properties(template, client, playlist, (key, value) => { + console.log("Change property %o => %o", key, value); + changed_properties[key] = value; + update_save(); + }, callback_song_id); + + template.find(".buttons .button-save").on('click', event => { + if(Object.keys(changed_properties).length != 0) { + changed_properties["playlist_id"] = playlist.playlist_id; + client.serverConnection.send_command("playlistedit", changed_properties).then(() => { + changed_properties = {}; + update_save(); + }).catch(error => { + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Failed to change properties."), tr("Failed to change playlist properties.
Error: ") + error).open(); + }); + } + if(Object.keys(changed_permissions).length != 0) { + const array: any[] = []; + + for(const permission_key of Object.keys(changed_permissions)) { + array.push({ + permvalue: changed_permissions[permission_key], + permnegated: false, + permskip: false, + permsid: permission_key + }); } - }); - } - }; - permission_tag.find(".permission").each((index, _element) => { - const element = $(_element); - element.find("input").on('change', event => { - console.log(element.find("input").val()); - change_permission(element.attr("permission"), parseInt(element.find("input").val().toString())); + array[0]["playlist_id"] = playlist.playlist_id; + client.serverConnection.send_command("playlistaddperm", array).then(() => { + changed_permissions = {}; + update_save(); + }).catch(error => { + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Failed to change permission."), tr("Failed to change playlist permissions.
Error: ") + error).open(); + }); + } + }); + + template.find(".buttons .button-close").on('click', event => { + if((Object.keys(changed_properties).length + Object.keys(changed_permissions).length) != 0) { + spawnYesNo(tr("Are you sure?"), tr("Do you really want to discard all your changes?"), result => { + if(result) + modal.close(); + }); + return; + } + modal.close(); + }); + return template; + }, + footer: undefined, + width: 750 + }); + update_save(); + + modal.open(); + return modal; +} + +function apply_songs(tag: JQuery, client: ConnectionHandler, playlist: Playlist) { + const owns_playlist = playlist.playlist_owner_dbid == client.getClient().properties.client_database_id; + const song_tag = tag.find(".container-songs"); + + let replaying_song_id: number = 0; + let selected_song: PlaylistSong; + + const set_song_info = (text: string) => { + const tag = song_tag.find(".info-message"); + if(text && text.length > 0) { + tag.text(text).show(); + } else + tag.hide(); + }; + + const set_current_song = (id: number) => { + /* this method shall enforce an update */ + replaying_song_id = id; + update_songs(); + }; + + const update_songs = () => { + set_song_info(tr("loading song list")); + client.serverConnection.command_helper.request_playlist_songs(playlist.playlist_id).then(result => { + const entries_tag = song_tag.find(".song-list-entries"); + const entry_template = $("#tmpl_playlist_edit-song_entry"); + entries_tag.empty(); + + for(const song of result) { + const rendered = entry_template.renderTag(song); + + rendered.find(".button-info").on('click', event => { + spawnPlaylistSongInfo(song); + }); + + const button_delete = rendered.find(".button-delete"); + if(!owns_playlist && !client.permissions.neededPermission(PermissionType.I_PLAYLIST_SONG_REMOVE_POWER).granted(playlist.needed_power_song_remove)) + button_delete.detach(); + else + button_delete.on('click', event => { + client.serverConnection.send_command("playlistsongremove", { + playlist_id: playlist.playlist_id, + song_id: song.song_id + }).then(() => { + rendered.slideToggle({duration: 250, done(animation: JQuery.Promise, jumpedToEnd: boolean): void { + rendered.detach(); + }}); + rendered.hide(250); + }).catch(error => { + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Failed to remove song."), tr("Failed to remove song/url from the playlist.
Error: ") + error).open(); + }); + }); + + if(song.song_id == replaying_song_id) + rendered.addClass("playing"); + + rendered.on('click', event => { + selected_song = song; + entries_tag.find(".selected").removeClass("selected"); + rendered.addClass("selected"); + }); + + entries_tag.append(rendered); + } + + const entry_container = song_tag.find(".song-list-entries-container"); + if(entry_container.hasScrollBar()) + entry_container.addClass("scrollbar"); + + set_song_info("displaying " + result.length + " songs"); + }).catch(error => { + console.error(error); + set_song_info(tr("failed to load song list")); + //TODO improve error handling! + }); + }; + + song_tag.find(".button-refresh").on('click', event => update_songs()); + song_tag.find(".button-song-add").on('click', event => { + spawnSongAdd(playlist, (url, loader) => { + //playlist_id invoker previous url + client.serverConnection.send_command("playlistsongadd", { + playlist_id: playlist.playlist_id, + invoker: loader, + url: url + }).then(() => { + update_songs(); + }).catch(error => { + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Failed to add song."), tr("Failed to add song/url to the playlist.
Error: ") + error).open(); }); }); + }).prop("disabled", !owns_playlist && !client.permissions.neededPermission(PermissionType.I_PLAYLIST_SONG_ADD_POWER).granted(playlist.needed_power_song_add)); + /* setTimeout(update_songs, 100); */ /* We dont have to call that here because it will get called over set_current_song when we received the current song id */ - update_permissions(); - return update_permissions; - } + return set_current_song; +} - function apply_properties(tag: JQuery, client: ConnectionHandler, playlist: Playlist, change_property: (key: string, value: string) => any, callback_current_song: (id: number) => any) { - const owns_playlist = playlist.playlist_owner_dbid == client.getClient().properties.client_database_id; +function apply_permissions(tag: JQuery, client: ConnectionHandler, playlist: Playlist, change_permission: (key: string, value: number) => any) { + const owns_playlist = playlist.playlist_owner_dbid == client.getClient().properties.client_database_id; + const permission_tag = tag.find(".container-permissions"); + const nopermission_tag = tag.find(".container-no-permissions"); - client.serverConnection.command_helper.request_playlist_info(playlist.playlist_id).then(info => { - tag.find(".property-owner input") - .val(info.playlist_owner_name + " (" + info.playlist_owner_dbid + ")"); + const update_permissions = () => { + if(!client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_PLAYLIST_PERMISSION_LIST).granted(1)) { + nopermission_tag.show(); + permission_tag.hide(); + } else { + nopermission_tag.hide(); + permission_tag.show(); - tag.find(".property-title input") - .val(info.playlist_title) - .prop("disabled", !owns_playlist && !client.permissions.neededPermission(PermissionType.I_PLAYLIST_MODIFY_POWER).granted(playlist.needed_power_modify)) - .on('change', event => { - change_property("playlist_title", (event.target).value); - }); + permission_tag.find(".permission input").prop("disabled", true); + client.permissions.requestPlaylistPermissions(playlist.playlist_id).then(permissions => { + permission_tag.find(".permission input") + .val(0) + .prop("disabled", !owns_playlist && !client.permissions.neededPermission(PermissionType.I_PLAYLIST_PERMISSION_MODIFY_POWER).granted(playlist.needed_power_permission_modify)); - tag.find(".property-description textarea") - .val(info.playlist_description) - .prop("disabled", !owns_playlist && !client.permissions.neededPermission(PermissionType.I_PLAYLIST_MODIFY_POWER).granted(playlist.needed_power_modify)) - .on('change', event => { - change_property("playlist_description", (event.target).value); - }); + for(const permission of permissions) { + const tag = permission_tag.find(".permission[permission='" + permission.type.name + "']"); + if(permission.value != -2) + tag.find("input").val(permission.value); + } + }); + } + }; - tag.find(".property-type select") - .val(info.playlist_type.toString()) - .prop("disabled", !owns_playlist && !client.permissions.neededPermission(PermissionType.I_PLAYLIST_MODIFY_POWER).granted(playlist.needed_power_modify)) - .on('change', event => { - change_property("playlist_description", (event.target).selectedIndex.toString()); - }); - - tag.find(".property-replay-mode select") - .val(info.playlist_replay_mode.toString()) - .prop("disabled", !owns_playlist && !client.permissions.neededPermission(PermissionType.I_PLAYLIST_MODIFY_POWER).granted(playlist.needed_power_modify)) - .on('change', event => { - change_property("playlist_replay_mode", (event.target).selectedIndex.toString()); - }); - - tag.find(".property-flag-delete-played input") - .prop("checked", info.playlist_flag_delete_played) - .prop("disabled", !owns_playlist && !client.permissions.neededPermission(PermissionType.I_PLAYLIST_MODIFY_POWER).granted(playlist.needed_power_modify)) - .on('change', event => { - change_property("playlist_flag_delete_played", (event.target).checked ? "1" : "0"); - }); - - tag.find(".property-current-song input") - .val(info.playlist_current_song_id); - callback_current_song(info.playlist_current_song_id); - - tag.find(".property-flag-finished input") - .prop("checked", info.playlist_flag_finished) - .prop("disabled", !owns_playlist && !client.permissions.neededPermission(PermissionType.I_PLAYLIST_MODIFY_POWER).granted(playlist.needed_power_modify)) - .on('change', event => { - change_property("playlist_flag_finished", (event.target).checked ? "1" : "0"); - }); - }).catch(error => { - if(error instanceof CommandResult) - error = error.extra_message || error.message; - createErrorModal(tr("Failed to query playlist info"), tr("Failed to query playlist info.
Error:") + error).open(); + permission_tag.find(".permission").each((index, _element) => { + const element = $(_element); + element.find("input").on('change', event => { + console.log(element.find("input").val()); + change_permission(element.attr("permission"), parseInt(element.find("input").val().toString())); }); - } + }); + + update_permissions(); + return update_permissions; +} + +function apply_properties(tag: JQuery, client: ConnectionHandler, playlist: Playlist, change_property: (key: string, value: string) => any, callback_current_song: (id: number) => any) { + const owns_playlist = playlist.playlist_owner_dbid == client.getClient().properties.client_database_id; + + client.serverConnection.command_helper.request_playlist_info(playlist.playlist_id).then(info => { + tag.find(".property-owner input") + .val(info.playlist_owner_name + " (" + info.playlist_owner_dbid + ")"); + + tag.find(".property-title input") + .val(info.playlist_title) + .prop("disabled", !owns_playlist && !client.permissions.neededPermission(PermissionType.I_PLAYLIST_MODIFY_POWER).granted(playlist.needed_power_modify)) + .on('change', event => { + change_property("playlist_title", (event.target).value); + }); + + tag.find(".property-description textarea") + .val(info.playlist_description) + .prop("disabled", !owns_playlist && !client.permissions.neededPermission(PermissionType.I_PLAYLIST_MODIFY_POWER).granted(playlist.needed_power_modify)) + .on('change', event => { + change_property("playlist_description", (event.target).value); + }); + + tag.find(".property-type select") + .val(info.playlist_type.toString()) + .prop("disabled", !owns_playlist && !client.permissions.neededPermission(PermissionType.I_PLAYLIST_MODIFY_POWER).granted(playlist.needed_power_modify)) + .on('change', event => { + change_property("playlist_description", (event.target).selectedIndex.toString()); + }); + + tag.find(".property-replay-mode select") + .val(info.playlist_replay_mode.toString()) + .prop("disabled", !owns_playlist && !client.permissions.neededPermission(PermissionType.I_PLAYLIST_MODIFY_POWER).granted(playlist.needed_power_modify)) + .on('change', event => { + change_property("playlist_replay_mode", (event.target).selectedIndex.toString()); + }); + + tag.find(".property-flag-delete-played input") + .prop("checked", info.playlist_flag_delete_played) + .prop("disabled", !owns_playlist && !client.permissions.neededPermission(PermissionType.I_PLAYLIST_MODIFY_POWER).granted(playlist.needed_power_modify)) + .on('change', event => { + change_property("playlist_flag_delete_played", (event.target).checked ? "1" : "0"); + }); + + tag.find(".property-current-song input") + .val(info.playlist_current_song_id); + callback_current_song(info.playlist_current_song_id); + + tag.find(".property-flag-finished input") + .prop("checked", info.playlist_flag_finished) + .prop("disabled", !owns_playlist && !client.permissions.neededPermission(PermissionType.I_PLAYLIST_MODIFY_POWER).granted(playlist.needed_power_modify)) + .on('change', event => { + change_property("playlist_flag_finished", (event.target).checked ? "1" : "0"); + }); + }).catch(error => { + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Failed to query playlist info"), tr("Failed to query playlist info.
Error:") + error).open(); + }); } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalPlaylistList.ts b/shared/js/ui/modal/ModalPlaylistList.ts index 58c7451c..f7d3c243 100644 --- a/shared/js/ui/modal/ModalPlaylistList.ts +++ b/shared/js/ui/modal/ModalPlaylistList.ts @@ -1,194 +1,196 @@ -/// -/// -/// -/// +import {settings} from "tc-shared/settings"; +import {createErrorModal, createInfoModal, createModal, Modal} from "tc-shared/ui/elements/Modal"; +import {CommandResult, Playlist} from "tc-shared/connection/ServerConnectionDeclaration"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import PermissionType from "tc-shared/permission/PermissionType"; +import {SingleCommandHandler} from "tc-shared/connection/ConnectionBase"; +import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; +import {spawnPlaylistEdit} from "tc-shared/ui/modal/ModalPlaylistEdit"; -namespace Modals { - export function spawnPlaylistManage(client: ConnectionHandler) { - { - createErrorModal(tr("Not implemented"), tr("Playlist management hasn't yet been implemented")).open(); +export function spawnPlaylistManage(client: ConnectionHandler) { + { + createErrorModal(tr("Not implemented"), tr("Playlist management hasn't yet been implemented")).open(); + return; + } + + + let modal: Modal; + let selected_playlist: Playlist; + let available_playlists: Playlist[]; + let highlight_own = settings.global("playlist-list-highlight-own", true); + + const update_selected = () => { + const buttons = modal.htmlTag.find(".header .buttons"); + + buttons.find(".button-playlist-edit").prop( + "disabled", + !selected_playlist + ); + buttons.find(".button-playlist-delete").prop( + "disabled", + !selected_playlist || !( /* not owner or permission */ + client.permissions.neededPermission(PermissionType.I_PLAYLIST_DELETE_POWER).granted(selected_playlist.needed_power_delete) || /* client has permissions */ + client.getClient().properties.client_database_id == selected_playlist.playlist_owner_dbid /* client is playlist owner */ + ) + ); + buttons.find(".button-playlist-create").prop( + "disabled", + !client.permissions.neededPermission(PermissionType.B_PLAYLIST_CREATE).granted(1) + ); + if(selected_playlist) { + buttons.find(".button-playlist-edit").prop( + "disabled", + false + ); + } + }; + + const update_list = async () => { + const info_tag = modal.htmlTag.find(".footer .info a"); + info_tag.text("loading..."); + + selected_playlist = undefined; + update_selected(); + + try { + available_playlists = await client.serverConnection.command_helper.request_playlist_list(); + } catch(error) { + info_tag.text("failed to query playlist list."); + //FIXME error handling? return; } + const entries_tag = modal.htmlTag.find(".playlist-list-entries"); + const entry_template = $("#tmpl_playlist_list-list_entry"); + entries_tag.empty(); - let modal: Modal; - let selected_playlist: Playlist; - let available_playlists: Playlist[]; - let highlight_own = settings.global("playlist-list-highlight-own", true); + const owndbid = client.getClient().properties.client_database_id; + for(const query of available_playlists) { + const tag = entry_template.renderTag(query).on('click', event => { + entries_tag.find(".entry.selected").removeClass("selected"); + $(event.target).parent(".entry").addClass("selected"); + selected_playlist = query; + update_selected(); + }); - const update_selected = () => { - const buttons = modal.htmlTag.find(".header .buttons"); + if(highlight_own && query.playlist_owner_dbid == owndbid) + tag.addClass("highlighted"); - buttons.find(".button-playlist-edit").prop( - "disabled", - !selected_playlist - ); - buttons.find(".button-playlist-delete").prop( - "disabled", - !selected_playlist || !( /* not owner or permission */ - client.permissions.neededPermission(PermissionType.I_PLAYLIST_DELETE_POWER).granted(selected_playlist.needed_power_delete) || /* client has permissions */ - client.getClient().properties.client_database_id == selected_playlist.playlist_owner_dbid /* client is playlist owner */ - ) - ); - buttons.find(".button-playlist-create").prop( - "disabled", - !client.permissions.neededPermission(PermissionType.B_PLAYLIST_CREATE).granted(1) - ); - if(selected_playlist) { - buttons.find(".button-playlist-edit").prop( - "disabled", - false - ); - } - }; + entries_tag.append(tag); + } - const update_list = async () => { - const info_tag = modal.htmlTag.find(".footer .info a"); - info_tag.text("loading..."); + const entry_container = modal.htmlTag.find(".playlist-list-entries-container"); + if(entry_container.hasScrollBar()) + entry_container.addClass("scrollbar"); - selected_playlist = undefined; - update_selected(); + info_tag.text("Showing " + available_playlists.length + " entries"); + update_selected(); + }; - try { - available_playlists = await client.serverConnection.command_helper.request_playlist_list(); - } catch(error) { - info_tag.text("failed to query playlist list."); - //FIXME error handling? - return; - } + modal = createModal({ + header: tr("Manage playlists"), + body: () => { + let template = $("#tmpl_playlist_list").renderTag(); - const entries_tag = modal.htmlTag.find(".playlist-list-entries"); - const entry_template = $("#tmpl_playlist_list-list_entry"); - entries_tag.empty(); + /* first open the modal */ + setTimeout(() => { + const entry_container = template.find(".playlist-list-entries-container"); + if(entry_container.hasScrollBar()) + entry_container.addClass("scrollbar"); + }, 100); - const owndbid = client.getClient().properties.client_database_id; - for(const query of available_playlists) { - const tag = entry_template.renderTag(query).on('click', event => { - entries_tag.find(".entry.selected").removeClass("selected"); - $(event.target).parent(".entry").addClass("selected"); - selected_playlist = query; - update_selected(); - }); + template.find(".footer .buttons .button-refresh").on('click', update_list); - if(highlight_own && query.playlist_owner_dbid == owndbid) - tag.addClass("highlighted"); - - entries_tag.append(tag); - } - - const entry_container = modal.htmlTag.find(".playlist-list-entries-container"); - if(entry_container.hasScrollBar()) - entry_container.addClass("scrollbar"); - - info_tag.text("Showing " + available_playlists.length + " entries"); - update_selected(); - }; - - modal = createModal({ - header: tr("Manage playlists"), - body: () => { - let template = $("#tmpl_playlist_list").renderTag(); - - /* first open the modal */ - setTimeout(() => { - const entry_container = template.find(".playlist-list-entries-container"); - if(entry_container.hasScrollBar()) - entry_container.addClass("scrollbar"); - }, 100); - - template.find(".footer .buttons .button-refresh").on('click', update_list); - - template.find(".button-playlist-create").on('click', event => { - const single_handler: connection.SingleCommandHandler = { - function: command => { - const json = command.arguments; - update_list().then(() => { - spawnYesNo(tr("Playlist created successful"), tr("The playlist has been successfully created.
Should we open the editor?"), result => { - if(result) { - for(const playlist of available_playlists) { - if(playlist.playlist_id == json[0]["playlist_id"]) { - spawnPlaylistEdit(client, playlist).close_listener.push(update_list); - return; - } + template.find(".button-playlist-create").on('click', event => { + const single_handler: SingleCommandHandler = { + function: command => { + const json = command.arguments; + update_list().then(() => { + spawnYesNo(tr("Playlist created successful"), tr("The playlist has been successfully created.
Should we open the editor?"), result => { + if(result) { + for(const playlist of available_playlists) { + if(playlist.playlist_id == json[0]["playlist_id"]) { + spawnPlaylistEdit(client, playlist).close_listener.push(update_list); + return; } } - }); - }); - - return true; - }, - command: "notifyplaylistcreated" - }; - client.serverConnection.command_handler_boss().register_single_handler(single_handler); - client.serverConnection.send_command("playlistcreate").catch(error => { - client.serverConnection.command_handler_boss().remove_single_handler(single_handler); - if(error instanceof CommandResult) - error = error.extra_message || error.message; - createErrorModal(tr("Unable to create playlist"), tr("Failed to create playlist
Message: ") + error).open(); - }); - }); - - template.find(".button-playlist-edit").on('click', event => { - if(!selected_playlist) return; - spawnPlaylistEdit(client, selected_playlist).close_listener.push(update_list); - }); - - template.find(".button-playlist-delete").on('click', () => { - if(!selected_playlist) return; - - Modals.spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this playlist?"), result => { - if(result) { - client.serverConnection.send_command("playlistdelete", {playlist_id: selected_playlist.playlist_id}).then(() => { - createInfoModal(tr("Playlist deleted successful"), tr("This playlist has been deleted successfully.")).open(); - update_list(); - }).catch(error => { - if(error instanceof CommandResult) { - /* TODO extra handling here */ - //if(error.id == ErrorID.PLAYLIST_IS_IN_USE) { } - error = error.extra_message || error.message; } - createErrorModal(tr("Unable to delete playlist"), tr("Failed to delete playlist
Message: ") + error).open(); }); - } - }); - }); + }); - template.find(".input-search").on('change keyup', () => { - const text = (template.find(".input-search").val() as string || "").toLowerCase(); - if(text.length == 0) { - template.find(".playlist-list-entries .entry").show(); - } else { - template.find(".playlist-list-entries .entry").each((_, e) => { - const element = $(e); - if(element.text().toLowerCase().indexOf(text) == -1) - element.hide(); - else - element.show(); - }) + return true; + }, + command: "notifyplaylistcreated" + }; + client.serverConnection.command_handler_boss().register_single_handler(single_handler); + client.serverConnection.send_command("playlistcreate").catch(error => { + client.serverConnection.command_handler_boss().remove_single_handler(single_handler); + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Unable to create playlist"), tr("Failed to create playlist
Message: ") + error).open(); + }); + }); + + template.find(".button-playlist-edit").on('click', event => { + if(!selected_playlist) return; + spawnPlaylistEdit(client, selected_playlist).close_listener.push(update_list); + }); + + template.find(".button-playlist-delete").on('click', () => { + if(!selected_playlist) return; + + spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this playlist?"), result => { + if(result) { + client.serverConnection.send_command("playlistdelete", {playlist_id: selected_playlist.playlist_id}).then(() => { + createInfoModal(tr("Playlist deleted successful"), tr("This playlist has been deleted successfully.")).open(); + update_list(); + }).catch(error => { + if(error instanceof CommandResult) { + /* TODO extra handling here */ + //if(error.id == ErrorID.PLAYLIST_IS_IN_USE) { } + error = error.extra_message || error.message; + } + createErrorModal(tr("Unable to delete playlist"), tr("Failed to delete playlist
Message: ") + error).open(); + }); } }); + }); - template.find(".button-highlight-own").on('change', event => { - const flag = (event.target).checked; - settings.changeGlobal("playlist-list-highlight-own", flag); - if(flag) { - const owndbid = client.getClient().properties.client_database_id; - template.find(".playlist-list-entries .entry").each((index, _element) => { - const element = $(_element); - if(parseInt(element.attr("playlist-owner-dbid")) == owndbid) - element.addClass("highlighted"); - }) - } else { - template.find(".playlist-list-entries .highlighted").removeClass("highlighted"); - } - }).prop("checked", highlight_own); - return template; - }, - footer: undefined, - width: 750 - }); + template.find(".input-search").on('change keyup', () => { + const text = (template.find(".input-search").val() as string || "").toLowerCase(); + if(text.length == 0) { + template.find(".playlist-list-entries .entry").show(); + } else { + template.find(".playlist-list-entries .entry").each((_, e) => { + const element = $(e); + if(element.text().toLowerCase().indexOf(text) == -1) + element.hide(); + else + element.show(); + }) + } + }); - update_list(); - modal.open(); - } + template.find(".button-highlight-own").on('change', event => { + const flag = (event.target).checked; + settings.changeGlobal("playlist-list-highlight-own", flag); + if(flag) { + const owndbid = client.getClient().properties.client_database_id; + template.find(".playlist-list-entries .entry").each((index, _element) => { + const element = $(_element); + if(parseInt(element.attr("playlist-owner-dbid")) == owndbid) + element.addClass("highlighted"); + }) + } else { + template.find(".playlist-list-entries .highlighted").removeClass("highlighted"); + } + }).prop("checked", highlight_own); + return template; + }, + footer: undefined, + width: 750 + }); + + update_list(); + modal.open(); } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalPoke.ts b/shared/js/ui/modal/ModalPoke.ts index d4769a2c..02ea59df 100644 --- a/shared/js/ui/modal/ModalPoke.ts +++ b/shared/js/ui/modal/ModalPoke.ts @@ -1,94 +1,94 @@ -/// -/// -/// +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {createModal, Modal} from "tc-shared/ui/elements/Modal"; +import * as htmltags from "tc-shared/ui/htmltags"; +import {bbcode_chat} from "tc-shared/ui/frames/chat"; -namespace Modals { - let global_modal: PokeModal; +let global_modal: PokeModal; - interface ServerEntry { - source: ConnectionHandler; - add_message(invoker: PokeInvoker, message: string); +interface ServerEntry { + source: ConnectionHandler; + add_message(invoker: PokeInvoker, message: string); +} + +declare const moment; +class PokeModal { + private _handle: Modal; + private source_map: ServerEntry[] = []; + + constructor() { + this._handle = createModal({ + header: tr("You have been poked!"), + body: () => { + let template = $("#tmpl_poke_popup").renderTag(); + template.find(".button-close").on('click', event => this._handle_close()); + return template; + }, + footer: undefined, + width: 750 + }); + this._handle.close_listener.push(() => this._handle_close()); } - class PokeModal { - private _handle: Modal; - private source_map: ServerEntry[] = []; - - constructor() { - this._handle = createModal({ - header: tr("You have been poked!"), - body: () => { - let template = $("#tmpl_poke_popup").renderTag(); - template.find(".button-close").on('click', event => this._handle_close()); - return template; - }, - footer: undefined, - width: 750 - }); - this._handle.close_listener.push(() => this._handle_close()); - } - - modal() { return this._handle; } - add_poke(source: ConnectionHandler, invoker: PokeInvoker, message: string) { - let handler: ServerEntry; - for(const entry of this.source_map) - if(entry.source === source) { - handler = entry; - break; - } - if(!handler) { - const html_tag = $.spawn("div").addClass("server"); - const poke_list = $.spawn("div").addClass("poke-list"); - $.spawn("div") - .addClass("server-name") - .text(source && source.channelTree && source.channelTree.server ? source.channelTree.server.properties.virtualserver_name : "unknown") - .appendTo(html_tag); - poke_list.appendTo(html_tag); - - this.source_map.push(handler = { - source: source, - add_message: (invoker: PokeInvoker, message: string) => { - const container = $.spawn("div").addClass("entry"); - - $.spawn("div").addClass("date").text(moment().format("HH:mm:ss") + " - ").appendTo(container); - $.spawn("div").addClass("user").append($(htmltags.generate_client({ - add_braces: true, - client_id: invoker.id, - client_name: invoker.name, - client_unique_id: invoker.unique_id - }))).appendTo(container); - if(message) { - $.spawn("div").addClass("text").text(tr("pokes you:")).appendTo(container); - $.spawn("div").addClass("poke-message").append(...MessageHelper.bbcode_chat(message)).appendTo(container); - } else { - $.spawn("div").addClass("text").text(tr("pokes you.")).appendTo(container); - } - - container.appendTo(poke_list); - } - }); - - this._handle.htmlTag.find(".container-servers").append(html_tag); + modal() { return this._handle; } + add_poke(source: ConnectionHandler, invoker: PokeInvoker, message: string) { + let handler: ServerEntry; + for(const entry of this.source_map) + if(entry.source === source) { + handler = entry; + break; } - handler.add_message(invoker, message); - } + if(!handler) { + const html_tag = $.spawn("div").addClass("server"); + const poke_list = $.spawn("div").addClass("poke-list"); + $.spawn("div") + .addClass("server-name") + .text(source && source.channelTree && source.channelTree.server ? source.channelTree.server.properties.virtualserver_name : "unknown") + .appendTo(html_tag); + poke_list.appendTo(html_tag); - private _handle_close() { - this._handle.close(); - global_modal = undefined; + this.source_map.push(handler = { + source: source, + add_message: (invoker: PokeInvoker, message: string) => { + const container = $.spawn("div").addClass("entry"); + + $.spawn("div").addClass("date").text(moment().format("HH:mm:ss") + " - ").appendTo(container); + $.spawn("div").addClass("user").append($(htmltags.generate_client({ + add_braces: true, + client_id: invoker.id, + client_name: invoker.name, + client_unique_id: invoker.unique_id + }))).appendTo(container); + if(message) { + $.spawn("div").addClass("text").text(tr("pokes you:")).appendTo(container); + $.spawn("div").addClass("poke-message").append(...bbcode_chat(message)).appendTo(container); + } else { + $.spawn("div").addClass("text").text(tr("pokes you.")).appendTo(container); + } + + container.appendTo(poke_list); + } + }); + + this._handle.htmlTag.find(".container-servers").append(html_tag); } + handler.add_message(invoker, message); } - export type PokeInvoker = { - name: string, - id: number, - unique_id: string - }; - - export function spawnPoke(source: ConnectionHandler, invoker: PokeInvoker, message: string) { - if(!global_modal) - global_modal = new PokeModal(); - global_modal.add_poke(source, invoker, message); - global_modal.modal().open(); + private _handle_close() { + this._handle.close(); + global_modal = undefined; } +} + +export type PokeInvoker = { + name: string, + id: number, + unique_id: string +}; + +export function spawnPoke(source: ConnectionHandler, invoker: PokeInvoker, message: string) { + if(!global_modal) + global_modal = new PokeModal(); + global_modal.add_poke(source, invoker, message); + global_modal.modal().open(); } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalQuery.ts b/shared/js/ui/modal/ModalQuery.ts index af3e8560..7ef528a2 100644 --- a/shared/js/ui/modal/ModalQuery.ts +++ b/shared/js/ui/modal/ModalQuery.ts @@ -1,86 +1,85 @@ -/// -/// -/// +import {createErrorModal, createModal} from "tc-shared/ui/elements/Modal"; +import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {SingleCommandHandler} from "tc-shared/connection/ConnectionBase"; -namespace Modals { - export function spawnQueryCreate(connection: ConnectionHandler, callback_created?: (user, pass) => any) { - let modal; - modal = createModal({ - header: tr("Create a server query login"), - body: () => { - let template = $("#tmpl_query_create").renderTag(); - template = $.spawn("div").append(template); +export function spawnQueryCreate(connection: ConnectionHandler, callback_created?: (user, pass) => any) { + let modal; + modal = createModal({ + header: tr("Create a server query login"), + body: () => { + let template = $("#tmpl_query_create").renderTag(); + template = $.spawn("div").append(template); - template.find(".button-close").on('click', event => modal.close()); - template.find(".button-create").on('click', event => { - const name = template.find(".input-name").val() as string; - if(name.length < 3 || name.length > 64) { - createErrorModal(tr("Invalid username"), tr("Please enter a valid name!")).open(); - return; - } + template.find(".button-close").on('click', event => modal.close()); + template.find(".button-create").on('click', event => { + const name = template.find(".input-name").val() as string; + if(name.length < 3 || name.length > 64) { + createErrorModal(tr("Invalid username"), tr("Please enter a valid name!")).open(); + return; + } - const single_handler: connection.SingleCommandHandler = { - function: command => { - const json = command.arguments[0]; + const single_handler: SingleCommandHandler = { + function: command => { + const json = command.arguments[0]; - spawnQueryCreated({ - username: name, - password: json.client_login_password - }, true); + spawnQueryCreated({ + username: name, + password: json.client_login_password + }, true); - if(callback_created) - callback_created(name, json.client_login_password); - return true; - }, - command: "notifyquerycreated" - }; - connection.serverConnection.command_handler_boss().register_single_handler(single_handler); - connection.serverConnection.send_command("querycreate", { - client_login_name: name - }).catch(error => { - if(error instanceof CommandResult) - error = error.extra_message || error.message; - createErrorModal(tr("Unable to create account"), tr("Failed to create account
Message: ") + error).open(); - }).then(() => connection.serverConnection.command_handler_boss().remove_single_handler(single_handler)); + if(callback_created) + callback_created(name, json.client_login_password); + return true; + }, + command: "notifyquerycreated" + }; + connection.serverConnection.command_handler_boss().register_single_handler(single_handler); + connection.serverConnection.send_command("querycreate", { + client_login_name: name + }).catch(error => { + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Unable to create account"), tr("Failed to create account
Message: ") + error).open(); + }).then(() => connection.serverConnection.command_handler_boss().remove_single_handler(single_handler)); - modal.close(); - }); - return template; - }, - footer: undefined, - width: 750 - }); - modal.open(); - } + modal.close(); + }); + return template; + }, + footer: undefined, + width: 750 + }); + modal.open(); +} - export function spawnQueryCreated(credentials: { - username: string, - password: string - }, just_created: boolean) { - let modal; - modal = createModal({ - header: just_created ? tr("Server query credentials") : tr("New server query credentials"), - body: () => { - let template = $("#tmpl_query_created").renderTag(credentials); - template = $.spawn("div").append(template); +export function spawnQueryCreated(credentials: { + username: string, + password: string +}, just_created: boolean) { + let modal; + modal = createModal({ + header: just_created ? tr("Server query credentials") : tr("New server query credentials"), + body: () => { + let template = $("#tmpl_query_created").renderTag(credentials); + template = $.spawn("div").append(template); - template.find(".button-close").on('click', event => modal.close()); - template.find(".query_name").text(credentials.username); - template.find(".query_password").text(credentials.password); + template.find(".button-close").on('click', event => modal.close()); + template.find(".query_name").text(credentials.username); + template.find(".query_password").text(credentials.password); - template.find(".btn_copy_name").on('click', () => { - template.find(".query_name").select(); - document.execCommand("copy"); - }); - template.find(".btn_copy_password").on('click', () => { - template.find(".query_password").select(); - document.execCommand("copy"); - }); - return template; - }, - footer: undefined, - width: 750 - }); - modal.open(); - } + template.find(".btn_copy_name").on('click', () => { + template.find(".query_name").select(); + document.execCommand("copy"); + }); + template.find(".btn_copy_password").on('click', () => { + template.find(".query_password").select(); + document.execCommand("copy"); + }); + return template; + }, + footer: undefined, + width: 750 + }); + modal.open(); } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalQueryManage.ts b/shared/js/ui/modal/ModalQueryManage.ts index c856ab83..ce78b233 100644 --- a/shared/js/ui/modal/ModalQueryManage.ts +++ b/shared/js/ui/modal/ModalQueryManage.ts @@ -1,102 +1,385 @@ -/// -/// -/// +/* +export function spawnQueryManage(client: ConnectionHandler) { + let modal: Modal; + let selected_query: QueryListEntry; -namespace Modals { - /* - export function spawnQueryManage(client: ConnectionHandler) { - let modal: Modal; - let selected_query: QueryListEntry; + const update_selected = () => { + const buttons = modal.htmlTag.find(".header .buttons"); - const update_selected = () => { - const buttons = modal.htmlTag.find(".header .buttons"); + //TODO gray out if no permissions (Server needs to send that... :D) + buttons.find(".button-query-delete").prop("disabled", selected_query === undefined); + buttons.find(".button-query-rename").prop("disabled", selected_query === undefined); + buttons.find(".button-query-change-password").prop("disabled", selected_query === undefined); + }; - //TODO gray out if no permissions (Server needs to send that... :D) - buttons.find(".button-query-delete").prop("disabled", selected_query === undefined); - buttons.find(".button-query-rename").prop("disabled", selected_query === undefined); - buttons.find(".button-query-change-password").prop("disabled", selected_query === undefined); - }; + const update_list = () => { + const info_tag = modal.htmlTag.find(".footer .info a"); + info_tag.text("loading..."); + client.serverConnection.command_helper.current_virtual_server_id().then(server_id => { + client.serverConnection.command_helper.request_query_list(server_id).then(result => { + selected_query = undefined; - const update_list = () => { - const info_tag = modal.htmlTag.find(".footer .info a"); - info_tag.text("loading..."); - client.serverConnection.command_helper.current_virtual_server_id().then(server_id => { - client.serverConnection.command_helper.request_query_list(server_id).then(result => { - selected_query = undefined; + const entries_tag = modal.htmlTag.find(".query-list-entries"); + const entry_template = $("#tmpl_query_manager-list_entry"); + entries_tag.empty(); - const entries_tag = modal.htmlTag.find(".query-list-entries"); - const entry_template = $("#tmpl_query_manager-list_entry"); - entries_tag.empty(); + for(const query of result.queries || []) { + entries_tag.append(entry_template.renderTag(query).on('click', event => { + entries_tag.find(".entry.selected").removeClass("selected"); + $(event.target).parent(".entry").addClass("selected"); + selected_query = query; + update_selected(); + })); + } - for(const query of result.queries || []) { - entries_tag.append(entry_template.renderTag(query).on('click', event => { - entries_tag.find(".entry.selected").removeClass("selected"); - $(event.target).parent(".entry").addClass("selected"); - selected_query = query; - update_selected(); - })); + const entry_container = modal.htmlTag.find(".query-list-entries-container"); + if(entry_container.hasScrollBar()) + entry_container.addClass("scrollbar"); + + if(!result || result.flag_all) { + info_tag.text("Showing all server queries"); + } else { + info_tag.text("Showing your server queries") + } + update_selected(); + }); + }); + //TODO error handling + }; + + modal = createModal({ + header: tr("Manage query accounts"), + body: () => { + let template = $("#tmpl_query_manager").renderTag(); + template = $.spawn("div").append(template); + + /* first open the modal + setTimeout(() => { + const entry_container = template.find(".query-list-entries-container"); + if(entry_container.hasScrollBar()) + entry_container.addClass("scrollbar"); + }, 100); + + template.find(".footer .buttons .button-refresh").on('click', update_list); + template.find(".button-query-create").on('click', () => { + Modals.spawnQueryCreate(client, (user, pass) => update_list()); + }); + template.find(".button-query-rename").on('click', () => { + if(!selected_query) return; + + createInputModal(tr("Change account name"), tr("Enter the new name for the login:
"), text => text.length >= 3, result => { + if(result) { + client.serverConnection.send_command("queryrename", { + client_login_name: selected_query.username, + client_new_login_name: result + }).catch(error => { + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Unable to rename account"), tr("Failed to rename account
Message: ") + error).open(); + }).then(() => { + createInfoModal(tr("Account successfully renamed"), tr("The query account has been renamed!")).open(); + update_list(); + }); } + }).open(); + }); + template.find(".button-query-change-password").on('click', () => { + if(!selected_query) return; - const entry_container = modal.htmlTag.find(".query-list-entries-container"); - if(entry_container.hasScrollBar()) - entry_container.addClass("scrollbar"); + createInputModal(tr("Change account's password"), tr("Enter a new password (leave blank for auto generation):
"), text => true, result => { + if(result !== false) { + const single_handler: connection.SingleCommandHandler = { + command: "notifyquerypasswordchanges", + function: command => { + Modals.spawnQueryCreated({ + username: command.arguments[0]["client_login_name"], + password: command.arguments[0]["client_login_password"] + }, false); - if(!result || result.flag_all) { - info_tag.text("Showing all server queries"); - } else { - info_tag.text("Showing your server queries") + return true; + } + }; + client.serverConnection.command_handler_boss().register_single_handler(single_handler); + + client.serverConnection.send_command("querychangepassword", { + client_login_name: selected_query.username, + client_login_password: result + }).catch(error => { + client.serverConnection.command_handler_boss().remove_single_handler(single_handler); + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Unable to change password"), tr("Failed to change password
Message: ") + error).open(); + }); + } + }).open(); + }); + template.find(".button-query-delete").on('click', () => { + if(!selected_query) return; + + Modals.spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this account?"), result => { + if(result) { + client.serverConnection.send_command("querydelete", { + client_login_name: selected_query.username + }).catch(error => { + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Unable to delete account"), tr("Failed to delete account
Message: ") + error).open(); + }).then(() => { + createInfoModal(tr("Account successfully deleted"), tr("The query account has been successfully deleted!")).open(); + update_list(); + }); } - update_selected(); }); }); - //TODO error handling - }; + template.find(".input-search").on('change keyup', () => { + const text = (template.find(".input-search").val() as string || "").toLowerCase(); + if(text.length == 0) { + template.find(".query-list-entries .entry").show(); + } else { + template.find(".query-list-entries .entry").each((_, e) => { + const element = $(e); + if(element.text().toLowerCase().indexOf(text) == -1) + element.hide(); + else + element.show(); + }) + } + }); + return template; + }, + footer: undefined, + width: 750 + }); - modal = createModal({ - header: tr("Manage query accounts"), - body: () => { - let template = $("#tmpl_query_manager").renderTag(); - template = $.spawn("div").append(template); + update_list(); + modal.open(); +} + */ - /* first open the modal - setTimeout(() => { - const entry_container = template.find(".query-list-entries-container"); - if(entry_container.hasScrollBar()) - entry_container.addClass("scrollbar"); - }, 100); +//tmpl_query_manager +import {createErrorModal, createInfoModal, createInputModal, createModal, Modal} from "tc-shared/ui/elements/Modal"; +import {CommandResult, ErrorID, QueryListEntry} from "tc-shared/connection/ServerConnectionDeclaration"; +import {SingleCommandHandler} from "tc-shared/connection/ConnectionBase"; +import {copy_to_clipboard} from "tc-shared/utils/helpers"; +import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; +import {LogCategory} from "tc-shared/log"; +import PermissionType from "tc-shared/permission/PermissionType"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import * as log from "tc-shared/log"; +import {spawnQueryCreate, spawnQueryCreated} from "tc-shared/ui/modal/ModalQuery"; +import {formatMessage} from "tc-shared/ui/frames/chat"; - template.find(".footer .buttons .button-refresh").on('click', update_list); - template.find(".button-query-create").on('click', () => { - Modals.spawnQueryCreate(client, (user, pass) => update_list()); +export function spawnQueryManage(client: ConnectionHandler) { + let modal: Modal; + + modal = createModal({ + header: tr("Manage query accounts"), + body: () => { + let template = $("#tmpl_query_manager").renderTag(); + + let current_server: number; + let selected_query: QueryListEntry; + let filter_callbacks: ((text: string) => boolean)[] = []; + const container_list = template.find(".container-list .container-entries"); + const container_list_empty = container_list.find(".container-empty"); + const container_list_error = container_list.find(".container-error"); + + const detail_name = template.find(".detail.login-name .value"); + const detail_unique_id = template.find(".detail.unique-id .value"); + const detail_bound_server = template.find(".detail.bound-server .value"); + + const detail_unique_id_copy = template.find(".detail.unique-id .button-copy"); + + const input_filter = template.find(".filter-input"); + + const button_create = template.find(".button-create"); + const button_delete = template.find(".button-delete"); + const button_rename = template.find(".button-rename"); + const button_change_password = template.find(".button-change-password"); + const button_update = template.find(".button-update"); + + const permission_create = client.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_CREATE).granted(1); + const permission_delete = client.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_DELETE).granted(1); + const permission_delete_own = client.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_DELETE_OWN).granted(1); + const permission_rename = client.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_RENAME).granted(1); + const permission_rename_own = client.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_RENAME_OWN).granted(1); + const permission_password = client.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_CHANGE_PASSWORD).granted(1); + const permission_password_own = client.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_CHANGE_OWN_PASSWORD).granted(1); + const permission_password_global = client.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_CHANGE_PASSWORD_GLOBAL).granted(1); + button_create.prop('disabled', !permission_create); + + const set_error = (error: string | undefined) => { + if(typeof(error) === "string") + container_list_error.text(error).show(); + else + container_list_error.hide(); + }; + + const update_list = (selected_entry: string | undefined) => { + button_update.prop('disabled', true); + container_list_empty.text(tr("loading...")).show(); + set_error(undefined); + set_selected(undefined, false); + filter_callbacks = []; + container_list.find(".entry").remove(); + + client.serverConnection.command_helper.current_virtual_server_id().then(server_id => { + current_server = server_id; + + client.serverConnection.command_helper.request_query_list(server_id).then(result => { + if(!result || !result.queries.length) { + container_list_empty.text(tr("No queries available")); + return; + } + + for(const entry of result.queries) { + const tag = $.spawn("div").addClass("entry").text(entry.username + " (" + entry.unique_id + ")"); + tag.on('click', event => { + container_list.find(".selected").removeClass("selected"); + tag.addClass("selected"); + set_selected(entry, false); + }); + container_list.append(tag); + if(entry.username === selected_entry) tag.trigger('click'); + + const text_mesh = (entry.username + " " + entry.unique_id + " " + entry.bounded_server).toLowerCase(); + filter_callbacks.push(text => { + if(typeof(text) === "undefined" || text_mesh.indexOf(text) != -1) { + tag.show(); + return true; + } else { + tag.hide(); + return false; + } + }); + } + + update_filter(); + container_list_empty.hide(); + button_update.prop('disabled', false); + }).catch(error => { + button_update.prop('disabled', false); + if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) { + set_error(tr("No permissions")); + return; + } + log.error(LogCategory.CLIENT, tr("Failed to request the query list: %o"), error); + set_error(tr("Failed to request list")); + }); + }).catch(error => { + button_update.prop('disabled', false); + log.error(LogCategory.CLIENT, tr("Failed to get own virtual server id: %o"), error); + set_error(tr("Failed to query server id")); }); - template.find(".button-query-rename").on('click', () => { + }; + + const set_selected = (entry: QueryListEntry | undefined, force: boolean) => { + if(entry === selected_query && !force) return; + selected_query = entry; + + if(!selected_query) { + detail_name.text("-"); + detail_unique_id.text("-"); + detail_bound_server.text("-"); + + button_delete.prop('disabled', true); + button_rename.prop('disabled', true); + button_change_password.prop('disabled', true); + } else { + detail_name.text(selected_query.username); + detail_unique_id.text(selected_query.unique_id); + if(selected_query.bounded_server == 0) + detail_bound_server.text(tr("On the instance")); + else if(selected_query.bounded_server === current_server) + detail_bound_server.text(tr("On the current server")); + else + detail_bound_server.text(selected_query.bounded_server.toString()); + + button_delete.prop('disabled', !permission_delete && !(selected_query.unique_id === client.getClient().properties.client_unique_identifier && permission_delete_own)); + button_rename.prop('disabled', !permission_rename && !(selected_query.unique_id === client.getClient().properties.client_unique_identifier && permission_rename_own)); + if(selected_query.bounded_server != 0) { + button_change_password.prop('disabled', !permission_password && !(selected_query.unique_id === client.getClient().properties.client_unique_identifier && permission_password_own)); + } else { + button_change_password.prop('disabled', !permission_password_global && !(selected_query.unique_id === client.getClient().properties.client_unique_identifier && permission_password_own)); + } + } + }; + + const update_filter = () => { + let value = input_filter.val() as string; + if(!value) value = undefined; + else value = value.toLowerCase(); + + const shown = filter_callbacks.filter(e => e(value)).length; + if(shown > 0) { + container_list_empty.hide(); + } else { + container_list_empty.text(tr("No accounts found")).show(); + } + }; + input_filter.on('change keyup', update_filter); + + /* all buttons */ + { + detail_unique_id_copy.on('click', event => { if(!selected_query) return; - createInputModal(tr("Change account name"), tr("Enter the new name for the login:
"), text => text.length >= 3, result => { + copy_to_clipboard(selected_query.unique_id); + createInfoModal(tr("Unique ID copied"), tr("The unique id has been successfully copied to your clipboard.")).open(); + }); + + button_create.on('click', event => { + spawnQueryCreate(client, (user, pass) => update_list(user)); + }); + + button_delete.on('click', event => { + if(!selected_query) return; + + spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this account?"), result => { + if(result) { + client.serverConnection.send_command("querydelete", { + client_login_name: selected_query.username + }).then(() => { + createInfoModal(tr("Account successfully deleted"), tr("The query account has been successfully deleted!")).open(); + update_list(undefined); + }).catch(error => { + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Unable to delete account"), formatMessage(tr("Failed to delete account{:br:}Message: {}"), error)).open(); + }); + } + }); + }); + + button_rename.on('click', () => { + if(!selected_query) return; + + createInputModal(tr("Change account name"), tr("Enter the new name for the login:"), text => text.length >= 3, result => { if(result) { client.serverConnection.send_command("queryrename", { client_login_name: selected_query.username, client_new_login_name: result + }).then(() => { + createInfoModal(tr("Account successfully renamed"), tr("The query account has been renamed!")).open(); + update_list(result as string); }).catch(error => { if(error instanceof CommandResult) error = error.extra_message || error.message; - createErrorModal(tr("Unable to rename account"), tr("Failed to rename account
Message: ") + error).open(); - }).then(() => { - createInfoModal(tr("Account successfully renamed"), tr("The query account has been renamed!")).open(); - update_list(); + createErrorModal(tr("Unable to rename account"), formatMessage(tr("Failed to rename account{:br:}Message: {}"), error)).open(); }); } }).open(); }); - template.find(".button-query-change-password").on('click', () => { + + button_change_password.on('click', () => { if(!selected_query) return; - createInputModal(tr("Change account's password"), tr("Enter a new password (leave blank for auto generation):
"), text => true, result => { + createInputModal(tr("Change account's password"), tr("Enter a new password (leave blank for auto generation):"), text => true, result => { if(result !== false) { - const single_handler: connection.SingleCommandHandler = { + const single_handler: SingleCommandHandler = { command: "notifyquerypasswordchanges", function: command => { - Modals.spawnQueryCreated({ + spawnQueryCreated({ username: command.arguments[0]["client_login_name"], password: command.arguments[0]["client_login_password"] }, false); @@ -113,304 +396,27 @@ namespace Modals { client.serverConnection.command_handler_boss().remove_single_handler(single_handler); if(error instanceof CommandResult) error = error.extra_message || error.message; - createErrorModal(tr("Unable to change password"), tr("Failed to change password
Message: ") + error).open(); + createErrorModal(tr("Unable to change password"), formatMessage(tr("Failed to change password{:br:}Message: {}"), error)).open(); }); } }).open(); }); - template.find(".button-query-delete").on('click', () => { - if(!selected_query) return; - Modals.spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this account?"), result => { - if(result) { - client.serverConnection.send_command("querydelete", { - client_login_name: selected_query.username - }).catch(error => { - if(error instanceof CommandResult) - error = error.extra_message || error.message; - createErrorModal(tr("Unable to delete account"), tr("Failed to delete account
Message: ") + error).open(); - }).then(() => { - createInfoModal(tr("Account successfully deleted"), tr("The query account has been successfully deleted!")).open(); - update_list(); - }); - } - }); - }); - template.find(".input-search").on('change keyup', () => { - const text = (template.find(".input-search").val() as string || "").toLowerCase(); - if(text.length == 0) { - template.find(".query-list-entries .entry").show(); - } else { - template.find(".query-list-entries .entry").each((_, e) => { - const element = $(e); - if(element.text().toLowerCase().indexOf(text) == -1) - element.hide(); - else - element.show(); - }) - } - }); - return template; - }, - footer: undefined, - width: 750 - }); + button_update.on('click', event => update_list(selected_query ? selected_query.username : undefined)); + } - update_list(); - modal.open(); - } - */ + modal.close_listener.push(() => filter_callbacks = undefined); - //tmpl_query_manager - export function spawnQueryManage(client: ConnectionHandler) { - let modal: Modal; + set_selected(undefined, true); + update_list(undefined); + template.dividerfy(); + return template; + }, + footer: null, - modal = createModal({ - header: tr("Manage query accounts"), - body: () => { - let template = $("#tmpl_query_manager").renderTag(); + min_width: "25em" + }); - let current_server: number; - let selected_query: QueryListEntry; - let filter_callbacks: ((text: string) => boolean)[] = []; - const container_list = template.find(".container-list .container-entries"); - const container_list_empty = container_list.find(".container-empty"); - const container_list_error = container_list.find(".container-error"); - - const detail_name = template.find(".detail.login-name .value"); - const detail_unique_id = template.find(".detail.unique-id .value"); - const detail_bound_server = template.find(".detail.bound-server .value"); - - const detail_unique_id_copy = template.find(".detail.unique-id .button-copy"); - - const input_filter = template.find(".filter-input"); - - const button_create = template.find(".button-create"); - const button_delete = template.find(".button-delete"); - const button_rename = template.find(".button-rename"); - const button_change_password = template.find(".button-change-password"); - const button_update = template.find(".button-update"); - - const permission_create = client.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_CREATE).granted(1); - const permission_delete = client.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_DELETE).granted(1); - const permission_delete_own = client.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_DELETE_OWN).granted(1); - const permission_rename = client.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_RENAME).granted(1); - const permission_rename_own = client.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_RENAME_OWN).granted(1); - const permission_password = client.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_CHANGE_PASSWORD).granted(1); - const permission_password_own = client.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_CHANGE_OWN_PASSWORD).granted(1); - const permission_password_global = client.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_CHANGE_PASSWORD_GLOBAL).granted(1); - button_create.prop('disabled', !permission_create); - - const set_error = (error: string | undefined) => { - if(typeof(error) === "string") - container_list_error.text(error).show(); - else - container_list_error.hide(); - }; - - const update_list = (selected_entry: string | undefined) => { - button_update.prop('disabled', true); - container_list_empty.text(tr("loading...")).show(); - set_error(undefined); - set_selected(undefined, false); - filter_callbacks = []; - container_list.find(".entry").remove(); - - client.serverConnection.command_helper.current_virtual_server_id().then(server_id => { - current_server = server_id; - - client.serverConnection.command_helper.request_query_list(server_id).then(result => { - if(!result || !result.queries.length) { - container_list_empty.text(tr("No queries available")); - return; - } - - for(const entry of result.queries) { - const tag = $.spawn("div").addClass("entry").text(entry.username + " (" + entry.unique_id + ")"); - tag.on('click', event => { - container_list.find(".selected").removeClass("selected"); - tag.addClass("selected"); - set_selected(entry, false); - }); - container_list.append(tag); - if(entry.username === selected_entry) tag.trigger('click'); - - const text_mesh = (entry.username + " " + entry.unique_id + " " + entry.bounded_server).toLowerCase(); - filter_callbacks.push(text => { - if(typeof(text) === "undefined" || text_mesh.indexOf(text) != -1) { - tag.show(); - return true; - } else { - tag.hide(); - return false; - } - }); - } - - update_filter(); - container_list_empty.hide(); - button_update.prop('disabled', false); - }).catch(error => { - button_update.prop('disabled', false); - if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) { - set_error(tr("No permissions")); - return; - } - log.error(LogCategory.CLIENT, tr("Failed to request the query list: %o"), error); - set_error(tr("Failed to request list")); - }); - }).catch(error => { - button_update.prop('disabled', false); - log.error(LogCategory.CLIENT, tr("Failed to get own virtual server id: %o"), error); - set_error(tr("Failed to query server id")); - }); - }; - - const set_selected = (entry: QueryListEntry | undefined, force: boolean) => { - if(entry === selected_query && !force) return; - selected_query = entry; - - if(!selected_query) { - detail_name.text("-"); - detail_unique_id.text("-"); - detail_bound_server.text("-"); - - button_delete.prop('disabled', true); - button_rename.prop('disabled', true); - button_change_password.prop('disabled', true); - } else { - detail_name.text(selected_query.username); - detail_unique_id.text(selected_query.unique_id); - if(selected_query.bounded_server == 0) - detail_bound_server.text(tr("On the instance")); - else if(selected_query.bounded_server === current_server) - detail_bound_server.text(tr("On the current server")); - else - detail_bound_server.text(selected_query.bounded_server.toString()); - - button_delete.prop('disabled', !permission_delete && !(selected_query.unique_id === client.getClient().properties.client_unique_identifier && permission_delete_own)); - button_rename.prop('disabled', !permission_rename && !(selected_query.unique_id === client.getClient().properties.client_unique_identifier && permission_rename_own)); - if(selected_query.bounded_server != 0) { - button_change_password.prop('disabled', !permission_password && !(selected_query.unique_id === client.getClient().properties.client_unique_identifier && permission_password_own)); - } else { - button_change_password.prop('disabled', !permission_password_global && !(selected_query.unique_id === client.getClient().properties.client_unique_identifier && permission_password_own)); - } - } - }; - - const update_filter = () => { - let value = input_filter.val() as string; - if(!value) value = undefined; - else value = value.toLowerCase(); - - const shown = filter_callbacks.filter(e => e(value)).length; - if(shown > 0) { - container_list_empty.hide(); - } else { - container_list_empty.text(tr("No accounts found")).show(); - } - }; - input_filter.on('change keyup', update_filter); - - /* all buttons */ - { - detail_unique_id_copy.on('click', event => { - if(!selected_query) return; - - copy_to_clipboard(selected_query.unique_id); - createInfoModal(tr("Unique ID copied"), tr("The unique id has been successfully copied to your clipboard.")).open(); - }); - - button_create.on('click', event => { - Modals.spawnQueryCreate(client, (user, pass) => update_list(user)); - }); - - button_delete.on('click', event => { - if(!selected_query) return; - - Modals.spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this account?"), result => { - if(result) { - client.serverConnection.send_command("querydelete", { - client_login_name: selected_query.username - }).then(() => { - createInfoModal(tr("Account successfully deleted"), tr("The query account has been successfully deleted!")).open(); - update_list(undefined); - }).catch(error => { - if(error instanceof CommandResult) - error = error.extra_message || error.message; - createErrorModal(tr("Unable to delete account"), MessageHelper.formatMessage(tr("Failed to delete account{:br:}Message: {}"), error)).open(); - }); - } - }); - }); - - button_rename.on('click', () => { - if(!selected_query) return; - - createInputModal(tr("Change account name"), tr("Enter the new name for the login:"), text => text.length >= 3, result => { - if(result) { - client.serverConnection.send_command("queryrename", { - client_login_name: selected_query.username, - client_new_login_name: result - }).then(() => { - createInfoModal(tr("Account successfully renamed"), tr("The query account has been renamed!")).open(); - update_list(result as string); - }).catch(error => { - if(error instanceof CommandResult) - error = error.extra_message || error.message; - createErrorModal(tr("Unable to rename account"), MessageHelper.formatMessage(tr("Failed to rename account{:br:}Message: {}"), error)).open(); - }); - } - }).open(); - }); - - button_change_password.on('click', () => { - if(!selected_query) return; - - createInputModal(tr("Change account's password"), tr("Enter a new password (leave blank for auto generation):"), text => true, result => { - if(result !== false) { - const single_handler: connection.SingleCommandHandler = { - command: "notifyquerypasswordchanges", - function: command => { - Modals.spawnQueryCreated({ - username: command.arguments[0]["client_login_name"], - password: command.arguments[0]["client_login_password"] - }, false); - - return true; - } - }; - client.serverConnection.command_handler_boss().register_single_handler(single_handler); - - client.serverConnection.send_command("querychangepassword", { - client_login_name: selected_query.username, - client_login_password: result - }).catch(error => { - client.serverConnection.command_handler_boss().remove_single_handler(single_handler); - if(error instanceof CommandResult) - error = error.extra_message || error.message; - createErrorModal(tr("Unable to change password"), MessageHelper.formatMessage(tr("Failed to change password{:br:}Message: {}"), error)).open(); - }); - } - }).open(); - }); - - button_update.on('click', event => update_list(selected_query ? selected_query.username : undefined)); - } - - modal.close_listener.push(() => filter_callbacks = undefined); - - set_selected(undefined, true); - update_list(undefined); - template.dividerfy(); - return template; - }, - footer: null, - - min_width: "25em" - }); - - modal.htmlTag.find(".modal-body").addClass("modal-query-manage"); - modal.open(); - } + modal.htmlTag.find(".modal-body").addClass("modal-query-manage"); + modal.open(); } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalServerEdit.ts b/shared/js/ui/modal/ModalServerEdit.ts index 089f1522..bcac98b1 100644 --- a/shared/js/ui/modal/ModalServerEdit.ts +++ b/shared/js/ui/modal/ModalServerEdit.ts @@ -1,767 +1,774 @@ -namespace Modals { - export function createServerModal(server: ServerEntry, callback: (properties?: ServerProperties) => Promise) { - const properties = Object.assign({}, server.properties); +import {ServerEntry, ServerProperties} from "tc-shared/ui/server"; +import {createModal, Modal} from "tc-shared/ui/elements/Modal"; +import PermissionType from "tc-shared/permission/PermissionType"; +import {GroupManager} from "tc-shared/permission/GroupManager"; +import {hashPassword} from "tc-shared/utils/helpers"; +import * as tooltip from "tc-shared/ui/elements/Tooltip"; +import {spawnIconSelect} from "tc-shared/ui/modal/ModalIconSelect"; +import {network} from "tc-shared/ui/frames/chat"; - let _valid_states: {[key: string]:boolean} = { - general: false - }; +export function createServerModal(server: ServerEntry, callback: (properties?: ServerProperties) => Promise) { + const properties = Object.assign({}, server.properties); - let _toggle_valid = (key: string | undefined, value?: boolean) => { - if(typeof(key) === "string") { - _valid_states[key] = value; + let _valid_states: {[key: string]:boolean} = { + general: false + }; + + let _toggle_valid = (key: string | undefined, value?: boolean) => { + if(typeof(key) === "string") { + _valid_states[key] = value; + } + + let flag = true; + for(const key of Object.keys(_valid_states)) + if(!_valid_states[key]) { + flag = false; + break; } - let flag = true; - for(const key of Object.keys(_valid_states)) - if(!_valid_states[key]) { - flag = false; + if(flag) { + flag = false; + for(const property_name of Object.keys(properties)) { + if(server.properties[property_name] !== properties[property_name]) { + flag = true; break; } - - if(flag) { - flag = false; - for(const property_name of Object.keys(properties)) { - if(server.properties[property_name] !== properties[property_name]) { - flag = true; - break; - } - } } - - button_save.prop("disabled", !flag); - }; - - const modal = createModal({ - header: tr("Manage the Virtual Server"), - body: () => { - const template = $("#tmpl_server_edit").renderTag(Object.assign(Object.assign({}, server.properties), { - server_icon: server.channelTree.client.fileManager.icons.generateTag(server.properties.virtualserver_icon_id) - })); - - /* the tab functionality */ - { - const container_tabs = template.find(".container-categories"); - container_tabs.find(".categories .entry").on('click', event => { - const entry = $(event.target); - - container_tabs.find(".bodies > .body").addClass("hidden"); - container_tabs.find(".categories > .selected").removeClass("selected"); - - entry.addClass("selected"); - container_tabs.find(".bodies > .body." + entry.attr("container")).removeClass("hidden"); - }); - - container_tabs.find(".entry").first().trigger('click'); - } - - apply_general_listener(template.find(".container-general"), server, properties, _toggle_valid); - apply_host_listener(template.find(".container-host"), server, properties, _toggle_valid); - apply_network_listener(template.find(".container-network"), server, properties, _toggle_valid, modal); - apply_security_listener(template.find(".container-security"), server, properties, _toggle_valid); - apply_messages_listener(template.find(".container-messages"), server, properties, _toggle_valid); - apply_misc_listener(template.find(".container-misc"), server, properties, _toggle_valid); - - return template.contents(); - }, - footer: null, - min_width: "35em" - }); - - tooltip(modal.htmlTag); - - const button_save = modal.htmlTag.find(".button-save"); - button_save.on('click', event => { - const changed = {} as ServerProperties; - for(const property_name of Object.keys(properties)) - if(server.properties[property_name] !== properties[property_name]) - changed[property_name] = properties[property_name]; - callback(changed).then(() => { - _toggle_valid(undefined); - }); - }); - - modal.htmlTag.find(".button-cancel").on('click', event => { - modal.close(); - callback(); - }); - - _toggle_valid("general", true); - modal.htmlTag.find(".modal-body").addClass("modal-server-edit modal-blue"); - modal.open(); - } - - - function apply_general_listener(tag: JQuery, server: ServerEntry, properties: ServerProperties, callback_valid: (key: string | undefined, flag?: boolean) => void) { - /* name */ - { - const container = tag.find(".virtualserver_name"); - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_NAME).granted(1); - - container.on('change', event => { - properties.virtualserver_name = container.val() as string; - - const invalid = properties.virtualserver_name.length > 70 || properties.virtualserver_name.length < 1; - container.firstParent(".input-boxed").toggleClass("is-invalid", invalid); - callback_valid("virtualserver_name", !invalid); - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); } - /* icon */ - { - tag.find(".button-select-icon").on('click', event => { - Modals.spawnIconSelect(server.channelTree.client, id => { - const icon_node = tag.find(".icon-preview"); - icon_node.children().remove(); - icon_node.append(server.channelTree.client.fileManager.icons.generateTag(id)); + button_save.prop("disabled", !flag); + }; - console.log("Selected icon ID: %d", id); - properties.virtualserver_icon_id = id; - callback_valid(undefined); //Toggle save button update - }, properties.virtualserver_icon_id); - }); + const modal = createModal({ + header: tr("Manage the Virtual Server"), + body: () => { + const template = $("#tmpl_server_edit").renderTag(Object.assign(Object.assign({}, server.properties), { + server_icon: server.channelTree.client.fileManager.icons.generateTag(server.properties.virtualserver_icon_id) + })); - tag.find(".button-icon-remove").on('click', event => { + /* the tab functionality */ + { + const container_tabs = template.find(".container-categories"); + container_tabs.find(".categories .entry").on('click', event => { + const entry = $(event.target); + + container_tabs.find(".bodies > .body").addClass("hidden"); + container_tabs.find(".categories > .selected").removeClass("selected"); + + entry.addClass("selected"); + container_tabs.find(".bodies > .body." + entry.attr("container")).removeClass("hidden"); + }); + + container_tabs.find(".entry").first().trigger('click'); + } + + apply_general_listener(template.find(".container-general"), server, properties, _toggle_valid); + apply_host_listener(template.find(".container-host"), server, properties, _toggle_valid); + apply_network_listener(template.find(".container-network"), server, properties, _toggle_valid, modal); + apply_security_listener(template.find(".container-security"), server, properties, _toggle_valid); + apply_messages_listener(template.find(".container-messages"), server, properties, _toggle_valid); + apply_misc_listener(template.find(".container-misc"), server, properties, _toggle_valid); + + return template.contents(); + }, + footer: null, + min_width: "35em" + }); + + tooltip.initialize(modal.htmlTag); + + const button_save = modal.htmlTag.find(".button-save"); + button_save.on('click', event => { + const changed = {} as ServerProperties; + for(const property_name of Object.keys(properties)) + if(server.properties[property_name] !== properties[property_name]) + changed[property_name] = properties[property_name]; + callback(changed).then(() => { + _toggle_valid(undefined); + }); + }); + + modal.htmlTag.find(".button-cancel").on('click', event => { + modal.close(); + callback(); + }); + + _toggle_valid("general", true); + modal.htmlTag.find(".modal-body").addClass("modal-server-edit modal-blue"); + modal.open(); +} + + +function apply_general_listener(tag: JQuery, server: ServerEntry, properties: ServerProperties, callback_valid: (key: string | undefined, flag?: boolean) => void) { + /* name */ + { + const container = tag.find(".virtualserver_name"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_NAME).granted(1); + + container.on('change', event => { + properties.virtualserver_name = container.val() as string; + + const invalid = properties.virtualserver_name.length > 70 || properties.virtualserver_name.length < 1; + container.firstParent(".input-boxed").toggleClass("is-invalid", invalid); + callback_valid("virtualserver_name", !invalid); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + + /* icon */ + { + tag.find(".button-select-icon").on('click', event => { + spawnIconSelect(server.channelTree.client, id => { const icon_node = tag.find(".icon-preview"); icon_node.children().remove(); - icon_node.append(server.channelTree.client.fileManager.icons.generateTag(0)); + icon_node.append(server.channelTree.client.fileManager.icons.generateTag(id)); - console.log("Remove server icon"); - properties.virtualserver_icon_id = 0; + console.log("Selected icon ID: %d", id); + properties.virtualserver_icon_id = id; callback_valid(undefined); //Toggle save button update - }); + }, properties.virtualserver_icon_id); + }); + + tag.find(".button-icon-remove").on('click', event => { + const icon_node = tag.find(".icon-preview"); + icon_node.children().remove(); + icon_node.append(server.channelTree.client.fileManager.icons.generateTag(0)); + + console.log("Remove server icon"); + properties.virtualserver_icon_id = 0; + callback_valid(undefined); //Toggle save button update + }); + } + + /* password */ + { + //TODO: On save let the user retype his password? + const container = tag.find(".virtualserver_password"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_PASSWORD).granted(1); + + container.on('change', event => { + const password = container.val() as string; + properties.virtualserver_flag_password = !!password; + if(properties.virtualserver_flag_password) { + hashPassword(password).then(pass => properties.virtualserver_password = pass); + } + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + + /* slots */ + { + const container_max = tag.find(".virtualserver_maxclients"); + const container_reserved = tag.find(".virtualserver_reserved_slots"); + + /* max users */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_MAXCLIENTS).granted(1); + + container_max.on('change', event => { + properties.virtualserver_maxclients = parseInt(container_max.val() as string); + + const invalid = properties.virtualserver_maxclients < 1 || properties.virtualserver_maxclients > 1024; + container_max.firstParent(".input-boxed").toggleClass("is-invalid", invalid); + callback_valid("virtualserver_maxclients", !invalid); + + container_reserved.trigger('change'); /* update the flag */ + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); } - /* password */ + /* reserved */ { - //TODO: On save let the user retype his password? - const container = tag.find(".virtualserver_password"); - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_PASSWORD).granted(1); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_RESERVED_SLOTS).granted(1); + + container_reserved.on('change', event => { + properties.virtualserver_reserved_slots = parseInt(container_reserved.val() as string); + + const invalid = properties.virtualserver_reserved_slots > properties.virtualserver_maxclients; + container_reserved.firstParent(".input-boxed").toggleClass("is-invalid", invalid); + callback_valid("virtualserver_reserved_slots", !invalid); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + } + + /* Welcome message */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_WELCOMEMESSAGE).granted(1); + const container = tag.find(".container-welcome-message"); + const input = container.find("textarea"); + + const insert_tag = (open: string, close: string) => { + if(input.prop("disabled")) + return; + + const node = input[0] as HTMLTextAreaElement; + if (node.selectionStart || node.selectionStart == 0) { + const startPos = node.selectionStart; + const endPos = node.selectionEnd; + node.value = node.value.substring(0, startPos) + open + node.value.substring(startPos, endPos) + close + node.value.substring(endPos); + node.selectionEnd = endPos + open.length; + node.selectionStart = node.selectionEnd; + } else { + node.value += open + close; + node.selectionEnd = node.value.length - close.length; + node.selectionStart = node.selectionEnd; + } + + input.focus().trigger('change'); + }; + + input.on('change', event => { + console.log(tr("Welcome message edited: %o"), input.val()); + properties.virtualserver_welcomemessage = input.val() as string; + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + container.find(".button-bold").on('click', () => insert_tag('[b]', '[/b]')); + container.find(".button-italic").on('click', () => insert_tag('[i]', '[/i]')); + container.find(".button-underline").on('click', () => insert_tag('[u]', '[/u]')); + container.find(".button-color input").on('change', event => { + insert_tag('[color=' + (event.target as HTMLInputElement).value + ']', '[/color]') + }); + } +} + +function apply_network_listener(tag: JQuery, server: ServerEntry, properties: ServerProperties, callback_valid: (key: string | undefined, flag?: boolean) => void, modal: Modal) { + /* binding */ + { + /* host */ + { + const container = tag.find(".virtualserver_host"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOST).granted(1); container.on('change', event => { - const password = container.val() as string; - properties.virtualserver_flag_password = !!password; - if(properties.virtualserver_flag_password) { - helpers.hashPassword(password).then(pass => properties.virtualserver_password = pass); - } - callback_valid(undefined); //Toggle save button update - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - } - - /* slots */ - { - const container_max = tag.find(".virtualserver_maxclients"); - const container_reserved = tag.find(".virtualserver_reserved_slots"); - - /* max users */ - { - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_MAXCLIENTS).granted(1); - - container_max.on('change', event => { - properties.virtualserver_maxclients = parseInt(container_max.val() as string); - - const invalid = properties.virtualserver_maxclients < 1 || properties.virtualserver_maxclients > 1024; - container_max.firstParent(".input-boxed").toggleClass("is-invalid", invalid); - callback_valid("virtualserver_maxclients", !invalid); - - container_reserved.trigger('change'); /* update the flag */ - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - } - - /* reserved */ - { - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_RESERVED_SLOTS).granted(1); - - container_reserved.on('change', event => { - properties.virtualserver_reserved_slots = parseInt(container_reserved.val() as string); - - const invalid = properties.virtualserver_reserved_slots > properties.virtualserver_maxclients; - container_reserved.firstParent(".input-boxed").toggleClass("is-invalid", invalid); - callback_valid("virtualserver_reserved_slots", !invalid); - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - } - } - - /* Welcome message */ - { - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_WELCOMEMESSAGE).granted(1); - const container = tag.find(".container-welcome-message"); - const input = container.find("textarea"); - - const insert_tag = (open: string, close: string) => { - if(input.prop("disabled")) - return; - - const node = input[0] as HTMLTextAreaElement; - if (node.selectionStart || node.selectionStart == 0) { - const startPos = node.selectionStart; - const endPos = node.selectionEnd; - node.value = node.value.substring(0, startPos) + open + node.value.substring(startPos, endPos) + close + node.value.substring(endPos); - node.selectionEnd = endPos + open.length; - node.selectionStart = node.selectionEnd; - } else { - node.value += open + close; - node.selectionEnd = node.value.length - close.length; - node.selectionStart = node.selectionEnd; - } - - input.focus().trigger('change'); - }; - - input.on('change', event => { - console.log(tr("Welcome message edited: %o"), input.val()); - properties.virtualserver_welcomemessage = input.val() as string; + properties.virtualserver_host = container.val() as string; callback_valid(undefined); //Toggle save button update }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - container.find(".button-bold").on('click', () => insert_tag('[b]', '[/b]')); - container.find(".button-italic").on('click', () => insert_tag('[i]', '[/i]')); - container.find(".button-underline").on('click', () => insert_tag('[u]', '[/u]')); - container.find(".button-color input").on('change', event => { - insert_tag('[color=' + (event.target as HTMLInputElement).value + ']', '[/color]') - }); + server.updateProperties().then(() => container.val(server.properties.virtualserver_host)); } - } - function apply_network_listener(tag: JQuery, server: ServerEntry, properties: ServerProperties, callback_valid: (key: string | undefined, flag?: boolean) => void, modal: Modal) { - /* binding */ + /* port */ { - /* host */ - { - const container = tag.find(".virtualserver_host"); - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOST).granted(1); - - container.on('change', event => { - properties.virtualserver_host = container.val() as string; - callback_valid(undefined); //Toggle save button update - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - - server.updateProperties().then(() => container.val(server.properties.virtualserver_host)); - } - - /* port */ - { - const container = tag.find(".virtualserver_port"); - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_PORT).granted(1); - - container.on('change', event => { - const value = parseInt(container.val() as string); - properties.virtualserver_port = value; - - const valid = value >= 1 && value < 65536; - callback_valid("virtualserver_port", valid); - container.firstParent(".input-boxed").toggleClass("is-invalid", !valid); - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - - server.updateProperties().then(() => container.val(server.properties.virtualserver_port)); - } - - /* TeamSpeak server list */ - { - const container = tag.find(".virtualserver_weblist_enabled"); - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_WEBLIST).granted(1); - - container.on('change', event => { - properties.virtualserver_weblist_enabled = container.prop("checked"); - callback_valid(undefined); - }).prop("disabled", !permission).firstParent(".checkbox").toggleClass("disabled", !permission); - - server.updateProperties().then(() => container.prop("checked", server.properties.virtualserver_weblist_enabled)); - } - } - - /* file download */ - { - /* bandwidth */ - { - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_SETTINGS).granted(1); - const container = tag.find(".virtualserver_max_download_total_bandwidth"); - - container.on('change', event => { - properties.virtualserver_max_download_total_bandwidth = parseInt(container.val() as string); - callback_valid(undefined); //Toggle save button update - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - - server.updateProperties().then(() => container.val(server.properties.virtualserver_max_download_total_bandwidth)); - } - - /* Quota */ - { - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_QUOTAS).granted(1); - const container = tag.find(".virtualserver_download_quota"); - - container.on('change', event => { - properties.virtualserver_download_quota = parseInt(container.val() as string); - callback_valid(undefined); //Toggle save button update - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - - server.updateProperties().then(() => container.val(server.properties.virtualserver_download_quota)); - } - } - - /* file upload */ - { - /* bandwidth */ - { - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_SETTINGS).granted(1); - const container = tag.find(".virtualserver_max_upload_total_bandwidth"); - - container.on('change', event => { - properties.virtualserver_max_upload_total_bandwidth = parseInt(container.val() as string); - callback_valid(undefined); //Toggle save button update - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - - server.updateProperties().then(() => container.val(server.properties.virtualserver_max_upload_total_bandwidth)); - } - - /* Quota */ - { - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_QUOTAS).granted(1); - const container = tag.find(".virtualserver_upload_quota"); - - container.on('change', event => { - properties.virtualserver_upload_quota = parseInt(container.val() as string); - callback_valid(undefined); //Toggle save button update - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - - server.updateProperties().then(() => container.val(server.properties.virtualserver_upload_quota)); - } - } - - /* quota info */ - { - server.updateProperties().then(() => { - tag.find(".value.virtualserver_month_bytes_downloaded").text(MessageHelper.network.format_bytes(server.properties.virtualserver_month_bytes_downloaded)); - tag.find(".value.virtualserver_month_bytes_uploaded").text(MessageHelper.network.format_bytes(server.properties.virtualserver_month_bytes_uploaded)); - - tag.find(".value.virtualserver_total_bytes_downloaded").text(MessageHelper.network.format_bytes(server.properties.virtualserver_total_bytes_downloaded)); - tag.find(".value.virtualserver_total_bytes_uploaded").text(MessageHelper.network.format_bytes(server.properties.virtualserver_total_bytes_uploaded)); - }); - } - - /* quota update task */ - if(server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CONNECTIONINFO_VIEW).granted(1)) { - const month_bytes_downloaded = tag.find(".value.virtualserver_month_bytes_downloaded")[0]; - const month_bytes_uploaded = tag.find(".value.virtualserver_month_bytes_uploaded")[0]; - const total_bytes_downloaded = tag.find(".value.virtualserver_total_bytes_downloaded")[0]; - const total_bytes_uploaded = tag.find(".value.virtualserver_total_bytes_uploaded")[0]; - - let id = setInterval(() => { - if(!modal.shown) { - clearInterval(id); - return; - } - - server.request_connection_info().then(info => { - if(info.connection_filetransfer_bytes_sent_month && month_bytes_downloaded) - month_bytes_downloaded.innerText = MessageHelper.network.format_bytes(info.connection_filetransfer_bytes_sent_month); - if(info.connection_filetransfer_bytes_received_month && month_bytes_uploaded) - month_bytes_uploaded.innerText = MessageHelper.network.format_bytes(info.connection_filetransfer_bytes_received_month); - - if(info.connection_filetransfer_bytes_sent_total && total_bytes_downloaded) - total_bytes_downloaded.innerText = MessageHelper.network.format_bytes(info.connection_filetransfer_bytes_sent_total); - if(info.connection_filetransfer_bytes_received_total && total_bytes_uploaded) - total_bytes_uploaded.innerText = MessageHelper.network.format_bytes(info.connection_filetransfer_bytes_received_total); - }); - }, 1000); - modal.close_listener.push(() => clearInterval(id)); - } - } - - function apply_host_listener(tag: JQuery, server: ServerEntry, properties: ServerProperties, callback_valid: (key: string | undefined, flag?: boolean) => void) { - /* host message */ - { - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTMESSAGE).granted(1); - - /* message */ - { - const container = tag.find(".virtualserver_hostmessage"); - - container.on('change', event => { - properties.virtualserver_hostmessage = container.val() as string; - callback_valid(undefined); //Toggle save button update - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - } - - /* mode */ - { - const container = tag.find(".virtualserver_hostmessage_mode"); - - container.on('change', event => { - properties.virtualserver_hostmessage_mode = Math.min(3, Math.max(0, parseInt(container.val() as string))); - callback_valid(undefined); //Toggle save button update - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - } - } - - /* host banner */ - { - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBANNER).granted(1); - - /* URL */ - { - const container = tag.find(".virtualserver_hostbanner_url"); - - container.on('change', event => { - properties.virtualserver_hostbanner_url = container.val() as string; - callback_valid(undefined); //Toggle save button update - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - } - - /* Image URL/Image Preview */ - { - const container = tag.find(".virtualserver_hostbanner_gfx_url"); - const container_preview = tag.find(".container-host-message .container-gfx-preview img"); - - container.on('change', event => { - properties.virtualserver_hostbanner_gfx_url = container.val() as string; - container_preview.attr("src", properties.virtualserver_hostbanner_gfx_url).toggle(!!properties.virtualserver_hostbanner_gfx_url); - callback_valid(undefined); //Toggle save button update - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - } - - /* Image Refresh */ - { - const container = tag.find(".virtualserver_hostbanner_gfx_interval"); - - container.on('change', event => { - const value = parseInt(container.val() as string); - properties.virtualserver_hostbanner_gfx_interval = value; - - const invalid = value < 60 && value != 0; - container.firstParent(".input-boxed").toggleClass("is-invalid", invalid); - callback_valid("virtualserver_hostbanner_gfx_interval", !invalid); - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - } - - /* mode */ - { - const container = tag.find(".virtualserver_hostbanner_mode"); - - container.on('change', event => { - properties.virtualserver_hostbanner_mode = Math.min(2, Math.max(0, parseInt(container.val() as string))); - callback_valid(undefined); //Toggle save button update - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - } - } - - /* host button */ - { - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBUTTON).granted(1); - - /* URL */ - { - const container = tag.find(".virtualserver_hostbutton_url"); - - container.on('change', event => { - properties.virtualserver_hostbutton_url = container.val() as string; - callback_valid(undefined); //Toggle save button update - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - } - - /* Tooltip */ - { - const container = tag.find(".virtualserver_hostbutton_tooltip"); - - container.on('change', event => { - properties.virtualserver_hostbutton_tooltip = container.val() as string; - callback_valid(undefined); //Toggle save button update - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - } - - /* Icon URL/Icon Preview */ - { - const container = tag.find(".virtualserver_hostbutton_gfx_url"); - const container_preview = tag.find(".container-host-button .container-gfx-preview img"); - - container.on('change', event => { - properties.virtualserver_hostbutton_gfx_url = container.val() as string; - container_preview.attr("src", properties.virtualserver_hostbutton_gfx_url).toggle(!!properties.virtualserver_hostbutton_gfx_url); - callback_valid(undefined); //Toggle save button update - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - } - } - } - - function apply_security_listener(tag: JQuery, server: ServerEntry, properties: ServerProperties, callback_valid: (key: string | undefined, flag?: boolean) => void) { - /* Anti flood */ - { - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1); - - /* reduce */ - { - const container = tag.find(".virtualserver_antiflood_points_tick_reduce"); - - container.on('change', event => { - const value = parseInt(container.val() as string); - properties.virtualserver_antiflood_points_tick_reduce = value; - - const invalid = value < 1 || value > 999999; - container.firstParent(".input-boxed").toggleClass("is-invalid", invalid); - callback_valid("virtualserver_antiflood_points_tick_reduce", !invalid); - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - - server.updateProperties().then(() => container.val(server.properties.virtualserver_antiflood_points_tick_reduce)); - } - - /* block commands */ - { - const container = tag.find(".virtualserver_antiflood_points_needed_command_block"); - - container.on('change', event => { - const value = parseInt(container.val() as string); - properties.virtualserver_antiflood_points_needed_command_block = value; - - const invalid = value < 1 || value > 999999; - container.firstParent(".input-boxed").toggleClass("is-invalid", invalid); - callback_valid("virtualserver_antiflood_points_needed_command_block", !invalid); - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - - server.updateProperties().then(() => container.val(server.properties.virtualserver_antiflood_points_needed_command_block)); - } - - /* block ip */ - { - const container = tag.find(".virtualserver_antiflood_points_needed_ip_block"); - - container.on('change', event => { - const value = parseInt(container.val() as string); - properties.virtualserver_antiflood_points_needed_ip_block = value; - - const invalid = value < 1 || value > 999999; - container.firstParent(".input-boxed").toggleClass("is-invalid", invalid); - callback_valid("virtualserver_antiflood_points_needed_ip_block", !invalid); - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - - server.updateProperties().then(() => container.val(server.properties.virtualserver_antiflood_points_needed_ip_block)); - } - } - - /* encryption */ - { - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_CODEC_ENCRYPTION_MODE).granted(1); - const container = tag.find(".virtualserver_codec_encryption_mode"); - - container.on('change', event => { - properties.virtualserver_codec_encryption_mode = Math.min(2, Math.max(0, parseInt(container.val() as string))); - callback_valid(undefined); //Toggle save button update - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - } - - /* security level */ - { - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_NEEDED_IDENTITY_SECURITY_LEVEL).granted(1); - const container = tag.find(".virtualserver_needed_identity_security_level"); + const container = tag.find(".virtualserver_port"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_PORT).granted(1); container.on('change', event => { const value = parseInt(container.val() as string); - properties.virtualserver_needed_identity_security_level = value; + properties.virtualserver_port = value; - const invalid = value < 8 || value > 99; + const valid = value >= 1 && value < 65536; + callback_valid("virtualserver_port", valid); + container.firstParent(".input-boxed").toggleClass("is-invalid", !valid); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_port)); + } + + /* TeamSpeak server list */ + { + const container = tag.find(".virtualserver_weblist_enabled"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_WEBLIST).granted(1); + + container.on('change', event => { + properties.virtualserver_weblist_enabled = container.prop("checked"); + callback_valid(undefined); + }).prop("disabled", !permission).firstParent(".checkbox").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.prop("checked", server.properties.virtualserver_weblist_enabled)); + } + } + + /* file download */ + { + /* bandwidth */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_SETTINGS).granted(1); + const container = tag.find(".virtualserver_max_download_total_bandwidth"); + + container.on('change', event => { + properties.virtualserver_max_download_total_bandwidth = parseInt(container.val() as string); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_max_download_total_bandwidth)); + } + + /* Quota */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_QUOTAS).granted(1); + const container = tag.find(".virtualserver_download_quota"); + + container.on('change', event => { + properties.virtualserver_download_quota = parseInt(container.val() as string); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_download_quota)); + } + } + + /* file upload */ + { + /* bandwidth */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_SETTINGS).granted(1); + const container = tag.find(".virtualserver_max_upload_total_bandwidth"); + + container.on('change', event => { + properties.virtualserver_max_upload_total_bandwidth = parseInt(container.val() as string); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_max_upload_total_bandwidth)); + } + + /* Quota */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_QUOTAS).granted(1); + const container = tag.find(".virtualserver_upload_quota"); + + container.on('change', event => { + properties.virtualserver_upload_quota = parseInt(container.val() as string); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_upload_quota)); + } + } + + /* quota info */ + { + server.updateProperties().then(() => { + tag.find(".value.virtualserver_month_bytes_downloaded").text(network.format_bytes(server.properties.virtualserver_month_bytes_downloaded)); + tag.find(".value.virtualserver_month_bytes_uploaded").text(network.format_bytes(server.properties.virtualserver_month_bytes_uploaded)); + + tag.find(".value.virtualserver_total_bytes_downloaded").text(network.format_bytes(server.properties.virtualserver_total_bytes_downloaded)); + tag.find(".value.virtualserver_total_bytes_uploaded").text(network.format_bytes(server.properties.virtualserver_total_bytes_uploaded)); + }); + } + + /* quota update task */ + if(server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CONNECTIONINFO_VIEW).granted(1)) { + const month_bytes_downloaded = tag.find(".value.virtualserver_month_bytes_downloaded")[0]; + const month_bytes_uploaded = tag.find(".value.virtualserver_month_bytes_uploaded")[0]; + const total_bytes_downloaded = tag.find(".value.virtualserver_total_bytes_downloaded")[0]; + const total_bytes_uploaded = tag.find(".value.virtualserver_total_bytes_uploaded")[0]; + + let id = setInterval(() => { + if(!modal.shown) { + clearInterval(id); + return; + } + + server.request_connection_info().then(info => { + if(info.connection_filetransfer_bytes_sent_month && month_bytes_downloaded) + month_bytes_downloaded.innerText = network.format_bytes(info.connection_filetransfer_bytes_sent_month); + if(info.connection_filetransfer_bytes_received_month && month_bytes_uploaded) + month_bytes_uploaded.innerText = network.format_bytes(info.connection_filetransfer_bytes_received_month); + + if(info.connection_filetransfer_bytes_sent_total && total_bytes_downloaded) + total_bytes_downloaded.innerText = network.format_bytes(info.connection_filetransfer_bytes_sent_total); + if(info.connection_filetransfer_bytes_received_total && total_bytes_uploaded) + total_bytes_uploaded.innerText = network.format_bytes(info.connection_filetransfer_bytes_received_total); + }); + }, 1000); + modal.close_listener.push(() => clearInterval(id)); + } +} + +function apply_host_listener(tag: JQuery, server: ServerEntry, properties: ServerProperties, callback_valid: (key: string | undefined, flag?: boolean) => void) { + /* host message */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTMESSAGE).granted(1); + + /* message */ + { + const container = tag.find(".virtualserver_hostmessage"); + + container.on('change', event => { + properties.virtualserver_hostmessage = container.val() as string; + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + + /* mode */ + { + const container = tag.find(".virtualserver_hostmessage_mode"); + + container.on('change', event => { + properties.virtualserver_hostmessage_mode = Math.min(3, Math.max(0, parseInt(container.val() as string))); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + } + + /* host banner */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBANNER).granted(1); + + /* URL */ + { + const container = tag.find(".virtualserver_hostbanner_url"); + + container.on('change', event => { + properties.virtualserver_hostbanner_url = container.val() as string; + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + + /* Image URL/Image Preview */ + { + const container = tag.find(".virtualserver_hostbanner_gfx_url"); + const container_preview = tag.find(".container-host-message .container-gfx-preview img"); + + container.on('change', event => { + properties.virtualserver_hostbanner_gfx_url = container.val() as string; + container_preview.attr("src", properties.virtualserver_hostbanner_gfx_url).toggle(!!properties.virtualserver_hostbanner_gfx_url); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + + /* Image Refresh */ + { + const container = tag.find(".virtualserver_hostbanner_gfx_interval"); + + container.on('change', event => { + const value = parseInt(container.val() as string); + properties.virtualserver_hostbanner_gfx_interval = value; + + const invalid = value < 60 && value != 0; container.firstParent(".input-boxed").toggleClass("is-invalid", invalid); - callback_valid("virtualserver_needed_identity_security_level", !invalid); + callback_valid("virtualserver_hostbanner_gfx_interval", !invalid); }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } - server.updateProperties().then(() => container.val(server.properties.virtualserver_needed_identity_security_level)); + /* mode */ + { + const container = tag.find(".virtualserver_hostbanner_mode"); + + container.on('change', event => { + properties.virtualserver_hostbanner_mode = Math.min(2, Math.max(0, parseInt(container.val() as string))); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); } } - function apply_messages_listener(tag: JQuery, server: ServerEntry, properties: ServerProperties, callback_valid: (key: string | undefined, flag?: boolean) => void) { - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_MESSAGES).granted(1); + /* host button */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBUTTON).granted(1); - /* channel topic */ + /* URL */ { - const container = tag.find(".virtualserver_default_channel_topic"); + const container = tag.find(".virtualserver_hostbutton_url"); container.on('change', event => { - properties.virtualserver_default_channel_topic = container.val() as string; + properties.virtualserver_hostbutton_url = container.val() as string; callback_valid(undefined); //Toggle save button update }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); } - /* channel description */ + /* Tooltip */ { - const container = tag.find(".virtualserver_default_channel_description"); + const container = tag.find(".virtualserver_hostbutton_tooltip"); container.on('change', event => { - properties.virtualserver_default_channel_description = container.val() as string; + properties.virtualserver_hostbutton_tooltip = container.val() as string; callback_valid(undefined); //Toggle save button update }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - - server.updateProperties().then(() => container.val(server.properties.virtualserver_default_channel_description)); } - /* client description */ + /* Icon URL/Icon Preview */ { - const container = tag.find(".virtualserver_default_client_description"); + const container = tag.find(".virtualserver_hostbutton_gfx_url"); + const container_preview = tag.find(".container-host-button .container-gfx-preview img"); container.on('change', event => { - properties.virtualserver_default_client_description = container.val() as string; + properties.virtualserver_hostbutton_gfx_url = container.val() as string; + container_preview.attr("src", properties.virtualserver_hostbutton_gfx_url).toggle(!!properties.virtualserver_hostbutton_gfx_url); callback_valid(undefined); //Toggle save button update }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + } +} - server.updateProperties().then(() => container.val(server.properties.virtualserver_default_client_description)); +function apply_security_listener(tag: JQuery, server: ServerEntry, properties: ServerProperties, callback_valid: (key: string | undefined, flag?: boolean) => void) { + /* Anti flood */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1); + + /* reduce */ + { + const container = tag.find(".virtualserver_antiflood_points_tick_reduce"); + + container.on('change', event => { + const value = parseInt(container.val() as string); + properties.virtualserver_antiflood_points_tick_reduce = value; + + const invalid = value < 1 || value > 999999; + container.firstParent(".input-boxed").toggleClass("is-invalid", invalid); + callback_valid("virtualserver_antiflood_points_tick_reduce", !invalid); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_antiflood_points_tick_reduce)); + } + + /* block commands */ + { + const container = tag.find(".virtualserver_antiflood_points_needed_command_block"); + + container.on('change', event => { + const value = parseInt(container.val() as string); + properties.virtualserver_antiflood_points_needed_command_block = value; + + const invalid = value < 1 || value > 999999; + container.firstParent(".input-boxed").toggleClass("is-invalid", invalid); + callback_valid("virtualserver_antiflood_points_needed_command_block", !invalid); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_antiflood_points_needed_command_block)); + } + + /* block ip */ + { + const container = tag.find(".virtualserver_antiflood_points_needed_ip_block"); + + container.on('change', event => { + const value = parseInt(container.val() as string); + properties.virtualserver_antiflood_points_needed_ip_block = value; + + const invalid = value < 1 || value > 999999; + container.firstParent(".input-boxed").toggleClass("is-invalid", invalid); + callback_valid("virtualserver_antiflood_points_needed_ip_block", !invalid); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_antiflood_points_needed_ip_block)); } } - function apply_misc_listener(tag: JQuery, server: ServerEntry, properties: ServerProperties, callback_valid: (key: string | undefined, flag?: boolean) => void) { - /* default groups */ + /* encryption */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_CODEC_ENCRYPTION_MODE).granted(1); + const container = tag.find(".virtualserver_codec_encryption_mode"); + + container.on('change', event => { + properties.virtualserver_codec_encryption_mode = Math.min(2, Math.max(0, parseInt(container.val() as string))); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + + /* security level */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_NEEDED_IDENTITY_SECURITY_LEVEL).granted(1); + const container = tag.find(".virtualserver_needed_identity_security_level"); + + container.on('change', event => { + const value = parseInt(container.val() as string); + properties.virtualserver_needed_identity_security_level = value; + + const invalid = value < 8 || value > 99; + container.firstParent(".input-boxed").toggleClass("is-invalid", invalid); + callback_valid("virtualserver_needed_identity_security_level", !invalid); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_needed_identity_security_level)); + } +} + +function apply_messages_listener(tag: JQuery, server: ServerEntry, properties: ServerProperties, callback_valid: (key: string | undefined, flag?: boolean) => void) { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_MESSAGES).granted(1); + + /* channel topic */ + { + const container = tag.find(".virtualserver_default_channel_topic"); + + container.on('change', event => { + properties.virtualserver_default_channel_topic = container.val() as string; + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + + /* channel description */ + { + const container = tag.find(".virtualserver_default_channel_description"); + + container.on('change', event => { + properties.virtualserver_default_channel_description = container.val() as string; + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_default_channel_description)); + } + + /* client description */ + { + const container = tag.find(".virtualserver_default_client_description"); + + container.on('change', event => { + properties.virtualserver_default_client_description = container.val() as string; + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_default_client_description)); + } +} + +function apply_misc_listener(tag: JQuery, server: ServerEntry, properties: ServerProperties, callback_valid: (key: string | undefined, flag?: boolean) => void) { + /* default groups */ + { + /* Server Group */ { - /* Server Group */ - { - const container = tag.find(".virtualserver_default_server_group"); - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_SERVERGROUP).granted(1); + const container = tag.find(".virtualserver_default_server_group"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_SERVERGROUP).granted(1); - container.on('change', event => { - properties.virtualserver_default_server_group = parseInt(container.val() as string); - callback_valid(undefined); //Toggle save button update - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + container.on('change', event => { + properties.virtualserver_default_server_group = parseInt(container.val() as string); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - for(const group of server.channelTree.client.groups.serverGroups.sort(GroupManager.sorter())) { - if(group.type != 2) continue; - let group_tag = $.spawn("option").text(group.name + " [" + (group.properties.savedb ? "perm" : "tmp") + "]").attr("group-id", group.id); - if(group.id == server.properties.virtualserver_default_server_group) - group_tag.prop("selected", true); - group_tag.appendTo(container); - } - } - - /* Music Group */ - { - const container = tag.find(".virtualserver_default_music_group"); - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_MUSICGROUP).granted(1); - - container.on('change', event => { - properties.virtualserver_default_music_group = parseInt(container.val() as string); - callback_valid(undefined); //Toggle save button update - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - - for(const group of server.channelTree.client.groups.serverGroups.sort(GroupManager.sorter())) { - if(group.type != 2) continue; - let group_tag = $.spawn("option").text(group.name + " [" + (group.properties.savedb ? "perm" : "tmp") + "]").attr("group-id", group.id); - if(group.id == server.properties.virtualserver_default_music_group) - group_tag.prop("selected", true); - group_tag.appendTo(container); - } - } - - /* Channel Admin Group */ - { - const container = tag.find(".virtualserver_default_channel_admin_group"); - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_CHANNELADMINGROUP).granted(1); - - container.on('change', event => { - properties.virtualserver_default_channel_admin_group = parseInt(container.val() as string); - callback_valid(undefined); //Toggle save button update - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - - for(const group of server.channelTree.client.groups.channelGroups.sort(GroupManager.sorter())) { - if(group.type != 2) continue; - let group_tag = $.spawn("option").text(group.name + " [" + (group.properties.savedb ? "perm" : "tmp") + "]").attr("group-id", group.id); - if(group.id == server.properties.virtualserver_default_channel_admin_group) - group_tag.prop("selected", true); - group_tag.appendTo(container); - } - } - - /* Channel Guest Group */ - { - const container = tag.find(".virtualserver_default_channel_group"); - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_CHANNELGROUP).granted(1); - - container.on('change', event => { - properties.virtualserver_default_channel_group = parseInt(container.val() as string); - callback_valid(undefined); //Toggle save button update - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - - for(const group of server.channelTree.client.groups.channelGroups.sort(GroupManager.sorter())) { - if(group.type != 2) continue; - let group_tag = $.spawn("option").text(group.name + " [" + (group.properties.savedb ? "perm" : "tmp") + "]").attr("group-id", group.id); - if(group.id == server.properties.virtualserver_default_channel_group) - group_tag.prop("selected", true); - group_tag.appendTo(container); - } + for(const group of server.channelTree.client.groups.serverGroups.sort(GroupManager.sorter())) { + if(group.type != 2) continue; + let group_tag = $.spawn("option").text(group.name + " [" + (group.properties.savedb ? "perm" : "tmp") + "]").attr("group-id", group.id); + if(group.id == server.properties.virtualserver_default_server_group) + group_tag.prop("selected", true); + group_tag.appendTo(container); } } - /* complains */ + /* Music Group */ { - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_COMPLAIN).granted(1); + const container = tag.find(".virtualserver_default_music_group"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_MUSICGROUP).granted(1); - /* ban threshold */ - { - const container = tag.find(".virtualserver_complain_autoban_count"); + container.on('change', event => { + properties.virtualserver_default_music_group = parseInt(container.val() as string); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - container.on('change', event => { - properties.virtualserver_complain_autoban_count = parseInt(container.val() as string); - callback_valid(undefined); - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - - server.updateProperties().then(() => container.val(server.properties.virtualserver_complain_autoban_count)); - } - - /* ban time */ - { - const container = tag.find(".virtualserver_complain_autoban_time"); - - container.on('change', event => { - properties.virtualserver_complain_autoban_time = parseInt(container.val() as string); - callback_valid(undefined); - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - - server.updateProperties().then(() => container.val(server.properties.virtualserver_complain_autoban_time)); - } - - /* auto remove time */ - { - const container = tag.find(".virtualserver_complain_remove_time"); - - container.on('change', event => { - properties.virtualserver_complain_remove_time = parseInt(container.val() as string); - callback_valid(undefined); - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - - server.updateProperties().then(() => container.val(server.properties.virtualserver_complain_remove_time)); + for(const group of server.channelTree.client.groups.serverGroups.sort(GroupManager.sorter())) { + if(group.type != 2) continue; + let group_tag = $.spawn("option").text(group.name + " [" + (group.properties.savedb ? "perm" : "tmp") + "]").attr("group-id", group.id); + if(group.id == server.properties.virtualserver_default_music_group) + group_tag.prop("selected", true); + group_tag.appendTo(container); } } - /* others */ + /* Channel Admin Group */ { - /* clients before silence */ - { - const container = tag.find(".virtualserver_min_clients_in_channel_before_forced_silence"); - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_CHANNEL_FORCED_SILENCE).granted(1); + const container = tag.find(".virtualserver_default_channel_admin_group"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_CHANNELADMINGROUP).granted(1); - container.on('change', event => { - const value = parseInt(container.val() as string); - properties.virtualserver_min_clients_in_channel_before_forced_silence = value; - callback_valid("virtualserver_min_clients_in_channel_before_forced_silence", value > 1); - container.firstParent(".input-boxed").toggleClass("is-invalid", value <= 1); - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + container.on('change', event => { + properties.virtualserver_default_channel_admin_group = parseInt(container.val() as string); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - server.updateProperties().then(() => container.val(server.properties.virtualserver_min_clients_in_channel_before_forced_silence)); + for(const group of server.channelTree.client.groups.channelGroups.sort(GroupManager.sorter())) { + if(group.type != 2) continue; + let group_tag = $.spawn("option").text(group.name + " [" + (group.properties.savedb ? "perm" : "tmp") + "]").attr("group-id", group.id); + if(group.id == server.properties.virtualserver_default_channel_admin_group) + group_tag.prop("selected", true); + group_tag.appendTo(container); } + } - /* priority speaker dim factor */ - { - const container = tag.find(".virtualserver_priority_speaker_dimm_modificator"); - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_PRIORITY_SPEAKER_DIMM_MODIFICATOR).granted(1); + /* Channel Guest Group */ + { + const container = tag.find(".virtualserver_default_channel_group"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_CHANNELGROUP).granted(1); - container.on('change', event => { - properties.virtualserver_priority_speaker_dimm_modificator = parseInt(container.val() as string); - callback_valid(undefined); - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + container.on('change', event => { + properties.virtualserver_default_channel_group = parseInt(container.val() as string); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + for(const group of server.channelTree.client.groups.channelGroups.sort(GroupManager.sorter())) { + if(group.type != 2) continue; + let group_tag = $.spawn("option").text(group.name + " [" + (group.properties.savedb ? "perm" : "tmp") + "]").attr("group-id", group.id); + if(group.id == server.properties.virtualserver_default_channel_group) + group_tag.prop("selected", true); + group_tag.appendTo(container); } + } + } - /* channel delete delay */ - { - const container = tag.find(".virtualserver_channel_temp_delete_delay_default"); - const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_CHANNEL_TEMP_DELETE_DELAY_DEFAULT).granted(1); + /* complains */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_COMPLAIN).granted(1); - container.on('change', event => { - properties.virtualserver_channel_temp_delete_delay_default = parseInt(container.val() as string); - callback_valid(undefined); - }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - } + /* ban threshold */ + { + const container = tag.find(".virtualserver_complain_autoban_count"); + + container.on('change', event => { + properties.virtualserver_complain_autoban_count = parseInt(container.val() as string); + callback_valid(undefined); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_complain_autoban_count)); + } + + /* ban time */ + { + const container = tag.find(".virtualserver_complain_autoban_time"); + + container.on('change', event => { + properties.virtualserver_complain_autoban_time = parseInt(container.val() as string); + callback_valid(undefined); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_complain_autoban_time)); + } + + /* auto remove time */ + { + const container = tag.find(".virtualserver_complain_remove_time"); + + container.on('change', event => { + properties.virtualserver_complain_remove_time = parseInt(container.val() as string); + callback_valid(undefined); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_complain_remove_time)); + } + } + + /* others */ + { + /* clients before silence */ + { + const container = tag.find(".virtualserver_min_clients_in_channel_before_forced_silence"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_CHANNEL_FORCED_SILENCE).granted(1); + + container.on('change', event => { + const value = parseInt(container.val() as string); + properties.virtualserver_min_clients_in_channel_before_forced_silence = value; + callback_valid("virtualserver_min_clients_in_channel_before_forced_silence", value > 1); + container.firstParent(".input-boxed").toggleClass("is-invalid", value <= 1); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_min_clients_in_channel_before_forced_silence)); + } + + /* priority speaker dim factor */ + { + const container = tag.find(".virtualserver_priority_speaker_dimm_modificator"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_PRIORITY_SPEAKER_DIMM_MODIFICATOR).granted(1); + + container.on('change', event => { + properties.virtualserver_priority_speaker_dimm_modificator = parseInt(container.val() as string); + callback_valid(undefined); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + + /* channel delete delay */ + { + const container = tag.find(".virtualserver_channel_temp_delete_delay_default"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_CHANNEL_TEMP_DELETE_DELAY_DEFAULT).granted(1); + + container.on('change', event => { + properties.virtualserver_channel_temp_delete_delay_default = parseInt(container.val() as string); + callback_valid(undefined); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); } } } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalServerInfo.ts b/shared/js/ui/modal/ModalServerInfo.ts index 08fe0c71..5a3ef892 100644 --- a/shared/js/ui/modal/ModalServerInfo.ts +++ b/shared/js/ui/modal/ModalServerInfo.ts @@ -1,231 +1,245 @@ -namespace Modals { - export function openServerInfo(server: ServerEntry) { - let modal: Modal; - let update_callbacks: ServerBandwidthInfoUpdateCallback[] = []; +import { + openServerInfoBandwidth, + RequestInfoStatus, + ServerBandwidthInfoUpdateCallback +} from "tc-shared/ui/modal/ModalServerInfoBandwidth"; +import {ServerEntry} from "tc-shared/ui/server"; +import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; +import {createErrorModal, createModal, Modal} from "tc-shared/ui/elements/Modal"; +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; +import * as tooltip from "tc-shared/ui/elements/Tooltip"; +import * as i18nc from "tc-shared/i18n/country"; +import {format_time, formatMessage} from "tc-shared/ui/frames/chat"; +import {Hostbanner} from "tc-shared/ui/frames/hostbanner"; - modal = createModal({ - header: tr("Server Information: ") + server.properties.virtualserver_name, - body: () => { - const template = $("#tmpl_server_info").renderTag(); +declare const moment; +export function openServerInfo(server: ServerEntry) { + let modal: Modal; + let update_callbacks: ServerBandwidthInfoUpdateCallback[] = []; - const children = template.children(); - const top = template.find(".container-top"); - const update_values = () => { - apply_hostbanner(server, top); - apply_category_1(server, children, update_callbacks); - apply_category_2(server, children, update_callbacks); - apply_category_3(server, children, update_callbacks); - }; + modal = createModal({ + header: tr("Server Information: ") + server.properties.virtualserver_name, + body: () => { + const template = $("#tmpl_server_info").renderTag(); - const button_update = template.find(".button-update"); - button_update.on('click', event => { - button_update.prop("disabled", true); - server.updateProperties().then(() => { - update_callbacks = []; - update_values(); - }).catch(error => { - log.warn(LogCategory.CLIENT, tr("Failed to refresh server properties: %o"), error); - if(error instanceof CommandResult) - error = error.extra_message || error.message; - createErrorModal(tr("Refresh failed"), MessageHelper.formatMessage(tr("Failed to refresh server properties.{:br:}Error: {}"), error)).open(); - }).then(() => { - button_update.prop("disabled", false); - }); - }).trigger('click'); + const children = template.children(); + const top = template.find(".container-top"); + const update_values = () => { + apply_hostbanner(server, top); + apply_category_1(server, children, update_callbacks); + apply_category_2(server, children, update_callbacks); + apply_category_3(server, children, update_callbacks); + }; - update_values(); - tooltip(template); - return template.children(); - }, - footer: null, - min_width: "25em" + const button_update = template.find(".button-update"); + button_update.on('click', event => { + button_update.prop("disabled", true); + server.updateProperties().then(() => { + update_callbacks = []; + update_values(); + }).catch(error => { + log.warn(LogCategory.CLIENT, tr("Failed to refresh server properties: %o"), error); + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Refresh failed"), formatMessage(tr("Failed to refresh server properties.{:br:}Error: {}"), error)).open(); + }).then(() => { + button_update.prop("disabled", false); + }); + }).trigger('click'); + + update_values(); + tooltip.initialize(template); + return template.children(); + }, + footer: null, + min_width: "25em" + }); + + const updater = setInterval(() => { + server.request_connection_info().then(info => update_callbacks.forEach(e => e(RequestInfoStatus.SUCCESS, info))).catch(error => { + if(error instanceof CommandResult && error.id == ErrorID.PERMISSION_ERROR) { + update_callbacks.forEach(e => e(RequestInfoStatus.NO_PERMISSION)); + return; + } + update_callbacks.forEach(e => e(RequestInfoStatus.UNKNOWN)); + }); + }, 1000); + + + modal.htmlTag.find(".button-close").on('click', event => modal.close()); + modal.htmlTag.find(".button-show-bandwidth").on('click', event => { + const custom_callbacks = []; + const custom_callback_caller = (status, info) => { custom_callbacks.forEach(e => e(status, info)); }; + + update_callbacks.push(custom_callback_caller); + openServerInfoBandwidth(server, custom_callbacks).close_listener.push(() => { + update_callbacks.remove(custom_callback_caller); + }); + }); + + modal.htmlTag.find(".modal-body").addClass("modal-server-info"); + modal.open(); + modal.close_listener.push(() => clearInterval(updater)); +} + +function apply_hostbanner(server: ServerEntry, tag: JQuery) { + let container: JQuery; + tag.empty().append( + container = $.spawn("div").addClass("container-hostbanner") + ).addClass("hidden"); + + const htag = Hostbanner.generate_tag(server.properties.virtualserver_hostbanner_gfx_url, server.properties.virtualserver_hostbanner_gfx_interval, server.properties.virtualserver_hostbanner_mode); + htag.then(t => { + if(!t) return; + + tag.removeClass("hidden"); + container.append(t); + }); +} + +function apply_category_1(server: ServerEntry, tag: JQuery, update_callbacks: ServerBandwidthInfoUpdateCallback[]) { + /* server name */ + { + const container = tag.find(".server-name"); + container.text(server.properties.virtualserver_name); + } + + /* server region */ + { + const container = tag.find(".server-region").empty(); + container.append( + $.spawn("div").addClass("country flag-" + server.properties.virtualserver_country_code.toLowerCase()), + $.spawn("a").text(i18nc.country_name(server.properties.virtualserver_country_code, tr("Global"))) + ); + } + + /* slots */ + { + const container = tag.find(".server-slots"); + + let text = server.properties.virtualserver_clientsonline + "/" + server.properties.virtualserver_maxclients; + if(server.properties.virtualserver_queryclientsonline) + text += " +" + (server.properties.virtualserver_queryclientsonline > 1 ? + server.properties.virtualserver_queryclientsonline + " " + tr("Queries") : + server.properties.virtualserver_queryclientsonline + " " + tr("Query")); + if(server.properties.virtualserver_reserved_slots) + text += " (" + server.properties.virtualserver_reserved_slots + " " + tr("Reserved") + ")"; + + container.text(text); + } + + /* first run */ + { + const container = tag.find(".server-first-run"); + + container.text( + server.properties.virtualserver_created > 0 ? + moment(server.properties.virtualserver_created * 1000).format('MMMM Do YYYY, h:mm:ss a') : + tr("Unknown") + ); + } + + /* uptime */ + { + const container = tag.find(".server-uptime"); + const update = () => container.text(format_time(server.calculateUptime() * 1000, tr("just started"))); + update_callbacks.push(update); + update(); + } +} + +function apply_category_2(server: ServerEntry, tag: JQuery, update_callbacks: ServerBandwidthInfoUpdateCallback[]) { + /* ip */ + { + const container = tag.find(".server-ip"); + container.text(server.remote_address.host + (server.remote_address.port == 9987 ? "" : (":" + server.remote_address.port))) + } + + /* version */ + { + const container = tag.find(".server-version"); + + let timestamp = -1; + const version = (server.properties.virtualserver_version || "unknwon").replace(/ ?\[build: ?([0-9]+)]/gmi, (group, ts) => { + timestamp = parseInt(ts); + return ""; }); - const updater = setInterval(() => { - server.request_connection_info().then(info => update_callbacks.forEach(e => e(RequestInfoStatus.SUCCESS, info))).catch(error => { - if(error instanceof CommandResult && error.id == ErrorID.PERMISSION_ERROR) { - update_callbacks.forEach(e => e(RequestInfoStatus.NO_PERMISSION)); - return; - } - update_callbacks.forEach(e => e(RequestInfoStatus.UNKNOWN)); - }); - }, 1000); - - - modal.htmlTag.find(".button-close").on('click', event => modal.close()); - modal.htmlTag.find(".button-show-bandwidth").on('click', event => { - const custom_callbacks = []; - const custom_callback_caller = (status, info) => { custom_callbacks.forEach(e => e(status, info)); }; - - update_callbacks.push(custom_callback_caller); - Modals.openServerInfoBandwidth(server, custom_callbacks).close_listener.push(() => { - update_callbacks.remove(custom_callback_caller); - }); - }); - - modal.htmlTag.find(".modal-body").addClass("modal-server-info"); - modal.open(); - modal.close_listener.push(() => clearInterval(updater)); + container.find("a").text(version); + container.find(".container-tooltip").toggle(timestamp > 0).find(".tooltip a").text( + moment(timestamp * 1000).format('[Build timestamp:] YYYY-MM-DD HH:mm Z') + ); } - function apply_hostbanner(server: ServerEntry, tag: JQuery) { - let container: JQuery; - tag.empty().append( - container = $.spawn("div").addClass("container-hostbanner") - ).addClass("hidden"); - - const htag = Hostbanner.generate_tag(server.properties.virtualserver_hostbanner_gfx_url, server.properties.virtualserver_hostbanner_gfx_interval, server.properties.virtualserver_hostbanner_mode); - htag.then(t => { - if(!t) return; - - tag.removeClass("hidden"); - container.append(t); - }); + /* platform */ + { + const container = tag.find(".server-platform"); + container.text(server.properties.virtualserver_platform); } - function apply_category_1(server: ServerEntry, tag: JQuery, update_callbacks: ServerBandwidthInfoUpdateCallback[]) { - /* server name */ - { - const container = tag.find(".server-name"); - container.text(server.properties.virtualserver_name); - } - - /* server region */ - { - const container = tag.find(".server-region").empty(); - container.append( - $.spawn("div").addClass("country flag-" + server.properties.virtualserver_country_code.toLowerCase()), - $.spawn("a").text(i18n.country_name(server.properties.virtualserver_country_code, tr("Global"))) - ); - } - - /* slots */ - { - const container = tag.find(".server-slots"); - - let text = server.properties.virtualserver_clientsonline + "/" + server.properties.virtualserver_maxclients; - if(server.properties.virtualserver_queryclientsonline) - text += " +" + (server.properties.virtualserver_queryclientsonline > 1 ? - server.properties.virtualserver_queryclientsonline + " " + tr("Queries") : - server.properties.virtualserver_queryclientsonline + " " + tr("Query")); - if(server.properties.virtualserver_reserved_slots) - text += " (" + server.properties.virtualserver_reserved_slots + " " + tr("Reserved") + ")"; - - container.text(text); - } - - /* first run */ - { - const container = tag.find(".server-first-run"); - - container.text( - server.properties.virtualserver_created > 0 ? - moment(server.properties.virtualserver_created * 1000).format('MMMM Do YYYY, h:mm:ss a') : - tr("Unknown") - ); - } - - /* uptime */ - { - const container = tag.find(".server-uptime"); - const update = () => container.text(MessageHelper.format_time(server.calculateUptime() * 1000, tr("just started"))); - update_callbacks.push(update); - update(); - } - } - - function apply_category_2(server: ServerEntry, tag: JQuery, update_callbacks: ServerBandwidthInfoUpdateCallback[]) { - /* ip */ - { - const container = tag.find(".server-ip"); - container.text(server.remote_address.host + (server.remote_address.port == 9987 ? "" : (":" + server.remote_address.port))) - } - - /* version */ - { - const container = tag.find(".server-version"); - - let timestamp = -1; - const version = (server.properties.virtualserver_version || "unknwon").replace(/ ?\[build: ?([0-9]+)]/gmi, (group, ts) => { - timestamp = parseInt(ts); - return ""; - }); - - container.find("a").text(version); - container.find(".container-tooltip").toggle(timestamp > 0).find(".tooltip a").text( - moment(timestamp * 1000).format('[Build timestamp:] YYYY-MM-DD HH:mm Z') - ); - } - - /* platform */ - { - const container = tag.find(".server-platform"); - container.text(server.properties.virtualserver_platform); - } - - /* ping */ - { - const container = tag.find(".server-ping"); - container.text(tr("calculating...")); - update_callbacks.push((status, data) => { - if(status === RequestInfoStatus.SUCCESS) - container.text(data.connection_ping.toFixed(0) + " " + "ms"); - else if(status === RequestInfoStatus.NO_PERMISSION) - container.text(tr("No Permissions")); - else - container.text(tr("receiving...")); - }); - } - - /* packet loss */ - { - const container = tag.find(".server-packet-loss"); - container.text(tr("receiving...")); - update_callbacks.push((status, data) => { - if(status === RequestInfoStatus.SUCCESS) - container.text(data.connection_packetloss_total.toFixed(2) + "%"); - else if(status === RequestInfoStatus.NO_PERMISSION) - container.text(tr("No Permissions")); - else - container.text(tr("receiving...")); - }); - } - } - - function apply_category_3(server: ServerEntry, tag: JQuery, update_callbacks: ServerBandwidthInfoUpdateCallback[]) { - /* unique id */ - { - const container = tag.find(".server-unique-id"); - container.text(server.properties.virtualserver_unique_identifier || tr("Unknown")); - } - - /* voice encryption */ - { - const container = tag.find(".server-voice-encryption"); - if(server.properties.virtualserver_codec_encryption_mode == 0) - container.text(tr("Globally off")); - else if(server.properties.virtualserver_codec_encryption_mode == 1) - container.text(tr("Individually configured per channel")); + /* ping */ + { + const container = tag.find(".server-ping"); + container.text(tr("calculating...")); + update_callbacks.push((status, data) => { + if(status === RequestInfoStatus.SUCCESS) + container.text(data.connection_ping.toFixed(0) + " " + "ms"); + else if(status === RequestInfoStatus.NO_PERMISSION) + container.text(tr("No Permissions")); else - container.text(tr("Globally on")); - } + container.text(tr("receiving...")); + }); + } - /* channel count */ - { - const container = tag.find(".server-channel-count"); - container.text(server.properties.virtualserver_channelsonline); - } + /* packet loss */ + { + const container = tag.find(".server-packet-loss"); + container.text(tr("receiving...")); + update_callbacks.push((status, data) => { + if(status === RequestInfoStatus.SUCCESS) + container.text(data.connection_packetloss_total.toFixed(2) + "%"); + else if(status === RequestInfoStatus.NO_PERMISSION) + container.text(tr("No Permissions")); + else + container.text(tr("receiving...")); + }); + } +} - /* minimal security level */ - { - const container = tag.find(".server-min-security-level"); - container.text(server.properties.virtualserver_needed_identity_security_level); - } +function apply_category_3(server: ServerEntry, tag: JQuery, update_callbacks: ServerBandwidthInfoUpdateCallback[]) { + /* unique id */ + { + const container = tag.find(".server-unique-id"); + container.text(server.properties.virtualserver_unique_identifier || tr("Unknown")); + } - /* complains */ - { - const container = tag.find(".server-complains"); - container.text(server.properties.virtualserver_complain_autoban_count); - } + /* voice encryption */ + { + const container = tag.find(".server-voice-encryption"); + if(server.properties.virtualserver_codec_encryption_mode == 0) + container.text(tr("Globally off")); + else if(server.properties.virtualserver_codec_encryption_mode == 1) + container.text(tr("Individually configured per channel")); + else + container.text(tr("Globally on")); + } + + /* channel count */ + { + const container = tag.find(".server-channel-count"); + container.text(server.properties.virtualserver_channelsonline); + } + + /* minimal security level */ + { + const container = tag.find(".server-min-security-level"); + container.text(server.properties.virtualserver_needed_identity_security_level); + } + + /* complains */ + { + const container = tag.find(".server-complains"); + container.text(server.properties.virtualserver_complain_autoban_count); } } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalServerInfoBandwidth.ts b/shared/js/ui/modal/ModalServerInfoBandwidth.ts index 8bb6fdc8..b8c25629 100644 --- a/shared/js/ui/modal/ModalServerInfoBandwidth.ts +++ b/shared/js/ui/modal/ModalServerInfoBandwidth.ts @@ -1,184 +1,189 @@ -namespace Modals { - export enum RequestInfoStatus { - SUCCESS, - UNKNOWN, - NO_PERMISSION +import {ServerConnectionInfo, ServerEntry} from "tc-shared/ui/server"; +import {createModal, Modal} from "tc-shared/ui/elements/Modal"; +import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; +import {Graph} from "tc-shared/ui/elements/NetGraph"; +import * as tooltip from "tc-shared/ui/elements/Tooltip"; +import {network} from "tc-shared/ui/frames/chat"; + +export enum RequestInfoStatus { + SUCCESS, + UNKNOWN, + NO_PERMISSION +} +export type ServerBandwidthInfoUpdateCallback = (status: RequestInfoStatus, info?: ServerConnectionInfo) => any; +export function openServerInfoBandwidth(server: ServerEntry, update_callbacks?: ServerBandwidthInfoUpdateCallback[]) : Modal { + let modal: Modal; + let own_callbacks = !update_callbacks; + update_callbacks = update_callbacks || []; + + modal = createModal({ + header: tr("Server bandwidth data"), + body: () => { + const template = $("#tmpl_server_info_bandwidth").renderTag(); + + const children = template.children(); + initialize_current_bandwidth(modal, children.find(".statistic-bandwidth"), update_callbacks); + initialize_ft_bandwidth(modal, children.find(".statistic-ft-bandwidth"), update_callbacks); + initialize_general(template.find(".top"), update_callbacks); + + tooltip.initialize(template); + return template.children(); + }, + footer: null, + min_width: "25em" + }); + + if(own_callbacks) { + const updater = setInterval(() => { + server.request_connection_info().then(info => update_callbacks.forEach(e => e(RequestInfoStatus.SUCCESS, info))).catch(error => { + if(error instanceof CommandResult && error.id == ErrorID.PERMISSION_ERROR) { + update_callbacks.forEach(e => e(RequestInfoStatus.NO_PERMISSION)); + return; + } + update_callbacks.forEach(e => e(RequestInfoStatus.UNKNOWN)); + }); + }, 1000); + modal.close_listener.push(() => clearInterval(updater)); } - export type ServerBandwidthInfoUpdateCallback = (status: RequestInfoStatus, info?: ServerConnectionInfo) => any; - export function openServerInfoBandwidth(server: ServerEntry, update_callbacks?: ServerBandwidthInfoUpdateCallback[]) : Modal { - let modal: Modal; - let own_callbacks = !update_callbacks; - update_callbacks = update_callbacks || []; - modal = createModal({ - header: tr("Server bandwidth data"), - body: () => { - const template = $("#tmpl_server_info_bandwidth").renderTag(); - const children = template.children(); - initialize_current_bandwidth(modal, children.find(".statistic-bandwidth"), update_callbacks); - initialize_ft_bandwidth(modal, children.find(".statistic-ft-bandwidth"), update_callbacks); - initialize_general(template.find(".top"), update_callbacks); + modal.htmlTag.find(".button-close").on('click', event => modal.close()); + modal.htmlTag.find(".modal-body").addClass("modal-server-info-bandwidth"); + modal.open(); + return modal; +} - tooltip(template); - return template.children(); - }, - footer: null, - min_width: "25em" - }); +function initialize_graph(modal: Modal, tag: JQuery, callbacks: ServerBandwidthInfoUpdateCallback[], fields: {uplaod: string, download: string}) { + const canvas = tag.find("canvas")[0] as HTMLCanvasElement; + const label_upload = tag.find(".upload"); + const label_download = tag.find(".download"); + let last_info: { status: RequestInfoStatus, info: ServerConnectionInfo }; + let custom_info = false; - if(own_callbacks) { - const updater = setInterval(() => { - server.request_connection_info().then(info => update_callbacks.forEach(e => e(RequestInfoStatus.SUCCESS, info))).catch(error => { - if(error instanceof CommandResult && error.id == ErrorID.PERMISSION_ERROR) { - update_callbacks.forEach(e => e(RequestInfoStatus.NO_PERMISSION)); - return; - } - update_callbacks.forEach(e => e(RequestInfoStatus.UNKNOWN)); - }); - }, 1000); - modal.close_listener.push(() => clearInterval(updater)); + const show_info = (upload: number | undefined, download: number | undefined) => { + let fallback_text = last_info && last_info.status === RequestInfoStatus.NO_PERMISSION ? tr("No permission") : tr("receiving..."); + + if(typeof upload !== "number") + upload = last_info ? last_info[fields.uplaod] : undefined; + + if(typeof download !== "number") + download = last_info ? last_info[fields.download] : undefined; + + if(typeof upload !== "number") + label_upload.text(fallback_text); + else + label_upload.text(network.format_bytes(upload, {unit: "Bytes", time: "s", exact: false})); + + if(typeof download !== "number") + label_download.text(fallback_text); + else + label_download.text(network.format_bytes(download, {unit: "Bytes", time: "s", exact: false})); + }; + show_info(undefined, undefined); + + const graph = new Graph(canvas); + graph.insert_entry({ timestamp: Date.now(), upload: undefined, download: undefined}); + callbacks.push((status, values) => { + last_info = {status: status, info: values}; + + if(!values) { + graph.insert_entry({ timestamp: Date.now(), upload: undefined, download: undefined}); + } else { + graph.insert_entry({ + timestamp: Date.now(), + download: values[fields.download], //values.connection_bandwidth_received_last_second_total, + upload: values[fields.uplaod], //values.connection_bandwidth_sent_last_second_total + }); } - - modal.htmlTag.find(".button-close").on('click', event => modal.close()); - modal.htmlTag.find(".modal-body").addClass("modal-server-info-bandwidth"); - modal.open(); - return modal; - } - - function initialize_graph(modal: Modal, tag: JQuery, callbacks: ServerBandwidthInfoUpdateCallback[], fields: {uplaod: string, download: string}) { - const canvas = tag.find("canvas")[0] as HTMLCanvasElement; - const label_upload = tag.find(".upload"); - const label_download = tag.find(".download"); - let last_info: { status: RequestInfoStatus, info: ServerConnectionInfo }; - let custom_info = false; - - const show_info = (upload: number | undefined, download: number | undefined) => { - let fallback_text = last_info && last_info.status === RequestInfoStatus.NO_PERMISSION ? tr("No permission") : tr("receiving..."); - - if(typeof upload !== "number") - upload = last_info ? last_info[fields.uplaod] : undefined; - - if(typeof download !== "number") - download = last_info ? last_info[fields.download] : undefined; - - if(typeof upload !== "number") - label_upload.text(fallback_text); - else - label_upload.text(MessageHelper.network.format_bytes(upload, {unit: "Bytes", time: "s", exact: false})); - - if(typeof download !== "number") - label_download.text(fallback_text); - else - label_download.text(MessageHelper.network.format_bytes(download, {unit: "Bytes", time: "s", exact: false})); + /* set set that we want to show the entry within one second */ + graph._time_span.origin = Object.assign(graph.calculate_time_span(), { time: Date.now() }); + graph._time_span.target = { + begin: Date.now() - 120 * 1000, + end: Date.now(), + time: Date.now() + 200 }; - show_info(undefined, undefined); - const graph = new net.graph.Graph(canvas); - graph.insert_entry({ timestamp: Date.now(), upload: undefined, download: undefined}); - callbacks.push((status, values) => { - last_info = {status: status, info: values}; - - if(!values) { - graph.insert_entry({ timestamp: Date.now(), upload: undefined, download: undefined}); - } else { - graph.insert_entry({ - timestamp: Date.now(), - download: values[fields.download], //values.connection_bandwidth_received_last_second_total, - upload: values[fields.uplaod], //values.connection_bandwidth_sent_last_second_total - }); - } - - /* set set that we want to show the entry within one second */ - graph._time_span.origin = Object.assign(graph.calculate_time_span(), { time: Date.now() }); - graph._time_span.target = { - begin: Date.now() - 120 * 1000, - end: Date.now(), - time: Date.now() + 200 - }; - - graph.cleanup(); - if(!custom_info) { - show_info(undefined, undefined); - graph.resize(); /* just to ensure (we have to rethink this maybe; cause it causes a recalculates the style */ - } - }); - - graph.max_gap_size(0); - graph.initialize(); - - graph.callback_detailed_hide = () => { - custom_info = false; + graph.cleanup(); + if(!custom_info) { show_info(undefined, undefined); - }; + graph.resize(); /* just to ensure (we have to rethink this maybe; cause it causes a recalculates the style */ + } + }); - graph.callback_detailed_info = (upload, download, timestamp, event) => { - custom_info = true; - show_info(upload, download); - }; + graph.max_gap_size(0); + graph.initialize(); - modal.close_listener.push(() => graph.terminate()); - modal.open_listener.push(() => graph.resize()); + graph.callback_detailed_hide = () => { + custom_info = false; + show_info(undefined, undefined); + }; - tag.addClass("window-resize-listener").on('resize', event => graph.resize()); - } + graph.callback_detailed_info = (upload, download, timestamp, event) => { + custom_info = true; + show_info(upload, download); + }; - function initialize_current_bandwidth(modal: Modal, tag: JQuery, callbacks: ServerBandwidthInfoUpdateCallback[]) { - initialize_graph(modal, tag, callbacks, { - uplaod: "connection_bandwidth_sent_last_second_total", - download: "connection_bandwidth_received_last_second_total" - }); - } + modal.close_listener.push(() => graph.terminate()); + modal.open_listener.push(() => graph.resize()); - function initialize_ft_bandwidth(modal: Modal, tag: JQuery, callbacks: ServerBandwidthInfoUpdateCallback[]) { - initialize_graph(modal, tag, callbacks, { - uplaod: "connection_filetransfer_bandwidth_sent", - download: "connection_filetransfer_bandwidth_received" - }); - } + tag.addClass("window-resize-listener").on('resize', event => graph.resize()); +} - function initialize_general(tag: JQuery, callbacks: ServerBandwidthInfoUpdateCallback[]) { - const tag_packets_upload = tag.find(".statistic-packets .upload"); - const tag_packets_download = tag.find(".statistic-packets .download"); +function initialize_current_bandwidth(modal: Modal, tag: JQuery, callbacks: ServerBandwidthInfoUpdateCallback[]) { + initialize_graph(modal, tag, callbacks, { + uplaod: "connection_bandwidth_sent_last_second_total", + download: "connection_bandwidth_received_last_second_total" + }); +} - const tag_bytes_upload = tag.find(".statistic-bytes .upload"); - const tag_bytes_download = tag.find(".statistic-bytes .download"); +function initialize_ft_bandwidth(modal: Modal, tag: JQuery, callbacks: ServerBandwidthInfoUpdateCallback[]) { + initialize_graph(modal, tag, callbacks, { + uplaod: "connection_filetransfer_bandwidth_sent", + download: "connection_filetransfer_bandwidth_received" + }); +} - const tag_ft_bytes_upload = tag.find(".statistic-ft-bytes .upload"); - const tag_ft_bytes_download = tag.find(".statistic-ft-bytes .download"); +function initialize_general(tag: JQuery, callbacks: ServerBandwidthInfoUpdateCallback[]) { + const tag_packets_upload = tag.find(".statistic-packets .upload"); + const tag_packets_download = tag.find(".statistic-packets .download"); - const update = (tag, value: undefined | null | number) => { - if(typeof value === "undefined") - tag.text(tr("receiving...")); - else if(value === null) - tag.text(tr("no permissions")); - else - tag.text(MessageHelper.network.format_bytes(value, {unit: "Bytes", exact: false})); - }; + const tag_bytes_upload = tag.find(".statistic-bytes .upload"); + const tag_bytes_download = tag.find(".statistic-bytes .download"); - const props = [ - {tag: tag_packets_download, property: "connection_packets_received_total"}, - {tag: tag_packets_upload, property: "connection_packets_sent_total"}, + const tag_ft_bytes_upload = tag.find(".statistic-ft-bytes .upload"); + const tag_ft_bytes_download = tag.find(".statistic-ft-bytes .download"); - {tag: tag_bytes_download, property: "connection_bytes_received_total"}, - {tag: tag_bytes_upload, property: "connection_bytes_sent_total"}, + const update = (tag, value: undefined | null | number) => { + if(typeof value === "undefined") + tag.text(tr("receiving...")); + else if(value === null) + tag.text(tr("no permissions")); + else + tag.text(network.format_bytes(value, {unit: "Bytes", exact: false})); + }; - {tag: tag_ft_bytes_upload, property: "connection_filetransfer_bytes_received_total"}, - {tag: tag_ft_bytes_download, property: "connection_filetransfer_bytes_sent_total"}, - ]; + const props = [ + {tag: tag_packets_download, property: "connection_packets_received_total"}, + {tag: tag_packets_upload, property: "connection_packets_sent_total"}, - callbacks.push((status, info) => { - if(status === RequestInfoStatus.SUCCESS) { - for(const entry of props) - update(entry.tag, info[entry.property]); - } else if(status === RequestInfoStatus.NO_PERMISSION) { - for(const entry of props) - update(entry.tag, null); - } else { - for(const entry of props) - update(entry.tag, undefined); - } - }); - } + {tag: tag_bytes_download, property: "connection_bytes_received_total"}, + {tag: tag_bytes_upload, property: "connection_bytes_sent_total"}, + + {tag: tag_ft_bytes_upload, property: "connection_filetransfer_bytes_received_total"}, + {tag: tag_ft_bytes_download, property: "connection_filetransfer_bytes_sent_total"}, + ]; + + callbacks.push((status, info) => { + if(status === RequestInfoStatus.SUCCESS) { + for(const entry of props) + update(entry.tag, info[entry.property]); + } else if(status === RequestInfoStatus.NO_PERMISSION) { + for(const entry of props) + update(entry.tag, null); + } else { + for(const entry of props) + update(entry.tag, undefined); + } + }); } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalSettings.ts b/shared/js/ui/modal/ModalSettings.ts index 155e3076..6e3c6c58 100644 --- a/shared/js/ui/modal/ModalSettings.ts +++ b/shared/js/ui/modal/ModalSettings.ts @@ -1,442 +1,1882 @@ -namespace Modals { - export function spawnSettingsModal(default_page?: string) : Modal { - let modal: Modal; - modal = createModal({ - header: tr("Settings"), - body: () => { - const tag = $("#tmpl_settings").renderTag().dividerfy(); +import {createErrorModal, createInfoModal, createInputModal, createModal, Modal} from "tc-shared/ui/elements/Modal"; +import {sliderfy} from "tc-shared/ui/elements/Slider"; +import {settings, Settings} from "tc-shared/settings"; +import {manager, set_master_volume, Sound} from "tc-shared/sound/Sounds"; +import {ConnectionProfile} from "tc-shared/profiles/ConnectionProfile"; +import {IdentitifyType} from "tc-shared/profiles/Identity"; +import {TeaForumIdentity} from "tc-shared/profiles/identities/TeaForumIdentity"; +import {TeaSpeakIdentity} from "tc-shared/profiles/identities/TeamSpeakIdentity"; +import {NameIdentity} from "tc-shared/profiles/identities/NameIdentity"; +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import * as profiles from "tc-shared/profiles/ConnectionProfile"; +import {RepositoryTranslation, TranslationRepository} from "tc-shared/i18n/localize"; +import {Registry} from "tc-shared/events"; +import {key_description} from "tc-shared/PPTListener"; +import {default_recorder} from "tc-shared/voice/RecorderProfile"; +import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; +import * as i18n from "tc-shared/i18n/localize"; +import * as i18nc from "tc-shared/i18n/country"; +import {server_connections} from "tc-shared/ui/frames/connection_handlers"; +import * as events from "tc-shared/events"; +import * as sound from "tc-shared/sound/Sounds"; +import * as forum from "tc-shared/profiles/identities/teaspeak-forum"; +import {formatMessage, set_icon_size} from "tc-shared/ui/frames/chat"; +import {spawnKeySelect} from "tc-shared/ui/modal/ModalKeySelect"; +import {spawnTeamSpeakIdentityImport, spawnTeamSpeakIdentityImprove} from "tc-shared/ui/modal/ModalIdentity"; +import {Device} from "tc-shared/audio/player"; +import {LevelMeter} from "tc-shared/voice/RecorderBase"; +import * as loader from "tc-loader"; +import * as aplayer from "tc-backend/audio/player"; +import * as arecorder from "tc-backend/audio/recorder"; +import * as ppt from "tc-backend/ppt"; - /* general "tab" mechanic */ - const left = tag.find("> .left"); - const right = tag.find("> .right"); - { - left.find(".entry:not(.group)").on('click', event => { - const entry = $(event.target); - right.find("> .container").addClass("hidden"); - left.find(".selected").removeClass("selected"); +export function spawnSettingsModal(default_page?: string) : Modal { + let modal: Modal; + modal = createModal({ + header: tr("Settings"), + body: () => { + const tag = $("#tmpl_settings").renderTag().dividerfy(); - const target = entry.attr("container"); - if(!target) return; - - right.find("> .container." + target).removeClass("hidden"); - entry.addClass("selected"); - }) - } - - /* initialize all tabs */ - - /* enable one tab */ - { - left.find(".entry[container" + (default_page ? ("='" + default_page + "'") : "") + "]").first().trigger('click'); - } - - return tag; - }, - footer: null - }); - modal.htmlTag.find(".modal-body").addClass("modal-settings"); - - settings_general_application(modal.htmlTag.find(".right .container.general-application"), modal); - settings_general_language(modal.htmlTag.find(".right .container.general-language"), modal); - settings_general_chat(modal.htmlTag.find(".right .container.general-chat"), modal); - settings_audio_microphone(modal.htmlTag.find(".right .container.audio-microphone"), modal); - settings_audio_speaker(modal.htmlTag.find(".right .container.audio-speaker"), modal); - settings_audio_sounds(modal.htmlTag.find(".right .container.audio-sounds"), modal); - const update_profiles = settings_identity_profiles(modal.htmlTag.find(".right .container.identity-profiles"), modal); - settings_identity_forum(modal.htmlTag.find(".right .container.identity-forum"), modal, update_profiles as any); - - modal.close_listener.push(() => { - if(profiles.requires_save()) - profiles.save(); - }); - - modal.open(); - return modal; - } - - function settings_general_application(container: JQuery, modal: Modal) { - /* hostbanner */ - { - const option = container.find(".option-hostbanner-background") as JQuery; - option.on('change', event => { - settings.changeGlobal(Settings.KEY_HOSTBANNER_BACKGROUND, option[0].checked); - for(const sc of server_connections.server_connection_handlers()) - sc.hostbanner.update(); - }).prop("checked", settings.static_global(Settings.KEY_HOSTBANNER_BACKGROUND)); - } - - /* font size */ - { - const current_size = parseInt(getComputedStyle(document.body).fontSize); //settings.static_global(Settings.KEY_FONT_SIZE, 12); - const select = container.find(".option-font-size"); - - if(select.find("option[value='" + current_size + "']").length) - select.find("option[value='" + current_size + "']").prop("selected", true); - else - select.find("option[value='-1']").prop("selected", true); - - select.on('change', event => { - const value = parseInt(select.val() as string); - settings.changeGlobal(Settings.KEY_FONT_SIZE, value); - console.log("Changed font size to %dpx", value); - - $(document.body).css("font-size", value + "px"); - }); - } - - /* all permissions */ - { - const option = container.find(".option-all-permissions") as JQuery; - option.on('change', event => { - settings.changeGlobal(Settings.KEY_HOSTBANNER_BACKGROUND, option[0].checked); - }).prop("checked", settings.global(Settings.KEY_PERMISSIONS_SHOW_ALL)); - } - } - - function settings_general_language(container: JQuery, modal: Modal) { - - const container_entries = container.find(".container-list .entries"); - - const tag_loading = container.find(".cover-loading"); - const template = $("#settings-translations-list-entry"); - - const restart_hint = container.find(".restart-note").hide(); - - const display_repository_info = (repository: i18n.TranslationRepository) => { - const info_modal = createModal({ - header: tr("Repository info"), - body: () => { - return $("#settings-translations-list-entry-info").renderTag({ - type: "repository", - name: repository.name, - url: repository.url, - contact: repository.contact, - translations: repository.translations || [] - }); - }, - footer: () => { - let footer = $.spawn("div"); - footer.addClass("modal-button-group"); - footer.css("margin-top", "5px"); - footer.css("margin-bottom", "5px"); - footer.css("text-align", "right"); - - let buttonOk = $.spawn("button"); - buttonOk.text(tr("Close")); - buttonOk.click(() => info_modal.close()); - footer.append(buttonOk); - - return footer; - } - }); - info_modal.open() - }; - const display_translation_info = (translation: i18n.RepositoryTranslation, repository: i18n.TranslationRepository) => { - const info_modal = createModal({ - header: tr("Translation info"), - body: () => { - const tag = $("#settings-translations-list-entry-info").renderTag({ - type: "translation", - name: translation.name, - url: translation.path, - repository_name: repository.name, - contributors: translation.contributors || [] - }); - - tag.find(".button-info").on('click', () => display_repository_info(repository)); - - return tag; - }, - footer: () => { - let footer = $.spawn("div"); - footer.addClass("modal-button-group"); - footer.css("margin-top", "5px"); - footer.css("margin-bottom", "5px"); - footer.css("text-align", "right"); - - let buttonOk = $.spawn("button"); - buttonOk.text(tr("Close")); - buttonOk.click(() => info_modal.close()); - footer.append(buttonOk); - - return footer; - } - }); - info_modal.open() - }; - - const update_current_selected = () => { - const container_current = container.find(".selected-language6"); - container_current.empty().text(tr("Loading")); - - let current_translation: i18n.RepositoryTranslation; - i18n.iterate_repositories(repository => { - if(current_translation) return; - for(const entry of repository.translations) - if(i18n.config.translation_config().current_translation_path == entry.path) { - current_translation = entry; - return; - } - }).then(() => { - container_current.empty(); - - const language = current_translation ? current_translation.country_code : "gb"; - $.spawn("div").addClass("country flag-" + language.toLowerCase()).attr('title', i18n.country_name(language, tr("Unknown language"))).appendTo(container_current); - $.spawn("a").text(current_translation ? current_translation.name : tr("English (Default)")).appendTo(container_current); - }).catch(error => { - /* This shall never happen */ - }); - }; - - const initially_selected = i18n.config.translation_config().current_translation_url; - const update_list = () => { - container_entries.empty(); - - const currently_selected = i18n.config.translation_config().current_translation_url; - //Default translation + /* general "tab" mechanic */ + const left = tag.find("> .left"); + const right = tag.find("> .right"); { - const tag = template.renderTag({ - type: "default", - selected: !currently_selected || currently_selected == "default" - }); - tag.on('click', () => { - i18n.select_translation(undefined, undefined); - container_entries.find(".selected").removeClass("selected"); - tag.addClass("selected"); + left.find(".entry:not(.group)").on('click', event => { + const entry = $(event.target); + right.find("> .container").addClass("hidden"); + left.find(".selected").removeClass("selected"); - update_current_selected(); - restart_hint.toggle(initially_selected !== i18n.config.translation_config().current_translation_url); - }); - tag.appendTo(container_entries); - } + const target = entry.attr("container"); + if(!target) return; - { - tag_loading.show(); - i18n.iterate_repositories(repo => { - let repo_tag = container_entries.find("[repository=\"" + repo.unique_id + "\"]"); - if (repo_tag.length == 0) { - repo_tag = template.renderTag({ - type: "repository", - name: repo.name || repo.url, - id: repo.unique_id - }); - - repo_tag.find(".button-delete").on('click', e => { - e.preventDefault(); - - Modals.spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this repository?"), answer => { - if (answer) { - i18n.delete_repository(repo); - update_list(); - } - }); - }); - repo_tag.find(".button-info").on('click', e => { - e.preventDefault(); - display_repository_info(repo); - }); - - container_entries.append(repo_tag); - } - - for(const translation of repo.translations) { - const tag = template.renderTag({ - type: "translation", - name: translation.name || translation.path, - id: repo.unique_id, - country_code: translation.country_code, - selected: i18n.config.translation_config().current_translation_path == translation.path - }); - tag.find(".button-info").on('click', e => { - e.preventDefault(); - display_translation_info(translation, repo); - }); - tag.on('click', e => { - if (e.isDefaultPrevented()) return; - i18n.select_translation(repo, translation); - container_entries.find(".selected").removeClass("selected"); - tag.addClass("selected"); - - update_current_selected(); - restart_hint.toggle(initially_selected !== i18n.config.translation_config().current_translation_url); - }); - tag.insertAfter(repo_tag); - } - }).then(() => tag_loading.hide()).catch(error => { - console.error(error); - /* this should NEVER happen */ + right.find("> .container." + target).removeClass("hidden"); + entry.addClass("selected"); }) } - }; + /* initialize all tabs */ - /* button add repository */ - { - container.find(".button-add-repository").on('click', () => { - createInputModal(tr("Enter repository URL"), tr("Enter repository URL:"), text => { - try { - new URL(text); - return true; - } catch(error) { - return false; - } - }, url => { - if (!url) return; + /* enable one tab */ + { + left.find(".entry[container" + (default_page ? ("='" + default_page + "'") : "") + "]").first().trigger('click'); + } - tag_loading.show(); - i18n.load_repository(url as string).then(repository => { - i18n.register_repository(repository); - update_list(); - }).catch(error => { - tag_loading.hide(); - createErrorModal("Failed to load repository", tr("Failed to query repository.
Ensure that this repository is valid and reachable.
Error: ") + error).open(); - }) - }).open(); - }); - } + return tag; + }, + footer: null + }); + modal.htmlTag.find(".modal-body").addClass("modal-settings"); - container.find(".button-restart").on('click', () => { - if(app.is_web()) { - location.reload(); - } else { - createErrorModal(tr("Not implemented"), tr("Client restart isn't implemented.
Please do it manually!")).open(); + settings_general_application(modal.htmlTag.find(".right .container.general-application"), modal); + settings_general_language(modal.htmlTag.find(".right .container.general-language"), modal); + settings_general_chat(modal.htmlTag.find(".right .container.general-chat"), modal); + settings_audio_microphone(modal.htmlTag.find(".right .container.audio-microphone"), modal); + settings_audio_speaker(modal.htmlTag.find(".right .container.audio-speaker"), modal); + settings_audio_sounds(modal.htmlTag.find(".right .container.audio-sounds"), modal); + const update_profiles = settings_identity_profiles(modal.htmlTag.find(".right .container.identity-profiles"), modal); + settings_identity_forum(modal.htmlTag.find(".right .container.identity-forum"), modal, update_profiles as any); + + modal.close_listener.push(() => { + if(profiles.requires_save()) + profiles.save(); + }); + + modal.open(); + return modal; +} + +function settings_general_application(container: JQuery, modal: Modal) { + /* hostbanner */ + { + const option = container.find(".option-hostbanner-background") as JQuery; + option.on('change', event => { + settings.changeGlobal(Settings.KEY_HOSTBANNER_BACKGROUND, option[0].checked); + for(const sc of server_connections.server_connection_handlers()) + sc.hostbanner.update(); + }).prop("checked", settings.static_global(Settings.KEY_HOSTBANNER_BACKGROUND)); + } + + /* font size */ + { + const current_size = parseInt(getComputedStyle(document.body).fontSize); //settings.static_global(Settings.KEY_FONT_SIZE, 12); + const select = container.find(".option-font-size"); + + if(select.find("option[value='" + current_size + "']").length) + select.find("option[value='" + current_size + "']").prop("selected", true); + else + select.find("option[value='-1']").prop("selected", true); + + select.on('change', event => { + const value = parseInt(select.val() as string); + settings.changeGlobal(Settings.KEY_FONT_SIZE, value); + console.log("Changed font size to %dpx", value); + + $(document.body).css("font-size", value + "px"); + }); + } + + /* all permissions */ + { + const option = container.find(".option-all-permissions") as JQuery; + option.on('change', event => { + settings.changeGlobal(Settings.KEY_HOSTBANNER_BACKGROUND, option[0].checked); + }).prop("checked", settings.global(Settings.KEY_PERMISSIONS_SHOW_ALL)); + } +} + +function settings_general_language(container: JQuery, modal: Modal) { + + const container_entries = container.find(".container-list .entries"); + + const tag_loading = container.find(".cover-loading"); + const template = $("#settings-translations-list-entry"); + + const restart_hint = container.find(".restart-note").hide(); + + const display_repository_info = (repository: TranslationRepository) => { + const info_modal = createModal({ + header: tr("Repository info"), + body: () => { + return $("#settings-translations-list-entry-info").renderTag({ + type: "repository", + name: repository.name, + url: repository.url, + contact: repository.contact, + translations: repository.translations || [], + }); + }, + footer: () => { + let footer = $.spawn("div"); + footer.addClass("modal-button-group"); + footer.css("margin-top", "5px"); + footer.css("margin-bottom", "5px"); + footer.css("text-align", "right"); + + let buttonOk = $.spawn("button"); + buttonOk.text(tr("Close")); + buttonOk.click(() => info_modal.close()); + footer.append(buttonOk); + + return footer; } }); + info_modal.open() + }; + const display_translation_info = (translation: RepositoryTranslation, repository: TranslationRepository) => { + const info_modal = createModal({ + header: tr("Translation info"), + body: () => { + const tag = $("#settings-translations-list-entry-info").renderTag({ + type: "translation", + name: translation.name, + url: translation.path, + repository_name: repository.name, + contributors: translation.contributors || [] + }); - update_list(); - update_current_selected(); - } + tag.find(".button-info").on('click', () => display_repository_info(repository)); - function settings_general_chat(container: JQuery, modal: Modal) { - /* timestamp format */ - { - const option_fixed = container.find(".option-fixed-timestamps") as JQuery; - const option_colloquial = container.find(".option-colloquial-timestamps") as JQuery; + return tag; + }, + footer: () => { + let footer = $.spawn("div"); + footer.addClass("modal-button-group"); + footer.css("margin-top", "5px"); + footer.css("margin-bottom", "5px"); + footer.css("text-align", "right"); - option_colloquial.on('change', event => { - settings.changeGlobal(Settings.KEY_CHAT_COLLOQUIAL_TIMESTAMPS, option_colloquial[0].checked); - }); + let buttonOk = $.spawn("button"); + buttonOk.text(tr("Close")); + buttonOk.click(() => info_modal.close()); + footer.append(buttonOk); - option_fixed.on('change', event => { - settings.changeGlobal(Settings.KEY_CHAT_FIXED_TIMESTAMPS, option_fixed[0].checked); - option_colloquial - .prop("disabled", option_fixed[0].checked) - .parents("label").toggleClass("disabled", option_fixed[0].checked); - if(option_fixed[0].checked) { - option_colloquial.prop("checked", false); - } else { - option_colloquial.prop("checked", settings.static_global(Settings.KEY_CHAT_COLLOQUIAL_TIMESTAMPS)); + return footer; + } + }); + info_modal.open() + }; + + const update_current_selected = () => { + const container_current = container.find(".selected-language6"); + container_current.empty().text(tr("Loading")); + + let current_translation: RepositoryTranslation; + i18n.iterate_repositories(repository => { + if(current_translation) return; + for(const entry of repository.translations) + if(i18n.config.translation_config().current_translation_path == entry.path) { + current_translation = entry; + return; } - }).prop("checked", settings.static_global(Settings.KEY_CHAT_FIXED_TIMESTAMPS)).trigger('change'); - } + }).then(() => { + container_current.empty(); - { - const option = container.find(".option-instant-channel-switch") as JQuery; - option.on('change', event => { - settings.changeGlobal(Settings.KEY_SWITCH_INSTANT_CHAT, option[0].checked); - }).prop("checked", settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)); - } - { - const option = container.find(".option-instant-client-switch") as JQuery; - option.on('change', event => { - settings.changeGlobal(Settings.KEY_SWITCH_INSTANT_CLIENT, option[0].checked); - }).prop("checked", settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT)); - } - { - const option = container.find(".option-colored-emojies") as JQuery; - option.on('change', event => { - settings.changeGlobal(Settings.KEY_CHAT_COLORED_EMOJIES, option[0].checked); - }).prop("checked", settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES)); - } - - const update_format_helper = () => server_connections.server_connection_handlers().map(e => e.side_bar).forEach(e => { - e.private_conversations().update_input_format_helper(); - e.channel_conversations().update_input_format_helper(); + const language = current_translation ? current_translation.country_code : "gb"; + $.spawn("div").addClass("country flag-" + language.toLowerCase()).attr('title', i18nc.country_name(language, tr("Unknown language"))).appendTo(container_current); + $.spawn("a").text(current_translation ? current_translation.name : tr("English (Default)")).appendTo(container_current); + }).catch(error => { + /* This shall never happen */ }); - { - const option = container.find(".option-support-markdown") as JQuery; - option.on('change', event => { - settings.changeGlobal(Settings.KEY_CHAT_ENABLE_MARKDOWN, option[0].checked); - update_format_helper(); - }).prop("checked", settings.static_global(Settings.KEY_CHAT_ENABLE_MARKDOWN)); - } - { - const option = container.find(".option-support-bbcode") as JQuery; - option.on('change', event => { - settings.changeGlobal(Settings.KEY_CHAT_ENABLE_BBCODE, option[0].checked); - update_format_helper(); - }).prop("checked", settings.static_global(Settings.KEY_CHAT_ENABLE_BBCODE)); - } - { - const option = container.find(".option-url-tagging") as JQuery; - option.on('change', event => { - settings.changeGlobal(Settings.KEY_CHAT_TAG_URLS, option[0].checked); - }).prop("checked", settings.static_global(Settings.KEY_CHAT_TAG_URLS)); - } - /* Icon size */ - { - const container_slider = container.find(".container-icon-size .container-slider"); - const container_value = container.find(".container-icon-size .value"); + }; - sliderfy(container_slider, { - unit: '%', - min_value: 25, - max_value: 300, - step: 5, - initial_value: settings.static_global(Settings.KEY_ICON_SIZE), - value_field: container_value + const initially_selected = i18n.config.translation_config().current_translation_url; + const update_list = () => { + container_entries.empty(); + + const currently_selected = i18n.config.translation_config().current_translation_url; + //Default translation + { + const tag = template.renderTag({ + type: "default", + selected: !currently_selected || currently_selected == "default", + fallback_country_name: i18nc.country_name('gb'), }); + tag.on('click', () => { + i18n.select_translation(undefined, undefined); + container_entries.find(".selected").removeClass("selected"); + tag.addClass("selected"); - container_slider.on('change', event => { - const value = parseInt(container_slider.attr("value") as string); - settings.changeGlobal(Settings.KEY_ICON_SIZE, value); - console.log("Changed icon size to %sem", (value / 100).toFixed(2)); + update_current_selected(); + restart_hint.toggle(initially_selected !== i18n.config.translation_config().current_translation_url); + }); + tag.appendTo(container_entries); + } - MessageHelper.set_icon_size((value / 100).toFixed(2) + "em"); + { + tag_loading.show(); + i18n.iterate_repositories(repo => { + let repo_tag = container_entries.find("[repository=\"" + repo.unique_id + "\"]"); + if (repo_tag.length == 0) { + repo_tag = template.renderTag({ + type: "repository", + name: repo.name || repo.url, + id: repo.unique_id + }); + + repo_tag.find(".button-delete").on('click', e => { + e.preventDefault(); + + spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this repository?"), answer => { + if (answer) { + i18n.delete_repository(repo); + update_list(); + } + }); + }); + repo_tag.find(".button-info").on('click', e => { + e.preventDefault(); + display_repository_info(repo); + }); + + container_entries.append(repo_tag); + } + + for(const translation of repo.translations) { + const tag = template.renderTag({ + type: "translation", + name: translation.name || translation.path, + id: repo.unique_id, + country_code: translation.country_code, + selected: i18n.config.translation_config().current_translation_path == translation.path, + fallback_country_name: i18nc.country_name('gb'), + country_name: i18nc.country_name((translation.country_code || "XX").toLowerCase()), + }); + tag.find(".button-info").on('click', e => { + e.preventDefault(); + display_translation_info(translation, repo); + }); + tag.on('click', e => { + if (e.isDefaultPrevented()) return; + i18n.select_translation(repo, translation); + container_entries.find(".selected").removeClass("selected"); + tag.addClass("selected"); + + update_current_selected(); + restart_hint.toggle(initially_selected !== i18n.config.translation_config().current_translation_url); + }); + tag.insertAfter(repo_tag); + } + }).then(() => tag_loading.hide()).catch(error => { + console.error(error); + /* this should NEVER happen */ + }) + } + + }; + + /* button add repository */ + { + container.find(".button-add-repository").on('click', () => { + createInputModal(tr("Enter repository URL"), tr("Enter repository URL:"), text => { + try { + new URL(text); + return true; + } catch(error) { + return false; + } + }, url => { + if (!url) return; + + tag_loading.show(); + i18n.load_repository(url as string).then(repository => { + i18n.register_repository(repository); + update_list(); + }).catch(error => { + tag_loading.hide(); + createErrorModal("Failed to load repository", tr("Failed to query repository.
Ensure that this repository is valid and reachable.
Error: ") + error).open(); + }) + }).open(); + }); + } + + container.find(".button-restart").on('click', () => { + if(loader.version().type === "web") { + location.reload(); + } else { + createErrorModal(tr("Not implemented"), tr("Client restart isn't implemented.
Please do it manually!")).open(); + } + }); + + update_list(); + update_current_selected(); +} + +function settings_general_chat(container: JQuery, modal: Modal) { + /* timestamp format */ + { + const option_fixed = container.find(".option-fixed-timestamps") as JQuery; + const option_colloquial = container.find(".option-colloquial-timestamps") as JQuery; + + option_colloquial.on('change', event => { + settings.changeGlobal(Settings.KEY_CHAT_COLLOQUIAL_TIMESTAMPS, option_colloquial[0].checked); + }); + + option_fixed.on('change', event => { + settings.changeGlobal(Settings.KEY_CHAT_FIXED_TIMESTAMPS, option_fixed[0].checked); + option_colloquial + .prop("disabled", option_fixed[0].checked) + .parents("label").toggleClass("disabled", option_fixed[0].checked); + if(option_fixed[0].checked) { + option_colloquial.prop("checked", false); + } else { + option_colloquial.prop("checked", settings.static_global(Settings.KEY_CHAT_COLLOQUIAL_TIMESTAMPS)); + } + }).prop("checked", settings.static_global(Settings.KEY_CHAT_FIXED_TIMESTAMPS)).trigger('change'); + } + + { + const option = container.find(".option-instant-channel-switch") as JQuery; + option.on('change', event => { + settings.changeGlobal(Settings.KEY_SWITCH_INSTANT_CHAT, option[0].checked); + }).prop("checked", settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)); + } + { + const option = container.find(".option-instant-client-switch") as JQuery; + option.on('change', event => { + settings.changeGlobal(Settings.KEY_SWITCH_INSTANT_CLIENT, option[0].checked); + }).prop("checked", settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT)); + } + { + const option = container.find(".option-colored-emojies") as JQuery; + option.on('change', event => { + settings.changeGlobal(Settings.KEY_CHAT_COLORED_EMOJIES, option[0].checked); + }).prop("checked", settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES)); + } + + const update_format_helper = () => server_connections.server_connection_handlers().map(e => e.side_bar).forEach(e => { + e.private_conversations().update_input_format_helper(); + e.channel_conversations().update_input_format_helper(); + }); + { + const option = container.find(".option-support-markdown") as JQuery; + option.on('change', event => { + settings.changeGlobal(Settings.KEY_CHAT_ENABLE_MARKDOWN, option[0].checked); + update_format_helper(); + }).prop("checked", settings.static_global(Settings.KEY_CHAT_ENABLE_MARKDOWN)); + } + { + const option = container.find(".option-support-bbcode") as JQuery; + option.on('change', event => { + settings.changeGlobal(Settings.KEY_CHAT_ENABLE_BBCODE, option[0].checked); + update_format_helper(); + }).prop("checked", settings.static_global(Settings.KEY_CHAT_ENABLE_BBCODE)); + } + { + const option = container.find(".option-url-tagging") as JQuery; + option.on('change', event => { + settings.changeGlobal(Settings.KEY_CHAT_TAG_URLS, option[0].checked); + }).prop("checked", settings.static_global(Settings.KEY_CHAT_TAG_URLS)); + } + /* Icon size */ + { + const container_slider = container.find(".container-icon-size .container-slider"); + const container_value = container.find(".container-icon-size .value"); + + sliderfy(container_slider, { + unit: '%', + min_value: 25, + max_value: 300, + step: 5, + initial_value: settings.static_global(Settings.KEY_ICON_SIZE), + value_field: container_value + }); + + container_slider.on('change', event => { + const value = parseInt(container_slider.attr("value") as string); + settings.changeGlobal(Settings.KEY_ICON_SIZE, value); + console.log("Changed icon size to %sem", (value / 100).toFixed(2)); + + set_icon_size((value / 100).toFixed(2) + "em"); + }); + } +} + +function settings_audio_microphone(container: JQuery, modal: Modal) { + const registry = new Registry(); + registry.enable_debug("settings-microphone"); + modal_settings.initialize_audio_microphone_controller(registry); + modal_settings.initialize_audio_microphone_view(container, registry); + + modal.close_listener.push(() => registry.fire_async("deinitialize")); + return; +} + +function settings_identity_profiles(container: JQuery, modal: Modal) { + const registry = new Registry(); + //registry.enable_debug("settings-identity"); + modal_settings.initialize_identity_profiles_controller(registry); + modal_settings.initialize_identity_profiles_view(container, registry, { + forum_setuppable: true + }); + + registry.on("setup-forum-connection", event => { + modal.htmlTag.find('.entry[container="identity-forum"]').trigger('click'); + }); + return () => registry.fire("reload-profile"); +} + +function settings_audio_speaker(container: JQuery, modal: Modal) { + /* devices */ + { + const container_devices = container.find(".left .container-devices"); + const contianer_error = container.find(".left .container-error"); + + const update_devices = () => { + container_devices.children().remove(); + + const current_selected = aplayer.current_device(); + const generate_device = (device: Device | undefined) => { + const selected = device === current_selected || (typeof(current_selected) !== "undefined" && typeof(device) !== "undefined" && current_selected.device_id == device.device_id); + + const tag = $.spawn("div").addClass("device").toggleClass("selected", selected).append( + $.spawn("div").addClass("container-selected").append( + $.spawn("div").addClass("icon_em client-apply") + ), + $.spawn("div").addClass("container-name").append( + $.spawn("div").addClass("device-driver").text( + device ? (device.driver || "Unknown driver") : "No device" + ), + $.spawn("div").addClass("device-name").text( + device ? (device.name || "Unknown name") : "No device" + ) + ) + ); + + tag.on('click', event => { + if(tag.hasClass("selected")) + return; + + const _old = container_devices.find(".selected"); + _old.removeClass("selected"); + tag.addClass("selected"); + + aplayer.set_device(device ? device.device_id : null).then(() => { + console.debug(tr("Changed default speaker device")); + }).catch((error) => { + _old.addClass("selected"); + tag.removeClass("selected"); + + console.error(tr("Failed to change speaker to device %o: %o"), device, error); + createErrorModal(tr("Failed to change speaker"), formatMessage(tr("Failed to change the speaker device to the target speaker{:br:}{}"), error)).open(); + }); + }); + + return tag; + }; + + generate_device(undefined).appendTo(container_devices); + aplayer.available_devices().then(result => { + contianer_error.text("").hide(); + result.forEach(e => generate_device(e).appendTo(container_devices)); + }).catch(error => { + if(typeof(error) === "string") + contianer_error.text(error).show(); + + console.log(tr("Failed to query available speaker devices: %o"), error); + contianer_error.text(tr("Errors occurred (View console)")).show(); + }); + }; + update_devices(); + + const button_update = container.find(".button-update"); + button_update.on('click', async event => { + button_update.prop("disabled", true); + try { + update_devices(); + } catch(error) { + console.error(tr("Failed to build new speaker device list: %o"), error); + } + button_update.prop("disabled", false); + }); + } + + /* slider */ + { + + { + const container_master = container.find(".container-volume-master"); + const slider = container_master.find(".container-slider"); + sliderfy(slider, { + min_value: 0, + max_value: 100, + step: 1, + initial_value: settings.static_global(Settings.KEY_SOUND_MASTER, 100), + value_field: [container_master.find(".container-value")] + }); + slider.on('change', event => { + const volume = parseInt(slider.attr('value')); + + if(aplayer.set_master_volume) + aplayer.set_master_volume(volume / 100); + settings.changeGlobal(Settings.KEY_SOUND_MASTER, volume); + }); + } + + { + const container_soundpack = container.find(".container-volume-soundpack"); + const slider = container_soundpack.find(".container-slider"); + sliderfy(slider, { + min_value: 0, + max_value: 100, + step: 1, + initial_value: settings.static_global(Settings.KEY_SOUND_MASTER_SOUNDS, 100), + value_field: [container_soundpack.find(".container-value")] + }); + slider.on('change', event => { + const volume = parseInt(slider.attr('value')); + set_master_volume(volume / 100); + settings.changeGlobal(Settings.KEY_SOUND_MASTER_SOUNDS, volume); }); } } - function settings_audio_microphone(container: JQuery, modal: Modal) { - const registry = new events.Registry(); - registry.enable_debug("settings-microphone"); - modal_settings.initialize_audio_microphone_controller(registry); - modal_settings.initialize_audio_microphone_view(container, registry); + /* button test sound */ + { + container.find(".button-test-sound").on('click', event => { + manager.play(Sound.SOUND_TEST, { + default_volume: 1, + ignore_muted: true, + ignore_overlap: true + }) + }); + } +} - modal.close_listener.push(() => registry.fire_async("deinitialize")); - return; +function settings_audio_sounds(contianer: JQuery, modal: Modal) { + /* initialize sound list */ + { + const container_sounds = contianer.find(".container-sounds"); + + const generate_sound = (_sound: Sound) => { + let tag_play_pause: JQuery, tag_play: JQuery, tag_pause: JQuery, tag_input_muted: JQuery; + let tag = $.spawn("div").addClass("sound").append( + tag_play_pause = $.spawn("div").addClass("container-button-play_pause").append( + tag_play = $.spawn("img").attr("src", "img/icon_sound_play.svg"), + tag_pause = $.spawn("img").attr("src", "img/icon_sound_pause.svg") + ), + $.spawn("div").addClass("container-name").text(_sound), + $.spawn("label").addClass("container-button-toggle").append( + $.spawn("div").addClass("switch").append( + tag_input_muted = $.spawn("input").attr("type", "checkbox"), + $.spawn("span").addClass("slider").append( + $.spawn("div").addClass("dot") + ) + ) + ) + ); + + tag_play_pause.on('click', event => { + if(tag_pause.is(":visible")) + return; + tag_play.hide(); + tag_pause.show(); + + const _done = flag => { + tag_pause.hide(); + tag_play.show(); + }; + const _timeout = setTimeout(() => _done(false), 10 * 1000); /* the sounds are not longer than 10 seconds */ + + sound.manager.play(_sound, { + ignore_overlap: true, + ignore_muted: true, + default_volume: 1, + + callback: flag => { + clearTimeout(_timeout); + _done(flag); + } + }); + }); + tag_pause.hide(); + + tag_input_muted.prop("checked", sound.get_sound_volume(_sound, 1) > 0); + tag_input_muted.on('change', event => { + const volume = tag_input_muted.prop("checked") ? 1 : 0; + sound.set_sound_volume(_sound, volume); + console.log(tr("Changed sound volume to %o for sound %o"), volume, _sound); + }); + + return tag; + }; + + //container-sounds + for(const sound_key in Sound) + generate_sound(Sound[sound_key as any] as any).appendTo(container_sounds); + + /* the filter */ + const input_filter = contianer.find(".input-sounds-filter"); + input_filter.on('change keyup', event => { + const filter = input_filter.val() as string; + + container_sounds.find(".sound").each((_, _element) => { + const element = $(_element); + element.toggle(filter.length == 0 || element.text().toLowerCase().indexOf(filter) !== -1); + }) + }); } - function settings_identity_profiles(container: JQuery, modal: Modal) { - const registry = new events.Registry(); - //registry.enable_debug("settings-identity"); - modal_settings.initialize_identity_profiles_controller(registry); - modal_settings.initialize_identity_profiles_view(container, registry, { - forum_setuppable: true - }); + const overlap_tag = contianer.find(".option-overlap-same"); + overlap_tag.on('change', event => { + const activated = (event.target).checked; + sound.set_overlap_activated(activated); + }).prop("checked", sound.overlap_activated()); - registry.on("setup-forum-connection", event => { - modal.htmlTag.find('.entry[container="identity-forum"]').trigger('click'); - }); - return () => registry.fire("reload-profile"); + const mute_tag = contianer.find(".option-mute-output"); + mute_tag.on('change', event => { + const activated = (event.target).checked; + sound.set_ignore_output_muted(!activated); + }).prop("checked", !sound.ignore_output_muted()); + + modal.close_listener.push(sound.save); +} + +export namespace modal_settings { + export interface ProfileViewSettings { + forum_setuppable: boolean } + export function initialize_identity_profiles_controller(event_registry: Registry) { + const send_error = (event, profile, text) => event_registry.fire_async(event, { status: "error", profile_id: profile, error: text }); + event_registry.on("create-profile", event => { + const profile = profiles.create_new_profile(event.name); + profiles.mark_need_save(); + event_registry.fire_async("create-profile-result", { + status: "success", + name: event.name, + profile_id: profile.id + }); + }); - function settings_audio_speaker(container: JQuery, modal: Modal) { - /* devices */ + event_registry.on("delete-profile", event => { + const profile = profiles.find_profile(event.profile_id); + if(!profile) { + log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); + send_error("delete-profile-result", event.profile_id, tr("Unknown profile")); + return; + } + + profiles.delete_profile(profile); + event_registry.fire_async("delete-profile-result", { status: "success", profile_id: event.profile_id }); + }); + + const build_profile_info = (profile: ConnectionProfile) => { + const forum_data = profile.selected_identity(IdentitifyType.TEAFORO) as TeaForumIdentity; + const teamspeak_data = profile.selected_identity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity; + const nickname_data = profile.selected_identity(IdentitifyType.NICKNAME) as NameIdentity; + + return { + id: profile.id, + name: profile.profile_name, + nickname: profile.default_username, + identity_type: profile.selected_identity_type as any, + identity_forum: !forum_data ? undefined : { + valid: forum_data.valid(), + fallback_name: forum_data.fallback_name() + }, + identity_nickname: !nickname_data ? undefined : { + name: nickname_data.name(), + fallback_name: nickname_data.fallback_name() + }, + identity_teamspeak: !teamspeak_data ? undefined : { + unique_id: teamspeak_data.uid(), + fallback_name: teamspeak_data.fallback_name() + } + } + }; + event_registry.on("query-profile-list", event => { + event_registry.fire_async("query-profile-list-result", { status: "success", profiles: profiles.profiles().map(e => build_profile_info(e)) }); + }); + + event_registry.on("query-profile", event => { + const profile = profiles.find_profile(event.profile_id); + if(!profile) { + log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); + send_error("query-profile-result", event.profile_id, tr("Unknown profile")); + return; + } + + event_registry.fire_async("query-profile-result", { status: "success", profile_id: event.profile_id, info: build_profile_info(profile)}); + }); + + event_registry.on("set-default-profile", event => { + const profile = profiles.find_profile(event.profile_id); + if(!profile) { + log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); + send_error("set-default-profile-result", event.profile_id, tr("Unknown profile")); + return; + } + + const old = profiles.set_default_profile(profile); + event_registry.fire_async("set-default-profile-result", { status: "success", old_profile_id: event.profile_id, new_profile_id: old.id }); + }); + + event_registry.on("set-profile-name", event => { + const profile = profiles.find_profile(event.profile_id); + if(!profile) { + log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); + send_error("set-profile-name-result", event.profile_id, tr("Unknown profile")); + return; + } + + profile.profile_name = event.name; + profiles.mark_need_save(); + event_registry.fire_async("set-profile-name-result", { name: event.name, profile_id: event.profile_id, status: "success" }); + }); + + event_registry.on("set-default-name", event => { + const profile = profiles.find_profile(event.profile_id); + if(!profile) { + log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); + send_error("set-default-name-result", event.profile_id, tr("Unknown profile")); + return; + } + + profile.default_username = event.name; + profiles.mark_need_save(); + event_registry.fire_async("set-default-name-result", { name: event.name, profile_id: event.profile_id, status: "success" }); + }); + + event_registry.on("set-identity-name-name", event => { + const profile = profiles.find_profile(event.profile_id); + if(!profile) { + log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); + send_error("set-identity-name-name-result", event.profile_id, tr("Unknown profile")); + return; + } + + let identity = profile.selected_identity(IdentitifyType.NICKNAME) as NameIdentity; + if(!identity) + profile.set_identity(IdentitifyType.NICKNAME, identity = new NameIdentity()); + identity.set_name(event.name); + profiles.mark_need_save(); + + event_registry.fire_async("set-identity-name-name-result", { name: event.name, profile_id: event.profile_id, status: "success" }); + }); + + event_registry.on("query-profile-validity", event => { + const profile = profiles.find_profile(event.profile_id); + if(!profile) { + log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); + send_error("query-profile-validity-result", event.profile_id, tr("Unknown profile")); + return; + } + + event_registry.fire_async("query-profile-validity-result", { status: "success", profile_id: event.profile_id, valid: profile.valid() }); + }); + + event_registry.on("query-identity-teamspeak", event => { + const profile = profiles.find_profile(event.profile_id); + if(!profile) { + log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); + send_error("query-identity-teamspeak-result", event.profile_id, tr("Unknown profile")); + return; + } + + const ts = profile.selected_identity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity; + if(!ts) { + event_registry.fire_async("query-identity-teamspeak-result", { status: "error", profile_id: event.profile_id, error: tr("Missing identity") }); + return; + } + + ts.level().then(level => { + event_registry.fire_async("query-identity-teamspeak-result", { status: "success", level: level, profile_id: event.profile_id }); + }).catch(error => { + send_error("query-identity-teamspeak-result", event.profile_id, tr("failed to calculate level")); + }) + }); + + event_registry.on("select-identity-type", event => { + const profile = profiles.find_profile(event.profile_id); + if(!profile) { + log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); + return; + } + + profile.selected_identity_type = event.identity_type; + profiles.mark_need_save(); + }); + + event_registry.on("generate-identity-teamspeak", event => { + const profile = profiles.find_profile(event.profile_id); + if(!profile) { + log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); + send_error("generate-identity-teamspeak-result", event.profile_id, tr("Unknown profile")); + return; + } + + TeaSpeakIdentity.generate_new().then(identity => { + profile.set_identity(IdentitifyType.TEAMSPEAK, identity); + profiles.mark_need_save(); + + identity.level().then(level => { + event_registry.fire_async("generate-identity-teamspeak-result", { + status: "success", + profile_id: event.profile_id, + unique_id: identity.uid(), + level: level + }); + }).catch(error => { + console.error(tr("Failed to calculate level for a new identity. Error object: %o"), error); + send_error("generate-identity-teamspeak-result", event.profile_id, tr("failed to calculate level: ") + error); + }) + }).catch(error => { + console.error(tr("Failed to generate a new identity. Error object: %o"), error); + send_error("generate-identity-teamspeak-result", event.profile_id, tr("failed to generate identity: ") + error); + }); + }); + + event_registry.on("import-identity-teamspeak", event => { + const profile = profiles.find_profile(event.profile_id); + if(!profile) { + log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); + return; + } + + spawnTeamSpeakIdentityImport(identity => { + profile.set_identity(IdentitifyType.TEAMSPEAK, identity); + profiles.mark_need_save(); + + identity.level().catch(error => { + console.error(tr("Failed to calculate level for a new imported identity. Error object: %o"), error); + return Promise.resolve(undefined); + }).then(level => { + event_registry.fire_async("import-identity-teamspeak-result", { + profile_id: event.profile_id, + unique_id: identity.uid(), + level: level + }); + }); + }); + }); + + event_registry.on("improve-identity-teamspeak-level", event => { + const profile = profiles.find_profile(event.profile_id); + if(!profile) { + log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); + return; + } + + const identity = profile.selected_identity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity; + if (!identity) return; + + spawnTeamSpeakIdentityImprove(identity, profile.profile_name).close_listener.push(() => { + profiles.mark_need_save(); + + identity.level().then(level => { + event_registry.fire_async("improve-identity-teamspeak-level-update", { profile_id: event.profile_id, new_level: level }); + }).catch(error => { + log.error(LogCategory.CLIENT, tr("Failed to calculate identity level after improvement (%o)"), error); + }); + }); + }); + + event_registry.on("export-identity-teamspeak", event => { + const profile = profiles.find_profile(event.profile_id); + if(!profile) { + log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); + return; + } + + const identity = profile.selected_identity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity; + if (!identity) return; + + identity.export_ts(true).then(data => { + const element = $.spawn("a") + .text("donwload") + .attr("href", "data:test/plain;charset=utf-8," + encodeURIComponent(data)) + .attr("download", name + ".ini") + .css("display", "none") + .appendTo($("body")); + element[0].click(); + element.remove(); + }).catch(error => { + console.error(error); + createErrorModal(tr("Failed to export identity"), tr("Failed to export and save identity.
Error: ") + error).open(); + }); + }); + } + export function initialize_identity_profiles_view(container: JQuery, event_registry: Registry, settings: ProfileViewSettings) { + /* profile list */ { - const container_devices = container.find(".left .container-devices"); - const contianer_error = container.find(".left .container-error"); + const container_profiles = container.find(".container-profiles"); + let selected_profile; - const update_devices = () => { - container_devices.children().remove(); + const overlay_error = container_profiles.find(".overlay-error"); + const overlay_timeout = container_profiles.find(".overlay-timeout"); + const overlay_empty = container_profiles.find(".overlay-empty"); - const current_selected = audio.player.current_device(); - const generate_device = (device: audio.player.Device | undefined) => { - const selected = device === current_selected || (typeof(current_selected) !== "undefined" && typeof(device) !== "undefined" && current_selected.device_id == device.device_id); + const build_profile = (profile: events.modal.settings.ProfileInfo, selected: boolean) => { + let tag_avatar: JQuery, tag_default: JQuery; + let tag = $.spawn("div").addClass("profile").attr("profile-id", profile.id).append( + tag_avatar = $.spawn("div").addClass("container-avatar"), + $.spawn("div").addClass("container-info").append( + $.spawn("div").addClass("container-type").append( + $.spawn("div").addClass("identity-type").text(profile.identity_type || tr("Type unset")), + tag_default = $.spawn("div").addClass("tag-default").text(tr("(Default)")), + $.spawn("div").addClass("icon_em icon-status").hide() + ), + $.spawn("div").addClass("profile-name").text(profile.name || tr("Unnamed")) + ) + ); + tag_avatar.hide(); /* no avatars yet */ - const tag = $.spawn("div").addClass("device").toggleClass("selected", selected).append( + tag.on('click', event => event_registry.fire("select-profile", { profile_id: profile.id })); + tag.toggleClass("selected", selected); + tag_default.toggle(profile.id === "default"); + + event_registry.fire("query-profile-validity", { profile_id: profile.id }); + return tag; + }; + + event_registry.on("select-profile", event => { + container_profiles.find(".profile").removeClass("selected"); + container_profiles.find(".profile[profile-id='" + event.profile_id + "']").addClass("selected"); + selected_profile = event.profile_id; + }); + + + event_registry.on("query-profile-list", event => { + container_profiles.find(".profile").remove(); + }); + + event_registry.on("query-profile-list-result", event => { + container_profiles.find(".overlay").hide(); + if(event.status === "error") { + overlay_error.show().find(".error").text(event.error || tr("unknown error")); + return; + } else if(event.status === "timeout") { + overlay_timeout.show(); + return; + } + if(!event.profiles.length) { + overlay_empty.show(); + return; + } + + container_profiles.find(".overlay").hide(); + container_profiles.find(".profile").remove(); + event.profiles.forEach(e => build_profile(e, e.id == selected_profile).appendTo(container_profiles)); + }); + + event_registry.on("delete-profile-result", event => { + if(event.status !== "success") return; + + //TODO: Animate removal? + container_profiles.find(".profile[profile-id='" + event.profile_id + "']").remove(); + }); + + event_registry.on('create-profile-result', event => { + if(event.status !== "success") return; + + event_registry.fire("query-profile-list"); + event_registry.one("query-profile-list-result", e => event_registry.fire("select-profile", { profile_id: event.profile_id })); + }); + + event_registry.on("set-profile-name-result", event => { + if(event.status !== "success") return; + + const profile = container_profiles.find(".profile[profile-id='" + event.profile_id + "']"); + profile.find(".profile-name").text(event.name || tr("Unnamed")); + }); + + event_registry.on("set-default-profile-result", event => { + if(event.status !== "success") return; + + const old_profile = container_profiles.find(".profile[profile-id='default']"); + const new_profile = container_profiles.find(".profile[profile-id='" + event.old_profile_id + "']"); + old_profile.attr("profile-id", event.new_profile_id).find(".tag-default").hide(); + new_profile.attr("profile-id", "default").find(".tag-default").show(); + }); + + event_registry.on("select-identity-type", event => { + if(!event.identity_type) return; + + const profile = container_profiles.find(".profile[profile-id='" + event.profile_id + "']"); + profile.find(".identity-type").text(event.identity_type.toUpperCase() || tr("Type unset")); + }); + + event_registry.on("query-profile-validity-result", event => { + const profile = container_profiles.find(".profile[profile-id='" + event.profile_id + "']"); + profile.find(".icon-status") + .show() + .toggleClass("client-apply", event.status === "success" && event.valid) + .toggleClass("client-delete", event.status !== "success" || !event.valid) + .attr("title", event.status === "success" ? event.valid ? tr("Profile is valid") : tr("Provile is invalid") : event.error || tr("failed to query status")); + }); + + /* status indicator updaters */ + event_registry.on("select-identity-type", event => { + if(!event.profile_id) return; + + /* we need a short delay so everything could apply*/ + setTimeout(() => { + event_registry.fire("query-profile-validity", { profile_id: event.profile_id }); + }, 100); + }); + event_registry.on(["set-default-name-result", "set-profile-name-result", "set-identity-name-name-result", "generate-identity-teamspeak-result"], event => { + if(!('status' in event) ||!('profile_id' in event)) { + log.warn(LogCategory.CLIENT, tr("Profile status watcher encountered an unuseal event!")); + return; + } + if((event as any).status !== "success") return; + event_registry.fire("query-profile-validity", { profile_id: (event as any).profile_id }); + }) + } + + /* list buttons */ + { + /* reload */ + { + const button = container.find(".button-reload-list"); + + button.on('click', event => event_registry.fire("query-profile-list")); + + event_registry.on("query-profile-list", event => button.prop("disabled", true)); + event_registry.on("query-profile-list-result", event => button.prop("disabled", false)); + } + + /* set default */ + { + const button = container.find(".button-set-default"); + let current_profile; + + button.on('click', event => event_registry.fire("set-default-profile", { profile_id: current_profile })); + event_registry.on("select-profile", event => { + current_profile = event.profile_id; + button.prop("disabled", !event.profile_id || event.profile_id === "default"); + }); + + event_registry.on("set-default-profile-result", event => { + if(event.status === "success") return; + + createErrorModal(tr("Failed to set default profile"), tr("Failed to set default profile:") + "
" + (event.status === "timeout" ? tr("request timeout") : (event.error || tr("unknown error")))).open(); + }); + button.prop("disabled", true); + } + + /* delete button */ + { + const button = container.find(".button-delete"); + let current_profile; + + button.on('click', event => { + if(!current_profile || current_profile === "default") return; + + spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this profile?"), result => { + if (result) + event_registry.fire("delete-profile", { profile_id: current_profile }); + }); + }); + + event_registry.on("delete-profile-result", event => { + if(event.status === "success") return; + + createErrorModal(tr("Failed to delete profile"), tr("Failed to delete profile:") + "
" + (event.status === "timeout" ? tr("request timeout") : (event.error || tr("unknown error")))).open(); + }); + + event_registry.on("select-profile", event => { + current_profile = event.profile_id; + + button.prop("disabled", !event.profile_id || event.profile_id === "default"); + }); + } + + /* create button */ + { + const button = container.find(".button-create"); + button.on('click', event => { + createInputModal(tr("Please enter a name"), tr("Please enter a name for the new profile:"), text => text.length >= 3 && !profiles.find_profile_by_name(text), value => { + if (value) + event_registry.fire("create-profile", { name: value as string }); + }).open(); + }); + + event_registry.on('create-profile', event => button.prop("disabled", true)); + event_registry.on("create-profile-result", event => { + button.prop("disabled", false); + if(event.status === "success") { + event_registry.fire("select-profile", { profile_id: event.profile_id }); + return; + } + + createErrorModal(tr("Failed to create profile"), tr("Failed to create new profile:") + "
" + (event.status === "timeout" ? tr("request timeout") : (event.error || tr("unknown error")))).open(); + }) + } + } + + + /* profile info */ + { + let current_profile; + const error_text = event => event.status === "timeout" ? tr("request timeout") : (event.error || tr("unknown error")); + + /* general info */ + { + /* profile name */ + { + const input = container.find(".profile-name"); + let last_name; + + const update_name = () => input.prop("disabled", false) + .val(last_name) + .attr("placeholder", tr("Profile name")) + .parent().removeClass("is-invalid"); + + const info_name = text => input.prop("disabled", true) + .val(null) + .attr("placeholder", text) + .parent().removeClass("is-invalid"); + + event_registry.on("query-profile", event => { + if(event.profile_id !== current_profile) return; + + info_name(tr("loading")); + }); + + event_registry.on("query-profile-result", event => { + if(event.profile_id !== current_profile) return; + + if(event.status === "success") { + last_name = event.info.name; + update_name(); + } else { + info_name(error_text(event)); + } + }); + + event_registry.on("set-profile-name", event => { + if(event.profile_id !== current_profile) return; + + info_name(tr("saving")); + }); + + event_registry.on("set-profile-name-result", event => { + if(event.status !== "success") { + createErrorModal(tr("Failed to change profile name"), tr("Failed to create apply new name:") + "
" + error_text(event)).open(); + } else { + last_name = event.name; + } + update_name(); + }); + + input.on('keyup', event => { + const text = input.val() as string; + const profile = profiles.find_profile_by_name(text); + input.parent().toggleClass("is-invalid", text.length < 3 || (profile && profile.id != current_profile)); + }).on('change', event => { + const text = input.val() as string; + const profile = profiles.find_profile_by_name(text); + if(text.length < 3 || (profile && profile.id != current_profile)) return; + + event_registry.fire("set-profile-name", { profile_id: current_profile, name: text }); + }); + } + + /* nickname name */ + { + const input = container.find(".profile-default-name"); + let last_name = null, fallback_names = {}, current_identity_type = ""; + + const update_name = () => input.prop("disabled", false) + .val(last_name) + .attr("placeholder", fallback_names[current_identity_type] || tr("Another TeaSpeak user")) + .parent().removeClass("is-invalid"); + + const info_name = text => input.prop("disabled", true) + .val(null) + .attr("placeholder", text) + .parent().removeClass("is-invalid"); + + event_registry.on("query-profile", event => { + if(event.profile_id !== current_profile) return; + + input.prop("disabled", true).val(null).attr("placeholder", tr("loading")); + }); + + event_registry.on("query-profile-result", event => { + if(event.profile_id !== current_profile) return; + if(event.status === "success") { + current_identity_type = event.info.identity_type; + fallback_names["nickname"] = event.info.identity_nickname ? event.info.identity_nickname.fallback_name : undefined; + fallback_names["teaforo"] = event.info.identity_forum ? event.info.identity_forum.fallback_name : undefined; + fallback_names["teamspeak"] = event.info.identity_teamspeak ? event.info.identity_teamspeak.fallback_name : undefined; + + last_name = event.info.nickname; + update_name(); + } else { + info_name(error_text(event)); + } + }); + + event_registry.on("select-identity-type", event => { + if (current_identity_type === event.identity_type) return; + + current_identity_type = event.identity_type; + update_name(); + }); + + event_registry.on("set-default-name", event => { + if(event.profile_id !== current_profile) return; + + info_name(tr("saving")); + }); + + event_registry.on("set-default-name-result", event => { + if(event.status !== "success") { + createErrorModal(tr("Failed to change nickname"), tr("Failed to create apply new nickname:") + "
" + error_text(event)).open(); + } else { + last_name = event.name; + } + update_name(); + }); + + input.on('keyup', event => { + const text = input.val() as string; + input.parent().toggleClass("is-invalid", text.length != 0 && text.length < 3); + }).on('change', event => { + const text = input.val() as string; + if(text.length != 0 && text.length < 3) return; + + event_registry.fire("set-default-name", { profile_id: current_profile, name: text }); + }); + } + + /* identity type */ + { + const select_identity_type = container.find(".profile-identity-type"); + + const show_message = (text, is_invalid) => select_identity_type + .toggleClass("is-invalid", is_invalid) + .prop("disabled", true) + .find("option[value=error]") + .text(text) + .prop("selected", true); + + const set_type = type => select_identity_type + .toggleClass("is-invalid", type === "unset") + .prop("disabled", false) + .find("option[value=" + type + "]") + .prop("selected", true); + + event_registry.on("query-profile", event => show_message(tr("loading"), false)); + + event_registry.on("select-identity-type", event => { + if(event.profile_id !== current_profile) return; + + set_type(event.identity_type || "unset"); + }); + + event_registry.on("query-profile-result", event => { + if(event.profile_id !== current_profile) return; + + if(event.status === "success") + event_registry.fire("select-identity-type", { profile_id: event.profile_id, identity_type: event.info.identity_type }); + else + show_message(error_text(event), false); + }); + + select_identity_type.on('change', event => { + const type = (select_identity_type.val() as string).toLowerCase(); + if(type === "error" || type == "unset") return; + + event_registry.fire("select-identity-type", { profile_id: current_profile, identity_type: type as any }); + }); + } + + /* avatar */ + { + container.find(".button-change-avatar").hide(); + } + } + + /* special info TeamSpeak */ + { + const container_settings = container.find(".container-teamspeak"); + const container_valid = container_settings.find(".container-valid"); + const container_invalid = container_settings.find(".container-invalid"); + + const input_current_level = container_settings.find(".current-level"); + const input_unique_id = container_settings.find(".unique-id"); + + const button_new = container_settings.find(".button-new"); + const button_improve = container_settings.find(".button-improve"); + + const button_import = container_settings.find(".button-import"); + const button_export = container_settings.find(".button-export"); + + let is_profile_generated = false; + + event_registry.on("select-identity-type", event => { + if(event.profile_id !== current_profile) return; + + container_settings.toggle(event.identity_type === "teamspeak"); + }); + + event_registry.on("query-profile", event => { + input_unique_id.val(null).attr("placeholder", tr("loading")); + input_current_level.val(null).attr("placeholder", tr("loading")); + + button_new.prop("disabled", true); + button_improve.prop("disabled", true); + button_import.prop("disabled", true); + button_export.prop("disabled", true); + }); + + const update_identity = (state: "not-created" | "created", unique_id?: string, level?: number) => { + if(state === "not-created") { + container_invalid.show(); + container_valid.hide(); + + button_improve.prop("disabled", true); + button_export.prop("disabled", true); + } else { + container_invalid.hide(); + container_valid.show(); + + input_unique_id.val(unique_id).attr("placeholder", null); + if(typeof level !== "number") + event_registry.fire("query-identity-teamspeak", { profile_id: current_profile }); + else + input_current_level.val(level).attr("placeholder", null); + + button_improve.prop("disabled", false); + button_export.prop("disabled", false); + } + + is_profile_generated = state === "created"; + button_new.toggleClass("btn-blue", !is_profile_generated).toggleClass("btn-red", is_profile_generated); + button_import.toggleClass("btn-blue", !is_profile_generated).toggleClass("btn-red", is_profile_generated); + + button_new.prop("disabled", false); + button_import.prop("disabled", false); + }; + + event_registry.on("query-profile-result", event => { + if(event.profile_id !== current_profile) return; + + if(event.status !== "success") { + input_unique_id.val(null).attr("placeholder", error_text(event)); + return; + } + + if(!event.info.identity_teamspeak) + update_identity("not-created"); + else + update_identity("created", event.info.identity_teamspeak.unique_id); + }); + + event_registry.on("query-identity-teamspeak-result", event => { + if(event.profile_id !== current_profile) return; + + if(event.status === "success") { + input_current_level.val(event.level).attr("placeholder", null); + } else { + input_current_level.val(null).attr("placeholder", error_text(event)); + } + }); + + /* the new button */ + { + button_new.on('click', event => { + if(is_profile_generated) { + spawnYesNo(tr("Are you sure"), tr("Do you really want to generate a new identity and override the old identity?"), result => { + if (result) event_registry.fire("generate-identity-teamspeak", { profile_id: current_profile }); + }); + } else { + event_registry.fire("generate-identity-teamspeak", { profile_id: current_profile }); + } + }); + + event_registry.on("generate-identity-teamspeak-result", event => { + if(event.profile_id !== current_profile) return; + + if(event.status !== "success") { + createErrorModal(tr("Failed to generate a new identity"), tr("Failed to create a new identity:") + "
" + error_text(event)).open(); + return; + } + + update_identity("created", event.unique_id, event.level); + createInfoModal(tr("Identity generated"), tr("A new identity had been successfully generated")).open(); + }); + } + + /* the import identity */ + { + button_import.on('click', event => { + if(is_profile_generated) { + spawnYesNo(tr("Are you sure"), tr("Do you really want to import a new identity and override the old identity?"), result => { + if (result) event_registry.fire("import-identity-teamspeak", { profile_id: current_profile }); + }); + } else { + event_registry.fire("import-identity-teamspeak", { profile_id: current_profile }); + } + }); + + event_registry.on("improve-identity-teamspeak-level-update", event => { + if(event.profile_id !== current_profile) return; + + input_current_level.val(event.new_level).attr("placeholder", null); + }); + + event_registry.on("import-identity-teamspeak-result", event => { + if(event.profile_id !== current_profile) return; + + event_registry.fire_async("query-profile", { profile_id: event.profile_id }); /* we do it like this so the default nickname changes as well */ + createInfoModal(tr("Identity imported"), tr("Your identity had been successfully imported generated")).open(); + }); + } + + /* identity export */ + { + button_export.on('click', event => { + createInputModal(tr("File name"), tr("Please enter the file name"), text => !!text, name => { + if (name) + event_registry.fire("export-identity-teamspeak", { profile_id: current_profile, filename: name as string }); + }).open(); + }); + } + + /* the improve button */ + button_improve.on('click', event => event_registry.fire("improve-identity-teamspeak-level", { profile_id: current_profile })); + } + + /* special info TeaSpeak - Forum */ + { + const container_settings = container.find(".container-teaforo"); + const container_valid = container_settings.find(".container-valid"); + const container_invalid = container_settings.find(".container-invalid"); + + const button_setup = container_settings.find(".button-setup"); + + event_registry.on("select-identity-type", event => { + if(event.profile_id !== current_profile) return; + + container_settings.toggle(event.identity_type === "teaforo"); + }); + + event_registry.on("query-profile", event => { + container_valid.toggle(false); + container_invalid.toggle(false); + }); + + event_registry.on("query-profile-result", event => { + if(event.profile_id !== current_profile) return; + + const valid = event.status === "success" && event.info.identity_forum && event.info.identity_forum.valid; + container_valid.toggle(!!valid); + container_invalid.toggle(!valid); + }); + + button_setup.on('click', event => event_registry.fire_async("setup-forum-connection")); + button_setup.toggle(settings.forum_setuppable); + } + + /* special info nickname */ + { + const container_settings = container.find(".container-nickname"); + const input_nickname = container_settings.find(".nickname"); + let last_name; + + const update_name = () => input_nickname.prop("disabled", false) + .val(last_name) + .attr("placeholder", tr("Identity base name")) + .parent().removeClass("is-invalid"); + + const show_info = text => input_nickname.prop("disabled", true) + .val(null) + .attr("placeholder", text) + .parent().removeClass("is-invalid"); + + event_registry.on("select-identity-type", event => event.profile_id === current_profile && container_settings.toggle(event.identity_type === "nickname")); + + event_registry.on("query-profile", event => { + if(event.profile_id !== current_profile) return; + + show_info(tr("loading")); + }); + + event_registry.on("query-profile-result", event => { + if(event.profile_id !== current_profile) return; + + if(event.status === "success") { + last_name = event.info.identity_nickname ? event.info.identity_nickname.name : null; + update_name(); + } else { + show_info(error_text(event)); + } + }); + + event_registry.on("set-identity-name-name", event => { + if(event.profile_id !== current_profile) return; + show_info(tr("saving")); + }); + + event_registry.on("set-identity-name-name-result", event => { + if(event.status !== "success") { + createErrorModal(tr("Failed to change name"), tr("Failed to create new name:") + "
" + error_text(event)).open(); + } else { + last_name = event.name; + } + update_name(); + }); + + input_nickname.on('keyup', event => { + const text = input_nickname.val() as string; + const profile = profiles.find_profile_by_name(text); + input_nickname.parent().toggleClass("is-invalid", text.length < 3 || (profile && profile.id != current_profile)); + }).on('change', event => { + const text = input_nickname.val() as string; + const profile = profiles.find_profile_by_name(text); + if(text.length < 3 || (profile && profile.id != current_profile)) return; + + event_registry.fire("set-identity-name-name", { profile_id: current_profile, name: text }); + }); + } + event_registry.on("select-profile", e => current_profile = e.profile_id); + } + + /* timeouts */ + { + /* profile list */ + { + let timeout; + event_registry.on("query-profile-list", event => timeout = setTimeout(() => event_registry.fire("query-profile-list-result", { status: "timeout" }), 5000)); + event_registry.on("query-profile-list-result", event => { + clearTimeout(timeout); + timeout = undefined; + }); + } + + /* profile create */ + { + const timeouts = {}; + event_registry.on("create-profile", event => { + clearTimeout(timeouts[event.name]); + timeouts[event.name] = setTimeout(() => { + event_registry.fire("create-profile-result", { name: event.name, status: "timeout" }); + }, 5000); + }); + + event_registry.on("create-profile-result", event => { + clearTimeout(timeouts[event.name]); + delete timeouts[event.name]; + }); + } + + /* profile set default create */ + { + const timeouts = {}; + event_registry.on("set-default-profile", event => { + clearTimeout(timeouts[event.profile_id]); + timeouts[event.profile_id] = setTimeout(() => { + event_registry.fire("set-default-profile-result", { old_profile_id: event.profile_id, status: "timeout" }); + }, 5000); + }); + + event_registry.on("set-default-profile-result", event => { + clearTimeout(timeouts[event.old_profile_id]); + delete timeouts[event.old_profile_id]; + }); + } + + const create_standard_timeout = (event: keyof events.modal.settings.profiles, response_event: keyof events.modal.settings.profiles, key: string) => { + const timeouts = {}; + event_registry.on(event, event => { + clearTimeout(timeouts[event[key]]); + timeouts[event[key]] = setTimeout(() => { + const timeout_event = { status: "timeout" }; + timeout_event[key] = event[key]; + event_registry.fire(response_event, timeout_event as any); + }, 5000); + }); + + event_registry.on(response_event, event => { + clearTimeout(timeouts[event[key]]); + delete timeouts[event[key]]; + }); + }; + + create_standard_timeout("query-profile", "query-profile-result", "profile_id"); + create_standard_timeout("query-identity-teamspeak", "query-identity-teamspeak-result", "profile_id"); + create_standard_timeout("delete-profile", "delete-profile-result", "profile_id"); + create_standard_timeout("set-profile-name", "set-profile-name-result", "profile_id"); + create_standard_timeout("set-default-name", "set-default-name-result", "profile_id"); + create_standard_timeout("query-profile-validity", "query-profile-validity-result", "profile_id"); + create_standard_timeout("set-identity-name-name", "set-identity-name-name-result", "profile_id"); + create_standard_timeout("generate-identity-teamspeak", "generate-identity-teamspeak-result", "profile_id"); + } + + /* some view semantics */ + { + let selected_profile; + event_registry.on("delete-profile-result", event => { + if(event.status !== "success") return; + if(event.profile_id !== selected_profile) return; + + /* the selected profile has been deleted, so we need to select another one */ + event_registry.fire("select-profile", { profile_id: "default" }); + }); + + /* reselect the default profile or the new default profile */ + event_registry.on("set-default-profile-result", event => { + if(event.status !== "success") return; + if(selected_profile === "default") + event_registry.fire("select-profile", { profile_id: event.new_profile_id }); + else if(selected_profile === event.old_profile_id) + event_registry.fire("select-profile", { profile_id: "default" }); + }); + + event_registry.on("select-profile", event => { + selected_profile = event.profile_id; + event_registry.fire("query-profile", { profile_id: event.profile_id }); + }); + + event_registry.on("reload-profile", event => { + event_registry.fire("query-profile-list"); + event_registry.fire("select-profile", event.profile_id || selected_profile); + }); + } + + event_registry.fire("query-profile-list"); + event_registry.fire("select-profile", { profile_id: "default" }); + event_registry.fire("select-identity-type", { profile_id: "default", identity_type: undefined }); + } + + export function initialize_audio_microphone_controller(event_registry: Registry) { + /* level meters */ + { + const level_meters: {[key: string]:Promise} = {}; + const level_info: {[key: string]:any} = {}; + let level_update_task; + + const destroy_meters = () => { + Object.keys(level_meters).forEach(e => { + const meter = level_meters[e]; + delete level_meters[e]; + + meter.then(e => e.destory()); + }); + Object.keys(level_info).forEach(e => delete level_info[e]); + }; + + const update_level_meter = () => { + destroy_meters(); + + for(const device of arecorder.devices()) { + let promise = arecorder.create_levelmeter(device).then(meter => { + meter.set_observer(level => { + if(level_meters[device.unique_id] !== promise) return; /* old level meter */ + + level_info[device.unique_id] = { + device_id: device.unique_id, + status: "success", + level: level + }; + }); + return Promise.resolve(meter); + }).catch(error => { + if(level_meters[device.unique_id] !== promise) return; /* old level meter */ + level_info[device.unique_id] = { + device_id: device.unique_id, + status: "error", + + error: error + }; + + log.warn(LogCategory.AUDIO, tr("Failed to initialize a level meter for device %s (%s): %o"), device.unique_id, device.driver + ":" + device.name, error); + return Promise.reject(error); + }); + level_meters[device.unique_id] = promise; + } + }; + + level_update_task = setInterval(() => { + event_registry.fire("update-device-level", { + devices: Object.keys(level_info).map(e => level_info[e]) + }); + }, 50); + + event_registry.on("query-device-result", event => { + if(event.status !== "success") return; + + update_level_meter(); + }); + + event_registry.on("deinitialize", event => { + destroy_meters(); + clearInterval(level_update_task); + }); + } + + /* device list */ + { + event_registry.on("query-devices", event => { + Promise.resolve().then(() => { + return arecorder.device_refresh_available() && event.refresh_list ? arecorder.refresh_devices() : Promise.resolve(); + }).catch(error => { + log.warn(LogCategory.AUDIO, tr("Failed to refresh device list: %o"), error); + return Promise.resolve(); + }).then(() => { + const devices = arecorder.devices(); + + event_registry.fire_async("query-device-result", { + status: "success", + active_device: default_recorder.current_device() ? default_recorder.current_device().unique_id : "none", + devices: devices.map(e => { return { id: e.unique_id, name: e.name, driver: e.driver }}) + }); + }); + }); + + event_registry.on("set-device", event => { + const device = arecorder.devices().find(e => e.unique_id === event.device_id); + if(!device && event.device_id !== "none") { + event_registry.fire_async("set-device-result", { status: "error", error: tr("Invalid device id"), device_id: event.device_id }); + return; + } + + default_recorder.set_device(device).then(() => { + console.debug(tr("Changed default microphone device")); + event_registry.fire_async("set-device-result", { status: "success", device_id: event.device_id }); + }).catch((error) => { + log.warn(LogCategory.AUDIO, tr("Failed to change microphone to device %s: %o"), device ? device.unique_id : "none", error) + event_registry.fire_async("set-device-result", { status: "success", device_id: event.device_id }); + }); + }); + } + + /* settings */ + { + event_registry.on("query-settings", event => { + event_registry.fire_async("query-settings-result", { + status: "success", + info: { + volume: default_recorder.get_volume(), + vad_type: default_recorder.get_vad_type(), + vad_ppt: { + key: default_recorder.get_vad_ppt_key(), + release_delay: Math.abs(default_recorder.get_vad_ppt_delay()), + release_delay_active: default_recorder.get_vad_ppt_delay() >= 0 + }, + vad_threshold: { + threshold: default_recorder.get_vad_threshold() + } + } + }); + }); + + event_registry.on("set-setting", event => { + const ensure_type = (type: "object" | "string" | "boolean" | "number" | "undefined") => { + if(typeof event.value !== type) { + event_registry.fire_async("set-setting-result", { status: "error", error: tr("Invalid value type for key") + " (expected: " + type + ", received: " + typeof event.value + ")", setting: event.setting }); + return false; + } + return true; + }; + + switch (event.setting) { + case "volume": + if(!ensure_type("number")) return; + default_recorder.set_volume(event.value); + break; + + case "threshold-threshold": + if(!ensure_type("number")) return; + default_recorder.set_vad_threshold(event.value); + break; + + case "vad-type": + if(!ensure_type("string")) return; + if(!default_recorder.set_vad_type(event.value)) { + event_registry.fire_async("set-setting-result", { status: "error", error: tr("Unknown VAD type"), setting: event.setting }); + return; + } + break; + + case "ppt-key": + if(!ensure_type("object")) return; + default_recorder.set_vad_ppt_key(event.value); + break; + + case "ppt-release-delay": + if(!ensure_type("number")) return; + const sign = default_recorder.get_vad_ppt_delay() >= 0 ? 1 : -1; + default_recorder.set_vad_ppt_delay(sign * event.value); + break; + + case "ppt-release-delay-active": + if(!ensure_type("boolean")) return; + default_recorder.set_vad_ppt_delay(Math.abs(default_recorder.get_vad_ppt_delay()) * (event.value ? 1 : -1)); + break; + + default: + event_registry.fire_async("set-setting-result", { status: "error", error: tr("Invalid setting key"), setting: event.setting }); + return; + } + event_registry.fire_async("set-setting-result", { status: "success", setting: event.setting, value: event.value }); + }); + } + + aplayer.on_ready(() => event_registry.fire_async("audio-initialized", {})); + } + export function initialize_audio_microphone_view(container: JQuery, event_registry: Registry) { + /* device list */ + { + /* actual list */ + { + const container_devices = container.find(".container-devices"); + const volume_bar_tags: {[key: string]:{ volume: JQuery, error: JQuery }} = {}; + let pending_changes = 0; + let default_device_id; + + const build_device = (device: { id: string, name: string, driver: string }, selected: boolean) => { + let tag_volume: JQuery, tag_volume_error: JQuery; + const tag = $.spawn("div").attr("device-id", device ? device.id : "none").addClass("device").toggleClass("selected", selected).append( $.spawn("div").addClass("container-selected").append( - $.spawn("div").addClass("icon_em client-apply") + $.spawn("div").addClass("icon_em client-apply"), + $.spawn("div").addClass("icon-loading").append( + $.spawn("img").attr("src", "img/icon_settings_loading.svg") + ) ), $.spawn("div").addClass("container-name").append( $.spawn("div").addClass("device-driver").text( @@ -444,2015 +1884,609 @@ namespace Modals { ), $.spawn("div").addClass("device-name").text( device ? (device.name || "Unknown name") : "No device" + ), + ), + $.spawn("div").addClass("container-activity").append( + $.spawn("div").addClass("container-activity-bar").append( + tag_volume = $.spawn("div").addClass("bar-hider"), + tag_volume_error = $.spawn("div").addClass("bar-error") ) ) ); + tag_volume.css('width', '100%'); /* initially hide the bar */ + if(device) + volume_bar_tags[device.id] = { volume: tag_volume, error: tag_volume_error }; tag.on('click', event => { - if(tag.hasClass("selected")) - return; + if(tag.hasClass("selected") || pending_changes > 0) return; - const _old = container_devices.find(".selected"); - _old.removeClass("selected"); - tag.addClass("selected"); - - audio.player.set_device(device ? device.device_id : null).then(() => { - console.debug(tr("Changed default speaker device")); - }).catch((error) => { - _old.addClass("selected"); - tag.removeClass("selected"); - - console.error(tr("Failed to change speaker to device %o: %o"), device, error); - createErrorModal(tr("Failed to change speaker"), MessageHelper.formatMessage(tr("Failed to change the speaker device to the target speaker{:br:}{}"), error)).open(); - }); + event_registry.fire("set-device", { device_id: device ? device.id : "none" }); }); return tag; }; - generate_device(undefined).appendTo(container_devices); - audio.player.available_devices().then(result => { - contianer_error.text("").hide(); - result.forEach(e => generate_device(e).appendTo(container_devices)); - }).catch(error => { - if(typeof(error) === "string") - contianer_error.text(error).show(); - - console.log(tr("Failed to query available speaker devices: %o"), error); - contianer_error.text(tr("Errors occurred (View console)")).show(); - }); - }; - update_devices(); - - const button_update = container.find(".button-update"); - button_update.on('click', async event => { - button_update.prop("disabled", true); - try { - update_devices(); - } catch(error) { - console.error(tr("Failed to build new speaker device list: %o"), error); - } - button_update.prop("disabled", false); - }); - } - - /* slider */ - { - - { - const container_master = container.find(".container-volume-master"); - const slider = container_master.find(".container-slider"); - sliderfy(slider, { - min_value: 0, - max_value: 100, - step: 1, - initial_value: settings.static_global(Settings.KEY_SOUND_MASTER, 100), - value_field: [container_master.find(".container-value")] - }); - slider.on('change', event => { - const volume = parseInt(slider.attr('value')); - - if(audio.player.set_master_volume) - audio.player.set_master_volume(volume / 100); - settings.changeGlobal(Settings.KEY_SOUND_MASTER, volume); - }); - } - - { - const container_soundpack = container.find(".container-volume-soundpack"); - const slider = container_soundpack.find(".container-slider"); - sliderfy(slider, { - min_value: 0, - max_value: 100, - step: 1, - initial_value: settings.static_global(Settings.KEY_SOUND_MASTER_SOUNDS, 100), - value_field: [container_soundpack.find(".container-value")] - }); - slider.on('change', event => { - const volume = parseInt(slider.attr('value')); - sound.set_master_volume(volume / 100); - settings.changeGlobal(Settings.KEY_SOUND_MASTER_SOUNDS, volume); - }); - } - } - - /* button test sound */ - { - container.find(".button-test-sound").on('click', event => { - sound.manager.play(Sound.SOUND_TEST, { - default_volume: 1, - ignore_muted: true, - ignore_overlap: true - }) - }); - } - } - - function settings_audio_sounds(contianer: JQuery, modal: Modal) { - /* initialize sound list */ - { - const container_sounds = contianer.find(".container-sounds"); - - const generate_sound = (_sound: Sound) => { - let tag_play_pause: JQuery, tag_play: JQuery, tag_pause: JQuery, tag_input_muted: JQuery; - let tag = $.spawn("div").addClass("sound").append( - tag_play_pause = $.spawn("div").addClass("container-button-play_pause").append( - tag_play = $.spawn("img").attr("src", "img/icon_sound_play.svg"), - tag_pause = $.spawn("img").attr("src", "img/icon_sound_pause.svg") - ), - $.spawn("div").addClass("container-name").text(_sound), - $.spawn("label").addClass("container-button-toggle").append( - $.spawn("div").addClass("switch").append( - tag_input_muted = $.spawn("input").attr("type", "checkbox"), - $.spawn("span").addClass("slider").append( - $.spawn("div").addClass("dot") - ) - ) - ) - ); - - tag_play_pause.on('click', event => { - if(tag_pause.is(":visible")) - return; - tag_play.hide(); - tag_pause.show(); - - const _done = flag => { - tag_pause.hide(); - tag_play.show(); - }; - const _timeout = setTimeout(() => _done(false), 10 * 1000); /* the sounds are not longer than 10 seconds */ - - sound.manager.play(_sound, { - ignore_overlap: true, - ignore_muted: true, - default_volume: 1, - - callback: flag => { - clearTimeout(_timeout); - _done(flag); - } - }); - }); - tag_pause.hide(); - - tag_input_muted.prop("checked", sound.get_sound_volume(_sound, 1) > 0); - tag_input_muted.on('change', event => { - const volume = tag_input_muted.prop("checked") ? 1 : 0; - sound.set_sound_volume(_sound, volume); - console.log(tr("Changed sound volume to %o for sound %o"), volume, _sound); - }); - - return tag; - }; - - //container-sounds - for(const sound_key in Sound) - generate_sound(Sound[sound_key as any] as any).appendTo(container_sounds); - - /* the filter */ - const input_filter = contianer.find(".input-sounds-filter"); - input_filter.on('change keyup', event => { - const filter = input_filter.val() as string; - - container_sounds.find(".sound").each((_, _element) => { - const element = $(_element); - element.toggle(filter.length == 0 || element.text().toLowerCase().indexOf(filter) !== -1); - }) - }); - } - - const overlap_tag = contianer.find(".option-overlap-same"); - overlap_tag.on('change', event => { - const activated = (event.target).checked; - sound.set_overlap_activated(activated); - }).prop("checked", sound.overlap_activated()); - - const mute_tag = contianer.find(".option-mute-output"); - mute_tag.on('change', event => { - const activated = (event.target).checked; - sound.set_ignore_output_muted(!activated); - }).prop("checked", !sound.ignore_output_muted()); - - modal.close_listener.push(sound.save); - } - - export namespace modal_settings { - export interface ProfileViewSettings { - forum_setuppable: boolean - } - export function initialize_identity_profiles_controller(event_registry: events.Registry) { - const send_error = (event, profile, text) => event_registry.fire_async(event, { status: "error", profile_id: profile, error: text }); - event_registry.on("create-profile", event => { - const profile = profiles.create_new_profile(event.name); - profiles.mark_need_save(); - event_registry.fire_async("create-profile-result", { - status: "success", - name: event.name, - profile_id: profile.id - }); - }); - - event_registry.on("delete-profile", event => { - const profile = profiles.find_profile(event.profile_id); - if(!profile) { - log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); - send_error("delete-profile-result", event.profile_id, tr("Unknown profile")); - return; - } - - profiles.delete_profile(profile); - event_registry.fire_async("delete-profile-result", { status: "success", profile_id: event.profile_id }); - }); - - const build_profile_info = (profile: profiles.ConnectionProfile) => { - const forum_data = profile.selected_identity(profiles.identities.IdentitifyType.TEAFORO) as profiles.identities.TeaForumIdentity; - const teamspeak_data = profile.selected_identity(profiles.identities.IdentitifyType.TEAMSPEAK) as profiles.identities.TeaSpeakIdentity; - const nickname_data = profile.selected_identity(profiles.identities.IdentitifyType.NICKNAME) as profiles.identities.NameIdentity; - - return { - id: profile.id, - name: profile.profile_name, - nickname: profile.default_username, - identity_type: profile.selected_identity_type as any, - identity_forum: !forum_data ? undefined : { - valid: forum_data.valid(), - fallback_name: forum_data.fallback_name() - }, - identity_nickname: !nickname_data ? undefined : { - name: nickname_data.name(), - fallback_name: nickname_data.fallback_name() - }, - identity_teamspeak: !teamspeak_data ? undefined : { - unique_id: teamspeak_data.uid(), - fallback_name: teamspeak_data.fallback_name() - } - } - }; - event_registry.on("query-profile-list", event => { - event_registry.fire_async("query-profile-list-result", { status: "success", profiles: profiles.profiles().map(e => build_profile_info(e)) }); - }); - - event_registry.on("query-profile", event => { - const profile = profiles.find_profile(event.profile_id); - if(!profile) { - log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); - send_error("query-profile-result", event.profile_id, tr("Unknown profile")); - return; - } - - event_registry.fire_async("query-profile-result", { status: "success", profile_id: event.profile_id, info: build_profile_info(profile)}); - }); - - event_registry.on("set-default-profile", event => { - const profile = profiles.find_profile(event.profile_id); - if(!profile) { - log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); - send_error("set-default-profile-result", event.profile_id, tr("Unknown profile")); - return; - } - - const old = profiles.set_default_profile(profile); - event_registry.fire_async("set-default-profile-result", { status: "success", old_profile_id: event.profile_id, new_profile_id: old.id }); - }); - - event_registry.on("set-profile-name", event => { - const profile = profiles.find_profile(event.profile_id); - if(!profile) { - log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); - send_error("set-profile-name-result", event.profile_id, tr("Unknown profile")); - return; - } - - profile.profile_name = event.name; - profiles.mark_need_save(); - event_registry.fire_async("set-profile-name-result", { name: event.name, profile_id: event.profile_id, status: "success" }); - }); - - event_registry.on("set-default-name", event => { - const profile = profiles.find_profile(event.profile_id); - if(!profile) { - log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); - send_error("set-default-name-result", event.profile_id, tr("Unknown profile")); - return; - } - - profile.default_username = event.name; - profiles.mark_need_save(); - event_registry.fire_async("set-default-name-result", { name: event.name, profile_id: event.profile_id, status: "success" }); - }); - - event_registry.on("set-identity-name-name", event => { - const profile = profiles.find_profile(event.profile_id); - if(!profile) { - log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); - send_error("set-identity-name-name-result", event.profile_id, tr("Unknown profile")); - return; - } - - let identity = profile.selected_identity(profiles.identities.IdentitifyType.NICKNAME) as profiles.identities.NameIdentity; - if(!identity) - profile.set_identity(profiles.identities.IdentitifyType.NICKNAME, identity = new profiles.identities.NameIdentity()); - identity.set_name(event.name); - profiles.mark_need_save(); - - event_registry.fire_async("set-identity-name-name-result", { name: event.name, profile_id: event.profile_id, status: "success" }); - }); - - event_registry.on("query-profile-validity", event => { - const profile = profiles.find_profile(event.profile_id); - if(!profile) { - log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); - send_error("query-profile-validity-result", event.profile_id, tr("Unknown profile")); - return; - } - - event_registry.fire_async("query-profile-validity-result", { status: "success", profile_id: event.profile_id, valid: profile.valid() }); - }); - - event_registry.on("query-identity-teamspeak", event => { - const profile = profiles.find_profile(event.profile_id); - if(!profile) { - log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); - send_error("query-identity-teamspeak-result", event.profile_id, tr("Unknown profile")); - return; - } - - const ts = profile.selected_identity(profiles.identities.IdentitifyType.TEAMSPEAK) as profiles.identities.TeaSpeakIdentity; - if(!ts) { - event_registry.fire_async("query-identity-teamspeak-result", { status: "error", profile_id: event.profile_id, error: tr("Missing identity") }); - return; - } - - ts.level().then(level => { - event_registry.fire_async("query-identity-teamspeak-result", { status: "success", level: level, profile_id: event.profile_id }); - }).catch(error => { - send_error("query-identity-teamspeak-result", event.profile_id, tr("failed to calculate level")); - }) - }); - - event_registry.on("select-identity-type", event => { - const profile = profiles.find_profile(event.profile_id); - if(!profile) { - log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); - return; - } - - profile.selected_identity_type = event.identity_type; - profiles.mark_need_save(); - }); - - event_registry.on("generate-identity-teamspeak", event => { - const profile = profiles.find_profile(event.profile_id); - if(!profile) { - log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); - send_error("generate-identity-teamspeak-result", event.profile_id, tr("Unknown profile")); - return; - } - - profiles.identities.TeaSpeakIdentity.generate_new().then(identity => { - profile.set_identity(profiles.identities.IdentitifyType.TEAMSPEAK, identity); - profiles.mark_need_save(); - - identity.level().then(level => { - event_registry.fire_async("generate-identity-teamspeak-result", { - status: "success", - profile_id: event.profile_id, - unique_id: identity.uid(), - level: level - }); - }).catch(error => { - console.error(tr("Failed to calculate level for a new identity. Error object: %o"), error); - send_error("generate-identity-teamspeak-result", event.profile_id, tr("failed to calculate level: ") + error); - }) - }).catch(error => { - console.error(tr("Failed to generate a new identity. Error object: %o"), error); - send_error("generate-identity-teamspeak-result", event.profile_id, tr("failed to generate identity: ") + error); - }); - }); - - event_registry.on("import-identity-teamspeak", event => { - const profile = profiles.find_profile(event.profile_id); - if(!profile) { - log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); - return; - } - - spawnTeamSpeakIdentityImport(identity => { - profile.set_identity(profiles.identities.IdentitifyType.TEAMSPEAK, identity); - profiles.mark_need_save(); - - identity.level().catch(error => { - console.error(tr("Failed to calculate level for a new imported identity. Error object: %o"), error); - return Promise.resolve(undefined); - }).then(level => { - event_registry.fire_async("import-identity-teamspeak-result", { - profile_id: event.profile_id, - unique_id: identity.uid(), - level: level - }); - }); - }); - }); - - event_registry.on("improve-identity-teamspeak-level", event => { - const profile = profiles.find_profile(event.profile_id); - if(!profile) { - log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); - return; - } - - const identity = profile.selected_identity(profiles.identities.IdentitifyType.TEAMSPEAK) as profiles.identities.TeaSpeakIdentity; - if (!identity) return; - - Modals.spawnTeamSpeakIdentityImprove(identity, profile.profile_name).close_listener.push(() => { - profiles.mark_need_save(); - - identity.level().then(level => { - event_registry.fire_async("improve-identity-teamspeak-level-update", { profile_id: event.profile_id, new_level: level }); - }).catch(error => { - log.error(LogCategory.CLIENT, tr("Failed to calculate identity level after improvement (%o)"), error); - }); - }); - }); - - event_registry.on("export-identity-teamspeak", event => { - const profile = profiles.find_profile(event.profile_id); - if(!profile) { - log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); - return; - } - - const identity = profile.selected_identity(profiles.identities.IdentitifyType.TEAMSPEAK) as profiles.identities.TeaSpeakIdentity; - if (!identity) return; - - identity.export_ts(true).then(data => { - const element = $.spawn("a") - .text("donwload") - .attr("href", "data:test/plain;charset=utf-8," + encodeURIComponent(data)) - .attr("download", name + ".ini") - .css("display", "none") - .appendTo($("body")); - element[0].click(); - element.remove(); - }).catch(error => { - console.error(error); - createErrorModal(tr("Failed to export identity"), tr("Failed to export and save identity.
Error: ") + error).open(); - }); - }); - } - export function initialize_identity_profiles_view(container: JQuery, event_registry: events.Registry, settings: ProfileViewSettings) { - /* profile list */ - { - const container_profiles = container.find(".container-profiles"); - let selected_profile; - - const overlay_error = container_profiles.find(".overlay-error"); - const overlay_timeout = container_profiles.find(".overlay-timeout"); - const overlay_empty = container_profiles.find(".overlay-empty"); - - const build_profile = (profile: events.modal.settings.ProfileInfo, selected: boolean) => { - let tag_avatar: JQuery, tag_default: JQuery; - let tag = $.spawn("div").addClass("profile").attr("profile-id", profile.id).append( - tag_avatar = $.spawn("div").addClass("container-avatar"), - $.spawn("div").addClass("container-info").append( - $.spawn("div").addClass("container-type").append( - $.spawn("div").addClass("identity-type").text(profile.identity_type || tr("Type unset")), - tag_default = $.spawn("div").addClass("tag-default").text(tr("(Default)")), - $.spawn("div").addClass("icon_em icon-status").hide() - ), - $.spawn("div").addClass("profile-name").text(profile.name || tr("Unnamed")) - ) - ); - tag_avatar.hide(); /* no avatars yet */ - - tag.on('click', event => event_registry.fire("select-profile", { profile_id: profile.id })); - tag.toggleClass("selected", selected); - tag_default.toggle(profile.id === "default"); - - event_registry.fire("query-profile-validity", { profile_id: profile.id }); - return tag; - }; - - event_registry.on("select-profile", event => { - container_profiles.find(".profile").removeClass("selected"); - container_profiles.find(".profile[profile-id='" + event.profile_id + "']").addClass("selected"); - selected_profile = event.profile_id; - }); - - - event_registry.on("query-profile-list", event => { - container_profiles.find(".profile").remove(); - }); - - event_registry.on("query-profile-list-result", event => { - container_profiles.find(".overlay").hide(); - if(event.status === "error") { - overlay_error.show().find(".error").text(event.error || tr("unknown error")); - return; - } else if(event.status === "timeout") { - overlay_timeout.show(); - return; - } - if(!event.profiles.length) { - overlay_empty.show(); - return; - } - - container_profiles.find(".overlay").hide(); - container_profiles.find(".profile").remove(); - event.profiles.forEach(e => build_profile(e, e.id == selected_profile).appendTo(container_profiles)); - }); - - event_registry.on("delete-profile-result", event => { - if(event.status !== "success") return; - - //TODO: Animate removal? - container_profiles.find(".profile[profile-id='" + event.profile_id + "']").remove(); - }); - - event_registry.on('create-profile-result', event => { - if(event.status !== "success") return; - - event_registry.fire("query-profile-list"); - event_registry.one("query-profile-list-result", e => event_registry.fire("select-profile", { profile_id: event.profile_id })); - }); - - event_registry.on("set-profile-name-result", event => { - if(event.status !== "success") return; - - const profile = container_profiles.find(".profile[profile-id='" + event.profile_id + "']"); - profile.find(".profile-name").text(event.name || tr("Unnamed")); - }); - - event_registry.on("set-default-profile-result", event => { - if(event.status !== "success") return; - - const old_profile = container_profiles.find(".profile[profile-id='default']"); - const new_profile = container_profiles.find(".profile[profile-id='" + event.old_profile_id + "']"); - old_profile.attr("profile-id", event.new_profile_id).find(".tag-default").hide(); - new_profile.attr("profile-id", "default").find(".tag-default").show(); - }); - - event_registry.on("select-identity-type", event => { - if(!event.identity_type) return; - - const profile = container_profiles.find(".profile[profile-id='" + event.profile_id + "']"); - profile.find(".identity-type").text(event.identity_type.toUpperCase() || tr("Type unset")); - }); - - event_registry.on("query-profile-validity-result", event => { - const profile = container_profiles.find(".profile[profile-id='" + event.profile_id + "']"); - profile.find(".icon-status") - .show() - .toggleClass("client-apply", event.status === "success" && event.valid) - .toggleClass("client-delete", event.status !== "success" || !event.valid) - .attr("title", event.status === "success" ? event.valid ? tr("Profile is valid") : tr("Provile is invalid") : event.error || tr("failed to query status")); - }); - - /* status indicator updaters */ - event_registry.on("select-identity-type", event => { - if(!event.profile_id) return; - - /* we need a short delay so everything could apply*/ - setTimeout(() => { - event_registry.fire("query-profile-validity", { profile_id: event.profile_id }); - }, 100); - }); - event_registry.on(["set-default-name-result", "set-profile-name-result", "set-identity-name-name-result", "generate-identity-teamspeak-result"], event => { - if(!('status' in event) ||!('profile_id' in event)) { - log.warn(LogCategory.CLIENT, tr("Profile status watcher encountered an unuseal event!")); - return; - } - if((event as any).status !== "success") return; - event_registry.fire("query-profile-validity", { profile_id: (event as any).profile_id }); - }) - } - - /* list buttons */ - { - /* reload */ - { - const button = container.find(".button-reload-list"); - - button.on('click', event => event_registry.fire("query-profile-list")); - - event_registry.on("query-profile-list", event => button.prop("disabled", true)); - event_registry.on("query-profile-list-result", event => button.prop("disabled", false)); - } - - /* set default */ - { - const button = container.find(".button-set-default"); - let current_profile; - - button.on('click', event => event_registry.fire("set-default-profile", { profile_id: current_profile })); - event_registry.on("select-profile", event => { - current_profile = event.profile_id; - button.prop("disabled", !event.profile_id || event.profile_id === "default"); - }); - - event_registry.on("set-default-profile-result", event => { - if(event.status === "success") return; - - createErrorModal(tr("Failed to set default profile"), tr("Failed to set default profile:") + "
" + (event.status === "timeout" ? tr("request timeout") : (event.error || tr("unknown error")))).open(); - }); - button.prop("disabled", true); - } - - /* delete button */ - { - const button = container.find(".button-delete"); - let current_profile; - - button.on('click', event => { - if(!current_profile || current_profile === "default") return; - - spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this profile?"), result => { - if (result) - event_registry.fire("delete-profile", { profile_id: current_profile }); - }); - }); - - event_registry.on("delete-profile-result", event => { - if(event.status === "success") return; - - createErrorModal(tr("Failed to delete profile"), tr("Failed to delete profile:") + "
" + (event.status === "timeout" ? tr("request timeout") : (event.error || tr("unknown error")))).open(); - }); - - event_registry.on("select-profile", event => { - current_profile = event.profile_id; - - button.prop("disabled", !event.profile_id || event.profile_id === "default"); - }); - } - - /* create button */ - { - const button = container.find(".button-create"); - button.on('click', event => { - createInputModal(tr("Please enter a name"), tr("Please enter a name for the new profile:"), text => text.length >= 3 && !profiles.find_profile_by_name(text), value => { - if (value) - event_registry.fire("create-profile", { name: value as string }); - }).open(); - }); - - event_registry.on('create-profile', event => button.prop("disabled", true)); - event_registry.on("create-profile-result", event => { - button.prop("disabled", false); - if(event.status === "success") { - event_registry.fire("select-profile", { profile_id: event.profile_id }); - return; - } - - createErrorModal(tr("Failed to create profile"), tr("Failed to create new profile:") + "
" + (event.status === "timeout" ? tr("request timeout") : (event.error || tr("unknown error")))).open(); - }) - } - } - - - /* profile info */ - { - let current_profile; - const error_text = event => event.status === "timeout" ? tr("request timeout") : (event.error || tr("unknown error")); - - /* general info */ - { - /* profile name */ - { - const input = container.find(".profile-name"); - let last_name; - - const update_name = () => input.prop("disabled", false) - .val(last_name) - .attr("placeholder", tr("Profile name")) - .parent().removeClass("is-invalid"); - - const info_name = text => input.prop("disabled", true) - .val(null) - .attr("placeholder", text) - .parent().removeClass("is-invalid"); - - event_registry.on("query-profile", event => { - if(event.profile_id !== current_profile) return; - - info_name(tr("loading")); - }); - - event_registry.on("query-profile-result", event => { - if(event.profile_id !== current_profile) return; - - if(event.status === "success") { - last_name = event.info.name; - update_name(); - } else { - info_name(error_text(event)); - } - }); - - event_registry.on("set-profile-name", event => { - if(event.profile_id !== current_profile) return; - - info_name(tr("saving")); - }); - - event_registry.on("set-profile-name-result", event => { - if(event.status !== "success") { - createErrorModal(tr("Failed to change profile name"), tr("Failed to create apply new name:") + "
" + error_text(event)).open(); - } else { - last_name = event.name; - } - update_name(); - }); - - input.on('keyup', event => { - const text = input.val() as string; - const profile = profiles.find_profile_by_name(text); - input.parent().toggleClass("is-invalid", text.length < 3 || (profile && profile.id != current_profile)); - }).on('change', event => { - const text = input.val() as string; - const profile = profiles.find_profile_by_name(text); - if(text.length < 3 || (profile && profile.id != current_profile)) return; - - event_registry.fire("set-profile-name", { profile_id: current_profile, name: text }); - }); - } - - /* nickname name */ - { - const input = container.find(".profile-default-name"); - let last_name = null, fallback_names = {}, current_identity_type = ""; - - const update_name = () => input.prop("disabled", false) - .val(last_name) - .attr("placeholder", fallback_names[current_identity_type] || tr("Another TeaSpeak user")) - .parent().removeClass("is-invalid"); - - const info_name = text => input.prop("disabled", true) - .val(null) - .attr("placeholder", text) - .parent().removeClass("is-invalid"); - - event_registry.on("query-profile", event => { - if(event.profile_id !== current_profile) return; - - input.prop("disabled", true).val(null).attr("placeholder", tr("loading")); - }); - - event_registry.on("query-profile-result", event => { - if(event.profile_id !== current_profile) return; - if(event.status === "success") { - current_identity_type = event.info.identity_type; - fallback_names["nickname"] = event.info.identity_nickname ? event.info.identity_nickname.fallback_name : undefined; - fallback_names["teaforo"] = event.info.identity_forum ? event.info.identity_forum.fallback_name : undefined; - fallback_names["teamspeak"] = event.info.identity_teamspeak ? event.info.identity_teamspeak.fallback_name : undefined; - - last_name = event.info.nickname; - update_name(); - } else { - info_name(error_text(event)); - } - }); - - event_registry.on("select-identity-type", event => { - if (current_identity_type === event.identity_type) return; - - current_identity_type = event.identity_type; - update_name(); - }); - - event_registry.on("set-default-name", event => { - if(event.profile_id !== current_profile) return; - - info_name(tr("saving")); - }); - - event_registry.on("set-default-name-result", event => { - if(event.status !== "success") { - createErrorModal(tr("Failed to change nickname"), tr("Failed to create apply new nickname:") + "
" + error_text(event)).open(); - } else { - last_name = event.name; - } - update_name(); - }); - - input.on('keyup', event => { - const text = input.val() as string; - input.parent().toggleClass("is-invalid", text.length != 0 && text.length < 3); - }).on('change', event => { - const text = input.val() as string; - if(text.length != 0 && text.length < 3) return; - - event_registry.fire("set-default-name", { profile_id: current_profile, name: text }); - }); - } - - /* identity type */ - { - const select_identity_type = container.find(".profile-identity-type"); - - const show_message = (text, is_invalid) => select_identity_type - .toggleClass("is-invalid", is_invalid) - .prop("disabled", true) - .find("option[value=error]") - .text(text) - .prop("selected", true); - - const set_type = type => select_identity_type - .toggleClass("is-invalid", type === "unset") - .prop("disabled", false) - .find("option[value=" + type + "]") - .prop("selected", true); - - event_registry.on("query-profile", event => show_message(tr("loading"), false)); - - event_registry.on("select-identity-type", event => { - if(event.profile_id !== current_profile) return; - - set_type(event.identity_type || "unset"); - }); - - event_registry.on("query-profile-result", event => { - if(event.profile_id !== current_profile) return; - - if(event.status === "success") - event_registry.fire("select-identity-type", { profile_id: event.profile_id, identity_type: event.info.identity_type }); - else - show_message(error_text(event), false); - }); - - select_identity_type.on('change', event => { - const type = (select_identity_type.val() as string).toLowerCase(); - if(type === "error" || type == "unset") return; - - event_registry.fire("select-identity-type", { profile_id: current_profile, identity_type: type as any }); - }); - } - - /* avatar */ - { - container.find(".button-change-avatar").hide(); - } - } - - /* special info TeamSpeak */ - { - const container_settings = container.find(".container-teamspeak"); - const container_valid = container_settings.find(".container-valid"); - const container_invalid = container_settings.find(".container-invalid"); - - const input_current_level = container_settings.find(".current-level"); - const input_unique_id = container_settings.find(".unique-id"); - - const button_new = container_settings.find(".button-new"); - const button_improve = container_settings.find(".button-improve"); - - const button_import = container_settings.find(".button-import"); - const button_export = container_settings.find(".button-export"); - - let is_profile_generated = false; - - event_registry.on("select-identity-type", event => { - if(event.profile_id !== current_profile) return; - - container_settings.toggle(event.identity_type === "teamspeak"); - }); - - event_registry.on("query-profile", event => { - input_unique_id.val(null).attr("placeholder", tr("loading")); - input_current_level.val(null).attr("placeholder", tr("loading")); - - button_new.prop("disabled", true); - button_improve.prop("disabled", true); - button_import.prop("disabled", true); - button_export.prop("disabled", true); - }); - - const update_identity = (state: "not-created" | "created", unique_id?: string, level?: number) => { - if(state === "not-created") { - container_invalid.show(); - container_valid.hide(); - - button_improve.prop("disabled", true); - button_export.prop("disabled", true); - } else { - container_invalid.hide(); - container_valid.show(); - - input_unique_id.val(unique_id).attr("placeholder", null); - if(typeof level !== "number") - event_registry.fire("query-identity-teamspeak", { profile_id: current_profile }); - else - input_current_level.val(level).attr("placeholder", null); - - button_improve.prop("disabled", false); - button_export.prop("disabled", false); - } - - is_profile_generated = state === "created"; - button_new.toggleClass("btn-blue", !is_profile_generated).toggleClass("btn-red", is_profile_generated); - button_import.toggleClass("btn-blue", !is_profile_generated).toggleClass("btn-red", is_profile_generated); - - button_new.prop("disabled", false); - button_import.prop("disabled", false); - }; - - event_registry.on("query-profile-result", event => { - if(event.profile_id !== current_profile) return; - - if(event.status !== "success") { - input_unique_id.val(null).attr("placeholder", error_text(event)); - return; - } - - if(!event.info.identity_teamspeak) - update_identity("not-created"); - else - update_identity("created", event.info.identity_teamspeak.unique_id); - }); - - event_registry.on("query-identity-teamspeak-result", event => { - if(event.profile_id !== current_profile) return; - - if(event.status === "success") { - input_current_level.val(event.level).attr("placeholder", null); - } else { - input_current_level.val(null).attr("placeholder", error_text(event)); - } - }); - - /* the new button */ - { - button_new.on('click', event => { - if(is_profile_generated) { - spawnYesNo(tr("Are you sure"), tr("Do you really want to generate a new identity and override the old identity?"), result => { - if (result) event_registry.fire("generate-identity-teamspeak", { profile_id: current_profile }); - }); - } else { - event_registry.fire("generate-identity-teamspeak", { profile_id: current_profile }); - } - }); - - event_registry.on("generate-identity-teamspeak-result", event => { - if(event.profile_id !== current_profile) return; - - if(event.status !== "success") { - createErrorModal(tr("Failed to generate a new identity"), tr("Failed to create a new identity:") + "
" + error_text(event)).open(); - return; - } - - update_identity("created", event.unique_id, event.level); - createInfoModal(tr("Identity generated"), tr("A new identity had been successfully generated")).open(); - }); - } - - /* the import identity */ - { - button_import.on('click', event => { - if(is_profile_generated) { - spawnYesNo(tr("Are you sure"), tr("Do you really want to import a new identity and override the old identity?"), result => { - if (result) event_registry.fire("import-identity-teamspeak", { profile_id: current_profile }); - }); - } else { - event_registry.fire("import-identity-teamspeak", { profile_id: current_profile }); - } - }); - - event_registry.on("improve-identity-teamspeak-level-update", event => { - if(event.profile_id !== current_profile) return; - - input_current_level.val(event.new_level).attr("placeholder", null); - }); - - event_registry.on("import-identity-teamspeak-result", event => { - if(event.profile_id !== current_profile) return; - - event_registry.fire_async("query-profile", { profile_id: event.profile_id }); /* we do it like this so the default nickname changes as well */ - createInfoModal(tr("Identity imported"), tr("Your identity had been successfully imported generated")).open(); - }); - } - - /* identity export */ - { - button_export.on('click', event => { - createInputModal(tr("File name"), tr("Please enter the file name"), text => !!text, name => { - if (name) - event_registry.fire("export-identity-teamspeak", { profile_id: current_profile, filename: name as string }); - }).open(); - }); - } - - /* the improve button */ - button_improve.on('click', event => event_registry.fire("improve-identity-teamspeak-level", { profile_id: current_profile })); - } - - /* special info TeaSpeak - Forum */ - { - const container_settings = container.find(".container-teaforo"); - const container_valid = container_settings.find(".container-valid"); - const container_invalid = container_settings.find(".container-invalid"); - - const button_setup = container_settings.find(".button-setup"); - - event_registry.on("select-identity-type", event => { - if(event.profile_id !== current_profile) return; - - container_settings.toggle(event.identity_type === "teaforo"); - }); - - event_registry.on("query-profile", event => { - container_valid.toggle(false); - container_invalid.toggle(false); - }); - - event_registry.on("query-profile-result", event => { - if(event.profile_id !== current_profile) return; - - const valid = event.status === "success" && event.info.identity_forum && event.info.identity_forum.valid; - container_valid.toggle(!!valid); - container_invalid.toggle(!valid); - }); - - button_setup.on('click', event => event_registry.fire_async("setup-forum-connection")); - button_setup.toggle(settings.forum_setuppable); - } - - /* special info nickname */ - { - const container_settings = container.find(".container-nickname"); - const input_nickname = container_settings.find(".nickname"); - let last_name; - - const update_name = () => input_nickname.prop("disabled", false) - .val(last_name) - .attr("placeholder", tr("Identity base name")) - .parent().removeClass("is-invalid"); - - const show_info = text => input_nickname.prop("disabled", true) - .val(null) - .attr("placeholder", text) - .parent().removeClass("is-invalid"); - - event_registry.on("select-identity-type", event => event.profile_id === current_profile && container_settings.toggle(event.identity_type === "nickname")); - - event_registry.on("query-profile", event => { - if(event.profile_id !== current_profile) return; - - show_info(tr("loading")); - }); - - event_registry.on("query-profile-result", event => { - if(event.profile_id !== current_profile) return; - - if(event.status === "success") { - last_name = event.info.identity_nickname ? event.info.identity_nickname.name : null; - update_name(); - } else { - show_info(error_text(event)); - } - }); - - event_registry.on("set-identity-name-name", event => { - if(event.profile_id !== current_profile) return; - show_info(tr("saving")); - }); - - event_registry.on("set-identity-name-name-result", event => { - if(event.status !== "success") { - createErrorModal(tr("Failed to change name"), tr("Failed to create new name:") + "
" + error_text(event)).open(); - } else { - last_name = event.name; - } - update_name(); - }); - - input_nickname.on('keyup', event => { - const text = input_nickname.val() as string; - const profile = profiles.find_profile_by_name(text); - input_nickname.parent().toggleClass("is-invalid", text.length < 3 || (profile && profile.id != current_profile)); - }).on('change', event => { - const text = input_nickname.val() as string; - const profile = profiles.find_profile_by_name(text); - if(text.length < 3 || (profile && profile.id != current_profile)) return; - - event_registry.fire("set-identity-name-name", { profile_id: current_profile, name: text }); - }); - } - event_registry.on("select-profile", e => current_profile = e.profile_id); - } - - /* timeouts */ - { - /* profile list */ - { - let timeout; - event_registry.on("query-profile-list", event => timeout = setTimeout(() => event_registry.fire("query-profile-list-result", { status: "timeout" }), 5000)); - event_registry.on("query-profile-list-result", event => { - clearTimeout(timeout); - timeout = undefined; - }); - } - - /* profile create */ - { - const timeouts = {}; - event_registry.on("create-profile", event => { - clearTimeout(timeouts[event.name]); - timeouts[event.name] = setTimeout(() => { - event_registry.fire("create-profile-result", { name: event.name, status: "timeout" }); - }, 5000); - }); - - event_registry.on("create-profile-result", event => { - clearTimeout(timeouts[event.name]); - delete timeouts[event.name]; - }); - } - - /* profile set default create */ - { - const timeouts = {}; - event_registry.on("set-default-profile", event => { - clearTimeout(timeouts[event.profile_id]); - timeouts[event.profile_id] = setTimeout(() => { - event_registry.fire("set-default-profile-result", { old_profile_id: event.profile_id, status: "timeout" }); - }, 5000); - }); - - event_registry.on("set-default-profile-result", event => { - clearTimeout(timeouts[event.old_profile_id]); - delete timeouts[event.old_profile_id]; - }); - } - - const create_standard_timeout = (event: keyof events.modal.settings.profiles, response_event: keyof events.modal.settings.profiles, key: string) => { - const timeouts = {}; - event_registry.on(event, event => { - clearTimeout(timeouts[event[key]]); - timeouts[event[key]] = setTimeout(() => { - const timeout_event = { status: "timeout" }; - timeout_event[key] = event[key]; - event_registry.fire(response_event, timeout_event as any); - }, 5000); - }); - - event_registry.on(response_event, event => { - clearTimeout(timeouts[event[key]]); - delete timeouts[event[key]]; - }); - }; - - create_standard_timeout("query-profile", "query-profile-result", "profile_id"); - create_standard_timeout("query-identity-teamspeak", "query-identity-teamspeak-result", "profile_id"); - create_standard_timeout("delete-profile", "delete-profile-result", "profile_id"); - create_standard_timeout("set-profile-name", "set-profile-name-result", "profile_id"); - create_standard_timeout("set-default-name", "set-default-name-result", "profile_id"); - create_standard_timeout("query-profile-validity", "query-profile-validity-result", "profile_id"); - create_standard_timeout("set-identity-name-name", "set-identity-name-name-result", "profile_id"); - create_standard_timeout("generate-identity-teamspeak", "generate-identity-teamspeak-result", "profile_id"); - } - - /* some view semantics */ - { - let selected_profile; - event_registry.on("delete-profile-result", event => { - if(event.status !== "success") return; - if(event.profile_id !== selected_profile) return; - - /* the selected profile has been deleted, so we need to select another one */ - event_registry.fire("select-profile", { profile_id: "default" }); - }); - - /* reselect the default profile or the new default profile */ - event_registry.on("set-default-profile-result", event => { - if(event.status !== "success") return; - if(selected_profile === "default") - event_registry.fire("select-profile", { profile_id: event.new_profile_id }); - else if(selected_profile === event.old_profile_id) - event_registry.fire("select-profile", { profile_id: "default" }); - }); - - event_registry.on("select-profile", event => { - selected_profile = event.profile_id; - event_registry.fire("query-profile", { profile_id: event.profile_id }); - }); - - event_registry.on("reload-profile", event => { - event_registry.fire("query-profile-list"); - event_registry.fire("select-profile", event.profile_id || selected_profile); - }); - } - - event_registry.fire("query-profile-list"); - event_registry.fire("select-profile", { profile_id: "default" }); - event_registry.fire("select-identity-type", { profile_id: "default", identity_type: undefined }); - } - - export function initialize_audio_microphone_controller(event_registry: events.Registry) { - /* level meters */ - { - const level_meters: {[key: string]:Promise} = {}; - const level_info: {[key: string]:any} = {}; - let level_update_task; - - const destroy_meters = () => { - Object.keys(level_meters).forEach(e => { - const meter = level_meters[e]; - delete level_meters[e]; - - meter.then(e => e.destory()); - }); - Object.keys(level_info).forEach(e => delete level_info[e]); - }; - - const update_level_meter = () => { - destroy_meters(); - - for(const device of audio.recorder.devices()) { - let promise = audio.recorder.create_levelmeter(device).then(meter => { - meter.set_observer(level => { - if(level_meters[device.unique_id] !== promise) return; /* old level meter */ - - level_info[device.unique_id] = { - device_id: device.unique_id, - status: "success", - level: level - }; - }); - return Promise.resolve(meter); - }).catch(error => { - if(level_meters[device.unique_id] !== promise) return; /* old level meter */ - level_info[device.unique_id] = { - device_id: device.unique_id, - status: "error", - - error: error - }; - - log.warn(LogCategory.AUDIO, tr("Failed to initialize a level meter for device %s (%s): %o"), device.unique_id, device.driver + ":" + device.name, error); - return Promise.reject(error); - }); - level_meters[device.unique_id] = promise; - } - }; - - level_update_task = setInterval(() => { - event_registry.fire("update-device-level", { - devices: Object.keys(level_info).map(e => level_info[e]) - }); - }, 50); - - event_registry.on("query-device-result", event => { - if(event.status !== "success") return; - - update_level_meter(); - }); - - event_registry.on("deinitialize", event => { - destroy_meters(); - clearInterval(level_update_task); - }); - } - - /* device list */ - { - event_registry.on("query-devices", event => { - Promise.resolve().then(() => { - return audio.recorder.device_refresh_available() && event.refresh_list ? audio.recorder.refresh_devices() : Promise.resolve(); - }).catch(error => { - log.warn(LogCategory.AUDIO, tr("Failed to refresh device list: %o"), error); - return Promise.resolve(); - }).then(() => { - const devices = audio.recorder.devices(); - - event_registry.fire_async("query-device-result", { - status: "success", - active_device: default_recorder.current_device() ? default_recorder.current_device().unique_id : "none", - devices: devices.map(e => { return { id: e.unique_id, name: e.name, driver: e.driver }}) - }); - }); - }); - event_registry.on("set-device", event => { - const device = audio.recorder.devices().find(e => e.unique_id === event.device_id); - if(!device && event.device_id !== "none") { - event_registry.fire_async("set-device-result", { status: "error", error: tr("Invalid device id"), device_id: event.device_id }); + pending_changes++; + + const default_device = container_devices.find(".selected"); + default_device_id = default_device.attr("device-id"); + default_device.removeClass("selected"); + + const new_device = container_devices.find(".device[device-id='" + event.device_id + "']"); + new_device.addClass("loading"); + }); + event_registry.on("set-device-result", event => { + pending_changes--; + container_devices.find(".loading").removeClass("loading"); + + if(event.status !== "success") { + createErrorModal(tr("Failed to change microphone"), formatMessage(tr("Failed to change the microphone to the target microphone{:br:}{}"), event.status === "timeout" ? tr("Timeout") : event.error || tr("Unknown error"))).open(); + } else { + default_device_id = event.device_id; + } + + container_devices.find(".device[device-id='" + default_device_id + "']").addClass("selected"); + }); + + event_registry.on('query-devices', event => { + Object.keys(volume_bar_tags).forEach(e => delete volume_bar_tags[e]); + container_devices.find(".device").remove(); + container_devices.find(".overlay").hide(); + container_devices.find(".overlay.overlay-loading").show(); + }); + + event_registry.on("query-device-result", event => { + container_devices.find(".device").remove(); + container_devices.find(".overlay").hide(); + + if(event.status !== "success") { + const container_text = container_devices.find(".overlay.overlay-error").show().find(".error-text"); + container_text.text(event.status === "timeout" ? tr("Timeout while loading") : event.error || tr("An unknown error happened")); return; } - default_recorder.set_device(device).then(() => { - console.debug(tr("Changed default microphone device")); - event_registry.fire_async("set-device-result", { status: "success", device_id: event.device_id }); - }).catch((error) => { - log.warn(LogCategory.AUDIO, tr("Failed to change microphone to device %s: %o"), device ? device.unique_id : "none", error) - event_registry.fire_async("set-device-result", { status: "success", device_id: event.device_id }); - }); - }); - } - - /* settings */ - { - event_registry.on("query-settings", event => { - event_registry.fire_async("query-settings-result", { - status: "success", - info: { - volume: default_recorder.get_volume(), - vad_type: default_recorder.get_vad_type(), - vad_ppt: { - key: default_recorder.get_vad_ppt_key(), - release_delay: Math.abs(default_recorder.get_vad_ppt_delay()), - release_delay_active: default_recorder.get_vad_ppt_delay() >= 0 - }, - vad_threshold: { - threshold: default_recorder.get_vad_threshold() - } - } - }); + build_device(undefined, event.active_device === "none").appendTo(container_devices); + for(const device of event.devices) + build_device(device, event.active_device === device.id).appendTo(container_devices); }); - event_registry.on("set-setting", event => { - const ensure_type = (type: "object" | "string" | "boolean" | "number" | "undefined") => { - if(typeof event.value !== type) { - event_registry.fire_async("set-setting-result", { status: "error", error: tr("Invalid value type for key") + " (expected: " + type + ", received: " + typeof event.value + ")", setting: event.setting }); - return false; - } - return true; - }; + event_registry.on("update-device-level", event => { + for(const device of event.devices) { + const tags = volume_bar_tags[device.device_id]; + if(!tags) continue; - switch (event.setting) { - case "volume": - if(!ensure_type("number")) return; - default_recorder.set_volume(event.value); - break; - - case "threshold-threshold": - if(!ensure_type("number")) return; - default_recorder.set_vad_threshold(event.value); - break; - - case "vad-type": - if(!ensure_type("string")) return; - if(!default_recorder.set_vad_type(event.value)) { - event_registry.fire_async("set-setting-result", { status: "error", error: tr("Unknown VAD type"), setting: event.setting }); - return; - } - break; - - case "ppt-key": - if(!ensure_type("object")) return; - default_recorder.set_vad_ppt_key(event.value); - break; - - case "ppt-release-delay": - if(!ensure_type("number")) return; - const sign = default_recorder.get_vad_ppt_delay() >= 0 ? 1 : -1; - default_recorder.set_vad_ppt_delay(sign * event.value); - break; - - case "ppt-release-delay-active": - if(!ensure_type("boolean")) return; - default_recorder.set_vad_ppt_delay(Math.abs(default_recorder.get_vad_ppt_delay()) * (event.value ? 1 : -1)); - break; - - default: - event_registry.fire_async("set-setting-result", { status: "error", error: tr("Invalid setting key"), setting: event.setting }); - return; + let level = typeof device.level === "number" ? device.level : 100; + if(level > 100) level = 100; + else if(level < 0) level = 0; + tags.error.attr('title', device.error || null).text(device.error || null); + tags.volume.css('width', (100 - level) + '%'); } - event_registry.fire_async("set-setting-result", { status: "success", setting: event.setting, value: event.value }); }); } - audio.player.on_ready(() => event_registry.fire_async("audio-initialized", {})); - } - export function initialize_audio_microphone_view(container: JQuery, event_registry: events.Registry) { - /* device list */ + /* device list update button */ { - /* actual list */ - { - const container_devices = container.find(".container-devices"); - const volume_bar_tags: {[key: string]:{ volume: JQuery, error: JQuery }} = {}; - let pending_changes = 0; - let default_device_id; - const build_device = (device: { id: string, name: string, driver: string }, selected: boolean) => { - let tag_volume: JQuery, tag_volume_error: JQuery; - const tag = $.spawn("div").attr("device-id", device ? device.id : "none").addClass("device").toggleClass("selected", selected).append( - $.spawn("div").addClass("container-selected").append( - $.spawn("div").addClass("icon_em client-apply"), - $.spawn("div").addClass("icon-loading").append( - $.spawn("img").attr("src", "img/icon_settings_loading.svg") - ) - ), - $.spawn("div").addClass("container-name").append( - $.spawn("div").addClass("device-driver").text( - device ? (device.driver || "Unknown driver") : "No device" - ), - $.spawn("div").addClass("device-name").text( - device ? (device.name || "Unknown name") : "No device" - ), - ), - $.spawn("div").addClass("container-activity").append( - $.spawn("div").addClass("container-activity-bar").append( - tag_volume = $.spawn("div").addClass("bar-hider"), - tag_volume_error = $.spawn("div").addClass("bar-error") - ) - ) - ); - tag_volume.css('width', '100%'); /* initially hide the bar */ - if(device) - volume_bar_tags[device.id] = { volume: tag_volume, error: tag_volume_error }; + const button_update = container.find(".button-update"); + event_registry.on(["query-devices", "set-device"], event => button_update.prop("disabled", true)); + event_registry.on(["query-device-result", "set-device-result"], event => button_update.prop("disabled", false)); - tag.on('click', event => { - if(tag.hasClass("selected") || pending_changes > 0) return; + button_update.on("click", event => event_registry.fire("query-devices", { refresh_list: true })); + } + } - event_registry.fire("set-device", { device_id: device ? device.id : "none" }); - }); + /* settings */ + { + /* TODO: Query settings error handling */ - return tag; - }; + /* volume */ + { + const container_volume = container.find(".container-volume"); + const slider_tag = container_volume.find(".container-slider"); + let triggered_events = 0; + let last_value = -1; - event_registry.on("set-device", event => { - pending_changes++; + const slider = sliderfy(slider_tag, { + min_value: 0, + max_value: 100, + step: 1, + initial_value: 0 + }); - const default_device = container_devices.find(".selected"); - default_device_id = default_device.attr("device-id"); - default_device.removeClass("selected"); + slider_tag.on('change', event => { + const value = parseInt(slider_tag.attr("value")); + if(last_value === value) return; - const new_device = container_devices.find(".device[device-id='" + event.device_id + "']"); - new_device.addClass("loading"); - }); - event_registry.on("set-device-result", event => { - pending_changes--; - container_devices.find(".loading").removeClass("loading"); + triggered_events++; + event_registry.fire("set-setting", { setting: "volume", value: value }); + }); - if(event.status !== "success") { - createErrorModal(tr("Failed to change microphone"), MessageHelper.formatMessage(tr("Failed to change the microphone to the target microphone{:br:}{}"), event.status === "timeout" ? tr("Timeout") : event.error || tr("Unknown error"))).open(); - } else { - default_device_id = event.device_id; - } + event_registry.on("query-settings-result", event => { + if(event.status !== "success") return; - container_devices.find(".device[device-id='" + default_device_id + "']").addClass("selected"); - }); + last_value = event.info.volume; + slider.value(event.info.volume); + }); - event_registry.on('query-devices', event => { - Object.keys(volume_bar_tags).forEach(e => delete volume_bar_tags[e]); - container_devices.find(".device").remove(); - container_devices.find(".overlay").hide(); - container_devices.find(".overlay.overlay-loading").show(); - }); + event_registry.on("set-setting-result", event => { + if(event.setting !== "volume") return; + if(triggered_events > 0) { + triggered_events--; + return; + } + if(event.status !== "success") return; - event_registry.on("query-device-result", event => { - container_devices.find(".device").remove(); - container_devices.find(".overlay").hide(); - - if(event.status !== "success") { - const container_text = container_devices.find(".overlay.overlay-error").show().find(".error-text"); - container_text.text(event.status === "timeout" ? tr("Timeout while loading") : event.error || tr("An unknown error happened")); - return; - } - - build_device(undefined, event.active_device === "none").appendTo(container_devices); - for(const device of event.devices) - build_device(device, event.active_device === device.id).appendTo(container_devices); - }); - - event_registry.on("update-device-level", event => { - for(const device of event.devices) { - const tags = volume_bar_tags[device.device_id]; - if(!tags) continue; - - let level = typeof device.level === "number" ? device.level : 100; - if(level > 100) level = 100; - else if(level < 0) level = 0; - tags.error.attr('title', device.error || null).text(device.error || null); - tags.volume.css('width', (100 - level) + '%'); - } - }); - } - - /* device list update button */ - { - - const button_update = container.find(".button-update"); - event_registry.on(["query-devices", "set-device"], event => button_update.prop("disabled", true)); - event_registry.on(["query-device-result", "set-device-result"], event => button_update.prop("disabled", false)); - - button_update.on("click", event => event_registry.fire("query-devices", { refresh_list: true })); - } + last_value = event.value; + slider.value(event.value); + }); } - /* settings */ + /* vad type */ { - /* TODO: Query settings error handling */ + const container_select = container.find(".container-select-vad"); + let last_value; - /* volume */ - { - const container_volume = container.find(".container-volume"); - const slider_tag = container_volume.find(".container-slider"); - let triggered_events = 0; - let last_value = -1; + container_select.find("input").on('change', event => { + if(!(event.target).checked) + return; - const slider = sliderfy(slider_tag, { - min_value: 0, - max_value: 100, - step: 1, - initial_value: 0 - }); + const mode = (event.target).value; + if(mode === last_value) return; - slider_tag.on('change', event => { - const value = parseInt(slider_tag.attr("value")); - if(last_value === value) return; + event_registry.fire("set-setting", { setting: "vad-type", value: mode }); + }); - triggered_events++; - event_registry.fire("set-setting", { setting: "volume", value: value }); - }); + const select_vad_type = type => { + let elements = container_select.find('input[value="' + type + '"]'); + if(elements.length < 1) + elements = container_select.find('input[value]'); + elements.first().trigger('click'); + }; - event_registry.on("query-settings-result", event => { + event_registry.on("query-settings-result", event => { + if(event.status !== "success") return; + + last_value = event.info.vad_type; + select_vad_type(event.info.vad_type); + }); + + event_registry.on("set-setting-result", event => { + if(event.setting !== "vad-type") return; + if(event.status !== "success") { + createErrorModal(tr("Failed to change setting"), formatMessage(tr("Failed to change vad type{:br:}{}"), event.status === "timeout" ? tr("Timeout") : event.error || tr("Unknown error"))).open(); + } else { + last_value = event.value; + } + + select_vad_type(last_value); + }); + } + + /* Sensitivity */ + { + const container_sensitivity = container.find(".container-sensitivity"); + + const container_bar = container_sensitivity.find(".container-activity-bar"); + const bar_hider = container_bar.find(".bar-hider"); + + let last_value; + let triggered_events = 0; + let enabled; + + const slider = sliderfy(container_bar, { + min_value: 0, + max_value: 100, + step: 1, + initial_value: 0 + }); + + const set_enabled = value => { + if(enabled === value) return; + + enabled = value; + container_sensitivity.toggleClass("disabled", !value); + }; + + container_bar.on('change', event => { + const value = parseInt(container_bar.attr("value")); + if(last_value === value) return; + + triggered_events++; + event_registry.fire("set-setting", { setting: "threshold-threshold", value: value }); + }); + + event_registry.on("query-settings", event => set_enabled(false)); + event_registry.on("query-settings-result", event => { + if(event.status !== "success") return; + + last_value = event.info.vad_threshold.threshold; + slider.value(event.info.vad_threshold.threshold); + set_enabled(event.info.vad_type === "threshold"); + }); + + event_registry.on("set-setting-result", event => { + if(event.setting === "threshold-threshold") { if(event.status !== "success") return; - last_value = event.info.volume; - slider.value(event.info.volume); - }); - - event_registry.on("set-setting-result", event => { - if(event.setting !== "volume") return; if(triggered_events > 0) { triggered_events--; return; } - if(event.status !== "success") return; last_value = event.value; slider.value(event.value); - }); - } + } else if(event.setting === "vad-type") { + if(event.status !== "success") return; - /* vad type */ + set_enabled(event.value === "threshold"); + } + }); + + let selected_device; + event_registry.on("query-device-result", event => { + if(event.status !== "success") return; + + selected_device = event.active_device; + }); + event_registry.on("set-device-result", event => { + if(event.status !== "success") return; + + selected_device = event.device_id; + }); + + + bar_hider.css("width", "100%"); + event_registry.on("update-device-level", event => { + if(!enabled) return; + + const data = event.devices.find(e => e.device_id === selected_device); + let level = data && typeof data.level === "number" ? data.level : 0; + if(level > 100) level = 100; + else if(level < 0) level = 0; + + bar_hider.css("width", (100 - level) + "%"); + }); + + set_enabled(false); + } + + /* ppt settings */ + { + /* PPT Key */ { - const container_select = container.find(".container-select-vad"); + const button_key = container.find(".container-ppt button"); + event_registry.on("query-settings", event => button_key.prop("disabled", true).text(tr("loading"))); let last_value; - container_select.find("input").on('change', event => { - if(!(event.target).checked) - return; - - const mode = (event.target).value; - if(mode === last_value) return; - - event_registry.fire("set-setting", { setting: "vad-type", value: mode }); - }); - - const select_vad_type = type => { - let elements = container_select.find('input[value="' + type + '"]'); - if(elements.length < 1) - elements = container_select.find('input[value]'); - elements.first().trigger('click'); - }; - event_registry.on("query-settings-result", event => { if(event.status !== "success") return; - last_value = event.info.vad_type; - select_vad_type(event.info.vad_type); + button_key.prop('disabled', event.info.vad_type !== "push_to_talk"); + button_key.text(last_value = key_description(event.info.vad_ppt.key)); + }); + + + event_registry.on("set-setting", event => { + if(event.setting !== "ppt-key") return; + + button_key.prop("enabled", false); + button_key.text(tr("applying")); }); event_registry.on("set-setting-result", event => { - if(event.setting !== "vad-type") return; - if(event.status !== "success") { - createErrorModal(tr("Failed to change setting"), MessageHelper.formatMessage(tr("Failed to change vad type{:br:}{}"), event.status === "timeout" ? tr("Timeout") : event.error || tr("Unknown error"))).open(); - } else { - last_value = event.value; - } - - select_vad_type(last_value); - }); - } - - /* Sensitivity */ - { - const container_sensitivity = container.find(".container-sensitivity"); - - const container_bar = container_sensitivity.find(".container-activity-bar"); - const bar_hider = container_bar.find(".bar-hider"); - - let last_value; - let triggered_events = 0; - let enabled; - - const slider = sliderfy(container_bar, { - min_value: 0, - max_value: 100, - step: 1, - initial_value: 0 - }); - - const set_enabled = value => { - if(enabled === value) return; - - enabled = value; - container_sensitivity.toggleClass("disabled", !value); - }; - - container_bar.on('change', event => { - const value = parseInt(container_bar.attr("value")); - if(last_value === value) return; - - triggered_events++; - event_registry.fire("set-setting", { setting: "threshold-threshold", value: value }); - }); - - event_registry.on("query-settings", event => set_enabled(false)); - event_registry.on("query-settings-result", event => { - if(event.status !== "success") return; - - last_value = event.info.vad_threshold.threshold; - slider.value(event.info.vad_threshold.threshold); - set_enabled(event.info.vad_type === "threshold"); - }); - - event_registry.on("set-setting-result", event => { - if(event.setting === "threshold-threshold") { + if(event.setting === "vad-type") { if(event.status !== "success") return; - if(triggered_events > 0) { - triggered_events--; - return; + button_key.prop('disabled', event.value !== "push_to_talk"); + } else if(event.setting === "ppt-key") { + if(event.status !== "success") { + createErrorModal(tr("Failed to change PPT key"), formatMessage(tr("Failed to change PPT key:{:br:}{}"), event.status === "timeout" ? tr("Timeout") : event.error || tr("Unknown error"))).open(); + } else { + last_value = key_description(event.value); } - - last_value = event.value; - slider.value(event.value); - } else if(event.setting === "vad-type") { - if(event.status !== "success") return; - - set_enabled(event.value === "threshold"); + button_key.text(last_value); } }); - let selected_device; - event_registry.on("query-device-result", event => { - if(event.status !== "success") return; + button_key.on('click', event => { + spawnKeySelect(key => { + if(!key) return; - selected_device = event.active_device; + event_registry.fire("set-setting", { setting: "ppt-key", value: key }); + }); }); - event_registry.on("set-device-result", event => { - if(event.status !== "success") return; - - selected_device = event.device_id; - }); - - - bar_hider.css("width", "100%"); - event_registry.on("update-device-level", event => { - if(!enabled) return; - - const data = event.devices.find(e => e.device_id === selected_device); - let level = data && typeof data.level === "number" ? data.level : 0; - if(level > 100) level = 100; - else if(level < 0) level = 0; - - bar_hider.css("width", (100 - level) + "%"); - }); - - set_enabled(false); } - /* ppt settings */ + /* delay */ { - /* PPT Key */ - { - const button_key = container.find(".container-ppt button"); - event_registry.on("query-settings", event => button_key.prop("disabled", true).text(tr("loading"))); - let last_value; + const container_delay = container.find(".container-ppt-delay"); + /* toggle button */ + { + const input_enabled = container_delay.find("input.delay-enabled"); + const update_enabled_state = () => { + const value = !loading && !applying && ppt_selected; + input_enabled.prop("disabled", !value).parent().toggleClass("disabled", !value); + }; + + let last_state; + let loading = true, applying = false, ppt_selected = false; + + event_registry.on("query-settings", event => { loading = true; update_enabled_state(); }); event_registry.on("query-settings-result", event => { if(event.status !== "success") return; - button_key.prop('disabled', event.info.vad_type !== "push_to_talk"); - button_key.text(last_value = ppt.key_description(event.info.vad_ppt.key)); + loading = false; + ppt_selected = event.info.vad_type === "push_to_talk"; + update_enabled_state(); + input_enabled.prop("checked", last_state = event.info.vad_ppt.release_delay_active); }); - event_registry.on("set-setting", event => { - if(event.setting !== "ppt-key") return; + if(event.setting !== "ppt-release-delay-active") return; - button_key.prop("enabled", false); - button_key.text(tr("applying")); + applying = true; + update_enabled_state(); }); event_registry.on("set-setting-result", event => { if(event.setting === "vad-type") { if(event.status !== "success") return; - button_key.prop('disabled', event.value !== "push_to_talk"); - } else if(event.setting === "ppt-key") { + ppt_selected = event.value === "push_to_talk"; + update_enabled_state(); + } else if(event.setting === "ppt-release-delay-active") { + applying = false; + update_enabled_state(); + if(event.status !== "success") { - createErrorModal(tr("Failed to change PPT key"), MessageHelper.formatMessage(tr("Failed to change PPT key:{:br:}{}"), event.status === "timeout" ? tr("Timeout") : event.error || tr("Unknown error"))).open(); + createErrorModal(tr("Failed to change PPT delay state"), formatMessage(tr("Failed to change PPT delay state:{:br:}{}"), event.status === "timeout" ? tr("Timeout") : event.error || tr("Unknown error"))).open(); } else { - last_value = ppt.key_description(event.value); + last_state = event.value; } - button_key.text(last_value); + input_enabled.prop("checked", last_state); } }); - button_key.on('click', event => { - Modals.spawnKeySelect(key => { - if(!key) return; - - event_registry.fire("set-setting", { setting: "ppt-key", value: key }); - }); + input_enabled.on('change', event => { + event_registry.fire("set-setting", { setting: "ppt-release-delay-active", value: input_enabled.prop("checked") }); }); } - /* delay */ + /* delay input */ { - const container_delay = container.find(".container-ppt-delay"); + const input_time = container_delay.find("input.delay-time"); + const update_enabled_state = () => { + const value = !loading && !applying && ppt_selected && delay_active; + input_time.prop("disabled", !value).parent().toggleClass("disabled", !value); + }; - /* toggle button */ - { - const input_enabled = container_delay.find("input.delay-enabled"); - const update_enabled_state = () => { - const value = !loading && !applying && ppt_selected; - input_enabled.prop("disabled", !value).parent().toggleClass("disabled", !value); - }; + let last_state; + let loading = true, applying = false, ppt_selected = false, delay_active = false; - let last_state; - let loading = true, applying = false, ppt_selected = false; + event_registry.on("query-settings", event => { loading = true; update_enabled_state(); }); + event_registry.on("query-settings-result", event => { + if(event.status !== "success") return; - event_registry.on("query-settings", event => { loading = true; update_enabled_state(); }); - event_registry.on("query-settings-result", event => { + loading = false; + ppt_selected = event.info.vad_type === "push_to_talk"; + delay_active = event.info.vad_ppt.release_delay_active; + update_enabled_state(); + input_time.val(last_state = event.info.vad_ppt.release_delay); + }); + + event_registry.on("set-setting", event => { + if(event.setting !== "ppt-release-delay") return; + + applying = true; + update_enabled_state(); + }); + + event_registry.on("set-setting-result", event => { + if(event.setting === "vad-type") { if(event.status !== "success") return; - loading = false; - ppt_selected = event.info.vad_type === "push_to_talk"; + ppt_selected = event.value === "push_to_talk"; update_enabled_state(); - input_enabled.prop("checked", last_state = event.info.vad_ppt.release_delay_active); - }); - - event_registry.on("set-setting", event => { - if(event.setting !== "ppt-release-delay-active") return; - - applying = true; - update_enabled_state(); - }); - - event_registry.on("set-setting-result", event => { - if(event.setting === "vad-type") { - if(event.status !== "success") return; - - ppt_selected = event.value === "push_to_talk"; - update_enabled_state(); - } else if(event.setting === "ppt-release-delay-active") { - applying = false; - update_enabled_state(); - - if(event.status !== "success") { - createErrorModal(tr("Failed to change PPT delay state"), MessageHelper.formatMessage(tr("Failed to change PPT delay state:{:br:}{}"), event.status === "timeout" ? tr("Timeout") : event.error || tr("Unknown error"))).open(); - } else { - last_state = event.value; - } - input_enabled.prop("checked", last_state); - } - }); - - input_enabled.on('change', event => { - event_registry.fire("set-setting", { setting: "ppt-release-delay-active", value: input_enabled.prop("checked") }); - }); - } - - /* delay input */ - { - const input_time = container_delay.find("input.delay-time"); - const update_enabled_state = () => { - const value = !loading && !applying && ppt_selected && delay_active; - input_time.prop("disabled", !value).parent().toggleClass("disabled", !value); - }; - - let last_state; - let loading = true, applying = false, ppt_selected = false, delay_active = false; - - event_registry.on("query-settings", event => { loading = true; update_enabled_state(); }); - event_registry.on("query-settings-result", event => { + } else if(event.setting === "ppt-release-delay-active") { if(event.status !== "success") return; - loading = false; - ppt_selected = event.info.vad_type === "push_to_talk"; - delay_active = event.info.vad_ppt.release_delay_active; + delay_active = event.value; update_enabled_state(); - input_time.val(last_state = event.info.vad_ppt.release_delay); - }); - - event_registry.on("set-setting", event => { - if(event.setting !== "ppt-release-delay") return; - - applying = true; + } else if(event.setting === "ppt-release-delay") { + applying = false; update_enabled_state(); - }); - event_registry.on("set-setting-result", event => { - if(event.setting === "vad-type") { - if(event.status !== "success") return; - - ppt_selected = event.value === "push_to_talk"; - update_enabled_state(); - } else if(event.setting === "ppt-release-delay-active") { - if(event.status !== "success") return; - - delay_active = event.value; - update_enabled_state(); - } else if(event.setting === "ppt-release-delay") { - applying = false; - update_enabled_state(); - - if(event.status !== "success") { - createErrorModal(tr("Failed to change PPT delay"), MessageHelper.formatMessage(tr("Failed to change PPT delay:{:br:}{}"), event.status === "timeout" ? tr("Timeout") : event.error || tr("Unknown error"))).open(); - } else { - last_state = event.value; - } - input_time.val(last_state); + if(event.status !== "success") { + createErrorModal(tr("Failed to change PPT delay"), formatMessage(tr("Failed to change PPT delay:{:br:}{}"), event.status === "timeout" ? tr("Timeout") : event.error || tr("Unknown error"))).open(); + } else { + last_state = event.value; } - }); + input_time.val(last_state); + } + }); - input_time.on('change', event => { - event_registry.fire("set-setting", { setting: "ppt-release-delay", value: parseInt(input_time.val() as any) }); - }); - } + input_time.on('change', event => { + event_registry.fire("set-setting", { setting: "ppt-release-delay", value: parseInt(input_time.val() as any) }); + }); } } } - - /* timeouts */ - { - /* device query */ - { - let timeout; - event_registry.on('query-devices', event => { - clearTimeout(timeout); - timeout = setTimeout(() => { - event_registry.fire("query-device-result", { status: "timeout" }); - }, 5000); - }); - - event_registry.on("query-device-result", event => clearTimeout(timeout)); - } - - /* device set */ - { - let timeouts = {}; - event_registry.on('set-device', event => { - clearTimeout(timeouts[event.device_id]); - timeouts[event.device_id] = setTimeout(() => { - event_registry.fire("set-device-result", { status: "timeout", device_id: event.device_id }); - }, 5000); - }); - - event_registry.on("set-device-result", event => clearTimeout(timeouts[event.device_id])); - } - - /* settings query */ - { - let timeout; - event_registry.on('query-settings', event => { - clearTimeout(timeout); - timeout = setTimeout(() => { - event_registry.fire("query-settings-result", { status: "timeout" }); - }, 5000); - }); - - event_registry.on("query-settings-result", event => clearTimeout(timeout)); - } - - /* settings change */ - { - let timeouts = {}; - event_registry.on('set-setting', event => { - clearTimeout(timeouts[event.setting]); - timeouts[event.setting] = setTimeout(() => { - event_registry.fire("set-setting-result", { status: "timeout", setting: event.setting }); - }, 5000); - }); - - event_registry.on("set-setting-result", event => clearTimeout(timeouts[event.setting])); - } - } - - event_registry.on("audio-initialized", () => { - event_registry.fire("query-settings"); - event_registry.fire("query-devices", { refresh_list: false }); - }); } - } - function settings_identity_forum(container: JQuery, modal: Modal, update_profiles: () => any) { - const containers_connected = container.find(".show-connected"); - const containers_disconnected = container.find(".show-disconnected"); + /* timeouts */ + { + /* device query */ + { + let timeout; + event_registry.on('query-devices', event => { + clearTimeout(timeout); + timeout = setTimeout(() => { + event_registry.fire("query-device-result", { status: "timeout" }); + }, 5000); + }); - const update_state = () => { - const logged_in = forum.logged_in(); - containers_connected.toggle(logged_in); - containers_disconnected.toggle(!logged_in); - - if(logged_in) { - container.find(".forum-username").text(forum.data().name()); - container.find(".forum-premium").text(forum.data().is_premium() ? tr("Yes") : tr("No")); + event_registry.on("query-device-result", event => clearTimeout(timeout)); } + + /* device set */ + { + let timeouts = {}; + event_registry.on('set-device', event => { + clearTimeout(timeouts[event.device_id]); + timeouts[event.device_id] = setTimeout(() => { + event_registry.fire("set-device-result", { status: "timeout", device_id: event.device_id }); + }, 5000); + }); + + event_registry.on("set-device-result", event => clearTimeout(timeouts[event.device_id])); + } + + /* settings query */ + { + let timeout; + event_registry.on('query-settings', event => { + clearTimeout(timeout); + timeout = setTimeout(() => { + event_registry.fire("query-settings-result", { status: "timeout" }); + }, 5000); + }); + + event_registry.on("query-settings-result", event => clearTimeout(timeout)); + } + + /* settings change */ + { + let timeouts = {}; + event_registry.on('set-setting', event => { + clearTimeout(timeouts[event.setting]); + timeouts[event.setting] = setTimeout(() => { + event_registry.fire("set-setting-result", { status: "timeout", setting: event.setting }); + }, 5000); + }); + + event_registry.on("set-setting-result", event => clearTimeout(timeouts[event.setting])); + } + } + + event_registry.on("audio-initialized", () => { + event_registry.fire("query-settings"); + event_registry.fire("query-devices", { refresh_list: false }); + }); + } +} + +function settings_identity_forum(container: JQuery, modal: Modal, update_profiles: () => any) { + const containers_connected = container.find(".show-connected"); + const containers_disconnected = container.find(".show-disconnected"); + + const update_state = () => { + const logged_in = forum.logged_in(); + containers_connected.toggle(logged_in); + containers_disconnected.toggle(!logged_in); + + if(logged_in) { + container.find(".forum-username").text(forum.data().name()); + container.find(".forum-premium").text(forum.data().is_premium() ? tr("Yes") : tr("No")); + } + }; + + /* login */ + { + const button_login = container.find(".button-login"); + const input_username = container.find(".input-username"); + const input_password = container.find(".input-password"); + const container_error = container.find(".container-login .container-error"); + + const container_captcha_g = container.find(".g-recaptcha"); + let captcha: boolean | string = false; + + const update_button_state = () => { + let enabled = true; + enabled = enabled && !!input_password.val(); + enabled = enabled && !!input_username.val(); + enabled = enabled && (typeof(captcha) === "boolean" ? !captcha : !!captcha); + button_login.prop("disabled", !enabled); }; - /* login */ - { - const button_login = container.find(".button-login"); - const input_username = container.find(".input-username"); - const input_password = container.find(".input-password"); - const container_error = container.find(".container-login .container-error"); + /* username */ + input_username.on('change keyup', update_button_state); - const container_captcha_g = container.find(".g-recaptcha"); - let captcha: boolean | string = false; + /* password */ + input_password.on('change keyup', update_button_state); - const update_button_state = () => { - let enabled = true; - enabled = enabled && !!input_password.val(); - enabled = enabled && !!input_username.val(); - enabled = enabled && (typeof(captcha) === "boolean" ? !captcha : !!captcha); - button_login.prop("disabled", !enabled); - }; + button_login.on('click', event => { + input_username.prop("disabled", true); + input_password.prop("disabled", true); + button_login.prop("disabled", true); + container_error.removeClass("shown"); - /* username */ - input_username.on('change keyup', update_button_state); + forum.login(input_username.val() as string, input_password.val() as string, typeof(captcha) === "string" ? captcha : undefined).then(state => { + captcha = false; - /* password */ - input_password.on('change keyup', update_button_state); - - button_login.on('click', event => { - input_username.prop("disabled", true); - input_password.prop("disabled", true); - button_login.prop("disabled", true); - container_error.removeClass("shown"); - - forum.login(input_username.val() as string, input_password.val() as string, typeof(captcha) === "string" ? captcha : undefined).then(state => { - captcha = false; - - console.debug(tr("Forum login result: %o"), state); - if(state.status === "success") { - update_state(); - update_profiles(); - return; - } - - setTimeout(() => { - if(!!state.error_message) /* clear password if we have an error */ - input_password.val(""); - input_password.focus(); - update_button_state(); - }, 0); - if(state.status === "captcha") { - //TODO Works currently only with localhost! - button_login.hide(); - container_error.text(state.error_message || tr("Captcha required")).addClass("shown"); - - captcha = ""; - - console.log(tr("Showing captcha for site-key: %o"), state.captcha.data); - forum.gcaptcha.spawn(container_captcha_g, state.captcha.data, token => { - captcha = token; - console.debug(tr("Got captcha token: %o"), token); - container_captcha_g.hide(); - button_login.show(); - update_button_state(); - }).catch(error => { - console.error(tr("Failed to initialize forum captcha: %o"), error); - container_error.text("Failed to initialize GReCaptcha! No authentication possible.").addClass("shown"); - container_captcha_g.hide(); - button_login.hide(); - }); - container_captcha_g.show(); - } else { - container_error.text(state.error_message || tr("Unknown error")).addClass("shown"); - } - }).catch(error => { - console.error(tr("Failed to login within the forum. Error: %o"), error); - createErrorModal(tr("Forum login failed."), tr("Forum login failed. Lookup the console for more information")).open(); - }).then(() => { - input_username.prop("disabled", false); - input_password.prop("disabled", false); - update_button_state(); - }); - }); - update_button_state(); - } - - /* logout */ - { - container.find(".button-logout").on('click', event => { - forum.logout().catch(error => { - console.error(tr("Failed to logout from forum: %o"), error); - createErrorModal(tr("Forum logout failed"), MessageHelper.formatMessage(tr("Failed to logout from forum account.{:br:}Error: {}"), error)).open(); - }).then(() => { - if (modal.shown) - update_state(); + console.debug(tr("Forum login result: %o"), state); + if(state.status === "success") { + update_state(); update_profiles(); - }); - }); - } + return; + } - update_state(); + setTimeout(() => { + if(!!state.error_message) /* clear password if we have an error */ + input_password.val(""); + input_password.focus(); + update_button_state(); + }, 0); + if(state.status === "captcha") { + //TODO Works currently only with localhost! + button_login.hide(); + container_error.text(state.error_message || tr("Captcha required")).addClass("shown"); + + captcha = ""; + + console.log(tr("Showing captcha for site-key: %o"), state.captcha.data); + forum.gcaptcha.spawn(container_captcha_g, state.captcha.data, token => { + captcha = token; + console.debug(tr("Got captcha token: %o"), token); + container_captcha_g.hide(); + button_login.show(); + update_button_state(); + }).catch(error => { + console.error(tr("Failed to initialize forum captcha: %o"), error); + container_error.text("Failed to initialize GReCaptcha! No authentication possible.").addClass("shown"); + container_captcha_g.hide(); + button_login.hide(); + }); + container_captcha_g.show(); + } else { + container_error.text(state.error_message || tr("Unknown error")).addClass("shown"); + } + }).catch(error => { + console.error(tr("Failed to login within the forum. Error: %o"), error); + createErrorModal(tr("Forum login failed."), tr("Forum login failed. Lookup the console for more information")).open(); + }).then(() => { + input_username.prop("disabled", false); + input_password.prop("disabled", false); + update_button_state(); + }); + }); + update_button_state(); } + + /* logout */ + { + container.find(".button-logout").on('click', event => { + forum.logout().catch(error => { + console.error(tr("Failed to logout from forum: %o"), error); + createErrorModal(tr("Forum logout failed"), formatMessage(tr("Failed to logout from forum account.{:br:}Error: {}"), error)).open(); + }).then(() => { + if (modal.shown) + update_state(); + update_profiles(); + }); + }); + } + + update_state(); } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalYesNo.ts b/shared/js/ui/modal/ModalYesNo.ts index 4562d4ac..51be7aef 100644 --- a/shared/js/ui/modal/ModalYesNo.ts +++ b/shared/js/ui/modal/ModalYesNo.ts @@ -1,47 +1,45 @@ -/// +import {BodyCreator, createModal, ModalFunctions} from "tc-shared/ui/elements/Modal"; -namespace Modals { - export function spawnYesNo(header: BodyCreator, body: BodyCreator, callback: (_: boolean) => any, properties?: { - text_yes?: string, - text_no?: string, +export function spawnYesNo(header: BodyCreator, body: BodyCreator, callback: (_: boolean) => any, properties?: { + text_yes?: string, + text_no?: string, - closeable?: boolean; - }) { - properties = properties || {}; + closeable?: boolean; +}) { + properties = properties || {}; - const props = ModalFunctions.warpProperties({}); - props.template_properties || (props.template_properties = {}); - props.template_properties.text_yes = properties.text_yes || tr("Yes"); - props.template_properties.text_no = properties.text_no || tr("No"); - props.template = "#tmpl_modal_yesno"; + const props = ModalFunctions.warpProperties({}); + props.template_properties || (props.template_properties = {}); + props.template_properties.text_yes = properties.text_yes || tr("Yes"); + props.template_properties.text_no = properties.text_no || tr("No"); + props.template = "#tmpl_modal_yesno"; - props.header = header; - props.template_properties.question = ModalFunctions.jqueriefy(body); + props.header = header; + props.template_properties.question = ModalFunctions.jqueriefy(body); - props.closeable = typeof(properties.closeable) !== "boolean" || properties.closeable; - const modal = createModal(props); - let submited = false; - const button_yes = modal.htmlTag.find(".button-yes"); - const button_no = modal.htmlTag.find(".button-no"); + props.closeable = typeof(properties.closeable) !== "boolean" || properties.closeable; + const modal = createModal(props); + let submited = false; + const button_yes = modal.htmlTag.find(".button-yes"); + const button_no = modal.htmlTag.find(".button-no"); - button_yes.on('click', event => { - if(!submited) { - submited = true; - callback(true); - } - modal.close(); - }); + button_yes.on('click', event => { + if(!submited) { + submited = true; + callback(true); + } + modal.close(); + }); - button_no.on('click', event => { - if(!submited) { - submited = true; - callback(false); - } - modal.close(); - }); + button_no.on('click', event => { + if(!submited) { + submited = true; + callback(false); + } + modal.close(); + }); - modal.close_listener.push(() => button_no.trigger('click')); - modal.open(); - return modal; - } + modal.close_listener.push(() => button_no.trigger('click')); + modal.open(); + return modal; } \ No newline at end of file diff --git a/shared/js/ui/modal/permission/AbstractPermissionEditor.ts b/shared/js/ui/modal/permission/AbstractPermissionEditor.ts new file mode 100644 index 00000000..63101b0d --- /dev/null +++ b/shared/js/ui/modal/permission/AbstractPermissionEditor.ts @@ -0,0 +1,61 @@ +import {GroupedPermissions, PermissionInfo, PermissionValue} from "tc-shared/permission/PermissionManager"; +import PermissionType from "tc-shared/permission/PermissionType"; + +export enum PermissionEditorMode { + VISIBLE, + NO_PERMISSION, + UNSET +} + +export abstract class AbstractPermissionEditor { + protected _permissions: GroupedPermissions[]; + protected _listener_update: () => any; + protected _listener_change: ChangeListener = () => Promise.resolve(); + protected _toggle_callback: () => string; + + icon_resolver: (id: number) => Promise; + icon_selector: (current_id: number) => Promise; + + protected constructor() {} + + abstract set_mode(mode: PermissionEditorMode); + + abstract initialize(permissions: GroupedPermissions[]); + abstract html_tag() : JQuery; + abstract set_permissions(permissions?: PermissionValue[]); + abstract set_hidden_permissions(permissions: PermissionType[]); + + set_listener(listener?: ChangeListener) { + this._listener_change = listener || (() => Promise.resolve()); + } + + set_listener_update(listener?: () => any) { this._listener_update = listener; } + trigger_update() { if(this._listener_update) this._listener_update(); } + + abstract set_toggle_button(callback: () => string, initial: string); +} + +export interface PermissionEntry { + tag: JQuery; + tag_value: JQuery; + tag_grant: JQuery; + tag_flag_negate: JQuery; + tag_flag_skip: JQuery; + + id: number; + filter: string; + is_bool: boolean; + +} + +export interface ChangedPermissionValue { + remove: boolean; /* if set remove the set permission (value or granted) */ + + granted?: number; + value?: number; + + flag_skip?: boolean; + flag_negate?: boolean; +} + +export type ChangeListener = (permission: PermissionInfo, value?: ChangedPermissionValue) => Promise; \ No newline at end of file diff --git a/shared/js/ui/modal/permission/CanvasPermissionEditor.ts b/shared/js/ui/modal/permission/CanvasPermissionEditor.ts index f2c5ecfd..4afced0e 100644 --- a/shared/js/ui/modal/permission/CanvasPermissionEditor.ts +++ b/shared/js/ui/modal/permission/CanvasPermissionEditor.ts @@ -1,1586 +1,1453 @@ /// /* first needs the AbstractPermissionEdit */ /* Canvas Permission Editor */ -namespace pe { - namespace ui { - export namespace scheme { - export interface CheckBox { - border: string; - checkmark: string; - checkmark_font: string; +import PermissionType from "tc-shared/permission/PermissionType"; +import {KeyCode} from "tc-shared/PPTListener"; +import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; +import {createInfoModal} from "tc-shared/ui/elements/Modal"; +import {copy_to_clipboard} from "tc-shared/utils/helpers"; +import {AbstractPermissionEditor, PermissionEditorMode} from "tc-shared/ui/modal/permission/AbstractPermissionEditor"; +import {GroupedPermissions, PermissionInfo, PermissionValue} from "tc-shared/permission/PermissionManager"; - background_checked: string; - background_checked_hovered: string; +namespace ui { + export namespace scheme { + export interface CheckBox { + border: string; + checkmark: string; + checkmark_font: string; + background_checked: string; + background_checked_hovered: string; + + background: string; + background_hovered: string; + } + + export interface TextField { + color: string; + font: string; + + background: string; + background_hovered: string; + } + + export interface ColorScheme { + permission: { background: string; - background_hovered: string; + background_selected: string; + + name: string; + name_unset: string; + name_font: string; + + value: TextField; + value_b: CheckBox; + granted: TextField; + negate: CheckBox; + skip: CheckBox; } - export interface TextField { - color: string; - font: string; - - background: string; - background_hovered: string; - } - - export interface ColorScheme { - permission: { - background: string; - background_selected: string; - - name: string; - name_unset: string; - name_font: string; - - value: TextField; - value_b: CheckBox; - granted: TextField; - negate: CheckBox; - skip: CheckBox; - } - - group: { - name: string; - name_font: string; - } - } - } - - export enum RepaintMode { - NONE, - REPAINT, - REPAINT_OBJECT_FULL, - REPAINT_FULL - } - - export interface AxisAlignedBoundingBox { - x: number; - y: number; - - width: number; - height: number; - } - - export enum ClickEventType { - SIGNLE, - DOUBLE, - CONTEXT_MENU - } - - export interface InteractionClickEvent { - type: ClickEventType; - consumed: boolean; - offset_x: number; - offset_y: number; - } - - export interface InteractionListener { - region: AxisAlignedBoundingBox; - region_weight: number; - - /** - * @return true if a redraw is required - */ - on_mouse_enter?: () => RepaintMode; - - /** - * @return true if a redraw is required - */ - on_mouse_leave?: () => RepaintMode; - - /** - * @return true if a redraw is required - */ - on_click?: (event: InteractionClickEvent) => RepaintMode; - - mouse_cursor?: string; - - set_full_draw?: () => any; - disabled?: boolean; - } - - abstract class DrawableObject { - abstract draw(context: CanvasRenderingContext2D, full: boolean); - - private _object_full_draw = false; - private _width: number = 0; - - set_width(value: number) { - this._width = value; - } - - request_full_draw() { - this._object_full_draw = true; - } - - pop_full_draw() { - const result = this._object_full_draw; - this._object_full_draw = false; - return result; - } - - width() { - return this._width; - } - - abstract height(); - - private _transforms: DOMMatrix[] = []; - - protected push_transform(context: CanvasRenderingContext2D) { - this._transforms.push(context.getTransform()); - } - - protected pop_transform(context: CanvasRenderingContext2D) { - const transform = this._transforms.pop(); - context.setTransform( - transform.a, - transform.b, - transform.c, - transform.d, - transform.e, - transform.f - ); - } - - protected original_x(context: CanvasRenderingContext2D, x: number) { - return context.getTransform().e + x; - } - - protected original_y(context: CanvasRenderingContext2D, y: number) { - return context.getTransform().f + y; - } - - protected colors: scheme.ColorScheme = {} as any; - - set_color_scheme(scheme: scheme.ColorScheme) { - this.colors = scheme; - } - - protected manager: PermissionEditor; - - set_manager(manager: PermissionEditor) { - this.manager = manager; - } - - abstract initialize(); - - abstract finalize(); - } - - class PermissionGroup extends DrawableObject { - public static readonly HEIGHT = parseFloat(getComputedStyle(document.documentElement).fontSize) * (3 / 2); /* 24 */ - public static readonly ARROW_SIZE = 10; /* 12 */ - - group: GroupedPermissions; - _sub_elements: PermissionGroup[] = []; - _element_permissions: PermissionList; - - collapsed = false; - private _listener_colaps: InteractionListener; - - constructor(group: GroupedPermissions) { - super(); - - this.group = group; - - this._element_permissions = new PermissionList(this.group.permissions); - for (const sub of this.group.children) - this._sub_elements.push(new PermissionGroup(sub)); - } - - draw(context: CanvasRenderingContext2D, full: boolean) { - const _full = this.pop_full_draw() || full; - this.push_transform(context); - context.translate(PermissionGroup.ARROW_SIZE + 20, PermissionGroup.HEIGHT); - - let sum_height = 0; - /* let first draw the elements, because if the sum height is zero then we could hide ourselves */ - if (!this.collapsed) { /* draw the next groups */ - for (const group of this._sub_elements) { - group.draw(context, full); - - const height = group.height(); - sum_height += height; - context.translate(0, height); - } - - this._element_permissions.draw(context, full); - if (sum_height == 0) - sum_height += this._element_permissions.height(); - } else { - const process_group = (group: PermissionGroup) => { - for (const g of group._sub_elements) - process_group(g); - group._element_permissions.handle_hide(); - if (sum_height == 0 && group._element_permissions.height() > 0) { - sum_height = 1; - } - }; - process_group(this); - } - this.pop_transform(context); - - if (_full && sum_height > 0) { - const arrow_stretch = 2 / 3; - if (!full) { - context.clearRect(0, 0, this.width(), PermissionGroup.HEIGHT); - } - context.fillStyle = this.colors.group.name; - - /* arrow */ - { - const x1 = this.collapsed ? PermissionGroup.ARROW_SIZE * arrow_stretch / 2 : 0; - const y1 = (PermissionGroup.HEIGHT - PermissionGroup.ARROW_SIZE) / 2 + (this.collapsed ? 0 : PermissionGroup.ARROW_SIZE * arrow_stretch / 2); /* center arrow */ - - const x2 = this.collapsed ? x1 + PermissionGroup.ARROW_SIZE * arrow_stretch : x1 + PermissionGroup.ARROW_SIZE / 2; - const y2 = this.collapsed ? y1 + PermissionGroup.ARROW_SIZE / 2 : y1 + PermissionGroup.ARROW_SIZE * arrow_stretch; - - const x3 = this.collapsed ? x1 : x1 + PermissionGroup.ARROW_SIZE; - const y3 = this.collapsed ? y1 + PermissionGroup.ARROW_SIZE : y1; - - context.beginPath(); - context.moveTo(x1, y1); - - context.lineTo(x2, y2); - context.lineTo(x3, y3); - - context.moveTo(x2, y2); - context.lineTo(x3, y3); - context.fill(); - - this._listener_colaps.region.x = this.original_x(context, 0); - this._listener_colaps.region.y = this.original_y(context, y1); - } - /* text */ - { - context.font = this.colors.group.name_font; - context.textBaseline = "middle"; - context.textAlign = "start"; - - context.fillText(this.group.group.name, PermissionGroup.ARROW_SIZE + 5, PermissionGroup.HEIGHT / 2); - } - } - } - - set_width(value: number) { - super.set_width(value); - for (const element of this._sub_elements) - element.set_width(value - PermissionGroup.ARROW_SIZE - 20); - this._element_permissions.set_width(value - PermissionGroup.ARROW_SIZE - 20); - } - - set_color_scheme(scheme: scheme.ColorScheme) { - super.set_color_scheme(scheme); - for (const child of this._sub_elements) - child.set_color_scheme(scheme); - this._element_permissions.set_color_scheme(scheme); - } - - set_manager(manager: PermissionEditor) { - super.set_manager(manager); - for (const child of this._sub_elements) - child.set_manager(manager); - this._element_permissions.set_manager(manager); - } - - height() { - let result = 0; - - if (!this.collapsed) { - for (const element of this._sub_elements) - result += element.height(); - - result += this._element_permissions.height(); - } else { - //We've to figure out if we have permissions - const process_group = (group: PermissionGroup) => { - if (result == 0 && group._element_permissions.height() > 0) { - result = 1; - } else { - for (const g of group._sub_elements) - process_group(g); - } - }; - process_group(this); - - if (result > 0) - return PermissionGroup.HEIGHT; - - return 0; - } - if (result > 0) { - result += PermissionGroup.HEIGHT; - return result; - } else { - return 0; - } - } - - initialize() { - for (const child of this._sub_elements) - child.initialize(); - this._element_permissions.initialize(); - - - this._listener_colaps = { - region: { - x: 0, - y: 0, - height: PermissionGroup.ARROW_SIZE, - width: PermissionGroup.ARROW_SIZE - }, - region_weight: 10, - /* - on_mouse_enter: () => { - this.collapsed_hovered = true; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_mouse_leave: () => { - this.collapsed_hovered = false; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - */ - on_click: () => { - this.collapsed = !this.collapsed; - return RepaintMode.REPAINT_FULL; - }, - set_full_draw: () => this.request_full_draw(), - mouse_cursor: "pointer" - }; - - this.manager.intercept_manager().register_listener(this._listener_colaps); - } - - finalize() { - for (const child of this._sub_elements) - child.finalize(); - this._element_permissions.finalize(); - } - - collapse_group() { - for (const child of this._sub_elements) - child.collapse_group(); - - this.collapsed = true; - } - - expend_group() { - for (const child of this._sub_elements) - child.expend_group(); - - this.collapsed = false; - } - } - - class PermissionList extends DrawableObject { - permissions: PermissionEntry[] = []; - - constructor(permissions: PermissionInfo[]) { - super(); - - for (const permission of permissions) - this.permissions.push(new PermissionEntry(permission)); - } - - set_width(value: number) { - super.set_width(value); - for (const entry of this.permissions) - entry.set_width(value); - } - - - draw(context: CanvasRenderingContext2D, full: boolean) { - this.push_transform(context); - - for (const permission of this.permissions) { - permission.draw(context, full); - context.translate(0, permission.height()); - } - - this.pop_transform(context); - } - - height() { - let height = 0; - for (const permission of this.permissions) - height += permission.height(); - return height; - } - - - set_color_scheme(scheme: scheme.ColorScheme) { - super.set_color_scheme(scheme); - for (const entry of this.permissions) - entry.set_color_scheme(scheme); - } - - set_manager(manager: PermissionEditor) { - super.set_manager(manager); - - for (const entry of this.permissions) - entry.set_manager(manager); - } - - initialize() { - for (const entry of this.permissions) - entry.initialize(); - } - - finalize() { - for (const entry of this.permissions) - entry.finalize(); - } - - handle_hide() { - for (const entry of this.permissions) - entry.handle_hide(); - } - } - - class PermissionEntry extends DrawableObject { - public static readonly HEIGHT = PermissionGroup.HEIGHT; /* 24 */ - public static readonly HALF_HEIGHT = PermissionEntry.HEIGHT / 2; - public static readonly CHECKBOX_HEIGHT = PermissionEntry.HEIGHT - 2; - - public static readonly COLUMN_PADDING = 2; - public static readonly COLUMN_VALUE = 75; - public static readonly COLUMN_GRANTED = 75; - //public static readonly COLUMN_NEGATE = 25; - //public static readonly COLUMN_SKIP = 25; - public static readonly COLUMN_NEGATE = 75; - public static readonly COLUMN_SKIP = 75; - - private _permission: PermissionInfo; - - hidden: boolean; - - granted: number = 22; - value: number; - flag_skip: boolean = true; - flag_negate: boolean; - - private _prev_selected = false; - selected: boolean; - - flag_skip_hovered = false; - flag_negate_hovered = false; - flag_value_hovered = false; - flag_grant_hovered = false; - - private _listener_checkbox_skip: InteractionListener; - private _listener_checkbox_negate: InteractionListener; - private _listener_value: InteractionListener; - private _listener_grant: InteractionListener; - private _listener_general: InteractionListener; - private _icon_image: HTMLImageElement | undefined; - - on_icon_select?: (current_id: number) => Promise; - on_context_menu?: (x: number, y: number) => any; - on_grant_change?: () => any; - on_change?: () => any; - - constructor(permission: PermissionInfo) { - super(); - this._permission = permission; - } - - set_icon_id_image(image: HTMLImageElement | undefined) { - if (this._icon_image === image) - return; - this._icon_image = image; - if (image) { - image.height = 16; - image.width = 16; - } - } - - permission() { - return this._permission; - } - - draw(ctx: CanvasRenderingContext2D, full: boolean) { - if (!this.pop_full_draw() && !full) { /* Note: do not change this order! */ - /* test for update! */ - return; - } - if (this.hidden) { - this.handle_hide(); - return; - } - ctx.lineWidth = 1; - - /* debug box */ - if (false) { - ctx.fillStyle = "#FF0000"; - ctx.fillRect(0, 0, this.width(), PermissionEntry.HEIGHT); - ctx.fillStyle = "#000000"; - ctx.strokeRect(0, 0, this.width(), PermissionEntry.HEIGHT); - } - - if (!full) { - const off = this.selected || this._prev_selected ? ctx.getTransform().e : 0; - ctx.clearRect(-off, 0, this.width() + off, PermissionEntry.HEIGHT); - } - - if (this.selected) - ctx.fillStyle = this.colors.permission.background_selected; - else - ctx.fillStyle = this.colors.permission.background; - const off = this.selected ? ctx.getTransform().e : 0; - ctx.fillRect(-off, 0, this.width() + off, PermissionEntry.HEIGHT); - this._prev_selected = this.selected; - - /* permission name */ - { - ctx.fillStyle = typeof (this.value) !== "undefined" ? this.colors.permission.name : this.colors.permission.name_unset; - ctx.textBaseline = "middle"; - ctx.textAlign = "start"; - ctx.font = this.colors.permission.name_font; - - ctx.fillText(this._permission.name, 0, PermissionEntry.HALF_HEIGHT); - } - - const original_y = this.original_y(ctx, 0); - const original_x = this.original_x(ctx, 0); - const width = this.width(); - - /* draw granted */ - let w = width - PermissionEntry.COLUMN_GRANTED; - if (typeof (this.granted) === "number") { - this._listener_grant.region.x = original_x + w; - this._listener_grant.region.y = original_y; - - this._draw_number_field(ctx, this.colors.permission.granted, w, 0, PermissionEntry.COLUMN_VALUE, this.granted, this.flag_grant_hovered); - } else { - this._listener_grant.region.y = original_y; - this._listener_grant.region.x = - original_x - + width - - PermissionEntry.COLUMN_GRANTED; - } - - /* draw value and the skip stuff */ - if (typeof (this.value) === "number") { - w -= PermissionEntry.COLUMN_SKIP + PermissionEntry.COLUMN_PADDING; - { - const x = w + (PermissionEntry.COLUMN_SKIP - PermissionEntry.CHECKBOX_HEIGHT) / 2; - const y = 1; - - this._listener_checkbox_skip.region.x = original_x + x; - this._listener_checkbox_skip.region.y = original_y + y; - - this._draw_checkbox_field(ctx, this.colors.permission.skip, x, y, PermissionEntry.CHECKBOX_HEIGHT, this.flag_skip, this.flag_skip_hovered); - } - - w -= PermissionEntry.COLUMN_NEGATE + PermissionEntry.COLUMN_PADDING; - { - const x = w + (PermissionEntry.COLUMN_NEGATE - PermissionEntry.CHECKBOX_HEIGHT) / 2; - const y = 1; - - this._listener_checkbox_negate.region.x = original_x + x; - this._listener_checkbox_negate.region.y = original_y + y; - - this._draw_checkbox_field(ctx, this.colors.permission.negate, x, y, PermissionEntry.CHECKBOX_HEIGHT, this.flag_negate, this.flag_negate_hovered); - } - - w -= PermissionEntry.COLUMN_VALUE + PermissionEntry.COLUMN_PADDING; - if (this._permission.is_boolean()) { - const x = w + PermissionEntry.COLUMN_VALUE - PermissionEntry.CHECKBOX_HEIGHT; - const y = 1; - - this._listener_value.region.width = PermissionEntry.CHECKBOX_HEIGHT; - this._listener_value.region.x = original_x + x; - this._listener_value.region.y = original_y + y; - - this._draw_checkbox_field(ctx, this.colors.permission.value_b, x, y, PermissionEntry.CHECKBOX_HEIGHT, this.value > 0, this.flag_value_hovered); - } else if (this._permission.name === "i_icon_id" && this._icon_image) { - this._listener_value.region.x = original_x + w; - this._listener_value.region.y = original_y; - this._listener_value.region.width = PermissionEntry.CHECKBOX_HEIGHT; - - this._draw_icon_field(ctx, this.colors.permission.value_b, w, 0, PermissionEntry.COLUMN_VALUE, this.flag_value_hovered, this._icon_image); - } else { - this._listener_value.region.width = PermissionEntry.COLUMN_VALUE; - this._listener_value.region.x = original_x + w; - this._listener_value.region.y = original_y; - - this._draw_number_field(ctx, this.colors.permission.value, w, 0, PermissionEntry.COLUMN_VALUE, this.value, this.flag_value_hovered); - } - this._listener_value.disabled = false; - } else { - this._listener_checkbox_skip.region.y = -1e8; - this._listener_checkbox_negate.region.y = -1e8; - - this._listener_value.region.y = original_y; - this._listener_value.region.x = - original_x - + width - - PermissionEntry.COLUMN_GRANTED - - PermissionEntry.COLUMN_NEGATE - - PermissionEntry.COLUMN_VALUE - - PermissionEntry.COLUMN_PADDING * 4; - this._listener_value.disabled = true; - } - - this._listener_general.region.y = original_y; - this._listener_general.region.x = original_x; - } - - handle_hide() { - /* so the listener wound get triggered */ - this._listener_value.region.x = -1e8; - this._listener_grant.region.x = -1e8; - this._listener_checkbox_negate.region.x = -1e8; - this._listener_checkbox_skip.region.x = -1e8; - this._listener_general.region.x = -1e8; - } - - private _draw_icon_field(ctx: CanvasRenderingContext2D, scheme: scheme.CheckBox, x: number, y: number, width: number, hovered: boolean, image: HTMLImageElement) { - const line = ctx.lineWidth; - ctx.lineWidth = 2; - ctx.fillStyle = scheme.border; - ctx.strokeRect(x + 1, y + 1, PermissionEntry.HEIGHT - 2, PermissionEntry.HEIGHT - 2); - ctx.lineWidth = line; - - ctx.fillStyle = hovered ? scheme.background_hovered : scheme.background; - ctx.fillRect(x + 1, y + 1, PermissionEntry.HEIGHT - 2, PermissionEntry.HEIGHT - 2); - - const center_y = y + PermissionEntry.HEIGHT / 2; - const center_x = x + PermissionEntry.HEIGHT / 2; - ctx.drawImage(image, center_x - image.width / 2, center_y - image.height / 2); - } - - private _draw_number_field(ctx: CanvasRenderingContext2D, scheme: scheme.TextField, x: number, y: number, width: number, value: number, hovered: boolean) { - ctx.fillStyle = hovered ? scheme.background_hovered : scheme.background; - ctx.fillRect(x, y, width, PermissionEntry.HEIGHT); - - ctx.fillStyle = scheme.color; - ctx.font = scheme.font; //Math.floor(2/3 * PermissionEntry.HEIGHT) + "px Arial"; - ctx.textAlign = "start"; - ctx.fillText(value + "", x, y + PermissionEntry.HALF_HEIGHT, width); - - ctx.strokeStyle = "#6e6e6e"; - const line = ctx.lineWidth; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(x, y + PermissionEntry.HEIGHT - 2); - ctx.lineTo(x + width, y + PermissionEntry.HEIGHT - 2); - ctx.stroke(); - ctx.lineWidth = line; - } - - private _draw_checkbox_field(ctx: CanvasRenderingContext2D, scheme: scheme.CheckBox, x: number, y: number, height: number, checked: boolean, hovered: boolean) { - ctx.fillStyle = scheme.border; - ctx.strokeRect(x, y, height, height); - - - ctx.fillStyle = checked ? - (hovered ? scheme.background_checked_hovered : scheme.background_checked) : - (hovered ? scheme.background_hovered : scheme.background); - ctx.fillRect(x + 1, y + 1, height - 2, height - 2); - - if (checked) { - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillStyle = scheme.checkmark; - ctx.font = scheme.checkmark_font; //Math.floor((5/4) * PermissionEntry.HEIGHT) + "px Arial"; - ctx.fillText("✓", x + height / 2, y + height / 2); - } - } - - height() { - return this.hidden ? 0 : PermissionEntry.HEIGHT; - } - - set_width(value: number) { - super.set_width(value); - this._listener_general.region.width = value; - } - - initialize() { - this._listener_checkbox_skip = { - region: { - x: -1e8, - y: -1e8, - height: PermissionEntry.CHECKBOX_HEIGHT, - width: PermissionEntry.CHECKBOX_HEIGHT - }, - region_weight: 10, - on_mouse_enter: () => { - this.flag_skip_hovered = true; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_mouse_leave: () => { - this.flag_skip_hovered = false; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_click: () => { - this.flag_skip = !this.flag_skip; - if (this.on_change) - this.on_change(); - return RepaintMode.REPAINT_OBJECT_FULL; - }, - set_full_draw: () => this.request_full_draw(), - mouse_cursor: "pointer" - }; - this._listener_checkbox_negate = { - region: { - x: -1e8, - y: -1e8, - height: PermissionEntry.CHECKBOX_HEIGHT, - width: PermissionEntry.CHECKBOX_HEIGHT - }, - region_weight: 10, - on_mouse_enter: () => { - this.flag_negate_hovered = true; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_mouse_leave: () => { - this.flag_negate_hovered = false; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_click: () => { - this.flag_negate = !this.flag_negate; - if (this.on_change) - this.on_change(); - return RepaintMode.REPAINT_OBJECT_FULL; - }, - set_full_draw: () => this.request_full_draw(), - mouse_cursor: "pointer" - }; - this._listener_value = { - region: { - x: -1e8, - y: -1e8, - height: this._permission.is_boolean() ? PermissionEntry.CHECKBOX_HEIGHT : PermissionEntry.HEIGHT, - width: this._permission.is_boolean() ? PermissionEntry.CHECKBOX_HEIGHT : PermissionEntry.COLUMN_VALUE - }, - region_weight: 10, - on_mouse_enter: () => { - this.flag_value_hovered = true; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_mouse_leave: () => { - this.flag_value_hovered = false; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_click: () => { - if (this._permission.is_boolean()) { - this.value = this.value > 0 ? 0 : 1; - if (this.on_change) - this.on_change(); - return RepaintMode.REPAINT_OBJECT_FULL; - } else if (this._permission.name === "i_icon_id") { - this.on_icon_select(this.value).then(value => { - this.value = value; - if (this.on_change) - this.on_change(); - }).catch(error => { - console.warn(tr("Failed to select icon: %o"), error); - }) - } else { - this._spawn_number_edit( - this._listener_value.region.x, - this._listener_value.region.y, - this._listener_value.region.width, - this._listener_value.region.height, - this.colors.permission.value, - this.value || 0, - value => { - if (typeof (value) === "number") { - this.value = value; - this.request_full_draw(); - this.manager.request_draw(false); - if (this.on_change) - this.on_change(); - } - } - ) - } - return RepaintMode.REPAINT_OBJECT_FULL; - }, - set_full_draw: () => this.request_full_draw(), - mouse_cursor: "pointer" - }; - this._listener_grant = { - region: { - x: -1e8, - y: -1e8, - height: PermissionEntry.HEIGHT, - width: PermissionEntry.COLUMN_VALUE - }, - region_weight: 10, - on_mouse_enter: () => { - this.flag_grant_hovered = true; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_mouse_leave: () => { - this.flag_grant_hovered = false; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_click: () => { - this._spawn_number_edit( - this._listener_grant.region.x, - this._listener_grant.region.y, - this._listener_grant.region.width, - this._listener_grant.region.height, - this.colors.permission.granted, - this.granted || 0, //TODO use max assignable value? - value => { - if (typeof (value) === "number") { - this.granted = value; - this.request_full_draw(); - this.manager.request_draw(false); - - if (this.on_grant_change) - this.on_grant_change(); - } - } - ); - return RepaintMode.REPAINT_OBJECT_FULL; - }, - set_full_draw: () => this.request_full_draw(), - mouse_cursor: "pointer" - }; - - this._listener_general = { - region: { - x: -1e8, - y: -1e8, - height: PermissionEntry.HEIGHT, - width: 0 - }, - region_weight: 0, - /* - on_mouse_enter: () => { - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_mouse_leave: () => { - return RepaintMode.REPAINT_OBJECT_FULL; - }, - */ - on_click: (event: InteractionClickEvent) => { - this.manager.set_selected_entry(this); - - if (event.type == ClickEventType.DOUBLE && typeof (this.value) === "undefined") - return this._listener_value.on_click(event); - else if (event.type == ClickEventType.CONTEXT_MENU) { - const mouse = this.manager.mouse; - if (this.on_context_menu) { - this.on_context_menu(mouse.x, mouse.y); - event.consumed = true; - } - } - return RepaintMode.NONE; - }, - set_full_draw: () => this.request_full_draw(), - }; - - this.manager.intercept_manager().register_listener(this._listener_checkbox_negate); - this.manager.intercept_manager().register_listener(this._listener_checkbox_skip); - this.manager.intercept_manager().register_listener(this._listener_value); - this.manager.intercept_manager().register_listener(this._listener_grant); - this.manager.intercept_manager().register_listener(this._listener_general); - } - - finalize() { - } - - private _spawn_number_edit(x: number, y: number, width: number, height: number, color: scheme.TextField, value: number, callback: (new_value?: number) => any) { - const element = $.spawn("div"); - element.prop("contentEditable", true); - element - .css("pointer-events", "none") - .css("background", color.background) - .css("display", "block") - .css("position", "absolute") - .css("top", y) - .css("left", x) - .css("width", width) - .css("height", height) - .css("z-index", 1e6); - element.text(value); - element.appendTo(this.manager.canvas_container); - element.focus(); - - element.on('focusout', event => { - console.log("permission changed to " + element.text()); - if (!isNaN(parseInt(element.text()))) { - callback(parseInt(element.text())); - } else { - callback(undefined); - } - element.remove(); - }); - - element.on('keypress', event => { - if (event.which == KeyCode.KEY_RETURN) - element.trigger('focusout'); - - const text = String.fromCharCode(event.which); - if (isNaN(parseInt(text)) && text != "-") - event.preventDefault(); - - if (element.text().length > 7) - event.preventDefault(); - }); - - if (window.getSelection) { - const selection = window.getSelection(); - const range = document.createRange(); - range.selectNodeContents(element[0]); - selection.removeAllRanges(); - selection.addRange(range); - } - } - - trigger_value_assign() { - this._listener_value.on_click(undefined); - } - - trigger_grant_assign() { - this._listener_grant.on_click(undefined); - } - } - - export class InteractionManager { - private _listeners: InteractionListener[] = []; - private _entered_listeners: InteractionListener[] = []; - - register_listener(listener: InteractionListener) { - this._listeners.push(listener); - } - - remove_listener(listener: InteractionListener) { - this._listeners.remove(listener); - } - - process_mouse_move(new_x: number, new_y: number): { repaint: RepaintMode, cursor: string } { - let _entered_listeners: InteractionListener[] = []; - for (const listener of this._listeners) { - const aabb = listener.region; - - if (listener.disabled) - continue; - - if (new_x < aabb.x || new_x > aabb.x + aabb.width) - continue; - - if (new_y < aabb.y || new_y > aabb.y + aabb.height) - continue; - - _entered_listeners.push(listener); - } - - let repaint: RepaintMode = RepaintMode.NONE; - _entered_listeners.sort((a, b) => (a.region_weight || 0) - (b.region_weight || 0)); - for (const listener of this._entered_listeners) { - if (listener.on_mouse_leave && _entered_listeners.indexOf(listener) == -1) { - let mode = listener.on_mouse_leave(); - if (mode == RepaintMode.REPAINT_OBJECT_FULL) { - mode = RepaintMode.REPAINT; - if (listener.set_full_draw) - listener.set_full_draw(); - } - if (mode > repaint) - repaint = mode; - } - } - for (const listener of _entered_listeners) { - if (listener.on_mouse_enter && this._entered_listeners.indexOf(listener) == -1) { - let mode = listener.on_mouse_enter(); - if (mode == RepaintMode.REPAINT_OBJECT_FULL) { - mode = RepaintMode.REPAINT; - if (listener.set_full_draw) - listener.set_full_draw(); - } - if (mode > repaint) - repaint = mode; - } - } - this._entered_listeners = _entered_listeners; - - let cursor; - for (const listener of _entered_listeners) - if (typeof (listener.mouse_cursor) === "string") { - cursor = listener.mouse_cursor; - } - return { - repaint: repaint, - cursor: cursor - }; - } - - private process_click_event(x: number, y: number, event: InteractionClickEvent): RepaintMode { - const move_result = this.process_mouse_move(x, y); - - let repaint: RepaintMode = move_result.repaint; - for (const listener of this._entered_listeners) - if (listener.on_click) { - let mode = listener.on_click(event); - if (mode == RepaintMode.REPAINT_OBJECT_FULL) { - mode = RepaintMode.REPAINT; - if (listener.set_full_draw) - listener.set_full_draw(); - } - if (mode > repaint) - repaint = mode; - } - - return repaint; - } - - process_click(x: number, y: number): RepaintMode { - const event: InteractionClickEvent = { - consumed: false, - type: ClickEventType.SIGNLE, - offset_x: x, - offset_y: y - }; - - return this.process_click_event(x, y, event); - } - - process_dblclick(x: number, y: number): RepaintMode { - const event: InteractionClickEvent = { - consumed: false, - type: ClickEventType.DOUBLE, - offset_x: x, - offset_y: y - }; - - return this.process_click_event(x, y, event); - } - - process_context_menu(js_event: MouseEvent, x: number, y: number): RepaintMode { - const event: InteractionClickEvent = { - consumed: js_event.defaultPrevented, - type: ClickEventType.CONTEXT_MENU, - offset_x: x, - offset_y: y - }; - - const result = this.process_click_event(x, y, event); - if (event.consumed) - js_event.preventDefault(); - return result; - } - } - - export class PermissionEditor { - private static readonly PERMISSION_HEIGHT = PermissionEntry.HEIGHT; - private static readonly PERMISSION_GROUP_HEIGHT = PermissionGroup.HEIGHT; - - readonly grouped_permissions: GroupedPermissions[]; - readonly canvas: HTMLCanvasElement; - readonly canvas_container: HTMLDivElement; - private _max_height: number = 0; - - private _permission_count: number = 0; - private _permission_group_count: number = 0; - private _canvas_context: CanvasRenderingContext2D; - - private _selected_entry: PermissionEntry; - - private _draw_requested: boolean = false; - private _draw_requested_full: boolean = false; - - private _elements: PermissionGroup[] = []; - private _intersect_manager: InteractionManager; - - private _permission_entry_map: { [key: number]: PermissionEntry } = {}; - - mouse: { - x: number, - y: number - } = { - x: 0, - y: 0 - }; - - constructor(permissions: GroupedPermissions[]) { - this.grouped_permissions = permissions; - - this.canvas_container = $.spawn("div") - .addClass("window-resize-listener") /* we want to handle resized */ - .css("min-width", "750px") - .css("position", "relative") - .css("user-select", "none") - [0]; - this.canvas = $.spawn("canvas")[0]; - - this.canvas_container.appendChild(this.canvas); - - this._intersect_manager = new InteractionManager(); - this.canvas_container.onmousemove = event => { - this.mouse.x = event.pageX; - this.mouse.y = event.pageY; - - const draw = this._intersect_manager.process_mouse_move(event.offsetX, event.offsetY); - this.canvas_container.style.cursor = draw.cursor || ""; - this._handle_repaint(draw.repaint); - }; - this.canvas_container.onclick = event => { - this._handle_repaint(this._intersect_manager.process_click(event.offsetX, event.offsetY)); - }; - this.canvas_container.ondblclick = event => { - this._handle_repaint(this._intersect_manager.process_dblclick(event.offsetX, event.offsetY)); - }; - this.canvas_container.oncontextmenu = (event: MouseEvent) => { - this._handle_repaint(this._intersect_manager.process_context_menu(event, event.offsetX, event.offsetY)); - }; - this.canvas_container.onresize = () => this.request_draw(true); - - - this.initialize(); - } - - private _handle_repaint(mode: RepaintMode) { - if (mode == RepaintMode.REPAINT || mode == RepaintMode.REPAINT_FULL) - this.request_draw(mode == RepaintMode.REPAINT_FULL); - } - - request_draw(full?: boolean) { - this._draw_requested_full = this._draw_requested_full || full; - if (this._draw_requested) - return; - this._draw_requested = true; - requestAnimationFrame(() => { - this.draw(this._draw_requested_full); - }); - } - - draw(full?: boolean) { - this._draw_requested = false; - this._draw_requested_full = false; - - /* clear max height */ - this.canvas_container.style.overflowY = "shown"; - this.canvas_container.style.height = undefined; - - const max_height = this._max_height; - const max_width = this.canvas_container.clientWidth; - const update_width = this.canvas.width != max_width; - const full_draw = typeof (full) !== "boolean" || full || update_width; - - if (update_width) { - this.canvas.width = max_width; - for (const element of this._elements) - element.set_width(max_width); - } - - console.log("Drawing%s on %dx%d", full_draw ? " full" : "", max_width, max_height); - if (full_draw) - this.canvas.height = max_height; - const ctx = this._canvas_context; - ctx.resetTransform(); - if (full_draw) - ctx.clearRect(0, 0, max_width, max_height); - - let sum_height = 0; - for (const element of this._elements) { - element.draw(ctx, full_draw); - const height = element.height(); - sum_height += height; - ctx.translate(0, height); - } - - this.canvas_container.style.overflowY = "hidden"; - this.canvas_container.style.height = sum_height + "px"; - } - - private initialize() { - /* setup the canvas */ - { - const apply_group = (group: GroupedPermissions) => { - for (const g of group.children || []) - apply_group(g); - this._permission_group_count++; - this._permission_count += group.permissions.length; - }; - for (const group of this.grouped_permissions) - apply_group(group); - - this._max_height = this._permission_count * PermissionEditor.PERMISSION_HEIGHT + this._permission_group_count * PermissionEditor.PERMISSION_GROUP_HEIGHT; - console.log("%d permissions and %d groups required %d height", this._permission_count, this._permission_group_count, this._max_height); - - this.canvas.style.width = "100%"; - - this.canvas.style.flexShrink = "0"; - this.canvas_container.style.flexShrink = "0"; - - this._canvas_context = this.canvas.getContext("2d"); - } - - const font = Math.floor(2 / 3 * PermissionEntry.HEIGHT) + "px Arial"; - const font_checkmark = Math.floor((5 / 4) * PermissionEntry.HEIGHT) + "px Arial"; - const checkbox = { - background: "#303036", - background_hovered: "#CCCCCC", - - background_checked: "#0000AA", - background_checked_hovered: "#0000AA77", - - border: "#000000", - checkmark: "#303036", - checkmark_font: font_checkmark - }; - const input: scheme.TextField = { - color: "#000000", - font: font, - - background_hovered: "#CCCCCCCC", - background: "#30303600" - }; - - const color_scheme: scheme.ColorScheme = { - group: { - name: "#808080", - name_font: font - }, - //#28282c - permission: { - name: "#808080", - name_unset: "#1a1a1a", - name_font: font, - - background: "#303036", - background_selected: "#00007788", - - value: input, - value_b: checkbox, - granted: input, - negate: checkbox, - skip: checkbox - } - }; - (window as any).scheme = color_scheme; - /* setup elements to draw */ - { - const process_group = (group: PermissionGroup) => { - for (const permission of group._element_permissions.permissions) - this._permission_entry_map[permission.permission().id] = permission; - for (const g of group._sub_elements) - process_group(g); - }; - - for (const group of this.grouped_permissions) { - const element = new PermissionGroup(group); - element.set_color_scheme(color_scheme); - element.set_manager(this); - process_group(element); - this._elements.push(element); - } - for (const element of this._elements) { - element.initialize(); - } - } - } - - intercept_manager() { - return this._intersect_manager; - } - - set_selected_entry(entry?: PermissionEntry) { - if (this._selected_entry === entry) - return; - - if (this._selected_entry) { - this._selected_entry.selected = false; - this._selected_entry.request_full_draw(); - } - this._selected_entry = entry; - if (this._selected_entry) { - this._selected_entry.selected = true; - this._selected_entry.request_full_draw(); - } - this.request_draw(false); - } - - permission_entries(): PermissionEntry[] { - return Object.keys(this._permission_entry_map).map(e => this._permission_entry_map[e]); - } - - collapse_all() { - for (const group of this._elements) - group.collapse_group(); - this.request_draw(true); - } - - expend_all() { - for (const group of this._elements) - group.expend_group(); - this.request_draw(true); + group: { + name: string; + name_font: string; } } } - export class CanvasPermissionEditor extends Modals.AbstractPermissionEditor { - private container: JQuery; + export enum RepaintMode { + NONE, + REPAINT, + REPAINT_OBJECT_FULL, + REPAINT_FULL + } - private mode_container_permissions: JQuery; - private mode_container_error_permission: JQuery; - private mode_container_unset: JQuery; + export interface AxisAlignedBoundingBox { + x: number; + y: number; - /* references within the container tag */ - private permission_value_map: {[key:number]:PermissionValue} = {}; + width: number; + height: number; + } - private entry_editor: ui.PermissionEditor; + export enum ClickEventType { + SIGNLE, + DOUBLE, + CONTEXT_MENU + } - icon_resolver: (id: number) => Promise; - icon_selector: (current_id: number) => Promise; + export interface InteractionClickEvent { + type: ClickEventType; + consumed: boolean; + offset_x: number; + offset_y: number; + } - constructor() { + export interface InteractionListener { + region: AxisAlignedBoundingBox; + region_weight: number; + + /** + * @return true if a redraw is required + */ + on_mouse_enter?: () => RepaintMode; + + /** + * @return true if a redraw is required + */ + on_mouse_leave?: () => RepaintMode; + + /** + * @return true if a redraw is required + */ + on_click?: (event: InteractionClickEvent) => RepaintMode; + + mouse_cursor?: string; + + set_full_draw?: () => any; + disabled?: boolean; + } + + abstract class DrawableObject { + abstract draw(context: CanvasRenderingContext2D, full: boolean); + + private _object_full_draw = false; + private _width: number = 0; + + set_width(value: number) { + this._width = value; + } + + request_full_draw() { + this._object_full_draw = true; + } + + pop_full_draw() { + const result = this._object_full_draw; + this._object_full_draw = false; + return result; + } + + width() { + return this._width; + } + + abstract height(); + + private _transforms: DOMMatrix[] = []; + + protected push_transform(context: CanvasRenderingContext2D) { + this._transforms.push(context.getTransform()); + } + + protected pop_transform(context: CanvasRenderingContext2D) { + const transform = this._transforms.pop(); + context.setTransform( + transform.a, + transform.b, + transform.c, + transform.d, + transform.e, + transform.f + ); + } + + protected original_x(context: CanvasRenderingContext2D, x: number) { + return context.getTransform().e + x; + } + + protected original_y(context: CanvasRenderingContext2D, y: number) { + return context.getTransform().f + y; + } + + protected colors: scheme.ColorScheme = {} as any; + + set_color_scheme(scheme: scheme.ColorScheme) { + this.colors = scheme; + } + + protected manager: PermissionEditor; + + set_manager(manager: PermissionEditor) { + this.manager = manager; + } + + abstract initialize(); + + abstract finalize(); + } + + class PermissionGroup extends DrawableObject { + public static readonly HEIGHT = parseFloat(getComputedStyle(document.documentElement).fontSize) * (3 / 2); /* 24 */ + public static readonly ARROW_SIZE = 10; /* 12 */ + + group: GroupedPermissions; + _sub_elements: PermissionGroup[] = []; + _element_permissions: PermissionList; + + collapsed = false; + private _listener_colaps: InteractionListener; + + constructor(group: GroupedPermissions) { super(); + + this.group = group; + + this._element_permissions = new PermissionList(this.group.permissions); + for (const sub of this.group.children) + this._sub_elements.push(new PermissionGroup(sub)); } - initialize(permissions: GroupedPermissions[]) { - this._permissions = permissions; - this.entry_editor = new ui.PermissionEditor(permissions); - this.build_tag(); - } + draw(context: CanvasRenderingContext2D, full: boolean) { + const _full = this.pop_full_draw() || full; + this.push_transform(context); + context.translate(PermissionGroup.ARROW_SIZE + 20, PermissionGroup.HEIGHT); - html_tag() { return this.container; } + let sum_height = 0; + /* let first draw the elements, because if the sum height is zero then we could hide ourselves */ + if (!this.collapsed) { /* draw the next groups */ + for (const group of this._sub_elements) { + group.draw(context, full); - private build_tag() { - this.container = $("#tmpl_permission_editor_canvas").renderTag(); - /* search for that as long we've not that much nodes */ - this.mode_container_permissions = this.container.find(".container-mode-permissions"); - this.mode_container_error_permission = this.container.find(".container-mode-no-permissions"); - this.mode_container_unset = this.container.find(".container-mode-unset"); - this.set_mode(Modals.PermissionEditorMode.UNSET); + const height = group.height(); + sum_height += height; + context.translate(0, height); + } - /* the filter */ - { - const tag_filter_input = this.container.find(".filter-input"); - const tag_filter_granted = this.container.find(".filter-granted"); - - tag_filter_granted.on('change', event => tag_filter_input.trigger('change')); - tag_filter_input.on('keyup change', event => { - let filter_mask = tag_filter_input.val() as string; - let req_granted = tag_filter_granted.prop("checked"); - - - for(const entry of this.entry_editor.permission_entries()) { - const permission = entry.permission(); - - let shown = filter_mask.length == 0 || permission.name.indexOf(filter_mask) != -1; - if(shown && req_granted) { - const value: PermissionValue = this.permission_value_map[permission.id]; - shown = value && (value.hasValue() || value.hasGrant()); - } - - entry.hidden = !shown; + this._element_permissions.draw(context, full); + if (sum_height == 0) + sum_height += this._element_permissions.height(); + } else { + const process_group = (group: PermissionGroup) => { + for (const g of group._sub_elements) + process_group(g); + group._element_permissions.handle_hide(); + if (sum_height == 0 && group._element_permissions.height() > 0) { + sum_height = 1; } - this.entry_editor.request_draw(true); - }); - } - - /* update button */ - { - this.container.find(".button-update").on('click', this.trigger_update.bind(this)); - } - - /* global context menu listener */ - { - this.container.on('contextmenu', event => { - if(event.isDefaultPrevented()) return; - event.preventDefault(); - - /* TODO allow collapse and expend all */ - }); - } - - { - const tag_container = this.container.find(".entry-editor-container"); - tag_container.append(this.entry_editor.canvas_container); - - tag_container.parent().on('contextmenu', event => { - if(event.isDefaultPrevented()) return; - event.preventDefault(); - - contextmenu.spawn_context_menu(event.pageX, event.pageY, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Expend all"), - callback: () => this.entry_editor.expend_all() - }, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Collapse all"), - callback: () => this.entry_editor.collapse_all() - }); - }); - } - - /* setup the permissions */ - for(const entry of this.entry_editor.permission_entries()) { - const permission = entry.permission(); - entry.on_change = () => { - const flag_remove = typeof(entry.value) !== "number"; - this._listener_change(permission, { - remove: flag_remove, - flag_negate: entry.flag_negate, - flag_skip: entry.flag_skip, - value: flag_remove ? -2 : entry.value - }).then(() => { - if(flag_remove) { - const element = this.permission_value_map[permission.id]; - if(!element) return; /* This should never happen, if so how are we displaying this permission?! */ - - element.value = undefined; - element.flag_negate = false; - element.flag_skip = false; - } else { - const element = this.permission_value_map[permission.id] || (this.permission_value_map[permission.id] = new PermissionValue(permission)); - - element.value = entry.value; - element.flag_skip = entry.flag_skip; - element.flag_negate = entry.flag_negate; - } - - if(permission.name === "i_icon_id") { - this.icon_resolver(entry.value).then(e => { - entry.set_icon_id_image(e); - entry.request_full_draw(); - this.entry_editor.request_draw(false); - }).catch(error => { - console.warn(tr("Failed to load icon for permission editor: %o"), error); - }); - } - entry.request_full_draw(); - this.entry_editor.request_draw(false); - }).catch(() => { - const element = this.permission_value_map[permission.id]; - - entry.value = element && element.hasValue() ? element.value : undefined; - entry.flag_skip = element && element.flag_skip; - entry.flag_negate = element && element.flag_negate; - - entry.request_full_draw(); - this.entry_editor.request_draw(false); - }); }; + process_group(this); + } + this.pop_transform(context); - entry.on_grant_change = () => { - const flag_remove = typeof(entry.granted) !== "number"; + if (_full && sum_height > 0) { + const arrow_stretch = 2 / 3; + if (!full) { + context.clearRect(0, 0, this.width(), PermissionGroup.HEIGHT); + } + context.fillStyle = this.colors.group.name; - this._listener_change(permission, { - remove: flag_remove, - granted: flag_remove ? -2 : entry.granted, - }).then(() => { - if(flag_remove) { - const element = this.permission_value_map[permission.id]; - if (!element) return; /* This should never happen, if so how are we displaying this permission?! */ + /* arrow */ + { + const x1 = this.collapsed ? PermissionGroup.ARROW_SIZE * arrow_stretch / 2 : 0; + const y1 = (PermissionGroup.HEIGHT - PermissionGroup.ARROW_SIZE) / 2 + (this.collapsed ? 0 : PermissionGroup.ARROW_SIZE * arrow_stretch / 2); /* center arrow */ - element.granted_value = undefined; - } else { - const element = this.permission_value_map[permission.id] || (this.permission_value_map[permission.id] = new PermissionValue(permission)); - element.granted_value = entry.granted; - } - entry.request_full_draw(); - this.entry_editor.request_draw(false); - }).catch(() => { - const element = this.permission_value_map[permission.id]; + const x2 = this.collapsed ? x1 + PermissionGroup.ARROW_SIZE * arrow_stretch : x1 + PermissionGroup.ARROW_SIZE / 2; + const y2 = this.collapsed ? y1 + PermissionGroup.ARROW_SIZE / 2 : y1 + PermissionGroup.ARROW_SIZE * arrow_stretch; - entry.granted = element && element.hasGrant() ? element.granted_value : undefined; - entry.request_full_draw(); - this.entry_editor.request_draw(false); - }); - }; + const x3 = this.collapsed ? x1 : x1 + PermissionGroup.ARROW_SIZE; + const y3 = this.collapsed ? y1 + PermissionGroup.ARROW_SIZE : y1; - entry.on_context_menu = (x, y) => { - let entries: contextmenu.MenuEntry[] = []; - if(typeof(entry.value) === "undefined") { - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Add permission"), - callback: () => entry.trigger_value_assign() - }); - } else { - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Remove permission"), - callback: () => { - entry.value = undefined; - entry.on_change(); - } - }); - } + context.beginPath(); + context.moveTo(x1, y1); - if(typeof(entry.granted) === "undefined") { - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Add grant permission"), - callback: () => entry.trigger_grant_assign() - }); - } else { - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Remove grant permission"), - callback: () => { - entry.granted = undefined; - entry.on_grant_change(); - } - }); - } - entries.push(contextmenu.Entry.HR()); - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Expend all"), - callback: () => this.entry_editor.expend_all() - }); - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Collapse all"), - callback: () => this.entry_editor.collapse_all() - }); - entries.push(contextmenu.Entry.HR()); - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Show permission description"), - callback: () => { - createInfoModal( - tr("Permission description"), - tr("Permission description for permission ") + permission.name + ":
" + permission.description - ).open(); - } - }); - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Copy permission name"), - callback: () => { - copy_to_clipboard(permission.name); - } - }); + context.lineTo(x2, y2); + context.lineTo(x3, y3); - contextmenu.spawn_context_menu(x, y, ...entries); + context.moveTo(x2, y2); + context.lineTo(x3, y3); + context.fill(); + + this._listener_colaps.region.x = this.original_x(context, 0); + this._listener_colaps.region.y = this.original_y(context, y1); + } + /* text */ + { + context.font = this.colors.group.name_font; + context.textBaseline = "middle"; + context.textAlign = "start"; + + context.fillText(this.group.group.name, PermissionGroup.ARROW_SIZE + 5, PermissionGroup.HEIGHT / 2); } } } - set_permissions(permissions?: PermissionValue[]) { - permissions = permissions || []; - this.permission_value_map = {}; + set_width(value: number) { + super.set_width(value); + for (const element of this._sub_elements) + element.set_width(value - PermissionGroup.ARROW_SIZE - 20); + this._element_permissions.set_width(value - PermissionGroup.ARROW_SIZE - 20); + } - for(const permission of permissions) - this.permission_value_map[permission.type.id] = permission; + set_color_scheme(scheme: scheme.ColorScheme) { + super.set_color_scheme(scheme); + for (const child of this._sub_elements) + child.set_color_scheme(scheme); + this._element_permissions.set_color_scheme(scheme); + } - for(const entry of this.entry_editor.permission_entries()) { - const permission = entry.permission(); - const value: PermissionValue = this.permission_value_map[permission.id]; + set_manager(manager: PermissionEditor) { + super.set_manager(manager); + for (const child of this._sub_elements) + child.set_manager(manager); + this._element_permissions.set_manager(manager); + } - if(permission.name === "i_icon_id") { - entry.set_icon_id_image(undefined); - entry.on_icon_select = this.icon_selector; + height() { + let result = 0; + + if (!this.collapsed) { + for (const element of this._sub_elements) + result += element.height(); + + result += this._element_permissions.height(); + } else { + //We've to figure out if we have permissions + const process_group = (group: PermissionGroup) => { + if (result == 0 && group._element_permissions.height() > 0) { + result = 1; + } else { + for (const g of group._sub_elements) + process_group(g); + } + }; + process_group(this); + + if (result > 0) + return PermissionGroup.HEIGHT; + + return 0; + } + if (result > 0) { + result += PermissionGroup.HEIGHT; + return result; + } else { + return 0; + } + } + + initialize() { + for (const child of this._sub_elements) + child.initialize(); + this._element_permissions.initialize(); + + + this._listener_colaps = { + region: { + x: 0, + y: 0, + height: PermissionGroup.ARROW_SIZE, + width: PermissionGroup.ARROW_SIZE + }, + region_weight: 10, + /* + on_mouse_enter: () => { + this.collapsed_hovered = true; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_mouse_leave: () => { + this.collapsed_hovered = false; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + */ + on_click: () => { + this.collapsed = !this.collapsed; + return RepaintMode.REPAINT_FULL; + }, + set_full_draw: () => this.request_full_draw(), + mouse_cursor: "pointer" + }; + + this.manager.intercept_manager().register_listener(this._listener_colaps); + } + + finalize() { + for (const child of this._sub_elements) + child.finalize(); + this._element_permissions.finalize(); + } + + collapse_group() { + for (const child of this._sub_elements) + child.collapse_group(); + + this.collapsed = true; + } + + expend_group() { + for (const child of this._sub_elements) + child.expend_group(); + + this.collapsed = false; + } + } + + class PermissionList extends DrawableObject { + permissions: PermissionEntry[] = []; + + constructor(permissions: PermissionInfo[]) { + super(); + + for (const permission of permissions) + this.permissions.push(new PermissionEntry(permission)); + } + + set_width(value: number) { + super.set_width(value); + for (const entry of this.permissions) + entry.set_width(value); + } + + + draw(context: CanvasRenderingContext2D, full: boolean) { + this.push_transform(context); + + for (const permission of this.permissions) { + permission.draw(context, full); + context.translate(0, permission.height()); + } + + this.pop_transform(context); + } + + height() { + let height = 0; + for (const permission of this.permissions) + height += permission.height(); + return height; + } + + + set_color_scheme(scheme: scheme.ColorScheme) { + super.set_color_scheme(scheme); + for (const entry of this.permissions) + entry.set_color_scheme(scheme); + } + + set_manager(manager: PermissionEditor) { + super.set_manager(manager); + + for (const entry of this.permissions) + entry.set_manager(manager); + } + + initialize() { + for (const entry of this.permissions) + entry.initialize(); + } + + finalize() { + for (const entry of this.permissions) + entry.finalize(); + } + + handle_hide() { + for (const entry of this.permissions) + entry.handle_hide(); + } + } + + class PermissionEntry extends DrawableObject { + public static readonly HEIGHT = PermissionGroup.HEIGHT; /* 24 */ + public static readonly HALF_HEIGHT = PermissionEntry.HEIGHT / 2; + public static readonly CHECKBOX_HEIGHT = PermissionEntry.HEIGHT - 2; + + public static readonly COLUMN_PADDING = 2; + public static readonly COLUMN_VALUE = 75; + public static readonly COLUMN_GRANTED = 75; + //public static readonly COLUMN_NEGATE = 25; + //public static readonly COLUMN_SKIP = 25; + public static readonly COLUMN_NEGATE = 75; + public static readonly COLUMN_SKIP = 75; + + private _permission: PermissionInfo; + + hidden: boolean; + + granted: number = 22; + value: number; + flag_skip: boolean = true; + flag_negate: boolean; + + private _prev_selected = false; + selected: boolean; + + flag_skip_hovered = false; + flag_negate_hovered = false; + flag_value_hovered = false; + flag_grant_hovered = false; + + private _listener_checkbox_skip: InteractionListener; + private _listener_checkbox_negate: InteractionListener; + private _listener_value: InteractionListener; + private _listener_grant: InteractionListener; + private _listener_general: InteractionListener; + private _icon_image: HTMLImageElement | undefined; + + on_icon_select?: (current_id: number) => Promise; + on_context_menu?: (x: number, y: number) => any; + on_grant_change?: () => any; + on_change?: () => any; + + constructor(permission: PermissionInfo) { + super(); + this._permission = permission; + } + + set_icon_id_image(image: HTMLImageElement | undefined) { + if (this._icon_image === image) + return; + this._icon_image = image; + if (image) { + image.height = 16; + image.width = 16; + } + } + + permission() { + return this._permission; + } + + draw(ctx: CanvasRenderingContext2D, full: boolean) { + if (!this.pop_full_draw() && !full) { /* Note: do not change this order! */ + /* test for update! */ + return; + } + if (this.hidden) { + this.handle_hide(); + return; + } + ctx.lineWidth = 1; + + /* debug box */ + if (false) { + ctx.fillStyle = "#FF0000"; + ctx.fillRect(0, 0, this.width(), PermissionEntry.HEIGHT); + ctx.fillStyle = "#000000"; + ctx.strokeRect(0, 0, this.width(), PermissionEntry.HEIGHT); + } + + if (!full) { + const off = this.selected || this._prev_selected ? ctx.getTransform().e : 0; + ctx.clearRect(-off, 0, this.width() + off, PermissionEntry.HEIGHT); + } + + if (this.selected) + ctx.fillStyle = this.colors.permission.background_selected; + else + ctx.fillStyle = this.colors.permission.background; + const off = this.selected ? ctx.getTransform().e : 0; + ctx.fillRect(-off, 0, this.width() + off, PermissionEntry.HEIGHT); + this._prev_selected = this.selected; + + /* permission name */ + { + ctx.fillStyle = typeof (this.value) !== "undefined" ? this.colors.permission.name : this.colors.permission.name_unset; + ctx.textBaseline = "middle"; + ctx.textAlign = "start"; + ctx.font = this.colors.permission.name_font; + + ctx.fillText(this._permission.name, 0, PermissionEntry.HALF_HEIGHT); + } + + const original_y = this.original_y(ctx, 0); + const original_x = this.original_x(ctx, 0); + const width = this.width(); + + /* draw granted */ + let w = width - PermissionEntry.COLUMN_GRANTED; + if (typeof (this.granted) === "number") { + this._listener_grant.region.x = original_x + w; + this._listener_grant.region.y = original_y; + + this._draw_number_field(ctx, this.colors.permission.granted, w, 0, PermissionEntry.COLUMN_VALUE, this.granted, this.flag_grant_hovered); + } else { + this._listener_grant.region.y = original_y; + this._listener_grant.region.x = + original_x + + width + - PermissionEntry.COLUMN_GRANTED; + } + + /* draw value and the skip stuff */ + if (typeof (this.value) === "number") { + w -= PermissionEntry.COLUMN_SKIP + PermissionEntry.COLUMN_PADDING; + { + const x = w + (PermissionEntry.COLUMN_SKIP - PermissionEntry.CHECKBOX_HEIGHT) / 2; + const y = 1; + + this._listener_checkbox_skip.region.x = original_x + x; + this._listener_checkbox_skip.region.y = original_y + y; + + this._draw_checkbox_field(ctx, this.colors.permission.skip, x, y, PermissionEntry.CHECKBOX_HEIGHT, this.flag_skip, this.flag_skip_hovered); } - if(value && value.hasValue()) { - entry.value = value.value; - entry.flag_skip = value.flag_skip; - entry.flag_negate = value.flag_negate; + w -= PermissionEntry.COLUMN_NEGATE + PermissionEntry.COLUMN_PADDING; + { + const x = w + (PermissionEntry.COLUMN_NEGATE - PermissionEntry.CHECKBOX_HEIGHT) / 2; + const y = 1; + + this._listener_checkbox_negate.region.x = original_x + x; + this._listener_checkbox_negate.region.y = original_y + y; + + this._draw_checkbox_field(ctx, this.colors.permission.negate, x, y, PermissionEntry.CHECKBOX_HEIGHT, this.flag_negate, this.flag_negate_hovered); + } + + w -= PermissionEntry.COLUMN_VALUE + PermissionEntry.COLUMN_PADDING; + if (this._permission.is_boolean()) { + const x = w + PermissionEntry.COLUMN_VALUE - PermissionEntry.CHECKBOX_HEIGHT; + const y = 1; + + this._listener_value.region.width = PermissionEntry.CHECKBOX_HEIGHT; + this._listener_value.region.x = original_x + x; + this._listener_value.region.y = original_y + y; + + this._draw_checkbox_field(ctx, this.colors.permission.value_b, x, y, PermissionEntry.CHECKBOX_HEIGHT, this.value > 0, this.flag_value_hovered); + } else if (this._permission.name === "i_icon_id" && this._icon_image) { + this._listener_value.region.x = original_x + w; + this._listener_value.region.y = original_y; + this._listener_value.region.width = PermissionEntry.CHECKBOX_HEIGHT; + + this._draw_icon_field(ctx, this.colors.permission.value_b, w, 0, PermissionEntry.COLUMN_VALUE, this.flag_value_hovered, this._icon_image); + } else { + this._listener_value.region.width = PermissionEntry.COLUMN_VALUE; + this._listener_value.region.x = original_x + w; + this._listener_value.region.y = original_y; + + this._draw_number_field(ctx, this.colors.permission.value, w, 0, PermissionEntry.COLUMN_VALUE, this.value, this.flag_value_hovered); + } + this._listener_value.disabled = false; + } else { + this._listener_checkbox_skip.region.y = -1e8; + this._listener_checkbox_negate.region.y = -1e8; + + this._listener_value.region.y = original_y; + this._listener_value.region.x = + original_x + + width + - PermissionEntry.COLUMN_GRANTED + - PermissionEntry.COLUMN_NEGATE + - PermissionEntry.COLUMN_VALUE + - PermissionEntry.COLUMN_PADDING * 4; + this._listener_value.disabled = true; + } + + this._listener_general.region.y = original_y; + this._listener_general.region.x = original_x; + } + + handle_hide() { + /* so the listener wound get triggered */ + this._listener_value.region.x = -1e8; + this._listener_grant.region.x = -1e8; + this._listener_checkbox_negate.region.x = -1e8; + this._listener_checkbox_skip.region.x = -1e8; + this._listener_general.region.x = -1e8; + } + + private _draw_icon_field(ctx: CanvasRenderingContext2D, scheme: scheme.CheckBox, x: number, y: number, width: number, hovered: boolean, image: HTMLImageElement) { + const line = ctx.lineWidth; + ctx.lineWidth = 2; + ctx.fillStyle = scheme.border; + ctx.strokeRect(x + 1, y + 1, PermissionEntry.HEIGHT - 2, PermissionEntry.HEIGHT - 2); + ctx.lineWidth = line; + + ctx.fillStyle = hovered ? scheme.background_hovered : scheme.background; + ctx.fillRect(x + 1, y + 1, PermissionEntry.HEIGHT - 2, PermissionEntry.HEIGHT - 2); + + const center_y = y + PermissionEntry.HEIGHT / 2; + const center_x = x + PermissionEntry.HEIGHT / 2; + ctx.drawImage(image, center_x - image.width / 2, center_y - image.height / 2); + } + + private _draw_number_field(ctx: CanvasRenderingContext2D, scheme: scheme.TextField, x: number, y: number, width: number, value: number, hovered: boolean) { + ctx.fillStyle = hovered ? scheme.background_hovered : scheme.background; + ctx.fillRect(x, y, width, PermissionEntry.HEIGHT); + + ctx.fillStyle = scheme.color; + ctx.font = scheme.font; //Math.floor(2/3 * PermissionEntry.HEIGHT) + "px Arial"; + ctx.textAlign = "start"; + ctx.fillText(value + "", x, y + PermissionEntry.HALF_HEIGHT, width); + + ctx.strokeStyle = "#6e6e6e"; + const line = ctx.lineWidth; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(x, y + PermissionEntry.HEIGHT - 2); + ctx.lineTo(x + width, y + PermissionEntry.HEIGHT - 2); + ctx.stroke(); + ctx.lineWidth = line; + } + + private _draw_checkbox_field(ctx: CanvasRenderingContext2D, scheme: scheme.CheckBox, x: number, y: number, height: number, checked: boolean, hovered: boolean) { + ctx.fillStyle = scheme.border; + ctx.strokeRect(x, y, height, height); + + + ctx.fillStyle = checked ? + (hovered ? scheme.background_checked_hovered : scheme.background_checked) : + (hovered ? scheme.background_hovered : scheme.background); + ctx.fillRect(x + 1, y + 1, height - 2, height - 2); + + if (checked) { + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillStyle = scheme.checkmark; + ctx.font = scheme.checkmark_font; //Math.floor((5/4) * PermissionEntry.HEIGHT) + "px Arial"; + ctx.fillText("✓", x + height / 2, y + height / 2); + } + } + + height() { + return this.hidden ? 0 : PermissionEntry.HEIGHT; + } + + set_width(value: number) { + super.set_width(value); + this._listener_general.region.width = value; + } + + initialize() { + this._listener_checkbox_skip = { + region: { + x: -1e8, + y: -1e8, + height: PermissionEntry.CHECKBOX_HEIGHT, + width: PermissionEntry.CHECKBOX_HEIGHT + }, + region_weight: 10, + on_mouse_enter: () => { + this.flag_skip_hovered = true; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_mouse_leave: () => { + this.flag_skip_hovered = false; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_click: () => { + this.flag_skip = !this.flag_skip; + if (this.on_change) + this.on_change(); + return RepaintMode.REPAINT_OBJECT_FULL; + }, + set_full_draw: () => this.request_full_draw(), + mouse_cursor: "pointer" + }; + this._listener_checkbox_negate = { + region: { + x: -1e8, + y: -1e8, + height: PermissionEntry.CHECKBOX_HEIGHT, + width: PermissionEntry.CHECKBOX_HEIGHT + }, + region_weight: 10, + on_mouse_enter: () => { + this.flag_negate_hovered = true; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_mouse_leave: () => { + this.flag_negate_hovered = false; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_click: () => { + this.flag_negate = !this.flag_negate; + if (this.on_change) + this.on_change(); + return RepaintMode.REPAINT_OBJECT_FULL; + }, + set_full_draw: () => this.request_full_draw(), + mouse_cursor: "pointer" + }; + this._listener_value = { + region: { + x: -1e8, + y: -1e8, + height: this._permission.is_boolean() ? PermissionEntry.CHECKBOX_HEIGHT : PermissionEntry.HEIGHT, + width: this._permission.is_boolean() ? PermissionEntry.CHECKBOX_HEIGHT : PermissionEntry.COLUMN_VALUE + }, + region_weight: 10, + on_mouse_enter: () => { + this.flag_value_hovered = true; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_mouse_leave: () => { + this.flag_value_hovered = false; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_click: () => { + if (this._permission.is_boolean()) { + this.value = this.value > 0 ? 0 : 1; + if (this.on_change) + this.on_change(); + return RepaintMode.REPAINT_OBJECT_FULL; + } else if (this._permission.name === "i_icon_id") { + this.on_icon_select(this.value).then(value => { + this.value = value; + if (this.on_change) + this.on_change(); + }).catch(error => { + console.warn(tr("Failed to select icon: %o"), error); + }) + } else { + this._spawn_number_edit( + this._listener_value.region.x, + this._listener_value.region.y, + this._listener_value.region.width, + this._listener_value.region.height, + this.colors.permission.value, + this.value || 0, + value => { + if (typeof (value) === "number") { + this.value = value; + this.request_full_draw(); + this.manager.request_draw(false); + if (this.on_change) + this.on_change(); + } + } + ) + } + return RepaintMode.REPAINT_OBJECT_FULL; + }, + set_full_draw: () => this.request_full_draw(), + mouse_cursor: "pointer" + }; + this._listener_grant = { + region: { + x: -1e8, + y: -1e8, + height: PermissionEntry.HEIGHT, + width: PermissionEntry.COLUMN_VALUE + }, + region_weight: 10, + on_mouse_enter: () => { + this.flag_grant_hovered = true; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_mouse_leave: () => { + this.flag_grant_hovered = false; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_click: () => { + this._spawn_number_edit( + this._listener_grant.region.x, + this._listener_grant.region.y, + this._listener_grant.region.width, + this._listener_grant.region.height, + this.colors.permission.granted, + this.granted || 0, //TODO use max assignable value? + value => { + if (typeof (value) === "number") { + this.granted = value; + this.request_full_draw(); + this.manager.request_draw(false); + + if (this.on_grant_change) + this.on_grant_change(); + } + } + ); + return RepaintMode.REPAINT_OBJECT_FULL; + }, + set_full_draw: () => this.request_full_draw(), + mouse_cursor: "pointer" + }; + + this._listener_general = { + region: { + x: -1e8, + y: -1e8, + height: PermissionEntry.HEIGHT, + width: 0 + }, + region_weight: 0, + /* + on_mouse_enter: () => { + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_mouse_leave: () => { + return RepaintMode.REPAINT_OBJECT_FULL; + }, + */ + on_click: (event: InteractionClickEvent) => { + this.manager.set_selected_entry(this); + + if (event.type == ClickEventType.DOUBLE && typeof (this.value) === "undefined") + return this._listener_value.on_click(event); + else if (event.type == ClickEventType.CONTEXT_MENU) { + const mouse = this.manager.mouse; + if (this.on_context_menu) { + this.on_context_menu(mouse.x, mouse.y); + event.consumed = true; + } + } + return RepaintMode.NONE; + }, + set_full_draw: () => this.request_full_draw(), + }; + + this.manager.intercept_manager().register_listener(this._listener_checkbox_negate); + this.manager.intercept_manager().register_listener(this._listener_checkbox_skip); + this.manager.intercept_manager().register_listener(this._listener_value); + this.manager.intercept_manager().register_listener(this._listener_grant); + this.manager.intercept_manager().register_listener(this._listener_general); + } + + finalize() { + } + + private _spawn_number_edit(x: number, y: number, width: number, height: number, color: scheme.TextField, value: number, callback: (new_value?: number) => any) { + const element = $.spawn("div"); + element.prop("contentEditable", true); + element + .css("pointer-events", "none") + .css("background", color.background) + .css("display", "block") + .css("position", "absolute") + .css("top", y) + .css("left", x) + .css("width", width) + .css("height", height) + .css("z-index", 1e6); + element.text(value); + element.appendTo(this.manager.canvas_container); + element.focus(); + + element.on('focusout', event => { + console.log("permission changed to " + element.text()); + if (!isNaN(parseInt(element.text()))) { + callback(parseInt(element.text())); + } else { + callback(undefined); + } + element.remove(); + }); + + element.on('keypress', event => { + if (event.which == KeyCode.KEY_RETURN) + element.trigger('focusout'); + + const text = String.fromCharCode(event.which); + if (isNaN(parseInt(text)) && text != "-") + event.preventDefault(); + + if (element.text().length > 7) + event.preventDefault(); + }); + + if (window.getSelection) { + const selection = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents(element[0]); + selection.removeAllRanges(); + selection.addRange(range); + } + } + + trigger_value_assign() { + this._listener_value.on_click(undefined); + } + + trigger_grant_assign() { + this._listener_grant.on_click(undefined); + } + } + + export class InteractionManager { + private _listeners: InteractionListener[] = []; + private _entered_listeners: InteractionListener[] = []; + + register_listener(listener: InteractionListener) { + this._listeners.push(listener); + } + + remove_listener(listener: InteractionListener) { + this._listeners.remove(listener); + } + + process_mouse_move(new_x: number, new_y: number): { repaint: RepaintMode, cursor: string } { + let _entered_listeners: InteractionListener[] = []; + for (const listener of this._listeners) { + const aabb = listener.region; + + if (listener.disabled) + continue; + + if (new_x < aabb.x || new_x > aabb.x + aabb.width) + continue; + + if (new_y < aabb.y || new_y > aabb.y + aabb.height) + continue; + + _entered_listeners.push(listener); + } + + let repaint: RepaintMode = RepaintMode.NONE; + _entered_listeners.sort((a, b) => (a.region_weight || 0) - (b.region_weight || 0)); + for (const listener of this._entered_listeners) { + if (listener.on_mouse_leave && _entered_listeners.indexOf(listener) == -1) { + let mode = listener.on_mouse_leave(); + if (mode == RepaintMode.REPAINT_OBJECT_FULL) { + mode = RepaintMode.REPAINT; + if (listener.set_full_draw) + listener.set_full_draw(); + } + if (mode > repaint) + repaint = mode; + } + } + for (const listener of _entered_listeners) { + if (listener.on_mouse_enter && this._entered_listeners.indexOf(listener) == -1) { + let mode = listener.on_mouse_enter(); + if (mode == RepaintMode.REPAINT_OBJECT_FULL) { + mode = RepaintMode.REPAINT; + if (listener.set_full_draw) + listener.set_full_draw(); + } + if (mode > repaint) + repaint = mode; + } + } + this._entered_listeners = _entered_listeners; + + let cursor; + for (const listener of _entered_listeners) + if (typeof (listener.mouse_cursor) === "string") { + cursor = listener.mouse_cursor; + } + return { + repaint: repaint, + cursor: cursor + }; + } + + private process_click_event(x: number, y: number, event: InteractionClickEvent): RepaintMode { + const move_result = this.process_mouse_move(x, y); + + let repaint: RepaintMode = move_result.repaint; + for (const listener of this._entered_listeners) + if (listener.on_click) { + let mode = listener.on_click(event); + if (mode == RepaintMode.REPAINT_OBJECT_FULL) { + mode = RepaintMode.REPAINT; + if (listener.set_full_draw) + listener.set_full_draw(); + } + if (mode > repaint) + repaint = mode; + } + + return repaint; + } + + process_click(x: number, y: number): RepaintMode { + const event: InteractionClickEvent = { + consumed: false, + type: ClickEventType.SIGNLE, + offset_x: x, + offset_y: y + }; + + return this.process_click_event(x, y, event); + } + + process_dblclick(x: number, y: number): RepaintMode { + const event: InteractionClickEvent = { + consumed: false, + type: ClickEventType.DOUBLE, + offset_x: x, + offset_y: y + }; + + return this.process_click_event(x, y, event); + } + + process_context_menu(js_event: MouseEvent, x: number, y: number): RepaintMode { + const event: InteractionClickEvent = { + consumed: js_event.defaultPrevented, + type: ClickEventType.CONTEXT_MENU, + offset_x: x, + offset_y: y + }; + + const result = this.process_click_event(x, y, event); + if (event.consumed) + js_event.preventDefault(); + return result; + } + } + + export class PermissionEditor { + private static readonly PERMISSION_HEIGHT = PermissionEntry.HEIGHT; + private static readonly PERMISSION_GROUP_HEIGHT = PermissionGroup.HEIGHT; + + readonly grouped_permissions: GroupedPermissions[]; + readonly canvas: HTMLCanvasElement; + readonly canvas_container: HTMLDivElement; + private _max_height: number = 0; + + private _permission_count: number = 0; + private _permission_group_count: number = 0; + private _canvas_context: CanvasRenderingContext2D; + + private _selected_entry: PermissionEntry; + + private _draw_requested: boolean = false; + private _draw_requested_full: boolean = false; + + private _elements: PermissionGroup[] = []; + private _intersect_manager: InteractionManager; + + private _permission_entry_map: { [key: number]: PermissionEntry } = {}; + + mouse: { + x: number, + y: number + } = { + x: 0, + y: 0 + }; + + constructor(permissions: GroupedPermissions[]) { + this.grouped_permissions = permissions; + + this.canvas_container = $.spawn("div") + .addClass("window-resize-listener") /* we want to handle resized */ + .css("min-width", "750px") + .css("position", "relative") + .css("user-select", "none") + [0]; + this.canvas = $.spawn("canvas")[0]; + + this.canvas_container.appendChild(this.canvas); + + this._intersect_manager = new InteractionManager(); + this.canvas_container.onmousemove = event => { + this.mouse.x = event.pageX; + this.mouse.y = event.pageY; + + const draw = this._intersect_manager.process_mouse_move(event.offsetX, event.offsetY); + this.canvas_container.style.cursor = draw.cursor || ""; + this._handle_repaint(draw.repaint); + }; + this.canvas_container.onclick = event => { + this._handle_repaint(this._intersect_manager.process_click(event.offsetX, event.offsetY)); + }; + this.canvas_container.ondblclick = event => { + this._handle_repaint(this._intersect_manager.process_dblclick(event.offsetX, event.offsetY)); + }; + this.canvas_container.oncontextmenu = (event: MouseEvent) => { + this._handle_repaint(this._intersect_manager.process_context_menu(event, event.offsetX, event.offsetY)); + }; + this.canvas_container.onresize = () => this.request_draw(true); + + + this.initialize(); + } + + private _handle_repaint(mode: RepaintMode) { + if (mode == RepaintMode.REPAINT || mode == RepaintMode.REPAINT_FULL) + this.request_draw(mode == RepaintMode.REPAINT_FULL); + } + + request_draw(full?: boolean) { + this._draw_requested_full = this._draw_requested_full || full; + if (this._draw_requested) + return; + this._draw_requested = true; + requestAnimationFrame(() => { + this.draw(this._draw_requested_full); + }); + } + + draw(full?: boolean) { + this._draw_requested = false; + this._draw_requested_full = false; + + /* clear max height */ + this.canvas_container.style.overflowY = "shown"; + this.canvas_container.style.height = undefined; + + const max_height = this._max_height; + const max_width = this.canvas_container.clientWidth; + const update_width = this.canvas.width != max_width; + const full_draw = typeof (full) !== "boolean" || full || update_width; + + if (update_width) { + this.canvas.width = max_width; + for (const element of this._elements) + element.set_width(max_width); + } + + console.log("Drawing%s on %dx%d", full_draw ? " full" : "", max_width, max_height); + if (full_draw) + this.canvas.height = max_height; + const ctx = this._canvas_context; + ctx.resetTransform(); + if (full_draw) + ctx.clearRect(0, 0, max_width, max_height); + + let sum_height = 0; + for (const element of this._elements) { + element.draw(ctx, full_draw); + const height = element.height(); + sum_height += height; + ctx.translate(0, height); + } + + this.canvas_container.style.overflowY = "hidden"; + this.canvas_container.style.height = sum_height + "px"; + } + + private initialize() { + /* setup the canvas */ + { + const apply_group = (group: GroupedPermissions) => { + for (const g of group.children || []) + apply_group(g); + this._permission_group_count++; + this._permission_count += group.permissions.length; + }; + for (const group of this.grouped_permissions) + apply_group(group); + + this._max_height = this._permission_count * PermissionEditor.PERMISSION_HEIGHT + this._permission_group_count * PermissionEditor.PERMISSION_GROUP_HEIGHT; + console.log("%d permissions and %d groups required %d height", this._permission_count, this._permission_group_count, this._max_height); + + this.canvas.style.width = "100%"; + + this.canvas.style.flexShrink = "0"; + this.canvas_container.style.flexShrink = "0"; + + this._canvas_context = this.canvas.getContext("2d"); + } + + const font = Math.floor(2 / 3 * PermissionEntry.HEIGHT) + "px Arial"; + const font_checkmark = Math.floor((5 / 4) * PermissionEntry.HEIGHT) + "px Arial"; + const checkbox = { + background: "#303036", + background_hovered: "#CCCCCC", + + background_checked: "#0000AA", + background_checked_hovered: "#0000AA77", + + border: "#000000", + checkmark: "#303036", + checkmark_font: font_checkmark + }; + const input: scheme.TextField = { + color: "#000000", + font: font, + + background_hovered: "#CCCCCCCC", + background: "#30303600" + }; + + const color_scheme: scheme.ColorScheme = { + group: { + name: "#808080", + name_font: font + }, + //#28282c + permission: { + name: "#808080", + name_unset: "#1a1a1a", + name_font: font, + + background: "#303036", + background_selected: "#00007788", + + value: input, + value_b: checkbox, + granted: input, + negate: checkbox, + skip: checkbox + } + }; + (window as any).scheme = color_scheme; + /* setup elements to draw */ + { + const process_group = (group: PermissionGroup) => { + for (const permission of group._element_permissions.permissions) + this._permission_entry_map[permission.permission().id] = permission; + for (const g of group._sub_elements) + process_group(g); + }; + + for (const group of this.grouped_permissions) { + const element = new PermissionGroup(group); + element.set_color_scheme(color_scheme); + element.set_manager(this); + process_group(element); + this._elements.push(element); + } + for (const element of this._elements) { + element.initialize(); + } + } + } + + intercept_manager() { + return this._intersect_manager; + } + + set_selected_entry(entry?: PermissionEntry) { + if (this._selected_entry === entry) + return; + + if (this._selected_entry) { + this._selected_entry.selected = false; + this._selected_entry.request_full_draw(); + } + this._selected_entry = entry; + if (this._selected_entry) { + this._selected_entry.selected = true; + this._selected_entry.request_full_draw(); + } + this.request_draw(false); + } + + permission_entries(): PermissionEntry[] { + return Object.keys(this._permission_entry_map).map(e => this._permission_entry_map[e]); + } + + collapse_all() { + for (const group of this._elements) + group.collapse_group(); + this.request_draw(true); + } + + expend_all() { + for (const group of this._elements) + group.expend_group(); + this.request_draw(true); + } + } +} + +export class CanvasPermissionEditor extends AbstractPermissionEditor { + private container: JQuery; + + private mode_container_permissions: JQuery; + private mode_container_error_permission: JQuery; + private mode_container_unset: JQuery; + + /* references within the container tag */ + private permission_value_map: {[key:number]:PermissionValue} = {}; + + private entry_editor: ui.PermissionEditor; + + icon_resolver: (id: number) => Promise; + icon_selector: (current_id: number) => Promise; + + constructor() { + super(); + } + + initialize(permissions: GroupedPermissions[]) { + this._permissions = permissions; + this.entry_editor = new ui.PermissionEditor(permissions); + this.build_tag(); + } + + html_tag() { return this.container; } + + private build_tag() { + this.container = $("#tmpl_permission_editor_canvas").renderTag(); + /* search for that as long we've not that much nodes */ + this.mode_container_permissions = this.container.find(".container-mode-permissions"); + this.mode_container_error_permission = this.container.find(".container-mode-no-permissions"); + this.mode_container_unset = this.container.find(".container-mode-unset"); + this.set_mode(PermissionEditorMode.UNSET); + + /* the filter */ + { + const tag_filter_input = this.container.find(".filter-input"); + const tag_filter_granted = this.container.find(".filter-granted"); + + tag_filter_granted.on('change', event => tag_filter_input.trigger('change')); + tag_filter_input.on('keyup change', event => { + let filter_mask = tag_filter_input.val() as string; + let req_granted = tag_filter_granted.prop("checked"); + + + for(const entry of this.entry_editor.permission_entries()) { + const permission = entry.permission(); + + let shown = filter_mask.length == 0 || permission.name.indexOf(filter_mask) != -1; + if(shown && req_granted) { + const value: PermissionValue = this.permission_value_map[permission.id]; + shown = value && (value.hasValue() || value.hasGrant()); + } + + entry.hidden = !shown; + } + this.entry_editor.request_draw(true); + }); + } + + /* update button */ + { + this.container.find(".button-update").on('click', this.trigger_update.bind(this)); + } + + /* global context menu listener */ + { + this.container.on('contextmenu', event => { + if(event.isDefaultPrevented()) return; + event.preventDefault(); + + /* TODO allow collapse and expend all */ + }); + } + + { + const tag_container = this.container.find(".entry-editor-container"); + tag_container.append(this.entry_editor.canvas_container); + + tag_container.parent().on('contextmenu', event => { + if(event.isDefaultPrevented()) return; + event.preventDefault(); + + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Expend all"), + callback: () => this.entry_editor.expend_all() + }, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Collapse all"), + callback: () => this.entry_editor.collapse_all() + }); + }); + } + + /* setup the permissions */ + for(const entry of this.entry_editor.permission_entries()) { + const permission = entry.permission(); + entry.on_change = () => { + const flag_remove = typeof(entry.value) !== "number"; + this._listener_change(permission, { + remove: flag_remove, + flag_negate: entry.flag_negate, + flag_skip: entry.flag_skip, + value: flag_remove ? -2 : entry.value + }).then(() => { + if(flag_remove) { + const element = this.permission_value_map[permission.id]; + if(!element) return; /* This should never happen, if so how are we displaying this permission?! */ + + element.value = undefined; + element.flag_negate = false; + element.flag_skip = false; + } else { + const element = this.permission_value_map[permission.id] || (this.permission_value_map[permission.id] = new PermissionValue(permission)); + + element.value = entry.value; + element.flag_skip = entry.flag_skip; + element.flag_negate = entry.flag_negate; + } + if(permission.name === "i_icon_id") { - this.icon_resolver(value.value).then(e => { + this.icon_resolver(entry.value).then(e => { entry.set_icon_id_image(e); entry.request_full_draw(); this.entry_editor.request_draw(false); @@ -1588,39 +1455,178 @@ namespace pe { console.warn(tr("Failed to load icon for permission editor: %o"), error); }); } + entry.request_full_draw(); + this.entry_editor.request_draw(false); + }).catch(() => { + const element = this.permission_value_map[permission.id]; + + entry.value = element && element.hasValue() ? element.value : undefined; + entry.flag_skip = element && element.flag_skip; + entry.flag_negate = element && element.flag_negate; + + entry.request_full_draw(); + this.entry_editor.request_draw(false); + }); + }; + + entry.on_grant_change = () => { + const flag_remove = typeof(entry.granted) !== "number"; + + this._listener_change(permission, { + remove: flag_remove, + granted: flag_remove ? -2 : entry.granted, + }).then(() => { + if(flag_remove) { + const element = this.permission_value_map[permission.id]; + if (!element) return; /* This should never happen, if so how are we displaying this permission?! */ + + element.granted_value = undefined; + } else { + const element = this.permission_value_map[permission.id] || (this.permission_value_map[permission.id] = new PermissionValue(permission)); + element.granted_value = entry.granted; + } + entry.request_full_draw(); + this.entry_editor.request_draw(false); + }).catch(() => { + const element = this.permission_value_map[permission.id]; + + entry.granted = element && element.hasGrant() ? element.granted_value : undefined; + entry.request_full_draw(); + this.entry_editor.request_draw(false); + }); + }; + + entry.on_context_menu = (x, y) => { + let entries: contextmenu.MenuEntry[] = []; + if(typeof(entry.value) === "undefined") { + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Add permission"), + callback: () => entry.trigger_value_assign() + }); } else { - entry.value = undefined; - entry.flag_skip = false; - entry.flag_negate = false; + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Remove permission"), + callback: () => { + entry.value = undefined; + entry.on_change(); + } + }); } - if(value && value.hasGrant()) { - entry.granted = value.granted_value; + if(typeof(entry.granted) === "undefined") { + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Add grant permission"), + callback: () => entry.trigger_grant_assign() + }); } else { - entry.granted = undefined; + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Remove grant permission"), + callback: () => { + entry.granted = undefined; + entry.on_grant_change(); + } + }); } + entries.push(contextmenu.Entry.HR()); + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Expend all"), + callback: () => this.entry_editor.expend_all() + }); + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Collapse all"), + callback: () => this.entry_editor.collapse_all() + }); + entries.push(contextmenu.Entry.HR()); + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Show permission description"), + callback: () => { + createInfoModal( + tr("Permission description"), + tr("Permission description for permission ") + permission.name + ":
" + permission.description + ).open(); + } + }); + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Copy permission name"), + callback: () => { + copy_to_clipboard(permission.name); + } + }); + + contextmenu.spawn_context_menu(x, y, ...entries); } - this.entry_editor.request_draw(true); - } - - set_mode(mode: Modals.PermissionEditorMode) { - this.mode_container_permissions.css('display', mode == Modals.PermissionEditorMode.VISIBLE ? 'flex' : 'none'); - this.mode_container_error_permission.css('display', mode == Modals.PermissionEditorMode.NO_PERMISSION ? 'flex' : 'none'); - this.mode_container_unset.css('display', mode == Modals.PermissionEditorMode.UNSET ? 'block' : 'none'); - if(mode == Modals.PermissionEditorMode.VISIBLE) - this.entry_editor.draw(true); - } - - update_ui() { - this.entry_editor.draw(true); - } - - set_toggle_button(callback: () => string, initial: string) { - throw "not implemented"; - } - - set_hidden_permissions(permissions: PermissionType[]) { - //TODO: Stuff here } } + + set_permissions(permissions?: PermissionValue[]) { + permissions = permissions || []; + this.permission_value_map = {}; + + for(const permission of permissions) + this.permission_value_map[permission.type.id] = permission; + + for(const entry of this.entry_editor.permission_entries()) { + const permission = entry.permission(); + const value: PermissionValue = this.permission_value_map[permission.id]; + + if(permission.name === "i_icon_id") { + entry.set_icon_id_image(undefined); + entry.on_icon_select = this.icon_selector; + } + + if(value && value.hasValue()) { + entry.value = value.value; + entry.flag_skip = value.flag_skip; + entry.flag_negate = value.flag_negate; + if(permission.name === "i_icon_id") { + this.icon_resolver(value.value).then(e => { + entry.set_icon_id_image(e); + entry.request_full_draw(); + this.entry_editor.request_draw(false); + }).catch(error => { + console.warn(tr("Failed to load icon for permission editor: %o"), error); + }); + } + } else { + entry.value = undefined; + entry.flag_skip = false; + entry.flag_negate = false; + } + + if(value && value.hasGrant()) { + entry.granted = value.granted_value; + } else { + entry.granted = undefined; + } + } + this.entry_editor.request_draw(true); + } + + set_mode(mode: PermissionEditorMode) { + this.mode_container_permissions.css('display', mode == PermissionEditorMode.VISIBLE ? 'flex' : 'none'); + this.mode_container_error_permission.css('display', mode == PermissionEditorMode.NO_PERMISSION ? 'flex' : 'none'); + this.mode_container_unset.css('display', mode == PermissionEditorMode.UNSET ? 'block' : 'none'); + if(mode == PermissionEditorMode.VISIBLE) + this.entry_editor.draw(true); + } + + update_ui() { + this.entry_editor.draw(true); + } + + set_toggle_button(callback: () => string, initial: string) { + throw "not implemented"; + } + + set_hidden_permissions(permissions: PermissionType[]) { + //TODO: Stuff here + } } \ No newline at end of file diff --git a/shared/js/ui/modal/permission/HTMLPermissionEditor.ts b/shared/js/ui/modal/permission/HTMLPermissionEditor.ts index f1400da9..262a5e7d 100644 --- a/shared/js/ui/modal/permission/HTMLPermissionEditor.ts +++ b/shared/js/ui/modal/permission/HTMLPermissionEditor.ts @@ -1,530 +1,318 @@ /// /* first needs the AbstractPermissionEdit */ -namespace pe { - class HTMLPermission { - readonly handle: HTMLPermissionEditor; - readonly group: HTMLPermissionGroup; - readonly permission: PermissionInfo; - readonly index: number; - - tag: JQuery; - tag_name: JQuery; - tag_container_value: JQuery; - tag_container_granted: JQuery; - tag_container_skip: JQuery; - tag_container_negate: JQuery; - - hidden: boolean; - - /* the "actual" values */ - private _mask = 0; /* fourth bit: hidden by filer | third bit: value type | second bit: grant shown | first bit: value shown */ - - private _tag_value: JQuery; - private _tag_value_input: JQuery; - - private _tag_granted: JQuery; - private _tag_granted_input: JQuery; - - private _tag_skip: JQuery; - private _tag_skip_input: JQuery; - - private _tag_negate: JQuery; - private _tag_negate_input: JQuery; - - private _value: number | undefined; - private _grant: number | undefined; - private flags: number; /* 0x01 := Skip | 0x02 := Negate */ - - constructor(handle: HTMLPermissionEditor, group: HTMLPermissionGroup, permission: PermissionInfo, index: number) { - this.handle = handle; - this.permission = permission; - this.index = index; - this.group = group; - - this.build_tag(); - } - - private static build_checkbox() : {tag: JQuery, input: JQuery} { - let tag, input; - tag = $.spawn("label").addClass("switch").append([ - input = $.spawn("input").attr("type", "checkbox"), - $.spawn("span").addClass("slider").append( - $.spawn("div").addClass("dot") - ) - ]); - return {tag: tag, input: input}; - } - - private static number_filter_re = /^[-+]?([0-9]{0,9})$/; - private static number_filter = (event: KeyboardEvent) => { - if(event.ctrlKey) - return; - - const target = event.target; - if(event.key === "Enter") { - target.blur(); - return; - } - - if('keyCode' in event) { - /* everything under 46 is a control key except 32 its space */ - if(event.keyCode < 46 && event.keyCode != 32) - return; - - if(!HTMLPermission.number_filter_re.test(target.value + String.fromCharCode(event.keyCode))) { - event.preventDefault(); - return; - } - } else { - const e = event; /* for some reason typescript deducts the event type to "never" */ - if(!HTMLPermission.number_filter_re.test(e.key)) { - e.preventDefault(); - return; - } - } - }; - - private build_tag() { - this.tag = $.spawn("div").addClass("entry permission").css('padding-left', this.index + "em").append([ - this.tag_name = $.spawn("div").addClass("column-name").text(this.permission.name), - this.tag_container_value = $.spawn("div").addClass("column-value"), - this.tag_container_skip = $.spawn("div").addClass("column-skip"), - this.tag_container_negate = $.spawn("div").addClass("column-negate"), - this.tag_container_granted = $.spawn("div").addClass("column-granted") - ]); - - if(this.permission.is_boolean()) { - let value = HTMLPermission.build_checkbox(); - this._tag_value = value.tag; - this._tag_value_input = value.input; - - this._tag_value_input.on('change', event => { - const value = this._tag_value_input.prop('checked') ? 1 : 0; - - this.handle.trigger_change(this.permission, { - remove: false, - - value: value, - flag_skip: (this.flags & 0x01) > 0, - flag_negate: (this.flags & 0x02) > 0 - }).then(() => { - this._value = value; - }).catch(error => { - this._reset_value(); - }); - }); - - this._mask |= 0x04; - } else { - this._tag_value = $.spawn("input").addClass("number"); - this._tag_value_input = this._tag_value; - - this._tag_value_input.on('keydown', HTMLPermission.number_filter as any); - this._tag_value_input.on('change', event => { - const str_value = this._tag_value_input.val() as string; - const value = parseInt(str_value); - if(!HTMLPermission.number_filter_re.test(str_value) || isNaN(value)) { - console.warn(tr("Failed to parse given permission value string: %s"), this._tag_value_input.val()); - this._reset_value(); - return; - } - - this.handle.trigger_change(this.permission, { - remove: false, - - value: value, - flag_skip: (this.flags & 0x01) > 0, - flag_negate: (this.flags & 0x02) > 0 - }).then(() => { - this._value = value; - this._update_active_class(); - }).catch(error => { - this._reset_value(); - }); - }); - } - - { - let skip = HTMLPermission.build_checkbox(); - this._tag_skip = skip.tag; - this._tag_skip_input = skip.input; - - this._tag_skip_input.on('change', event => { - const value = this._tag_skip_input.prop('checked'); - - this.handle.trigger_change(this.permission, { - remove: false, - - value: this._value, - flag_skip: value, - flag_negate: (this.flags & 0x02) > 0 - }).then(() => { - if(value) - this.flags |= 0x01; - else - this.flags &= ~0x1; - this._update_active_class(); - }).catch(error => { - this._reset_value(); - }); - }); - } - - { - let negate = HTMLPermission.build_checkbox(); - this._tag_negate = negate.tag; - this._tag_negate_input = negate.input; - - this._tag_negate_input.on('change', event => { - const value = this._tag_negate_input.prop('checked'); - - console.log("Negate value: %o", value); - this.handle.trigger_change(this.permission, { - remove: false, - - value: this._value, - flag_skip: (this.flags & 0x01) > 0, - flag_negate: value - }).then(() => { - if(value) - this.flags |= 0x02; - else - this.flags &= ~0x2; - this._update_active_class(); - }).catch(error => { - this._reset_value(); - }); - }); - } - - { - this._tag_granted = $.spawn("input").addClass("number"); - this._tag_granted_input = this._tag_granted; - - this._tag_granted_input.on('keydown', HTMLPermission.number_filter as any); - this._tag_granted_input.on('change', event => { - const str_value = this._tag_granted_input.val() as string; - const value = parseInt(str_value); - if(!HTMLPermission.number_filter_re.test(str_value) || Number.isNaN(value)) { - console.warn(tr("Failed to parse given permission granted value string: %s"), this._tag_granted_input.val()); - this._reset_value(); - return; - } - - this.handle.trigger_change(this.permission, { - remove: false, - - granted: value - }).then(() => { - this._grant = value; - this._update_active_class(); - }).catch(error => { - this._reset_grant(); - }); - }); - } - - /* double click handler */ - { - this.tag.on('dblclick', event => this._trigger_value_assign()) - } - - /* context menu */ - { - this.tag.on('contextmenu', event => { - if(event.isDefaultPrevented()) - return; - event.preventDefault(); - - let entries: contextmenu.MenuEntry[] = []; - if(typeof(this._value) === "undefined") { - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Add permission"), - callback: () => this._trigger_value_assign() - }); - } else { - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Remove permission"), - callback: () => { - this.handle.trigger_change(this.permission, { - remove: true, - value: 0 - }).then(() => { - this.value(undefined); - }).catch(error => { - //We have to do nothing - }); - } - }); - } - - if(typeof(this._grant) === "undefined") { - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Add grant permission"), - callback: () => this._trigger_grant_assign() - }); - } else { - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Remove grant permission"), - callback: () => { - this.handle.trigger_change(this.permission, { - remove: true, - granted: 0 - }).then(() => { - this.granted(undefined); - }).catch(error => { - //We have to do nothing - }); - } - }); - } - entries.push(contextmenu.Entry.HR()); - if(this.group.collapsed) - entries.push({ /* This could never happen! */ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Expend group"), - callback: () => this.group.expend() - }); - else - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Collapse group"), - callback: () => this.group.collapse() - }); - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Expend all"), - callback: () => this.handle.expend_all() - }); - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Collapse all"), - callback: () => this.handle.collapse_all() - }); - entries.push(contextmenu.Entry.HR()); - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Show permission description"), - callback: () => { - createInfoModal( - tr("Permission description"), - tr("Permission description for permission ") + this.permission.name + ":
" + this.permission.description - ).open(); - } - }); - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Copy permission name"), - callback: () => { - copy_to_clipboard(this.permission.name); - } - }); - - contextmenu.spawn_context_menu(event.pageX, event.pageY, ...entries); - }); - } - } - - private _trigger_value_assign() { - if(typeof(this._value) === "undefined") - this.value(this._grant || 1, false, false); //TODO: Use max granted value? - this._tag_value_input.focus(); - if(this.permission.is_boolean()) - this._tag_value_input.trigger('change'); - } - - private _trigger_grant_assign() { - this.granted(1); //TODO: Use max granted value? - this._tag_granted_input.focus(); - } - - hide() { - this._mask &= ~0x08; - for(const element of this.tag) - (element).style.display = 'none'; - } - - show() { - this._mask |= 0x08; - for(const element of this.tag) - (element).style.display = 'flex'; - } - - is_filtered() : boolean { - return (this._mask & 0x10) > 0; - } - - set_filtered(flag: boolean) { - if(flag) - this._mask |= 0x10; - else - this._mask &= ~0x10; - } - - is_set() : boolean { - return (this._mask & 0x03) > 0; - } - - get_value() { return this._value; } - - value(value: number | undefined, skip?: boolean, negate?: boolean) { - if(typeof value === "undefined") { - this._tag_value.detach(); - this._tag_negate.detach(); - this._tag_skip.detach(); - - this._value = undefined; - this.flags = 0; - - this._update_active_class(); - this._mask &= ~0x1; - return; - } - - if((this._mask & 0x1) == 0) { - this._tag_value.appendTo(this.tag_container_value); - this._tag_negate.appendTo(this.tag_container_negate); - this._tag_skip.appendTo(this.tag_container_skip); - - this._update_active_class(); - this._mask |= 0x01; - } - - if((this._mask & 0x04) > 0) - this._tag_value_input.prop('checked', !!value); - else - this._tag_value_input.val(value); - this._tag_skip_input.prop('checked', !!skip); - this._tag_negate_input.prop('checked', !!negate); - - this._value = value; - this.flags = (!!skip ? 0x01 : 0) | (!!negate ? 0x2 : 0); - } - - granted(value: number | undefined) { - if(typeof value === "undefined") { - this._tag_granted.detach(); - - this._update_active_class(); - this._grant = undefined; - this._mask &= ~0x2; - return; - } - - if((this._mask & 0x2) == 0) { - this._mask |= 0x02; - this._tag_granted.appendTo(this.tag_container_granted); - this._update_active_class(); - } - this._tag_granted_input.val(value); - this._grant = value; - } - - reset() { - this._mask &= ~0x03; - - this._tag_value.detach(); - this._tag_negate.detach(); - this._tag_skip.detach(); - - this._tag_granted.detach(); - - this._value = undefined; - this._grant = undefined; - this.flags = 0; - - const tag = this.tag[0] as HTMLDivElement; - tag.classList.remove("active"); - } - - private _reset_value() { - if(typeof(this._value) === "undefined") { - if((this._mask & 0x1) != 0) - this.value(undefined); - } else { - this.value(this._value, (this.flags & 0x1) > 1, (this.flags & 0x2) > 1); - } - } - - private _reset_grant() { - if(typeof(this._grant) === "undefined") { - if((this._mask & 0x2) != 0) - this.granted(undefined); - } else { - this.granted(this._grant); - } - } - - private _update_active_class() { - const value = typeof(this._value) !== "undefined" || typeof(this._grant) !== "undefined"; - const tag = this.tag[0] as HTMLDivElement; - if(value) - tag.classList.add("active"); - else - tag.classList.remove("active"); - } +import { + GroupedPermissions, + PermissionGroup, + PermissionInfo, + PermissionValue +} from "tc-shared/permission/PermissionManager"; +import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; +import {createInfoModal} from "tc-shared/ui/elements/Modal"; +import {copy_to_clipboard} from "tc-shared/utils/helpers"; +import PermissionType from "tc-shared/permission/PermissionType"; +import {IconManager} from "tc-shared/FileManager"; +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; +import { + AbstractPermissionEditor, + ChangedPermissionValue, + PermissionEditorMode +} from "tc-shared/ui/modal/permission/AbstractPermissionEditor"; + +class HTMLPermission { + readonly handle: HTMLPermissionEditor; + readonly group: HTMLPermissionGroup; + readonly permission: PermissionInfo; + readonly index: number; + + tag: JQuery; + tag_name: JQuery; + tag_container_value: JQuery; + tag_container_granted: JQuery; + tag_container_skip: JQuery; + tag_container_negate: JQuery; + + hidden: boolean; + + /* the "actual" values */ + private _mask = 0; /* fourth bit: hidden by filer | third bit: value type | second bit: grant shown | first bit: value shown */ + + private _tag_value: JQuery; + private _tag_value_input: JQuery; + + private _tag_granted: JQuery; + private _tag_granted_input: JQuery; + + private _tag_skip: JQuery; + private _tag_skip_input: JQuery; + + private _tag_negate: JQuery; + private _tag_negate_input: JQuery; + + private _value: number | undefined; + private _grant: number | undefined; + private flags: number; /* 0x01 := Skip | 0x02 := Negate */ + + constructor(handle: HTMLPermissionEditor, group: HTMLPermissionGroup, permission: PermissionInfo, index: number) { + this.handle = handle; + this.permission = permission; + this.index = index; + this.group = group; + + this.build_tag(); } - class HTMLPermissionGroup { - readonly handle: HTMLPermissionEditor; - readonly group: PermissionGroup; - readonly index: number; + private static build_checkbox() : {tag: JQuery, input: JQuery} { + let tag, input; + tag = $.spawn("label").addClass("switch").append([ + input = $.spawn("input").attr("type", "checkbox"), + $.spawn("span").addClass("slider").append( + $.spawn("div").addClass("dot") + ) + ]); + return {tag: tag, input: input}; + } - private _tag_arrow: JQuery; + private static number_filter_re = /^[-+]?([0-9]{0,9})$/; + private static number_filter = (event: KeyboardEvent) => { + if(event.ctrlKey) + return; - permissions: HTMLPermission[] = []; - children: HTMLPermissionGroup[] = []; - - tag: JQuery; - visible: boolean; - - collapsed: boolean; - parent_collapsed: boolean; - - constructor(handle: HTMLPermissionEditor, group: PermissionGroup, index: number) { - this.handle = handle; - this.group = group; - this.index = index; - - this._build_tag(); + const target = event.target; + if(event.key === "Enter") { + target.blur(); + return; } - private _build_tag() { - this.tag = $.spawn("div").addClass("entry group").css('padding-left', this.index + "em").append([ - $.spawn("div").addClass("column-name").append([ - this._tag_arrow = $.spawn("div").addClass("arrow down"), - $.spawn("div").addClass("group-name").text(this.group.name) - ]), - $.spawn("div").addClass("column-value"), - $.spawn("div").addClass("column-skip"), - $.spawn("div").addClass("column-negate"), - $.spawn("div").addClass("column-granted") - ]); + if('keyCode' in event) { + /* everything under 46 is a control key except 32 its space */ + if(event.keyCode < 46 && event.keyCode != 32) + return; + if(!HTMLPermission.number_filter_re.test(target.value + String.fromCharCode(event.keyCode))) { + event.preventDefault(); + return; + } + } else { + const e = event; /* for some reason typescript deducts the event type to "never" */ + if(!HTMLPermission.number_filter_re.test(e.key)) { + e.preventDefault(); + return; + } + } + }; + + private build_tag() { + this.tag = $.spawn("div").addClass("entry permission").css('padding-left', this.index + "em").append([ + this.tag_name = $.spawn("div").addClass("column-name").text(this.permission.name), + this.tag_container_value = $.spawn("div").addClass("column-value"), + this.tag_container_skip = $.spawn("div").addClass("column-skip"), + this.tag_container_negate = $.spawn("div").addClass("column-negate"), + this.tag_container_granted = $.spawn("div").addClass("column-granted") + ]); + + if(this.permission.is_boolean()) { + let value = HTMLPermission.build_checkbox(); + this._tag_value = value.tag; + this._tag_value_input = value.input; + + this._tag_value_input.on('change', event => { + const value = this._tag_value_input.prop('checked') ? 1 : 0; + + this.handle.trigger_change(this.permission, { + remove: false, + + value: value, + flag_skip: (this.flags & 0x01) > 0, + flag_negate: (this.flags & 0x02) > 0 + }).then(() => { + this._value = value; + }).catch(error => { + this._reset_value(); + }); + }); + + this._mask |= 0x04; + } else { + this._tag_value = $.spawn("input").addClass("number"); + this._tag_value_input = this._tag_value; + + this._tag_value_input.on('keydown', HTMLPermission.number_filter as any); + this._tag_value_input.on('change', event => { + const str_value = this._tag_value_input.val() as string; + const value = parseInt(str_value); + if(!HTMLPermission.number_filter_re.test(str_value) || isNaN(value)) { + console.warn(tr("Failed to parse given permission value string: %s"), this._tag_value_input.val()); + this._reset_value(); + return; + } + + this.handle.trigger_change(this.permission, { + remove: false, + + value: value, + flag_skip: (this.flags & 0x01) > 0, + flag_negate: (this.flags & 0x02) > 0 + }).then(() => { + this._value = value; + this._update_active_class(); + }).catch(error => { + this._reset_value(); + }); + }); + } + + { + let skip = HTMLPermission.build_checkbox(); + this._tag_skip = skip.tag; + this._tag_skip_input = skip.input; + + this._tag_skip_input.on('change', event => { + const value = this._tag_skip_input.prop('checked'); + + this.handle.trigger_change(this.permission, { + remove: false, + + value: this._value, + flag_skip: value, + flag_negate: (this.flags & 0x02) > 0 + }).then(() => { + if(value) + this.flags |= 0x01; + else + this.flags &= ~0x1; + this._update_active_class(); + }).catch(error => { + this._reset_value(); + }); + }); + } + + { + let negate = HTMLPermission.build_checkbox(); + this._tag_negate = negate.tag; + this._tag_negate_input = negate.input; + + this._tag_negate_input.on('change', event => { + const value = this._tag_negate_input.prop('checked'); + + console.log("Negate value: %o", value); + this.handle.trigger_change(this.permission, { + remove: false, + + value: this._value, + flag_skip: (this.flags & 0x01) > 0, + flag_negate: value + }).then(() => { + if(value) + this.flags |= 0x02; + else + this.flags &= ~0x2; + this._update_active_class(); + }).catch(error => { + this._reset_value(); + }); + }); + } + + { + this._tag_granted = $.spawn("input").addClass("number"); + this._tag_granted_input = this._tag_granted; + + this._tag_granted_input.on('keydown', HTMLPermission.number_filter as any); + this._tag_granted_input.on('change', event => { + const str_value = this._tag_granted_input.val() as string; + const value = parseInt(str_value); + if(!HTMLPermission.number_filter_re.test(str_value) || Number.isNaN(value)) { + console.warn(tr("Failed to parse given permission granted value string: %s"), this._tag_granted_input.val()); + this._reset_value(); + return; + } + + this.handle.trigger_change(this.permission, { + remove: false, + + granted: value + }).then(() => { + this._grant = value; + this._update_active_class(); + }).catch(error => { + this._reset_grant(); + }); + }); + } + + /* double click handler */ + { + this.tag.on('dblclick', event => this._trigger_value_assign()) + } + + /* context menu */ + { this.tag.on('contextmenu', event => { if(event.isDefaultPrevented()) return; event.preventDefault(); - const entries: contextmenu.MenuEntry[] = []; - if(this.collapsed) + let entries: contextmenu.MenuEntry[] = []; + if(typeof(this._value) === "undefined") { entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Add permission"), + callback: () => this._trigger_value_assign() + }); + } else { + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Remove permission"), + callback: () => { + this.handle.trigger_change(this.permission, { + remove: true, + value: 0 + }).then(() => { + this.value(undefined); + }).catch(error => { + //We have to do nothing + }); + } + }); + } + + if(typeof(this._grant) === "undefined") { + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Add grant permission"), + callback: () => this._trigger_grant_assign() + }); + } else { + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Remove grant permission"), + callback: () => { + this.handle.trigger_change(this.permission, { + remove: true, + granted: 0 + }).then(() => { + this.granted(undefined); + }).catch(error => { + //We have to do nothing + }); + } + }); + } + entries.push(contextmenu.Entry.HR()); + if(this.group.collapsed) + entries.push({ /* This could never happen! */ type: contextmenu.MenuEntryType.ENTRY, name: tr("Expend group"), - callback: () => this.expend(), + callback: () => this.group.expend() }); else entries.push({ type: contextmenu.MenuEntryType.ENTRY, name: tr("Collapse group"), - callback: () => this.collapse(), + callback: () => this.group.collapse() }); - entries.push(contextmenu.Entry.HR()); - entries.push({ type: contextmenu.MenuEntryType.ENTRY, name: tr("Expend all"), @@ -535,415 +323,644 @@ namespace pe { name: tr("Collapse all"), callback: () => this.handle.collapse_all() }); + entries.push(contextmenu.Entry.HR()); + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Show permission description"), + callback: () => { + createInfoModal( + tr("Permission description"), + tr("Permission description for permission ") + this.permission.name + ":
" + this.permission.description + ).open(); + } + }); + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Copy permission name"), + callback: () => { + copy_to_clipboard(this.permission.name); + } + }); contextmenu.spawn_context_menu(event.pageX, event.pageY, ...entries); }); - - this._tag_arrow.on('click', event => { - if(this.collapsed) - this.expend(); - else - this.collapse(); - }) - } - - update_visibility() { - let flag = false; - if (!flag) { - for (const group of this.children) { - if (group.visible) { - flag = true; - break; - } - } - } - - if (!flag) { - for (const permission of this.permissions) { - if (!permission.is_filtered()) { - flag = true; - break; - } - } - } - - this.visible = flag; - - flag = flag && !this.parent_collapsed; - for(const element of this.tag) - (element).style.display = flag ? 'flex' : 'none'; - - const arrow_node = this._tag_arrow[0]; - arrow_node.classList.remove(this.collapsed ? "down" : "right"); - arrow_node.classList.add(!this.collapsed ? "down" : "right"); - } - - collapse() { - this.collapsed = true; - - const children = [...this.children]; - while (true) { - const child = children.pop(); - if(!child) break; - - child.parent_collapsed = true; - children.push(...child.children); - } - - this.handle.update_view(); - } - - expend() { - this.collapsed = false; - - if(this.parent_collapsed) - return; - - const children = [...this.children]; - while (true) { - const child = children.pop(); - if(!child) break; - - child.parent_collapsed = false; - if(!child.collapsed) - children.push(...child.children); - } - - this.handle.update_view(); } } - export class HTMLPermissionEditor extends Modals.AbstractPermissionEditor { - container: JQuery; + private _trigger_value_assign() { + if(typeof(this._value) === "undefined") + this.value(this._grant || 1, false, false); //TODO: Use max granted value? + this._tag_value_input.focus(); + if(this.permission.is_boolean()) + this._tag_value_input.trigger('change'); + } - private mode_container_permissions: JQuery; - private mode_container_error_permission: JQuery; - private mode_container_unset: JQuery; + private _trigger_grant_assign() { + this.granted(1); //TODO: Use max granted value? + this._tag_granted_input.focus(); + } - private icon_shown: boolean; + hide() { + this._mask &= ~0x08; + for(const element of this.tag) + (element).style.display = 'none'; + } - private filter_input: JQuery; - private filter_grant: JQuery; + show() { + this._mask |= 0x08; + for(const element of this.tag) + (element).style.display = 'flex'; + } - private hidden_permissions: PermissionType[]; + is_filtered() : boolean { + return (this._mask & 0x10) > 0; + } - private button_toggle: JQuery; + set_filtered(flag: boolean) { + if(flag) + this._mask |= 0x10; + else + this._mask &= ~0x10; + } - private even_list: ({ visible() : boolean; set_even(flag: boolean); })[]; - private permission_map: Array; - private permission_groups: HTMLPermissionGroup[]; + is_set() : boolean { + return (this._mask & 0x03) > 0; + } - constructor() { - super(); + get_value() { return this._value; } + + value(value: number | undefined, skip?: boolean, negate?: boolean) { + if(typeof value === "undefined") { + this._tag_value.detach(); + this._tag_negate.detach(); + this._tag_skip.detach(); + + this._value = undefined; + this.flags = 0; + + this._update_active_class(); + this._mask &= ~0x1; + return; } - initialize(permissions: GroupedPermissions[]) { - this._permissions = permissions; - this.build_tag(); + if((this._mask & 0x1) == 0) { + this._tag_value.appendTo(this.tag_container_value); + this._tag_negate.appendTo(this.tag_container_negate); + this._tag_skip.appendTo(this.tag_container_skip); + + this._update_active_class(); + this._mask |= 0x01; } - set_hidden_permissions(permissions: PermissionType[]) { - this.hidden_permissions = permissions; - this.update_filter(); + if((this._mask & 0x04) > 0) + this._tag_value_input.prop('checked', !!value); + else + this._tag_value_input.val(value); + this._tag_skip_input.prop('checked', !!skip); + this._tag_negate_input.prop('checked', !!negate); + + this._value = value; + this.flags = (!!skip ? 0x01 : 0) | (!!negate ? 0x2 : 0); + } + + granted(value: number | undefined) { + if(typeof value === "undefined") { + this._tag_granted.detach(); + + this._update_active_class(); + this._grant = undefined; + this._mask &= ~0x2; + return; } - private update_filter() { - const value = (this.filter_input.val() as string).toLowerCase(); - const grant = !!this.filter_grant.prop('checked'); - - const _filter = (permission: HTMLPermission) => { - if(value && permission.permission.name.indexOf(value) == -1) return false; - if(grant && !permission.is_set()) return false; - if(this.hidden_permissions && this.hidden_permissions.find(e => e && e.toLocaleLowerCase() == permission.permission.name.toLowerCase())) - return false; - - return true; - }; - - for(let id = 1; id < this.permission_map.length; id++) { - const permission = this.permission_map[id]; - let flag = _filter(permission); - permission.set_filtered(!flag); - - - flag = flag && !permission.group.collapsed && !permission.group.parent_collapsed; /* hide when parent is filtered */ - if(flag) permission.show(); - else permission.hide(); - } - - /* run in both directions, to update the parent visibility and the actiual visibility */ - for(const group of this.permission_groups) - group.update_visibility(); - for(const group of this.permission_groups.slice().reverse()) - group.update_visibility(); - - - let index = 0; - for(const entry of this.even_list) { - if(!entry.visible()) continue; - entry.set_even((index++ & 0x1) == 0); - } + if((this._mask & 0x2) == 0) { + this._mask |= 0x02; + this._tag_granted.appendTo(this.tag_container_granted); + this._update_active_class(); } + this._tag_granted_input.val(value); + this._grant = value; + } - private update_icon() { - const permission = this.icon_shown ? this.permission_map.find(e => e && e.permission.name === "i_icon_id") : undefined; - const icon_id = permission ? permission.get_value() : 0; + reset() { + this._mask &= ~0x03; - const icon_node = this.container.find(".container-icon-select .icon-preview"); - icon_node.children().remove(); + this._tag_value.detach(); + this._tag_negate.detach(); + this._tag_skip.detach(); - let resolve: Promise>; - if(icon_id >= 0 && icon_id <= 1000) - resolve = Promise.resolve(IconManager.generate_tag({id: icon_id, url: ""})); - else - resolve = this.icon_resolver(permission ? permission.get_value() : 0).then(e => $(e)); + this._tag_granted.detach(); - resolve.then(tag => tag.appendTo(icon_node)) - .catch(error => { - log.error(LogCategory.PERMISSIONS, tr("Failed to generate empty icon preview: %o"), error); + this._value = undefined; + this._grant = undefined; + this.flags = 0; + + const tag = this.tag[0] as HTMLDivElement; + tag.classList.remove("active"); + } + + private _reset_value() { + if(typeof(this._value) === "undefined") { + if((this._mask & 0x1) != 0) + this.value(undefined); + } else { + this.value(this._value, (this.flags & 0x1) > 1, (this.flags & 0x2) > 1); + } + } + + private _reset_grant() { + if(typeof(this._grant) === "undefined") { + if((this._mask & 0x2) != 0) + this.granted(undefined); + } else { + this.granted(this._grant); + } + } + + private _update_active_class() { + const value = typeof(this._value) !== "undefined" || typeof(this._grant) !== "undefined"; + const tag = this.tag[0] as HTMLDivElement; + if(value) + tag.classList.add("active"); + else + tag.classList.remove("active"); + } +} + +class HTMLPermissionGroup { + readonly handle: HTMLPermissionEditor; + readonly group: PermissionGroup; + readonly index: number; + + private _tag_arrow: JQuery; + + permissions: HTMLPermission[] = []; + children: HTMLPermissionGroup[] = []; + + tag: JQuery; + visible: boolean; + + collapsed: boolean; + parent_collapsed: boolean; + + constructor(handle: HTMLPermissionEditor, group: PermissionGroup, index: number) { + this.handle = handle; + this.group = group; + this.index = index; + + this._build_tag(); + } + + private _build_tag() { + this.tag = $.spawn("div").addClass("entry group").css('padding-left', this.index + "em").append([ + $.spawn("div").addClass("column-name").append([ + this._tag_arrow = $.spawn("div").addClass("arrow down"), + $.spawn("div").addClass("group-name").text(this.group.name) + ]), + $.spawn("div").addClass("column-value"), + $.spawn("div").addClass("column-skip"), + $.spawn("div").addClass("column-negate"), + $.spawn("div").addClass("column-granted") + ]); + + this.tag.on('contextmenu', event => { + if(event.isDefaultPrevented()) + return; + event.preventDefault(); + + const entries: contextmenu.MenuEntry[] = []; + if(this.collapsed) + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Expend group"), + callback: () => this.expend(), }); + else + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Collapse group"), + callback: () => this.collapse(), + }); + entries.push(contextmenu.Entry.HR()); + + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Expend all"), + callback: () => this.handle.expend_all() + }); + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Collapse all"), + callback: () => this.handle.collapse_all() + }); + + contextmenu.spawn_context_menu(event.pageX, event.pageY, ...entries); + }); + + this._tag_arrow.on('click', event => { + if(this.collapsed) + this.expend(); + else + this.collapse(); + }) + } + + update_visibility() { + let flag = false; + if (!flag) { + for (const group of this.children) { + if (group.visible) { + flag = true; + break; + } + } } - private build_tag() { - this.container = $("#tmpl_permission_editor_html").renderTag(); - this.container.find("input").on('change', event => { - $(event.target).parents(".form-group").toggleClass('is-filled', !!(event.target as HTMLInputElement).value); - }); - - /* search for that as long we've not that much nodes */ - this.mode_container_permissions = this.container.find(".container-mode-permissions"); - this.mode_container_error_permission = this.container.find(".container-mode-no-permissions"); - this.mode_container_unset = this.container.find(".container-mode-unset"); - - this.filter_input = this.container.find(".filter-input"); - this.filter_input.on('change keyup', event => this.update_filter()); - - this.filter_grant = this.container.find(".filter-granted"); - this.filter_grant.on('change', event => this.update_filter()); - - this.button_toggle = this.container.find(".button-toggle-clients"); - this.button_toggle.on('click', () => { - if(this._toggle_callback) - this.button_toggle.text(this._toggle_callback()); - }); - - this.container.find(".button-update").on('click', event => this.trigger_update()); - - /* allocate array space */ - { - let max_index = 0; - let tmp: GroupedPermissions[] = []; - while(true) { - const entry = tmp.pop(); - if(!entry) break; - for(const permission of entry.permissions) - if(permission.id > max_index) - max_index = permission.id; - tmp.push(...entry.children); + if (!flag) { + for (const permission of this.permissions) { + if (!permission.is_filtered()) { + flag = true; + break; } - this.permission_map = new Array(max_index + 1); } - this.permission_groups = []; - this.even_list = []; + } - { - const container_permission = this.mode_container_permissions.find(".container-permission-list .body"); + this.visible = flag; - const build_group = (pgroup: HTMLPermissionGroup, group: GroupedPermissions, index: number) => { - const hgroup = new HTMLPermissionGroup(this, group.group, index); - hgroup.tag.appendTo(container_permission); + flag = flag && !this.parent_collapsed; + for(const element of this.tag) + (element).style.display = flag ? 'flex' : 'none'; + + const arrow_node = this._tag_arrow[0]; + arrow_node.classList.remove(this.collapsed ? "down" : "right"); + arrow_node.classList.add(!this.collapsed ? "down" : "right"); + } + + collapse() { + this.collapsed = true; + + const children = [...this.children]; + while (true) { + const child = children.pop(); + if(!child) break; + + child.parent_collapsed = true; + children.push(...child.children); + } + + this.handle.update_view(); + } + + expend() { + this.collapsed = false; + + if(this.parent_collapsed) + return; + + const children = [...this.children]; + while (true) { + const child = children.pop(); + if(!child) break; + + child.parent_collapsed = false; + if(!child.collapsed) + children.push(...child.children); + } + + this.handle.update_view(); + } +} + +export class HTMLPermissionEditor extends AbstractPermissionEditor { + container: JQuery; + + private mode_container_permissions: JQuery; + private mode_container_error_permission: JQuery; + private mode_container_unset: JQuery; + + private icon_shown: boolean; + + private filter_input: JQuery; + private filter_grant: JQuery; + + private hidden_permissions: PermissionType[]; + + private button_toggle: JQuery; + + private even_list: ({ visible() : boolean; set_even(flag: boolean); })[]; + private permission_map: Array; + private permission_groups: HTMLPermissionGroup[]; + + constructor() { + super(); + } + + initialize(permissions: GroupedPermissions[]) { + this._permissions = permissions; + this.build_tag(); + } + + set_hidden_permissions(permissions: PermissionType[]) { + this.hidden_permissions = permissions; + this.update_filter(); + } + + private update_filter() { + const value = (this.filter_input.val() as string).toLowerCase(); + const grant = !!this.filter_grant.prop('checked'); + + const _filter = (permission: HTMLPermission) => { + if(value && permission.permission.name.indexOf(value) == -1) return false; + if(grant && !permission.is_set()) return false; + if(this.hidden_permissions && this.hidden_permissions.find(e => e && e.toLocaleLowerCase() == permission.permission.name.toLowerCase())) + return false; + + return true; + }; + + for(let id = 1; id < this.permission_map.length; id++) { + const permission = this.permission_map[id]; + let flag = _filter(permission); + permission.set_filtered(!flag); + + + flag = flag && !permission.group.collapsed && !permission.group.parent_collapsed; /* hide when parent is filtered */ + if(flag) permission.show(); + else permission.hide(); + } + + /* run in both directions, to update the parent visibility and the actiual visibility */ + for(const group of this.permission_groups) + group.update_visibility(); + for(const group of this.permission_groups.slice().reverse()) + group.update_visibility(); + + + let index = 0; + for(const entry of this.even_list) { + if(!entry.visible()) continue; + entry.set_even((index++ & 0x1) == 0); + } + } + + private update_icon() { + const permission = this.icon_shown ? this.permission_map.find(e => e && e.permission.name === "i_icon_id") : undefined; + const icon_id = permission ? permission.get_value() : 0; + + const icon_node = this.container.find(".container-icon-select .icon-preview"); + icon_node.children().remove(); + + let resolve: Promise>; + if(icon_id >= 0 && icon_id <= 1000) + resolve = Promise.resolve(IconManager.generate_tag({id: icon_id, url: ""})); + else + resolve = this.icon_resolver(permission ? permission.get_value() : 0).then(e => $(e)); + + resolve.then(tag => tag.appendTo(icon_node)) + .catch(error => { + log.error(LogCategory.PERMISSIONS, tr("Failed to generate empty icon preview: %o"), error); + }); + } + + private build_tag() { + this.container = $("#tmpl_permission_editor_html").renderTag(); + this.container.find("input").on('change', event => { + $(event.target).parents(".form-group").toggleClass('is-filled', !!(event.target as HTMLInputElement).value); + }); + + /* search for that as long we've not that much nodes */ + this.mode_container_permissions = this.container.find(".container-mode-permissions"); + this.mode_container_error_permission = this.container.find(".container-mode-no-permissions"); + this.mode_container_unset = this.container.find(".container-mode-unset"); + + this.filter_input = this.container.find(".filter-input"); + this.filter_input.on('change keyup', event => this.update_filter()); + + this.filter_grant = this.container.find(".filter-granted"); + this.filter_grant.on('change', event => this.update_filter()); + + this.button_toggle = this.container.find(".button-toggle-clients"); + this.button_toggle.on('click', () => { + if(this._toggle_callback) + this.button_toggle.text(this._toggle_callback()); + }); + + this.container.find(".button-update").on('click', event => this.trigger_update()); + + /* allocate array space */ + { + let max_index = 0; + let tmp: GroupedPermissions[] = []; + while(true) { + const entry = tmp.pop(); + if(!entry) break; + for(const permission of entry.permissions) + if(permission.id > max_index) + max_index = permission.id; + tmp.push(...entry.children); + } + this.permission_map = new Array(max_index + 1); + } + this.permission_groups = []; + this.even_list = []; + + { + const container_permission = this.mode_container_permissions.find(".container-permission-list .body"); + + const build_group = (pgroup: HTMLPermissionGroup, group: GroupedPermissions, index: number) => { + const hgroup = new HTMLPermissionGroup(this, group.group, index); + hgroup.tag.appendTo(container_permission); + this.even_list.push({ + set_even(flag: boolean) { + if(flag) + hgroup.tag[0].classList.add('even'); + else + hgroup.tag[0].classList.remove('even'); + }, + + visible(): boolean { + return !hgroup.parent_collapsed && hgroup.visible; + } + }); + + if(pgroup) + pgroup.children.push(hgroup); + this.permission_groups.push(hgroup); + + index++; + for(const child of group.children) + build_group(hgroup, child, index); + + for(const permission of group.permissions) { + const perm = new HTMLPermission(this, hgroup, permission, index); + this.permission_map[perm.permission.id] = perm; + perm.tag.appendTo(container_permission); + hgroup.permissions.push(perm); this.even_list.push({ set_even(flag: boolean) { if(flag) - hgroup.tag[0].classList.add('even'); + perm.tag[0].classList.add('even'); else - hgroup.tag[0].classList.remove('even'); + perm.tag[0].classList.remove('even'); }, visible(): boolean { - return !hgroup.parent_collapsed && hgroup.visible; + return !perm.is_filtered() && !perm.group.collapsed && !perm.group.parent_collapsed; } }); + } + }; - if(pgroup) - pgroup.children.push(hgroup); - this.permission_groups.push(hgroup); + for(const group of this._permissions) + build_group(undefined, group, 0); + } - index++; - for(const child of group.children) - build_group(hgroup, child, index); - - for(const permission of group.permissions) { - const perm = new HTMLPermission(this, hgroup, permission, index); - this.permission_map[perm.permission.id] = perm; - perm.tag.appendTo(container_permission); - hgroup.permissions.push(perm); - this.even_list.push({ - set_even(flag: boolean) { - if(flag) - perm.tag[0].classList.add('even'); - else - perm.tag[0].classList.remove('even'); - }, - - visible(): boolean { - return !perm.is_filtered() && !perm.group.collapsed && !perm.group.parent_collapsed; - } - }); - } - }; - - for(const group of this._permissions) - build_group(undefined, group, 0); - } - - { - const container = this.container.find(".container-icon-select"); - container.find(".button-select-icon").on('click', event => { - const permission = this.permission_map.find(e => e && e.permission.name === "i_icon_id"); - this.icon_selector(permission ? permission.get_value() : 0).then(id => { - const permission = this.permission_map.find(e => e && e.permission.name === "i_icon_id"); - if(permission) { - this.trigger_change(permission.permission, { - remove: false, - value: id, - flag_skip: false, - flag_negate: false - }, false).then(() => { - log.debug(LogCategory.PERMISSIONS, tr("Selected new icon %s"), id); - - permission.value(id, false, false); - this.update_icon(); - }).catch(error => { - log.warn(LogCategory.PERMISSIONS, tr("Failed to set icon permission within permission editor: %o"), error); - }); - } else { - log.warn(LogCategory.PERMISSIONS, tr("Failed to find icon permissions within permission editor")); - } - }).catch(error => { - log.error(LogCategory.PERMISSIONS, tr("Failed to select an icon for the icon permission: %o"), error); - }); - }); - - container.find(".button-icon-remove").on('click', event => { + { + const container = this.container.find(".container-icon-select"); + container.find(".button-select-icon").on('click', event => { + const permission = this.permission_map.find(e => e && e.permission.name === "i_icon_id"); + this.icon_selector(permission ? permission.get_value() : 0).then(id => { const permission = this.permission_map.find(e => e && e.permission.name === "i_icon_id"); if(permission) { this.trigger_change(permission.permission, { - remove: true, + remove: false, + value: id, + flag_skip: false, + flag_negate: false }, false).then(() => { - permission.value(undefined); + log.debug(LogCategory.PERMISSIONS, tr("Selected new icon %s"), id); + + permission.value(id, false, false); this.update_icon(); }).catch(error => { - log.warn(LogCategory.PERMISSIONS, tr("Failed to remove icon permission within permission editor: %o"), error); + log.warn(LogCategory.PERMISSIONS, tr("Failed to set icon permission within permission editor: %o"), error); }); } else { - log.warn(LogCategory.PERMISSIONS, tr("Failed to find icon permission within permission editor")); + log.warn(LogCategory.PERMISSIONS, tr("Failed to find icon permissions within permission editor")); } + }).catch(error => { + log.error(LogCategory.PERMISSIONS, tr("Failed to select an icon for the icon permission: %o"), error); }); - } - - this.mode_container_permissions.on('contextmenu', event => { - if(event.isDefaultPrevented()) - return; - event.preventDefault(); - - const entries: contextmenu.MenuEntry[] = []; - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Expend all"), - callback: () => this.expend_all() - }); - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Collapse all"), - callback: () => this.collapse_all() - }); - - contextmenu.spawn_context_menu(event.pageX, event.pageY, ...entries); }); - this.set_mode(Modals.PermissionEditorMode.UNSET); - } - - html_tag(): JQuery { - return this.container; - } - - set_permissions(u_permissions?: PermissionValue[]) { - const permissions = new Array(this.permission_map.length); - - /* initialize update array, boundary checks are already made by js */ - for(const perm of u_permissions) - permissions[perm.type.id] = perm; - - /* there is no permission with id 0 */ - for(let id = 1; id < permissions.length; id++) { - const new_permission = permissions[id]; - const permission_handle = this.permission_map[id]; - if(!new_permission) { - permission_handle.reset(); - continue; - } - - permission_handle.value(new_permission.value, new_permission.flag_skip, new_permission.flag_negate); - permission_handle.granted(new_permission.granted_value); - } - - this.update_icon(); - this.update_filter(); - } - - set_mode(mode: Modals.PermissionEditorMode) { - this.mode_container_permissions.css('display', mode == Modals.PermissionEditorMode.VISIBLE ? 'flex' : 'none'); - this.mode_container_error_permission.css('display', mode == Modals.PermissionEditorMode.NO_PERMISSION ? 'flex' : 'none'); - this.mode_container_unset.css('display', mode == Modals.PermissionEditorMode.UNSET ? 'block' : 'none'); - if(this.icon_shown != (mode == Modals.PermissionEditorMode.VISIBLE)) { - this.icon_shown = mode == Modals.PermissionEditorMode.VISIBLE; - this.update_icon(); - } - } - - trigger_change(permission: PermissionInfo, value?: Modals.PermissionEditor.PermissionValue, update_icon?: boolean) : Promise { - if(this._listener_change) { - if((typeof(update_icon) !== "boolean" || update_icon) && permission && permission.name === "i_icon_id") - return this._listener_change(permission, value).then(e => { - setTimeout(() => this.update_icon(), 0); /* we need to fully handle the response and then only we're able to update the icon */ - return e; + container.find(".button-icon-remove").on('click', event => { + const permission = this.permission_map.find(e => e && e.permission.name === "i_icon_id"); + if(permission) { + this.trigger_change(permission.permission, { + remove: true, + }, false).then(() => { + permission.value(undefined); + this.update_icon(); + }).catch(error => { + log.warn(LogCategory.PERMISSIONS, tr("Failed to remove icon permission within permission editor: %o"), error); }); - else - return this._listener_change(permission, value); - } - - return Promise.reject(); + } else { + log.warn(LogCategory.PERMISSIONS, tr("Failed to find icon permission within permission editor")); + } + }); } - collapse_all() { - for(const group of this.permission_groups) { - group.collapsed = true; - for(const child of group.children) - child.parent_collapsed = true; + this.mode_container_permissions.on('contextmenu', event => { + if(event.isDefaultPrevented()) + return; + event.preventDefault(); + + const entries: contextmenu.MenuEntry[] = []; + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Expend all"), + callback: () => this.expend_all() + }); + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Collapse all"), + callback: () => this.collapse_all() + }); + + contextmenu.spawn_context_menu(event.pageX, event.pageY, ...entries); + }); + + this.set_mode(PermissionEditorMode.UNSET); + } + + html_tag(): JQuery { + return this.container; + } + + set_permissions(u_permissions?: PermissionValue[]) { + const permissions = new Array(this.permission_map.length); + + /* initialize update array, boundary checks are already made by js */ + for(const perm of u_permissions) + permissions[perm.type.id] = perm; + + /* there is no permission with id 0 */ + for(let id = 1; id < permissions.length; id++) { + const new_permission = permissions[id]; + const permission_handle = this.permission_map[id]; + if(!new_permission) { + permission_handle.reset(); + continue; } - this.update_filter(); /* update display state of all entries */ + + permission_handle.value(new_permission.value, new_permission.flag_skip, new_permission.flag_negate); + permission_handle.granted(new_permission.granted_value); } - expend_all() { - for(const group of this.permission_groups) { - group.collapsed = false; - group.parent_collapsed = false; - } - this.update_filter(); /* update display state of all entries */ + this.update_icon(); + this.update_filter(); + } + + set_mode(mode: PermissionEditorMode) { + this.mode_container_permissions.css('display', mode == PermissionEditorMode.VISIBLE ? 'flex' : 'none'); + this.mode_container_error_permission.css('display', mode == PermissionEditorMode.NO_PERMISSION ? 'flex' : 'none'); + this.mode_container_unset.css('display', mode == PermissionEditorMode.UNSET ? 'block' : 'none'); + if(this.icon_shown != (mode == PermissionEditorMode.VISIBLE)) { + this.icon_shown = mode == PermissionEditorMode.VISIBLE; + this.update_icon(); + } + } + + trigger_change(permission: PermissionInfo, value?: ChangedPermissionValue, update_icon?: boolean) : Promise { + if(this._listener_change) { + if((typeof(update_icon) !== "boolean" || update_icon) && permission && permission.name === "i_icon_id") + return this._listener_change(permission, value).then(e => { + setTimeout(() => this.update_icon(), 0); /* we need to fully handle the response and then only we're able to update the icon */ + return e; + }); + else + return this._listener_change(permission, value); } - update_view() { return this.update_filter(); } + return Promise.reject(); + } - set_toggle_button(callback: () => string, initial: string) { - this._toggle_callback = callback; - if(this._toggle_callback) { - this.button_toggle.text(initial); - this.button_toggle.show(); - } else { - this.button_toggle.hide(); - } + collapse_all() { + for(const group of this.permission_groups) { + group.collapsed = true; + for(const child of group.children) + child.parent_collapsed = true; + } + this.update_filter(); /* update display state of all entries */ + } + + expend_all() { + for(const group of this.permission_groups) { + group.collapsed = false; + group.parent_collapsed = false; + } + this.update_filter(); /* update display state of all entries */ + } + + update_view() { return this.update_filter(); } + + set_toggle_button(callback: () => string, initial: string) { + this._toggle_callback = callback; + if(this._toggle_callback) { + this.button_toggle.text(initial); + this.button_toggle.show(); + } else { + this.button_toggle.hide(); } } } \ No newline at end of file diff --git a/shared/js/ui/modal/permission/ModalPermissionEdit.ts b/shared/js/ui/modal/permission/ModalPermissionEdit.ts index f405dd9c..69e0bb91 100644 --- a/shared/js/ui/modal/permission/ModalPermissionEdit.ts +++ b/shared/js/ui/modal/permission/ModalPermissionEdit.ts @@ -1,497 +1,310 @@ -/// -/// -/// +import { + PermissionManager, +} from "tc-shared/permission/PermissionManager"; +import PermissionType from "tc-shared/permission/PermissionType"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {createErrorModal, createInfoModal, createInputModal, createModal, Modal} from "tc-shared/ui/elements/Modal"; +import {HTMLPermissionEditor} from "tc-shared/ui/modal/permission/HTMLPermissionEditor"; +import {spawnIconSelect} from "tc-shared/ui/modal/ModalIconSelect"; +import {CanvasPermissionEditor} from "tc-shared/ui/modal/permission/CanvasPermissionEditor"; +import {Settings, settings} from "tc-shared/settings"; +import {ChannelEntry} from "tc-shared/ui/channel"; +import {LogCategory} from "tc-shared/log"; +import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; +import * as log from "tc-shared/log"; +import {Group, GroupManager, GroupTarget, GroupType} from "tc-shared/permission/GroupManager"; +import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; +import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; +import {copy_to_clipboard} from "tc-shared/utils/helpers"; +import {formatMessage} from "tc-shared/ui/frames/chat"; +import { + senseless_channel_group_permissions, + senseless_channel_permissions, senseless_client_channel_permissions, + senseless_client_permissions, senseless_server_group_permissions +} from "tc-shared/ui/modal/permission/SenselessPermissions"; +import {AbstractPermissionEditor, PermissionEditorMode} from "tc-shared/ui/modal/permission/AbstractPermissionEditor"; -interface JQuery { - dropdown: any; +declare global { + interface JQuery { + dropdown: any; + } } -namespace Modals { - export namespace PermissionEditor { - export interface PermissionEntry { - tag: JQuery; - tag_value: JQuery; - tag_grant: JQuery; - tag_flag_negate: JQuery; - tag_flag_skip: JQuery; +export type OptionsServerGroup = {}; +export type OptionsChannelGroup = {}; +export type OptionsClientPermissions = { unique_id?: string }; +export type OptionsChannelPermissions = { channel_id?: number }; +export type OptionsClientChannelPermissions = OptionsClientPermissions & OptionsChannelPermissions; +export interface OptionMap { + "sg": OptionsServerGroup, + "cg": OptionsChannelGroup, + "clp": OptionsClientPermissions, + "chp": OptionsChannelPermissions, + "clchp": OptionsClientChannelPermissions +} - id: number; - filter: string; - is_bool: boolean; +export function _space() { + const now = Date.now(); + while(now + 100 > Date.now()); +} - } +export function spawnPermissionEdit(connection: ConnectionHandler, selected_tab?: T, options?: OptionMap[T]) : Modal { + options = options || {}; - export interface PermissionValue { - remove: boolean; /* if set remove the set permission (value or granted) */ + const modal = createModal({ + header: function() { + return tr("Server Permissions"); + }, + body: function () { + let properties: any = {}; + let tag = $("#tmpl_server_permissions").renderTag(properties); - granted?: number; - value?: number; + /* build the permission editor */ + const permission_editor: AbstractPermissionEditor = (() => { + const editor = new HTMLPermissionEditor(); + editor.initialize(connection.permissions.groupedPermissions()); + editor.icon_resolver = id => connection.fileManager.icons.resolve_icon(id).then(async icon => { + if(!icon) + return undefined; - flag_skip?: boolean; - flag_negate?: boolean; - } - - export type change_listener_t = (permission: PermissionInfo, value?: PermissionEditor.PermissionValue) => Promise; - } - - export enum PermissionEditorMode { - VISIBLE, - NO_PERMISSION, - UNSET - } - - export abstract class AbstractPermissionEditor { - protected _permissions: GroupedPermissions[]; - protected _listener_update: () => any; - protected _listener_change: PermissionEditor.change_listener_t = () => Promise.resolve(); - protected _toggle_callback: () => string; - - icon_resolver: (id: number) => Promise; - icon_selector: (current_id: number) => Promise; - - protected constructor() {} - - abstract set_mode(mode: PermissionEditorMode); - - abstract initialize(permissions: GroupedPermissions[]); - abstract html_tag() : JQuery; - abstract set_permissions(permissions?: PermissionValue[]); - abstract set_hidden_permissions(permissions: PermissionType[]); - - set_listener(listener?: PermissionEditor.change_listener_t) { - this._listener_change = listener || (() => Promise.resolve()); - } - - set_listener_update(listener?: () => any) { this._listener_update = listener; } - trigger_update() { if(this._listener_update) this._listener_update(); } - - abstract set_toggle_button(callback: () => string, initial: string); - } - - export type OptionsServerGroup = {}; - export type OptionsChannelGroup = {}; - export type OptionsClientPermissions = { unique_id?: string }; - export type OptionsChannelPermissions = { channel_id?: number }; - export type OptionsClientChannelPermissions = OptionsClientPermissions & OptionsChannelPermissions; - export interface OptionMap { - "sg": OptionsServerGroup, - "cg": OptionsChannelGroup, - "clp": OptionsClientPermissions, - "chp": OptionsChannelPermissions, - "clchp": OptionsClientChannelPermissions - } - - export function _space() { - const now = Date.now(); - while(now + 100 > Date.now()); - } - - export function spawnPermissionEdit(connection: ConnectionHandler, selected_tab?: T, options?: OptionMap[T]) : Modal { - options = options || {}; - - const modal = createModal({ - header: function() { - return tr("Server Permissions"); - }, - body: function () { - let properties: any = {}; - let tag = $("#tmpl_server_permissions").renderTag(properties); - - /* build the permission editor */ - const permission_editor: AbstractPermissionEditor = (() => { - const editor = new pe.HTMLPermissionEditor(); - editor.initialize(connection.permissions.groupedPermissions()); - editor.icon_resolver = id => connection.fileManager.icons.resolve_icon(id).then(async icon => { - if(!icon) - return undefined; - - const tag = document.createElement("img"); - await new Promise((resolve, reject) => { - tag.onerror = reject; - tag.onload = resolve; - tag.src = icon.url; - }); - return tag; - }); - editor.icon_selector = current_icon => new Promise(resolve => { - spawnIconSelect(connection, id => resolve(new Int32Array([id])[0]), current_icon); + const tag = document.createElement("img"); + await new Promise((resolve, reject) => { + tag.onerror = reject; + tag.onload = resolve; + tag.src = icon.url; }); + return tag; + }); + editor.icon_selector = current_icon => new Promise(resolve => { + spawnIconSelect(connection, id => resolve(new Int32Array([id])[0]), current_icon); + }); - if(editor instanceof pe.CanvasPermissionEditor) - setTimeout(() => editor.update_ui(), 500); - return editor; - })(); - - const container_tab_list = tag.find(".right > .header"); - { - const label_current = tag.find(".left .container-selected"); - const create_tab = (tab_entry: JQuery, container_name: string, hidden_permissions: PermissionType[]) => { - const target_container = tag.find(".body .container." + container_name); - - tab_entry.on('click', () => { - /* Using a timeout here prevents unnecessary style calculations required by other click event handlers */ - setTimeout(() => { - container_tab_list.find(".selected").removeClass("selected"); - tab_entry.addClass("selected"); - label_current.text(tab_entry.find("a").text()); - - /* dont use show() here because it causes a style recalculation */ - for(const element of tag.find(".body .container")) - (element).style.display = "none"; - - permission_editor.set_hidden_permissions(settings.static_global(Settings.KEY_PERMISSIONS_SHOW_ALL) ? undefined : hidden_permissions); - permission_editor.html_tag()[0].remove(); - target_container.find(".permission-editor").trigger('show'); - target_container.find(".permission-editor").append(permission_editor.html_tag()); - - for(const element of target_container) - (element).style.display = null; - }, 0); - }); - }; - - create_tab(container_tab_list.find(".sg"), "container-view-server-groups", permissions.senseless_server_group_permissions); - create_tab(container_tab_list.find(".cg"), "container-view-channel-groups", permissions.senseless_channel_group_permissions); - create_tab(container_tab_list.find(".chp"), "container-view-channel-permissions", permissions.senseless_channel_permissions); - create_tab(container_tab_list.find(".clp"), "container-view-client-permissions", permissions.senseless_client_permissions); - create_tab(container_tab_list.find(".clchp"), "container-view-client-channel-permissions", permissions.senseless_client_channel_permissions); - } - - apply_server_groups(connection, permission_editor, tag.find(".left .container-view-server-groups"), tag.find(".right .container-view-server-groups")); - apply_channel_groups(connection, permission_editor, tag.find(".left .container-view-channel-groups"), tag.find(".right .container-view-channel-groups")); - apply_channel_permission(connection, permission_editor, tag.find(".left .container-view-channel-permissions"), tag.find(".right .container-view-channel-permissions")); - apply_client_permission(connection, permission_editor, tag.find(".left .container-view-client-permissions"), tag.find(".right .container-view-client-permissions"), selected_tab == "clp" ? options : {}); - apply_client_channel_permission(connection, permission_editor, tag.find(".left .container-view-client-channel-permissions"), tag.find(".right .container-view-client-channel-permissions"), selected_tab == "clchp" ? options : {}); - - setTimeout(() => container_tab_list.find("." + (selected_tab || "sg")).trigger('click'), 0); - return tag.dividerfy(); - }, - footer: undefined, - - min_width: "30em", - height: "80%", - trigger_tab: false, - full_size: true - }); - - const tag = modal.htmlTag; - tag.find(".modal-body").addClass("modal-permission-editor"); - if(selected_tab) - setTimeout(() => tag.find(".tab-header .entry[x-id=" + selected_tab + "]").first().trigger("click"), 1); - tag.find(".btn_close").on('click', () => { - modal.close(); - }); - - return modal; - } - - function build_channel_tree(connection: ConnectionHandler, channel_list: JQuery, selected_channel: number, select_callback: (channel: ChannelEntry, icon_update: (id: number) => any) => any) { - const root = connection.channelTree.get_first_channel(); - if(!root) return; - - const build_channel = (channel: ChannelEntry, level: number) => { - let tag = $.spawn("div").addClass("channel").css("padding-left", "calc(0.25em + " + (level * 16) + "px)").attr("channel-id", channel.channelId); - let icon_tag = connection.fileManager.icons.generateTag(channel.properties.channel_icon_id); - icon_tag.appendTo(tag); - const _update_icon = icon_id => icon_tag.replaceWith(icon_tag = connection.fileManager.icons.generateTag(icon_id)); + if(editor instanceof CanvasPermissionEditor) + setTimeout(() => editor.update_ui(), 500); + return editor; + })(); + const container_tab_list = tag.find(".right > .header"); { - let name = $.spawn("a").text(channel.channelName() + " (" + channel.channelId + ")").addClass("name"); - name.appendTo(tag); + const label_current = tag.find(".left .container-selected"); + const create_tab = (tab_entry: JQuery, container_name: string, hidden_permissions: PermissionType[]) => { + const target_container = tag.find(".body .container." + container_name); + + tab_entry.on('click', () => { + /* Using a timeout here prevents unnecessary style calculations required by other click event handlers */ + setTimeout(() => { + container_tab_list.find(".selected").removeClass("selected"); + tab_entry.addClass("selected"); + label_current.text(tab_entry.find("a").text()); + + /* dont use show() here because it causes a style recalculation */ + for(const element of tag.find(".body .container")) + (element).style.display = "none"; + + permission_editor.set_hidden_permissions(settings.static_global(Settings.KEY_PERMISSIONS_SHOW_ALL) ? undefined : hidden_permissions); + permission_editor.html_tag()[0].remove(); + target_container.find(".permission-editor").trigger('show'); + target_container.find(".permission-editor").append(permission_editor.html_tag()); + + for(const element of target_container) + (element).style.display = null; + }, 0); + }); + }; + + create_tab(container_tab_list.find(".sg"), "container-view-server-groups", senseless_server_group_permissions); + create_tab(container_tab_list.find(".cg"), "container-view-channel-groups", senseless_channel_group_permissions); + create_tab(container_tab_list.find(".chp"), "container-view-channel-permissions", senseless_channel_permissions); + create_tab(container_tab_list.find(".clp"), "container-view-client-permissions", senseless_client_permissions); + create_tab(container_tab_list.find(".clchp"), "container-view-client-channel-permissions", senseless_client_channel_permissions); } - tag.on('click', event => { - channel_list.find(".selected").removeClass("selected"); - tag.addClass("selected"); - select_callback(channel, _update_icon); - }); + apply_server_groups(connection, permission_editor, tag.find(".left .container-view-server-groups"), tag.find(".right .container-view-server-groups")); + apply_channel_groups(connection, permission_editor, tag.find(".left .container-view-channel-groups"), tag.find(".right .container-view-channel-groups")); + apply_channel_permission(connection, permission_editor, tag.find(".left .container-view-channel-permissions"), tag.find(".right .container-view-channel-permissions")); + apply_client_permission(connection, permission_editor, tag.find(".left .container-view-client-permissions"), tag.find(".right .container-view-client-permissions"), selected_tab == "clp" ? options : {}); + apply_client_channel_permission(connection, permission_editor, tag.find(".left .container-view-client-channel-permissions"), tag.find(".right .container-view-client-channel-permissions"), selected_tab == "clchp" ? options : {}); - return tag; - }; + setTimeout(() => container_tab_list.find("." + (selected_tab || "sg")).trigger('click'), 0); + return tag.dividerfy(); + }, + footer: undefined, - const build_channels = (root: ChannelEntry, level: number) => { - build_channel(root, level).appendTo(channel_list); - const child_head = root.children(false).find(e => e.channel_previous === undefined); - if(child_head) - build_channels(child_head, level + 1); - if(root.channel_next) - build_channels(root.channel_next, level) - }; - build_channels(root, 0); + min_width: "30em", + height: "80%", + trigger_tab: false, + full_size: true + }); - let selected_channel_tag = channel_list.find(".channel[channel-id=" + selected_channel + "]"); - if(!selected_channel_tag || selected_channel_tag.length < 1) - selected_channel_tag = channel_list.find('.channel').first(); - setTimeout(() => selected_channel_tag.trigger('click'), 0); - } + const tag = modal.htmlTag; + tag.find(".modal-body").addClass("modal-permission-editor"); + if(selected_tab) + setTimeout(() => tag.find(".tab-header .entry[x-id=" + selected_tab + "]").first().trigger("click"), 1); + tag.find(".btn_close").on('click', () => { + modal.close(); + }); - function apply_client_channel_permission(connection: ConnectionHandler, editor: AbstractPermissionEditor, tab_left: JQuery, tab_right: JQuery, options: OptionsClientChannelPermissions) { - let current_cldbid: number = 0; - let current_channel: ChannelEntry; + return modal; +} + +function build_channel_tree(connection: ConnectionHandler, channel_list: JQuery, selected_channel: number, select_callback: (channel: ChannelEntry, icon_update: (id: number) => any) => any) { + const root = connection.channelTree.get_first_channel(); + if(!root) return; + + const build_channel = (channel: ChannelEntry, level: number) => { + let tag = $.spawn("div").addClass("channel").css("padding-left", "calc(0.25em + " + (level * 16) + "px)").attr("channel-id", channel.channelId); + let icon_tag = connection.fileManager.icons.generateTag(channel.properties.channel_icon_id); + icon_tag.appendTo(tag); + const _update_icon = icon_id => icon_tag.replaceWith(icon_tag = connection.fileManager.icons.generateTag(icon_id)); - /* the editor */ { - const pe_client = tab_right.find(".permission-editor"); - tab_right.on('show', event => { - editor.set_toggle_button(undefined, undefined); - pe_client.append(editor.html_tag()); - if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CLIENT_PERMISSION_LIST).granted(1)) { - if(current_cldbid && current_channel) - editor.set_mode(PermissionEditorMode.VISIBLE); - else - editor.set_mode(PermissionEditorMode.UNSET); - } else { - editor.set_mode(PermissionEditorMode.NO_PERMISSION); - return; - } - - - editor.set_listener_update(() => { - if(!current_cldbid || !current_channel) return; - - connection.permissions.requestClientChannelPermissions(current_cldbid, current_channel.channelId).then(result => { - editor.set_permissions(result); - editor.set_mode(PermissionEditorMode.VISIBLE); - }).catch(error => { - console.log(error); //TODO handling? - }); - }); - - /* TODO: Error handling? */ - editor.set_listener(async (permission, value) => { - if (!current_cldbid) - throw "unset client"; - if (!current_channel) - throw "unset channel"; - - if (value.remove) { - /* remove the permission */ - if (typeof (value.value) !== "undefined") { - log.info(LogCategory.PERMISSIONS, tr("Removing client channel permission %s. permission.id: %o"), - permission.name, - permission.id, - ); - - await connection.serverConnection.send_command("channelclientdelperm", { - cldbid: current_cldbid, - cid: current_channel.channelId, - permid: permission.id, - }); - } else { - log.info(LogCategory.PERMISSIONS, tr("Removing client channel grant permission %s. permission.id: %o"), - permission.name, - permission.id_grant(), - value.granted, - ); - - await connection.serverConnection.send_command("channelclientdelperm", { - cldbid: current_cldbid, - cid: current_channel.channelId, - permid: permission.id_grant(), - }); - } - } else { - /* add the permission */ - if (typeof (value.value) !== "undefined") { - log.info(LogCategory.PERMISSIONS, tr("Adding or updating client channel permission %s. permission.{id: %o, value: %o, flag_skip: %o, flag_negate: %o}"), - permission.name, - permission.id, - value.value, - value.flag_skip, - value.flag_negate - ); - - await connection.serverConnection.send_command("channelclientaddperm", { - cldbid: current_cldbid, - cid: current_channel.channelId, - permid: permission.id, - permvalue: value.value, - permskip: value.flag_skip, - permnegated: value.flag_negate - }); - } else { - log.info(LogCategory.PERMISSIONS, tr("Adding or updating client channel grant permission %s. permission.{id: %o, value: %o}"), - permission.name, - permission.id_grant(), - value.granted, - ); - - await connection.serverConnection.send_command("channelclientaddperm", { - cldbid: current_cldbid, - cid: current_channel.channelId, - permid: permission.id_grant(), - permvalue: value.granted, - permskip: false, - permnegated: false - }); - } - } - }); - - /* FIXME: Use cached permissions */ - editor.trigger_update(); - }); + let name = $.spawn("a").text(channel.channelName() + " (" + channel.channelId + ")").addClass("name"); + name.appendTo(tag); } - build_channel_tree(connection, tab_left.find(".list-channel .entries"), options.channel_id || 0, channel => { - if(current_channel == channel) return; + tag.on('click', event => { + channel_list.find(".selected").removeClass("selected"); + tag.addClass("selected"); + select_callback(channel, _update_icon); + }); - current_channel = channel; + return tag; + }; - /* TODO: Test for visibility */ + const build_channels = (root: ChannelEntry, level: number) => { + build_channel(root, level).appendTo(channel_list); + const child_head = root.children(false).find(e => e.channel_previous === undefined); + if(child_head) + build_channels(child_head, level + 1); + if(root.channel_next) + build_channels(root.channel_next, level) + }; + build_channels(root, 0); + + let selected_channel_tag = channel_list.find(".channel[channel-id=" + selected_channel + "]"); + if(!selected_channel_tag || selected_channel_tag.length < 1) + selected_channel_tag = channel_list.find('.channel').first(); + setTimeout(() => selected_channel_tag.trigger('click'), 0); +} + +function apply_client_channel_permission(connection: ConnectionHandler, editor: AbstractPermissionEditor, tab_left: JQuery, tab_right: JQuery, options: OptionsClientChannelPermissions) { + let current_cldbid: number = 0; + let current_channel: ChannelEntry; + + /* the editor */ + { + const pe_client = tab_right.find(".permission-editor"); + tab_right.on('show', event => { + editor.set_toggle_button(undefined, undefined); + pe_client.append(editor.html_tag()); + if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CLIENT_PERMISSION_LIST).granted(1)) { + if(current_cldbid && current_channel) + editor.set_mode(PermissionEditorMode.VISIBLE); + else + editor.set_mode(PermissionEditorMode.UNSET); + } else { + editor.set_mode(PermissionEditorMode.NO_PERMISSION); + return; + } + + + editor.set_listener_update(() => { + if(!current_cldbid || !current_channel) return; + + connection.permissions.requestClientChannelPermissions(current_cldbid, current_channel.channelId).then(result => { + editor.set_permissions(result); + editor.set_mode(PermissionEditorMode.VISIBLE); + }).catch(error => { + console.log(error); //TODO handling? + }); + }); + + /* TODO: Error handling? */ + editor.set_listener(async (permission, value) => { + if (!current_cldbid) + throw "unset client"; + if (!current_channel) + throw "unset channel"; + + if (value.remove) { + /* remove the permission */ + if (typeof (value.value) !== "undefined") { + log.info(LogCategory.PERMISSIONS, tr("Removing client channel permission %s. permission.id: %o"), + permission.name, + permission.id, + ); + + await connection.serverConnection.send_command("channelclientdelperm", { + cldbid: current_cldbid, + cid: current_channel.channelId, + permid: permission.id, + }); + } else { + log.info(LogCategory.PERMISSIONS, tr("Removing client channel grant permission %s. permission.id: %o"), + permission.name, + permission.id_grant(), + value.granted, + ); + + await connection.serverConnection.send_command("channelclientdelperm", { + cldbid: current_cldbid, + cid: current_channel.channelId, + permid: permission.id_grant(), + }); + } + } else { + /* add the permission */ + if (typeof (value.value) !== "undefined") { + log.info(LogCategory.PERMISSIONS, tr("Adding or updating client channel permission %s. permission.{id: %o, value: %o, flag_skip: %o, flag_negate: %o}"), + permission.name, + permission.id, + value.value, + value.flag_skip, + value.flag_negate + ); + + await connection.serverConnection.send_command("channelclientaddperm", { + cldbid: current_cldbid, + cid: current_channel.channelId, + permid: permission.id, + permvalue: value.value, + permskip: value.flag_skip, + permnegated: value.flag_negate + }); + } else { + log.info(LogCategory.PERMISSIONS, tr("Adding or updating client channel grant permission %s. permission.{id: %o, value: %o}"), + permission.name, + permission.id_grant(), + value.granted, + ); + + await connection.serverConnection.send_command("channelclientaddperm", { + cldbid: current_cldbid, + cid: current_channel.channelId, + permid: permission.id_grant(), + permvalue: value.granted, + permskip: false, + permnegated: false + }); + } + } + }); + + /* FIXME: Use cached permissions */ editor.trigger_update(); }); - - { - - const tag_select = tab_left.find(".client-select"); - const tag_select_uid = tag_select.find("input"); - const tag_select_error = tag_select.find(".invalid-feedback"); - - const tag_client_name = tab_left.find(".client-name"); - const tag_client_uid = tab_left.find(".client-uid"); - const tag_client_dbid = tab_left.find(".client-dbid"); - - - const resolve_client = () => { - let client_uid = tag_select_uid.val() as string; - connection.serverConnection.command_helper.info_from_uid(client_uid).then(result => { - if(!result || result.length == 0) return Promise.reject("invalid data"); - tag_select.removeClass('is-invalid'); - - tag_client_name.val(result[0].client_nickname ); - tag_client_uid.val(result[0].client_unique_id); - tag_client_dbid.val(result[0].client_database_id); - - current_cldbid = result[0].client_database_id; - editor.trigger_update(); - }).catch(error => { - console.log(error); - if(error instanceof CommandResult) { - if(error.id == ErrorID.EMPTY_RESULT) - error = "unknown client"; - else - error = error.extra_message || error.message; - } - - tag_client_name.val(""); - tag_client_uid.val(""); - tag_client_dbid.val(""); - - tag_select_error.text(error); - tag_select.addClass('is-invalid'); - editor.set_mode(PermissionEditorMode.UNSET); - }); - }; - - tag_select_uid.on('change', event => resolve_client()); - if(options.unique_id) { - tag_select_uid.val(options.unique_id); - setTimeout(() => resolve_client()); - } - } } - function apply_client_permission(connection: ConnectionHandler, editor: AbstractPermissionEditor, tab_left: JQuery, tab_right: JQuery, options: OptionsClientPermissions) { - let current_cldbid: number = 0; + build_channel_tree(connection, tab_left.find(".list-channel .entries"), options.channel_id || 0, channel => { + if(current_channel == channel) return; - /* the editor */ - { - const pe_client = tab_right.find("permission-editor.client"); - tab_right.on('show', event => { - editor.set_toggle_button(undefined, undefined); - pe_client.append(editor.html_tag()); - if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CLIENT_PERMISSION_LIST).granted(1)) { - if(current_cldbid) - editor.set_mode(PermissionEditorMode.VISIBLE); - else - editor.set_mode(PermissionEditorMode.UNSET); - } else { - editor.set_mode(PermissionEditorMode.NO_PERMISSION); - return; - } + current_channel = channel; - editor.set_listener_update(() => { - if(!current_cldbid) return; - - connection.permissions.requestClientPermissions(current_cldbid).then(result => { - editor.set_permissions(result); - editor.set_mode(PermissionEditorMode.VISIBLE); - }).catch(error => { - console.log(error); //TODO handling? - }); - }); - - /* TODO: Error handling? */ - editor.set_listener(async (permission, value) => { - if (!current_cldbid) - throw "unset client"; - - if (value.remove) { - /* remove the permission */ - if (typeof (value.value) !== "undefined") { - log.info(LogCategory.PERMISSIONS, tr("Removing client permission %s. permission.id: %o"), - permission.name, - permission.id, - ); - - await connection.serverConnection.send_command("clientdelperm", { - cldbid: current_cldbid, - permid: permission.id, - }); - } else { - log.info(LogCategory.PERMISSIONS, tr("Removing client grant permission %s. permission.id: %o"), - permission.name, - permission.id_grant(), - value.granted, - ); - - await connection.serverConnection.send_command("clientdelperm", { - cldbid: current_cldbid, - permid: permission.id_grant(), - }); - } - } else { - /* add the permission */ - if (typeof (value.value) !== "undefined") { - log.info(LogCategory.PERMISSIONS, tr("Adding or updating client permission %s. permission.{id: %o, value: %o, flag_skip: %o, flag_negate: %o}"), - permission.name, - permission.id, - value.value, - value.flag_skip, - value.flag_negate - ); - - await connection.serverConnection.send_command("clientaddperm", { - cldbid: current_cldbid, - permid: permission.id, - permvalue: value.value, - permskip: value.flag_skip, - permnegated: value.flag_negate - }); - } else { - log.info(LogCategory.PERMISSIONS, tr("Adding or updating client grant permission %s. permission.{id: %o, value: %o}"), - permission.name, - permission.id_grant(), - value.granted, - ); - - await connection.serverConnection.send_command("clientaddperm", { - cldbid: current_cldbid, - permid: permission.id_grant(), - permvalue: value.granted, - permskip: false, - permnegated: false - }); - } - } - }); - - /* FIXME: Use cached permissions */ - editor.trigger_update(); - }); - } + /* TODO: Test for visibility */ + editor.trigger_update(); + }); + { const tag_select = tab_left.find(".client-select"); const tag_select_uid = tag_select.find("input"); @@ -501,11 +314,12 @@ namespace Modals { const tag_client_uid = tab_left.find(".client-uid"); const tag_client_dbid = tab_left.find(".client-dbid"); + const resolve_client = () => { let client_uid = tag_select_uid.val() as string; connection.serverConnection.command_helper.info_from_uid(client_uid).then(result => { if(!result || result.length == 0) return Promise.reject("invalid data"); - tag_select.removeClass("is-invalid"); + tag_select.removeClass('is-invalid'); tag_client_name.val(result[0].client_nickname ); tag_client_uid.val(result[0].client_unique_id); @@ -527,7 +341,7 @@ namespace Modals { tag_client_dbid.val(""); tag_select_error.text(error); - tag_select.addClass("is-invalid"); + tag_select.addClass('is-invalid'); editor.set_mode(PermissionEditorMode.UNSET); }); }; @@ -538,965 +352,1112 @@ namespace Modals { setTimeout(() => resolve_client()); } } +} - function apply_channel_permission(connection: ConnectionHandler, editor: AbstractPermissionEditor, tab_left: JQuery, tab_right: JQuery) { - let current_channel: ChannelEntry | undefined; - let update_channel_icon: (id: number) => any; +function apply_client_permission(connection: ConnectionHandler, editor: AbstractPermissionEditor, tab_left: JQuery, tab_right: JQuery, options: OptionsClientPermissions) { + let current_cldbid: number = 0; - /* the editor */ - { - const pe_channel = tab_right.find(".permission-editor"); - tab_right.on('show', event => { - editor.set_toggle_button(undefined, undefined); - pe_channel.append(editor.html_tag()); - if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNEL_PERMISSION_LIST).granted(1)) + /* the editor */ + { + const pe_client = tab_right.find("permission-editor.client"); + tab_right.on('show', event => { + editor.set_toggle_button(undefined, undefined); + pe_client.append(editor.html_tag()); + if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CLIENT_PERMISSION_LIST).granted(1)) { + if(current_cldbid) editor.set_mode(PermissionEditorMode.VISIBLE); - else { - editor.set_mode(PermissionEditorMode.NO_PERMISSION); - return; - } + else + editor.set_mode(PermissionEditorMode.UNSET); + } else { + editor.set_mode(PermissionEditorMode.NO_PERMISSION); + return; + } - editor.set_listener_update(() => { - if(!current_channel) return; + editor.set_listener_update(() => { + if(!current_cldbid) return; - connection.permissions.requestChannelPermissions(current_channel.channelId).then(result => editor.set_permissions(result)).catch(error => { - editor.set_permissions([]); - console.log(error); //TODO handling? - }); + connection.permissions.requestClientPermissions(current_cldbid).then(result => { + editor.set_permissions(result); + editor.set_mode(PermissionEditorMode.VISIBLE); + }).catch(error => { + console.log(error); //TODO handling? }); - - editor.set_listener(async (permission, value) => { - if (!current_channel) - throw "unset channel"; - - if (value.remove) { - /* remove the permission */ - if (typeof (value.value) !== "undefined") { - log.info(LogCategory.PERMISSIONS, tr("Removing channel permission %s. permission.id: %o"), - permission.name, - permission.id, - ); - - await connection.serverConnection.send_command("channeldelperm", { - cid: current_channel.channelId, - permid: permission.id, - }).then(e => { - if(permission.name === "i_icon_id" && update_channel_icon) - update_channel_icon(undefined); - return e; - }); - } else { - /* TODO Remove this because its totally useless. Remove this from the UI as well */ - log.info(LogCategory.PERMISSIONS, tr("Removing channel grant permission %s. permission.id: %o"), - permission.name, - permission.id_grant(), - value.granted, - ); - - await connection.serverConnection.send_command("channeldelperm", { - cid: current_channel.channelId, - permid: permission.id_grant(), - }); - } - } else { - /* add the permission */ - if (typeof (value.value) !== "undefined") { - log.info(LogCategory.PERMISSIONS, tr("Adding or updating channel permission %s. permission.{id: %o, value: %o, flag_skip: %o, flag_negate: %o}"), - permission.name, - permission.id, - value.value, - value.flag_skip, - value.flag_negate - ); - - await connection.serverConnection.send_command("channeladdperm", { - cid: current_channel.channelId, - permid: permission.id, - permvalue: value.value, - permskip: value.flag_skip, - permnegated: value.flag_negate - }).then(e => { - if(permission.name === "i_icon_id" && update_channel_icon) - update_channel_icon(value.value); - return e; - }); - } else { - /* TODO Remove this because its totally useless. Remove this from the UI as well */ - log.info(LogCategory.PERMISSIONS, tr("Adding or updating channel grant permission %s. permission.{id: %o, value: %o}"), - permission.name, - permission.id_grant(), - value.granted, - ); - - await connection.serverConnection.send_command("channeladdperm", { - cid: current_channel.channelId, - permid: permission.id_grant(), - permvalue: value.granted, - permskip: false, - permnegated: false - }); - } - } - }); - - /* FIXME: Use cached permissions */ - editor.trigger_update(); }); - } - let channel_list = tab_left.find(".list-channel .entries"); - build_channel_tree(connection, channel_list, 0, (channel, update) => { - current_channel = channel; - update_channel_icon = update; + /* TODO: Error handling? */ + editor.set_listener(async (permission, value) => { + if (!current_cldbid) + throw "unset client"; + + if (value.remove) { + /* remove the permission */ + if (typeof (value.value) !== "undefined") { + log.info(LogCategory.PERMISSIONS, tr("Removing client permission %s. permission.id: %o"), + permission.name, + permission.id, + ); + + await connection.serverConnection.send_command("clientdelperm", { + cldbid: current_cldbid, + permid: permission.id, + }); + } else { + log.info(LogCategory.PERMISSIONS, tr("Removing client grant permission %s. permission.id: %o"), + permission.name, + permission.id_grant(), + value.granted, + ); + + await connection.serverConnection.send_command("clientdelperm", { + cldbid: current_cldbid, + permid: permission.id_grant(), + }); + } + } else { + /* add the permission */ + if (typeof (value.value) !== "undefined") { + log.info(LogCategory.PERMISSIONS, tr("Adding or updating client permission %s. permission.{id: %o, value: %o, flag_skip: %o, flag_negate: %o}"), + permission.name, + permission.id, + value.value, + value.flag_skip, + value.flag_negate + ); + + await connection.serverConnection.send_command("clientaddperm", { + cldbid: current_cldbid, + permid: permission.id, + permvalue: value.value, + permskip: value.flag_skip, + permnegated: value.flag_negate + }); + } else { + log.info(LogCategory.PERMISSIONS, tr("Adding or updating client grant permission %s. permission.{id: %o, value: %o}"), + permission.name, + permission.id_grant(), + value.granted, + ); + + await connection.serverConnection.send_command("clientaddperm", { + cldbid: current_cldbid, + permid: permission.id_grant(), + permvalue: value.granted, + permskip: false, + permnegated: false + }); + } + } + }); + + /* FIXME: Use cached permissions */ editor.trigger_update(); }); } - function apply_channel_groups(connection: ConnectionHandler, editor: AbstractPermissionEditor, tab_left: JQuery, tab_right: JQuery) { - let current_group; - let update_group_icon: (id: number) => any; - let update_groups: (selected_group: number) => any; - let update_buttons: () => any; - /* the editor */ - { - const pe_server = tab_right.find(".permission-editor"); - tab_right.on('show', event => { - editor.set_toggle_button(undefined, undefined); - pe_server.append(editor.html_tag()); - if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_PERMISSION_LIST).granted(1)) - editor.set_mode(PermissionEditorMode.VISIBLE); - else { - editor.set_mode(PermissionEditorMode.NO_PERMISSION); - return; - } - - editor.set_listener_update(() => { - if(!current_group) return; - - connection.groups.request_permissions(current_group).then(result => editor.set_permissions(result)).catch(error => { - console.log(error); //TODO handling? - }); - }); - - editor.set_listener(async (permission, value) => { - if (!current_group) - throw "unset channel group"; - - if (value.remove) { - /* remove the permission */ - if (typeof (value.value) !== "undefined") { - log.info(LogCategory.PERMISSIONS, tr("Removing channel group permission %s. permission.id: %o"), - permission.name, - permission.id, - ); - - await connection.serverConnection.send_command("channelgroupdelperm", { - cgid: current_group.id, - permid: permission.id, - }).then(e => { - if(permission.name === "i_icon_id" && update_group_icon) - update_group_icon(undefined); - return e; - }); - } else { - log.info(LogCategory.PERMISSIONS, tr("Removing channel group grant permission %s. permission.id: %o"), - permission.name, - permission.id_grant(), - value.granted, - ); - - await connection.serverConnection.send_command("channelgroupdelperm", { - cgid: current_group.id, - permid: permission.id_grant(), - }); - } - } else { - /* add the permission */ - if (typeof (value.value) !== "undefined") { - log.info(LogCategory.PERMISSIONS, tr("Adding or updating channel group permission %s. permission.{id: %o, value: %o, flag_skip: %o, flag_negate: %o}"), - permission.name, - permission.id, - value.value, - value.flag_skip, - value.flag_negate - ); - - await connection.serverConnection.send_command("channelgroupaddperm", { - cgid: current_group.id, - permid: permission.id, - permvalue: value.value, - permskip: value.flag_skip, - permnegated: value.flag_negate - }).then(e => { - if(permission.name === "i_icon_id" && update_group_icon) - update_group_icon(value.value); - return e; - }); - } else { - log.info(LogCategory.PERMISSIONS, tr("Adding or updating channel group grant permission %s. permission.{id: %o, value: %o}"), - permission.name, - permission.id_grant(), - value.granted, - ); - - await connection.serverConnection.send_command("channelgroupaddperm", { - cgid: current_group.id, - permid: permission.id_grant(), - permvalue: value.granted, - permskip: false, - permnegated: false - }); - } - } - }); - - /* FIXME: Use cached permissions */ - editor.trigger_update(); - }); - } - - /* list all channel groups */ - { - let group_list = tab_left.find(".list-groups .entries"); - - update_groups = (selected_group: number) => { - group_list.children().remove(); - - const allow_query_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_QUERYGROUP).granted(1); - const allow_template_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_TEMPLATES).granted(1); - for (let group of connection.groups.channelGroups.sort(GroupManager.sorter())) { - if (group.type == GroupType.QUERY) { - if (!allow_query_groups) - continue; - } else if (group.type == GroupType.TEMPLATE) { - if (!allow_template_groups) - continue; - } - - let tag = $.spawn("div").addClass("group").attr("group-id", group.id); - let icon_tag = connection.fileManager.icons.generateTag(group.properties.iconid); - icon_tag.appendTo(tag); - const _update_icon = icon_id => icon_tag.replaceWith(icon_tag = connection.fileManager.icons.generateTag(icon_id)); - - { - let name = $.spawn("a").text(group.name + " (" + group.id + ")").addClass("name"); - if (group.properties.savedb) - name.addClass("savedb"); - if (connection.channelTree.server.properties.virtualserver_default_channel_group == group.id) - name.addClass("default"); - name.appendTo(tag); - } - tag.appendTo(group_list); - - tag.on('click', event => { - current_group = group; - update_group_icon = _update_icon; - group_list.find(".selected").removeClass("selected"); - tag.addClass("selected"); - - update_buttons(); - //TODO trigger only if the editor is in channel group mode! - editor.trigger_update(); - }); - tag.on('contextmenu', event => { - if(event.isDefaultPrevented()) - return; - - contextmenu.spawn_context_menu(event.pageX, event.pageY, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Create a channel group"), - icon_class: 'client-add', - invalidPermission: !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_CREATE).granted(1), - callback: () => tab_left.find(".button-add").trigger('click') - }, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Rename channel group"), - icon_class: 'client-edit', - invalidPermission: !connection.permissions.neededPermission(PermissionType.I_CHANNEL_GROUP_MODIFY_POWER).granted(current_group.requiredModifyPower), - callback: () => tab_left.find(".button-rename").trigger('click') - }, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Duplicate channel group"), - icon_class: 'client-copy', - callback: () => tab_left.find(".button-duplicate").trigger('click') - }, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Delete channel group"), - icon_class: 'client-delete', - invalidPermission: !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_DELETE).granted(1), - callback: () => tab_left.find(".button-delete").trigger('click') - }); - event.preventDefault(); - }); - if(group.id === selected_group) { - setTimeout(() => tag.trigger('click'), 0); - selected_group = undefined; - } - } - - /* because the server menu is the first which will be shown */ - if(typeof(selected_group) !== "undefined") { - setTimeout(() => group_list.find('.group').first().trigger('click'), 0); - } - }; - - tab_left.find(".list-groups").on('contextmenu', event => { - if(event.isDefaultPrevented()) - return; - - contextmenu.spawn_context_menu(event.pageX, event.pageY, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Create a channel group"), - icon_class: 'client-add', - callback: () => tab_left.find(".button-add").trigger('click') - }); - event.preventDefault(); - }); - } - - { - const container_buttons = tab_left.find(".container-buttons"); - - const button_add = container_buttons.find(".button-add"); - const button_rename = container_buttons.find(".button-rename"); - const button_duplicate = container_buttons.find(".button-duplicate"); - const button_delete = container_buttons.find(".button-delete"); - - button_add.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_CREATE).granted(1)); - button_delete.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_CREATE).granted(1)); - update_buttons = () => { - const permission_modify = current_group && connection.permissions.neededPermission(PermissionType.I_CHANNEL_GROUP_MODIFY_POWER).granted(current_group.requiredModifyPower); - button_rename.prop("disabled", !permission_modify); - button_duplicate.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_CREATE).granted(1)); - }; - - button_add.on('click', () => { - spawnGroupAdd(false, connection.permissions, (name, type) => name.length > 0 && !connection.groups.channelGroups.find(e => e.target == GroupTarget.CHANNEL && e.name.toLowerCase() === name.toLowerCase() && e.type == type) , (name, type) => { - console.log("Creating channel group: %o, %o", name, type); - connection.serverConnection.send_command('channelgroupadd', { - name: name, - type: type - }).then(() => { - createInfoModal(tr("Group created"), tr("The channel group has been created.")).open(); - update_groups(0); //TODO: May get the created group? - }).catch(error => { - console.warn(tr("Failed to create channel group: %o"), error); - if(error instanceof CommandResult) { - error = error.extra_message || error.message; - } - createErrorModal(tr("Failed to create group"), MessageHelper.formatMessage(tr("Failed to create group:{:br:}"), error)).open(); - }); - }); - }); - - button_rename.on('click', () => { - if(!current_group) - return; - - createInputModal(tr("Rename group"), tr("Enter the new group name"), name => name.length > 0 && !connection.groups.channelGroups.find(e => e.target == GroupTarget.CHANNEL && e.name.toLowerCase() === name.toLowerCase() && e.type == current_group.type), result => { - if(typeof(result) !== "string" || !result) - return; - connection.serverConnection.send_command('channelgrouprename', { - cgid: current_group.id, - name: result - }).then(() => { - createInfoModal(tr("Group renamed"), tr("The channel group has been renamed.")).open(); - update_groups(current_group.id); - }).catch(error => { - console.warn(tr("Failed to rename channel group: %o"), error); - if(error instanceof CommandResult) { - error = error.extra_message || error.message; - } - createErrorModal(tr("Failed to rename group"), MessageHelper.formatMessage(tr("Failed to rename group:{:br:}"), error)).open(); - }); - }).open(); - }); - - button_duplicate.on('click', () => { - createErrorModal(tr("Not implemented yet"), tr("This function hasn't been implemented yet!")).open(); - }); - - button_delete.on('click', () => { - if(!current_group) - return; - - spawnYesNo(tr("Are you sure?"), MessageHelper.formatMessage(tr("Do you really want to delete the group {}?"), current_group.name), result => { - if(result !== true) - return; - - connection.serverConnection.send_command("channelgroupdel", { - cgid: current_group.id, - force: true - }).then(() => { - createInfoModal(tr("Group deleted"), tr("The channel group has been deleted.")).open(); - update_groups(0); - }).catch(error => { - console.warn(tr("Failed to delete channel group: %o"), error); - if(error instanceof CommandResult) { - error = error.extra_message || error.message; - } - createErrorModal(tr("Failed to delete group"), MessageHelper.formatMessage(tr("Failed to delete group:{:br:}"), error)).open(); - }); - }); - }); - } - update_groups(0); - } - - function apply_server_groups(connection: ConnectionHandler, editor: AbstractPermissionEditor, tab_left: JQuery, tab_right: JQuery) { - let current_group: Group; - let current_group_changed: (() => any)[] = []; - - let update_buttons: () => any; - /* list all groups */ - - let update_icon: ((icon_id: number) => any)[] = []; - - let update_groups: (selected_group: number) => any; - { - let group_list = tab_left.find(".container-group-list .list-groups .entries"); - let group_list_update_icon: (i: number) => any; - update_icon.push(i => group_list_update_icon(i)); - - update_groups = (selected_group: number) => { - group_list.children().remove(); - - const allow_query_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_QUERYGROUP).granted(1); - const allow_template_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_TEMPLATES).granted(1); - for(const group of connection.groups.serverGroups.sort(GroupManager.sorter())) { - if(group.type == GroupType.QUERY) { - if(!allow_query_groups) - continue; - } else if(group.type == GroupType.TEMPLATE) { - if(!allow_template_groups) - continue; - } - let tag = $.spawn("div").addClass("group").attr("group-id", group.id); - let icon_tag = connection.fileManager.icons.generateTag(group.properties.iconid); - icon_tag.appendTo(tag); - const _update_icon = icon_id => icon_tag.replaceWith(icon_tag = connection.fileManager.icons.generateTag(icon_id)); - - { - let name = $.spawn("div").text(group.name + " (" + group.id + ")").addClass("name"); - if(group.properties.savedb) - name.addClass("savedb"); - if(connection.channelTree.server.properties.virtualserver_default_server_group == group.id) - name.addClass("default"); - name.appendTo(tag); - } - tag.appendTo(group_list); - - tag.on('click', event => { - if(current_group === group) - return; - - current_group = group; - group_list_update_icon = _update_icon; - if(update_buttons) - update_buttons(); - for(const entry of current_group_changed) - entry(); - - group_list.find(".selected").removeClass("selected"); - tag.addClass("selected"); - editor.trigger_update(); - }); - tag.on('contextmenu', event => { - if(event.isDefaultPrevented()) - return; - - contextmenu.spawn_context_menu(event.pageX, event.pageY, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Create a server group"), - icon_class: 'client-add', - invalidPermission: !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_CREATE).granted(1), - callback: () => tab_left.find(".button-add").trigger('click') - }, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Rename server group"), - icon_class: 'client-edit', - invalidPermission: !connection.permissions.neededPermission(PermissionType.I_SERVER_GROUP_MODIFY_POWER).granted(current_group.requiredModifyPower), - callback: () => tab_left.find(".button-rename").trigger('click') - }, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Duplicate server group"), - icon_class: 'client-copy', - callback: () => tab_left.find(".button-duplicate").trigger('click') - }, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Delete server group"), - icon_class: 'client-delete', - invalidPermission: !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_DELETE).granted(1), - callback: () => tab_left.find(".button-delete").trigger('click') - }); - event.preventDefault(); - }); - - if(group.id === selected_group) { - setTimeout(() => tag.trigger('click'), 0); - selected_group = undefined; - } - } - - /* because the server menu is the first which will be shown */ - if(typeof(selected_group) !== "undefined") { - setTimeout(() => group_list.find('.group').first().trigger('click'), 0); - } - }; - - - tab_left.find(".list-groups").on('contextmenu', event => { - if(event.isDefaultPrevented()) - return; - - contextmenu.spawn_context_menu(event.pageX, event.pageY, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Create a server group"), - icon_class: 'client-add', - callback: () => tab_left.find(".button-add").trigger('click') - }); - event.preventDefault(); - }); - } - { - const container_buttons = tab_left.find(".container-group-list .container-buttons"); - - const button_add = container_buttons.find(".button-add"); - const button_rename = container_buttons.find(".button-rename"); - const button_duplicate = container_buttons.find(".button-duplicate"); - const button_delete = container_buttons.find(".button-delete"); - - button_add.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_CREATE).granted(1)); - button_delete.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_DELETE).granted(1)); - update_buttons = () => { - const permission_modify = current_group && connection.permissions.neededPermission(PermissionType.I_SERVER_GROUP_MODIFY_POWER).granted(current_group.requiredModifyPower); - button_rename.prop("disabled", !permission_modify); - button_duplicate.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_CREATE).granted(1)); - }; - - button_add.on('click', () => { - spawnGroupAdd(true, connection.permissions, (name, type) => name.length > 0 && !connection.groups.serverGroups.find(e => e.target == GroupTarget.SERVER && e.name.toLowerCase() === name.toLowerCase() && e.type == type) , (name, type) => { - console.log("Creating group: %o, %o", name, type); - connection.serverConnection.send_command('servergroupadd', { - name: name, - type: type - }).then(() => { - createInfoModal(tr("Group created"), tr("The server group has been created.")).open(); - update_groups(0); //TODO: May get the created group? - }).catch(error => { - console.warn(tr("Failed to create server group: %o"), error); - if(error instanceof CommandResult) { - error = error.extra_message || error.message; - } - createErrorModal(tr("Failed to create group"), MessageHelper.formatMessage(tr("Failed to create group:{:br:}"), error)).open(); - }); - }); - }); - - button_rename.on('click', () => { - if(!current_group) - return; - - createInputModal(tr("Rename group"), tr("Enter the new group name"), name => name.length > 0 && !connection.groups.serverGroups.find(e => e.target == GroupTarget.SERVER && e.name.toLowerCase() === name.toLowerCase() && e.type == current_group.type), result => { - if(typeof(result) !== "string" || !result) - return; - connection.serverConnection.send_command('servergrouprename', { - sgid: current_group.id, - name: result - }).then(() => { - createInfoModal(tr("Group renamed"), tr("The server group has been renamed.")).open(); - update_groups(current_group.id); - }).catch(error => { - console.warn(tr("Failed to rename server group: %o"), error); - if(error instanceof CommandResult) { - error = error.extra_message || error.message; - } - createErrorModal(tr("Failed to rename group"), MessageHelper.formatMessage(tr("Failed to rename group:{:br:}"), error)).open(); - }); - }).open(); - }); - - button_duplicate.on('click', () => { - createErrorModal(tr("Not implemented yet"), tr("This function hasn't been implemented yet!")).open(); - }); - - button_delete.on('click', () => { - if(!current_group) - return; - - spawnYesNo(tr("Are you sure?"), MessageHelper.formatMessage(tr("Do you really want to delete the group {}?"), current_group.name), result => { - if(result !== true) - return; - - connection.serverConnection.send_command("servergroupdel", { - sgid: current_group.id, - force: true - }).then(() => { - createInfoModal(tr("Group deleted"), tr("The server group has been deleted.")).open(); - update_groups(0); - }).catch(error => { - console.warn(tr("Failed to delete server group: %o"), error); - if(error instanceof CommandResult) { - error = error.extra_message || error.message; - } - createErrorModal(tr("Failed to delete group"), MessageHelper.formatMessage(tr("Failed to delete group:{:br:}"), error)).open(); - }); - }); - }); - } - update_groups(0); - - /* the editor */ - { - const pe_server = tab_right.find(".permission-editor"); - tab_right.on('show', event => { - pe_server.append(editor.html_tag()); - if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_PERMISSION_LIST).granted(1)) - editor.set_mode(PermissionEditorMode.VISIBLE); - else { - editor.set_mode(PermissionEditorMode.NO_PERMISSION); - return; - } - editor.set_listener_update(() => { - console.log("Updating permissions"); - connection.groups.request_permissions(current_group).then(result => editor.set_permissions(result)).catch(error => { - console.log(error); //TODO handling? - }); - }); - - editor.set_listener(async (permission, value) => { - if (!current_group) - throw "unset server group"; - - if (value.remove) { - /* remove the permission */ - if (typeof (value.value) !== "undefined") { - log.info(LogCategory.PERMISSIONS, tr("Removing server group permission %s. permission.id: %o"), - permission.name, - permission.id, - ); - - await connection.serverConnection.send_command("servergroupdelperm", { - sgid: current_group.id, - permid: permission.id, - }).then(e => { - if(permission.name === "i_icon_id") - for(const c of update_icon) - c(0); - return e; - }); - } else { - log.info(LogCategory.PERMISSIONS, tr("Removing server group grant permission %s. permission.id: %o"), - permission.name, - permission.id_grant(), - value.granted, - ); - - await connection.serverConnection.send_command("servergroupdelperm", { - sgid: current_group.id, - permid: permission.id_grant(), - }); - } - } else { - /* add the permission */ - if (typeof (value.value) !== "undefined") { - log.info(LogCategory.PERMISSIONS, tr("Adding or updating server group permission %s. permission.{id: %o, value: %o, flag_skip: %o, flag_negate: %o}"), - permission.name, - permission.id, - value.value, - value.flag_skip, - value.flag_negate - ); - - await connection.serverConnection.send_command("servergroupaddperm", { - sgid: current_group.id, - permid: permission.id, - permvalue: value.value, - permskip: value.flag_skip, - permnegated: value.flag_negate - }).then(e => { - if(permission.name === "i_icon_id") - for(const c of update_icon) - c(value.value); - return e; - }); - } else { - log.info(LogCategory.PERMISSIONS, tr("Adding or updating server group grant permission %s. permission.{id: %o, value: %o}"), - permission.name, - permission.id_grant(), - value.granted, - ); - - await connection.serverConnection.send_command("servergroupaddperm", { - sgid: current_group.id, - permid: permission.id_grant(), - permvalue: value.granted, - permskip: false, - permnegated: false - }); - } - } - }); - - editor.trigger_update(); - }); - } - - /* client list */ - { - //container-client-list container-group-list - let clients_visible = false; - let selected_client: { - tag: JQuery, - dbid: number - }; - - const container_client_list = tab_left.find(".container-client-list").addClass("hidden"); - const container_group_list = tab_left.find(".container-group-list"); - - const container_selected_group = container_client_list.find(".container-current-group"); - const container_clients = container_client_list.find(".list-clients .entries"); - - const input_filter = container_client_list.find(".filter-client-list"); - - const button_add = container_client_list.find(".button-add"); - const button_delete = container_client_list.find(".button-delete"); - - const update_filter = () => { - const filter_text = (input_filter.val() || "").toString().toLowerCase(); - if(!filter_text) { - container_clients.find(".entry").css('display', 'block'); - } else { - const entries = container_clients.find(".entry"); - for(const _entry of entries) { - const entry = $(_entry); - if(entry.attr("search-string").toLowerCase().indexOf(filter_text) !== -1) - entry.css('display', 'block'); - else - entry.css('display', 'none'); - } - } - }; - - const update_client_list = () => { - container_clients.empty(); - button_delete.prop('disabled', true); - - connection.serverConnection.command_helper.request_clients_by_server_group(current_group.id).then(clients => { - for(const client of clients) { - const tag = $.spawn("div").addClass("client").text(client.client_nickname); - tag.attr("search-string", client.client_nickname + "-" + client.client_unique_identifier + "-" + client.client_database_id); - container_clients.append(tag); - - tag.on('click contextmenu', event => { - container_clients.find(".selected").removeClass("selected"); - tag.addClass("selected"); - - selected_client = { - tag: tag, - dbid: client.client_database_id - }; - button_delete.prop('disabled', false); - }); - - tag.on('contextmenu', event => { - if(event.isDefaultPrevented()) - return; - - event.preventDefault(); - contextmenu.spawn_context_menu(event.pageX, event.pageY, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Add client"), - icon_class: 'client-add', - callback: () => button_add.trigger('click') - }, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Remove client"), - icon_class: 'client-delete', - callback: () => button_delete.trigger('click') - }, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Copy unique id"), - icon_class: 'client-copy', - callback: () => copy_to_clipboard(client.client_unique_identifier) - }) - }); - } - update_filter(); - }).catch(error => { - if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) - return; - console.warn(tr("Failed to receive server group clients for group %d: %o"), current_group.id, error); - }); - }; - current_group_changed.push(update_client_list); - - button_delete.on('click', event => { - const client = selected_client; - if(!client) return; - - connection.serverConnection.send_command("servergroupdelclient", { - sgid: current_group.id, - cldbid: client.dbid - }).then(() => { - selected_client.tag.detach(); - button_delete.prop('disabled', true); /* nothing is selected */ - }).catch(error => { - console.log(tr("Failed to delete client %o from server group %o: %o"), client.dbid, current_group.id, error); - if(error instanceof CommandResult) - error = error.extra_message || error.message; - createErrorModal(tr("Failed to remove client"), tr("Failed to remove client from server group")).open(); - }); - }); - - button_add.on('click', event => { - createInputModal(tr("Add client to server group"), tr("Enter the client unique id or database id"), text => { - if(!text) return false; - if(!!text.match(/^[0-9]+$/)) - return true; - try { - return atob(text).length >= 20; - } catch(error) { - return false; - } - }, async text => { - if(typeof(text) !== "string") - return; - - let dbid; - if(!!text.match(/^[0-9]+$/)) { - dbid = parseInt(text); - debugger; - } else { - try { - const data = await connection.serverConnection.command_helper.info_from_uid(text.trim()); - dbid = data[0].client_database_id; - } catch(error) { - console.log(tr("Failed to resolve client database id from unique id (%s): %o"), text, error); - if(error instanceof CommandResult) - error = error.extra_message || error.message; - createErrorModal(tr("Failed to add client"), MessageHelper.formatMessage(tr("Failed to add client to server group\nFailed to resolve database id: {}."), error)).open(); - return; - } - } - if(!dbid) { - console.log(tr("Failed to resolve client database id from unique id (%s): Client not found")); - createErrorModal(tr("Failed to add client"), tr("Failed to add client to server group\nClient database id not found")).open(); - return; - } - - - connection.serverConnection.send_command("servergroupaddclient", { - sgid: current_group.id, - cldbid: dbid - }).then(() => { - update_client_list(); - }).catch(error => { - console.log(tr("Failed to add client %o to server group %o: %o"), dbid, current_group.id, error); - if(error instanceof CommandResult) - error = error.extra_message || error.message; - createErrorModal(tr("Failed to add client"), tr("Failed to add client to server group\n" + error)).open(); - }); - }).open(); - }); - - container_client_list.on('contextmenu', event => { - if(event.isDefaultPrevented()) - return; - - event.preventDefault(); - contextmenu.spawn_context_menu(event.pageX, event.pageY, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Add client"), - icon_class: 'client-add', - callback: () => button_add.trigger('click') - }) - }); - - /* icon handler and current group display */ - { - let update_icon_callback: (i: number) => any; - update_icon.push(i => update_icon_callback(i)); - - input_filter.on('change keyup', event => update_filter()); - current_group_changed.push(() => { - container_selected_group.empty(); - if(!current_group) return; - - let icon_container = $.spawn("div").addClass("icon-container").appendTo(container_selected_group); - - connection.fileManager.icons.generateTag(current_group.properties.iconid).appendTo(icon_container); - update_icon_callback = icon => { - icon_container.empty(); - connection.fileManager.icons.generateTag(icon).appendTo(icon_container); - }; - $.spawn("div").addClass("name").text(current_group.name + " (" + current_group.id + ")").appendTo(container_selected_group); - }); + const tag_select = tab_left.find(".client-select"); + const tag_select_uid = tag_select.find("input"); + const tag_select_error = tag_select.find(".invalid-feedback"); + + const tag_client_name = tab_left.find(".client-name"); + const tag_client_uid = tab_left.find(".client-uid"); + const tag_client_dbid = tab_left.find(".client-dbid"); + + const resolve_client = () => { + let client_uid = tag_select_uid.val() as string; + connection.serverConnection.command_helper.info_from_uid(client_uid).then(result => { + if(!result || result.length == 0) return Promise.reject("invalid data"); + tag_select.removeClass("is-invalid"); + + tag_client_name.val(result[0].client_nickname ); + tag_client_uid.val(result[0].client_unique_id); + tag_client_dbid.val(result[0].client_database_id); + + current_cldbid = result[0].client_database_id; + editor.trigger_update(); + }).catch(error => { + console.log(error); + if(error instanceof CommandResult) { + if(error.id == ErrorID.EMPTY_RESULT) + error = "unknown client"; + else + error = error.extra_message || error.message; } - tab_right.on('show', event => { - editor.set_toggle_button(() => { - clients_visible = !clients_visible; + tag_client_name.val(""); + tag_client_uid.val(""); + tag_client_dbid.val(""); - container_client_list.toggleClass("hidden", !clients_visible); - container_group_list.toggleClass("hidden", clients_visible); + tag_select_error.text(error); + tag_select.addClass("is-invalid"); + editor.set_mode(PermissionEditorMode.UNSET); + }); + }; - return clients_visible ? tr("Hide clients in group") : tr("Show clients in group"); - }, clients_visible ? tr("Hide clients in group") : tr("Show clients in group")); + tag_select_uid.on('change', event => resolve_client()); + if(options.unique_id) { + tag_select_uid.val(options.unique_id); + setTimeout(() => resolve_client()); + } +} + +function apply_channel_permission(connection: ConnectionHandler, editor: AbstractPermissionEditor, tab_left: JQuery, tab_right: JQuery) { + let current_channel: ChannelEntry | undefined; + let update_channel_icon: (id: number) => any; + + /* the editor */ + { + const pe_channel = tab_right.find(".permission-editor"); + tab_right.on('show', event => { + editor.set_toggle_button(undefined, undefined); + pe_channel.append(editor.html_tag()); + if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNEL_PERMISSION_LIST).granted(1)) + editor.set_mode(PermissionEditorMode.VISIBLE); + else { + editor.set_mode(PermissionEditorMode.NO_PERMISSION); + return; + } + + editor.set_listener_update(() => { + if(!current_channel) return; + + connection.permissions.requestChannelPermissions(current_channel.channelId).then(result => editor.set_permissions(result)).catch(error => { + editor.set_permissions([]); + console.log(error); //TODO handling? + }); + }); + + editor.set_listener(async (permission, value) => { + if (!current_channel) + throw "unset channel"; + + if (value.remove) { + /* remove the permission */ + if (typeof (value.value) !== "undefined") { + log.info(LogCategory.PERMISSIONS, tr("Removing channel permission %s. permission.id: %o"), + permission.name, + permission.id, + ); + + await connection.serverConnection.send_command("channeldelperm", { + cid: current_channel.channelId, + permid: permission.id, + }).then(e => { + if(permission.name === "i_icon_id" && update_channel_icon) + update_channel_icon(undefined); + return e; + }); + } else { + /* TODO Remove this because its totally useless. Remove this from the UI as well */ + log.info(LogCategory.PERMISSIONS, tr("Removing channel grant permission %s. permission.id: %o"), + permission.name, + permission.id_grant(), + value.granted, + ); + + await connection.serverConnection.send_command("channeldelperm", { + cid: current_channel.channelId, + permid: permission.id_grant(), + }); + } + } else { + /* add the permission */ + if (typeof (value.value) !== "undefined") { + log.info(LogCategory.PERMISSIONS, tr("Adding or updating channel permission %s. permission.{id: %o, value: %o, flag_skip: %o, flag_negate: %o}"), + permission.name, + permission.id, + value.value, + value.flag_skip, + value.flag_negate + ); + + await connection.serverConnection.send_command("channeladdperm", { + cid: current_channel.channelId, + permid: permission.id, + permvalue: value.value, + permskip: value.flag_skip, + permnegated: value.flag_negate + }).then(e => { + if(permission.name === "i_icon_id" && update_channel_icon) + update_channel_icon(value.value); + return e; + }); + } else { + /* TODO Remove this because its totally useless. Remove this from the UI as well */ + log.info(LogCategory.PERMISSIONS, tr("Adding or updating channel grant permission %s. permission.{id: %o, value: %o}"), + permission.name, + permission.id_grant(), + value.granted, + ); + + await connection.serverConnection.send_command("channeladdperm", { + cid: current_channel.channelId, + permid: permission.id_grant(), + permvalue: value.granted, + permskip: false, + permnegated: false + }); + } + } + }); + + /* FIXME: Use cached permissions */ + editor.trigger_update(); + }); + } + + let channel_list = tab_left.find(".list-channel .entries"); + build_channel_tree(connection, channel_list, 0, (channel, update) => { + current_channel = channel; + update_channel_icon = update; + editor.trigger_update(); + }); +} + +function apply_channel_groups(connection: ConnectionHandler, editor: AbstractPermissionEditor, tab_left: JQuery, tab_right: JQuery) { + let current_group; + let update_group_icon: (id: number) => any; + let update_groups: (selected_group: number) => any; + let update_buttons: () => any; + + /* the editor */ + { + const pe_server = tab_right.find(".permission-editor"); + tab_right.on('show', event => { + editor.set_toggle_button(undefined, undefined); + pe_server.append(editor.html_tag()); + if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_PERMISSION_LIST).granted(1)) + editor.set_mode(PermissionEditorMode.VISIBLE); + else { + editor.set_mode(PermissionEditorMode.NO_PERMISSION); + return; + } + + editor.set_listener_update(() => { + if(!current_group) return; + + connection.groups.request_permissions(current_group).then(result => editor.set_permissions(result)).catch(error => { + console.log(error); //TODO handling? + }); + }); + + editor.set_listener(async (permission, value) => { + if (!current_group) + throw "unset channel group"; + + if (value.remove) { + /* remove the permission */ + if (typeof (value.value) !== "undefined") { + log.info(LogCategory.PERMISSIONS, tr("Removing channel group permission %s. permission.id: %o"), + permission.name, + permission.id, + ); + + await connection.serverConnection.send_command("channelgroupdelperm", { + cgid: current_group.id, + permid: permission.id, + }).then(e => { + if(permission.name === "i_icon_id" && update_group_icon) + update_group_icon(undefined); + return e; + }); + } else { + log.info(LogCategory.PERMISSIONS, tr("Removing channel group grant permission %s. permission.id: %o"), + permission.name, + permission.id_grant(), + value.granted, + ); + + await connection.serverConnection.send_command("channelgroupdelperm", { + cgid: current_group.id, + permid: permission.id_grant(), + }); + } + } else { + /* add the permission */ + if (typeof (value.value) !== "undefined") { + log.info(LogCategory.PERMISSIONS, tr("Adding or updating channel group permission %s. permission.{id: %o, value: %o, flag_skip: %o, flag_negate: %o}"), + permission.name, + permission.id, + value.value, + value.flag_skip, + value.flag_negate + ); + + await connection.serverConnection.send_command("channelgroupaddperm", { + cgid: current_group.id, + permid: permission.id, + permvalue: value.value, + permskip: value.flag_skip, + permnegated: value.flag_negate + }).then(e => { + if(permission.name === "i_icon_id" && update_group_icon) + update_group_icon(value.value); + return e; + }); + } else { + log.info(LogCategory.PERMISSIONS, tr("Adding or updating channel group grant permission %s. permission.{id: %o, value: %o}"), + permission.name, + permission.id_grant(), + value.granted, + ); + + await connection.serverConnection.send_command("channelgroupaddperm", { + cgid: current_group.id, + permid: permission.id_grant(), + permvalue: value.granted, + permskip: false, + permnegated: false + }); + } + } + }); + + /* FIXME: Use cached permissions */ + editor.trigger_update(); + }); + } + + /* list all channel groups */ + { + let group_list = tab_left.find(".list-groups .entries"); + + update_groups = (selected_group: number) => { + group_list.children().remove(); + + const allow_query_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_QUERYGROUP).granted(1); + const allow_template_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_TEMPLATES).granted(1); + for (let group of connection.groups.channelGroups.sort(GroupManager.sorter())) { + if (group.type == GroupType.QUERY) { + if (!allow_query_groups) + continue; + } else if (group.type == GroupType.TEMPLATE) { + if (!allow_template_groups) + continue; + } + + let tag = $.spawn("div").addClass("group").attr("group-id", group.id); + let icon_tag = connection.fileManager.icons.generateTag(group.properties.iconid); + icon_tag.appendTo(tag); + const _update_icon = icon_id => icon_tag.replaceWith(icon_tag = connection.fileManager.icons.generateTag(icon_id)); + + { + let name = $.spawn("a").text(group.name + " (" + group.id + ")").addClass("name"); + if (group.properties.savedb) + name.addClass("savedb"); + if (connection.channelTree.server.properties.virtualserver_default_channel_group == group.id) + name.addClass("default"); + name.appendTo(tag); + } + tag.appendTo(group_list); + + tag.on('click', event => { + current_group = group; + update_group_icon = _update_icon; + group_list.find(".selected").removeClass("selected"); + tag.addClass("selected"); + + update_buttons(); + //TODO trigger only if the editor is in channel group mode! + editor.trigger_update(); + }); + tag.on('contextmenu', event => { + if(event.isDefaultPrevented()) + return; + + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Create a channel group"), + icon_class: 'client-add', + invalidPermission: !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_CREATE).granted(1), + callback: () => tab_left.find(".button-add").trigger('click') + }, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Rename channel group"), + icon_class: 'client-edit', + invalidPermission: !connection.permissions.neededPermission(PermissionType.I_CHANNEL_GROUP_MODIFY_POWER).granted(current_group.requiredModifyPower), + callback: () => tab_left.find(".button-rename").trigger('click') + }, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Duplicate channel group"), + icon_class: 'client-copy', + callback: () => tab_left.find(".button-duplicate").trigger('click') + }, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Delete channel group"), + icon_class: 'client-delete', + invalidPermission: !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_DELETE).granted(1), + callback: () => tab_left.find(".button-delete").trigger('click') + }); + event.preventDefault(); + }); + if(group.id === selected_group) { + setTimeout(() => tag.trigger('click'), 0); + selected_group = undefined; + } + } + + /* because the server menu is the first which will be shown */ + if(typeof(selected_group) !== "undefined") { + setTimeout(() => group_list.find('.group').first().trigger('click'), 0); + } + }; + + tab_left.find(".list-groups").on('contextmenu', event => { + if(event.isDefaultPrevented()) + return; + + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Create a channel group"), + icon_class: 'client-add', + callback: () => tab_left.find(".button-add").trigger('click') + }); + event.preventDefault(); + }); + } + + { + const container_buttons = tab_left.find(".container-buttons"); + + const button_add = container_buttons.find(".button-add"); + const button_rename = container_buttons.find(".button-rename"); + const button_duplicate = container_buttons.find(".button-duplicate"); + const button_delete = container_buttons.find(".button-delete"); + + button_add.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_CREATE).granted(1)); + button_delete.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_CREATE).granted(1)); + update_buttons = () => { + const permission_modify = current_group && connection.permissions.neededPermission(PermissionType.I_CHANNEL_GROUP_MODIFY_POWER).granted(current_group.requiredModifyPower); + button_rename.prop("disabled", !permission_modify); + button_duplicate.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_CREATE).granted(1)); + }; + + button_add.on('click', () => { + spawnGroupAdd(false, connection.permissions, (name, type) => name.length > 0 && !connection.groups.channelGroups.find(e => e.target == GroupTarget.CHANNEL && e.name.toLowerCase() === name.toLowerCase() && e.type == type) , (name, type) => { + console.log("Creating channel group: %o, %o", name, type); + connection.serverConnection.send_command('channelgroupadd', { + name: name, + type: type + }).then(() => { + createInfoModal(tr("Group created"), tr("The channel group has been created.")).open(); + update_groups(0); //TODO: May get the created group? + }).catch(error => { + console.warn(tr("Failed to create channel group: %o"), error); + if(error instanceof CommandResult) { + error = error.extra_message || error.message; + } + createErrorModal(tr("Failed to create group"), formatMessage(tr("Failed to create group:{:br:}"), error)).open(); + }); + }); + }); + + button_rename.on('click', () => { + if(!current_group) + return; + + createInputModal(tr("Rename group"), tr("Enter the new group name"), name => name.length > 0 && !connection.groups.channelGroups.find(e => e.target == GroupTarget.CHANNEL && e.name.toLowerCase() === name.toLowerCase() && e.type == current_group.type), result => { + if(typeof(result) !== "string" || !result) + return; + connection.serverConnection.send_command('channelgrouprename', { + cgid: current_group.id, + name: result + }).then(() => { + createInfoModal(tr("Group renamed"), tr("The channel group has been renamed.")).open(); + update_groups(current_group.id); + }).catch(error => { + console.warn(tr("Failed to rename channel group: %o"), error); + if(error instanceof CommandResult) { + error = error.extra_message || error.message; + } + createErrorModal(tr("Failed to rename group"), formatMessage(tr("Failed to rename group:{:br:}"), error)).open(); + }); + }).open(); + }); + + button_duplicate.on('click', () => { + createErrorModal(tr("Not implemented yet"), tr("This function hasn't been implemented yet!")).open(); + }); + + button_delete.on('click', () => { + if(!current_group) + return; + + spawnYesNo(tr("Are you sure?"), formatMessage(tr("Do you really want to delete the group {}?"), current_group.name), result => { + if(result !== true) + return; + + connection.serverConnection.send_command("channelgroupdel", { + cgid: current_group.id, + force: true + }).then(() => { + createInfoModal(tr("Group deleted"), tr("The channel group has been deleted.")).open(); + update_groups(0); + }).catch(error => { + console.warn(tr("Failed to delete channel group: %o"), error); + if(error instanceof CommandResult) { + error = error.extra_message || error.message; + } + createErrorModal(tr("Failed to delete group"), formatMessage(tr("Failed to delete group:{:br:}"), error)).open(); + }); + }); + }); + } + update_groups(0); +} + +function apply_server_groups(connection: ConnectionHandler, editor: AbstractPermissionEditor, tab_left: JQuery, tab_right: JQuery) { + let current_group: Group; + let current_group_changed: (() => any)[] = []; + + let update_buttons: () => any; + /* list all groups */ + + let update_icon: ((icon_id: number) => any)[] = []; + + let update_groups: (selected_group: number) => any; + { + let group_list = tab_left.find(".container-group-list .list-groups .entries"); + let group_list_update_icon: (i: number) => any; + update_icon.push(i => group_list_update_icon(i)); + + update_groups = (selected_group: number) => { + group_list.children().remove(); + + const allow_query_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_QUERYGROUP).granted(1); + const allow_template_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_TEMPLATES).granted(1); + for(const group of connection.groups.serverGroups.sort(GroupManager.sorter())) { + if(group.type == GroupType.QUERY) { + if(!allow_query_groups) + continue; + } else if(group.type == GroupType.TEMPLATE) { + if(!allow_template_groups) + continue; + } + let tag = $.spawn("div").addClass("group").attr("group-id", group.id); + let icon_tag = connection.fileManager.icons.generateTag(group.properties.iconid); + icon_tag.appendTo(tag); + const _update_icon = icon_id => icon_tag.replaceWith(icon_tag = connection.fileManager.icons.generateTag(icon_id)); + + { + let name = $.spawn("div").text(group.name + " (" + group.id + ")").addClass("name"); + if(group.properties.savedb) + name.addClass("savedb"); + if(connection.channelTree.server.properties.virtualserver_default_server_group == group.id) + name.addClass("default"); + name.appendTo(tag); + } + tag.appendTo(group_list); + + tag.on('click', event => { + if(current_group === group) + return; + + current_group = group; + group_list_update_icon = _update_icon; + if(update_buttons) + update_buttons(); + for(const entry of current_group_changed) + entry(); + + group_list.find(".selected").removeClass("selected"); + tag.addClass("selected"); + editor.trigger_update(); + }); + tag.on('contextmenu', event => { + if(event.isDefaultPrevented()) + return; + + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Create a server group"), + icon_class: 'client-add', + invalidPermission: !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_CREATE).granted(1), + callback: () => tab_left.find(".button-add").trigger('click') + }, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Rename server group"), + icon_class: 'client-edit', + invalidPermission: !connection.permissions.neededPermission(PermissionType.I_SERVER_GROUP_MODIFY_POWER).granted(current_group.requiredModifyPower), + callback: () => tab_left.find(".button-rename").trigger('click') + }, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Duplicate server group"), + icon_class: 'client-copy', + callback: () => tab_left.find(".button-duplicate").trigger('click') + }, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Delete server group"), + icon_class: 'client-delete', + invalidPermission: !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_DELETE).granted(1), + callback: () => tab_left.find(".button-delete").trigger('click') + }); + event.preventDefault(); + }); + + if(group.id === selected_group) { + setTimeout(() => tag.trigger('click'), 0); + selected_group = undefined; + } + } + + /* because the server menu is the first which will be shown */ + if(typeof(selected_group) !== "undefined") { + setTimeout(() => group_list.find('.group').first().trigger('click'), 0); + } + }; + + + tab_left.find(".list-groups").on('contextmenu', event => { + if(event.isDefaultPrevented()) + return; + + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Create a server group"), + icon_class: 'client-add', + callback: () => tab_left.find(".button-add").trigger('click') + }); + event.preventDefault(); + }); + } + { + const container_buttons = tab_left.find(".container-group-list .container-buttons"); + + const button_add = container_buttons.find(".button-add"); + const button_rename = container_buttons.find(".button-rename"); + const button_duplicate = container_buttons.find(".button-duplicate"); + const button_delete = container_buttons.find(".button-delete"); + + button_add.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_CREATE).granted(1)); + button_delete.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_DELETE).granted(1)); + update_buttons = () => { + const permission_modify = current_group && connection.permissions.neededPermission(PermissionType.I_SERVER_GROUP_MODIFY_POWER).granted(current_group.requiredModifyPower); + button_rename.prop("disabled", !permission_modify); + button_duplicate.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_CREATE).granted(1)); + }; + + button_add.on('click', () => { + spawnGroupAdd(true, connection.permissions, (name, type) => name.length > 0 && !connection.groups.serverGroups.find(e => e.target == GroupTarget.SERVER && e.name.toLowerCase() === name.toLowerCase() && e.type == type) , (name, type) => { + console.log("Creating group: %o, %o", name, type); + connection.serverConnection.send_command('servergroupadd', { + name: name, + type: type + }).then(() => { + createInfoModal(tr("Group created"), tr("The server group has been created.")).open(); + update_groups(0); //TODO: May get the created group? + }).catch(error => { + console.warn(tr("Failed to create server group: %o"), error); + if(error instanceof CommandResult) { + error = error.extra_message || error.message; + } + createErrorModal(tr("Failed to create group"), formatMessage(tr("Failed to create group:{:br:}"), error)).open(); + }); + }); + }); + + button_rename.on('click', () => { + if(!current_group) + return; + + createInputModal(tr("Rename group"), tr("Enter the new group name"), name => name.length > 0 && !connection.groups.serverGroups.find(e => e.target == GroupTarget.SERVER && e.name.toLowerCase() === name.toLowerCase() && e.type == current_group.type), result => { + if(typeof(result) !== "string" || !result) + return; + connection.serverConnection.send_command('servergrouprename', { + sgid: current_group.id, + name: result + }).then(() => { + createInfoModal(tr("Group renamed"), tr("The server group has been renamed.")).open(); + update_groups(current_group.id); + }).catch(error => { + console.warn(tr("Failed to rename server group: %o"), error); + if(error instanceof CommandResult) { + error = error.extra_message || error.message; + } + createErrorModal(tr("Failed to rename group"), formatMessage(tr("Failed to rename group:{:br:}"), error)).open(); + }); + }).open(); + }); + + button_duplicate.on('click', () => { + createErrorModal(tr("Not implemented yet"), tr("This function hasn't been implemented yet!")).open(); + }); + + button_delete.on('click', () => { + if(!current_group) + return; + + spawnYesNo(tr("Are you sure?"), formatMessage(tr("Do you really want to delete the group {}?"), current_group.name), result => { + if(result !== true) + return; + + connection.serverConnection.send_command("servergroupdel", { + sgid: current_group.id, + force: true + }).then(() => { + createInfoModal(tr("Group deleted"), tr("The server group has been deleted.")).open(); + update_groups(0); + }).catch(error => { + console.warn(tr("Failed to delete server group: %o"), error); + if(error instanceof CommandResult) { + error = error.extra_message || error.message; + } + createErrorModal(tr("Failed to delete group"), formatMessage(tr("Failed to delete group:{:br:}"), error)).open(); + }); + }); + }); + } + update_groups(0); + + /* the editor */ + { + const pe_server = tab_right.find(".permission-editor"); + tab_right.on('show', event => { + pe_server.append(editor.html_tag()); + if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_PERMISSION_LIST).granted(1)) + editor.set_mode(PermissionEditorMode.VISIBLE); + else { + editor.set_mode(PermissionEditorMode.NO_PERMISSION); + return; + } + editor.set_listener_update(() => { + console.log("Updating permissions"); + connection.groups.request_permissions(current_group).then(result => editor.set_permissions(result)).catch(error => { + console.log(error); //TODO handling? + }); + }); + + editor.set_listener(async (permission, value) => { + if (!current_group) + throw "unset server group"; + + if (value.remove) { + /* remove the permission */ + if (typeof (value.value) !== "undefined") { + log.info(LogCategory.PERMISSIONS, tr("Removing server group permission %s. permission.id: %o"), + permission.name, + permission.id, + ); + + await connection.serverConnection.send_command("servergroupdelperm", { + sgid: current_group.id, + permid: permission.id, + }).then(e => { + if(permission.name === "i_icon_id") + for(const c of update_icon) + c(0); + return e; + }); + } else { + log.info(LogCategory.PERMISSIONS, tr("Removing server group grant permission %s. permission.id: %o"), + permission.name, + permission.id_grant(), + value.granted, + ); + + await connection.serverConnection.send_command("servergroupdelperm", { + sgid: current_group.id, + permid: permission.id_grant(), + }); + } + } else { + /* add the permission */ + if (typeof (value.value) !== "undefined") { + log.info(LogCategory.PERMISSIONS, tr("Adding or updating server group permission %s. permission.{id: %o, value: %o, flag_skip: %o, flag_negate: %o}"), + permission.name, + permission.id, + value.value, + value.flag_skip, + value.flag_negate + ); + + await connection.serverConnection.send_command("servergroupaddperm", { + sgid: current_group.id, + permid: permission.id, + permvalue: value.value, + permskip: value.flag_skip, + permnegated: value.flag_negate + }).then(e => { + if(permission.name === "i_icon_id") + for(const c of update_icon) + c(value.value); + return e; + }); + } else { + log.info(LogCategory.PERMISSIONS, tr("Adding or updating server group grant permission %s. permission.{id: %o, value: %o}"), + permission.name, + permission.id_grant(), + value.granted, + ); + + await connection.serverConnection.send_command("servergroupaddperm", { + sgid: current_group.id, + permid: permission.id_grant(), + permvalue: value.granted, + permskip: false, + permnegated: false + }); + } + } + }); + + editor.trigger_update(); + }); + } + + /* client list */ + { + //container-client-list container-group-list + let clients_visible = false; + let selected_client: { + tag: JQuery, + dbid: number + }; + + const container_client_list = tab_left.find(".container-client-list").addClass("hidden"); + const container_group_list = tab_left.find(".container-group-list"); + + const container_selected_group = container_client_list.find(".container-current-group"); + const container_clients = container_client_list.find(".list-clients .entries"); + + const input_filter = container_client_list.find(".filter-client-list"); + + const button_add = container_client_list.find(".button-add"); + const button_delete = container_client_list.find(".button-delete"); + + const update_filter = () => { + const filter_text = (input_filter.val() || "").toString().toLowerCase(); + if(!filter_text) { + container_clients.find(".entry").css('display', 'block'); + } else { + const entries = container_clients.find(".entry"); + for(const _entry of entries) { + const entry = $(_entry); + if(entry.attr("search-string").toLowerCase().indexOf(filter_text) !== -1) + entry.css('display', 'block'); + else + entry.css('display', 'none'); + } + } + }; + + const update_client_list = () => { + container_clients.empty(); + button_delete.prop('disabled', true); + + connection.serverConnection.command_helper.request_clients_by_server_group(current_group.id).then(clients => { + for(const client of clients) { + const tag = $.spawn("div").addClass("client").text(client.client_nickname); + tag.attr("search-string", client.client_nickname + "-" + client.client_unique_identifier + "-" + client.client_database_id); + container_clients.append(tag); + + tag.on('click contextmenu', event => { + container_clients.find(".selected").removeClass("selected"); + tag.addClass("selected"); + + selected_client = { + tag: tag, + dbid: client.client_database_id + }; + button_delete.prop('disabled', false); + }); + + tag.on('contextmenu', event => { + if(event.isDefaultPrevented()) + return; + + event.preventDefault(); + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Add client"), + icon_class: 'client-add', + callback: () => button_add.trigger('click') + }, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Remove client"), + icon_class: 'client-delete', + callback: () => button_delete.trigger('click') + }, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Copy unique id"), + icon_class: 'client-copy', + callback: () => copy_to_clipboard(client.client_unique_identifier) + }) + }); + } + update_filter(); + }).catch(error => { + if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) + return; + console.warn(tr("Failed to receive server group clients for group %d: %o"), current_group.id, error); + }); + }; + current_group_changed.push(update_client_list); + + button_delete.on('click', event => { + const client = selected_client; + if(!client) return; + + connection.serverConnection.send_command("servergroupdelclient", { + sgid: current_group.id, + cldbid: client.dbid + }).then(() => { + selected_client.tag.detach(); + button_delete.prop('disabled', true); /* nothing is selected */ + }).catch(error => { + console.log(tr("Failed to delete client %o from server group %o: %o"), client.dbid, current_group.id, error); + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Failed to remove client"), tr("Failed to remove client from server group")).open(); + }); + }); + + button_add.on('click', event => { + createInputModal(tr("Add client to server group"), tr("Enter the client unique id or database id"), text => { + if(!text) return false; + if(!!text.match(/^[0-9]+$/)) + return true; + try { + return atob(text).length >= 20; + } catch(error) { + return false; + } + }, async text => { + if(typeof(text) !== "string") + return; + + let dbid; + if(!!text.match(/^[0-9]+$/)) { + dbid = parseInt(text); + debugger; + } else { + try { + const data = await connection.serverConnection.command_helper.info_from_uid(text.trim()); + dbid = data[0].client_database_id; + } catch(error) { + console.log(tr("Failed to resolve client database id from unique id (%s): %o"), text, error); + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Failed to add client"), formatMessage(tr("Failed to add client to server group\nFailed to resolve database id: {}."), error)).open(); + return; + } + } + if(!dbid) { + console.log(tr("Failed to resolve client database id from unique id (%s): Client not found")); + createErrorModal(tr("Failed to add client"), tr("Failed to add client to server group\nClient database id not found")).open(); + return; + } + + + connection.serverConnection.send_command("servergroupaddclient", { + sgid: current_group.id, + cldbid: dbid + }).then(() => { + update_client_list(); + }).catch(error => { + console.log(tr("Failed to add client %o to server group %o: %o"), dbid, current_group.id, error); + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Failed to add client"), tr("Failed to add client to server group\n" + error)).open(); + }); + }).open(); + }); + + container_client_list.on('contextmenu', event => { + if(event.isDefaultPrevented()) + return; + + event.preventDefault(); + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Add client"), + icon_class: 'client-add', + callback: () => button_add.trigger('click') + }) + }); + + /* icon handler and current group display */ + { + let update_icon_callback: (i: number) => any; + update_icon.push(i => update_icon_callback(i)); + + input_filter.on('change keyup', event => update_filter()); + current_group_changed.push(() => { + container_selected_group.empty(); + if(!current_group) return; + + let icon_container = $.spawn("div").addClass("icon-container").appendTo(container_selected_group); + + connection.fileManager.icons.generateTag(current_group.properties.iconid).appendTo(icon_container); + update_icon_callback = icon => { + icon_container.empty(); + connection.fileManager.icons.generateTag(icon).appendTo(icon_container); + }; + $.spawn("div").addClass("name").text(current_group.name + " (" + current_group.id + ")").appendTo(container_selected_group); }); } + + tab_right.on('show', event => { + editor.set_toggle_button(() => { + clients_visible = !clients_visible; + + container_client_list.toggleClass("hidden", !clients_visible); + container_group_list.toggleClass("hidden", clients_visible); + + return clients_visible ? tr("Hide clients in group") : tr("Show clients in group"); + }, clients_visible ? tr("Hide clients in group") : tr("Show clients in group")); + }); } +} - function spawnGroupAdd(server_group: boolean, permissions: PermissionManager, valid_name: (name: string, group_type: number) => boolean, callback: (group_name: string, group_type: number) => any) { - let modal: Modal; - modal = createModal({ - header: tr("Create a new group"), - body: () => { - let tag = $("#tmpl_group_add").renderTag({ - server_group: server_group - }); +function spawnGroupAdd(server_group: boolean, permissions: PermissionManager, valid_name: (name: string, group_type: number) => boolean, callback: (group_name: string, group_type: number) => any) { + let modal: Modal; + modal = createModal({ + header: tr("Create a new group"), + body: () => { + let tag = $("#tmpl_group_add").renderTag({ + server_group: server_group + }); - tag.find(".group-type-template").prop("disabled", !permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_TEMPLATES).granted(1)); - tag.find(".group-type-query").prop("disabled", !permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_QUERYGROUP).granted(1)); + tag.find(".group-type-template").prop("disabled", !permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_TEMPLATES).granted(1)); + tag.find(".group-type-query").prop("disabled", !permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_QUERYGROUP).granted(1)); - const container_name = tag.find(".group-name"); - const button_create = tag.find(".button-create"); + const container_name = tag.find(".group-name"); + const button_create = tag.find(".button-create"); - const group_type = () => (tag.find(".group-type")[0] as HTMLSelectElement).selectedIndex; - container_name.on('keyup change', (event: Event) => { - if(event.type === 'keyup') { - const kevent = event as KeyboardEvent; - if(!kevent.shiftKey && kevent.key == 'Enter') { - button_create.trigger('click'); - return; - } - } - const valid = valid_name(container_name.val() as string, group_type()); - button_create.prop("disabled", !valid); - container_name.parent().toggleClass("is-invalid", !valid); - }).trigger('change'); - tag.find(".group-type").on('change', () => container_name.trigger('change')); - - button_create.on('click', event => { - if(button_create.prop("disabled")) + const group_type = () => (tag.find(".group-type")[0] as HTMLSelectElement).selectedIndex; + container_name.on('keyup change', (event: Event) => { + if(event.type === 'keyup') { + const kevent = event as KeyboardEvent; + if(!kevent.shiftKey && kevent.key == 'Enter') { + button_create.trigger('click'); return; - button_create.prop("disabled", true); /* disable double clicking */ + } + } + const valid = valid_name(container_name.val() as string, group_type()); + button_create.prop("disabled", !valid); + container_name.parent().toggleClass("is-invalid", !valid); + }).trigger('change'); + tag.find(".group-type").on('change', () => container_name.trigger('change')); - modal.close(); - callback(container_name.val() as string, group_type()); - }); - return tag; - }, - footer: null, + button_create.on('click', event => { + if(button_create.prop("disabled")) + return; + button_create.prop("disabled", true); /* disable double clicking */ - width: 600 - }); - modal.htmlTag.find(".modal-body").addClass("modal-group-add"); - modal.open_listener.push(() => { - modal.htmlTag.find(".group-name").focus(); - }); + modal.close(); + callback(container_name.val() as string, group_type()); + }); + return tag; + }, + footer: null, - modal.open(); - } + width: 600 + }); + modal.htmlTag.find(".modal-body").addClass("modal-group-add"); + modal.open_listener.push(() => { + modal.htmlTag.find(".group-name").focus(); + }); + + modal.open(); } \ No newline at end of file diff --git a/shared/js/ui/modal/permission/SenselessPermissions.ts b/shared/js/ui/modal/permission/SenselessPermissions.ts index 06ceed6a..248521af 100644 --- a/shared/js/ui/modal/permission/SenselessPermissions.ts +++ b/shared/js/ui/modal/permission/SenselessPermissions.ts @@ -1,73 +1,71 @@ -/// +import PermissionType from "tc-shared/permission/PermissionType"; -namespace permissions { - export const senseless_server_group_permissions: PermissionType[] = [ - PermissionType.B_CHANNEL_GROUP_INHERITANCE_END - ]; +export const senseless_server_group_permissions: PermissionType[] = [ + PermissionType.B_CHANNEL_GROUP_INHERITANCE_END +]; - const filter = (text, ignore_type) => Object.keys(PermissionType) - .filter(e => e.toLowerCase().substr(ignore_type ? 1 : 0).startsWith(text)).map(e => PermissionType[e]); +const filter = (text, ignore_type) => Object.keys(PermissionType) + .filter(e => e.toLowerCase().substr(ignore_type ? 1 : 0).startsWith(text)).map(e => PermissionType[e]); - export const senseless_channel_group_permissions: PermissionType[] = [ - //Not sensefull to assign serverinstance permission to channel groups - ...Object.keys(PermissionType).filter(e => e.toLowerCase().startsWith("b_serverinstance_")).map(e => PermissionType[e]), - PermissionType.B_SERVERQUERY_LOGIN, +export const senseless_channel_group_permissions: PermissionType[] = [ + //Not sensefull to assign serverinstance permission to channel groups + ...Object.keys(PermissionType).filter(e => e.toLowerCase().startsWith("b_serverinstance_")).map(e => PermissionType[e]), + PermissionType.B_SERVERQUERY_LOGIN, - //Not sensefull to assign virtual server permission to channel groups - ...Object.keys(PermissionType).filter(e => e.toLowerCase().startsWith("b_virtualserver") && e.toLowerCase() === "b_virtualserver_channel_permission_list").map(e => PermissionType[e]), + //Not sensefull to assign virtual server permission to channel groups + ...Object.keys(PermissionType).filter(e => e.toLowerCase().startsWith("b_virtualserver") && e.toLowerCase() === "b_virtualserver_channel_permission_list").map(e => PermissionType[e]), - //Not sensefull to require some playlist permissions - ...Object.keys(PermissionType).filter(e => e.toLowerCase().startsWith("i_playlist")).map(e => PermissionType[e]), - PermissionType.B_PLAYLIST_CREATE, + //Not sensefull to require some playlist permissions + ...Object.keys(PermissionType).filter(e => e.toLowerCase().startsWith("i_playlist")).map(e => PermissionType[e]), + PermissionType.B_PLAYLIST_CREATE, - //Not sensefull to require some playlist permissions - ...Object.keys(PermissionType).filter(e => e.toLowerCase().startsWith("i_client_music")).map(e => PermissionType[e]), - ...Object.keys(PermissionType).filter(e => e.toLowerCase().startsWith("b_client_music")).map(e => PermissionType[e]), + //Not sensefull to require some playlist permissions + ...Object.keys(PermissionType).filter(e => e.toLowerCase().startsWith("i_client_music")).map(e => PermissionType[e]), + ...Object.keys(PermissionType).filter(e => e.toLowerCase().startsWith("b_client_music")).map(e => PermissionType[e]), - ...Object.keys(PermissionType).filter(e => e.toLowerCase().startsWith("i_server_group")).map(e => PermissionType[e]), + ...Object.keys(PermissionType).filter(e => e.toLowerCase().startsWith("i_server_group")).map(e => PermissionType[e]), - PermissionType.I_MAX_ICON_FILESIZE, - PermissionType.I_MAX_PLAYLIST_SIZE, - PermissionType.I_MAX_PLAYLISTS, + PermissionType.I_MAX_ICON_FILESIZE, + PermissionType.I_MAX_PLAYLIST_SIZE, + PermissionType.I_MAX_PLAYLISTS, - PermissionType.I_CLIENT_KICK_FROM_SERVER_POWER, //Why should channel groups kick clients from server. Yes there are cases, but not the usual once - PermissionType.I_CLIENT_NEEDED_KICK_FROM_SERVER_POWER, + PermissionType.I_CLIENT_KICK_FROM_SERVER_POWER, //Why should channel groups kick clients from server. Yes there are cases, but not the usual once + PermissionType.I_CLIENT_NEEDED_KICK_FROM_SERVER_POWER, - PermissionType.I_CLIENT_BAN_POWER, //Why should channel groups ban clients from server. Yes there are cases, but not the usual once - PermissionType.I_CLIENT_NEEDED_BAN_POWER, + PermissionType.I_CLIENT_BAN_POWER, //Why should channel groups ban clients from server. Yes there are cases, but not the usual once + PermissionType.I_CLIENT_NEEDED_BAN_POWER, - ...Object.keys(PermissionType).filter(e => e.toLowerCase().startsWith("b_client_complain")).map(e => PermissionType[e]), - ...Object.keys(PermissionType).filter(e => e.toLowerCase().startsWith("b_client_ban")).map(e => PermissionType[e]), - PermissionType.I_CLIENT_BAN_MAX_BANTIME, - PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND, + ...Object.keys(PermissionType).filter(e => e.toLowerCase().startsWith("b_client_complain")).map(e => PermissionType[e]), + ...Object.keys(PermissionType).filter(e => e.toLowerCase().startsWith("b_client_ban")).map(e => PermissionType[e]), + PermissionType.I_CLIENT_BAN_MAX_BANTIME, + PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND, - ...Object.keys(PermissionType).filter(e => e.toLowerCase().startsWith("b_client_query")).map(e => PermissionType[e]), - PermissionType.B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN, - PermissionType.B_CLIENT_DELETE_DBPROPERTIES, - PermissionType.B_CLIENT_MODIFY_DBPROPERTIES - ]; + ...Object.keys(PermissionType).filter(e => e.toLowerCase().startsWith("b_client_query")).map(e => PermissionType[e]), + PermissionType.B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN, + PermissionType.B_CLIENT_DELETE_DBPROPERTIES, + PermissionType.B_CLIENT_MODIFY_DBPROPERTIES +]; - export const senseless_channel_permissions: PermissionType[] = [ - ...senseless_channel_group_permissions, //Powers and needed powers are not inherited here. We "hide" all powers +export const senseless_channel_permissions: PermissionType[] = [ + ...senseless_channel_group_permissions, //Powers and needed powers are not inherited here. We "hide" all powers - ...filter("_channel_create", true), - ...filter("_client", true), - ...filter("_channel_group", true), - ...filter("_group", true), - ...filter("b_channel_", false), - ...Object.keys(PermissionType).filter(e => { - e = e.toLowerCase(); - return e.indexOf("_power") > 0 && e.indexOf("_needed_") == -1; - }).map(e => PermissionType[e]), + ...filter("_channel_create", true), + ...filter("_client", true), + ...filter("_channel_group", true), + ...filter("_group", true), + ...filter("b_channel_", false), + ...Object.keys(PermissionType).filter(e => { + e = e.toLowerCase(); + return e.indexOf("_power") > 0 && e.indexOf("_needed_") == -1; + }).map(e => PermissionType[e]), - PermissionType.B_ICON_MANAGE, - PermissionType.B_CLIENT_USE_PRIORITY_SPEAKER, - PermissionType.B_CLIENT_USE_PRIORITY_SPEAKER, - PermissionType.B_CHANNEL_IGNORE_DESCRIPTION_VIEW_POWER - ]; + PermissionType.B_ICON_MANAGE, + PermissionType.B_CLIENT_USE_PRIORITY_SPEAKER, + PermissionType.B_CLIENT_USE_PRIORITY_SPEAKER, + PermissionType.B_CHANNEL_IGNORE_DESCRIPTION_VIEW_POWER +]; - export const senseless_client_permissions: PermissionType[] = [ ]; +export const senseless_client_permissions: PermissionType[] = [ ]; - export const senseless_client_channel_permissions: PermissionType[] = [ ]; -} \ No newline at end of file +export const senseless_client_channel_permissions: PermissionType[] = [ ]; \ No newline at end of file diff --git a/shared/js/ui/server.ts b/shared/js/ui/server.ts index 598f9d22..2cc93c97 100644 --- a/shared/js/ui/server.ts +++ b/shared/js/ui/server.ts @@ -1,7 +1,21 @@ -/// -/// +import {ChannelTree} from "tc-shared/ui/view"; +import {Settings, settings} from "tc-shared/settings"; +import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; +import * as log from "tc-shared/log"; +import {LogCategory, LogType} from "tc-shared/log"; +import {Sound} from "tc-shared/sound/Sounds"; +import * as bookmarks from "tc-shared/bookmarks"; +import {spawnInviteEditor} from "tc-shared/ui/modal/ModalInvite"; +import {openServerInfo} from "tc-shared/ui/modal/ModalServerInfo"; +import {createServerModal} from "tc-shared/ui/modal/ModalServerEdit"; +import {spawnIconSelect} from "tc-shared/ui/modal/ModalIconSelect"; +import {spawnAvatarList} from "tc-shared/ui/modal/ModalAvatarList"; +import {server_connections} from "tc-shared/ui/frames/connection_handlers"; +import {control_bar} from "tc-shared/ui/frames/ControlBar"; +import {connection_log} from "tc-shared/ui/modal/ModalConnect"; +import * as top_menu from "./frames/MenuBar"; -class ServerProperties { +export class ServerProperties { virtualserver_host: string = ""; virtualserver_port: number = 0; @@ -78,7 +92,7 @@ class ServerProperties { virtualserver_total_bytes_uploaded: number = 0; } -interface ServerConnectionInfo { +export interface ServerConnectionInfo { connection_filetransfer_bandwidth_sent: number; connection_filetransfer_bandwidth_received: number; @@ -103,12 +117,12 @@ interface ServerConnectionInfo { connection_ping: number; } -interface ServerAddress { +export interface ServerAddress { host: string; port: number; } -class ServerEntry { +export class ServerEntry { remote_address: ServerAddress; channelTree: ChannelTree; properties: ServerProperties; @@ -209,14 +223,14 @@ class ServerEntry { name: tr("Show server info"), callback: () => { trigger_close = false; - Modals.openServerInfo(this); + openServerInfo(this); }, icon_class: "client-about" }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-invite_buddy", name: tr("Invite buddy"), - callback: () => Modals.spawnInviteEditor(this.channelTree.client) + callback: () => spawnInviteEditor(this.channelTree.client) }, { type: contextmenu.MenuEntryType.HR, name: '' @@ -234,7 +248,7 @@ class ServerEntry { icon_class: "client-virtualserver_edit", name: tr("Edit"), callback: () => { - Modals.createServerModal(this, properties => { + createServerModal(this, properties => { log.info(LogCategory.SERVER, tr("Changing server properties %o"), properties); console.log(tr("Changed properties: %o"), properties); if (properties) { @@ -255,13 +269,13 @@ class ServerEntry { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-iconviewer", name: tr("View icons"), - callback: () => Modals.spawnIconSelect(this.channelTree.client) + callback: () => spawnIconSelect(this.channelTree.client) }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: 'client-iconsview', name: tr("View avatars"), visible: false, //TODO: Enable again as soon the new design is finished - callback: () => Modals.spawnAvatarList(this.channelTree.client) + callback: () => spawnAvatarList(this.channelTree.client) }, contextmenu.Entry.CLOSE(() => trigger_close ? on_close() : {}) ); diff --git a/shared/js/ui/view.ts b/shared/js/ui/view.ts index 4530e28e..1e1c0f32 100644 --- a/shared/js/ui/view.ts +++ b/shared/js/ui/view.ts @@ -1,15 +1,25 @@ -/// -/// -/// -/// -/// -/// -/// -/// -/// +import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; +import * as log from "tc-shared/log"; +import {Settings, settings} from "tc-shared/settings"; +import PermissionType from "tc-shared/permission/PermissionType"; +import {LogCategory} from "tc-shared/log"; +import {KeyCode, SpecialKey} from "tc-shared/PPTListener"; +import {createInputModal} from "tc-shared/ui/elements/Modal"; +import {Sound} from "tc-shared/sound/Sounds"; +import {Group} from "tc-shared/permission/GroupManager"; +import * as server_log from "tc-shared/ui/frames/server_log"; +import {ServerAddress, ServerEntry} from "tc-shared/ui/server"; +import {ClientMover} from "tc-shared/ui/client_move"; +import {ChannelEntry, ChannelSubscribeMode} from "tc-shared/ui/channel"; +import {ClientEntry, ClientType, LocalClientEntry, MusicClientEntry} from "tc-shared/ui/client"; +import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler"; +import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; +import {formatMessage} from "tc-shared/ui/frames/chat"; +import {spawnBanClient} from "tc-shared/ui/modal/ModalBanClient"; +import {createChannelModal} from "tc-shared/ui/modal/ModalCreateChannel"; +import * as ppt from "tc-backend/ppt"; - -class ChannelTree { +export class ChannelTree { client: ConnectionHandler; server: ServerEntry; @@ -465,7 +475,7 @@ class ChannelTree { } onSelect(entry?: ChannelEntry | ClientEntry | ServerEntry, enforce_single?: boolean, flag_shift?: boolean) { - if(this.currently_selected && (ppt.key_pressed(ppt.SpecialKey.SHIFT) || flag_shift) && entry instanceof ClientEntry) { //Currently we're only supporting client multiselects :D + if(this.currently_selected && (ppt.key_pressed(SpecialKey.SHIFT) || flag_shift) && entry instanceof ClientEntry) { //Currently we're only supporting client multiselects :D if(!entry) return; //Nowhere if($.isArray(this.currently_selected)) { @@ -615,7 +625,7 @@ class ChannelTree { name: tr("Ban clients"), invalidPermission: !this.client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).granted(1), callback: () => { - Modals.spawnBanClient(this.client, (clients).map(entry => { + spawnBanClient(this.client, (clients).map(entry => { return { name: entry.clientNickName(), unique_id: entry.properties.client_unique_identifier @@ -644,9 +654,9 @@ class ChannelTree { callback: () => { const param_string = clients.map((_, index) => "{" + index + "}").join(', '); const param_values = clients.map(client => client.createChatTag(true)); - const tag = $.spawn("div").append(...MessageHelper.formatMessage(tr("Do you really want to delete ") + param_string, ...param_values)); + const tag = $.spawn("div").append(...formatMessage(tr("Do you really want to delete ") + param_string, ...param_values)); const tag_container = $.spawn("div").append(tag); - Modals.spawnYesNo(tr("Are you sure?"), tag_container, result => { + spawnYesNo(tr("Are you sure?"), tag_container, result => { if(result) { for(const client of clients) this.client.serverConnection.send_command("musicbotdelete", { @@ -706,7 +716,7 @@ class ChannelTree { } spawnCreateChannel(parent?: ChannelEntry) { - Modals.createChannelModal(this.client, undefined, parent, this.client.permissions, (properties?, permissions?) => { + createChannelModal(this.client, undefined, parent, this.client.permissions, (properties?, permissions?) => { if(!properties) return; properties["cpid"] = parent ? parent.channelId : 0; log.debug(LogCategory.CHANNEL, tr("Creating a new channel.\nProperties: %o\nPermissions: %o"), properties); @@ -735,7 +745,7 @@ class ChannelTree { return new Promise(resolve => { resolve(channel); }) }).then(channel => { - this.client.log.log(log.server.Type.CHANNEL_CREATE, { + this.client.log.log(server_log.Type.CHANNEL_CREATE, { channel: channel.log_data(), creator: this.client.getClient().log_data(), own_action: true diff --git a/shared/js/utils/LaterPromise.ts b/shared/js/utils/LaterPromise.ts new file mode 100644 index 00000000..3c26df5a --- /dev/null +++ b/shared/js/utils/LaterPromise.ts @@ -0,0 +1,49 @@ +export class LaterPromise extends Promise { + private readonly _time: number; + private readonly _handle: Promise; + private _resolve: ($: T) => any; + private _reject: ($: any) => any; + + constructor() { + super((resolve, reject) => {}); + this._handle = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); + this._time = Date.now(); + } + + resolved(object: T) { + this._resolve(object); + } + + rejected(reason) { + this._reject(reason); + } + + function_rejected() { + return error => this.rejected(error); + } + + time() { return this._time; } + + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, + onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise { + return this._handle.then(onfulfilled, onrejected); + } + + /** + * Attaches a callback for only the rejection of the Promise. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of the callback. + */ + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise { + return this._handle.then(onrejected); + } +} \ No newline at end of file diff --git a/shared/js/utils/buffers.ts b/shared/js/utils/buffers.ts new file mode 100644 index 00000000..83043e44 --- /dev/null +++ b/shared/js/utils/buffers.ts @@ -0,0 +1,71 @@ +export function str2ab8(str) { + const buf = new ArrayBuffer(str.length); + const bufView = new Uint8Array(buf); + for (let i = 0, strLen = str.length; i < strLen; i++) { + bufView[i] = str.charCodeAt(i); + } + return buf; +} + +/* FIXME Dont use atob, because it sucks for non UTF-8 tings */ +export function arrayBufferBase64(base64: string) { + base64 = atob(base64); + const buf = new ArrayBuffer(base64.length); + const bufView = new Uint8Array(buf); + for (let i = 0, strLen = base64.length; i < strLen; i++) { + bufView[i] = base64.charCodeAt(i); + } + return buf; +} + +export function base64_encode_ab(source: ArrayBufferLike) { + const encodings = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + let base64 = ""; + + const bytes = new Uint8Array(source); + const byte_length = bytes.byteLength; + const byte_reminder = byte_length % 3; + const main_length = byte_length - byte_reminder; + + let a, b, c, d; + let chunk; + + // Main loop deals with bytes in chunks of 3 + for (let i = 0; i < main_length; i = i + 3) { + // Combine the three bytes into a single integer + chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]; + + // Use bitmasks to extract 6-bit segments from the triplet + a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18 + b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12 + c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6 + d = (chunk & 63) >> 0; // 63 = (2^6 - 1) << 0 + + // Convert the raw binary segments to the appropriate ASCII encoding + base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]; + } + + // Deal with the remaining bytes and padding + if (byte_reminder == 1) { + chunk = bytes[main_length]; + + a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2 + + // Set the 4 least significant bits to zero + b = (chunk & 3) << 4; // 3 = 2^2 - 1 + + base64 += encodings[a] + encodings[b] + '=='; + } else if (byte_reminder == 2) { + chunk = (bytes[main_length] << 8) | bytes[main_length + 1]; + + a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10 + b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4 + + // Set the 2 least significant bits to zero + c = (chunk & 15) << 2; // 15 = 2^4 - 1 + + base64 += encodings[a] + encodings[b] + encodings[c] + '='; + } + + return base64 +} \ No newline at end of file diff --git a/shared/js/utils/helpers.ts b/shared/js/utils/helpers.ts index 1b8cc2d9..9528f4d8 100644 --- a/shared/js/utils/helpers.ts +++ b/shared/js/utils/helpers.ts @@ -1,66 +1,12 @@ -/// - -namespace helpers { - export function hashPassword(password: string) : Promise { - return new Promise((resolve, reject) => { - sha.sha1(password).then(result => { - resolve(btoa(String.fromCharCode.apply(null, new Uint8Array(result)))); - }); +export function hashPassword(password: string) : Promise { + return new Promise((resolve, reject) => { + sha.sha1(password).then(result => { + resolve(btoa(String.fromCharCode.apply(null, new Uint8Array(result)))); }); - } + }); } -class LaterPromise extends Promise { - private _handle: Promise; - private _resolve: ($: T) => any; - private _reject: ($: any) => any; - private _time: number; - - constructor() { - super((resolve, reject) => {}); - this._handle = new Promise((resolve, reject) => { - this._resolve = resolve; - this._reject = reject; - }); - this._time = Date.now(); - } - - resolved(object: T) { - this._resolve(object); - } - - rejected(reason) { - this._reject(reason); - } - - function_rejected() { - return error => this.rejected(error); - } - - time() { return this._time; } - - /** - * Attaches callbacks for the resolution and/or rejection of the Promise. - * @param onfulfilled The callback to execute when the Promise is resolved. - * @param onrejected The callback to execute when the Promise is rejected. - * @returns A Promise for the completion of which ever callback is executed. - */ - then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, - onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise { - return this._handle.then(onfulfilled, onrejected); - } - - /** - * Attaches a callback for only the rejection of the Promise. - * @param onrejected The callback to execute when the Promise is rejected. - * @returns A Promise for the completion of the callback. - */ - catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise { - return this._handle.then(onrejected); - } -} - -const copy_to_clipboard = str => { +export const copy_to_clipboard = str => { console.log(tr("Copy text to clipboard: %s"), str); const el = document.createElement('textarea'); el.value = str; diff --git a/shared/js/voice/RecorderBase.ts b/shared/js/voice/RecorderBase.ts index ba32fb38..deab0e8f 100644 --- a/shared/js/voice/RecorderBase.ts +++ b/shared/js/voice/RecorderBase.ts @@ -1,126 +1,122 @@ -namespace audio { - export namespace recorder { - export interface InputDevice { - unique_id: string; - driver: string; - name: string; - default_input: boolean; +export interface InputDevice { + unique_id: string; + driver: string; + name: string; + default_input: boolean; - supported: boolean; + supported: boolean; - sample_rate: number; - channels: number; - } + sample_rate: number; + channels: number; +} - export enum InputConsumerType { - CALLBACK, - NODE, - NATIVE - } +export enum InputConsumerType { + CALLBACK, + NODE, + NATIVE +} - export interface InputConsumer { - type: InputConsumerType; - } +export interface InputConsumer { + type: InputConsumerType; +} - export interface CallbackInputConsumer extends InputConsumer { - callback_audio?: (buffer: AudioBuffer) => any; - callback_buffer?: (buffer: Float32Array, samples: number, channels: number) => any; - } +export interface CallbackInputConsumer extends InputConsumer { + callback_audio?: (buffer: AudioBuffer) => any; + callback_buffer?: (buffer: Float32Array, samples: number, channels: number) => any; +} - export interface NodeInputConsumer extends InputConsumer { - callback_node: (source_node: AudioNode) => any; - callback_disconnect: (source_node: AudioNode) => any; - } +export interface NodeInputConsumer extends InputConsumer { + callback_node: (source_node: AudioNode) => any; + callback_disconnect: (source_node: AudioNode) => any; +} - export namespace filter { - export enum Type { - THRESHOLD, - VOICE_LEVEL, - STATE - } - - export interface Filter { - type: Type; - - is_enabled() : boolean; - } - - export interface MarginedFilter { - get_margin_frames() : number; - set_margin_frames(value: number); - } - - export interface ThresholdFilter extends Filter, MarginedFilter { - get_threshold() : number; - set_threshold(value: number) : Promise; - - get_attack_smooth() : number; - get_release_smooth() : number; - - set_attack_smooth(value: number); - set_release_smooth(value: number); - - callback_level?: (value: number) => any; - } - - export interface VoiceLevelFilter extends Filter, MarginedFilter { - get_level() : number; - } - - export interface StateFilter extends Filter { - set_state(state: boolean) : Promise; - is_active() : boolean; /* if true the the filter allows data to pass */ - } - } - - export enum InputState { - PAUSED, - INITIALIZING, - RECORDING, - DRY - } - - export enum InputStartResult { - EOK = "eok", - EUNKNOWN = "eunknown", - EBUSY = "ebusy", - ENOTALLOWED = "enotallowed", - ENOTSUPPORTED = "enotsupported" - } - - export interface AbstractInput { - callback_begin: () => any; - callback_end: () => any; - - current_state() : InputState; - - start() : Promise; - stop() : Promise; - - current_device() : InputDevice | undefined; - set_device(device: InputDevice | undefined) : Promise; - - current_consumer() : InputConsumer | undefined; - set_consumer(consumer: InputConsumer) : Promise; - - get_filter(type: filter.Type) : filter.Filter | undefined; - supports_filter(type: audio.recorder.filter.Type) : boolean; - - clear_filter(); - disable_filter(type: filter.Type); - enable_filter(type: filter.Type); - - get_volume() : number; - set_volume(volume: number); - } - - export interface LevelMeter { - device() : InputDevice; - - set_observer(callback: (value: number) => any); - - destory(); - } +export namespace filter { + export enum Type { + THRESHOLD, + VOICE_LEVEL, + STATE } + + export interface Filter { + type: Type; + + is_enabled() : boolean; + } + + export interface MarginedFilter { + get_margin_frames() : number; + set_margin_frames(value: number); + } + + export interface ThresholdFilter extends Filter, MarginedFilter { + get_threshold() : number; + set_threshold(value: number) : Promise; + + get_attack_smooth() : number; + get_release_smooth() : number; + + set_attack_smooth(value: number); + set_release_smooth(value: number); + + callback_level?: (value: number) => any; + } + + export interface VoiceLevelFilter extends Filter, MarginedFilter { + get_level() : number; + } + + export interface StateFilter extends Filter { + set_state(state: boolean) : Promise; + is_active() : boolean; /* if true the the filter allows data to pass */ + } +} + +export enum InputState { + PAUSED, + INITIALIZING, + RECORDING, + DRY +} + +export enum InputStartResult { + EOK = "eok", + EUNKNOWN = "eunknown", + EBUSY = "ebusy", + ENOTALLOWED = "enotallowed", + ENOTSUPPORTED = "enotsupported" +} + +export interface AbstractInput { + callback_begin: () => any; + callback_end: () => any; + + current_state() : InputState; + + start() : Promise; + stop() : Promise; + + current_device() : InputDevice | undefined; + set_device(device: InputDevice | undefined) : Promise; + + current_consumer() : InputConsumer | undefined; + set_consumer(consumer: InputConsumer) : Promise; + + get_filter(type: filter.Type) : filter.Filter | undefined; + supports_filter(type: filter.Type) : boolean; + + clear_filter(); + disable_filter(type: filter.Type); + enable_filter(type: filter.Type); + + get_volume() : number; + set_volume(volume: number); +} + +export interface LevelMeter { + device() : InputDevice; + + set_observer(callback: (value: number) => any); + + destory(); } \ No newline at end of file diff --git a/shared/js/voice/RecorderProfile.ts b/shared/js/voice/RecorderProfile.ts index ec5ea0dc..26825c0e 100644 --- a/shared/js/voice/RecorderProfile.ts +++ b/shared/js/voice/RecorderProfile.ts @@ -1,7 +1,15 @@ -/// +import * as log from "tc-shared/log"; +import {AbstractInput, filter, InputDevice} from "tc-shared/voice/RecorderBase"; +import {KeyDescriptor, KeyHook} from "tc-shared/PPTListener"; +import {LogCategory} from "tc-shared/log"; +import {Settings, settings} from "tc-shared/settings"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import * as aplayer from "tc-backend/audio/player"; +import * as arecorder from "tc-backend/audio/recorder"; +import * as ppt from "tc-backend/ppt"; -type VadType = "threshold" | "push_to_talk" | "active"; -interface RecorderProfileConfig { +export type VadType = "threshold" | "push_to_talk" | "active"; +export interface RecorderProfileConfig { version: number; /* devices unique id */ @@ -25,13 +33,16 @@ interface RecorderProfileConfig { } } -let default_recorder: RecorderProfile; /* needs initialize */ -class RecorderProfile { +export let default_recorder: RecorderProfile; /* needs initialize */ +export function set_default_recorder(recorder: RecorderProfile) { + default_recorder = recorder; +} +export class RecorderProfile { readonly name; readonly volatile; /* not saving profile */ config: RecorderProfileConfig; - input: audio.recorder.AbstractInput; + input: AbstractInput; current_handler: ConnectionHandler; @@ -43,7 +54,7 @@ class RecorderProfile { record_supported: boolean; - private _ppt_hook: ppt.KeyHook; + private _ppt_hook: KeyHook; private _ppt_timeout: NodeJS.Timer; private _ppt_hook_registered: boolean; @@ -57,28 +68,26 @@ class RecorderProfile { clearTimeout(this._ppt_timeout); this._ppt_timeout = setTimeout(() => { - const filter = this.input.get_filter(audio.recorder.filter.Type.STATE) as audio.recorder.filter.StateFilter; - if(filter) - filter.set_state(true); + const f = this.input.get_filter(filter.Type.STATE) as filter.StateFilter; + if(f) f.set_state(true); }, Math.min(this.config.vad_push_to_talk.delay, 0)); }, callback_press: () => { if(this._ppt_timeout) clearTimeout(this._ppt_timeout); - const filter = this.input.get_filter(audio.recorder.filter.Type.STATE) as audio.recorder.filter.StateFilter; - if(filter) - filter.set_state(false); + const f = this.input.get_filter(filter.Type.STATE) as filter.StateFilter; + if(f) f.set_state(false); }, cancel: false - } as ppt.KeyHook; + } as KeyHook; this._ppt_hook_registered = false; this.record_supported = true; } async initialize() : Promise { - audio.player.on_ready(async () => { + aplayer.on_ready(async () => { this.initialize_input(); await this.load(); await this.reinitialize_filter(); @@ -86,7 +95,7 @@ class RecorderProfile { } private initialize_input() { - this.input = audio.recorder.create_input(); + this.input = arecorder.create_input(); this.input.callback_begin = () => { log.debug(LogCategory.VOICE, "Voice start"); if(this.callback_start) @@ -127,7 +136,7 @@ class RecorderProfile { this.input.set_volume(this.config.volume / 100); { - const all_devices = audio.recorder.devices(); + const all_devices = arecorder.devices(); const devices = all_devices.filter(e => e.default_input || e.unique_id === this.config.device_id); const device = devices.find(e => e.unique_id === this.config.device_id) || devices[0]; @@ -155,27 +164,27 @@ class RecorderProfile { } if(this.config.vad_type === "threshold") { - const filter = this.input.get_filter(audio.recorder.filter.Type.THRESHOLD) as audio.recorder.filter.ThresholdFilter; - await filter.set_threshold(this.config.vad_threshold.threshold); - await filter.set_margin_frames(10); /* 500ms */ + const filter_ = this.input.get_filter(filter.Type.THRESHOLD) as filter.ThresholdFilter; + await filter_.set_threshold(this.config.vad_threshold.threshold); + await filter_.set_margin_frames(10); /* 500ms */ /* legacy client support */ - if('set_attack_smooth' in filter) - filter.set_attack_smooth(.25); - if('set_release_smooth' in filter) - filter.set_release_smooth(.9); + if('set_attack_smooth' in filter_) + filter_.set_attack_smooth(.25); + if('set_release_smooth' in filter_) + filter_.set_release_smooth(.9); - this.input.enable_filter(audio.recorder.filter.Type.THRESHOLD); + this.input.enable_filter(filter.Type.THRESHOLD); } else if(this.config.vad_type === "push_to_talk") { - const filter = this.input.get_filter(audio.recorder.filter.Type.STATE) as audio.recorder.filter.StateFilter; - await filter.set_state(true); + const filter_ = this.input.get_filter(filter.Type.STATE) as filter.StateFilter; + await filter_.set_state(true); for(const key of ["key_alt", "key_ctrl", "key_shift", "key_windows", "key_code"]) this._ppt_hook[key] = this.config.vad_push_to_talk[key]; ppt.register_key_hook(this._ppt_hook); this._ppt_hook_registered = true; - this.input.enable_filter(audio.recorder.filter.Type.STATE); + this.input.enable_filter(filter.Type.STATE); } else if(this.config.vad_type === "active") {} } @@ -219,8 +228,8 @@ class RecorderProfile { this.save(); } - get_vad_ppt_key() : ppt.KeyDescriptor { return this.config.vad_push_to_talk; } - set_vad_ppt_key(key: ppt.KeyDescriptor) { + get_vad_ppt_key() : KeyDescriptor { return this.config.vad_push_to_talk; } + set_vad_ppt_key(key: KeyDescriptor) { for(const _key of ["key_alt", "key_ctrl", "key_shift", "key_windows", "key_code"]) this.config.vad_push_to_talk[_key] = key[_key]; @@ -239,8 +248,8 @@ class RecorderProfile { } - current_device() : audio.recorder.InputDevice | undefined { return this.input.current_device(); } - set_device(device: audio.recorder.InputDevice | undefined) : Promise { + current_device() : InputDevice | undefined { return this.input.current_device(); } + set_device(device: InputDevice | undefined) : Promise { this.config.device_id = device ? device.unique_id : undefined; this.save(); return this.input.set_device(device); diff --git a/shared/loader/loader.ts b/shared/loader/loader.ts deleted file mode 100644 index bc537fdb..00000000 --- a/shared/loader/loader.ts +++ /dev/null @@ -1,762 +0,0 @@ -interface Window { - tr(message: string) : string; -} - -namespace loader { - export namespace config { - export const loader_groups = false; - export const verbose = false; - export const error = true; - } - - export type Task = { - name: string, - priority: number, /* tasks with the same priority will be executed in sync */ - function: () => Promise - }; - - export enum Stage { - /* - loading loader required files (incl this) - */ - INITIALIZING, - /* - setting up the loading process - */ - SETUP, - /* - loading all style sheet files - */ - STYLE, - /* - loading all javascript files - */ - JAVASCRIPT, - /* - loading all template files - */ - TEMPLATES, - /* - initializing static/global stuff - */ - JAVASCRIPT_INITIALIZING, - /* - finalizing load process - */ - FINALIZING, - /* - invoking main task - */ - LOADED, - - DONE - } - - let cache_tag: string | undefined; - let current_stage: Stage = undefined; - const tasks: {[key:number]:Task[]} = {}; - - /* test if all files shall be load from cache or fetch again */ - function loader_cache_tag() { - const app_version = (() => { - const version_node = document.getElementById("app_version"); - if(!version_node) return undefined; - - const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined; - if(!version) return undefined; - - if(!version || version == "unknown" || version.replace(/0+/, "").length == 0) - return undefined; - - return version; - })(); - if(config.verbose) console.log("Found current app version: %o", app_version); - - if(!app_version) { - /* TODO add warning */ - cache_tag = "?_ts=" + Date.now(); - return; - } - const cached_version = localStorage.getItem("cached_version"); - if(!cached_version || cached_version != app_version) { - loader.register_task(loader.Stage.LOADED, { - priority: 0, - name: "cached version updater", - function: async () => { - localStorage.setItem("cached_version", app_version); - } - }); - } - cache_tag = "?_version=" + app_version; - } - - export function get_cache_version() { return cache_tag; } - - export function finished() { - return current_stage == Stage.DONE; - } - export function running() { return typeof(current_stage) !== "undefined"; } - - export function register_task(stage: Stage, task: Task) { - if(current_stage > stage) { - if(config.error) - console.warn("Register loading task, but it had already been finished. Executing task anyways!"); - task.function().catch(error => { - if(config.error) { - console.error("Failed to execute delayed loader task!"); - console.log(" - %s: %o", task.name, error); - } - - loader.critical_error(error); - }); - return; - } - - const task_array = tasks[stage] || []; - task_array.push(task); - tasks[stage] = task_array.sort((a, b) => a.priority - b.priority); - } - - export async function execute() { - document.getElementById("loader-overlay").classList.add("started"); - loader_cache_tag(); - - const load_begin = Date.now(); - - let begin: number = 0; - let end: number = Date.now(); - while(current_stage <= Stage.LOADED || typeof(current_stage) === "undefined") { - - let current_tasks: Task[] = []; - while((tasks[current_stage] || []).length > 0) { - if(current_tasks.length == 0 || current_tasks[0].priority == tasks[current_stage][0].priority) { - current_tasks.push(tasks[current_stage].pop()); - } else break; - } - - const errors: { - error: any, - task: Task - }[] = []; - - const promises: Promise[] = []; - for(const task of current_tasks) { - try { - if(config.verbose) console.debug("Executing loader %s (%d)", task.name, task.priority); - promises.push(task.function().catch(error => { - errors.push({ - task: task, - error: error - }); - return Promise.resolve(); - })); - } catch(error) { - errors.push({ - task: task, - error: error - }); - } - } - - if(promises.length > 0) { - await Promise.all([...promises]); - } - - if(errors.length > 0) { - if(config.loader_groups) console.groupEnd(); - console.error("Failed to execute loader. The following tasks failed (%d):", errors.length); - for(const error of errors) - console.error(" - %s: %o", error.task.name, error.error); - - throw "failed to process step " + Stage[current_stage]; - } - - if(current_tasks.length == 0) { - if(typeof(current_stage) === "undefined") { - current_stage = -1; - if(config.verbose) console.debug("[loader] Booting app"); - } else if(current_stage < Stage.INITIALIZING) { - if(config.loader_groups) console.groupEnd(); - if(config.verbose) console.debug("[loader] Entering next state (%s). Last state took %dms", Stage[current_stage + 1], (end = Date.now()) - begin); - } else { - if(config.loader_groups) console.groupEnd(); - if(config.verbose) console.debug("[loader] Finish invoke took %dms", (end = Date.now()) - begin); - } - - begin = end; - current_stage += 1; - - if(current_stage != Stage.DONE && config.loader_groups) - console.groupCollapsed("Executing loading stage %s", Stage[current_stage]); - } - } - - /* cleanup */ - { - _script_promises = {}; - } - if(config.verbose) console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - load_begin); - } - export function execute_managed() { - loader.execute().then(() => { - if(config.verbose) { - let message; - if(typeof(window.tr) !== "undefined") - message = tr("App loaded successfully!"); - else - message = "App loaded successfully!"; - - if(typeof(log) !== "undefined") { - /* We're having our log module */ - log.info(LogCategory.GENERAL, message); - } else { - console.log(message); - } - } - }).catch(error => { - console.error("App loading failed: %o", error); - loader.critical_error("Failed to execute loader", "Lookup the console for more detail"); - }); - } - - export type DependSource = { - url: string; - depends: string[]; - } - export type SourcePath = string | DependSource | string[]; - - function script_name(path: SourcePath) { - if(Array.isArray(path)) { - let buffer = ""; - let _or = " or "; - for(let entry of path) - buffer += _or + script_name(entry); - return buffer.slice(_or.length); - } else if(typeof(path) === "string") - return "" + path + ""; - else - return "" + path.url + ""; - } - - class SyntaxError { - source: any; - - constructor(source: any) { - this.source = source; - } - } - - let _script_promises: {[key: string]: Promise} = {}; - export async function load_script(path: SourcePath) : Promise { - if(Array.isArray(path)) { //We have some fallback - return load_script(path[0]).catch(error => { - if(error instanceof SyntaxError) - return Promise.reject(error.source); - - if(path.length > 1) - return load_script(path.slice(1)); - - return Promise.reject(error); - }); - } else { - const source = typeof(path) === "string" ? {url: path, depends: []} : path; - if(source.url.length == 0) return Promise.resolve(); - - return _script_promises[source.url] = (async () => { - /* await depends */ - for(const depend of source.depends) { - if(!_script_promises[depend]) - throw "Missing dependency " + depend; - await _script_promises[depend]; - } - - const tag: HTMLScriptElement = document.createElement("script"); - - await new Promise((resolve, reject) => { - let error = false; - const error_handler = (event: ErrorEvent) => { - if(event.filename == tag.src && event.message.indexOf("Illegal constructor") == -1) { //Our tag throw an uncaught error - if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error); - window.removeEventListener('error', error_handler as any); - - reject(new SyntaxError(event.error)); - event.preventDefault(); - error = true; - } - }; - window.addEventListener('error', error_handler as any); - - const cleanup = () => { - tag.onerror = undefined; - tag.onload = undefined; - - clearTimeout(timeout_handle); - window.removeEventListener('error', error_handler as any); - }; - const timeout_handle = setTimeout(() => { - cleanup(); - reject("timeout"); - }, 5000); - tag.type = "application/javascript"; - tag.async = true; - tag.defer = true; - tag.onerror = error => { - cleanup(); - tag.remove(); - reject(error); - }; - tag.onload = () => { - cleanup(); - - if(config.verbose) console.debug("Script %o loaded", path); - setTimeout(resolve, 100); - }; - - document.getElementById("scripts").appendChild(tag); - - tag.src = source.url + (cache_tag || ""); - }); - })(); - } - } - - export async function load_scripts(paths: SourcePath[]) : Promise { - const promises: Promise[] = []; - const errors: { - script: SourcePath, - error: any - }[] = []; - - for(const script of paths) - promises.push(load_script(script).catch(error => { - errors.push({ - script: script, - error: error - }); - return Promise.resolve(); - })); - - await Promise.all([...promises]); - - if(errors.length > 0) { - if(config.error) { - console.error("Failed to load the following scripts:"); - for(const script of errors) - console.log(" - %o: %o", script.script, script.error); - } - - loader.critical_error("Failed to load script " + script_name(errors[0].script) + "
" + "View the browser console for more information!"); - throw "failed to load script " + script_name(errors[0].script); - } - } - - export async function load_style(path: SourcePath) : Promise { - if(Array.isArray(path)) { //We have some fallback - return load_style(path[0]).catch(error => { - if(error instanceof SyntaxError) - return Promise.reject(error.source); - - if(path.length > 1) - return load_script(path.slice(1)); - - return Promise.reject(error); - }); - } else { - if(!path) { - return Promise.resolve(); - } - - return new Promise((resolve, reject) => { - const tag: HTMLLinkElement = document.createElement("link"); - - let error = false; - const error_handler = (event: ErrorEvent) => { - if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error); - if(event.filename == tag.href) { //FIXME! - window.removeEventListener('error', error_handler as any); - - reject(new SyntaxError(event.error)); - event.preventDefault(); - error = true; - } - }; - window.addEventListener('error', error_handler as any); - - tag.type = "text/css"; - tag.rel = "stylesheet"; - - const cleanup = () => { - tag.onerror = undefined; - tag.onload = undefined; - - clearTimeout(timeout_handle); - window.removeEventListener('error', error_handler as any); - }; - - const timeout_handle = setTimeout(() => { - cleanup(); - reject("timeout"); - }, 5000); - - tag.onerror = error => { - cleanup(); - tag.remove(); - if(config.error) - console.error("File load error for file %s: %o", path, error); - reject("failed to load file " + path); - }; - tag.onload = () => { - cleanup(); - { - const css: CSSStyleSheet = tag.sheet as CSSStyleSheet; - const rules = css.cssRules; - const rules_remove: number[] = []; - const rules_add: string[] = []; - - for(let index = 0; index < rules.length; index++) { - const rule = rules.item(index); - let rule_text = rule.cssText; - - if(rule.cssText.indexOf("%%base_path%%") != -1) { - rules_remove.push(index); - rules_add.push(rule_text.replace("%%base_path%%", document.location.origin + document.location.pathname)); - } - } - - for(const index of rules_remove.sort((a, b) => b > a ? 1 : 0)) { - if(css.removeRule) - css.removeRule(index); - else - css.deleteRule(index); - } - for(const rule of rules_add) - css.insertRule(rule, rules_remove[0]); - } - - if(config.verbose) console.debug("Style sheet %o loaded", path); - setTimeout(resolve, 100); - }; - - document.getElementById("style").appendChild(tag); - tag.href = path + (cache_tag || ""); - }); - } - } - - export async function load_styles(paths: SourcePath[]) : Promise { - const promises: Promise[] = []; - const errors: { - sheet: SourcePath, - error: any - }[] = []; - - for(const sheet of paths) - promises.push(load_style(sheet).catch(error => { - errors.push({ - sheet: sheet, - error: error - }); - return Promise.resolve(); - })); - - await Promise.all([...promises]); - - if(errors.length > 0) { - if(loader.config.error) { - console.error("Failed to load the following style sheet:"); - for(const sheet of errors) - console.log(" - %o: %o", sheet.sheet, sheet.error); - } - - loader.critical_error("Failed to load style sheet " + script_name(errors[0].sheet) + "
" + "View the browser console for more information!"); - throw "failed to load style sheet " + script_name(errors[0].sheet); - } - } - - export async function load_template(path: SourcePath) : Promise { - try { - const response = await $.ajax(path + (cache_tag || "")); - - let node = document.createElement("html"); - node.innerHTML = response; - let tags: HTMLCollection; - if(node.getElementsByTagName("body").length > 0) - tags = node.getElementsByTagName("body")[0].children; - else - tags = node.children; - - let root = document.getElementById("templates"); - if(!root) { - loader.critical_error("Failed to find template tag!"); - throw "Failed to find template tag"; - } - while(tags.length > 0){ - let tag = tags.item(0); - root.appendChild(tag); - - } - } catch(error) { - let msg; - if('responseText' in error) - msg = error.responseText; - else if(error instanceof Error) - msg = error.message; - - loader.critical_error("failed to load template " + script_name(path), msg); - throw "template error"; - } - } - - export async function load_templates(paths: SourcePath[]) : Promise { - const promises: Promise[] = []; - const errors: { - template: SourcePath, - error: any - }[] = []; - - for(const template of paths) - promises.push(load_template(template).catch(error => { - errors.push({ - template: template, - error: error - }); - return Promise.resolve(); - })); - - await Promise.all([...promises]); - - if(errors.length > 0) { - if (loader.config.error) { - console.error("Failed to load the following templates:"); - for (const sheet of errors) - console.log(" - %o: %o", sheet.template, sheet.error); - } - - loader.critical_error("Failed to load template " + script_name(errors[0].template) + "
" + "View the browser console for more information!"); - throw "failed to load template " + script_name(errors[0].template); - } - } - - - export type ErrorHandler = (message: string, detail: string) => void; - - let _callback_critical_error: ErrorHandler; - let _callback_critical_called: boolean = false; - - export function critical_error(message: string, detail?: string) { - if(_callback_critical_called) { - console.warn("[CRITICAL] %s", message); - if(typeof(detail) === "string") - console.warn("[CRITICAL] %s", detail); - return; - } - - _callback_critical_called = true; - if(_callback_critical_error) { - _callback_critical_error(message, detail); - return; - } - - /* default handling */ - let tag = document.getElementById("critical-load"); - - { - const error_tags = tag.getElementsByClassName("error"); - error_tags[0].innerHTML = message; - } - - if(typeof(detail) === "string") { - let node_detail = tag.getElementsByClassName("detail")[0]; - node_detail.innerHTML = detail; - } - - tag.classList.add("shown"); - } - - export function critical_error_handler(handler?: ErrorHandler, override?: boolean) : ErrorHandler { - if((typeof(handler) === "object" && handler !== _callback_critical_error) || override) - _callback_critical_error = handler; - return _callback_critical_error; - } -} - -{ - - const hello_world = () => { - const clog = console.log; - const print_security = () => { - { - const css = [ - "display: block", - "text-align: center", - "font-size: 42px", - "font-weight: bold", - "-webkit-text-stroke: 2px black", - "color: red" - ].join(";"); - clog("%c ", "font-size: 100px;"); - clog("%cSecurity warning:", css); - } - { - const css = [ - "display: block", - "text-align: center", - "font-size: 18px", - "font-weight: bold" - ].join(";"); - - clog("%cPasting anything in here could give attackers access to your data.", css); - clog("%cUnless you understand exactly what you are doing, close this window and stay safe.", css); - clog("%c ", "font-size: 100px;"); - } - }; - - /* print the hello world */ - { - const css = [ - "display: block", - "text-align: center", - "font-size: 72px", - "font-weight: bold", - "-webkit-text-stroke: 2px black", - "color: #18BC9C" - ].join(";"); - clog("%cHey, hold on!", css); - } - { - const css = [ - "display: block", - "text-align: center", - "font-size: 26px", - "font-weight: bold" - ].join(";"); - - const css_2 = [ - "display: block", - "text-align: center", - "font-size: 26px", - "font-weight: bold", - "color: blue" - ].join(";"); - - const display_detect = /./; - display_detect.toString = function() { print_security(); return ""; }; - - clog("%cLovely to see you using and debugging the TeaSpeak-Web client.", css); - clog("%cIf you have some good ideas or already done some incredible changes,", css); - clog("%cyou'll be may interested to share them here: %chttps://github.com/TeaSpeak/TeaWeb", css, css_2); - clog("%c ", display_detect); - } - }; - - try { /* lets try to print it as VM code :)*/ - let hello_world_code = hello_world.toString(); - hello_world_code = hello_world_code.substr(hello_world_code.indexOf('() => {') + 8); - hello_world_code = hello_world_code.substring(0, hello_world_code.lastIndexOf("}")); - - //Look aheads are not possible with firefox - //hello_world_code = hello_world_code.replace(/(? fadeoutLoader(duration, 0, true), minAge - age); - return; - } - */ - - $(".loader .bookshelf_wrapper").animate({top: 0, opacity: 0}, duration); - $(".loader .half").animate({width: 0}, duration, () => { - $(".loader").detach(); - }); -} - - -/* set a timeout here, so if this script is merged with the actual loader (like in rel mode) the actual could load this manually here */ -setTimeout(() => { - if(loader.running()) { - if(loader.config.verbose) - console.debug("Do not execute debug loading"); - return; - } - - loader.register_task(loader.Stage.INITIALIZING, { - priority: 100, - function: async () => { - let loader_type; - /* - location.search.replace(/(?:^\?|&)([a-zA-Z_]+)=([.a-zA-Z0-9]+)(?=$|&)/g, (_, key: string, value: string) => { - if(key.toLowerCase() == "loader_target") - loader_type = value; - return ""; - }); - */ - for(const node of document.head.getElementsByTagName("meta")) { - if(node.name === "app-loader-target") { - loader_type = node.content; - if(loader_type) - break; - } - } - loader_type = loader_type || "app"; - if(loader_type === "app") { - try { - await loader.load_scripts(["loader/app.js"]); - } catch (error) { - console.error("Failed to load main app script: %o", error); - loader.critical_error("Failed to load main app script", error); - throw "app loader failed"; - } - } else if(loader_type === "certaccept") { - try { - await loader.load_scripts(["loader/certaccept.js"]); - } catch (error) { - console.error("Failed to load cert accept script: %o", error); - loader.critical_error("Failed to load cert accept script", error); - throw "app loader failed"; - } - } else { - loader.critical_error("Missing loader target: " + loader_type); - throw "Missing loader target: " + loader_type; - } - }, - name: "loading target" - }); - - loader.execute_managed(); -}, 0); diff --git a/shared/popup/certaccept/js/main.ts b/shared/popup/certaccept/js/main.ts index 7d31c110..003129be 100644 --- a/shared/popup/certaccept/js/main.ts +++ b/shared/popup/certaccept/js/main.ts @@ -1,4 +1,9 @@ +import {settings, Settings} from "tc-shared/settings"; +import * as loader from "tc-loader"; +import {LogCategory} from "tc-shared/log"; +import * as bipc from "tc-shared/BrowserIPC"; +const is_debug = false; //TODO: Sync with loader! function tr(text: string) { return text; } loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { @@ -38,4 +43,25 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { } }, priority: 10 +}); + + + +loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { + name: "settings initialisation", + function: async () => Settings.initialize(), + priority: 200 +}); + +loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { + name: "bipc initialisation", + function: async () => bipc.setup(), + priority: 100 +}); + + +loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { + name: "log enabled initialisation", + function: async () => log.initialize(is_debug ? log.LogType.TRACE : log.LogType.INFO), + priority: 150 }); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index fdf294a3..9bd65288 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,8 @@ "paths": { "*": ["shared/declarations/*"], "tc-shared/*": ["shared/js/*"], - "tc-backend/*": ["web/js/*"] + "tc-backend/*": ["shared/backend.d/*"], + "tc-loader": ["loader/exports/loader.d.ts"] } }, "exclude": [ diff --git a/web/js/WebPPTListener.ts b/web/js/WebPPTListener.ts deleted file mode 100644 index 92ec8883..00000000 --- a/web/js/WebPPTListener.ts +++ /dev/null @@ -1,152 +0,0 @@ -/// - - -namespace ppt { - interface WebKeyEvent extends KeyEvent { - canceled: boolean; - } - - let key_listener: ((_: KeyEvent) => any)[] = []; - - function listener_key(type: EventType, event: KeyboardEvent) { - const key_event = { - type: type, - - key: event.key, - key_code: event.code, - - key_ctrl: event.ctrlKey, - key_shift: event.shiftKey, - key_alt: event.altKey, - key_windows: event.metaKey, - - canceled: event.defaultPrevented - } as WebKeyEvent; - //console.debug("Trigger key event %o", key_event); - - for(const listener of key_listener) - listener(key_event); - - if(key_event.canceled) - event.preventDefault(); - } - - const proxy_key_press = event => listener_key(EventType.KEY_PRESS, event); - const proxy_key_release = event => listener_key(EventType.KEY_RELEASE, event); - const proxy_key_typed = event => listener_key(EventType.KEY_TYPED, event); - - export function initialize() : Promise { - document.addEventListener('keypress', proxy_key_typed); - document.addEventListener('keydown', proxy_key_press); - document.addEventListener('keyup', proxy_key_release); - window.addEventListener('blur', listener_blur); - - register_key_listener(listener_hook); - return Promise.resolve(); - } - - export function finalize() { - document.removeEventListener("keypress", proxy_key_typed); - document.removeEventListener("keydown", proxy_key_press); - document.removeEventListener("keyup", proxy_key_release); - window.removeEventListener('blur', listener_blur); - - unregister_key_listener(listener_hook); - } - - export function register_key_listener(listener: (_: KeyEvent) => any) { - key_listener.push(listener); - } - - export function unregister_key_listener(listener: (_: KeyEvent) => any) { - key_listener.remove(listener); - } - - - let key_hooks: KeyHook[] = []; - - interface CurrentState { - keys: {[code: string]:KeyEvent}; - special: { [key:number]:boolean }; - } - let current_state: CurrentState = { - special: [] - } as any; - - let key_hooks_active: KeyHook[] = []; - - function listener_blur() { - current_state.special[SpecialKey.ALT] = false; - current_state.special[SpecialKey.CTRL] = false; - current_state.special[SpecialKey.SHIFT] = false; - current_state.special[SpecialKey.WINDOWS] = false; - - for(const code of Object.keys(current_state)) - if(code !== "special") - delete current_state[code]; - - for(const hook of key_hooks_active) - hook.callback_release(); - key_hooks_active = []; - } - - function listener_hook(event: KeyEvent) { - if(event.type == EventType.KEY_TYPED) - return; - - let old_hooks = [...key_hooks_active]; - let new_hooks = []; - - current_state.special[SpecialKey.ALT] = event.key_alt; - current_state.special[SpecialKey.CTRL] = event.key_ctrl; - current_state.special[SpecialKey.SHIFT] = event.key_shift; - current_state.special[SpecialKey.WINDOWS] = event.key_windows; - - current_state[event.key_code] = undefined; - if(event.type == EventType.KEY_PRESS) { - current_state[event.key_code] = event; - - for(const hook of key_hooks) { - if(hook.key_code !== event.key_code) continue; - if(hook.key_alt != event.key_alt) continue; - if(hook.key_ctrl != event.key_ctrl) continue; - if(hook.key_shift != event.key_shift) continue; - if(hook.key_windows != event.key_windows) continue; - - new_hooks.push(hook); - if(!old_hooks.remove(hook) && hook.callback_press) { - hook.callback_press(); - log.trace(LogCategory.GENERAL, tr("Trigger key press for %o!"), hook); - } - } - } - - //We have a new situation - for(const hook of old_hooks) { - //Do not test for meta key states because they could differ in a key release event - if(hook.key_code === event.key_code) { - if(hook.callback_release) { - hook.callback_release(); - log.trace(LogCategory.GENERAL, tr("Trigger key release for %o!"), hook); - } - } else { - new_hooks.push(hook); - } - } - key_hooks_active = new_hooks; - } - - export function register_key_hook(hook: KeyHook) { - key_hooks.push(hook); - } - - export function unregister_key_hook(hook: KeyHook) { - key_hooks.remove(hook); - } - - export function key_pressed(code: string | SpecialKey) : boolean { - if(typeof(code) === 'string') - return typeof current_state[code] !== "undefined"; - return current_state.special[code]; - } -} \ No newline at end of file diff --git a/web/js/audio/AudioPlayer.ts b/web/js/audio/AudioPlayer.ts deleted file mode 100644 index 25538997..00000000 --- a/web/js/audio/AudioPlayer.ts +++ /dev/null @@ -1,103 +0,0 @@ -namespace audio.player { - let _globalContext: AudioContext; - let _global_destination: GainNode; - - let _globalContextPromise: Promise; - let _initialized_listener: (() => any)[] = []; - let _master_volume: number = 1; - let _no_device = false; - - export function initialize() : boolean { - context(); - return true; - } - - export function initialized() : boolean { - return !!_globalContext && _globalContext.state === 'running'; - } - - function fire_initialized() { - log.info(LogCategory.AUDIO, tr("File initialized for %d listeners"), _initialized_listener.length); - while(_initialized_listener.length > 0) - _initialized_listener.pop_front()(); - } - - - export function context() : AudioContext { - if(_globalContext && _globalContext.state != "suspended") return _globalContext; - - if(!_globalContext) - _globalContext = new (window.webkitAudioContext || window.AudioContext)(); - - _initialized_listener.unshift(() => { - _global_destination = _globalContext.createGain(); - _global_destination.gain.value = _no_device ? 0 : _master_volume; - _global_destination.connect(_globalContext.destination); - }); - if(_globalContext.state == "suspended") { - if(!_globalContextPromise) { - (_globalContextPromise = _globalContext.resume()).then(() => { - fire_initialized(); - }).catch(error => { - loader.critical_error("Failed to initialize global audio context! (" + error + ")"); - }); - } - _globalContext.resume(); //We already have our listener - return _globalContext; - } - - if(_globalContext.state == "running") { - fire_initialized(); - return _globalContext; - } - return _globalContext; - } - - export function get_master_volume() : number { - return _master_volume; - } - export function set_master_volume(volume: number) { - _master_volume = volume; - if(_global_destination) - _global_destination.gain.value = _no_device ? 0 : _master_volume; - } - - export function destination() : AudioNode { - const ctx = context(); - if(!ctx) throw tr("Audio player isn't initialized yet!"); - - return _global_destination; - } - - export function on_ready(cb: () => any) { - if(initialized()) - cb(); - else - _initialized_listener.push(cb); - } - - export const WEB_DEVICE: Device = { - device_id: "default", - name: "default playback", - driver: 'Web Audio' - }; - - export function available_devices() : Promise { - return Promise.resolve([WEB_DEVICE]) - } - - export function set_device(device_id: string) : Promise { - _no_device = !device_id; - _global_destination.gain.value = _no_device ? 0 : _master_volume; - - return Promise.resolve(); - } - - export function current_device() : Device { - return WEB_DEVICE; - } - - export function initializeFromGesture() { - context(); - } -} diff --git a/web/js/audio/WebCodec.ts b/web/js/audio/WebCodec.ts deleted file mode 100644 index fa472b71..00000000 --- a/web/js/audio/WebCodec.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// - -namespace audio.codec { - export function new_instance(type: CodecType) : BasicCodec { - return new CodecWrapperWorker(type); - } - - export function supported(type: CodecType) : boolean { - return type == CodecType.OPUS_MUSIC || type == CodecType.OPUS_VOICE; - } -} \ No newline at end of file diff --git a/web/js/audio/player.ts b/web/js/audio/player.ts new file mode 100644 index 00000000..a929e0e5 --- /dev/null +++ b/web/js/audio/player.ts @@ -0,0 +1,128 @@ +/* +import {Device} from "tc-shared/audio/player"; + +export function initialize() : boolean; +export function initialized() : boolean; + +export function context() : AudioContext; +export function get_master_volume() : number; +export function set_master_volume(volume: number); + +export function destination() : AudioNode; + +export function on_ready(cb: () => any); + +export function available_devices() : Promise; +export function set_device(device_id: string) : Promise; + +export function current_device() : Device; + +export function initializeFromGesture(); +*/ + +import {Device} from "tc-shared/audio/player"; +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; +import * as loader from "tc-loader"; + +let _globalContext: AudioContext; +let _global_destination: GainNode; + +let _globalContextPromise: Promise; +let _initialized_listener: (() => any)[] = []; +let _master_volume: number = 1; +let _no_device = false; + +export function initialize() : boolean { + context(); + return true; +} + +export function initialized() : boolean { + return !!_globalContext && _globalContext.state === 'running'; +} + +function fire_initialized() { + log.info(LogCategory.AUDIO, tr("File initialized for %d listeners"), _initialized_listener.length); + while(_initialized_listener.length > 0) + _initialized_listener.pop_front()(); +} + + +export function context() : AudioContext { + if(_globalContext && _globalContext.state != "suspended") return _globalContext; + + if(!_globalContext) + _globalContext = new (window.webkitAudioContext || window.AudioContext)(); + + _initialized_listener.unshift(() => { + _global_destination = _globalContext.createGain(); + _global_destination.gain.value = _no_device ? 0 : _master_volume; + _global_destination.connect(_globalContext.destination); + }); + if(_globalContext.state == "suspended") { + if(!_globalContextPromise) { + (_globalContextPromise = _globalContext.resume()).then(() => { + fire_initialized(); + }).catch(error => { + loader.critical_error("Failed to initialize global audio context! (" + error + ")"); + }); + } + _globalContext.resume(); //We already have our listener + return _globalContext; + } + + if(_globalContext.state == "running") { + fire_initialized(); + return _globalContext; + } + return _globalContext; +} + +export function get_master_volume() : number { + return _master_volume; +} +export function set_master_volume(volume: number) { + _master_volume = volume; + if(_global_destination) + _global_destination.gain.value = _no_device ? 0 : _master_volume; +} + +export function destination() : AudioNode { + const ctx = context(); + if(!ctx) throw tr("Audio player isn't initialized yet!"); + + return _global_destination; +} + +export function on_ready(cb: () => any) { + if(initialized()) + cb(); + else + _initialized_listener.push(cb); +} + +export const WEB_DEVICE: Device = { + device_id: "default", + name: "default playback", + driver: 'Web Audio' +}; + +export function available_devices() : Promise { + return Promise.resolve([WEB_DEVICE]) +} + +export function set_device(device_id: string) : Promise { + _no_device = !device_id; + _global_destination.gain.value = _no_device ? 0 : _master_volume; + + return Promise.resolve(); +} + +export function current_device() : Device { + return WEB_DEVICE; +} + +export function initializeFromGesture() { + context(); +} \ No newline at end of file diff --git a/web/js/audio/recorder.ts b/web/js/audio/recorder.ts new file mode 100644 index 00000000..d79e2db9 --- /dev/null +++ b/web/js/audio/recorder.ts @@ -0,0 +1,873 @@ +import { + AbstractInput, CallbackInputConsumer, + InputConsumer, + InputConsumerType, + InputDevice, InputStartResult, + InputState, + LevelMeter, NodeInputConsumer +} from "tc-shared/voice/RecorderBase"; +import * as log from "tc-shared/log"; +import * as loader from "tc-loader"; +import {LogCategory} from "tc-shared/log"; +import * as aplayer from "./player"; +import * as rbase from "tc-shared/voice/RecorderBase"; + +declare global { + interface MediaStream { + stop(); + } +} + +let _queried_devices: JavascriptInputDevice[]; +let _queried_permissioned: boolean = false; + +export interface JavascriptInputDevice extends InputDevice { + device_id: string; + group_id: string; +} + +function getUserMediaFunctionPromise() : (constraints: MediaStreamConstraints) => Promise { + if('mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices) + return constraints => navigator.mediaDevices.getUserMedia(constraints); + + const _callbacked_function = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; + if(!_callbacked_function) + return undefined; + + return constraints => new Promise((resolve, reject) => _callbacked_function(constraints, resolve, reject)); +} + +async function query_devices() { + const general_supported = !!getUserMediaFunctionPromise(); + + try { + const context = aplayer.context(); + const devices = await navigator.mediaDevices.enumerateDevices(); + + _queried_permissioned = false; + if(devices.filter(e => !!e.label).length > 0) + _queried_permissioned = true; + + _queried_devices = devices.filter(e => e.kind === "audioinput").map((e: MediaDeviceInfo): JavascriptInputDevice => { + return { + channels: context ? context.destination.channelCount : 2, + sample_rate: context ? context.sampleRate : 44100, + + default_input: e.deviceId == "default", + + driver: "WebAudio", + name: e.label || "device-id{" + e.deviceId+ "}", + + supported: general_supported, + + device_id: e.deviceId, + group_id: e.groupId, + + unique_id: e.deviceId + } + }); + if(_queried_devices.length > 0 && _queried_devices.filter(e => e.default_input).length == 0) + _queried_devices[0].default_input = true; + } catch(error) { + log.error(LogCategory.AUDIO, tr("Failed to query microphone devices (%o)"), error); + _queried_devices = []; + } +} + +export function devices() : InputDevice[] { + if(typeof(_queried_devices) === "undefined") + query_devices(); + + return _queried_devices || []; +} + + +export function device_refresh_available() : boolean { return true; } +export function refresh_devices() : Promise { return query_devices(); } + +export function create_input() : AbstractInput { return new JavascriptInput(); } + +export async function create_levelmeter(device: InputDevice) : Promise { + const meter = new JavascriptLevelmeter(device as any); + await meter.initialize(); + return meter; +} + +loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { + function: async () => { query_devices(); }, /* May wait for it? */ + priority: 10, + name: "query media devices" +}); + +export namespace filter { + export abstract class JAbstractFilter implements rbase.filter.Filter { + type; + + source_node: AudioNode; + audio_node: NodeType; + + context: AudioContext; + enabled: boolean = false; + + active: boolean = false; /* if true the filter filters! */ + callback_active_change: (new_state: boolean) => any; + + paused: boolean = true; + + abstract initialize(context: AudioContext, source_node: AudioNode); + abstract finalize(); + + /* whatever the input has been paused and we don't expect any input */ + abstract set_pause(flag: boolean); + + is_enabled(): boolean { + return this.enabled; + } + } + + export class JThresholdFilter extends JAbstractFilter implements rbase.filter.ThresholdFilter { + public static update_task_interval = 20; /* 20ms */ + + type = rbase.filter.Type.THRESHOLD; + callback_level?: (value: number) => any; + + private _threshold = 50; + + private _update_task: any; + private _analyser: AnalyserNode; + private _analyse_buffer: Uint8Array; + + private _silence_count = 0; + private _margin_frames = 5; + + private _current_level = 0; + private _smooth_release = 0; + private _smooth_attack = 0; + + finalize() { + this.set_pause(true); + + if(this.source_node) { + try { this.source_node.disconnect(this._analyser) } catch (error) {} + try { this.source_node.disconnect(this.audio_node) } catch (error) {} + } + + this._analyser = undefined; + this.source_node = undefined; + this.audio_node = undefined; + this.context = undefined; + } + + initialize(context: AudioContext, source_node: AudioNode) { + this.context = context; + this.source_node = source_node; + + this.audio_node = context.createGain(); + this._analyser = context.createAnalyser(); + + const optimal_ftt_size = Math.ceil((source_node.context || context).sampleRate * (JThresholdFilter.update_task_interval / 1000)); + const base2_ftt = Math.pow(2, Math.ceil(Math.log2(optimal_ftt_size))); + this._analyser.fftSize = base2_ftt; + + if(!this._analyse_buffer || this._analyse_buffer.length < this._analyser.fftSize) + this._analyse_buffer = new Uint8Array(this._analyser.fftSize); + + this.active = false; + this.audio_node.gain.value = 1; + + this.source_node.connect(this.audio_node); + this.source_node.connect(this._analyser); + + /* force update paused state */ + this.set_pause(!(this.paused = !this.paused)); + } + + get_margin_frames(): number { return this._margin_frames; } + set_margin_frames(value: number) { + this._margin_frames = value; + } + + get_attack_smooth(): number { + return this._smooth_attack; + } + + get_release_smooth(): number { + return this._smooth_release; + } + + set_attack_smooth(value: number) { + this._smooth_attack = value; + } + + set_release_smooth(value: number) { + this._smooth_release = value; + } + + get_threshold(): number { + return this._threshold; + } + + set_threshold(value: number): Promise { + this._threshold = value; + return Promise.resolve(); + } + + public static process(buffer: Uint8Array, ftt_size: number, previous: number, smooth: number) { + let level; + { + let total = 0, float, rms; + + for(let index = 0; index < ftt_size; index++) { + float = ( buffer[index++] / 0x7f ) - 1; + total += (float * float); + } + rms = Math.sqrt(total / ftt_size); + let db = 20 * ( Math.log(rms) / Math.log(10) ); + // sanity check + + db = Math.max(-192, Math.min(db, 0)); + level = 100 + ( db * 1.92 ); + } + + return previous * smooth + level * (1 - smooth); + } + + private _analyse() { + this._analyser.getByteTimeDomainData(this._analyse_buffer); + + let smooth; + if(this._silence_count == 0) + smooth = this._smooth_release; + else + smooth = this._smooth_attack; + + this._current_level = JThresholdFilter.process(this._analyse_buffer, this._analyser.fftSize, this._current_level, smooth); + + this._update_gain_node(); + if(this.callback_level) + this.callback_level(this._current_level); + } + + private _update_gain_node() { + let state; + if(this._current_level > this._threshold) { + this._silence_count = 0; + state = true; + } else { + state = this._silence_count++ < this._margin_frames; + } + if(state) { + this.audio_node.gain.value = 1; + if(this.active) { + this.active = false; + this.callback_active_change(false); + } + } else { + this.audio_node.gain.value = 0; + if(!this.active) { + this.active = true; + this.callback_active_change(true); + } + } + } + + set_pause(flag: boolean) { + if(flag === this.paused) return; + this.paused = flag; + + if(this.paused) { + clearInterval(this._update_task); + this._update_task = undefined; + + if(this.active) { + this.active = false; + this.callback_active_change(false); + } + } else { + if(!this._update_task && this._analyser) + this._update_task = setInterval(() => this._analyse(), JThresholdFilter.update_task_interval); + } + } + } + + export class JStateFilter extends JAbstractFilter implements rbase.filter.StateFilter { + type = rbase.filter.Type.STATE; + + finalize() { + if(this.source_node) { + try { this.source_node.disconnect(this.audio_node) } catch (error) {} + } + + this.source_node = undefined; + this.audio_node = undefined; + this.context = undefined; + } + + initialize(context: AudioContext, source_node: AudioNode) { + this.context = context; + this.source_node = source_node; + + this.audio_node = context.createGain(); + this.audio_node.gain.value = this.active ? 0 : 1; + + this.source_node.connect(this.audio_node); + } + + is_active(): boolean { + return this.active; + } + + set_state(state: boolean): Promise { + if(this.active === state) + return Promise.resolve(); + + this.active = state; + if(this.audio_node) + this.audio_node.gain.value = state ? 0 : 1; + this.callback_active_change(state); + return Promise.resolve(); + } + + set_pause(flag: boolean) { + this.paused = flag; + } + } +} + +class JavascriptInput implements AbstractInput { + private _state: InputState = InputState.PAUSED; + private _current_device: JavascriptInputDevice | undefined; + private _current_consumer: InputConsumer; + + private _current_stream: MediaStream; + private _current_audio_stream: MediaStreamAudioSourceNode; + + private _audio_context: AudioContext; + private _source_node: AudioNode; /* last node which could be connected to the target; target might be the _consumer_node */ + private _consumer_callback_node: ScriptProcessorNode; + private readonly _consumer_audio_callback; + private _volume_node: GainNode; + private _mute_node: GainNode; + + private _filters: rbase.filter.Filter[] = []; + private _filter_active: boolean = false; + + private _volume: number = 1; + + callback_begin: () => any = undefined; + callback_end: () => any = undefined; + + constructor() { + aplayer.on_ready(() => this._audio_initialized()); + this._consumer_audio_callback = this._audio_callback.bind(this); + } + + private _audio_initialized() { + this._audio_context = aplayer.context(); + if(!this._audio_context) + return; + + this._mute_node = this._audio_context.createGain(); + this._mute_node.gain.value = 0; + this._mute_node.connect(this._audio_context.destination); + + this._consumer_callback_node = this._audio_context.createScriptProcessor(1024 * 4); + this._consumer_callback_node.connect(this._mute_node); + + this._volume_node = this._audio_context.createGain(); + this._volume_node.gain.value = this._volume; + + this._initialize_filters(); + if(this._state === InputState.INITIALIZING) + this.start(); + } + + private _initialize_filters() { + const filters = this._filters as any as filter.JAbstractFilter[]; + for(const filter of filters) { + if(filter.is_enabled()) + filter.finalize(); + } + + if(this._audio_context && this._volume_node) { + const active_filter = filters.filter(e => e.is_enabled()); + let stream: AudioNode = this._volume_node; + for(const f of active_filter) { + f.initialize(this._audio_context, stream); + stream = f.audio_node; + } + this._switch_source_node(stream); + } + } + + private _audio_callback(event: AudioProcessingEvent) { + if(!this._current_consumer || this._current_consumer.type !== InputConsumerType.CALLBACK) + return; + + const callback = this._current_consumer as CallbackInputConsumer; + if(callback.callback_audio) + callback.callback_audio(event.inputBuffer); + if(callback.callback_buffer) { + log.warn(LogCategory.AUDIO, tr("AudioInput has callback buffer, but this isn't supported yet!")); + } + } + + current_state() : InputState { return this._state; }; + + private _start_promise: Promise; + async start() : Promise { + if(this._start_promise) { + try { + await this._start_promise; + if(this._state != InputState.PAUSED) + return; + } catch(error) { + log.debug(LogCategory.AUDIO, tr("JavascriptInput:start() Start promise await resulted in an error: %o"), error); + } + } + + return await (this._start_promise = this._start()); + } + + /* request permission for devices only one per time! */ + private static _running_request: Promise; + static async request_media_stream(device_id: string, group_id: string) : Promise { + while(this._running_request) { + try { + await this._running_request; + } catch(error) { } + } + const promise = (this._running_request = this.request_media_stream0(device_id, group_id)); + try { + return await this._running_request; + } finally { + if(this._running_request === promise) + this._running_request = undefined; + } + } + + static async request_media_stream0(device_id: string, group_id: string) : Promise { + const media_function = getUserMediaFunctionPromise(); + if(!media_function) return InputStartResult.ENOTSUPPORTED; + + try { + log.info(LogCategory.AUDIO, tr("Requesting a microphone stream for device %s in group %s"), device_id, group_id); + + const audio_constrains: MediaTrackConstraints = {}; + audio_constrains.deviceId = device_id; + audio_constrains.groupId = group_id; + + audio_constrains.echoCancellation = true; + /* may supported */ (audio_constrains as any).autoGainControl = true; + /* may supported */ (audio_constrains as any).noiseSuppression = true; + /* disabled because most the time we get a OverconstrainedError */ //audio_constrains.sampleSize = {min: 420, max: 960 * 10, ideal: 960}; + + const stream = await media_function({audio: audio_constrains, video: undefined}); + if(!_queried_permissioned) query_devices(); /* we now got permissions, requery devices */ + return stream; + } catch(error) { + if('name' in error) { + if(error.name === "NotAllowedError") { + //createErrorModal(tr("Failed to create microphone"), tr("Microphone recording failed. Please allow TeaWeb access to your microphone")).open(); + //FIXME: Move this to somewhere else! + + log.warn(LogCategory.AUDIO, tr("Microphone request failed (No permissions). Browser message: %o"), error.message); + return InputStartResult.ENOTALLOWED; + } else { + log.warn(LogCategory.AUDIO, tr("Microphone request failed. Request resulted in error: %o: %o"), error.name, error); + } + } else { + log.warn(LogCategory.AUDIO, tr("Failed to initialize recording stream (%o)"), error); + } + return InputStartResult.EUNKNOWN; + } + } + + private async _start() : Promise { + try { + if(this._state != InputState.PAUSED) + throw tr("recorder already started"); + + this._state = InputState.INITIALIZING; + if(!this._current_device) + throw tr("invalid device"); + + if(!this._audio_context) { + debugger; + throw tr("missing audio context"); + } + + const _result = await JavascriptInput.request_media_stream(this._current_device.device_id, this._current_device.group_id); + if(!(_result instanceof MediaStream)) { + this._state = InputState.PAUSED; + return _result; + } + this._current_stream = _result; + + for(const f of this._filters) + if(f.is_enabled() && f instanceof filter.JAbstractFilter) + f.set_pause(false); + this._consumer_callback_node.addEventListener('audioprocess', this._consumer_audio_callback); + + this._current_audio_stream = this._audio_context.createMediaStreamSource(this._current_stream); + this._current_audio_stream.connect(this._volume_node); + this._state = InputState.RECORDING; + return InputStartResult.EOK; + } catch(error) { + if(this._state == InputState.INITIALIZING) { + this._state = InputState.PAUSED; + } + throw error; + } finally { + this._start_promise = undefined; + } + } + + async stop() { + /* await all starts */ + try { + if(this._start_promise) + await this._start_promise; + } catch(error) {} + + this._state = InputState.PAUSED; + if(this._current_audio_stream) + this._current_audio_stream.disconnect(); + + if(this._current_stream) { + if(this._current_stream.stop) + this._current_stream.stop(); + else + this._current_stream.getTracks().forEach(value => { + value.stop(); + }); + } + + this._current_stream = undefined; + this._current_audio_stream = undefined; + for(const f of this._filters) + if(f.is_enabled() && f instanceof filter.JAbstractFilter) + f.set_pause(true); + if(this._consumer_callback_node) + this._consumer_callback_node.removeEventListener('audioprocess', this._consumer_audio_callback); + return undefined; + } + + + current_device(): InputDevice | undefined { + return this._current_device; + } + + async set_device(device: InputDevice | undefined) { + if(this._current_device === device) + return; + + + const saved_state = this._state; + try { + await this.stop(); + } catch(error) { + log.warn(LogCategory.AUDIO, tr("Failed to stop previous record session (%o)"), error); + } + + this._current_device = device as any; /* TODO: Test for device_id and device_group */ + if(!device) { + this._state = saved_state === InputState.PAUSED ? InputState.PAUSED : InputState.DRY; + return; + } + + if(saved_state !== InputState.PAUSED) { + try { + await this.start() + } catch(error) { + log.warn(LogCategory.AUDIO, tr("Failed to start new recording stream (%o)"), error); + throw "failed to start record"; + } + } + return; + } + + + get_filter(type: rbase.filter.Type): rbase.filter.Filter | undefined { + for(const filter of this._filters) + if(filter.type == type) + return filter; + + let new_filter: filter.JAbstractFilter; + switch (type) { + case rbase.filter.Type.STATE: + new_filter = new filter.JStateFilter(); + break; + case rbase.filter.Type.VOICE_LEVEL: + throw "voice filter isn't supported!"; + case rbase.filter.Type.THRESHOLD: + new_filter = new filter.JThresholdFilter(); + break; + default: + throw "invalid filter type, or type isn't implemented! (" + type + ")"; + } + + new_filter.callback_active_change = () => this._recalculate_filter_status(); + this._filters.push(new_filter as any); + this.enable_filter(type); + return new_filter as any; + } + + supports_filter(type: rbase.filter.Type) : boolean { + switch (type) { + case rbase.filter.Type.THRESHOLD: + case rbase.filter.Type.STATE: + return true; + default: + return false; + } + } + + private find_filter(type: rbase.filter.Type) : filter.JAbstractFilter | undefined { + for(const filter of this._filters) + if(filter.type == type) + return filter as any; + return undefined; + } + + clear_filter() { + for(const _filter of this._filters) { + if(!_filter.is_enabled()) + continue; + + const c_filter = _filter as any as filter.JAbstractFilter; + c_filter.finalize(); + c_filter.enabled = false; + } + + this._initialize_filters(); + this._recalculate_filter_status(); + } + + disable_filter(type: rbase.filter.Type) { + const filter = this.find_filter(type); + if(!filter) return; + + /* test if the filter is active */ + if(!filter.is_enabled()) + return; + + filter.enabled = false; + filter.set_pause(true); + filter.finalize(); + this._initialize_filters(); + this._recalculate_filter_status(); + } + + enable_filter(type: rbase.filter.Type) { + const filter = this.get_filter(type) as any as filter.JAbstractFilter; + if(filter.is_enabled()) + return; + + filter.enabled = true; + filter.set_pause(typeof this._current_audio_stream !== "object"); + this._initialize_filters(); + this._recalculate_filter_status(); + } + + private _recalculate_filter_status() { + let filtered = this._filters.filter(e => e.is_enabled()).filter(e => (e as any as filter.JAbstractFilter).active).length > 0; + if(filtered === this._filter_active) + return; + + this._filter_active = filtered; + if(filtered) { + if(this.callback_end) + this.callback_end(); + } else { + if(this.callback_begin) + this.callback_begin(); + } + } + + current_consumer(): InputConsumer | undefined { + return this._current_consumer; + } + + async set_consumer(consumer: InputConsumer) { + if(this._current_consumer) { + if(this._current_consumer.type == InputConsumerType.NODE) { + if(this._source_node) + (this._current_consumer as NodeInputConsumer).callback_disconnect(this._source_node) + } else if(this._current_consumer.type === InputConsumerType.CALLBACK) { + if(this._source_node) + this._source_node.disconnect(this._consumer_callback_node); + } + } + + if(consumer) { + if(consumer.type == InputConsumerType.CALLBACK) { + if(this._source_node) + this._source_node.connect(this._consumer_callback_node); + } else if(consumer.type == InputConsumerType.NODE) { + if(this._source_node) + (consumer as NodeInputConsumer).callback_node(this._source_node); + } else { + throw "native callback consumers are not supported!"; + } + } + this._current_consumer = consumer; + } + + private _switch_source_node(new_node: AudioNode) { + if(this._current_consumer) { + if(this._current_consumer.type == InputConsumerType.NODE) { + const node_consumer = this._current_consumer as NodeInputConsumer; + if(this._source_node) + node_consumer.callback_disconnect(this._source_node); + if(new_node) + node_consumer.callback_node(new_node); + } else if(this._current_consumer.type == InputConsumerType.CALLBACK) { + this._source_node.disconnect(this._consumer_callback_node); + if(new_node) + new_node.connect(this._consumer_callback_node); + } + } + this._source_node = new_node; + } + + get_volume(): number { + return this._volume; + } + + set_volume(volume: number) { + if(volume === this._volume) + return; + this._volume = volume; + this._volume_node.gain.value = volume; + } +} + +class JavascriptLevelmeter implements LevelMeter { + private static _instances: JavascriptLevelmeter[] = []; + private static _update_task: number; + + readonly _device: JavascriptInputDevice; + + private _callback: (num: number) => any; + + private _context: AudioContext; + private _gain_node: GainNode; + private _source_node: MediaStreamAudioSourceNode; + private _analyser_node: AnalyserNode; + + private _media_stream: MediaStream; + + private _analyse_buffer: Uint8Array; + + private _current_level = 0; + + constructor(device: JavascriptInputDevice) { + this._device = device; + } + + async initialize() { + try { + await new Promise((resolve, reject) => { + const timeout = setTimeout(reject, 5000); + aplayer.on_ready(() => { + clearTimeout(timeout); + resolve(); + }); + }); + } catch(error) { + throw tr("audio context timeout"); + } + this._context = aplayer.context(); + if(!this._context) throw tr("invalid context"); + + this._gain_node = this._context.createGain(); + this._gain_node.gain.setValueAtTime(0, 0); + + /* analyser node */ + this._analyser_node = this._context.createAnalyser(); + + const optimal_ftt_size = Math.ceil(this._context.sampleRate * (filter.JThresholdFilter.update_task_interval / 1000)); + this._analyser_node.fftSize = Math.pow(2, Math.ceil(Math.log2(optimal_ftt_size))); + + if(!this._analyse_buffer || this._analyse_buffer.length < this._analyser_node.fftSize) + this._analyse_buffer = new Uint8Array(this._analyser_node.fftSize); + + /* starting stream */ + const _result = await JavascriptInput.request_media_stream(this._device.device_id, this._device.group_id); + if(!(_result instanceof MediaStream)){ + if(_result === InputStartResult.ENOTALLOWED) + throw tr("No permissions"); + if(_result === InputStartResult.ENOTSUPPORTED) + throw tr("Not supported"); + if(_result === InputStartResult.EBUSY) + throw tr("Device busy"); + if(_result === InputStartResult.EUNKNOWN) + throw tr("an error occurred"); + throw _result; + } + this._media_stream = _result; + + this._source_node = this._context.createMediaStreamSource(this._media_stream); + this._source_node.connect(this._analyser_node); + this._analyser_node.connect(this._gain_node); + this._gain_node.connect(this._context.destination); + + JavascriptLevelmeter._instances.push(this); + if(JavascriptLevelmeter._instances.length == 1) { + clearInterval(JavascriptLevelmeter._update_task); + JavascriptLevelmeter._update_task = setInterval(() => JavascriptLevelmeter._analyse_all(), filter.JThresholdFilter.update_task_interval) as any; + } + } + + destory() { + JavascriptLevelmeter._instances.remove(this); + if(JavascriptLevelmeter._instances.length == 0) { + clearInterval(JavascriptLevelmeter._update_task); + JavascriptLevelmeter._update_task = 0; + } + + if(this._source_node) { + this._source_node.disconnect(); + this._source_node = undefined; + } + if(this._media_stream) { + if(this._media_stream.stop) + this._media_stream.stop(); + else + this._media_stream.getTracks().forEach(value => { + value.stop(); + }); + this._media_stream = undefined; + } + if(this._gain_node) { + this._gain_node.disconnect(); + this._gain_node = undefined; + } + if(this._analyser_node) { + this._analyser_node.disconnect(); + this._analyser_node = undefined; + } + } + + device(): InputDevice { + return this._device; + } + + set_observer(callback: (value: number) => any) { + this._callback = callback; + } + + private static _analyse_all() { + for(const instance of [...this._instances]) + instance._analyse(); + } + + private _analyse() { + this._analyser_node.getByteTimeDomainData(this._analyse_buffer); + + this._current_level = filter.JThresholdFilter.process(this._analyse_buffer, this._analyser_node.fftSize, this._current_level, .75); + if(this._callback) + this._callback(this._current_level); + } +} \ No newline at end of file diff --git a/web/js/audio/sounds.ts b/web/js/audio/sounds.ts index 117733bf..86a01874 100644 --- a/web/js/audio/sounds.ts +++ b/web/js/audio/sounds.ts @@ -1,126 +1,129 @@ -namespace audio.sounds { - interface SoundEntry { - cached?: AudioBuffer; - node?: HTMLAudioElement; - } +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; +import {SoundFile} from "tc-shared/sound/Sounds"; +import * as aplayer from "./player"; - const error_already_handled = "---- error handled ---"; +interface SoundEntry { + cached?: AudioBuffer; + node?: HTMLAudioElement; +} - const file_cache: {[key: string]: Promise & { timestamp: number }} = {}; - let warned = false; +const error_already_handled = "---- error handled ---"; - function get_song_entry(file: sound.SoundFile) : Promise { - if(typeof file_cache[file.path] === "object") { - return new Promise((resolve, reject) => { - if(file_cache[file.path].timestamp + 60 * 1000 > Date.now()) { - file_cache[file.path].then(resolve).catch(reject); - return; - } +const file_cache: {[key: string]: Promise & { timestamp: number }} = {}; +let warned = false; - const original_timestamp = Date.now(); - return file_cache[file.path].catch(error => { - if(file_cache[file.path].timestamp + 60 * 1000 > original_timestamp) - return Promise.reject(error); - delete file_cache[file.path]; - return get_song_entry(file); - }); - }); - } - - const context = audio.player.context(); - if(!context) throw tr("audio context not initialized"); - - return (file_cache[file.path] = Object.assign((async () => { - const entry = {} as SoundEntry; - if(context.decodeAudioData) { - const xhr = new XMLHttpRequest(); - xhr.open('GET', file.path, true); - xhr.responseType = 'arraybuffer'; - - try { - const result = new Promise((resolve, reject) => { - xhr.onload = resolve; - xhr.onerror = reject; - }); - - xhr.send(); - await result; - - if (xhr.status != 200) - throw "invalid response code (" + xhr.status + ")"; - - try { - entry.cached = await context.decodeAudioData(xhr.response); - } catch(error) { - log.error(LogCategory.AUDIO, error); - throw tr("failed to decode audio data"); - } - } catch(error) { - log.error(LogCategory.AUDIO, tr("Failed to load audio file %s. Error: %o"), sound, error); - throw error_already_handled; - } - } else { - if(!warned) { - warned = true; - log.warn(LogCategory.AUDIO, tr("Your browser does not support decodeAudioData! Using a node to playback! This bypasses the audio output and volume regulation!")); - } - const container = $("#sounds"); - const node = $.spawn("audio").attr("src", file.path); - node.appendTo(container); - - entry.node = node[0]; - } - return entry; - })(), { timestamp: Date.now() })); - } - - export async function play_sound(file: sound.SoundFile) : Promise { - const entry = get_song_entry(file); - if(!entry) { - log.warn(LogCategory.AUDIO, tr("Failed to replay sound %s because it could not be resolved."), file.path); - return; - } - - try { - const sound = await entry; - - if(sound.cached) { - const context = audio.player.context(); - if(!context) throw tr("audio context not initialized (this error should never show up!)"); - - const player = context.createBufferSource(); - player.buffer = sound.cached; - player.start(0); - - const play_promise = new Promise(resolve => player.onended = resolve); - if(file.volume != 1 && context.createGain) { - const gain = context.createGain(); - if(gain.gain.setValueAtTime) - gain.gain.setValueAtTime(file.volume, 0); - else - gain.gain.value = file.volume; - - player.connect(gain); - gain.connect(context.destination); - } else { - player.connect(context.destination); - } - - await play_promise; - } else if(sound.node) { - sound.node.currentTime = 0; - await sound.node.play(); - } else { - throw "missing playback handle"; - } - } catch(error) { - if(error === error_already_handled) { - log.warn(LogCategory.AUDIO, tr("Failed to replay sound %s because of an error while loading (see log above)."), file.path); +function get_song_entry(file: SoundFile) : Promise { + if(typeof file_cache[file.path] === "object") { + return new Promise((resolve, reject) => { + if(file_cache[file.path].timestamp + 60 * 1000 > Date.now()) { + file_cache[file.path].then(resolve).catch(reject); return; } - log.warn(LogCategory.AUDIO, tr("Failed to replay sound %s: %o"), file.path, error); + const original_timestamp = Date.now(); + return file_cache[file.path].catch(error => { + if(file_cache[file.path].timestamp + 60 * 1000 > original_timestamp) + return Promise.reject(error); + delete file_cache[file.path]; + return get_song_entry(file); + }); + }); + } + + const context = aplayer.context(); + if(!context) throw tr("audio context not initialized"); + + return (file_cache[file.path] = Object.assign((async () => { + const entry = {} as SoundEntry; + if(context.decodeAudioData) { + const xhr = new XMLHttpRequest(); + xhr.open('GET', file.path, true); + xhr.responseType = 'arraybuffer'; + + try { + const result = new Promise((resolve, reject) => { + xhr.onload = resolve; + xhr.onerror = reject; + }); + + xhr.send(); + await result; + + if (xhr.status != 200) + throw "invalid response code (" + xhr.status + ")"; + + try { + entry.cached = await context.decodeAudioData(xhr.response); + } catch(error) { + log.error(LogCategory.AUDIO, error); + throw tr("failed to decode audio data"); + } + } catch(error) { + log.error(LogCategory.AUDIO, tr("Failed to load audio file %s. Error: %o"), file, error); + throw error_already_handled; + } + } else { + if(!warned) { + warned = true; + log.warn(LogCategory.AUDIO, tr("Your browser does not support decodeAudioData! Using a node to playback! This bypasses the audio output and volume regulation!")); + } + const container = $("#sounds"); + const node = $.spawn("audio").attr("src", file.path); + node.appendTo(container); + + entry.node = node[0]; + } + return entry; + })(), { timestamp: Date.now() })); +} + +export async function play_sound(file: SoundFile) : Promise { + const entry = get_song_entry(file); + if(!entry) { + log.warn(LogCategory.AUDIO, tr("Failed to replay sound %s because it could not be resolved."), file.path); + return; + } + + try { + const sound = await entry; + + if(sound.cached) { + const context = aplayer.context(); + if(!context) throw tr("audio context not initialized (this error should never show up!)"); + + const player = context.createBufferSource(); + player.buffer = sound.cached; + player.start(0); + + const play_promise = new Promise(resolve => player.onended = resolve); + if(file.volume != 1 && context.createGain) { + const gain = context.createGain(); + if(gain.gain.setValueAtTime) + gain.gain.setValueAtTime(file.volume, 0); + else + gain.gain.value = file.volume; + + player.connect(gain); + gain.connect(context.destination); + } else { + player.connect(context.destination); + } + + await play_promise; + } else if(sound.node) { + sound.node.currentTime = 0; + await sound.node.play(); + } else { + throw "missing playback handle"; + } + } catch(error) { + if(error === error_already_handled) { + log.warn(LogCategory.AUDIO, tr("Failed to replay sound %s because of an error while loading (see log above)."), file.path); return; } + + log.warn(LogCategory.AUDIO, tr("Failed to replay sound %s: %o"), file.path, error); + return; } } \ No newline at end of file diff --git a/web/js/codec/BasicCodec.ts b/web/js/codec/BasicCodec.ts index 5334686d..6c772a0c 100644 --- a/web/js/codec/BasicCodec.ts +++ b/web/js/codec/BasicCodec.ts @@ -1,4 +1,8 @@ -/// +import * as log from "tc-shared/log"; +import * as aplayer from "../audio/player"; +import {LogCategory} from "tc-shared/log"; +import {BufferChunk, Codec, CodecClientCache} from "./Codec"; +import {AudioResampler} from "../voice/AudioResampler"; class AVGCalculator { history_size: number = 100; @@ -18,7 +22,7 @@ class AVGCalculator { } } -abstract class BasicCodec implements Codec { +export abstract class BasicCodec implements Codec { protected _audioContext: OfflineAudioContext; protected _decodeResampler: AudioResampler; protected _encodeResampler: AudioResampler; @@ -32,9 +36,9 @@ abstract class BasicCodec implements Codec { protected constructor(codecSampleRate: number) { this.channelCount = 1; this.samplesPerUnit = 960; - this._audioContext = new (window.webkitOfflineAudioContext || window.OfflineAudioContext)(audio.player.destination().channelCount, 1024, audio.player.context().sampleRate); + this._audioContext = new (window.webkitOfflineAudioContext || window.OfflineAudioContext)(aplayer.destination().channelCount, 1024, aplayer.context().sampleRate); this._codecSampleRate = codecSampleRate; - this._decodeResampler = new AudioResampler(audio.player.context().sampleRate); + this._decodeResampler = new AudioResampler(aplayer.context().sampleRate); this._encodeResampler = new AudioResampler(codecSampleRate); } diff --git a/web/js/codec/Codec.ts b/web/js/codec/Codec.ts index fbbd416d..9b74ae52 100644 --- a/web/js/codec/Codec.ts +++ b/web/js/codec/Codec.ts @@ -1,8 +1,8 @@ -interface CodecCostructor { +export interface CodecConstructor { new (codecSampleRate: number) : Codec; } -enum CodecType { +export enum CodecType { OPUS_VOICE, OPUS_MUSIC, @@ -12,7 +12,7 @@ enum CodecType { CELT_MONO } -class BufferChunk { +export class BufferChunk { buffer: AudioBuffer; index: number; @@ -34,7 +34,7 @@ class BufferChunk { } } -class CodecClientCache { +export class CodecClientCache { _last_access: number; _chunks: BufferChunk[] = []; @@ -46,7 +46,7 @@ class CodecClientCache { } } -interface Codec { +export interface Codec { on_encoded_data: (Uint8Array) => void; channelCount: number; diff --git a/web/js/codec/CodecWrapperRaw.ts b/web/js/codec/CodecWrapperRaw.ts index dc7b937d..b6b591f1 100644 --- a/web/js/codec/CodecWrapperRaw.ts +++ b/web/js/codec/CodecWrapperRaw.ts @@ -1,6 +1,6 @@ -/// +import {BasicCodec} from "./BasicCodec"; -class CodecWrapperRaw extends BasicCodec { +export class CodecWrapperRaw extends BasicCodec { converterRaw: any; converter: Uint8Array; bufferSize: number = 4096 * 4; diff --git a/web/js/codec/CodecWrapperWorker.ts b/web/js/codec/CodecWrapperWorker.ts index de79127b..ea6c42ed 100644 --- a/web/js/codec/CodecWrapperWorker.ts +++ b/web/js/codec/CodecWrapperWorker.ts @@ -1,6 +1,10 @@ -/// +import {BasicCodec} from "./BasicCodec"; +import {CodecType} from "./Codec"; +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; +import {settings} from "tc-shared/settings"; -class CodecWrapperWorker extends BasicCodec { +export class CodecWrapperWorker extends BasicCodec { private _worker: Worker; private _workerListener: {token: string, resolve: (data: any) => void}[] = []; private _workerCallbackToken = "callback_token"; diff --git a/web/js/connection.ts b/web/js/connection.ts new file mode 100644 index 00000000..f6867385 --- /dev/null +++ b/web/js/connection.ts @@ -0,0 +1,4 @@ +import * as sconnection from "./connection/ServerConnection"; + +export const spawn_server_connection = sconnection.spawn_server_connection; +export const destroy_server_connection = sconnection.destroy_server_connection; \ No newline at end of file diff --git a/web/js/connection/ServerConnection.ts b/web/js/connection/ServerConnection.ts index 2c9d3bc9..9b7b3469 100644 --- a/web/js/connection/ServerConnection.ts +++ b/web/js/connection/ServerConnection.ts @@ -1,3 +1,23 @@ +import { + AbstractServerConnection, + CommandOptionDefaults, + CommandOptions, + ConnectionStateListener, voice +} from "tc-shared/connection/ConnectionBase"; +import {ConnectionHandler, ConnectionState, DisconnectReason} from "tc-shared/ConnectionHandler"; +import {ServerAddress} from "tc-shared/ui/server"; +import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler"; +import {ConnectionCommandHandler, ServerConnectionCommandBoss} from "tc-shared/connection/CommandHandler"; +import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; +import {settings, Settings} from "tc-shared/settings"; +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; +import {Regex} from "tc-shared/ui/modal/ModalConnect"; +import AbstractVoiceConnection = voice.AbstractVoiceConnection; +import {AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler"; +import * as elog from "tc-shared/ui/frames/server_log"; +import {VoiceConnection} from "../voice/VoiceHandler"; + class ReturnListener { resolve: (value?: T | PromiseLike) => void; reject: (reason?: any) => void; @@ -6,499 +26,497 @@ class ReturnListener { timeout: NodeJS.Timer; } -namespace connection { - export class ServerConnection extends AbstractServerConnection { - _connectionState: ConnectionState = ConnectionState.UNCONNECTED; +export class ServerConnection extends AbstractServerConnection { + _connectionState: ConnectionState = ConnectionState.UNCONNECTED; - private _remote_address: ServerAddress; - private _handshakeHandler: HandshakeHandler; + private _remote_address: ServerAddress; + private _handshakeHandler: HandshakeHandler; - private _command_boss: ServerConnectionCommandBoss; - private _command_handler_default: ConnectionCommandHandler; + private _command_boss: ServerConnectionCommandBoss; + private _command_handler_default: ConnectionCommandHandler; - private _socket_connected: WebSocket; + private _socket_connected: WebSocket; - private _connect_timeout_timer: NodeJS.Timer = undefined; + private _connect_timeout_timer: NodeJS.Timer = undefined; - private _connected: boolean = false; - private _retCodeIdx: number; - private _retListener: ReturnListener[]; + private _connected: boolean = false; + private _retCodeIdx: number; + private _retListener: ReturnListener[]; - private _connection_state_listener: connection.ConnectionStateListener; - private _voice_connection: audio.js.VoiceConnection; + private _connection_state_listener: ConnectionStateListener; + private _voice_connection: VoiceConnection; - private _ping = { - thread_id: 0, + private _ping = { + thread_id: 0, - last_request: 0, - last_response: 0, + last_request: 0, + last_response: 0, - request_id: 0, - interval: 5000, - timeout: 7500, + request_id: 0, + interval: 5000, + timeout: 7500, - value: 0, - value_native: 0 /* ping value for native (WS) */ + value: 0, + value_native: 0 /* ping value for native (WS) */ + }; + + constructor(client : ConnectionHandler) { + super(client); + + this._retCodeIdx = 0; + this._retListener = []; + + this._command_boss = new ServerConnectionCommandBoss(this); + this._command_handler_default = new ConnectionCommandHandler(this); + + this._command_boss.register_handler(this._command_handler_default); + this.command_helper.initialize(); + + if(!settings.static_global(Settings.KEY_DISABLE_VOICE, false)) + this._voice_connection = new VoiceConnection(this); + } + + destroy() { + this.disconnect("handle destroyed").catch(error => { + log.warn(LogCategory.NETWORKING, tr("Failed to disconnect on server connection destroy: %o"), error); + }).then(() => { + clearInterval(this._ping.thread_id); + clearTimeout(this._connect_timeout_timer); + + for(const listener of this._retListener) { + try { + listener.reject("handler destroyed"); + } catch(error) { + log.warn(LogCategory.NETWORKING, tr("Failed to reject command promise: %o"), error); + } + } + this._retListener = undefined; + + this.command_helper.destroy(); + + this._command_handler_default && this._command_boss.unregister_handler(this._command_handler_default); + this._command_handler_default = undefined; + + this._voice_connection && this._voice_connection.destroy(); + this._voice_connection = undefined; + + this._command_boss && this._command_boss.destroy(); + this._command_boss = undefined; + }); + } + + private generateReturnCode() : string { + return (this._retCodeIdx++).toString(); + } + + async connect(address : ServerAddress, handshake: HandshakeHandler, timeout?: number) : Promise { + timeout = typeof(timeout) === "number" ? timeout : 5000; + + try { + await this.disconnect() + } catch(error) { + log.error(LogCategory.NETWORKING, tr("Failed to close old connection properly. Error: %o"), error); + throw "failed to cleanup old connection"; + } + + this.updateConnectionState(ConnectionState.CONNECTING); + this._remote_address = address; + + this._handshakeHandler = handshake; + this._handshakeHandler.setConnection(this); + + /* The direct one connect directly to the target address. The other via the .con-gate.work */ + let local_direct_socket: WebSocket; + let local_proxy_socket: WebSocket; + let connected_socket: WebSocket; + let local_timeout_timer: NodeJS.Timer; + + /* setting up an timeout */ + local_timeout_timer = setTimeout(async () => { + log.error(LogCategory.NETWORKING, tr("Connect timeout triggered. Aborting connect attempt!")); + try { + await this.disconnect(); + } catch(error) { + log.warn(LogCategory.NETWORKING, tr("Failed to close connection after timeout had been triggered! (%o)"), error); + } + + error_cleanup(); + this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE); + }, timeout); + this._connect_timeout_timer = local_timeout_timer; + + const error_cleanup = () => { + try { local_direct_socket.close(); } catch(ex) {} + try { local_proxy_socket.close(); } catch(ex) {} + clearTimeout(local_timeout_timer); }; - constructor(client : ConnectionHandler) { - super(client); + try { + let proxy_host; + if(Regex.IP_V4.test(address.host)) + proxy_host = address.host.replace(/\./g, "-") + ".con-gate.work"; + else if(Regex.IP_V6.test(address.host)) + proxy_host = address.host.replace(/\[(.*)]/, "$1").replace(/:/g, "_") + ".con-gate.work"; - this._retCodeIdx = 0; - this._retListener = []; + if(proxy_host && !settings.static_global(Settings.KEY_CONNECT_NO_DNSPROXY)) + local_proxy_socket = new WebSocket('wss://' + proxy_host + ":" + address.port); + local_direct_socket = new WebSocket('wss://' + address.host + ":" + address.port); - this._command_boss = new ServerConnectionCommandBoss(this); - this._command_handler_default = new ConnectionCommandHandler(this); + connected_socket = await new Promise(resolve => { + let pending = 0, succeed = false; + if(local_proxy_socket) { + pending++; - this._command_boss.register_handler(this._command_handler_default); - this.command_helper.initialize(); + local_proxy_socket.onerror = event => { + --pending; + if(this._connect_timeout_timer != local_timeout_timer) + log.trace(LogCategory.NETWORKING, tr("Proxy socket send an error while connecting. Pending sockets: %d. Any succeed: %s"), pending, succeed ? tr("yes") : tr("no")); + if(!succeed && pending == 0) + resolve(undefined); + }; - if(!settings.static_global(Settings.KEY_DISABLE_VOICE, false)) - this._voice_connection = new audio.js.VoiceConnection(this); - } - - destroy() { - this.disconnect("handle destroyed").catch(error => { - log.warn(LogCategory.NETWORKING, tr("Failed to disconnect on server connection destroy: %o"), error); - }).then(() => { - clearInterval(this._ping.thread_id); - clearTimeout(this._connect_timeout_timer); - - for(const listener of this._retListener) { - try { - listener.reject("handler destroyed"); - } catch(error) { - log.warn(LogCategory.NETWORKING, tr("Failed to reject command promise: %o"), error); - } - } - this._retListener = undefined; - - this.command_helper.destroy(); - - this._command_handler_default && this._command_boss.unregister_handler(this._command_handler_default); - this._command_handler_default = undefined; - - this._voice_connection && this._voice_connection.destroy(); - this._voice_connection = undefined; - - this._command_boss && this._command_boss.destroy(); - this._command_boss = undefined; - }); - } - - private generateReturnCode() : string { - return (this._retCodeIdx++).toString(); - } - - async connect(address : ServerAddress, handshake: HandshakeHandler, timeout?: number) : Promise { - timeout = typeof(timeout) === "number" ? timeout : 5000; - - try { - await this.disconnect() - } catch(error) { - log.error(LogCategory.NETWORKING, tr("Failed to close old connection properly. Error: %o"), error); - throw "failed to cleanup old connection"; - } - - this.updateConnectionState(ConnectionState.CONNECTING); - this._remote_address = address; - - this._handshakeHandler = handshake; - this._handshakeHandler.setConnection(this); - - /* The direct one connect directly to the target address. The other via the .con-gate.work */ - let local_direct_socket: WebSocket; - let local_proxy_socket: WebSocket; - let connected_socket: WebSocket; - let local_timeout_timer: NodeJS.Timer; - - /* setting up an timeout */ - local_timeout_timer = setTimeout(async () => { - log.error(LogCategory.NETWORKING, tr("Connect timeout triggered. Aborting connect attempt!")); - try { - await this.disconnect(); - } catch(error) { - log.warn(LogCategory.NETWORKING, tr("Failed to close connection after timeout had been triggered! (%o)"), error); - } - - error_cleanup(); - this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE); - }, timeout); - this._connect_timeout_timer = local_timeout_timer; - - const error_cleanup = () => { - try { local_direct_socket.close(); } catch(ex) {} - try { local_proxy_socket.close(); } catch(ex) {} - clearTimeout(local_timeout_timer); - }; - - try { - let proxy_host; - if(Modals.Regex.IP_V4.test(address.host)) - proxy_host = address.host.replace(/\./g, "-") + ".con-gate.work"; - else if(Modals.Regex.IP_V6.test(address.host)) - proxy_host = address.host.replace(/\[(.*)]/, "$1").replace(/:/g, "_") + ".con-gate.work"; - - if(proxy_host && !settings.static_global(Settings.KEY_CONNECT_NO_DNSPROXY)) - local_proxy_socket = new WebSocket('wss://' + proxy_host + ":" + address.port); - local_direct_socket = new WebSocket('wss://' + address.host + ":" + address.port); - - connected_socket = await new Promise(resolve => { - let pending = 0, succeed = false; - if(local_proxy_socket) { - pending++; - - local_proxy_socket.onerror = event => { - --pending; - if(this._connect_timeout_timer != local_timeout_timer) - log.trace(LogCategory.NETWORKING, tr("Proxy socket send an error while connecting. Pending sockets: %d. Any succeed: %s"), pending, succeed ? tr("yes") : tr("no")); - if(!succeed && pending == 0) - resolve(undefined); - }; - - local_proxy_socket.onopen = event => { - --pending; - if(this._connect_timeout_timer != local_timeout_timer) - log.trace(LogCategory.NETWORKING, tr("Proxy socket connected. Pending sockets: %d. Any succeed before: %s"), pending, succeed ? tr("yes") : tr("no")); - if(!succeed) { - succeed = true; - resolve(local_proxy_socket); - } - }; - } - - if(local_direct_socket) { - pending++; - - local_direct_socket.onerror = event => { - --pending; - if(this._connect_timeout_timer != local_timeout_timer) - log.trace(LogCategory.NETWORKING, tr("Direct socket send an error while connecting. Pending sockets: %d. Any succeed: %s"), pending, succeed ? tr("yes") : tr("no")); - if(!succeed && pending == 0) - resolve(undefined); - }; - - local_direct_socket.onopen = event => { - --pending; - if(this._connect_timeout_timer != local_timeout_timer) - log.trace(LogCategory.NETWORKING, tr("Direct socket connected. Pending sockets: %d. Any succeed before: %s"), pending, succeed ? tr("yes") : tr("no")); - if(!succeed) { - succeed = true; - resolve(local_direct_socket); - } - }; - } - - if(local_proxy_socket && local_proxy_socket.readyState == WebSocket.OPEN) - local_proxy_socket.onopen(undefined); - - if(local_direct_socket && local_direct_socket.readyState == WebSocket.OPEN) - local_direct_socket.onopen(undefined); - }); - - if(!connected_socket) { - //We failed to connect. Lets test if we're still relevant - if(this._connect_timeout_timer != local_timeout_timer) { - log.trace(LogCategory.NETWORKING, tr("Failed to connect to %s, but we're already obsolete."), address.host + ":" + address.port); - error_cleanup(); - } else { - try { - await this.disconnect(); - } catch(error) { - log.warn(LogCategory.NETWORKING, tr("Failed to cleanup connection after unsuccessful connect attempt: %o"), error); + local_proxy_socket.onopen = event => { + --pending; + if(this._connect_timeout_timer != local_timeout_timer) + log.trace(LogCategory.NETWORKING, tr("Proxy socket connected. Pending sockets: %d. Any succeed before: %s"), pending, succeed ? tr("yes") : tr("no")); + if(!succeed) { + succeed = true; + resolve(local_proxy_socket); } - error_cleanup(); - this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE); - } - return; + }; } + if(local_direct_socket) { + pending++; + + local_direct_socket.onerror = event => { + --pending; + if(this._connect_timeout_timer != local_timeout_timer) + log.trace(LogCategory.NETWORKING, tr("Direct socket send an error while connecting. Pending sockets: %d. Any succeed: %s"), pending, succeed ? tr("yes") : tr("no")); + if(!succeed && pending == 0) + resolve(undefined); + }; + + local_direct_socket.onopen = event => { + --pending; + if(this._connect_timeout_timer != local_timeout_timer) + log.trace(LogCategory.NETWORKING, tr("Direct socket connected. Pending sockets: %d. Any succeed before: %s"), pending, succeed ? tr("yes") : tr("no")); + if(!succeed) { + succeed = true; + resolve(local_direct_socket); + } + }; + } + + if(local_proxy_socket && local_proxy_socket.readyState == WebSocket.OPEN) + local_proxy_socket.onopen(undefined); + + if(local_direct_socket && local_direct_socket.readyState == WebSocket.OPEN) + local_direct_socket.onopen(undefined); + }); + + if(!connected_socket) { + //We failed to connect. Lets test if we're still relevant if(this._connect_timeout_timer != local_timeout_timer) { - log.trace(LogCategory.NETWORKING, tr("Successfully connected to %s, but we're already obsolete. Closing connections"), address.host + ":" + address.port); + log.trace(LogCategory.NETWORKING, tr("Failed to connect to %s, but we're already obsolete."), address.host + ":" + address.port); error_cleanup(); - return; - } - - clearTimeout(local_timeout_timer); - this._connect_timeout_timer = undefined; - - if(connected_socket == local_proxy_socket) { - log.debug(LogCategory.NETWORKING, tr("Established a TCP connection to %s via proxy to %s"), address.host + ":" + address.port, proxy_host); - this._remote_address.host = proxy_host; } else { - log.debug(LogCategory.NETWORKING, tr("Established a TCP connection to %s directly"), address.host + ":" + address.port); - } - - this._socket_connected = connected_socket; - this._socket_connected.onclose = event => { - if(this._socket_connected != connected_socket) return; /* this socket isn't from interest anymore */ - - this.client.handleDisconnect(this._connected ? DisconnectReason.CONNECTION_CLOSED : DisconnectReason.CONNECT_FAILURE, { - code: event.code, - reason: event.reason, - event: event - }); - }; - - this._socket_connected.onerror = e => { - if(this._socket_connected != connected_socket) return; /* this socket isn't from interest anymore */ - - log.warn(LogCategory.NETWORKING, tr("Received web socket error: (%o)"), e); - }; - - this._socket_connected.onmessage = msg => { - if(this._socket_connected != connected_socket) return; /* this socket isn't from interest anymore */ - - this.handle_socket_message(msg.data); - }; - - this._connected = true; - this.start_handshake(); - } catch (error) { - error_cleanup(); - if(this._socket_connected != connected_socket && this._connect_timeout_timer != local_timeout_timer) - return; /* we're not from interest anymore */ - - log.warn(LogCategory.NETWORKING, tr("Received unexpected error while connecting: %o"), error); - try { - await this.disconnect(); - } catch(error) { - log.warn(LogCategory.NETWORKING, tr("Failed to cleanup connection after unsuccessful connect attempt: %o"), error); - } - this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE, error); - } - } - - private start_handshake() { - this.updateConnectionState(ConnectionState.INITIALISING); - this.client.log.log(log.server.Type.CONNECTION_LOGIN, {}); - this._handshakeHandler.initialize(); - this._handshakeHandler.startHandshake(); - } - - updateConnectionState(state: ConnectionState) { - const old_state = this._connectionState; - this._connectionState = state; - if(this._connection_state_listener) - this._connection_state_listener(old_state, state); - } - - async disconnect(reason?: string) : Promise { - clearTimeout(this._connect_timeout_timer); - this._connect_timeout_timer = undefined; - - clearTimeout(this._ping.thread_id); - this._ping.thread_id = undefined; - - if(typeof(reason) === "string") { - //TODO send disconnect reason - } - - - if(this._connectionState != ConnectionState.UNCONNECTED) - this.updateConnectionState(ConnectionState.UNCONNECTED); - - if(this._voice_connection) - this._voice_connection.drop_rtp_session(); - - - if(this._socket_connected) { - this._socket_connected.close(3000 + 0xFF, tr("request disconnect")); - this._socket_connected = undefined; - } - - - for(let future of this._retListener) - future.reject(tr("Connection closed")); - this._retListener = []; - - this._connected = false; - this._retCodeIdx = 0; - } - - private handle_socket_message(data) { - if(typeof(data) === "string") { - let json; - try { - json = JSON.parse(data); - } catch(e) { - log.warn(LogCategory.NETWORKING, tr("Could not parse message json!")); - alert(e); // error in the above string (in this case, yes)! - return; - } - if(json["type"] === undefined) { - log.warn(LogCategory.NETWORKING, tr("Missing data type in message!")); - return; - } - if(json["type"] === "command") { - let group = log.group(log.LogType.DEBUG, LogCategory.NETWORKING, tr("Handling command '%s'"), json["command"]); - group.log(tr("Handling command '%s'"), json["command"]); - group.group(log.LogType.TRACE, tr("Json:")).collapsed(true).log("%o", json).end(); - - this._command_boss.invoke_handle({ - command: json["command"], - arguments: json["data"] - }); - - if(json["command"] === "initserver") { - this._ping.thread_id = setInterval(() => this.do_ping(), this._ping.interval) as any; - this.do_ping(); - this.updateConnectionState(ConnectionState.CONNECTED); - if(this._voice_connection) - this._voice_connection.start_rtc_session(); /* FIXME: Move it to a handler boss and not here! */ + try { + await this.disconnect(); + } catch(error) { + log.warn(LogCategory.NETWORKING, tr("Failed to cleanup connection after unsuccessful connect attempt: %o"), error); } - group.end(); - } else if(json["type"] === "WebRTC") { - if(this._voice_connection) - this._voice_connection.handleControlPacket(json); - else - log.warn(LogCategory.NETWORKING, tr("Dropping WebRTC command packet, because we haven't a bridge.")) - } else if(json["type"] === "ping") { - this.sendData(JSON.stringify({ - type: 'pong', - payload: json["payload"] - })); - } else if(json["type"] === "pong") { - const id = parseInt(json["payload"]); - if(id != this._ping.request_id) { - log.warn(LogCategory.NETWORKING, tr("Received pong which is older than the last request. Delay may over %oms? (Index: %o, Current index: %o)"), this._ping.timeout, id, this._ping.request_id); - } else { - this._ping.last_response = 'now' in performance ? performance.now() : Date.now(); - this._ping.value = this._ping.last_response - this._ping.last_request; - this._ping.value_native = parseInt(json["ping_native"]) / 1000; /* we're getting it in microseconds and not milliseconds */ - log.debug(LogCategory.NETWORKING, tr("Received new pong. Updating ping to: JS: %o Native: %o"), this._ping.value.toFixed(3), this._ping.value_native.toFixed(3)); - } - } else { - log.warn(LogCategory.NETWORKING, tr("Unknown command type %o"), json["type"]); + error_cleanup(); + this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE); } - } else { - log.warn(LogCategory.NETWORKING, tr("Received unknown message of type %s. Dropping message"), typeof(data)); - } - } - - sendData(data: any) { - if(!this._socket_connected || this._socket_connected.readyState != 1) { - log.warn(LogCategory.NETWORKING, tr("Tried to send on a invalid socket (%s)"), this._socket_connected ? "invalid state (" + this._socket_connected.readyState + ")" : "invalid socket"); return; } - this._socket_connected.send(data); - } - private commandiefy(input: any) : string { - return JSON.stringify(input, (key, value) => { - switch (typeof value) { - case "boolean": return value == true ? "1" : "0"; - case "function": return value(); - default: - return value; - } - }); - - } - - send_command(command: string, data?: any | any[], _options?: CommandOptions) : Promise { - if(!this._socket_connected || !this.connected()) { - log.warn(LogCategory.NETWORKING, tr("Tried to send a command without a valid connection.")); - return Promise.reject(tr("not connected")); + if(this._connect_timeout_timer != local_timeout_timer) { + log.trace(LogCategory.NETWORKING, tr("Successfully connected to %s, but we're already obsolete. Closing connections"), address.host + ":" + address.port); + error_cleanup(); + return; } - const options: CommandOptions = {}; - Object.assign(options, CommandOptionDefaults); - Object.assign(options, _options); + clearTimeout(local_timeout_timer); + this._connect_timeout_timer = undefined; - data = $.isArray(data) ? data : [data || {}]; - if(data.length == 0) /* we require min one arg to append return_code */ - data.push({}); - - const _this = this; - let result = new Promise((resolve, failed) => { - let _data = $.isArray(data) ? data : [data]; - let retCode = _data[0]["return_code"] !== undefined ? _data[0].return_code : _this.generateReturnCode(); - _data[0].return_code = retCode; - - let listener = new ReturnListener(); - listener.resolve = resolve; - listener.reject = failed; - listener.code = retCode; - listener.timeout = setTimeout(() => { - _this._retListener.remove(listener); - listener.reject("timeout"); - }, 1500); - this._retListener.push(listener); - - this._socket_connected.send(this.commandiefy({ - "type": "command", - "command": command, - "data": _data, - "flags": options.flagset.filter(entry => entry.length != 0) - })); - }); - - return this._command_handler_default.proxy_command_promise(result, options); - } - - connected() : boolean { - return !!this._socket_connected && this._socket_connected.readyState == WebSocket.OPEN; - } - - support_voice(): boolean { - return this._voice_connection !== undefined; - } - - voice_connection(): connection.voice.AbstractVoiceConnection | undefined { - return this._voice_connection; - } - - command_handler_boss(): connection.AbstractCommandHandlerBoss { - return this._command_boss; - } - - - get onconnectionstatechanged() : connection.ConnectionStateListener { - return this._connection_state_listener; - } - set onconnectionstatechanged(listener: connection.ConnectionStateListener) { - this._connection_state_listener = listener; - } - - handshake_handler(): connection.HandshakeHandler { - return this._handshakeHandler; - } - - remote_address(): ServerAddress { - return this._remote_address; - } - - private do_ping() { - if(this._ping.last_request + this._ping.timeout < Date.now()) { - this._ping.value = this._ping.timeout; - this._ping.last_response = this._ping.last_request + 1; + if(connected_socket == local_proxy_socket) { + log.debug(LogCategory.NETWORKING, tr("Established a TCP connection to %s via proxy to %s"), address.host + ":" + address.port, proxy_host); + this._remote_address.host = proxy_host; + } else { + log.debug(LogCategory.NETWORKING, tr("Established a TCP connection to %s directly"), address.host + ":" + address.port); } - if(this._ping.last_response > this._ping.last_request) { - this._ping.last_request = 'now' in performance ? performance.now() : Date.now(); - this.sendData(JSON.stringify({ - type: 'ping', - payload: (++this._ping.request_id).toString() - })); - } - } - ping(): { native: number; javascript?: number } { - return { - javascript: this._ping.value, - native: this._ping.value_native + this._socket_connected = connected_socket; + this._socket_connected.onclose = event => { + if(this._socket_connected != connected_socket) return; /* this socket isn't from interest anymore */ + + this.client.handleDisconnect(this._connected ? DisconnectReason.CONNECTION_CLOSED : DisconnectReason.CONNECT_FAILURE, { + code: event.code, + reason: event.reason, + event: event + }); }; + + this._socket_connected.onerror = e => { + if(this._socket_connected != connected_socket) return; /* this socket isn't from interest anymore */ + + log.warn(LogCategory.NETWORKING, tr("Received web socket error: (%o)"), e); + }; + + this._socket_connected.onmessage = msg => { + if(this._socket_connected != connected_socket) return; /* this socket isn't from interest anymore */ + + this.handle_socket_message(msg.data); + }; + + this._connected = true; + this.start_handshake(); + } catch (error) { + error_cleanup(); + if(this._socket_connected != connected_socket && this._connect_timeout_timer != local_timeout_timer) + return; /* we're not from interest anymore */ + + log.warn(LogCategory.NETWORKING, tr("Received unexpected error while connecting: %o"), error); + try { + await this.disconnect(); + } catch(error) { + log.warn(LogCategory.NETWORKING, tr("Failed to cleanup connection after unsuccessful connect attempt: %o"), error); + } + this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE, error); } } - export function spawn_server_connection(handle: ConnectionHandler) : AbstractServerConnection { - return new ServerConnection(handle); /* will be overridden by the client */ + private start_handshake() { + this.updateConnectionState(ConnectionState.INITIALISING); + this.client.log.log(elog.Type.CONNECTION_LOGIN, {}); + this._handshakeHandler.initialize(); + this._handshakeHandler.startHandshake(); } - export function destroy_server_connection(handle: AbstractServerConnection) { - if(!(handle instanceof ServerConnection)) - throw "invalid handle"; - handle.destroy(); + updateConnectionState(state: ConnectionState) { + const old_state = this._connectionState; + this._connectionState = state; + if(this._connection_state_listener) + this._connection_state_listener(old_state, state); } + + async disconnect(reason?: string) : Promise { + clearTimeout(this._connect_timeout_timer); + this._connect_timeout_timer = undefined; + + clearTimeout(this._ping.thread_id); + this._ping.thread_id = undefined; + + if(typeof(reason) === "string") { + //TODO send disconnect reason + } + + + if(this._connectionState != ConnectionState.UNCONNECTED) + this.updateConnectionState(ConnectionState.UNCONNECTED); + + if(this._voice_connection) + this._voice_connection.drop_rtp_session(); + + + if(this._socket_connected) { + this._socket_connected.close(3000 + 0xFF, tr("request disconnect")); + this._socket_connected = undefined; + } + + + for(let future of this._retListener) + future.reject(tr("Connection closed")); + this._retListener = []; + + this._connected = false; + this._retCodeIdx = 0; + } + + private handle_socket_message(data) { + if(typeof(data) === "string") { + let json; + try { + json = JSON.parse(data); + } catch(e) { + log.warn(LogCategory.NETWORKING, tr("Could not parse message json!")); + alert(e); // error in the above string (in this case, yes)! + return; + } + if(json["type"] === undefined) { + log.warn(LogCategory.NETWORKING, tr("Missing data type in message!")); + return; + } + if(json["type"] === "command") { + let group = log.group(log.LogType.DEBUG, LogCategory.NETWORKING, tr("Handling command '%s'"), json["command"]); + group.log(tr("Handling command '%s'"), json["command"]); + group.group(log.LogType.TRACE, tr("Json:")).collapsed(true).log("%o", json).end(); + + this._command_boss.invoke_handle({ + command: json["command"], + arguments: json["data"] + }); + + if(json["command"] === "initserver") { + this._ping.thread_id = setInterval(() => this.do_ping(), this._ping.interval) as any; + this.do_ping(); + this.updateConnectionState(ConnectionState.CONNECTED); + if(this._voice_connection) + this._voice_connection.start_rtc_session(); /* FIXME: Move it to a handler boss and not here! */ + } + group.end(); + } else if(json["type"] === "WebRTC") { + if(this._voice_connection) + this._voice_connection.handleControlPacket(json); + else + log.warn(LogCategory.NETWORKING, tr("Dropping WebRTC command packet, because we haven't a bridge.")) + } else if(json["type"] === "ping") { + this.sendData(JSON.stringify({ + type: 'pong', + payload: json["payload"] + })); + } else if(json["type"] === "pong") { + const id = parseInt(json["payload"]); + if(id != this._ping.request_id) { + log.warn(LogCategory.NETWORKING, tr("Received pong which is older than the last request. Delay may over %oms? (Index: %o, Current index: %o)"), this._ping.timeout, id, this._ping.request_id); + } else { + this._ping.last_response = 'now' in performance ? performance.now() : Date.now(); + this._ping.value = this._ping.last_response - this._ping.last_request; + this._ping.value_native = parseInt(json["ping_native"]) / 1000; /* we're getting it in microseconds and not milliseconds */ + log.debug(LogCategory.NETWORKING, tr("Received new pong. Updating ping to: JS: %o Native: %o"), this._ping.value.toFixed(3), this._ping.value_native.toFixed(3)); + } + } else { + log.warn(LogCategory.NETWORKING, tr("Unknown command type %o"), json["type"]); + } + } else { + log.warn(LogCategory.NETWORKING, tr("Received unknown message of type %s. Dropping message"), typeof(data)); + } + } + + sendData(data: any) { + if(!this._socket_connected || this._socket_connected.readyState != 1) { + log.warn(LogCategory.NETWORKING, tr("Tried to send on a invalid socket (%s)"), this._socket_connected ? "invalid state (" + this._socket_connected.readyState + ")" : "invalid socket"); + return; + } + this._socket_connected.send(data); + } + + private commandiefy(input: any) : string { + return JSON.stringify(input, (key, value) => { + switch (typeof value) { + case "boolean": return value == true ? "1" : "0"; + case "function": return value(); + default: + return value; + } + }); + + } + + send_command(command: string, data?: any | any[], _options?: CommandOptions) : Promise { + if(!this._socket_connected || !this.connected()) { + log.warn(LogCategory.NETWORKING, tr("Tried to send a command without a valid connection.")); + return Promise.reject(tr("not connected")); + } + + const options: CommandOptions = {}; + Object.assign(options, CommandOptionDefaults); + Object.assign(options, _options); + + data = $.isArray(data) ? data : [data || {}]; + if(data.length == 0) /* we require min one arg to append return_code */ + data.push({}); + + const _this = this; + let result = new Promise((resolve, failed) => { + let _data = $.isArray(data) ? data : [data]; + let retCode = _data[0]["return_code"] !== undefined ? _data[0].return_code : _this.generateReturnCode(); + _data[0].return_code = retCode; + + let listener = new ReturnListener(); + listener.resolve = resolve; + listener.reject = failed; + listener.code = retCode; + listener.timeout = setTimeout(() => { + _this._retListener.remove(listener); + listener.reject("timeout"); + }, 1500); + this._retListener.push(listener); + + this._socket_connected.send(this.commandiefy({ + "type": "command", + "command": command, + "data": _data, + "flags": options.flagset.filter(entry => entry.length != 0) + })); + }); + + return this._command_handler_default.proxy_command_promise(result, options); + } + + connected() : boolean { + return !!this._socket_connected && this._socket_connected.readyState == WebSocket.OPEN; + } + + support_voice(): boolean { + return this._voice_connection !== undefined; + } + + voice_connection(): AbstractVoiceConnection | undefined { + return this._voice_connection; + } + + command_handler_boss(): AbstractCommandHandlerBoss { + return this._command_boss; + } + + + get onconnectionstatechanged() : ConnectionStateListener { + return this._connection_state_listener; + } + set onconnectionstatechanged(listener: ConnectionStateListener) { + this._connection_state_listener = listener; + } + + handshake_handler(): HandshakeHandler { + return this._handshakeHandler; + } + + remote_address(): ServerAddress { + return this._remote_address; + } + + private do_ping() { + if(this._ping.last_request + this._ping.timeout < Date.now()) { + this._ping.value = this._ping.timeout; + this._ping.last_response = this._ping.last_request + 1; + } + if(this._ping.last_response > this._ping.last_request) { + this._ping.last_request = 'now' in performance ? performance.now() : Date.now(); + this.sendData(JSON.stringify({ + type: 'ping', + payload: (++this._ping.request_id).toString() + })); + } + } + + ping(): { native: number; javascript?: number } { + return { + javascript: this._ping.value, + native: this._ping.value_native + }; + } +} + +export function spawn_server_connection(handle: ConnectionHandler) : AbstractServerConnection { + return new ServerConnection(handle); /* will be overridden by the client */ +} + +export function destroy_server_connection(handle: AbstractServerConnection) { + if(!(handle instanceof ServerConnection)) + throw "invalid handle"; + handle.destroy(); } \ No newline at end of file diff --git a/web/js/dns.ts b/web/js/dns.ts new file mode 100644 index 00000000..d6412d0b --- /dev/null +++ b/web/js/dns.ts @@ -0,0 +1,481 @@ +import {ServerAddress} from "tc-shared/ui/server"; +import {AddressTarget, default_options, ResolveOptions} from "tc-shared/dns"; +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; + +export enum RRType { + A = 1, // a host address,[RFC1035], + NS = 2, // an authoritative name server,[RFC1035], + MD = 3, // a mail destination (OBSOLETE - use MX),[RFC1035], + MF = 4, // a mail forwarder (OBSOLETE - use MX),[RFC1035], + CNAME = 5, // the canonical name for an alias,[RFC1035], + SOA = 6, // marks the start of a zone of authority,[RFC1035], + MB = 7, // a mailbox domain name (EXPERIMENTAL),[RFC1035], + MG = 8, // a mail group member (EXPERIMENTAL),[RFC1035], + MR = 9, // a mail rename domain name (EXPERIMENTAL),[RFC1035], + NULL_ = 10, // a null RR (EXPERIMENTAL),[RFC1035], + WKS = 11, // a well known service description,[RFC1035], + PTR = 12, // a domain name pointer,[RFC1035], + HINFO = 13, // host information,[RFC1035], + MINFO = 14, // mailbox or mail list information,[RFC1035], + MX = 15, // mail exchange,[RFC1035], + TXT = 16, // text strings,[RFC1035], + RP = 17, // for Responsible Person,[RFC1183], + AFSDB = 18, // for AFS Data Base location,[RFC1183][RFC5864], + X25 = 19, // for X.25 PSDN address,[RFC1183], + ISDN = 20, // for ISDN address,[RFC1183], + RT = 21, // for Route Through,[RFC1183], + NSAP = 22, // "for NSAP address, NSAP style A record",[RFC1706], + NSAP_PTR = 23, // "for domain name pointer, NSAP style",[RFC1348][RFC1637][RFC1706], + SIG = 24, // for security signature,[RFC4034][RFC3755][RFC2535][RFC2536][RFC2537][RFC2931][RFC3110][RFC3008], + KEY = 25, // for security key,[RFC4034][RFC3755][RFC2535][RFC2536][RFC2537][RFC2539][RFC3008][RFC3110], + PX = 26, // X.400 mail mapping information,[RFC2163], + GPOS = 27, // Geographical Position,[RFC1712], + AAAA = 28, // IP6 Address,[RFC3596], + LOC = 29, // Location Information,[RFC1876], + NXT = 30, // Next Domain (OBSOLETE),[RFC3755][RFC2535], + EID = 31, // Endpoint Identifier,[Michael_Patton][http://ana-3.lcs.mit.edu/~jnc/nimrod/dns.txt], + NIMLOC = 32, // Nimrod Locator,[1][Michael_Patton][http://ana-3.lcs.mit.edu/~jnc/nimrod/dns.txt], + SRV = 33, // Server Selection,[1][RFC2782], + ATMA = 34, // ATM Address,"[ ATM Forum Technical Committee, ""ATM Name System, V2.0"", Doc ID: AF-DANS-0152.000, July 2000. Available from and held in escrow by IANA.]", + NAPTR = 35, // Naming Authority Pointer,[RFC2915][RFC2168][RFC3403], + KX = 36, // Key Exchanger,[RFC2230], + CERT = 37, //CERT, // [RFC4398], + A6 = 38, // A6 (OBSOLETE - use AAAA),[RFC3226][RFC2874][RFC6563], + DNAME = 39, //DNAME, // [RFC6672], + SINK = 40, //SINK, // [Donald_E_Eastlake][http://tools.ietf.org/html/draft-eastlake-kitchen-sink], + OPT = 41, //OPT, // [RFC6891][RFC3225], + APL = 42, //APL, // [RFC3123], + DS = 43, // Delegation Signer,[RFC4034][RFC3658], + SSHFP = 44, // SSH Key Fingerprint,[RFC4255], + IPSECKEY = 45, //IPSECKEY, // [RFC4025], + RRSIG = 46, //RRSIG, // [RFC4034][RFC3755], + NSEC = 47, //NSEC, // [RFC4034][RFC3755], + DNSKEY = 48, //DNSKEY, // [RFC4034][RFC3755], + DHCID = 49, //DHCID, // [RFC4701], + NSEC3 = 50, //NSEC3, // [RFC5155], + NSEC3PARAM = 51, //NSEC3PARAM, // [RFC5155], + TLSA = 52, //TLSA, // [RFC6698], + SMIMEA = 53, // S/MIME cert association,[RFC8162],SMIMEA/smimea-completed-template + Unassigned = 54, // , + HIP = 55, // Host Identity Protocol,[RFC8005], + NINFO = 56, //NINFO [Jim_Reid], // NINFO/ninfo-completed-template + RKEY = 57, //RKEY [Jim_Reid], // RKEY/rkey-completed-template + TALINK = 58, // Trust Anchor LINK,[Wouter_Wijngaards],TALINK/talink-completed-template + CDS = 59, // Child DS,[RFC7344],CDS/cds-completed-template + CDNSKEY = 60, // DNSKEY(s) the Child wants reflected in DS,[RFC7344], + OPENPGPKEY = 61, // OpenPGP Key,[RFC7929],OPENPGPKEY/openpgpkey-completed-template + CSYNC = 62, // Child-To-Parent Synchronization,[RFC7477], + ZONEMD = 63, // message digest for DNS zone,[draft-wessels-dns-zone-digest],ZONEMD/zonemd-completed-template + //Unassigned = 64-98, + SPF = 99, // [RFC7208], + UINFO = 100, // [IANA-Reserved], + UID = 101, // [IANA-Reserved], + GID = 102, // [IANA-Reserved], + UNSPEC = 103, // [IANA-Reserved], + NID = 104, //[RFC6742], // ILNP/nid-completed-template + L32 = 105, //[RFC6742], // ILNP/l32-completed-template + L64 = 106, //[RFC6742], // ILNP/l64-completed-template + LP = 107, //[RFC6742], // ILNP/lp-completed-template + EUI48 = 108, // an EUI-48 address,[RFC7043],EUI48/eui48-completed-template + EUI64 = 109, // an EUI-64 address,[RFC7043],EUI64/eui64-completed-template + //Unassigned = 110-248, // , + TKEY = 249, // Transaction Key,[RFC2930], + TSIG = 250, // Transaction Signature,[RFC2845], + IXFR = 251, // incremental transfer,[RFC1995], + AXFR = 252, // transfer of an entire zone,[RFC1035][RFC5936], + MAILB = 253, // "mailbox-related RRs (MB, MG or MR)",[RFC1035], + MAILA = 254, // mail agent RRs (OBSOLETE - see MX),[RFC1035], + ANY = 255, // A request for some or all records the server has available,[RFC1035][RFC6895][RFC8482], + URI = 256, //URI [RFC7553], // URI/uri-completed-template + CAA = 257, // Certification Authority Restriction,[RFC-ietf-lamps-rfc6844bis-07],CAA/caa-completed-template + AVC = 258, // Application Visibility and Control,[Wolfgang_Riedel],AVC/avc-completed-template + DOA = 259, // Digital Object Architecture,[draft-durand-doa-over-dns],DOA/doa-completed-template + AMTRELAY = 260, // Automatic Multicast Tunneling Relay,[draft-ietf-mboned-driad-amt-discovery],AMTRELAY/amtrelay-completed-template + //Unassigned = 261-32767, + TA = 32768, // DNSSEC Trust Authorities,"[Sam_Weiler][http://cameo.library.cmu.edu/][ Deploying DNSSEC Without a Signed Root. Technical Report 1999-19, + // Information Networking Institute, Carnegie Mellon University, April 2004.]", + DLV = 32769, // DNSSEC Lookaside Validation,[RFC4431], + //Unassigned = 32770-65279,, // , + //Private use,65280-65534,,,, + Reserved = 65535, +} +export enum ErrorCode { + NOERROR = 0, + FORMERR = 1, + SERVFAIL = 2, + NXDOMAIN = 3, + NOTIMP = 4, + REFUSED = 5, + YXDOMAIN = 6, + XRRSET = 7, + NOTAUTH = 8, + NOTZONE = 9 +} + +interface DNSAnswer { + name: string; + type: RRType; + TTL: null; + data: string; +} + +interface DNSQuery { + name: string; + type: RRType; +} + +interface DNSResponse { + Status: ErrorCode; + Comment: string; + + TC: boolean; /* truncated */ + RD: true; + RA: true; + AD: boolean; /* DNSSEC valid */ + CD: boolean; /* client DNSSEC disabled */ + + Question: DNSQuery[]; + Answer?: DNSAnswer[]; + Authority?: DNSAnswer[]; + Additional: any[]; +} + +export async function resolve(address: string, type: RRType) : Promise { + const parameters = {}; + parameters["name"] = address; + parameters["type"] = type; + parameters["cd"] = false; /* check disabled */ + parameters["do"] = true; /* DNSSEC info */ + + const parameter_string = Object.keys(parameters).reduceRight((a, b) => a + "&" + b + "=" + encodeURIComponent(parameters[b])); + const response = await fetch("https://dns.google/resolve?" + parameter_string, { + method: "GET" + }); + if(response.status !== 200) + throw response.statusText || tr("server returned ") + response.status; + + let response_string = "unknown"; + let response_data: DNSResponse; + try { + response_string = await response.text(); + response_data = JSON.parse(response_string); + } catch(ex) { + log.error(LogCategory.DNS, tr("Failed to parse response data: %o. Data: %s"), ex, response_string); + throw "failed to parse response"; + } + + if(response_data.TC) + throw "truncated response"; + + if(response_data.Status !== ErrorCode.NOERROR) { + if(response_data.Status === ErrorCode.NXDOMAIN) + return []; + throw "dns error code " + response_data.Status; + } + + log.trace(LogCategory.DNS, tr("Result for query %s (%s): %o"), address, RRType[type], response_data); + + if(!response_data.Answer) return []; + return response_data.Answer.filter(e => (e.name === address || e.name === address + ".") && e.type === type); +} + +type Address = { host: string, port: number }; + +interface DNSResolveMethod { + name() : string; + resolve(address: Address) : Promise
; +} + +class IPResolveMethod implements DNSResolveMethod { + readonly v6: boolean; + + constructor(v6: boolean) { + this.v6 = v6; + } + + + name(): string { + return "ip v" + (this.v6 ? "6" : "4") + " resolver"; + } + + resolve(address: Address): Promise
{ + return resolve(address.host, this.v6 ? RRType.AAAA : RRType.A).then(e => { + if(!e.length) return undefined; + + return { + host: e[0].data, + port: address.port + } + }); + } +} + +type ParsedSVRRecord = { + target: string; + port: number; + + priority: number; + weight: number; +} +class SRVResolveMethod implements DNSResolveMethod { + readonly application: string; + + constructor(app: string) { + this.application = app; + } + + name(): string { + return "srv resolve [" + this.application + "]"; + } + + resolve(address: Address): Promise
{ + return resolve((this.application ? this.application + "." : "") + address.host, RRType.SRV).then(e => { + if(!e) return undefined; + + const records: {[key: number]:ParsedSVRRecord[]} = {}; + for(const record of e) { + const parts = record.data.split(" "); + if(parts.length !== 4) { + log.warn(LogCategory.DNS, tr("Failed to parse SRV record %s. Invalid split length."), record); + continue; + } + + const priority = parseInt(parts[0]); + const weight = parseInt(parts[1]); + const port = parseInt(parts[2]); + + if((priority < 0 || priority > 65535) || (weight < 0 || weight > 65535) || (port < 0 || port > 65535)) { + log.warn(LogCategory.DNS, tr("Failed to parse SRV record %s. Malformed data."), record); + continue; + } + + (records[priority] || (records[priority] = [])).push({ + priority: priority, + weight: weight, + port: port, + target: parts[3] + }); + } + + /* get the record with the highest priority */ + const priority_strings = Object.keys(records); + if(!priority_strings.length) return undefined; + + let highest_priority: ParsedSVRRecord[]; + for(const priority_str of priority_strings) { + if(!highest_priority || !highest_priority.length) + highest_priority = records[priority_str]; + + if(highest_priority[0].priority < parseInt(priority_str)) + highest_priority = records[priority_str]; + } + + if(!highest_priority.length) return undefined; + + /* select randomly one record */ + let record: ParsedSVRRecord; + const max_weight = highest_priority.map(e => e.weight).reduce((a, b) => a + b, 0); + if(max_weight == 0) record = highest_priority[Math.floor(Math.random() * highest_priority.length)]; + else { + let rnd = Math.random() * max_weight; + for(let i = 0; i < highest_priority.length; i++) { + rnd -= highest_priority[i].weight; + if(rnd > 0) continue; + + record = highest_priority[i]; + break; + } + } + if(!record) /* shall never happen */ + record = highest_priority[0]; + return { + host: record.target, + port: record.port == 0 ? address.port : record.port + }; + }); + } +} + +class SRV_IPResolveMethod implements DNSResolveMethod { + readonly srv_resolver: DNSResolveMethod; + readonly ipv4_resolver: IPResolveMethod; + readonly ipv6_resolver: IPResolveMethod; + + constructor(srv_resolver: DNSResolveMethod, ipv4_resolver: IPResolveMethod, ipv6_resolver: IPResolveMethod) { + this.srv_resolver = srv_resolver; + this.ipv4_resolver = ipv4_resolver; + this.ipv6_resolver = ipv6_resolver; + } + + name(): string { + return "srv ip resolver [" + this.srv_resolver.name() + "; " + this.ipv4_resolver.name() + "; " + this.ipv6_resolver.name() + "]"; + } + + resolve(address: Address): Promise
{ + return this.srv_resolver.resolve(address).then(e => { + if(!e) return undefined; + + return this.ipv4_resolver.resolve(e).catch(() => this.ipv6_resolver.resolve(e)); + }); + } +} + +class DomainRootResolveMethod implements DNSResolveMethod { + readonly resolver: DNSResolveMethod; + + constructor(resolver: DNSResolveMethod) { + this.resolver = resolver; + } + + name(): string { + return "domain-root [" + this.resolver.name() + "]"; + } + + resolve(address: Address): Promise
{ + const parts = address.host.split("."); + if(parts.length < 3) return undefined; + + return this.resolver.resolve({ + host: parts.slice(-2).join("."), + port: address.port + }); + } +} + +class TeaSpeakDNSResolve { + readonly address: Address; + private resolvers: {[key: string]:{resolver: DNSResolveMethod, after: string[]}} = {}; + private resolving = false; + private timeout; + + private callback_success; + private callback_fail; + + private finished_resolvers: string[]; + private resolving_resolvers: string[]; + + constructor(addr: Address) { + this.address = addr; + } + + register_resolver(resolver: DNSResolveMethod, ...after: (string | DNSResolveMethod)[]) { + if(this.resolving) throw tr("resolver is already resolving"); + + this.resolvers[resolver.name()] = { resolver: resolver, after: after.map(e => typeof e === "string" ? e : e.name()) }; + } + + resolve(timeout: number) : Promise
{ + if(this.resolving) throw tr("already resolving"); + this.resolving = true; + + this.finished_resolvers = []; + this.resolving_resolvers = []; + + const cleanup = () => { + clearTimeout(this.timeout); + this.resolving = false; + }; + + this.timeout = setTimeout(() => { + this.callback_fail(tr("timeout")); + }, timeout); + log.trace(LogCategory.DNS, tr("Start resolving %s:%d"), this.address.host, this.address.port); + + return new Promise
((resolve, reject) => { + this.callback_success = data => { + cleanup(); + resolve(data); + }; + + this.callback_fail = error => { + cleanup(); + reject(error); + }; + + this.invoke_resolvers(); + }); + } + + private invoke_resolvers() { + let invoke_count = 0; + + _main_loop: + for(const resolver_name of Object.keys(this.resolvers)) { + if(this.resolving_resolvers.findIndex(e => e === resolver_name) !== -1) continue; + if(this.finished_resolvers.findIndex(e => e === resolver_name) !== -1) continue; + + const resolver = this.resolvers[resolver_name]; + for(const after of resolver.after) + if(this.finished_resolvers.findIndex(e => e === after) === -1) continue _main_loop; + + invoke_count++; + log.trace(LogCategory.DNS, tr(" Executing resolver %s"), resolver_name); + + this.resolving_resolvers.push(resolver_name); + resolver.resolver.resolve(this.address).then(result => { + if(!this.resolving || !this.callback_success) return; /* resolve has been finished already */ + this.finished_resolvers.push(resolver_name); + + if(!result) { + log.trace(LogCategory.DNS, tr(" Resolver %s returned an empty response."), resolver_name); + this.invoke_resolvers(); + return; + } + + log.trace(LogCategory.DNS, tr(" Successfully resolved address %s:%d to %s:%d via resolver %s"), + this.address.host, this.address.port, + result.host, result.port, + resolver_name); + this.callback_success(result); + }).catch(error => { + if(!this.resolving || !this.callback_success) return; /* resolve has been finished already */ + this.finished_resolvers.push(resolver_name); + + log.trace(LogCategory.DNS, tr(" Resolver %s ran into an error: %o"), resolver_name, error); + this.invoke_resolvers(); + }).then(() => { + this.resolving_resolvers.remove(resolver_name); + if(!this.resolving_resolvers.length && this.resolving) + this.invoke_resolvers(); + }); + } + + if(invoke_count === 0 && !this.resolving_resolvers.length && this.resolving) + this.callback_fail("no response"); + } +} + +const resolver_ip_v4 = new IPResolveMethod(false); +const resolver_ip_v6 = new IPResolveMethod(true); + +const resolver_srv_ts = new SRV_IPResolveMethod(new SRVResolveMethod("_ts._udp"), resolver_ip_v4, resolver_ip_v6); +const resolver_srv_ts3 = new SRV_IPResolveMethod(new SRVResolveMethod("_ts3._udp"), resolver_ip_v4, resolver_ip_v6); + +const resolver_dr_srv_ts = new DomainRootResolveMethod(resolver_srv_ts); +const resolver_dr_srv_ts3 = new DomainRootResolveMethod(resolver_srv_ts3); + +export function supported() { return true; } + +export async function resolve_address(address: ServerAddress, _options?: ResolveOptions) : Promise { + const options = Object.assign({}, default_options); + Object.assign(options, _options); + + const resolver = new TeaSpeakDNSResolve(address); + + resolver.register_resolver(resolver_srv_ts); + resolver.register_resolver(resolver_srv_ts3); + //TODO: TSDNS somehow? + + resolver.register_resolver(resolver_dr_srv_ts, resolver_srv_ts); + resolver.register_resolver(resolver_dr_srv_ts3, resolver_srv_ts3); + + resolver.register_resolver(resolver_ip_v4, resolver_srv_ts, resolver_srv_ts3); + resolver.register_resolver(resolver_ip_v6, resolver_ip_v4); + + const response = await resolver.resolve(options.timeout || 5000); + return { + target_ip: response.host, + target_port: response.port + }; +} \ No newline at end of file diff --git a/web/js/dns_impl.ts b/web/js/dns_impl.ts deleted file mode 100644 index b2460161..00000000 --- a/web/js/dns_impl.ts +++ /dev/null @@ -1,478 +0,0 @@ -namespace dns { - export enum RRType { - A = 1, // a host address,[RFC1035], - NS = 2, // an authoritative name server,[RFC1035], - MD = 3, // a mail destination (OBSOLETE - use MX),[RFC1035], - MF = 4, // a mail forwarder (OBSOLETE - use MX),[RFC1035], - CNAME = 5, // the canonical name for an alias,[RFC1035], - SOA = 6, // marks the start of a zone of authority,[RFC1035], - MB = 7, // a mailbox domain name (EXPERIMENTAL),[RFC1035], - MG = 8, // a mail group member (EXPERIMENTAL),[RFC1035], - MR = 9, // a mail rename domain name (EXPERIMENTAL),[RFC1035], - NULL_ = 10, // a null RR (EXPERIMENTAL),[RFC1035], - WKS = 11, // a well known service description,[RFC1035], - PTR = 12, // a domain name pointer,[RFC1035], - HINFO = 13, // host information,[RFC1035], - MINFO = 14, // mailbox or mail list information,[RFC1035], - MX = 15, // mail exchange,[RFC1035], - TXT = 16, // text strings,[RFC1035], - RP = 17, // for Responsible Person,[RFC1183], - AFSDB = 18, // for AFS Data Base location,[RFC1183][RFC5864], - X25 = 19, // for X.25 PSDN address,[RFC1183], - ISDN = 20, // for ISDN address,[RFC1183], - RT = 21, // for Route Through,[RFC1183], - NSAP = 22, // "for NSAP address, NSAP style A record",[RFC1706], - NSAP_PTR = 23, // "for domain name pointer, NSAP style",[RFC1348][RFC1637][RFC1706], - SIG = 24, // for security signature,[RFC4034][RFC3755][RFC2535][RFC2536][RFC2537][RFC2931][RFC3110][RFC3008], - KEY = 25, // for security key,[RFC4034][RFC3755][RFC2535][RFC2536][RFC2537][RFC2539][RFC3008][RFC3110], - PX = 26, // X.400 mail mapping information,[RFC2163], - GPOS = 27, // Geographical Position,[RFC1712], - AAAA = 28, // IP6 Address,[RFC3596], - LOC = 29, // Location Information,[RFC1876], - NXT = 30, // Next Domain (OBSOLETE),[RFC3755][RFC2535], - EID = 31, // Endpoint Identifier,[Michael_Patton][http://ana-3.lcs.mit.edu/~jnc/nimrod/dns.txt], - NIMLOC = 32, // Nimrod Locator,[1][Michael_Patton][http://ana-3.lcs.mit.edu/~jnc/nimrod/dns.txt], - SRV = 33, // Server Selection,[1][RFC2782], - ATMA = 34, // ATM Address,"[ ATM Forum Technical Committee, ""ATM Name System, V2.0"", Doc ID: AF-DANS-0152.000, July 2000. Available from and held in escrow by IANA.]", - NAPTR = 35, // Naming Authority Pointer,[RFC2915][RFC2168][RFC3403], - KX = 36, // Key Exchanger,[RFC2230], - CERT = 37, //CERT, // [RFC4398], - A6 = 38, // A6 (OBSOLETE - use AAAA),[RFC3226][RFC2874][RFC6563], - DNAME = 39, //DNAME, // [RFC6672], - SINK = 40, //SINK, // [Donald_E_Eastlake][http://tools.ietf.org/html/draft-eastlake-kitchen-sink], - OPT = 41, //OPT, // [RFC6891][RFC3225], - APL = 42, //APL, // [RFC3123], - DS = 43, // Delegation Signer,[RFC4034][RFC3658], - SSHFP = 44, // SSH Key Fingerprint,[RFC4255], - IPSECKEY = 45, //IPSECKEY, // [RFC4025], - RRSIG = 46, //RRSIG, // [RFC4034][RFC3755], - NSEC = 47, //NSEC, // [RFC4034][RFC3755], - DNSKEY = 48, //DNSKEY, // [RFC4034][RFC3755], - DHCID = 49, //DHCID, // [RFC4701], - NSEC3 = 50, //NSEC3, // [RFC5155], - NSEC3PARAM = 51, //NSEC3PARAM, // [RFC5155], - TLSA = 52, //TLSA, // [RFC6698], - SMIMEA = 53, // S/MIME cert association,[RFC8162],SMIMEA/smimea-completed-template - Unassigned = 54, // , - HIP = 55, // Host Identity Protocol,[RFC8005], - NINFO = 56, //NINFO [Jim_Reid], // NINFO/ninfo-completed-template - RKEY = 57, //RKEY [Jim_Reid], // RKEY/rkey-completed-template - TALINK = 58, // Trust Anchor LINK,[Wouter_Wijngaards],TALINK/talink-completed-template - CDS = 59, // Child DS,[RFC7344],CDS/cds-completed-template - CDNSKEY = 60, // DNSKEY(s) the Child wants reflected in DS,[RFC7344], - OPENPGPKEY = 61, // OpenPGP Key,[RFC7929],OPENPGPKEY/openpgpkey-completed-template - CSYNC = 62, // Child-To-Parent Synchronization,[RFC7477], - ZONEMD = 63, // message digest for DNS zone,[draft-wessels-dns-zone-digest],ZONEMD/zonemd-completed-template - //Unassigned = 64-98, - SPF = 99, // [RFC7208], - UINFO = 100, // [IANA-Reserved], - UID = 101, // [IANA-Reserved], - GID = 102, // [IANA-Reserved], - UNSPEC = 103, // [IANA-Reserved], - NID = 104, //[RFC6742], // ILNP/nid-completed-template - L32 = 105, //[RFC6742], // ILNP/l32-completed-template - L64 = 106, //[RFC6742], // ILNP/l64-completed-template - LP = 107, //[RFC6742], // ILNP/lp-completed-template - EUI48 = 108, // an EUI-48 address,[RFC7043],EUI48/eui48-completed-template - EUI64 = 109, // an EUI-64 address,[RFC7043],EUI64/eui64-completed-template - //Unassigned = 110-248, // , - TKEY = 249, // Transaction Key,[RFC2930], - TSIG = 250, // Transaction Signature,[RFC2845], - IXFR = 251, // incremental transfer,[RFC1995], - AXFR = 252, // transfer of an entire zone,[RFC1035][RFC5936], - MAILB = 253, // "mailbox-related RRs (MB, MG or MR)",[RFC1035], - MAILA = 254, // mail agent RRs (OBSOLETE - see MX),[RFC1035], - ANY = 255, // A request for some or all records the server has available,[RFC1035][RFC6895][RFC8482], - URI = 256, //URI [RFC7553], // URI/uri-completed-template - CAA = 257, // Certification Authority Restriction,[RFC-ietf-lamps-rfc6844bis-07],CAA/caa-completed-template - AVC = 258, // Application Visibility and Control,[Wolfgang_Riedel],AVC/avc-completed-template - DOA = 259, // Digital Object Architecture,[draft-durand-doa-over-dns],DOA/doa-completed-template - AMTRELAY = 260, // Automatic Multicast Tunneling Relay,[draft-ietf-mboned-driad-amt-discovery],AMTRELAY/amtrelay-completed-template - //Unassigned = 261-32767, - TA = 32768, // DNSSEC Trust Authorities,"[Sam_Weiler][http://cameo.library.cmu.edu/][ Deploying DNSSEC Without a Signed Root. Technical Report 1999-19, - // Information Networking Institute, Carnegie Mellon University, April 2004.]", - DLV = 32769, // DNSSEC Lookaside Validation,[RFC4431], - //Unassigned = 32770-65279,, // , - //Private use,65280-65534,,,, - Reserved = 65535, - } - export enum ErrorCode { - NOERROR = 0, - FORMERR = 1, - SERVFAIL = 2, - NXDOMAIN = 3, - NOTIMP = 4, - REFUSED = 5, - YXDOMAIN = 6, - XRRSET = 7, - NOTAUTH = 8, - NOTZONE = 9 - } - - interface DNSAnswer { - name: string; - type: RRType; - TTL: null; - data: string; - } - - interface DNSQuery { - name: string; - type: RRType; - } - - interface DNSResponse { - Status: ErrorCode; - Comment: string; - - TC: boolean; /* truncated */ - RD: true; - RA: true; - AD: boolean; /* DNSSEC valid */ - CD: boolean; /* client DNSSEC disabled */ - - Question: DNSQuery[]; - Answer?: DNSAnswer[]; - Authority?: DNSAnswer[]; - Additional: any[]; - } - - export async function resolve(address: string, type: RRType) : Promise { - const parameters = {}; - parameters["name"] = address; - parameters["type"] = type; - parameters["cd"] = false; /* check disabled */ - parameters["do"] = true; /* DNSSEC info */ - - const parameter_string = Object.keys(parameters).reduceRight((a, b) => a + "&" + b + "=" + encodeURIComponent(parameters[b])); - const response = await fetch("https://dns.google/resolve?" + parameter_string, { - method: "GET" - }); - if(response.status !== 200) - throw response.statusText || tr("server returned ") + response.status; - - let response_string = "unknown"; - let response_data: DNSResponse; - try { - response_string = await response.text(); - response_data = JSON.parse(response_string); - } catch(ex) { - log.error(LogCategory.DNS, tr("Failed to parse response data: %o. Data: %s"), ex, response_string); - throw "failed to parse response"; - } - - if(response_data.TC) - throw "truncated response"; - - if(response_data.Status !== ErrorCode.NOERROR) { - if(response_data.Status === ErrorCode.NXDOMAIN) - return []; - throw "dns error code " + response_data.Status; - } - - log.trace(LogCategory.DNS, tr("Result for query %s (%s): %o"), address, RRType[type], response_data); - - if(!response_data.Answer) return []; - return response_data.Answer.filter(e => (e.name === address || e.name === address + ".") && e.type === type); - } - - type Address = { host: string, port: number }; - - interface DNSResolveMethod { - name() : string; - resolve(address: Address) : Promise
; - } - - class IPResolveMethod implements DNSResolveMethod { - readonly v6: boolean; - - constructor(v6: boolean) { - this.v6 = v6; - } - - - name(): string { - return "ip v" + (this.v6 ? "6" : "4") + " resolver"; - } - - resolve(address: Address): Promise
{ - return resolve(address.host, this.v6 ? RRType.AAAA : RRType.A).then(e => { - if(!e.length) return undefined; - - return { - host: e[0].data, - port: address.port - } - }); - } - } - - type ParsedSVRRecord = { - target: string; - port: number; - - priority: number; - weight: number; - } - class SRVResolveMethod implements DNSResolveMethod { - readonly application: string; - - constructor(app: string) { - this.application = app; - } - - name(): string { - return "srv resolve [" + this.application + "]"; - } - - resolve(address: Address): Promise
{ - return resolve((this.application ? this.application + "." : "") + address.host, RRType.SRV).then(e => { - if(!e) return undefined; - - const records: {[key: number]:ParsedSVRRecord[]} = {}; - for(const record of e) { - const parts = record.data.split(" "); - if(parts.length !== 4) { - log.warn(LogCategory.DNS, tr("Failed to parse SRV record %s. Invalid split length."), record); - continue; - } - - const priority = parseInt(parts[0]); - const weight = parseInt(parts[1]); - const port = parseInt(parts[2]); - - if((priority < 0 || priority > 65535) || (weight < 0 || weight > 65535) || (port < 0 || port > 65535)) { - log.warn(LogCategory.DNS, tr("Failed to parse SRV record %s. Malformed data."), record); - continue; - } - - (records[priority] || (records[priority] = [])).push({ - priority: priority, - weight: weight, - port: port, - target: parts[3] - }); - } - - /* get the record with the highest priority */ - const priority_strings = Object.keys(records); - if(!priority_strings.length) return undefined; - - let highest_priority: ParsedSVRRecord[]; - for(const priority_str of priority_strings) { - if(!highest_priority || !highest_priority.length) - highest_priority = records[priority_str]; - - if(highest_priority[0].priority < parseInt(priority_str)) - highest_priority = records[priority_str]; - } - - if(!highest_priority.length) return undefined; - - /* select randomly one record */ - let record: ParsedSVRRecord; - const max_weight = highest_priority.map(e => e.weight).reduce((a, b) => a + b, 0); - if(max_weight == 0) record = highest_priority[Math.floor(Math.random() * highest_priority.length)]; - else { - let rnd = Math.random() * max_weight; - for(let i = 0; i < highest_priority.length; i++) { - rnd -= highest_priority[i].weight; - if(rnd > 0) continue; - - record = highest_priority[i]; - break; - } - } - if(!record) /* shall never happen */ - record = highest_priority[0]; - return { - host: record.target, - port: record.port == 0 ? address.port : record.port - }; - }); - } - } - - class SRV_IPResolveMethod implements DNSResolveMethod { - readonly srv_resolver: DNSResolveMethod; - readonly ipv4_resolver: IPResolveMethod; - readonly ipv6_resolver: IPResolveMethod; - - constructor(srv_resolver: DNSResolveMethod, ipv4_resolver: IPResolveMethod, ipv6_resolver: IPResolveMethod) { - this.srv_resolver = srv_resolver; - this.ipv4_resolver = ipv4_resolver; - this.ipv6_resolver = ipv6_resolver; - } - - name(): string { - return "srv ip resolver [" + this.srv_resolver.name() + "; " + this.ipv4_resolver.name() + "; " + this.ipv6_resolver.name() + "]"; - } - - resolve(address: Address): Promise
{ - return this.srv_resolver.resolve(address).then(e => { - if(!e) return undefined; - - return this.ipv4_resolver.resolve(e).catch(() => this.ipv6_resolver.resolve(e)); - }); - } - } - - class DomainRootResolveMethod implements DNSResolveMethod { - readonly resolver: DNSResolveMethod; - - constructor(resolver: DNSResolveMethod) { - this.resolver = resolver; - } - - name(): string { - return "domain-root [" + this.resolver.name() + "]"; - } - - resolve(address: Address): Promise
{ - const parts = address.host.split("."); - if(parts.length < 3) return undefined; - - return this.resolver.resolve({ - host: parts.slice(-2).join("."), - port: address.port - }); - } - } - - class TeaSpeakDNSResolve { - readonly address: Address; - private resolvers: {[key: string]:{resolver: DNSResolveMethod, after: string[]}} = {}; - private resolving = false; - private timeout; - - private callback_success; - private callback_fail; - - private finished_resolvers: string[]; - private resolving_resolvers: string[]; - - constructor(addr: Address) { - this.address = addr; - } - - register_resolver(resolver: DNSResolveMethod, ...after: (string | DNSResolveMethod)[]) { - if(this.resolving) throw tr("resolver is already resolving"); - - this.resolvers[resolver.name()] = { resolver: resolver, after: after.map(e => typeof e === "string" ? e : e.name()) }; - } - - resolve(timeout: number) : Promise
{ - if(this.resolving) throw tr("already resolving"); - this.resolving = true; - - this.finished_resolvers = []; - this.resolving_resolvers = []; - - const cleanup = () => { - clearTimeout(this.timeout); - this.resolving = false; - }; - - this.timeout = setTimeout(() => { - this.callback_fail(tr("timeout")); - }, timeout); - log.trace(LogCategory.DNS, tr("Start resolving %s:%d"), this.address.host, this.address.port); - - return new Promise
((resolve, reject) => { - this.callback_success = data => { - cleanup(); - resolve(data); - }; - - this.callback_fail = error => { - cleanup(); - reject(error); - }; - - this.invoke_resolvers(); - }); - } - - private invoke_resolvers() { - let invoke_count = 0; - - _main_loop: - for(const resolver_name of Object.keys(this.resolvers)) { - if(this.resolving_resolvers.findIndex(e => e === resolver_name) !== -1) continue; - if(this.finished_resolvers.findIndex(e => e === resolver_name) !== -1) continue; - - const resolver = this.resolvers[resolver_name]; - for(const after of resolver.after) - if(this.finished_resolvers.findIndex(e => e === after) === -1) continue _main_loop; - - invoke_count++; - log.trace(LogCategory.DNS, tr(" Executing resolver %s"), resolver_name); - - this.resolving_resolvers.push(resolver_name); - resolver.resolver.resolve(this.address).then(result => { - if(!this.resolving || !this.callback_success) return; /* resolve has been finished already */ - this.finished_resolvers.push(resolver_name); - - if(!result) { - log.trace(LogCategory.DNS, tr(" Resolver %s returned an empty response."), resolver_name); - this.invoke_resolvers(); - return; - } - - log.trace(LogCategory.DNS, tr(" Successfully resolved address %s:%d to %s:%d via resolver %s"), - this.address.host, this.address.port, - result.host, result.port, - resolver_name); - this.callback_success(result); - }).catch(error => { - if(!this.resolving || !this.callback_success) return; /* resolve has been finished already */ - this.finished_resolvers.push(resolver_name); - - log.trace(LogCategory.DNS, tr(" Resolver %s ran into an error: %o"), resolver_name, error); - this.invoke_resolvers(); - }).then(() => { - this.resolving_resolvers.remove(resolver_name); - if(!this.resolving_resolvers.length && this.resolving) - this.invoke_resolvers(); - }); - } - - if(invoke_count === 0 && !this.resolving_resolvers.length && this.resolving) - this.callback_fail("no response"); - } - } - - const resolver_ip_v4 = new IPResolveMethod(false); - const resolver_ip_v6 = new IPResolveMethod(true); - - const resolver_srv_ts = new SRV_IPResolveMethod(new SRVResolveMethod("_ts._udp"), resolver_ip_v4, resolver_ip_v6); - const resolver_srv_ts3 = new SRV_IPResolveMethod(new SRVResolveMethod("_ts3._udp"), resolver_ip_v4, resolver_ip_v6); - - const resolver_dr_srv_ts = new DomainRootResolveMethod(resolver_srv_ts); - const resolver_dr_srv_ts3 = new DomainRootResolveMethod(resolver_srv_ts3); - - export function supported() { return true; } - - export async function resolve_address(address: ServerAddress, _options?: ResolveOptions) : Promise { - const options = Object.assign({}, default_options); - Object.assign(options, _options); - - const resolver = new TeaSpeakDNSResolve(address); - - resolver.register_resolver(resolver_srv_ts); - resolver.register_resolver(resolver_srv_ts3); - //TODO: TSDNS somehow? - - resolver.register_resolver(resolver_dr_srv_ts, resolver_srv_ts); - resolver.register_resolver(resolver_dr_srv_ts3, resolver_srv_ts3); - - resolver.register_resolver(resolver_ip_v4, resolver_srv_ts, resolver_srv_ts3); - resolver.register_resolver(resolver_ip_v6, resolver_ip_v4); - - const response = await resolver.resolve(options.timeout || 5000); - return { - target_ip: response.host, - target_port: response.port - }; - } -} \ No newline at end of file diff --git a/web/js/index.ts b/web/js/index.ts index e69de29b..fa00a902 100644 --- a/web/js/index.ts +++ b/web/js/index.ts @@ -0,0 +1 @@ +const tc = require("tc-shared/main"); \ No newline at end of file diff --git a/web/js/ppt.ts b/web/js/ppt.ts new file mode 100644 index 00000000..6dd864e0 --- /dev/null +++ b/web/js/ppt.ts @@ -0,0 +1,151 @@ +import {EventType, KeyEvent, KeyHook, SpecialKey} from "tc-shared/PPTListener"; +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; + +interface WebKeyEvent extends KeyEvent { + canceled: boolean; +} + +let key_listener: ((_: KeyEvent) => any)[] = []; + +function listener_key(type: EventType, event: KeyboardEvent) { + const key_event = { + type: type, + + key: event.key, + key_code: event.code, + + key_ctrl: event.ctrlKey, + key_shift: event.shiftKey, + key_alt: event.altKey, + key_windows: event.metaKey, + + canceled: event.defaultPrevented + } as WebKeyEvent; + //console.debug("Trigger key event %o", key_event); + + for(const listener of key_listener) + listener(key_event); + + if(key_event.canceled) + event.preventDefault(); +} + +const proxy_key_press = event => listener_key(EventType.KEY_PRESS, event); +const proxy_key_release = event => listener_key(EventType.KEY_RELEASE, event); +const proxy_key_typed = event => listener_key(EventType.KEY_TYPED, event); + +export function initialize() : Promise { + document.addEventListener('keypress', proxy_key_typed); + document.addEventListener('keydown', proxy_key_press); + document.addEventListener('keyup', proxy_key_release); + window.addEventListener('blur', listener_blur); + + register_key_listener(listener_hook); + return Promise.resolve(); +} + +export function finalize() { + document.removeEventListener("keypress", proxy_key_typed); + document.removeEventListener("keydown", proxy_key_press); + document.removeEventListener("keyup", proxy_key_release); + window.removeEventListener('blur', listener_blur); + + unregister_key_listener(listener_hook); +} + +export function register_key_listener(listener: (_: KeyEvent) => any) { + key_listener.push(listener); +} + +export function unregister_key_listener(listener: (_: KeyEvent) => any) { + key_listener.remove(listener); +} + + +let key_hooks: KeyHook[] = []; + +interface CurrentState { + keys: {[code: string]:KeyEvent}; + special: { [key:number]:boolean }; +} +let current_state: CurrentState = { + special: [] +} as any; + +let key_hooks_active: KeyHook[] = []; + +function listener_blur() { + current_state.special[SpecialKey.ALT] = false; + current_state.special[SpecialKey.CTRL] = false; + current_state.special[SpecialKey.SHIFT] = false; + current_state.special[SpecialKey.WINDOWS] = false; + + for(const code of Object.keys(current_state)) + if(code !== "special") + delete current_state[code]; + + for(const hook of key_hooks_active) + hook.callback_release(); + key_hooks_active = []; +} + +function listener_hook(event: KeyEvent) { + if(event.type == EventType.KEY_TYPED) + return; + + let old_hooks = [...key_hooks_active]; + let new_hooks = []; + + current_state.special[SpecialKey.ALT] = event.key_alt; + current_state.special[SpecialKey.CTRL] = event.key_ctrl; + current_state.special[SpecialKey.SHIFT] = event.key_shift; + current_state.special[SpecialKey.WINDOWS] = event.key_windows; + + current_state[event.key_code] = undefined; + if(event.type == EventType.KEY_PRESS) { + current_state[event.key_code] = event; + + for(const hook of key_hooks) { + if(hook.key_code !== event.key_code) continue; + if(hook.key_alt != event.key_alt) continue; + if(hook.key_ctrl != event.key_ctrl) continue; + if(hook.key_shift != event.key_shift) continue; + if(hook.key_windows != event.key_windows) continue; + + new_hooks.push(hook); + if(!old_hooks.remove(hook) && hook.callback_press) { + hook.callback_press(); + log.trace(LogCategory.GENERAL, tr("Trigger key press for %o!"), hook); + } + } + } + + //We have a new situation + for(const hook of old_hooks) { + //Do not test for meta key states because they could differ in a key release event + if(hook.key_code === event.key_code) { + if(hook.callback_release) { + hook.callback_release(); + log.trace(LogCategory.GENERAL, tr("Trigger key release for %o!"), hook); + } + } else { + new_hooks.push(hook); + } + } + key_hooks_active = new_hooks; +} + +export function register_key_hook(hook: KeyHook) { + key_hooks.push(hook); +} + +export function unregister_key_hook(hook: KeyHook) { + key_hooks.remove(hook); +} + +export function key_pressed(code: string | SpecialKey) : boolean { + if(typeof(code) === 'string') + return typeof current_state[code] !== "undefined"; + return current_state.special[code]; +} \ No newline at end of file diff --git a/web/js/voice/AudioResampler.ts b/web/js/voice/AudioResampler.ts index 1092eea1..5d833300 100644 --- a/web/js/voice/AudioResampler.ts +++ b/web/js/voice/AudioResampler.ts @@ -1,4 +1,7 @@ -class AudioResampler { +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; + +export class AudioResampler { targetSampleRate: number; private _use_promise: boolean; diff --git a/web/js/voice/JavascriptRecorder.ts b/web/js/voice/JavascriptRecorder.ts deleted file mode 100644 index 57121118..00000000 --- a/web/js/voice/JavascriptRecorder.ts +++ /dev/null @@ -1,852 +0,0 @@ -/// - -interface MediaStream { - stop(); -} - -namespace audio { - export namespace recorder { - let _queried_devices: JavascriptInputDevice[]; - let _queried_permissioned: boolean = false; - - export interface JavascriptInputDevice extends InputDevice { - device_id: string; - group_id: string; - } - - async function query_devices() { - const general_supported = !!getUserMediaFunctionPromise(); - - try { - const context = player.context(); - const devices = await navigator.mediaDevices.enumerateDevices(); - - _queried_permissioned = false; - if(devices.filter(e => !!e.label).length > 0) - _queried_permissioned = true; - - _queried_devices = devices.filter(e => e.kind === "audioinput").map((e: MediaDeviceInfo): JavascriptInputDevice => { - return { - channels: context ? context.destination.channelCount : 2, - sample_rate: context ? context.sampleRate : 44100, - - default_input: e.deviceId == "default", - - driver: "WebAudio", - name: e.label || "device-id{" + e.deviceId+ "}", - - supported: general_supported, - - device_id: e.deviceId, - group_id: e.groupId, - - unique_id: e.deviceId - } - }); - if(_queried_devices.length > 0 && _queried_devices.filter(e => e.default_input).length == 0) - _queried_devices[0].default_input = true; - } catch(error) { - log.error(LogCategory.AUDIO, tr("Failed to query microphone devices (%o)"), error); - _queried_devices = []; - } - } - - export function devices() : InputDevice[] { - if(typeof(_queried_devices) === "undefined") - query_devices(); - - return _queried_devices || []; - } - - - export function device_refresh_available() : boolean { return true; } - export function refresh_devices() : Promise { return query_devices(); } - - export function create_input() : AbstractInput { return new JavascriptInput(); } - - export async function create_levelmeter(device: InputDevice) : Promise { - const meter = new JavascriptLevelmeter(device as any); - await meter.initialize(); - return meter; - } - - loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { - function: async () => { query_devices(); }, /* May wait for it? */ - priority: 10, - name: "query media devices" - }); - - export namespace filter { - export abstract class JAbstractFilter implements Filter { - type; - - source_node: AudioNode; - audio_node: NodeType; - - context: AudioContext; - enabled: boolean = false; - - active: boolean = false; /* if true the filter filters! */ - callback_active_change: (new_state: boolean) => any; - - paused: boolean = true; - - abstract initialize(context: AudioContext, source_node: AudioNode); - abstract finalize(); - - /* whatever the input has been paused and we don't expect any input */ - abstract set_pause(flag: boolean); - - is_enabled(): boolean { - return this.enabled; - } - } - - export class JThresholdFilter extends JAbstractFilter implements ThresholdFilter { - public static update_task_interval = 20; /* 20ms */ - - type = Type.THRESHOLD; - callback_level?: (value: number) => any; - - private _threshold = 50; - - private _update_task: any; - private _analyser: AnalyserNode; - private _analyse_buffer: Uint8Array; - - private _silence_count = 0; - private _margin_frames = 5; - - private _current_level = 0; - private _smooth_release = 0; - private _smooth_attack = 0; - - finalize() { - this.set_pause(true); - - if(this.source_node) { - try { this.source_node.disconnect(this._analyser) } catch (error) {} - try { this.source_node.disconnect(this.audio_node) } catch (error) {} - } - - this._analyser = undefined; - this.source_node = undefined; - this.audio_node = undefined; - this.context = undefined; - } - - initialize(context: AudioContext, source_node: AudioNode) { - this.context = context; - this.source_node = source_node; - - this.audio_node = context.createGain(); - this._analyser = context.createAnalyser(); - - const optimal_ftt_size = Math.ceil((source_node.context || context).sampleRate * (JThresholdFilter.update_task_interval / 1000)); - const base2_ftt = Math.pow(2, Math.ceil(Math.log2(optimal_ftt_size))); - this._analyser.fftSize = base2_ftt; - - if(!this._analyse_buffer || this._analyse_buffer.length < this._analyser.fftSize) - this._analyse_buffer = new Uint8Array(this._analyser.fftSize); - - this.active = false; - this.audio_node.gain.value = 1; - - this.source_node.connect(this.audio_node); - this.source_node.connect(this._analyser); - - /* force update paused state */ - this.set_pause(!(this.paused = !this.paused)); - } - - get_margin_frames(): number { return this._margin_frames; } - set_margin_frames(value: number) { - this._margin_frames = value; - } - - get_attack_smooth(): number { - return this._smooth_attack; - } - - get_release_smooth(): number { - return this._smooth_release; - } - - set_attack_smooth(value: number) { - this._smooth_attack = value; - } - - set_release_smooth(value: number) { - this._smooth_release = value; - } - - get_threshold(): number { - return this._threshold; - } - - set_threshold(value: number): Promise { - this._threshold = value; - return Promise.resolve(); - } - - public static process(buffer: Uint8Array, ftt_size: number, previous: number, smooth: number) { - let level; - { - let total = 0, float, rms; - - for(let index = 0; index < ftt_size; index++) { - float = ( buffer[index++] / 0x7f ) - 1; - total += (float * float); - } - rms = Math.sqrt(total / ftt_size); - let db = 20 * ( Math.log(rms) / Math.log(10) ); - // sanity check - - db = Math.max(-192, Math.min(db, 0)); - level = 100 + ( db * 1.92 ); - } - - return previous * smooth + level * (1 - smooth); - } - - private _analyse() { - this._analyser.getByteTimeDomainData(this._analyse_buffer); - - let smooth; - if(this._silence_count == 0) - smooth = this._smooth_release; - else - smooth = this._smooth_attack; - - this._current_level = JThresholdFilter.process(this._analyse_buffer, this._analyser.fftSize, this._current_level, smooth); - - this._update_gain_node(); - if(this.callback_level) - this.callback_level(this._current_level); - } - - private _update_gain_node() { - let state; - if(this._current_level > this._threshold) { - this._silence_count = 0; - state = true; - } else { - state = this._silence_count++ < this._margin_frames; - } - if(state) { - this.audio_node.gain.value = 1; - if(this.active) { - this.active = false; - this.callback_active_change(false); - } - } else { - this.audio_node.gain.value = 0; - if(!this.active) { - this.active = true; - this.callback_active_change(true); - } - } - } - - set_pause(flag: boolean) { - if(flag === this.paused) return; - this.paused = flag; - - if(this.paused) { - clearInterval(this._update_task); - this._update_task = undefined; - - if(this.active) { - this.active = false; - this.callback_active_change(false); - } - } else { - if(!this._update_task && this._analyser) - this._update_task = setInterval(() => this._analyse(), JThresholdFilter.update_task_interval); - } - } - } - - export class JStateFilter extends JAbstractFilter implements StateFilter { - type = Type.STATE; - - finalize() { - if(this.source_node) { - try { this.source_node.disconnect(this.audio_node) } catch (error) {} - } - - this.source_node = undefined; - this.audio_node = undefined; - this.context = undefined; - } - - initialize(context: AudioContext, source_node: AudioNode) { - this.context = context; - this.source_node = source_node; - - this.audio_node = context.createGain(); - this.audio_node.gain.value = this.active ? 0 : 1; - - this.source_node.connect(this.audio_node); - } - - is_active(): boolean { - return this.active; - } - - set_state(state: boolean): Promise { - if(this.active === state) - return Promise.resolve(); - - this.active = state; - if(this.audio_node) - this.audio_node.gain.value = state ? 0 : 1; - this.callback_active_change(state); - return Promise.resolve(); - } - - set_pause(flag: boolean) { - this.paused = flag; - } - } - } - - class JavascriptInput implements AbstractInput { - private _state: InputState = InputState.PAUSED; - private _current_device: JavascriptInputDevice | undefined; - private _current_consumer: InputConsumer; - - private _current_stream: MediaStream; - private _current_audio_stream: MediaStreamAudioSourceNode; - - private _audio_context: AudioContext; - private _source_node: AudioNode; /* last node which could be connected to the target; target might be the _consumer_node */ - private _consumer_callback_node: ScriptProcessorNode; - private readonly _consumer_audio_callback; - private _volume_node: GainNode; - private _mute_node: GainNode; - - private _filters: filter.Filter[] = []; - private _filter_active: boolean = false; - - private _volume: number = 1; - - callback_begin: () => any = undefined; - callback_end: () => any = undefined; - - constructor() { - player.on_ready(() => this._audio_initialized()); - this._consumer_audio_callback = this._audio_callback.bind(this); - } - - private _audio_initialized() { - this._audio_context = player.context(); - if(!this._audio_context) - return; - - this._mute_node = this._audio_context.createGain(); - this._mute_node.gain.value = 0; - this._mute_node.connect(this._audio_context.destination); - - this._consumer_callback_node = this._audio_context.createScriptProcessor(1024 * 4); - this._consumer_callback_node.connect(this._mute_node); - - this._volume_node = this._audio_context.createGain(); - this._volume_node.gain.value = this._volume; - - this._initialize_filters(); - if(this._state === InputState.INITIALIZING) - this.start(); - } - - private _initialize_filters() { - const filters = this._filters as any as filter.JAbstractFilter[]; - for(const filter of filters) { - if(filter.is_enabled()) - filter.finalize(); - } - - if(this._audio_context && this._volume_node) { - const active_filter = filters.filter(e => e.is_enabled()); - let stream: AudioNode = this._volume_node; - for(const f of active_filter) { - f.initialize(this._audio_context, stream); - stream = f.audio_node; - } - this._switch_source_node(stream); - } - } - - private _audio_callback(event: AudioProcessingEvent) { - if(!this._current_consumer || this._current_consumer.type !== InputConsumerType.CALLBACK) - return; - - const callback = this._current_consumer as CallbackInputConsumer; - if(callback.callback_audio) - callback.callback_audio(event.inputBuffer); - if(callback.callback_buffer) { - log.warn(LogCategory.AUDIO, tr("AudioInput has callback buffer, but this isn't supported yet!")); - } - } - - current_state() : InputState { return this._state; }; - - private _start_promise: Promise; - async start() : Promise { - if(this._start_promise) { - try { - await this._start_promise; - if(this._state != InputState.PAUSED) - return; - } catch(error) { - log.debug(LogCategory.AUDIO, tr("JavascriptInput:start() Start promise await resulted in an error: %o"), error); - } - } - - return await (this._start_promise = this._start()); - } - - /* request permission for devices only one per time! */ - private static _running_request: Promise; - static async request_media_stream(device_id: string, group_id: string) : Promise { - while(this._running_request) { - try { - await this._running_request; - } catch(error) { } - } - const promise = (this._running_request = this.request_media_stream0(device_id, group_id)); - try { - return await this._running_request; - } finally { - if(this._running_request === promise) - this._running_request = undefined; - } - } - - static async request_media_stream0(device_id: string, group_id: string) : Promise { - const media_function = getUserMediaFunctionPromise(); - if(!media_function) return InputStartResult.ENOTSUPPORTED; - - try { - log.info(LogCategory.AUDIO, tr("Requesting a microphone stream for device %s in group %s"), device_id, group_id); - - const audio_constrains: MediaTrackConstraints = {}; - audio_constrains.deviceId = device_id; - audio_constrains.groupId = group_id; - - audio_constrains.echoCancellation = true; - /* may supported */ (audio_constrains as any).autoGainControl = true; - /* may supported */ (audio_constrains as any).noiseSuppression = true; - /* disabled because most the time we get a OverconstrainedError */ //audio_constrains.sampleSize = {min: 420, max: 960 * 10, ideal: 960}; - - const stream = await media_function({audio: audio_constrains, video: undefined}); - if(!_queried_permissioned) query_devices(); /* we now got permissions, requery devices */ - return stream; - } catch(error) { - if('name' in error) { - if(error.name === "NotAllowedError") { - //createErrorModal(tr("Failed to create microphone"), tr("Microphone recording failed. Please allow TeaWeb access to your microphone")).open(); - //FIXME: Move this to somewhere else! - - log.warn(LogCategory.AUDIO, tr("Microphone request failed (No permissions). Browser message: %o"), error.message); - return InputStartResult.ENOTALLOWED; - } else { - log.warn(LogCategory.AUDIO, tr("Microphone request failed. Request resulted in error: %o: %o"), error.name, error); - } - } else { - log.warn(LogCategory.AUDIO, tr("Failed to initialize recording stream (%o)"), error); - } - return InputStartResult.EUNKNOWN; - } - } - - private async _start() : Promise { - try { - if(this._state != InputState.PAUSED) - throw tr("recorder already started"); - - this._state = InputState.INITIALIZING; - if(!this._current_device) - throw tr("invalid device"); - - if(!this._audio_context) { - debugger; - throw tr("missing audio context"); - } - - const _result = await JavascriptInput.request_media_stream(this._current_device.device_id, this._current_device.group_id); - if(!(_result instanceof MediaStream)) { - this._state = InputState.PAUSED; - return _result; - } - this._current_stream = _result; - - for(const f of this._filters) - if(f.is_enabled() && f instanceof filter.JAbstractFilter) - f.set_pause(false); - this._consumer_callback_node.addEventListener('audioprocess', this._consumer_audio_callback); - - this._current_audio_stream = this._audio_context.createMediaStreamSource(this._current_stream); - this._current_audio_stream.connect(this._volume_node); - this._state = InputState.RECORDING; - return InputStartResult.EOK; - } catch(error) { - if(this._state == InputState.INITIALIZING) { - this._state = InputState.PAUSED; - } - throw error; - } finally { - this._start_promise = undefined; - } - } - - async stop() { - /* await all starts */ - try { - if(this._start_promise) - await this._start_promise; - } catch(error) {} - - this._state = InputState.PAUSED; - if(this._current_audio_stream) - this._current_audio_stream.disconnect(); - - if(this._current_stream) { - if(this._current_stream.stop) - this._current_stream.stop(); - else - this._current_stream.getTracks().forEach(value => { - value.stop(); - }); - } - - this._current_stream = undefined; - this._current_audio_stream = undefined; - for(const f of this._filters) - if(f.is_enabled() && f instanceof filter.JAbstractFilter) - f.set_pause(true); - if(this._consumer_callback_node) - this._consumer_callback_node.removeEventListener('audioprocess', this._consumer_audio_callback); - return undefined; - } - - - current_device(): InputDevice | undefined { - return this._current_device; - } - - async set_device(device: InputDevice | undefined) { - if(this._current_device === device) - return; - - - const saved_state = this._state; - try { - await this.stop(); - } catch(error) { - log.warn(LogCategory.AUDIO, tr("Failed to stop previous record session (%o)"), error); - } - - this._current_device = device as any; /* TODO: Test for device_id and device_group */ - if(!device) { - this._state = saved_state === InputState.PAUSED ? InputState.PAUSED : InputState.DRY; - return; - } - - if(saved_state !== InputState.PAUSED) { - try { - await this.start() - } catch(error) { - log.warn(LogCategory.AUDIO, tr("Failed to start new recording stream (%o)"), error); - throw "failed to start record"; - } - } - return; - } - - - get_filter(type: filter.Type): filter.Filter | undefined { - for(const filter of this._filters) - if(filter.type == type) - return filter; - - let new_filter: filter.JAbstractFilter; - switch (type) { - case filter.Type.STATE: - new_filter = new filter.JStateFilter(); - break; - case filter.Type.VOICE_LEVEL: - throw "voice filter isn't supported!"; - case filter.Type.THRESHOLD: - new_filter = new filter.JThresholdFilter(); - break; - default: - throw "invalid filter type, or type isn't implemented! (" + type + ")"; - } - - new_filter.callback_active_change = () => this._recalculate_filter_status(); - this._filters.push(new_filter as any); - this.enable_filter(type); - return new_filter as any; - } - - supports_filter(type: audio.recorder.filter.Type) : boolean { - switch (type) { - case audio.recorder.filter.Type.THRESHOLD: - case audio.recorder.filter.Type.STATE: - return true; - default: - return false; - } - } - - private find_filter(type: filter.Type) : filter.JAbstractFilter | undefined { - for(const filter of this._filters) - if(filter.type == type) - return filter as any; - return undefined; - } - - clear_filter() { - for(const _filter of this._filters) { - if(!_filter.is_enabled()) - continue; - - const c_filter = _filter as any as filter.JAbstractFilter; - c_filter.finalize(); - c_filter.enabled = false; - } - - this._initialize_filters(); - this._recalculate_filter_status(); - } - - disable_filter(type: filter.Type) { - const filter = this.find_filter(type); - if(!filter) return; - - /* test if the filter is active */ - if(!filter.is_enabled()) - return; - - filter.enabled = false; - filter.set_pause(true); - filter.finalize(); - this._initialize_filters(); - this._recalculate_filter_status(); - } - - enable_filter(type: filter.Type) { - const filter = this.get_filter(type) as any as filter.JAbstractFilter; - if(filter.is_enabled()) - return; - - filter.enabled = true; - filter.set_pause(typeof this._current_audio_stream !== "object"); - this._initialize_filters(); - this._recalculate_filter_status(); - } - - private _recalculate_filter_status() { - let filtered = this._filters.filter(e => e.is_enabled()).filter(e => (e as any as filter.JAbstractFilter).active).length > 0; - if(filtered === this._filter_active) - return; - - this._filter_active = filtered; - if(filtered) { - if(this.callback_end) - this.callback_end(); - } else { - if(this.callback_begin) - this.callback_begin(); - } - } - - current_consumer(): InputConsumer | undefined { - return this._current_consumer; - } - - async set_consumer(consumer: InputConsumer) { - if(this._current_consumer) { - if(this._current_consumer.type == InputConsumerType.NODE) { - if(this._source_node) - (this._current_consumer as NodeInputConsumer).callback_disconnect(this._source_node) - } else if(this._current_consumer.type === InputConsumerType.CALLBACK) { - if(this._source_node) - this._source_node.disconnect(this._consumer_callback_node); - } - } - - if(consumer) { - if(consumer.type == InputConsumerType.CALLBACK) { - if(this._source_node) - this._source_node.connect(this._consumer_callback_node); - } else if(consumer.type == InputConsumerType.NODE) { - if(this._source_node) - (consumer as NodeInputConsumer).callback_node(this._source_node); - } else { - throw "native callback consumers are not supported!"; - } - } - this._current_consumer = consumer; - } - - private _switch_source_node(new_node: AudioNode) { - if(this._current_consumer) { - if(this._current_consumer.type == InputConsumerType.NODE) { - const node_consumer = this._current_consumer as NodeInputConsumer; - if(this._source_node) - node_consumer.callback_disconnect(this._source_node); - if(new_node) - node_consumer.callback_node(new_node); - } else if(this._current_consumer.type == InputConsumerType.CALLBACK) { - this._source_node.disconnect(this._consumer_callback_node); - if(new_node) - new_node.connect(this._consumer_callback_node); - } - } - this._source_node = new_node; - } - - get_volume(): number { - return this._volume; - } - - set_volume(volume: number) { - if(volume === this._volume) - return; - this._volume = volume; - this._volume_node.gain.value = volume; - } - } - - class JavascriptLevelmeter implements LevelMeter { - private static _instances: JavascriptLevelmeter[] = []; - private static _update_task: number; - - readonly _device: JavascriptInputDevice; - - private _callback: (num: number) => any; - - private _context: AudioContext; - private _gain_node: GainNode; - private _source_node: MediaStreamAudioSourceNode; - private _analyser_node: AnalyserNode; - - private _media_stream: MediaStream; - - private _analyse_buffer: Uint8Array; - - private _current_level = 0; - - constructor(device: JavascriptInputDevice) { - this._device = device; - } - - async initialize() { - try { - await new Promise((resolve, reject) => { - const timeout = setTimeout(reject, 5000); - player.on_ready(() => { - clearTimeout(timeout); - resolve(); - }); - }); - } catch(error) { - throw tr("audio context timeout"); - } - this._context = player.context(); - if(!this._context) throw tr("invalid context"); - - this._gain_node = this._context.createGain(); - this._gain_node.gain.setValueAtTime(0, 0); - - /* analyser node */ - this._analyser_node = this._context.createAnalyser(); - - const optimal_ftt_size = Math.ceil(this._context.sampleRate * (filter.JThresholdFilter.update_task_interval / 1000)); - this._analyser_node.fftSize = Math.pow(2, Math.ceil(Math.log2(optimal_ftt_size))); - - if(!this._analyse_buffer || this._analyse_buffer.length < this._analyser_node.fftSize) - this._analyse_buffer = new Uint8Array(this._analyser_node.fftSize); - - /* starting stream */ - const _result = await JavascriptInput.request_media_stream(this._device.device_id, this._device.group_id); - if(!(_result instanceof MediaStream)){ - if(_result === InputStartResult.ENOTALLOWED) - throw tr("No permissions"); - if(_result === InputStartResult.ENOTSUPPORTED) - throw tr("Not supported"); - if(_result === InputStartResult.EBUSY) - throw tr("Device busy"); - if(_result === InputStartResult.EUNKNOWN) - throw tr("an error occurred"); - throw _result; - } - this._media_stream = _result; - - this._source_node = this._context.createMediaStreamSource(this._media_stream); - this._source_node.connect(this._analyser_node); - this._analyser_node.connect(this._gain_node); - this._gain_node.connect(this._context.destination); - - JavascriptLevelmeter._instances.push(this); - if(JavascriptLevelmeter._instances.length == 1) { - clearInterval(JavascriptLevelmeter._update_task); - JavascriptLevelmeter._update_task = setInterval(() => JavascriptLevelmeter._analyse_all(), filter.JThresholdFilter.update_task_interval) as any; - } - } - - destory() { - JavascriptLevelmeter._instances.remove(this); - if(JavascriptLevelmeter._instances.length == 0) { - clearInterval(JavascriptLevelmeter._update_task); - JavascriptLevelmeter._update_task = 0; - } - - if(this._source_node) { - this._source_node.disconnect(); - this._source_node = undefined; - } - if(this._media_stream) { - if(this._media_stream.stop) - this._media_stream.stop(); - else - this._media_stream.getTracks().forEach(value => { - value.stop(); - }); - this._media_stream = undefined; - } - if(this._gain_node) { - this._gain_node.disconnect(); - this._gain_node = undefined; - } - if(this._analyser_node) { - this._analyser_node.disconnect(); - this._analyser_node = undefined; - } - } - - device(): audio.recorder.InputDevice { - return this._device; - } - - set_observer(callback: (value: number) => any) { - this._callback = callback; - } - - private static _analyse_all() { - for(const instance of [...this._instances]) - instance._analyse(); - } - - private _analyse() { - this._analyser_node.getByteTimeDomainData(this._analyse_buffer); - - this._current_level = filter.JThresholdFilter.process(this._analyse_buffer, this._analyser_node.fftSize, this._current_level, .75); - if(this._callback) - this._callback(this._current_level); - } - } - } -} \ No newline at end of file diff --git a/web/js/voice/VoiceClient.ts b/web/js/voice/VoiceClient.ts index c54584c4..3bc565fc 100644 --- a/web/js/voice/VoiceClient.ts +++ b/web/js/voice/VoiceClient.ts @@ -1,236 +1,239 @@ -/// +import {voice} from "tc-shared/connection/ConnectionBase"; +import VoiceClient = voice.VoiceClient; +import PlayerState = voice.PlayerState; +import {CodecClientCache} from "../codec/Codec"; +import * as aplayer from "../audio/player"; +import {LogCategory} from "tc-shared/log"; +import * as log from "tc-shared/log"; +import LatencySettings = voice.LatencySettings; -namespace audio { - export namespace js { - export class VoiceClientController implements connection.voice.VoiceClient { - callback_playback: () => any; - callback_state_changed: (new_state: connection.voice.PlayerState) => any; - callback_stopped: () => any; - client_id: number; +export class VoiceClientController implements VoiceClient { + callback_playback: () => any; + callback_state_changed: (new_state: PlayerState) => any; + callback_stopped: () => any; + client_id: number; - speakerContext: AudioContext; - private _player_state: connection.voice.PlayerState = connection.voice.PlayerState.STOPPED; - private _codecCache: CodecClientCache[] = []; + speakerContext: AudioContext; + private _player_state: PlayerState = PlayerState.STOPPED; + private _codecCache: CodecClientCache[] = []; - private _time_index: number = 0; - private _latency_buffer_length: number = 3; - private _buffer_timeout: NodeJS.Timer; + private _time_index: number = 0; + private _latency_buffer_length: number = 3; + private _buffer_timeout: NodeJS.Timer; - private _buffered_samples: AudioBuffer[] = []; - private _playing_nodes: AudioBufferSourceNode[] = []; + private _buffered_samples: AudioBuffer[] = []; + private _playing_nodes: AudioBufferSourceNode[] = []; - private _volume: number = 1; - allowBuffering: boolean = true; + private _volume: number = 1; + allowBuffering: boolean = true; - constructor(client_id: number) { - this.client_id = client_id; + constructor(client_id: number) { + this.client_id = client_id; - audio.player.on_ready(() => this.speakerContext = audio.player.context()); - } + aplayer.on_ready(() => this.speakerContext = aplayer.context()); + } - public initialize() { } + public initialize() { } - public close(){ } + public close(){ } - playback_buffer(buffer: AudioBuffer) { - if(!buffer) { - log.warn(LogCategory.VOICE, tr("[AudioController] Got empty or undefined buffer! Dropping it")); - return; - } + playback_buffer(buffer: AudioBuffer) { + if(!buffer) { + log.warn(LogCategory.VOICE, tr("[AudioController] Got empty or undefined buffer! Dropping it")); + return; + } - if(!this.speakerContext) { - log.warn(LogCategory.VOICE, tr("[AudioController] Failed to replay audio. Global audio context not initialized yet!")); - return; - } + if(!this.speakerContext) { + log.warn(LogCategory.VOICE, tr("[AudioController] Failed to replay audio. Global audio context not initialized yet!")); + return; + } - if (buffer.sampleRate != this.speakerContext.sampleRate) - log.warn(LogCategory.VOICE, tr("[AudioController] Source sample rate isn't equal to playback sample rate! (%o | %o)"), buffer.sampleRate, this.speakerContext.sampleRate); + if (buffer.sampleRate != this.speakerContext.sampleRate) + log.warn(LogCategory.VOICE, tr("[AudioController] Source sample rate isn't equal to playback sample rate! (%o | %o)"), buffer.sampleRate, this.speakerContext.sampleRate); - this.apply_volume_to_buffer(buffer); + this.apply_volume_to_buffer(buffer); - this._buffered_samples.push(buffer); - if(this._player_state == connection.voice.PlayerState.STOPPED || this._player_state == connection.voice.PlayerState.STOPPING) { - log.info(LogCategory.VOICE, tr("[Audio] Starting new playback")); - this.set_state(connection.voice.PlayerState.PREBUFFERING); - } + this._buffered_samples.push(buffer); + if(this._player_state == PlayerState.STOPPED || this._player_state == PlayerState.STOPPING) { + log.info(LogCategory.VOICE, tr("[Audio] Starting new playback")); + this.set_state(PlayerState.PREBUFFERING); + } - switch (this._player_state) { - case connection.voice.PlayerState.PREBUFFERING: - case connection.voice.PlayerState.BUFFERING: - this.reset_buffer_timeout(true); //Reset timeout, we got a new buffer - if(this._buffered_samples.length <= this._latency_buffer_length) { - if(this._player_state == connection.voice.PlayerState.BUFFERING) { - if(this.allowBuffering) - break; - } else - break; - } - if(this._player_state == connection.voice.PlayerState.PREBUFFERING) { - log.info(LogCategory.VOICE, tr("[Audio] Prebuffering succeeded (Replaying now)")); - if(this.callback_playback) - this.callback_playback(); - } else if(this.allowBuffering) { - log.info(LogCategory.VOICE, tr("[Audio] Buffering succeeded (Replaying now)")); - } - this._player_state = connection.voice.PlayerState.PLAYING; - case connection.voice.PlayerState.PLAYING: - this.replay_queue(); - break; - default: + switch (this._player_state) { + case PlayerState.PREBUFFERING: + case PlayerState.BUFFERING: + this.reset_buffer_timeout(true); //Reset timeout, we got a new buffer + if(this._buffered_samples.length <= this._latency_buffer_length) { + if(this._player_state == PlayerState.BUFFERING) { + if(this.allowBuffering) + break; + } else break; } - } - - private replay_queue() { - let buffer: AudioBuffer; - while((buffer = this._buffered_samples.pop_front())) { - if(this._playing_nodes.length >= this._latency_buffer_length * 1.5 + 3) { - log.info(LogCategory.VOICE, tr("Dropping buffer because playing queue grows to much")); - continue; /* drop the data (we're behind) */ - } - if(this._time_index < this.speakerContext.currentTime) - this._time_index = this.speakerContext.currentTime; - - const player = this.speakerContext.createBufferSource(); - player.buffer = buffer; - - player.onended = () => this.on_buffer_replay_finished(player); - this._playing_nodes.push(player); - - player.connect(audio.player.destination()); - player.start(this._time_index); - this._time_index += buffer.duration; + if(this._player_state == PlayerState.PREBUFFERING) { + log.info(LogCategory.VOICE, tr("[Audio] Prebuffering succeeded (Replaying now)")); + if(this.callback_playback) + this.callback_playback(); + } else if(this.allowBuffering) { + log.info(LogCategory.VOICE, tr("[Audio] Buffering succeeded (Replaying now)")); } + this._player_state = PlayerState.PLAYING; + case PlayerState.PLAYING: + this.replay_queue(); + break; + default: + break; + } + } + + private replay_queue() { + let buffer: AudioBuffer; + while((buffer = this._buffered_samples.pop_front())) { + if(this._playing_nodes.length >= this._latency_buffer_length * 1.5 + 3) { + log.info(LogCategory.VOICE, tr("Dropping buffer because playing queue grows to much")); + continue; /* drop the data (we're behind) */ } + if(this._time_index < this.speakerContext.currentTime) + this._time_index = this.speakerContext.currentTime; - private on_buffer_replay_finished(node: AudioBufferSourceNode) { - this._playing_nodes.remove(node); - this.test_buffer_queue(); - } + const player = this.speakerContext.createBufferSource(); + player.buffer = buffer; - stopAudio(now: boolean = false) { - this._player_state = connection.voice.PlayerState.STOPPING; - if(now) { - this._player_state = connection.voice.PlayerState.STOPPED; - this._buffered_samples = []; + player.onended = () => this.on_buffer_replay_finished(player); + this._playing_nodes.push(player); - for(const entry of this._playing_nodes) - entry.stop(0); - this._playing_nodes = []; + player.connect(aplayer.destination()); + player.start(this._time_index); + this._time_index += buffer.duration; + } + } - if(this.callback_stopped) - this.callback_stopped(); - } else { - this.test_buffer_queue(); /* test if we're not already done */ - this.replay_queue(); /* flush the queue */ - } - } + private on_buffer_replay_finished(node: AudioBufferSourceNode) { + this._playing_nodes.remove(node); + this.test_buffer_queue(); + } - private test_buffer_queue() { - if(this._buffered_samples.length == 0 && this._playing_nodes.length == 0) { - if(this._player_state != connection.voice.PlayerState.STOPPING && this._player_state != connection.voice.PlayerState.STOPPED) { - if(this._player_state == connection.voice.PlayerState.BUFFERING) - return; //We're already buffering + stopAudio(now: boolean = false) { + this._player_state = PlayerState.STOPPING; + if(now) { + this._player_state = PlayerState.STOPPED; + this._buffered_samples = []; - this._player_state = connection.voice.PlayerState.BUFFERING; - if(!this.allowBuffering) - log.warn(LogCategory.VOICE, tr("[Audio] Detected a buffer underflow!")); - this.reset_buffer_timeout(true); - } else { - this._player_state = connection.voice.PlayerState.STOPPED; - if(this.callback_stopped) - this.callback_stopped(); - } - } - } + for(const entry of this._playing_nodes) + entry.stop(0); + this._playing_nodes = []; - private reset_buffer_timeout(restart: boolean) { - if(this._buffer_timeout) - clearTimeout(this._buffer_timeout); + if(this.callback_stopped) + this.callback_stopped(); + } else { + this.test_buffer_queue(); /* test if we're not already done */ + this.replay_queue(); /* flush the queue */ + } + } - if(restart) - this._buffer_timeout = setTimeout(() => { - if(this._player_state == connection.voice.PlayerState.PREBUFFERING || this._player_state == connection.voice.PlayerState.BUFFERING) { - log.warn(LogCategory.VOICE, tr("[Audio] Buffering exceeded timeout. Flushing and stopping replay")); - this.stopAudio(); - } - this._buffer_timeout = undefined; - }, 1000); - } + private test_buffer_queue() { + if(this._buffered_samples.length == 0 && this._playing_nodes.length == 0) { + if(this._player_state != PlayerState.STOPPING && this._player_state != PlayerState.STOPPED) { + if(this._player_state == PlayerState.BUFFERING) + return; //We're already buffering - private apply_volume_to_buffer(buffer: AudioBuffer) { - if(this._volume == 1) - return; - - for(let channel = 0; channel < buffer.numberOfChannels; channel++) { - let data = buffer.getChannelData(channel); - for(let sample = 0; sample < data.length; sample++) { - let lane = data[sample]; - lane *= this._volume; - data[sample] = lane; - } - } - } - - private set_state(state: connection.voice.PlayerState) { - if(this._player_state == state) - return; - - this._player_state = state; - if(this.callback_state_changed) - this.callback_state_changed(this._player_state); - } - - get_codec_cache(codec: number) : CodecClientCache { - while(this._codecCache.length <= codec) - this._codecCache.push(new CodecClientCache()); - - return this._codecCache[codec]; - } - - get_state(): connection.voice.PlayerState { - return this._player_state; - } - - get_volume(): number { - return this._volume; - } - - set_volume(volume: number): void { - if(this._volume == volume) - return; - - this._volume = volume; - - /* apply the volume to all other buffers */ - for(const buffer of this._buffered_samples) - this.apply_volume_to_buffer(buffer); - } - - abort_replay() { - this.stopAudio(true); - } - - latency_settings(settings?: connection.voice.LatencySettings): connection.voice.LatencySettings { - throw "not supported"; - } - - reset_latency_settings() { - throw "not supported"; - } - - support_latency_settings(): boolean { - return false; - } - - support_flush(): boolean { - return false; - } - - flush() { - throw "not supported"; + this._player_state = PlayerState.BUFFERING; + if(!this.allowBuffering) + log.warn(LogCategory.VOICE, tr("[Audio] Detected a buffer underflow!")); + this.reset_buffer_timeout(true); + } else { + this._player_state = PlayerState.STOPPED; + if(this.callback_stopped) + this.callback_stopped(); } } } + + private reset_buffer_timeout(restart: boolean) { + if(this._buffer_timeout) + clearTimeout(this._buffer_timeout); + + if(restart) + this._buffer_timeout = setTimeout(() => { + if(this._player_state == PlayerState.PREBUFFERING || this._player_state == PlayerState.BUFFERING) { + log.warn(LogCategory.VOICE, tr("[Audio] Buffering exceeded timeout. Flushing and stopping replay")); + this.stopAudio(); + } + this._buffer_timeout = undefined; + }, 1000); + } + + private apply_volume_to_buffer(buffer: AudioBuffer) { + if(this._volume == 1) + return; + + for(let channel = 0; channel < buffer.numberOfChannels; channel++) { + let data = buffer.getChannelData(channel); + for(let sample = 0; sample < data.length; sample++) { + let lane = data[sample]; + lane *= this._volume; + data[sample] = lane; + } + } + } + + private set_state(state: PlayerState) { + if(this._player_state == state) + return; + + this._player_state = state; + if(this.callback_state_changed) + this.callback_state_changed(this._player_state); + } + + get_codec_cache(codec: number) : CodecClientCache { + while(this._codecCache.length <= codec) + this._codecCache.push(new CodecClientCache()); + + return this._codecCache[codec]; + } + + get_state(): PlayerState { + return this._player_state; + } + + get_volume(): number { + return this._volume; + } + + set_volume(volume: number): void { + if(this._volume == volume) + return; + + this._volume = volume; + + /* apply the volume to all other buffers */ + for(const buffer of this._buffered_samples) + this.apply_volume_to_buffer(buffer); + } + + abort_replay() { + this.stopAudio(true); + } + + latency_settings(settings?: LatencySettings): LatencySettings { + throw "not supported"; + } + + reset_latency_settings() { + throw "not supported"; + } + + support_latency_settings(): boolean { + return false; + } + + support_flush(): boolean { + return false; + } + + flush() { + throw "not supported"; + } } \ No newline at end of file diff --git a/web/js/voice/VoiceHandler.ts b/web/js/voice/VoiceHandler.ts index a834e6dd..7d84850a 100644 --- a/web/js/voice/VoiceHandler.ts +++ b/web/js/voice/VoiceHandler.ts @@ -1,666 +1,683 @@ -/// -/// +import * as log from "tc-shared/log"; +import * as loader from "tc-loader"; +import * as aplayer from "../audio/player"; +import * as elog from "tc-shared/ui/frames/server_log"; +import {BasicCodec} from "../codec/BasicCodec"; +import {CodecType} from "../codec/Codec"; +import {LogCategory} from "tc-shared/log"; +import {createErrorModal} from "tc-shared/ui/elements/Modal"; +import {CodecWrapperWorker} from "../codec/CodecWrapperWorker"; +import {ServerConnection} from "../connection/ServerConnection"; +import {voice} from "tc-shared/connection/ConnectionBase"; +import AbstractVoiceConnection = voice.AbstractVoiceConnection; +import {RecorderProfile} from "tc-shared/voice/RecorderProfile"; +import {VoiceClientController} from "./VoiceClient"; +import {settings} from "tc-shared/settings"; +import {CallbackInputConsumer, InputConsumerType, NodeInputConsumer} from "tc-shared/voice/RecorderBase"; +import VoiceClient = voice.VoiceClient; -namespace audio { - export namespace js { - export namespace codec { - class CacheEntry { - instance: BasicCodec; - owner: number; +export namespace codec { + class CacheEntry { + instance: BasicCodec; + owner: number; - last_access: number; - } + last_access: number; + } - export class CodecPool { - codecIndex: number; - name: string; - type: CodecType; + export function codec_supported(type: CodecType) { + return type == CodecType.OPUS_MUSIC || type == CodecType.OPUS_VOICE; + } - entries: CacheEntry[] = []; - maxInstances: number = 2; + export class CodecPool { + codecIndex: number; + name: string; + type: CodecType; - private _supported: boolean = true; + entries: CacheEntry[] = []; + maxInstances: number = 2; - initialize(cached: number) { - /* test if we're able to use this codec */ - const dummy_client_id = 0xFFEF; + private _supported: boolean = true; - this.ownCodec(dummy_client_id, _ => {}).then(codec => { - log.info(LogCategory.VOICE, tr("Release again! (%o)"), codec); - this.releaseCodec(dummy_client_id); - }).catch(error => { - if(this._supported) { - log.warn(LogCategory.VOICE, tr("Disabling codec support for "), this.name); - createErrorModal(tr("Could not load codec driver"), tr("Could not load or initialize codec ") + this.name + "
" + - "Error: " + JSON.stringify(error) + "").open(); - log.error(LogCategory.VOICE, tr("Failed to initialize the opus codec. Error: %o"), error); - } else { - log.debug(LogCategory.VOICE, tr("Failed to initialize already disabled codec. Error: %o"), error); - } - this._supported = false; - }); - } + initialize(cached: number) { + /* test if we're able to use this codec */ + const dummy_client_id = 0xFFEF; - supported() { return this._supported; } - - ownCodec?(clientId: number, callback_encoded: (buffer: Uint8Array) => any, create: boolean = true) : Promise { - return new Promise((resolve, reject) => { - if(!this._supported) { - reject(tr("unsupported codec!")); - return; - } - - let free_slot = 0; - for(let index = 0; index < this.entries.length; index++) { - if(this.entries[index].owner == clientId) { - this.entries[index].last_access = Date.now(); - if(this.entries[index].instance.initialized()) - resolve(this.entries[index].instance); - else { - this.entries[index].instance.initialise().then((flag) => { - //TODO test success flag - this.ownCodec(clientId, callback_encoded, false).then(resolve).catch(reject); - }).catch(error => { - log.error(LogCategory.VOICE, tr("Could not initialize codec!\nError: %o"), error); - reject(typeof(error) === 'string' ? error : tr("Could not initialize codec!")); - }); - } - return; - } else if(this.entries[index].owner == 0) { - free_slot = index; - } - } - - if(!create) { - resolve(undefined); - return; - } - - if(free_slot == 0){ - free_slot = this.entries.length; - let entry = new CacheEntry(); - entry.instance = audio.codec.new_instance(this.type); - this.entries.push(entry); - } - this.entries[free_slot].owner = clientId; - this.entries[free_slot].last_access = new Date().getTime(); - this.entries[free_slot].instance.on_encoded_data = callback_encoded; - if(this.entries[free_slot].instance.initialized()) - this.entries[free_slot].instance.reset(); - else { - this.ownCodec(clientId, callback_encoded, false).then(resolve).catch(reject); - return; - } - resolve(this.entries[free_slot].instance); - }); - } - - releaseCodec(clientId: number) { - for(let index = 0; index < this.entries.length; index++) - if(this.entries[index].owner == clientId) this.entries[index].owner = 0; - } - - constructor(index: number, name: string, type: CodecType){ - this.codecIndex = index; - this.name = name; - this.type = type; - - this._supported = this.type !== undefined && audio.codec.supported(this.type); - } - } - } - - export enum VoiceEncodeType { - JS_ENCODE, - NATIVE_ENCODE - } - - export class VoiceConnection extends connection.voice.AbstractVoiceConnection { - readonly connection: connection.ServerConnection; - - rtcPeerConnection: RTCPeerConnection; - dataChannel: RTCDataChannel; - - private _type: VoiceEncodeType = VoiceEncodeType.NATIVE_ENCODE; - - /* - * To ensure we're not sending any audio because the settings activates the input, - * we self mute the audio stream - */ - local_audio_mute: GainNode; - local_audio_stream: MediaStreamAudioDestinationNode; - - static codec_pool: codec.CodecPool[]; - - static codecSupported(type: number) : boolean { - return this.codec_pool && this.codec_pool.length > type && this.codec_pool[type].supported(); - } - - private voice_packet_id: number = 0; - private chunkVPacketId: number = 0; - private send_task: NodeJS.Timer; - - private _audio_source: RecorderProfile; - private _audio_clients: audio.js.VoiceClientController[] = []; - - private _encoder_codec: number = 5; - - constructor(connection: connection.ServerConnection) { - super(connection); - this.connection = connection; - - this._type = settings.static_global("voice_connection_type", this._type); - } - - destroy() { - clearInterval(this.send_task); - this.drop_rtp_session(); - this.acquire_voice_recorder(undefined, true).catch(error => { - log.warn(LogCategory.VOICE, tr("Failed to release voice recorder: %o"), error); - }).then(() => { - for(const client of this._audio_clients) { - client.abort_replay(); - client.callback_playback = undefined; - client.callback_state_changed = undefined; - client.callback_stopped = undefined; - } - this._audio_clients = undefined; - this._audio_source = undefined; - }); - } - - static native_encoding_supported() : boolean { - const context = window.webkitAudioContext || window.AudioContext; - if(!context) - return false; - - if(!context.prototype.createMediaStreamDestination) - return false; //Required, but not available within edge - - return true; - } - - static javascript_encoding_supported() : boolean { - if(!window.RTCPeerConnection) - return false; - if(!RTCPeerConnection.prototype.createDataChannel) - return false; - return true; - } - - current_encoding_supported() : boolean { - switch (this._type) { - case VoiceEncodeType.JS_ENCODE: - return audio.js.VoiceConnection.javascript_encoding_supported(); - case VoiceEncodeType.NATIVE_ENCODE: - return audio.js.VoiceConnection.native_encoding_supported(); - } - return false; - } - - private setup_native() { - log.info(LogCategory.VOICE, tr("Setting up native voice stream!")); - if(!audio.js.VoiceConnection.native_encoding_supported()) { - log.warn(LogCategory.VOICE, tr("Native codec isn't supported!")); - return; - } - - if(!this.local_audio_stream) { - this.local_audio_stream = audio.player.context().createMediaStreamDestination(); - } - if(!this.local_audio_mute) { - this.local_audio_mute = audio.player.context().createGain(); - this.local_audio_mute.connect(this.local_audio_stream); - this.local_audio_mute.gain.value = 1; - } - } - - private setup_js() { - if(!audio.js.VoiceConnection.javascript_encoding_supported()) return; - if(!this.send_task) - this.send_task = setInterval(this.send_next_voice_packet.bind(this), 20); /* send all 20ms out voice packets */ - } - - async acquire_voice_recorder(recorder: RecorderProfile | undefined, enforce?: boolean) { - if(this._audio_source === recorder && !enforce) - return; - - if(recorder) - await recorder.unmount(); - - if(this._audio_source) - await this._audio_source.unmount(); - - this.handle_local_voice_ended(); - this._audio_source = recorder; - - if(recorder) { - recorder.current_handler = this.connection.client; - - recorder.callback_unmount = this.on_recorder_yield.bind(this); - recorder.callback_start = this.handle_local_voice_started.bind(this); - recorder.callback_stop = this.handle_local_voice_ended.bind(this); - - if(this._type == VoiceEncodeType.NATIVE_ENCODE) { - if(!this.local_audio_stream) - this.setup_native(); /* requires initialized audio */ - - await recorder.input.set_consumer({ - type: audio.recorder.InputConsumerType.NODE, - callback_node: node => { - if(!this.local_audio_stream || !this.local_audio_mute) - return; - - node.connect(this.local_audio_mute); - }, - callback_disconnect: node => { - if(!this.local_audio_mute) - return; - - node.disconnect(this.local_audio_mute); - } - } as audio.recorder.NodeInputConsumer); - } else { - await recorder.input.set_consumer({ - type: audio.recorder.InputConsumerType.CALLBACK, - callback_audio: buffer => this.handle_local_voice(buffer, false) - } as audio.recorder.CallbackInputConsumer); - } - } - this.connection.client.update_voice_status(undefined); - } - - get_encoder_type() : VoiceEncodeType { return this._type; } - set_encoder_type(target: VoiceEncodeType) { - if(target == this._type) return; - this._type = target; - - if(this._type == VoiceEncodeType.NATIVE_ENCODE) - this.setup_native(); - else - this.setup_js(); - this.start_rtc_session(); - } - - voice_playback_support() : boolean { - return this.dataChannel && this.dataChannel.readyState == "open"; - } - - voice_send_support() : boolean { - if(this._type == VoiceEncodeType.NATIVE_ENCODE) - return audio.js.VoiceConnection.native_encoding_supported() && this.rtcPeerConnection.getLocalStreams().length > 0; - else - return this.voice_playback_support(); - } - - private voice_send_queue: {data: Uint8Array, codec: number}[] = []; - handleEncodedVoicePacket(data: Uint8Array, codec: number){ - this.voice_send_queue.push({data: data, codec: codec}); - } - - private send_next_voice_packet() { - const buffer = this.voice_send_queue.pop_front(); - if(!buffer) - return; - this.send_voice_packet(buffer.data, buffer.codec); - } - - send_voice_packet(encoded_data: Uint8Array, codec: number) { - if(this.dataChannel) { - this.voice_packet_id++; - if(this.voice_packet_id > 65535) - this.voice_packet_id = 0; - - let packet = new Uint8Array(encoded_data.byteLength + 5); - packet[0] = this.chunkVPacketId++ < 5 ? 1 : 0; //Flag header - packet[1] = 0; //Flag fragmented - packet[2] = (this.voice_packet_id >> 8) & 0xFF; //HIGHT (voiceID) - packet[3] = (this.voice_packet_id >> 0) & 0xFF; //LOW (voiceID) - packet[4] = codec; //Codec - packet.set(encoded_data, 5); - try { - this.dataChannel.send(packet); - } catch (error) { - log.warn(LogCategory.VOICE, tr("Failed to send voice packet. Error: %o"), error); - } + this.ownCodec(dummy_client_id, _ => {}).then(codec => { + log.info(LogCategory.VOICE, tr("Release again! (%o)"), codec); + this.releaseCodec(dummy_client_id); + }).catch(error => { + if(this._supported) { + log.warn(LogCategory.VOICE, tr("Disabling codec support for "), this.name); + createErrorModal(tr("Could not load codec driver"), tr("Could not load or initialize codec ") + this.name + "
" + + "Error: " + JSON.stringify(error) + "").open(); + log.error(LogCategory.VOICE, tr("Failed to initialize the opus codec. Error: %o"), error); } else { - log.warn(LogCategory.VOICE, tr("Could not transfer audio (not connected)")); + log.debug(LogCategory.VOICE, tr("Failed to initialize already disabled codec. Error: %o"), error); } - } + this._supported = false; + }); + } - private _audio_player_waiting = false; - start_rtc_session() { - if(!audio.player.initialized()) { - log.info(LogCategory.VOICE, tr("Audio player isn't initialized yet. Waiting for gesture.")); - if(!this._audio_player_waiting) { - this._audio_player_waiting = true; - audio.player.on_ready(() => this.start_rtc_session()); - } + supported() { return this._supported; } + + ownCodec?(clientId: number, callback_encoded: (buffer: Uint8Array) => any, create: boolean = true) : Promise { + return new Promise((resolve, reject) => { + if(!this._supported) { + reject(tr("unsupported codec!")); return; } - if(!this.current_encoding_supported()) - return false; - - if(this._type == VoiceEncodeType.NATIVE_ENCODE) - this.setup_native(); - else - this.setup_js(); - - this.drop_rtp_session(); - this._ice_use_cache = true; - - - let config: RTCConfiguration = {}; - config.iceServers = []; - config.iceServers.push({ urls: 'stun:stun.l.google.com:19302' }); - this.rtcPeerConnection = new RTCPeerConnection(config); - const dataChannelConfig = { ordered: true, maxRetransmits: 0 }; - - this.dataChannel = this.rtcPeerConnection.createDataChannel('main', dataChannelConfig); - this.dataChannel.onmessage = this.on_data_channel_message.bind(this); - this.dataChannel.onopen = this.on_data_channel.bind(this); - this.dataChannel.binaryType = "arraybuffer"; - - let sdpConstraints : RTCOfferOptions = {}; - sdpConstraints.offerToReceiveAudio = this._type == VoiceEncodeType.NATIVE_ENCODE; - sdpConstraints.offerToReceiveVideo = false; - sdpConstraints.voiceActivityDetection = true; - - this.rtcPeerConnection.onicecandidate = this.on_local_ice_candidate.bind(this); - if(this.local_audio_stream) { //May a typecheck? - this.rtcPeerConnection.addStream(this.local_audio_stream.stream); - log.info(LogCategory.VOICE, tr("Adding native audio stream (%o)!"), this.local_audio_stream.stream); - } - - this.rtcPeerConnection.createOffer(sdpConstraints) - .then(offer => this.on_local_offer_created(offer)) - .catch(error => { - log.error(LogCategory.VOICE, tr("Could not create ice offer! error: %o"), error); - }); - } - - drop_rtp_session() { - if(this.dataChannel) { - this.dataChannel.close(); - this.dataChannel = undefined; - } - - if(this.rtcPeerConnection) { - this.rtcPeerConnection.close(); - this.rtcPeerConnection = undefined; - } - - this._ice_use_cache = true; - this._ice_cache = []; - - this.connection.client.update_voice_status(undefined); - } - - private _ice_use_cache: boolean = true; - private _ice_cache: any[] = []; - handleControlPacket(json) { - if(json["request"] === "answer") { - const session_description = new RTCSessionDescription(json["msg"]); - log.info(LogCategory.VOICE, tr("Received answer to our offer. Answer: %o"), session_description); - this.rtcPeerConnection.setRemoteDescription(session_description).then(() => { - log.info(LogCategory.VOICE, tr("Answer applied successfully. Applying ICE candidates (%d)."), this._ice_cache.length); - this._ice_use_cache = false; - for(let msg of this._ice_cache) { - this.rtcPeerConnection.addIceCandidate(new RTCIceCandidate(msg)).catch(error => { - log.info(LogCategory.VOICE, tr("Failed to add remote cached ice candidate %s: %o"), msg, error); + let free_slot = 0; + for(let index = 0; index < this.entries.length; index++) { + if(this.entries[index].owner == clientId) { + this.entries[index].last_access = Date.now(); + if(this.entries[index].instance.initialized()) + resolve(this.entries[index].instance); + else { + this.entries[index].instance.initialise().then((flag) => { + //TODO test success flag + this.ownCodec(clientId, callback_encoded, false).then(resolve).catch(reject); + }).catch(error => { + log.error(LogCategory.VOICE, tr("Could not initialize codec!\nError: %o"), error); + reject(typeof(error) === 'string' ? error : tr("Could not initialize codec!")); }); } - this._ice_cache = []; - }).catch(error => { - log.info(LogCategory.VOICE, tr("Failed to apply remote description: %o"), error); //FIXME error handling! - }); - } else if(json["request"] === "ice") { - if(!this._ice_use_cache) { - log.info(LogCategory.VOICE, tr("Add remote ice! (%o)"), json["msg"]); - this.rtcPeerConnection.addIceCandidate(new RTCIceCandidate(json["msg"])).catch(error => { - log.info(LogCategory.VOICE, tr("Failed to add remote ice candidate %s: %o"), json["msg"], error); - }); - } else { - log.info(LogCategory.VOICE, tr("Cache remote ice! (%o)"), json["msg"]); - this._ice_cache.push(json["msg"]); - } - } else if(json["request"] == "status") { - if(json["state"] == "failed") { - const chandler = this.connection.client; - chandler.log.log(log.server.Type.CONNECTION_VOICE_SETUP_FAILED, { - reason: json["reason"], - reconnect_delay: json["allow_reconnect"] ? 1 : 0 - }); - log.error(LogCategory.NETWORKING, tr("Failed to setup voice bridge (%s). Allow reconnect: %s"), json["reason"], json["allow_reconnect"]); - if(json["allow_reconnect"] == true) { - this.start_rtc_session(); - } - //TODO handle fail specially when its not allowed to reconnect + return; + } else if(this.entries[index].owner == 0) { + free_slot = index; } } + + if(!create) { + resolve(undefined); + return; + } + + if(free_slot == 0){ + free_slot = this.entries.length; + let entry = new CacheEntry(); + entry.instance = new CodecWrapperWorker(this.type); + this.entries.push(entry); + } + this.entries[free_slot].owner = clientId; + this.entries[free_slot].last_access = new Date().getTime(); + this.entries[free_slot].instance.on_encoded_data = callback_encoded; + if(this.entries[free_slot].instance.initialized()) + this.entries[free_slot].instance.reset(); + else { + this.ownCodec(clientId, callback_encoded, false).then(resolve).catch(reject); + return; + } + resolve(this.entries[free_slot].instance); + }); + } + + releaseCodec(clientId: number) { + for(let index = 0; index < this.entries.length; index++) + if(this.entries[index].owner == clientId) this.entries[index].owner = 0; + } + + constructor(index: number, name: string, type: CodecType){ + this.codecIndex = index; + this.name = name; + this.type = type; + + this._supported = this.type !== undefined && codec_supported(this.type); + } + } +} + +export enum VoiceEncodeType { + JS_ENCODE, + NATIVE_ENCODE +} + +export class VoiceConnection extends AbstractVoiceConnection { + readonly connection: ServerConnection; + + rtcPeerConnection: RTCPeerConnection; + dataChannel: RTCDataChannel; + + private _type: VoiceEncodeType = VoiceEncodeType.NATIVE_ENCODE; + + /* + * To ensure we're not sending any audio because the settings activates the input, + * we self mute the audio stream + */ + local_audio_mute: GainNode; + local_audio_stream: MediaStreamAudioDestinationNode; + + static codec_pool: codec.CodecPool[]; + + static codecSupported(type: number) : boolean { + return this.codec_pool && this.codec_pool.length > type && this.codec_pool[type].supported(); + } + + private voice_packet_id: number = 0; + private chunkVPacketId: number = 0; + private send_task: NodeJS.Timer; + + private _audio_source: RecorderProfile; + private _audio_clients: VoiceClientController[] = []; + + private _encoder_codec: number = 5; + + constructor(connection: ServerConnection) { + super(connection); + this.connection = connection; + + this._type = settings.static_global("voice_connection_type", this._type); + } + + destroy() { + clearInterval(this.send_task); + this.drop_rtp_session(); + this.acquire_voice_recorder(undefined, true).catch(error => { + log.warn(LogCategory.VOICE, tr("Failed to release voice recorder: %o"), error); + }).then(() => { + for(const client of this._audio_clients) { + client.abort_replay(); + client.callback_playback = undefined; + client.callback_state_changed = undefined; + client.callback_stopped = undefined; } + this._audio_clients = undefined; + this._audio_source = undefined; + }); + } - private on_local_ice_candidate(event: RTCPeerConnectionIceEvent) { - if (event) { - //if(event.candidate && event.candidate.protocol !== "udp") - // return; + static native_encoding_supported() : boolean { + const context = window.webkitAudioContext || window.AudioContext; + if(!context) + return false; - log.info(LogCategory.VOICE, tr("Gathered local ice candidate %o."), event.candidate); - if(event.candidate) { - this.connection.sendData(JSON.stringify({ - type: 'WebRTC', - request: "ice", - msg: event.candidate, - })); - } else { - this.connection.sendData(JSON.stringify({ - type: 'WebRTC', - request: "ice_finish" - })); + if(!context.prototype.createMediaStreamDestination) + return false; //Required, but not available within edge + + return true; + } + + static javascript_encoding_supported() : boolean { + if(!window.RTCPeerConnection) + return false; + if(!RTCPeerConnection.prototype.createDataChannel) + return false; + return true; + } + + current_encoding_supported() : boolean { + switch (this._type) { + case VoiceEncodeType.JS_ENCODE: + return VoiceConnection.javascript_encoding_supported(); + case VoiceEncodeType.NATIVE_ENCODE: + return VoiceConnection.native_encoding_supported(); + } + return false; + } + + private setup_native() { + log.info(LogCategory.VOICE, tr("Setting up native voice stream!")); + if(!VoiceConnection.native_encoding_supported()) { + log.warn(LogCategory.VOICE, tr("Native codec isn't supported!")); + return; + } + + if(!this.local_audio_stream) { + this.local_audio_stream = aplayer.context().createMediaStreamDestination(); + } + if(!this.local_audio_mute) { + this.local_audio_mute = aplayer.context().createGain(); + this.local_audio_mute.connect(this.local_audio_stream); + this.local_audio_mute.gain.value = 1; + } + } + + private setup_js() { + if(!VoiceConnection.javascript_encoding_supported()) return; + if(!this.send_task) + this.send_task = setInterval(this.send_next_voice_packet.bind(this), 20); /* send all 20ms out voice packets */ + } + + async acquire_voice_recorder(recorder: RecorderProfile | undefined, enforce?: boolean) { + if(this._audio_source === recorder && !enforce) + return; + + if(recorder) + await recorder.unmount(); + + if(this._audio_source) + await this._audio_source.unmount(); + + this.handle_local_voice_ended(); + this._audio_source = recorder; + + if(recorder) { + recorder.current_handler = this.connection.client; + + recorder.callback_unmount = this.on_recorder_yield.bind(this); + recorder.callback_start = this.handle_local_voice_started.bind(this); + recorder.callback_stop = this.handle_local_voice_ended.bind(this); + + if(this._type == VoiceEncodeType.NATIVE_ENCODE) { + if(!this.local_audio_stream) + this.setup_native(); /* requires initialized audio */ + + await recorder.input.set_consumer({ + type: InputConsumerType.NODE, + callback_node: node => { + if(!this.local_audio_stream || !this.local_audio_mute) + return; + + node.connect(this.local_audio_mute); + }, + callback_disconnect: node => { + if(!this.local_audio_mute) + return; + + node.disconnect(this.local_audio_mute); } - } - } - - private on_local_offer_created(localSession) { - log.info(LogCategory.VOICE, tr("Local offer created. Setting up local description. (%o)"), localSession); - this.rtcPeerConnection.setLocalDescription(localSession).then(() => { - log.info(LogCategory.VOICE, tr("Offer applied successfully. Sending offer to server.")); - this.connection.sendData(JSON.stringify({type: 'WebRTC', request: "create", msg: localSession})); - }).catch(error => { - log.info(LogCategory.VOICE, tr("Failed to apply local description: %o"), error); - //FIXME error handling - }); - } - - private on_data_channel(channel) { - log.info(LogCategory.VOICE, tr("Got new data channel! (%s)"), this.dataChannel.readyState); - - this.connection.client.update_voice_status(); - } - - private on_data_channel_message(message: MessageEvent) { - const chandler = this.connection.client; - if(chandler.client_status.output_muted) /* we dont need to do anything with sound playback when we're not listening to it */ - return; - - let bin = new Uint8Array(message.data); - let clientId = bin[2] << 8 | bin[3]; - let packetId = bin[0] << 8 | bin[1]; - let codec = bin[4]; - //log.info(LogCategory.VOICE, "Client id " + clientId + " PacketID " + packetId + " Codec: " + codec); - let client = this.find_client(clientId); - if(!client) { - log.error(LogCategory.VOICE, tr("Having voice from unknown audio client? (ClientID: %o)"), clientId); - return; - } - - let codec_pool = VoiceConnection.codec_pool[codec]; - if(!codec_pool) { - log.error(LogCategory.VOICE, tr("Could not playback codec %o"), codec); - return; - } - - let encodedData; - if(message.data.subarray) - encodedData = message.data.subarray(5); - else encodedData = new Uint8Array(message.data, 5); - - if(encodedData.length == 0) { - client.stopAudio(); - codec_pool.releaseCodec(clientId); - } else { - codec_pool.ownCodec(clientId, e => this.handleEncodedVoicePacket(e, codec), true) - .then(decoder => decoder.decodeSamples(client.get_codec_cache(codec), encodedData)) - .then(buffer => client.playback_buffer(buffer)).catch(error => { - log.error(LogCategory.VOICE, tr("Could not playback client's (%o) audio (%o)"), clientId, error); - if(error instanceof Error) - log.error(LogCategory.VOICE, error.stack); - }); - } - } - - private handle_local_voice(data: AudioBuffer, head: boolean) { - const chandler = this.connection.client; - if(!chandler.connected) - return false; - - if(chandler.client_status.input_muted) - return false; - - if(head) - this.chunkVPacketId = 0; - - let client = this.find_client(chandler.clientId); - if(!client) { - log.error(LogCategory.VOICE, tr("Tried to send voice data, but local client hasn't a voice client handle")); - return; - } - - const codec = this._encoder_codec; - VoiceConnection.codec_pool[codec] - .ownCodec(chandler.getClientId(), e => this.handleEncodedVoicePacket(e, codec), true) - .then(encoder => encoder.encodeSamples(client.get_codec_cache(codec), data)); - } - - private handle_local_voice_ended() { - const chandler = this.connection.client; - const ch = chandler.getClient(); - if(ch) ch.speaking = false; - - if(!chandler.connected) - return false; - if(chandler.client_status.input_muted) - return false; - log.info(LogCategory.VOICE, tr("Local voice ended")); - - if(this.dataChannel && this._encoder_codec >= 0) - this.send_voice_packet(new Uint8Array(0), this._encoder_codec); - } - - private handle_local_voice_started() { - const chandler = this.connection.client; - if(chandler.client_status.input_muted) { - /* evail hack due to the settings :D */ - log.warn(LogCategory.VOICE, tr("Received local voice started event, even thou we're muted! Do not send any voice.")); - if(this.local_audio_mute) - this.local_audio_mute.gain.value = 0; - return; - } - if(this.local_audio_mute) - this.local_audio_mute.gain.value = 1; - log.info(LogCategory.VOICE, tr("Local voice started")); - - const ch = chandler.getClient(); - if(ch) ch.speaking = true; - } - - private on_recorder_yield() { - log.info(LogCategory.VOICE, "Lost recorder!"); - this._audio_source = undefined; - this.acquire_voice_recorder(undefined, true); /* we can ignore the promise because we should finish this directly */ - } - - connected(): boolean { - return typeof(this.dataChannel) !== "undefined" && this.dataChannel.readyState === "open"; - } - - voice_recorder(): RecorderProfile { - return this._audio_source; - } - - available_clients(): connection.voice.VoiceClient[] { - return this._audio_clients; - } - - find_client(client_id: number) : audio.js.VoiceClientController | undefined { - for(const client of this._audio_clients) - if(client.client_id === client_id) - return client; - return undefined; - } - - unregister_client(client: connection.voice.VoiceClient): Promise { - if(!(client instanceof audio.js.VoiceClientController)) - throw "Invalid client type"; - - this._audio_clients.remove(client); - return Promise.resolve(); - } - - register_client(client_id: number): connection.voice.VoiceClient { - const client = new audio.js.VoiceClientController(client_id); - this._audio_clients.push(client); - return client; - } - - decoding_supported(codec: number): boolean { - return VoiceConnection.codecSupported(codec); - } - - encoding_supported(codec: number): boolean { - return VoiceConnection.codecSupported(codec); - } - - get_encoder_codec(): number { - return this._encoder_codec; - } - - set_encoder_codec(codec: number) { - this._encoder_codec = codec; + } as NodeInputConsumer); + } else { + await recorder.input.set_consumer({ + type: InputConsumerType.CALLBACK, + callback_audio: buffer => this.handle_local_voice(buffer, false) + } as CallbackInputConsumer); } } + this.connection.client.update_voice_status(undefined); + } + + get_encoder_type() : VoiceEncodeType { return this._type; } + set_encoder_type(target: VoiceEncodeType) { + if(target == this._type) return; + this._type = target; + + if(this._type == VoiceEncodeType.NATIVE_ENCODE) + this.setup_native(); + else + this.setup_js(); + this.start_rtc_session(); + } + + voice_playback_support() : boolean { + return this.dataChannel && this.dataChannel.readyState == "open"; + } + + voice_send_support() : boolean { + if(this._type == VoiceEncodeType.NATIVE_ENCODE) + return VoiceConnection.native_encoding_supported() && this.rtcPeerConnection.getLocalStreams().length > 0; + else + return this.voice_playback_support(); + } + + private voice_send_queue: {data: Uint8Array, codec: number}[] = []; + handleEncodedVoicePacket(data: Uint8Array, codec: number){ + this.voice_send_queue.push({data: data, codec: codec}); + } + + private send_next_voice_packet() { + const buffer = this.voice_send_queue.pop_front(); + if(!buffer) + return; + this.send_voice_packet(buffer.data, buffer.codec); + } + + send_voice_packet(encoded_data: Uint8Array, codec: number) { + if(this.dataChannel) { + this.voice_packet_id++; + if(this.voice_packet_id > 65535) + this.voice_packet_id = 0; + + let packet = new Uint8Array(encoded_data.byteLength + 5); + packet[0] = this.chunkVPacketId++ < 5 ? 1 : 0; //Flag header + packet[1] = 0; //Flag fragmented + packet[2] = (this.voice_packet_id >> 8) & 0xFF; //HIGHT (voiceID) + packet[3] = (this.voice_packet_id >> 0) & 0xFF; //LOW (voiceID) + packet[4] = codec; //Codec + packet.set(encoded_data, 5); + try { + this.dataChannel.send(packet); + } catch (error) { + log.warn(LogCategory.VOICE, tr("Failed to send voice packet. Error: %o"), error); + } + } else { + log.warn(LogCategory.VOICE, tr("Could not transfer audio (not connected)")); + } + } + + private _audio_player_waiting = false; + start_rtc_session() { + if(!aplayer.initialized()) { + log.info(LogCategory.VOICE, tr("Audio player isn't initialized yet. Waiting for gesture.")); + if(!this._audio_player_waiting) { + this._audio_player_waiting = true; + aplayer.on_ready(() => this.start_rtc_session()); + } + return; + } + + if(!this.current_encoding_supported()) + return false; + + if(this._type == VoiceEncodeType.NATIVE_ENCODE) + this.setup_native(); + else + this.setup_js(); + + this.drop_rtp_session(); + this._ice_use_cache = true; + + + let config: RTCConfiguration = {}; + config.iceServers = []; + config.iceServers.push({ urls: 'stun:stun.l.google.com:19302' }); + this.rtcPeerConnection = new RTCPeerConnection(config); + const dataChannelConfig = { ordered: true, maxRetransmits: 0 }; + + this.dataChannel = this.rtcPeerConnection.createDataChannel('main', dataChannelConfig); + this.dataChannel.onmessage = this.on_data_channel_message.bind(this); + this.dataChannel.onopen = this.on_data_channel.bind(this); + this.dataChannel.binaryType = "arraybuffer"; + + let sdpConstraints : RTCOfferOptions = {}; + sdpConstraints.offerToReceiveAudio = this._type == VoiceEncodeType.NATIVE_ENCODE; + sdpConstraints.offerToReceiveVideo = false; + sdpConstraints.voiceActivityDetection = true; + + this.rtcPeerConnection.onicecandidate = this.on_local_ice_candidate.bind(this); + if(this.local_audio_stream) { //May a typecheck? + this.rtcPeerConnection.addStream(this.local_audio_stream.stream); + log.info(LogCategory.VOICE, tr("Adding native audio stream (%o)!"), this.local_audio_stream.stream); + } + + this.rtcPeerConnection.createOffer(sdpConstraints) + .then(offer => this.on_local_offer_created(offer)) + .catch(error => { + log.error(LogCategory.VOICE, tr("Could not create ice offer! error: %o"), error); + }); + } + + drop_rtp_session() { + if(this.dataChannel) { + this.dataChannel.close(); + this.dataChannel = undefined; + } + + if(this.rtcPeerConnection) { + this.rtcPeerConnection.close(); + this.rtcPeerConnection = undefined; + } + + this._ice_use_cache = true; + this._ice_cache = []; + + this.connection.client.update_voice_status(undefined); + } + + private _ice_use_cache: boolean = true; + private _ice_cache: any[] = []; + handleControlPacket(json) { + if(json["request"] === "answer") { + const session_description = new RTCSessionDescription(json["msg"]); + log.info(LogCategory.VOICE, tr("Received answer to our offer. Answer: %o"), session_description); + this.rtcPeerConnection.setRemoteDescription(session_description).then(() => { + log.info(LogCategory.VOICE, tr("Answer applied successfully. Applying ICE candidates (%d)."), this._ice_cache.length); + this._ice_use_cache = false; + for(let msg of this._ice_cache) { + this.rtcPeerConnection.addIceCandidate(new RTCIceCandidate(msg)).catch(error => { + log.info(LogCategory.VOICE, tr("Failed to add remote cached ice candidate %s: %o"), msg, error); + }); + } + this._ice_cache = []; + }).catch(error => { + log.info(LogCategory.VOICE, tr("Failed to apply remote description: %o"), error); //FIXME error handling! + }); + } else if(json["request"] === "ice") { + if(!this._ice_use_cache) { + log.info(LogCategory.VOICE, tr("Add remote ice! (%o)"), json["msg"]); + this.rtcPeerConnection.addIceCandidate(new RTCIceCandidate(json["msg"])).catch(error => { + log.info(LogCategory.VOICE, tr("Failed to add remote ice candidate %s: %o"), json["msg"], error); + }); + } else { + log.info(LogCategory.VOICE, tr("Cache remote ice! (%o)"), json["msg"]); + this._ice_cache.push(json["msg"]); + } + } else if(json["request"] == "status") { + if(json["state"] == "failed") { + const chandler = this.connection.client; + chandler.log.log(elog.Type.CONNECTION_VOICE_SETUP_FAILED, { + reason: json["reason"], + reconnect_delay: json["allow_reconnect"] ? 1 : 0 + }); + log.error(LogCategory.NETWORKING, tr("Failed to setup voice bridge (%s). Allow reconnect: %s"), json["reason"], json["allow_reconnect"]); + if(json["allow_reconnect"] == true) { + this.start_rtc_session(); + } + //TODO handle fail specially when its not allowed to reconnect + } + } + } + + private on_local_ice_candidate(event: RTCPeerConnectionIceEvent) { + if (event) { + //if(event.candidate && event.candidate.protocol !== "udp") + // return; + + log.info(LogCategory.VOICE, tr("Gathered local ice candidate %o."), event.candidate); + if(event.candidate) { + this.connection.sendData(JSON.stringify({ + type: 'WebRTC', + request: "ice", + msg: event.candidate, + })); + } else { + this.connection.sendData(JSON.stringify({ + type: 'WebRTC', + request: "ice_finish" + })); + } + } + } + + private on_local_offer_created(localSession) { + log.info(LogCategory.VOICE, tr("Local offer created. Setting up local description. (%o)"), localSession); + this.rtcPeerConnection.setLocalDescription(localSession).then(() => { + log.info(LogCategory.VOICE, tr("Offer applied successfully. Sending offer to server.")); + this.connection.sendData(JSON.stringify({type: 'WebRTC', request: "create", msg: localSession})); + }).catch(error => { + log.info(LogCategory.VOICE, tr("Failed to apply local description: %o"), error); + //FIXME error handling + }); + } + + private on_data_channel(channel) { + log.info(LogCategory.VOICE, tr("Got new data channel! (%s)"), this.dataChannel.readyState); + + this.connection.client.update_voice_status(); + } + + private on_data_channel_message(message: MessageEvent) { + const chandler = this.connection.client; + if(chandler.client_status.output_muted) /* we dont need to do anything with sound playback when we're not listening to it */ + return; + + let bin = new Uint8Array(message.data); + let clientId = bin[2] << 8 | bin[3]; + let packetId = bin[0] << 8 | bin[1]; + let codec = bin[4]; + //log.info(LogCategory.VOICE, "Client id " + clientId + " PacketID " + packetId + " Codec: " + codec); + let client = this.find_client(clientId); + if(!client) { + log.error(LogCategory.VOICE, tr("Having voice from unknown audio client? (ClientID: %o)"), clientId); + return; + } + + let codec_pool = VoiceConnection.codec_pool[codec]; + if(!codec_pool) { + log.error(LogCategory.VOICE, tr("Could not playback codec %o"), codec); + return; + } + + let encodedData; + if(message.data.subarray) + encodedData = message.data.subarray(5); + else encodedData = new Uint8Array(message.data, 5); + + if(encodedData.length == 0) { + client.stopAudio(); + codec_pool.releaseCodec(clientId); + } else { + codec_pool.ownCodec(clientId, e => this.handleEncodedVoicePacket(e, codec), true) + .then(decoder => decoder.decodeSamples(client.get_codec_cache(codec), encodedData)) + .then(buffer => client.playback_buffer(buffer)).catch(error => { + log.error(LogCategory.VOICE, tr("Could not playback client's (%o) audio (%o)"), clientId, error); + if(error instanceof Error) + log.error(LogCategory.VOICE, error.stack); + }); + } + } + + private handle_local_voice(data: AudioBuffer, head: boolean) { + const chandler = this.connection.client; + if(!chandler.connected) + return false; + + if(chandler.client_status.input_muted) + return false; + + if(head) + this.chunkVPacketId = 0; + + let client = this.find_client(chandler.clientId); + if(!client) { + log.error(LogCategory.VOICE, tr("Tried to send voice data, but local client hasn't a voice client handle")); + return; + } + + const codec = this._encoder_codec; + VoiceConnection.codec_pool[codec] + .ownCodec(chandler.getClientId(), e => this.handleEncodedVoicePacket(e, codec), true) + .then(encoder => encoder.encodeSamples(client.get_codec_cache(codec), data)); + } + + private handle_local_voice_ended() { + const chandler = this.connection.client; + const ch = chandler.getClient(); + if(ch) ch.speaking = false; + + if(!chandler.connected) + return false; + if(chandler.client_status.input_muted) + return false; + log.info(LogCategory.VOICE, tr("Local voice ended")); + + if(this.dataChannel && this._encoder_codec >= 0) + this.send_voice_packet(new Uint8Array(0), this._encoder_codec); + } + + private handle_local_voice_started() { + const chandler = this.connection.client; + if(chandler.client_status.input_muted) { + /* evail hack due to the settings :D */ + log.warn(LogCategory.VOICE, tr("Received local voice started event, even thou we're muted! Do not send any voice.")); + if(this.local_audio_mute) + this.local_audio_mute.gain.value = 0; + return; + } + if(this.local_audio_mute) + this.local_audio_mute.gain.value = 1; + log.info(LogCategory.VOICE, tr("Local voice started")); + + const ch = chandler.getClient(); + if(ch) ch.speaking = true; + } + + private on_recorder_yield() { + log.info(LogCategory.VOICE, "Lost recorder!"); + this._audio_source = undefined; + this.acquire_voice_recorder(undefined, true); /* we can ignore the promise because we should finish this directly */ + } + + connected(): boolean { + return typeof(this.dataChannel) !== "undefined" && this.dataChannel.readyState === "open"; + } + + voice_recorder(): RecorderProfile { + return this._audio_source; + } + + available_clients(): VoiceClient[] { + return this._audio_clients; + } + + find_client(client_id: number) : VoiceClientController | undefined { + for(const client of this._audio_clients) + if(client.client_id === client_id) + return client; + return undefined; + } + + unregister_client(client: VoiceClient): Promise { + if(!(client instanceof VoiceClientController)) + throw "Invalid client type"; + + this._audio_clients.remove(client); + return Promise.resolve(); + } + + register_client(client_id: number): VoiceClient { + const client = new VoiceClientController(client_id); + this._audio_clients.push(client); + return client; + } + + decoding_supported(codec: number): boolean { + return VoiceConnection.codecSupported(codec); + } + + encoding_supported(codec: number): boolean { + return VoiceConnection.codecSupported(codec); + } + + get_encoder_codec(): number { + return this._encoder_codec; + } + + set_encoder_codec(codec: number) { + this._encoder_codec = codec; } } /* funny fact that typescript dosn't find this */ -interface RTCPeerConnection { - addStream(stream: MediaStream): void; - getLocalStreams(): MediaStream[]; - getStreamById(streamId: string): MediaStream | null; - removeStream(stream: MediaStream): void; - createOffer(successCallback?: RTCSessionDescriptionCallback, failureCallback?: RTCPeerConnectionErrorCallback, options?: RTCOfferOptions): Promise; +declare global { + interface RTCPeerConnection { + addStream(stream: MediaStream): void; + getLocalStreams(): MediaStream[]; + getStreamById(streamId: string): MediaStream | null; + removeStream(stream: MediaStream): void; + createOffer(successCallback?: RTCSessionDescriptionCallback, failureCallback?: RTCPeerConnectionErrorCallback, options?: RTCOfferOptions): Promise; + } } loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { priority: 10, function: async () => { - audio.player.on_ready(() => { + aplayer.on_ready(() => { log.info(LogCategory.VOICE, tr("Initializing voice handler after AudioController has been initialized!")); - audio.js.VoiceConnection.codec_pool = [ - new audio.js.codec.CodecPool(0, tr("Speex Narrowband"), CodecType.SPEEX_NARROWBAND), - new audio.js.codec.CodecPool(1, tr("Speex Wideband"), CodecType.SPEEX_WIDEBAND), - new audio.js.codec.CodecPool(2, tr("Speex Ultra Wideband"), CodecType.SPEEX_ULTRA_WIDEBAND), - new audio.js.codec.CodecPool(3, tr("CELT Mono"), CodecType.CELT_MONO), - new audio.js.codec.CodecPool(4, tr("Opus Voice"), CodecType.OPUS_VOICE), - new audio.js.codec.CodecPool(5, tr("Opus Music"), CodecType.OPUS_MUSIC) + VoiceConnection.codec_pool = [ + new codec.CodecPool(0, tr("Speex Narrowband"), CodecType.SPEEX_NARROWBAND), + new codec.CodecPool(1, tr("Speex Wideband"), CodecType.SPEEX_WIDEBAND), + new codec.CodecPool(2, tr("Speex Ultra Wideband"), CodecType.SPEEX_ULTRA_WIDEBAND), + new codec.CodecPool(3, tr("CELT Mono"), CodecType.CELT_MONO), + new codec.CodecPool(4, tr("Opus Voice"), CodecType.OPUS_VOICE), + new codec.CodecPool(5, tr("Opus Music"), CodecType.OPUS_MUSIC) ]; - audio.js.VoiceConnection.codec_pool[4].initialize(2); - audio.js.VoiceConnection.codec_pool[5].initialize(2); + VoiceConnection.codec_pool[4].initialize(2); + VoiceConnection.codec_pool[5].initialize(2); }); }, name: "registering codec initialisation" diff --git a/webpack.config.js b/webpack.config.js index ca37ea76..34b9cd30 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,16 +1,28 @@ const path = require('path'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const CircularDependencyPlugin = require('circular-dependency-plugin') const isDevelopment = process.env.NODE_ENV === 'development'; module.exports = { - entry: './shared/js/main.ts', + entry: { + //"shared-app": "./shared/js/main.ts" + "shared-app": "./web/js/index.ts" + }, devtool: 'inline-source-map', mode: "development", plugins: [ new MiniCssExtractPlugin({ filename: isDevelopment ? '[name].css' : '[name].[hash].css', chunkFilename: isDevelopment ? '[id].css' : '[id].[hash].css' + }), + /* + new CircularDependencyPlugin({ + //exclude: /a\.js|node_modules/, + failOnError: true, + allowAsyncCycles: false, + cwd: process.cwd(), }) + */ ], module: { rules: [ @@ -42,7 +54,7 @@ module.exports = { { loader: 'ts-loader', options: { - //transpileOnly: true + transpileOnly: true } } ] @@ -54,11 +66,17 @@ module.exports = { alias: { "tc-shared": path.resolve(__dirname, "shared/js"), "tc-backend": path.resolve(__dirname, "web/js") - } + //"tc-backend": path.resolve(__dirname, "shared/backend.d"), + }, + }, + externals: { + "tc-loader": "umd loader" }, output: { - filename: 'bundle.js', + filename: 'shared-app.js', path: path.resolve(__dirname, 'dist'), + libraryTarget: "umd", + library: "shared" }, optimization: { /*