diff --git a/modules/core/main.ts b/modules/core/main.ts index d4cb664..825de35 100644 --- a/modules/core/main.ts +++ b/modules/core/main.ts @@ -1,7 +1,6 @@ // Quit when all windows are closed. import * as electron from "electron"; import * as app_updater from "./app-updater"; -import * as forum from "./teaspeak-forum"; import { app } from "electron"; import MessageBoxOptions = electron.MessageBoxOptions; @@ -9,6 +8,7 @@ import MessageBoxOptions = electron.MessageBoxOptions; import {process_args, parse_arguments, Arguments} from "../shared/process-arguments"; import {open as open_changelog} from "./app-updater/changelog"; import * as crash_handler from "../crash_handler"; +import {open_preview} from "./url-preview"; async function execute_app() { /* legacy, will be removed soon */ @@ -153,20 +153,6 @@ async function execute_app() { } } - forum.setup(); - try { - await forum.initialize(); - }catch(error) { - console.error("Failed to initialize forum connection: %o", error); - const result = electron.dialog.showMessageBox({ - type: "error", - message: "Failed to initialize forum connection\nLookup the console for more info", - title: "Main execution failed!", - buttons: ["close"] - } as MessageBoxOptions); - electron.app.exit(1); - return; - } try { { const version = await app_updater.current_version(); diff --git a/modules/core/main_window.ts b/modules/core/main_window.ts index 09233bc..f3555c7 100644 --- a/modules/core/main_window.ts +++ b/modules/core/main_window.ts @@ -1,6 +1,7 @@ import {BrowserWindow, Menu, MenuItem, MessageBoxOptions, app, dialog} from "electron"; import * as electron from "electron"; import * as winmgr from "./window"; +import * as path from "path"; export let prevent_instant_close: boolean = true; export function set_prevent_instant_close(flag: boolean) { @@ -13,93 +14,12 @@ export let allow_dev_tools: boolean; import {Arguments, parse_arguments, process_args} from "../shared/process-arguments"; import * as updater from "./app-updater"; import * as loader from "./ui-loader"; -import {open as open_changelog} from "./app-updater/changelog"; import * as crash_handler from "../crash_handler"; // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. export let main_window: BrowserWindow = null; -function create_menu() : Menu { - const menu = new Menu(); - - if(allow_dev_tools) { - menu.append(new MenuItem({ - id: "developer-tools", - enabled: true, - label: "Developer", - - submenu: [ - { - id: "tool-dev-tools", - label: "Open developer tools", - enabled: true, - click: event => { - main_window.webContents.openDevTools(); - } - }, - - { - id: "tool-page-reload", - label: "Reload current page", - enabled: true, - click: event => { - main_window.reload(); - } - } - ] - })); - menu.items[0].visible = false; - } - menu.append(new MenuItem({ - id: "help", - enabled: true, - label: "Help", - - submenu: [ - { - id: "update-check", - label: "Check for updates", - click: () => updater.selected_channel().then(channel => updater.execute_graphical(channel, true)) - }, - { - id: "changelog", - label: "View ChangeLog file", - click: open_changelog - }, - { - id: "hr-01", - type: "separator" - }, - { - id: "visit-home", - label: "Visit TeaSpeak.de", - click: () => electron.shell.openExternal("https://teaspeak.de") - }, - { - id: "visit-support", - label: "Get support", - click: () => electron.shell.openExternal("https://forum.teaspeak.de") - }, - { - id: "about-teaclient", - label: "About TeaClient", - click: () => { - updater.current_version().then(version => { - dialog.showMessageBox({ - title: "TeaClient info", - message: "TeaClient by TeaSpeak (WolverinDEV)\nVersion: " + version.toString(true), - buttons: ["close"] - } as MessageBoxOptions, result => {}); - }); - } - }, - ] - })); - - return menu; -} - function spawn_main_window(entry_point: string) { // Create the browser window. console.log("Spawning main window"); @@ -112,17 +32,15 @@ function spawn_main_window(entry_point: string) { nodeIntegrationInWorker: true, nodeIntegration: true }, + icon: path.join(__dirname, "..", "..", "resources", "logo.ico") }); - const menu = create_menu(); - if(menu.items.length > 0) - main_window.setMenu(menu); - main_window.webContents.on('devtools-closed', event => { console.log("Dev tools destroyed!"); }); main_window.on('closed', () => { + require("./url-preview").close(); main_window = null; prevent_instant_close = false; }); @@ -145,21 +63,8 @@ function spawn_main_window(entry_point: string) { main_window.webContents.on('new-window', (event, url, frameName, disposition, options, additionalFeatures) => { console.log("Got new window " + frameName); - if (frameName === 'teaforo-login') { - // open window as modal - Object.assign(options, { - modal: true, - parent: main_window, - width: 100, - height: 100 - }); - - let a = new BrowserWindow(options); - a.show(); - } else { - const url_preview = require("./url-preview"); - url_preview.open_preview(url); - } + const url_preview = require("./url-preview"); + url_preview.open_preview(url); event.preventDefault(); }); @@ -245,6 +150,10 @@ export function execute() { Menu.setApplicationMenu(null); init_listener(); + + console.log("Setting up render backend"); + require("./render-backend"); + console.log("Spawn loading screen"); loader.ui.execute_loader().then(async (entry_point: string) => { /* test if the updater may have an update found */ diff --git a/modules/core/render-backend/index.ts b/modules/core/render-backend/index.ts new file mode 100644 index 0000000..c44c6cf --- /dev/null +++ b/modules/core/render-backend/index.ts @@ -0,0 +1,22 @@ +import "./menu"; + +import * as electron from "electron"; +import ipcMain = electron.ipcMain; +import BrowserWindow = electron.BrowserWindow; + +import {open as open_changelog} from "../app-updater/changelog"; +import * as updater from "../app-updater"; + +ipcMain.on('basic-action', (event, action, ...args: any[]) => { + const window = BrowserWindow.fromWebContents(event.sender); + + if(action === "open-changelog") { + open_changelog(); + } else if(action === "check-native-update") { + updater.selected_channel().then(channel => updater.execute_graphical(channel, true)); + } else if(action === "open-dev-tools") { + window.webContents.openDevTools(); + } else if(action === "reload-window") { + window.reload(); + } +}); diff --git a/modules/core/render-backend/menu.ts b/modules/core/render-backend/menu.ts new file mode 100644 index 0000000..407aa32 --- /dev/null +++ b/modules/core/render-backend/menu.ts @@ -0,0 +1,34 @@ +import * as electron from "electron"; +import ipcMain = electron.ipcMain; +import BrowserWindow = electron.BrowserWindow; + +ipcMain.on('top-menu', (event, menu_template: electron.MenuItemConstructorOptions[]) => { + const window = BrowserWindow.fromWebContents(event.sender); + + const process_template = (item: electron.MenuItemConstructorOptions) => { + if(typeof(item.icon) === "string" && item.icon.startsWith("data:")) + item.icon = electron.nativeImage.createFromDataURL(item.icon); + + item.click = () => window.webContents.send('top-menu', item.id); + for(const i of item.submenu as electron.MenuItemConstructorOptions[] || []) { + process_template(i); + } + }; + + for(const m of menu_template) + process_template(m); + + try { + const menu = new electron.Menu(); + for(const m of menu_template) { + try { + menu.append(new electron.MenuItem(m)); + } catch(error) { + console.error("Failed to build menu entry: %o\nSource: %o", error, m); + } + } + window.setMenu(menu_template.length == 0 ? undefined : menu); + } catch(error) { + console.error("Failed to set window menu: %o", error); + } +}); \ No newline at end of file diff --git a/modules/core/teaspeak-forum/index.ts b/modules/core/teaspeak-forum/index.ts deleted file mode 100644 index c799d88..0000000 --- a/modules/core/teaspeak-forum/index.ts +++ /dev/null @@ -1,138 +0,0 @@ -import * as path from "path"; -import * as electron from "electron"; -import * as fs from "fs-extra"; - -import {BrowserWindow, ipcMain as ipc} from "electron"; -import {Arguments, process_args} from "../../shared/process-arguments"; -import {main_window} from "../main_window"; -import * as winmgr from "../window"; - -export interface UserData { - session_id: string; - username: string; - - application_data: string; - application_data_sign: string; -} - -let current_window: BrowserWindow; -let _current_data: UserData; - -function update_data(data?: UserData) { - _current_data = data; - electron.webContents.getAllWebContents().forEach(content => { - content.send('teaforo-update', data); - }); -} - -function config_file_path() { - return path.join(electron.app.getPath('userData'), "forum_data.json"); -} - -async function load_data() { - try { - const file = config_file_path(); - if((await fs.stat(file)).isFile()) { - const raw_data = await fs.readFile(config_file_path()); - const data = JSON.parse(raw_data.toString()); - update_data(data as UserData); - console.log("Initialized forum account from config!"); - } else { - console.log("Missing forum config file. Ignoring forum auth"); - } - } catch(error) { - console.error("Failed to load forum account connection: %o", error); - } -} - -async function save_data() { - const file = config_file_path(); - try { - await fs.ensureFile(file); - } catch(error) { - console.error("Failed to ensure forum config file as file %o", error); - } - try { - await fs.writeJSON(file, _current_data); - } catch(error) { - console.error("Failed to save forum config: %o", error); - } -} - -export function open_login(enforce: boolean = false) : Promise { - if(_current_data && !enforce) return Promise.resolve(_current_data); - - if(current_window) { - current_window.close(); - current_window = undefined; - } - current_window = new BrowserWindow({ - width: 400, - height: 400, - show: true, - parent: main_window, - webPreferences: { - webSecurity: false, - nodeIntegration: true - }, - }); - current_window.setMenu(null); - winmgr.apply_bounds('forum-manager', current_window); - winmgr.track_bounds('forum-manager', current_window); - console.log("Main: " + main_window); - - current_window.loadFile(path.join(path.dirname(module.filename), "ui", "index.html")); - if(process_args.has_flag(...Arguments.DEV_TOOLS)) - current_window.webContents.openDevTools(); - - return new Promise((resolve, reject) => { - let response = false; - ipc.once("teaforo-callback", (event, data) => { - if(response) return; - response = true; - current_window.close(); - current_window = undefined; - - update_data(data); - save_data(); - if(data) - resolve(data); - else - reject(); - }); - - current_window.on('closed', event => { - if(response) return; - response = true; - - current_window = undefined; - reject(); - }); - }); -} - -export function current_data() : UserData | undefined { - return this._current_data; -} - -export function logout() { - update_data(undefined); - save_data(); -} - -export async function initialize() { - await load_data(); -} - -export function setup() { - ipc.on('teaforo-login', event => { - open_login().catch(error => {}); //TODO may local notify - }); - - ipc.on('teaforo-logout', event => { - logout(); - }); - ipc.on('teaforo-update', event => { - update_data(_current_data); - }); -} \ No newline at end of file diff --git a/modules/core/teaspeak-forum/ui/index.css b/modules/core/teaspeak-forum/ui/index.css deleted file mode 100644 index 13a43f2..0000000 --- a/modules/core/teaspeak-forum/ui/index.css +++ /dev/null @@ -1,99 +0,0 @@ -html { - overflow: visible; -} - -body { - padding: 0; - margin: 0; - overflow: visible; -} - -.inner { - position: absolute; -} - -.inner-container { - width: 400px; - height: 400px; - position: absolute; - top: calc(50vh - 200px); - left: calc(50vw - 200px); - overflow: hidden; -} - -.box { - position: absolute; - height: 100%; - width: 100%; - font-family: Helvetica, serif; - color: #fff; - background: rgba(0, 0, 0, 0.13); - padding: 30px 0px; - text-align: center; -} - -.box h1 { - text-align: center; - margin: 30px 0; - font-size: 30px; -} - -.box input { - display: block; - width: 300px; - margin: 20px auto; - padding: 15px; - background: rgba(0, 0, 0, 0.2); - color: #fff; - border: 0; -} - -.box input:focus, .box input:active, .box button:focus, .box button:active { - outline: none; -} - -.box button { - background: #742ECC; - border: 0; - color: #fff; - padding: 10px; - font-size: 20px; - width: 330px; - margin: 20px auto; - display: block; - cursor: pointer; -} - -.box button:disabled { - background: rgba(0, 0, 0, 0.2); -} - -.box button:active { - background: #27ae60; -} - -.box p { - font-size: 14px; - text-align: center; -} - -.box p span { - cursor: pointer; - color: #666; -} - -.box .error { - color: darkred; - display: none; -} - -#login { - display: block; -} - -#success { - margin-top: 50px; - display: none; -} - -/*# sourceMappingURL=index.css.map */ diff --git a/modules/core/teaspeak-forum/ui/index.css.map b/modules/core/teaspeak-forum/ui/index.css.map deleted file mode 100644 index 9774113..0000000 --- a/modules/core/teaspeak-forum/ui/index.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sourceRoot":"","sources":["index.scss"],"names":[],"mappings":"AAAA;EACI;;;AAEJ;EACI;EACA;EACA;;;AAEJ;EACI;;;AAEJ;EACI;EACA;EACA;EACA;EACA;EACA;;;AAEJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEJ;EACI;EACA;EACA;;;AAEJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEJ;EACI;;;AAEJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEJ;EACI;;;AAEJ;EACI;;;AAEJ;EACI;EACA;;;AAEJ;EACI;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;;;AAEJ;EACI;EACA","file":"index.css"} \ No newline at end of file diff --git a/modules/core/teaspeak-forum/ui/index.html b/modules/core/teaspeak-forum/ui/index.html deleted file mode 100644 index 4eafa08..0000000 --- a/modules/core/teaspeak-forum/ui/index.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - TeaSpeak forum login - - -
-
-

Login

-
- some error code - - - -

Create a account on forum.teaspeak.de

-
- -
-
- - - - \ No newline at end of file diff --git a/modules/core/teaspeak-forum/ui/index.scss b/modules/core/teaspeak-forum/ui/index.scss deleted file mode 100644 index e8f561b..0000000 --- a/modules/core/teaspeak-forum/ui/index.scss +++ /dev/null @@ -1,84 +0,0 @@ -html { - overflow: visible; -} -body{ - padding:0; - margin:0; - overflow: visible; -} -.inner { - position: absolute; -} -.inner-container{ - width:400px; - height:400px; - position:absolute; - top:calc(50vh - 200px); - left:calc(50vw - 200px); - overflow:hidden; -} -.box{ - position:absolute; - height:100%; - width:100%; - font-family: Helvetica, serif; - color:#fff; - background:rgba(0,0,0,0.13); - padding:30px 0px; - text-align: center; -} -.box h1{ - text-align:center; - margin:30px 0; - font-size:30px; -} -.box input{ - display:block; - width:300px; - margin:20px auto; - padding:15px; - background:rgba(0,0,0,0.2); - color:#fff; - border:0; -} -.box input:focus,.box input:active,.box button:focus,.box button:active{ - outline:none; -} -.box button { - background:#742ECC; - border:0; - color:#fff; - padding:10px; - font-size:20px; - width:330px; - margin:20px auto; - display:block; - cursor:pointer; -} -.box button:disabled { - background:rgba(0,0,0,0.2); -} -.box button:active{ - background:#27ae60; -} -.box p{ - font-size:14px; - text-align:center; -} -.box p span{ - cursor:pointer; - color:#666; -} - -.box .error { - color: darkred; - display: none; -} - -#login { - display: block; -} -#success { - margin-top: 50px; - display: none; -} \ No newline at end of file diff --git a/modules/core/teaspeak-forum/ui/index.ts b/modules/core/teaspeak-forum/ui/index.ts deleted file mode 100644 index 639f4e3..0000000 --- a/modules/core/teaspeak-forum/ui/index.ts +++ /dev/null @@ -1,86 +0,0 @@ -import {UserData} from "../index"; - -(window as any).$ = require("jquery"); -{ - const request = require('request'); - const util = require('util'); - const request_post = util.promisify(request.post); - - - const api_url = "https://web.teaspeak.de/"; - - - const btn_login = $("#btn_login"); - btn_login.on('click', () => { - btn_login - .prop("disabled", true) - .empty() - .append($(document.createElement("i")).addClass("fa fa-circle-o-notch fa-spin")); - submit_login($("#user").val() as string, $("#pass").val() as string).then(data => { - $("#login").hide(500); - $("#success").show(500); - - const ipc = require("electron").ipcRenderer; - ipc.send('teaforo-callback', data); - }).catch(error => { - console.log("Failed: " + error); - loginFailed(error); - }); - }); - - async function submit_login(user: string, pass: string) : Promise { - const {error, response, body} = await request_post(api_url + "auth.php", { - timeout: 5000, - form: { - action: "login", - user: user, - pass: pass - } - - }); - console.log("Error: %o", error); - console.log("response: %o", response); - console.log("body: %o", body); - - const data = JSON.parse(body); - if(!data["success"]) throw data["msg"]; - - let user_data: UserData = {} as any; - user_data.session_id = data["sessionId"]; - user_data.username = data["user_name"]; - user_data.application_data = data["user_data"]; - user_data.application_data_sign = data["user_sign"]; - return user_data; - } - - function loginFailed(err: string = "") { - btn_login - .prop("disabled", false) - .empty() - .append($(document.createElement("a")).text("Login")); - - let errTag = $(".box .error"); - if(err !== "") { - errTag.text(err).show(500); - } else errTag.hide(500); - } - - // - - $("#user").on('keydown', event => { - if(event.key == "Enter") $("#pass").focus(); - }); - - $("#pass").on('keydown', event => { - if(event.key == "Enter") $("#btn_login").trigger("click"); - }); - - //Patch for the external URL - $('body').on('click', 'a', (event) => { - event.preventDefault(); - let link = (event.target).href; - require("electron").shell.openExternal(link); - }); -} - - diff --git a/modules/core/url-preview/html/index.css b/modules/core/url-preview/html/index.css new file mode 100644 index 0000000..5bc86f2 --- /dev/null +++ b/modules/core/url-preview/html/index.css @@ -0,0 +1,86 @@ +#nav-body-ctrls { + background-color: #2a2a2a; + padding: 20px; + font-family: arial +} + +#nav-body-tabs { + background: linear-gradient(#2a2a2a 75%, #404040); + height: 36px; + font-family: arial +} + +#nav-body-views { + flex: 1 +} + +.nav-icons { + fill: #fcfcfc !important +} + +.nav-icons:hover { + fill: #c2c2c2 !important +} + +#nav-ctrls-back, #nav-ctrls-forward, #nav-ctrls-reload { + height: 30px; + width: 30px; + margin-right: 10px +} + +#nav-ctrls-url { + box-shadow: 0 0; + border: 0; + border-radius: 2px; + height: 30px !important; + margin-left: 8px; + font-size: 11pt; + outline: none; + padding-left: 10px; + color: #b7b7b7; + background-color: #404040 +} + +#nav-ctrls-url:focus { + color: #fcfcfc; + box-shadow: 0 0 5px #3d3d3d; +} + +#nav-tabs-add { + margin: 5px +} + +.nav-tabs-tab { + border-radius: 2px; + height: 35px +} + +.nav-tabs-tab.active { + background: #404040 +} + +.nav-tabs-favicon { + margin: 6px +} + +.nav-tabs-title { + padding-left: 5px; + font-style: normal; + font-weight: 700; + color: #fcfcfc +} + +.nav-tabs-title:hover { + color: #c2c2c2 +} + +.nav-tabs-close { + width: 20px; + height: 20px; + margin: 6px; + margin-left: 2px +} + +.nav-tabs-close:hover { + fill: #dc143c !important +} \ No newline at end of file diff --git a/modules/core/url-preview/html/index.html b/modules/core/url-preview/html/index.html new file mode 100644 index 0000000..4f3bafa --- /dev/null +++ b/modules/core/url-preview/html/index.html @@ -0,0 +1,34 @@ + + + + TeaClient - URL preview + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/core/url-preview/html/index.ts b/modules/core/url-preview/html/index.ts new file mode 100644 index 0000000..453a147 --- /dev/null +++ b/modules/core/url-preview/html/index.ts @@ -0,0 +1,78 @@ +import * as electron from "electron"; +import * as path from "path"; + +interface Options { + showBackButton: boolean, + showForwardButton: boolean, + showReloadButton: boolean, + showUrlBar: boolean, + showAddTabButton: boolean, + closableTabs: boolean, + verticalTabs: boolean, + defaultFavicons: boolean, + newTabCallback: (url: string, options: any) => any, + changeTabCallback: () => any, + newTabParams: any +} + +interface NewTabOptions { + id: string, + node: boolean, + readonlyUrl: boolean, + contextMenu: boolean, + webviewAttributes: any, + icon: "clean" | "default" | string, + title: "default", + close: boolean +} + +const enav = new (require('electron-navigation'))({ + closableTabs: true, + showAddTabButton: false, + defaultFavicons: true, + + changeTabCallback: new_tab => { + if(new_tab === undefined) + window.close(); + } +} as Options); + +/* Required here: https://github.com/simply-coded/electron-navigation/blob/master/index.js#L364 */ +enav.executeJavaScript = () => {}; /* just to suppress an error cause by the API */ + +let _id_counter = 0; +const execute_preview = (url: string) => { + const id = "preview_" + (++_id_counter); + const tab: HTMLElement & { executeJavaScript(js: string) : Promise } = enav.newTab(url, { + id: id, + contextMenu: false, + readonlyUrl: true, + icon: "default", + webviewAttributes: { + 'preload': path.join(__dirname, "inject.js") + } + } as NewTabOptions); + + /* we only want to preload our script once */ + const show_preview = () => { + tab.removeEventListener("dom-ready", show_preview); + tab.removeAttribute("preload"); + + tab.executeJavaScript('__teaclient_preview_notice()').catch((error) => console.log("Failed to show TeaClient overlay! Error: %o", error)); + }; + + tab.addEventListener("dom-ready", show_preview); + + tab.addEventListener('did-fail-load', (res: any) => { + console.error("Side load failed: %o", res); + if (res.errorCode != -3) { + res.target.executeJavaScript('__teaclient_preview_error("' + res.errorCode + '", "' + encodeURIComponent(res.errorDescription) + '", "' + encodeURIComponent(res.validatedURL) + '")').catch(error => { + console.warn("Failed to show error page: %o", error); + }); + } + }); + + tab.addEventListener('close', () => enav.closeTab(id)); +}; + +electron.ipcRenderer.on('preview', (event, url) => execute_preview(url)); \ No newline at end of file diff --git a/modules/core/url-preview/html/inject.ts b/modules/core/url-preview/html/inject.ts new file mode 100644 index 0000000..804b677 --- /dev/null +++ b/modules/core/url-preview/html/inject.ts @@ -0,0 +1,118 @@ +declare let __teaclient_preview_notice: () => any; +declare let __teaclient_preview_error; + +const electron = require("electron"); +const log_prefix = "[TeaSpeak::Preview] "; + +const html_overlay = +"
" + + "
" + + "
You're in TeaWeb website preview mode. Click here to open the website in the browser
" + + "
" + + "
" + + "" + + "✖" + + "" + + "
" + +"
"; + +let _close_overlay: () => void; +let _inject_overlay = () => { + const element = document.createElement("div"); + element.id = "TeaClient-Overlay-Container"; + document.body.append(element); + element.innerHTML = html_overlay; + + { + _close_overlay = () => { + console.trace(log_prefix + "Closing preview notice"); + element.remove(); + }; + + const buttons = element.getElementsByClassName("button-close"); + if(buttons.length < 1) { + console.warn(log_prefix + "Failed to find close button for preview notice!"); + } else { + for(const button of buttons) { + (button).onclick = _close_overlay; + } + } + } + { + const buttons = element.getElementsByClassName("button-open"); + if(buttons.length < 1) { + console.warn(log_prefix + "Failed to find open button for preview notice!"); + } else { + for(const element of buttons) { + (element).onclick = event => { + console.info(log_prefix + "Opening URL with default browser"); + electron.remote.shell.openExternal(location.href, { + activate: true + }).catch(error => { + console.warn(log_prefix + "Failed to open URL in browser window: %o", error); + }).then(() => { + window.close(); + }); + }; + } + } + } +}; + +/* Put this into the global scope. But we dont leek some nodejs stuff! */ +console.log(log_prefix + "Script loaded waiting to be called!"); +__teaclient_preview_notice = () => { + if(_inject_overlay) { + console.log(log_prefix + "TeaClient overlay called. Showing overlay."); + _inject_overlay(); + } else { + console.warn(log_prefix + "TeaClient overlay called, but overlay method undefined. May an load error occured?"); + } +}; + +const html_error = (error_code, error_desc, url) => +"
" + + "

Oops, this page failed to load correctly.

" + + "

ERROR [ " + error_code + ", " + error_desc + " ]

" + + '

' + + '

Try this

' + + '
  • Check your spelling - "' + url + '".

  • ' + + '
  • Refresh the page.

  • ' + + '
  • Perform a search instead.

  • ' + +"
    "; + +__teaclient_preview_error = (error_code, error_desc, url) => { + document.body.innerHTML = html_error(decodeURIComponent(error_code), decodeURIComponent(error_desc), decodeURIComponent(url)); + _inject_overlay = undefined; + if(_close_overlay) _close_overlay(); +}; \ No newline at end of file diff --git a/modules/core/url-preview/index.ts b/modules/core/url-preview/index.ts index 0d19fa3..79e9c95 100644 --- a/modules/core/url-preview/index.ts +++ b/modules/core/url-preview/index.ts @@ -1,36 +1,85 @@ import * as electron from "electron"; -import * as fs from "fs"; import * as path from "path"; import * as winmgr from "../window"; +let global_window: electron.BrowserWindow; +let global_window_promise: Promise; + +export async function close() { + while(global_window_promise) { + try { + await global_window_promise; + break; + } catch(error) {} /* error will be already logged */ + } + if(global_window) { + global_window.close(); + global_window = undefined; + global_window_promise = undefined; + } +} + export async function open_preview(url: string) { - console.log("Open URL as preview: %s", url); - const window = new electron.BrowserWindow({ - webPreferences: { - webSecurity: true, - nodeIntegration: false, - nodeIntegrationInWorker: false, - allowRunningInsecureContent: false, - }, - skipTaskbar: true, - center: true, - }); - await winmgr.apply_bounds('url-preview', window); - winmgr.track_bounds('url-preview', window); - window.setMenu(null); + while(global_window_promise) { + try { + await global_window_promise; + break; + } catch(error) {} /* error will be already logged */ + } + if(!global_window) { + global_window_promise = (async () => { + global_window = new electron.BrowserWindow({ + webPreferences: { + nodeIntegration: true, + webviewTag: true + }, + center: true, + show: false, + }); + global_window.setMenuBarVisibility(false); + global_window.setMenu(null); + global_window.loadFile(path.join(__dirname, "html", "index.html")).then(() => { + //global_window.webContents.openDevTools(); + }); + global_window.on('close', event => { + global_window = undefined; + }); - window.loadURL(url).then(() => { - //window.webContents.openDevTools(); - }); + try { + await winmgr.apply_bounds('url-preview', global_window); + winmgr.track_bounds('url-preview', global_window); - //FIXME try catch? - const inject_file = path.join(path.dirname(module.filename), "inject.js"); + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject("timeout"), 5000); + global_window.on('ready-to-show', () => { + clearTimeout(timeout); + resolve(); + }); + }); + } catch(error) { + console.warn("Failed to initialize preview window. Dont show preview! Error: %o", error); + throw "failed to initialize"; + } - const code_inject = fs.readFileSync(inject_file).toString(); - window.webContents.once('dom-ready', e => { - const code_inject = fs.readFileSync(inject_file).toString(); - window.webContents.executeJavaScript(code_inject, true); - }); + global_window.show(); + })(); + try { + await global_window_promise; + } catch(error) { + console.log("Failed to create preview window! Error: %o", error); + try { + global_window.close(); + } finally { + global_window = undefined; + } + global_window_promise = undefined; + return; + } + } + + global_window.webContents.send('preview', url); + if(!global_window.isFocused()) + global_window.focus(); } electron.ipcMain.on('preview-action', (event, args) => { diff --git a/modules/core/url-preview/inject.ts b/modules/core/url-preview/inject.ts deleted file mode 100644 index 3e97007..0000000 --- a/modules/core/url-preview/inject.ts +++ /dev/null @@ -1,78 +0,0 @@ -const log_prefix = "[TeaSpeak::Preview] "; - -const object = -"
    " + - "
    " + - "
    You're in TeaWeb website preview mode. Click here to open the website in the browser
    " + - "
    " + - "
    " + - "" + - "✖" + - "" + - "
    " + -"
    "; - -const element = document.createElement("div"); -element.id = "TeaClient-Overlay-Container"; -document.body.append(element); -element.innerHTML = object; - -{ - const buttons = element.getElementsByClassName("button-close"); - if(buttons.length < 1) { - console.warn(log_prefix + "Failed to find close button for preview notice!"); - } else { - for(const button of buttons) { - (button).onclick = event => { - console.trace(log_prefix + "Closing preview notice"); - element.remove(); - }; - } - } -} -{ - const buttons = element.getElementsByClassName("button-open"); - if(buttons.length < 1) { - console.warn(log_prefix + "Failed to find open button for preview notice!"); - } else { - for(const element of buttons) { - (element).onclick = event => { - console.info(log_prefix + "Opening URL with default browser"); - require("electron").ipcRenderer.send('preview-action', { - action: 'open-url', - url: document.documentURI - }); - }; - } - } -} \ No newline at end of file diff --git a/modules/renderer/audio/AudioRecorder.ts b/modules/renderer/audio/AudioRecorder.ts index 6a59d12..16b3594 100644 --- a/modules/renderer/audio/AudioRecorder.ts +++ b/modules/renderer/audio/AudioRecorder.ts @@ -1,7 +1,7 @@ -/// window["require_setup"](module); import {audio as naudio} from "teaclient_connection"; +import {audio, tr} from "../imports/imports_shared"; export namespace _audio.recorder { import InputDevice = audio.recorder.InputDevice; @@ -20,8 +20,9 @@ export namespace _audio.recorder { default_input: e.input_default, supported: e.input_supported, name: e.name, + driver: e.driver, sample_rate: 44100, /* TODO! */ - device_index: e.device_index + device_index: e.device_index, } as NativeDevice })); } @@ -67,7 +68,6 @@ export namespace _audio.recorder { get(): any { return this._callback_level; }, set(v: any): void { - console.log("SET CALLBACK LEVEL! %o", v); if(v === this._callback_level) return; @@ -272,9 +272,25 @@ export namespace _audio.recorder { const device = _device as NativeDevice; /* TODO: test for? */ this._current_device = _device; - this.handle.set_device(device ? device.device_index : -1); try { - this.handle.start(); /* TODO: Test for state! */ + await new Promise((resolve, reject) => { + this.handle.set_device(device ? device.device_index : -1, flag => { + if(typeof(flag) === "boolean" && flag) + resolve(); + else + reject("failed to set device" + (typeof(flag) === "string" ? (": " + flag) : "")); + }); + }); + if(!device) return; + + await new Promise((resolve, reject) => { + this.handle.start(flag => { + if(flag) + resolve(); + else + reject("start failed"); + }); + }); } catch(error) { console.warn(tr("Failed to start playback on new input device (%o)"), error); throw error; @@ -345,7 +361,7 @@ export namespace _audio.recorder { } } - async start(): Promise { + async start(): Promise { try { await this.stop(); } catch(error) { @@ -368,10 +384,18 @@ export namespace _audio.recorder { }; } - this.handle.start(); + await new Promise((resolve, reject) => { + this.handle.start(flag => { + if(flag) + resolve(); + else + reject("start failed"); + }); + }); for(const filter of this.filters) if(filter.is_enabled()) filter.initialize(); + return audio.recorder.InputStartResult.EOK; } catch(error) { this._current_state = audio.recorder.InputState.PAUSED; throw error; @@ -386,6 +410,93 @@ export namespace _audio.recorder { this.callback_end(); this._current_state = audio.recorder.InputState.PAUSED; } + + get_volume(): number { + return this.handle.get_volume(); + } + + set_volume(volume: number) { + this.handle.set_volume(volume); + } + } + + export async function create_levelmeter(device: InputDevice) : Promise { + const meter = new NativeLevelmenter(device as any); + await meter.initialize(); + return meter; + } + + class NativeLevelmenter implements audio.recorder.LevelMenter { + readonly _device: NativeDevice; + + private _callback: (num: number) => any; + private _recorder: naudio.record.AudioRecorder; + private _consumer: naudio.record.AudioConsumer; + private _filter: naudio.record.ThresholdConsumeFilter; + + constructor(device: NativeDevice) { + this._device = device; + } + + async initialize() { + try { + this._recorder = naudio.record.create_recorder(); + this._consumer = this._recorder.create_consumer(); + + this._filter = this._consumer.create_filter_threshold(.5); + this._filter.set_attack_smooth(.75); + this._filter.set_release_smooth(.75); + + await new Promise((resolve, reject) => { + this._recorder.set_device(this._device.device_index, flag => { + if(typeof(flag) === "boolean" && flag) + resolve(); + else + reject("initialize failed" + (typeof(flag) === "string" ? (": " + flag) : "")); + }); + }); + await new Promise((resolve, reject) => { + this._recorder.start(flag => { + if(flag) + resolve(); + else + reject("start failed"); + }); + }); + } catch(error) { + if(typeof(error) === "string") + throw error; + console.warn(tr("Failed to initialize levelmeter for device %o: %o"), this._device, error); + throw "initialize failed (lookup console)"; + } + + /* references this variable, needs a destory() call, else memory leak */ + this._filter.set_analyze_filter(value => { + (this._callback || (() => {}))(value); + }); + } + + destory() { + if(this._filter) { + this._filter.set_analyze_filter(undefined); + this._consumer.unregister_filter(this._filter); + } + if(this._consumer) + this._recorder.delete_consumer(this._consumer); + this._recorder.stop(); + this._recorder.set_device(-1, () => {}); /* -1 := No device */ + this._recorder = undefined; + this._consumer = undefined; + this._filter = undefined; + } + + device(): audio.recorder.InputDevice { + return this._device; + } + + set_observer(callback: (value: number) => any) { + this._callback = callback; + } } } diff --git a/modules/renderer/connection/FileTransfer.ts b/modules/renderer/connection/FileTransfer.ts index 0cccf61..c3c658f 100644 --- a/modules/renderer/connection/FileTransfer.ts +++ b/modules/renderer/connection/FileTransfer.ts @@ -56,7 +56,8 @@ namespace _transfer { status: 200, statusText: "success", headers: { - "X-media-bytes": base64ArrayBuffer(buffer) + "X-media-bytes": base64_encode_ab(buffer) + } })); } diff --git a/modules/renderer/connection/ServerConnection.ts b/modules/renderer/connection/ServerConnection.ts index e6f9339..d6ee3b2 100644 --- a/modules/renderer/connection/ServerConnection.ts +++ b/modules/renderer/connection/ServerConnection.ts @@ -284,6 +284,12 @@ export namespace _connection { }); return this._command_handler_default.proxy_command_promise(promise, options); } + + ping(): { native: number; javascript?: number } { + return { + native: this._native_handle ? (this._native_handle.current_ping() / 1000) : -2 + }; + } } } @@ -299,5 +305,12 @@ export namespace _connection { console.log("Spawning native connection"); return new native.ServerConnection(handle); /* will be overridden by the client */ } + + export function destroy_server_connection(handle: connection.AbstractServerConnection) { + if(!(handle instanceof native.ServerConnection)) + throw "invalid handle"; + //TODO: Here! + console.log("Call to destroy a server connection"); + } } Object.assign(window["connection"] || (window["connection"] = {}), _connection); \ No newline at end of file diff --git a/modules/renderer/context-menu.ts b/modules/renderer/context-menu.ts index ab1a321..6c1805a 100644 --- a/modules/renderer/context-menu.ts +++ b/modules/renderer/context-menu.ts @@ -1,3 +1,5 @@ +import {class_to_image} from "./icon-helper"; + window["require_setup"](module); import * as electron from "electron"; @@ -5,14 +7,11 @@ const remote = electron.remote; const {Menu, MenuItem} = remote; import {isFunction} from "util"; -import NativeImage = electron.NativeImage; class ElectronContextMenu implements contextmenu.ContextMenuProvider { private _close_listeners: (() => any)[] = []; private _current_menu: electron.Menu; - private _icon_mash_url: string; - private _icon_mask_img: NativeImage; private _div: JQuery; despawn_context_menu() { @@ -21,72 +20,22 @@ class ElectronContextMenu implements contextmenu.ContextMenuProvider { this._current_menu.closePopup(); this._current_menu = undefined; - for(const listener of this._close_listeners) - listener(); + for(const listener of this._close_listeners) { + if(listener) { + listener(); + } + } this._close_listeners = []; } finalize() { - this._icon_mask_img = undefined; - this._icon_mash_url = undefined; if(this._div) this._div.detach(); this._div = undefined; - this._cache_klass_map = undefined; } initialize() { - this.initialize_icons(); } - private async initialize_icons() { - if(!this._div) { - this._div = $.spawn("div"); - this._div.css('display', 'none'); - this._div.appendTo(document.body); - } - - const image = new Image(); - image.src = 'img/client_icon_sprite.svg'; - await new Promise((resolve, reject) => { - image.onload = resolve; - image.onerror = reject; - }); - - /* TODO: Get a size! */ - const canvas = document.createElement("canvas"); - canvas.width = 1024; - canvas.height = 1024; - canvas.getContext("2d").drawImage(image, 0, 0); - - this._icon_mash_url = canvas.toDataURL(); - this._icon_mask_img = remote.nativeImage.createFromDataURL(this._icon_mash_url); - } - - - private _cache_klass_map: {[key: string]: NativeImage} = {}; - private class_to_image(klass: string) : NativeImage { - if(!klass || !this._icon_mask_img) - return undefined; - if(this._cache_klass_map[klass]) - return this._cache_klass_map[klass]; - - this._div[0].classList.value = 'icon ' + klass; - const data = window.getComputedStyle(this._div[0]); - - const offset_x = parseInt(data.backgroundPositionX.split(",")[0]); - const offset_y = parseInt(data.backgroundPositionY.split(",")[0]); - - //http://localhost/home/TeaSpeak/Web-Client/web/environment/development/img/client_icon_sprite.svg - //const hight = element.css('height'); - //const width = element.css('width'); - console.log("Offset: x: %o y: %o;", offset_x, offset_y); - return this._cache_klass_map[klass] = this._icon_mask_img.crop({ - height: 16, - width: 16, - x: offset_x == 0 ? 0 : -offset_x, - y: offset_y == 0 ? 0 : -offset_y - }); - } private _entry_id = 0; private build_menu(entry: contextmenu.MenuEntry) : electron.MenuItem { @@ -107,7 +56,7 @@ class ElectronContextMenu implements contextmenu.ContextMenuProvider { label: (isFunction(entry.name) ? (entry.name as (() => string))() : entry.name) as string, type: "normal", click: click_callback, - icon: this.class_to_image(entry.icon_class), + icon: class_to_image(entry.icon_class), visible: entry.visible, enabled: !entry.disabled }); @@ -128,7 +77,7 @@ class ElectronContextMenu implements contextmenu.ContextMenuProvider { type: "checkbox", checked: !!entry.checkbox_checked, click: click_callback, - icon: this.class_to_image(entry.icon_class), + icon: class_to_image(entry.icon_class), visible: entry.visible, enabled: !entry.disabled }); @@ -146,7 +95,7 @@ class ElectronContextMenu implements contextmenu.ContextMenuProvider { type: "submenu", submenu: sub_menu, click: click_callback, - icon: this.class_to_image(entry.icon_class), + icon: class_to_image(entry.icon_class), visible: entry.visible, enabled: !entry.disabled }); diff --git a/modules/renderer/icon-helper.ts b/modules/renderer/icon-helper.ts new file mode 100644 index 0000000..d18f490 --- /dev/null +++ b/modules/renderer/icon-helper.ts @@ -0,0 +1,63 @@ +import * as electron from "electron"; +import NativeImage = electron.NativeImage; + +let _div: JQuery; +let _icon_mash_url: string; +let _icon_mask_img: NativeImage; +let _cache_klass_map: {[key: string]: NativeImage}; + +export function class_to_image(klass: string) : NativeImage { + if(!klass || !_icon_mask_img || !_cache_klass_map) + return undefined; + + if(_cache_klass_map[klass]) + return _cache_klass_map[klass]; + + _div[0].classList.value = 'icon ' + klass; + const data = window.getComputedStyle(_div[0]); + + const offset_x = parseInt(data.backgroundPositionX.split(",")[0]); + const offset_y = parseInt(data.backgroundPositionY.split(",")[0]); + + //http://localhost/home/TeaSpeak/Web-Client/web/environment/development/img/client_icon_sprite.svg + //const hight = element.css('height'); + //const width = element.css('width'); + console.log("Offset: x: %o y: %o;", offset_x, offset_y); + return _cache_klass_map[klass] = _icon_mask_img.crop({ + height: 16, + width: 16, + x: offset_x == 0 ? 0 : -offset_x, + y: offset_y == 0 ? 0 : -offset_y + }); +} + +export async function initialize() { + if(!_div) { + _div = $.spawn("div"); + _div.css('display', 'none'); + _div.appendTo(document.body); + } + + const image = new Image(); + image.src = 'img/client_icon_sprite.svg'; + await new Promise((resolve, reject) => { + image.onload = resolve; + image.onerror = reject; + }); + + /* TODO: Get a size! */ + const canvas = document.createElement("canvas"); + canvas.width = 1024; + canvas.height = 1024; + canvas.getContext("2d").drawImage(image, 0, 0); + + _cache_klass_map = {}; + _icon_mash_url = canvas.toDataURL(); + _icon_mask_img = electron.remote.nativeImage.createFromDataURL(_icon_mash_url); +} + +export function finalize() { + _icon_mask_img = undefined; + _icon_mash_url = undefined; + _cache_klass_map = undefined; +} \ No newline at end of file diff --git a/modules/renderer/imports/.copy_imports_shared.d.ts b/modules/renderer/imports/.copy_imports_shared.d.ts index f88b6f9..f3ac1d9 100644 --- a/modules/renderer/imports/.copy_imports_shared.d.ts +++ b/modules/renderer/imports/.copy_imports_shared.d.ts @@ -11,6 +11,7 @@ declare namespace audio { /* File: shared/js/bookmarks.ts */ declare namespace bookmarks { + export const boorkmak_connect; export interface ServerProperties { server_address: string; server_port: number; @@ -31,6 +32,7 @@ declare namespace bookmarks { default_channel_password_hash?: string; default_channel_password?: string; connect_profile: string; + last_icon_id?: number; } export interface DirectoryBookmark { type: BookmarkType; @@ -39,6 +41,7 @@ declare namespace bookmarks { display_name: string; } export function bookmarks(): DirectoryBookmark; + export function bookmarks_flat(): Bookmark[]; export function find_bookmark(uuid: string): Bookmark | DirectoryBookmark | undefined; export function parent_bookmark(bookmark: Bookmark): DirectoryBookmark; export function create_bookmark(display_name: string, directory: DirectoryBookmark, server_properties: ServerProperties, nickname: string): Bookmark; @@ -46,6 +49,7 @@ declare namespace bookmarks { export function change_directory(parent: DirectoryBookmark, bookmark: Bookmark | DirectoryBookmark); export function save_bookmark(bookmark?: Bookmark | DirectoryBookmark); export function delete_bookmark(bookmark: Bookmark | DirectoryBookmark); + export function add_current_server(); } /* File: shared/js/BrowserIPC.ts */ @@ -210,6 +214,7 @@ declare namespace connection { handleNotifyClientChannelGroupChanged(json); handleNotifyChannelSubscribed(json); handleNotifyChannelUnsubscribed(json); + handleNotifyConversationHistory(json: any[]); } } @@ -263,6 +268,10 @@ declare namespace connection { abstract set onconnectionstatechanged(listener: ConnectionStateListener); abstract remote_address(): ServerAddress; abstract handshake_handler(): HandshakeHandler; + abstract ping(): { + native: number; + javascript?: number; + }; } export namespace voice { export enum PlayerState { @@ -356,7 +365,9 @@ declare enum ErrorID { PERMISSION_ERROR, EMPTY_RESULT, PLAYLIST_IS_IN_USE, - FILE_ALREADY_EXISTS + FILE_ALREADY_EXISTS, + CLIENT_INVALID_ID, + CONVERSATION_MORE_DATA } declare class CommandResult { success: boolean; @@ -497,10 +508,12 @@ declare class ConnectionHandler { fileManager: FileManager; permissions: PermissionManager; groups: GroupManager; + side_bar: chat.Frame; select_info: InfoBar; chat: ChatBox; settings: ServerSettings; sound: sound.SoundManager; + readonly hostbanner: Hostbanner; readonly tag_connection_handler: JQuery; private _clientId: number; private _local_client: LocalClientEntry; @@ -509,9 +522,11 @@ declare class ConnectionHandler { private _connect_initialize_id: number; client_status: VoiceStatus; invoke_resized_on_activate: boolean; + log: log.ServerLog; constructor(); + tab_set_name(name: string); setup(); - startConnection(addr: string, profile: profiles.ConnectionProfile, parameters: ConnectParameters); + startConnection(addr: string, profile: profiles.ConnectionProfile, user_action: boolean, parameters: ConnectParameters); getClient(): LocalClientEntry; getClientId(); // @ts-ignore @@ -534,6 +549,7 @@ declare class ConnectionHandler { resize_elements(); acquire_recorder(voice_recoder: RecorderProfile, update_control_bar: boolean); reconnect_properties(profile?: profiles.ConnectionProfile): ConnectParameters; + update_avatar(); } /* File: shared/js/crypto/asn1.ts */ @@ -674,6 +690,11 @@ declare namespace dns { export function resolve_address(address: string, options?: ResolveOptions): Promise; } +/* File: shared/js/events.ts */ +declare namespace event { + namespace global { } +} + /* File: shared/js/FileManager.ts */ declare class FileEntry { name: string; @@ -726,7 +747,7 @@ declare class RequestFileDownload implements transfer.DownloadTransfer { declare class RequestFileUpload { readonly transfer_key: transfer.UploadKey; constructor(key: transfer.DownloadKey); - put_data(data: BufferSource | File); + put_data(data: BlobPart | File); try_put(data: FormData, url: string): Promise; } declare class FileManager extends connection.AbstractCommandHandler { @@ -766,7 +787,7 @@ declare enum ImageType { JPEG } declare function media_image_type(type: ImageType, file?: boolean); -declare function image_type(base64: string | ArrayBuffer); +declare function image_type(encoded_data: string | ArrayBuffer, base64_encoded?: boolean); declare class CacheManager { readonly cache_name: string; private _cache_category: Cache; @@ -794,11 +815,21 @@ declare class IconManager { delete_icon(id: number): Promise; iconList(): Promise; create_icon_download(id: number): Promise; - private _response_url(response: Response); + private static _response_url(response: Response); resolved_cached?(id: number): Promise; + private static _static_id_url: { + [icon: number]: string; + }; + private static _static_cached_promise: { + [icon: number]: Promise; + }; + static load_cached_icon(id: number, ignore_age?: boolean): Promise | Icon; private _load_icon(id: number): Promise; download_icon(id: number): Promise; resolve_icon(id: number): Promise; + static generate_tag(icon: Promise | Icon, options?: { + animate?: boolean; + }): JQuery; generateTag(id: number, options?: { animate?: boolean; }): JQuery; @@ -829,6 +860,12 @@ declare class AvatarManager { callback_image?: (tag: JQuery) => any; callback_avatar?: (avatar: Avatar) => any; }): JQuery; + unique_id_2_avatar_id(unique_id: string); + private generate_default_image(): JQuery; + generate_chat_tag(client: { + id?: number; + database_id?: number; + }, client_unique_id: string, callback_loaded?: (successfully: boolean, error?: any) => any): JQuery; } /* File: shared/js/i18n/country.ts */ @@ -939,7 +976,11 @@ declare namespace loader { export function finished(); export function register_task(stage: Stage, task: Task); export function execute(): Promise; - type SourcePath = string | string[]; + type DependSource = { + url: string; + depends: string[]; + }; + type SourcePath = string | DependSource | string[]; export class SyntaxError { source: any; constructor(source: any); @@ -1471,7 +1512,6 @@ declare class PermissionValue { hasGrant(): boolean; } declare class NeededPermissionValue extends PermissionValue { - changeListener: ((newValue: number) => void)[]; constructor(type, value); } declare class ChannelPermissionRequest { @@ -1491,6 +1531,9 @@ declare class PermissionManager extends connection.AbstractCommandHandler { permissionList: PermissionInfo[]; permissionGroups: PermissionGroup[]; neededPermissions: NeededPermissionValue[]; + needed_permission_change_listener: { + [permission: string]: (() => any)[]; + }; requests_channel_permissions: ChannelPermissionRequest[]; requests_client_permissions: TeaPermissionRequest[]; requests_client_channel_permissions: TeaPermissionRequest[]; @@ -1509,6 +1552,7 @@ declare class PermissionManager extends connection.AbstractCommandHandler { public requestPermissionList(); private onPermissionList(json); private onNeededPermissions(json); + register_needed_permission(key: PermissionType, listener: () => any); private onChannelPermList(json); resolveInfo?(key: number | string | PermissionType): PermissionInfo; requestChannelPermissions(channelId: number): Promise; @@ -1517,7 +1561,7 @@ declare class PermissionManager extends connection.AbstractCommandHandler { requestClientChannelPermissions(client_id: number, channel_id: number): Promise; private onPlaylistPermList(json: any[]); requestPlaylistPermissions(playlist_id: number): Promise; - neededPermission(key: number | string | PermissionType | PermissionInfo): PermissionValue; + neededPermission(key: number | string | PermissionType | PermissionInfo): NeededPermissionValue; groupedPermissions(): GroupedPermissions[]; export_permission_types(); } @@ -1889,6 +1933,10 @@ declare interface String { declare function concatenate(resultConstructor, ...arrays); declare function formatDate(secs: number): string; declare function calculate_width(text: string): number; +declare interface Twemoji { + parse(message: string): string; +} +declare let twemoji: Twemoji; declare class webkitAudioContext extends AudioContext { } declare class webkitOfflineAudioContext extends OfflineAudioContext { @@ -1901,6 +1949,7 @@ declare interface Window { readonly RTCPeerConnection: typeof RTCPeerConnection; readonly Pointer_stringify: any; readonly jsrender: any; + twemoji: Twemoji; require(id: string): any; } declare interface Navigator { @@ -1919,6 +1968,7 @@ declare interface SettingsKey { }; description?: string; default_value?: T; + require_restart?: boolean; } declare class SettingsBase { protected static readonly UPDATE_DIRECT: boolean; @@ -1941,6 +1991,7 @@ declare class StaticSettings extends SettingsBase { declare class Settings extends StaticSettings { static readonly KEY_DISABLE_COSMETIC_SLOWDOWN: SettingsKey; static readonly KEY_DISABLE_CONTEXT_MENU: SettingsKey; + static readonly KEY_DISABLE_GLOBAL_CONTEXT_MENU: SettingsKey; static readonly KEY_DISABLE_UNLOAD_DIALOG: SettingsKey; static readonly KEY_DISABLE_VOICE: SettingsKey; static readonly KEY_DISABLE_MULTI_SESSION: SettingsKey; @@ -1955,9 +2006,12 @@ declare class Settings extends StaticSettings { static readonly KEY_CONNECT_USERNAME: SettingsKey; static readonly KEY_CONNECT_PASSWORD: SettingsKey; static readonly KEY_FLAG_CONNECT_PASSWORD: SettingsKey; + static readonly KEY_CONNECT_HISTORY: SettingsKey; static readonly KEY_CERTIFICATE_CALLBACK: SettingsKey; static readonly KEY_SOUND_MASTER: SettingsKey; static readonly KEY_SOUND_MASTER_SOUNDS: SettingsKey; + static readonly KEY_CHAT_FIXED_TIMESTAMPS: SettingsKey; + static readonly KEY_CHAT_COLLOQUIAL_TIMESTAMPS: SettingsKey; static readonly FN_SERVER_CHANNEL_SUBSCRIBE_MODE: (channel: ChannelEntry) => SettingsKey; static readonly FN_PROFILE_RECORD: (name: string) => SettingsKey; static readonly KEYS; @@ -1988,6 +2042,10 @@ declare enum Sound { SOUND_EGG, AWAY_ACTIVATED, AWAY_DEACTIVATED, + MICROPHONE_MUTED, + MICROPHONE_ACTIVATED, + SOUND_MUTED, + SOUND_ACTIVATED, CONNECTION_CONNECTED, CONNECTION_DISCONNECTED, CONNECTION_BANNED, @@ -2208,6 +2266,7 @@ declare class ChannelEntry { get subscribe_mode(): ChannelSubscribeMode; // @ts-ignore set subscribe_mode(mode: ChannelSubscribeMode); + log_data(): log.server.base.Channel; } /* File: shared/js/ui/client_move.ts */ @@ -2261,6 +2320,7 @@ declare class ClientProperties { client_icon_id: number; client_away_message: string; client_away: boolean; + client_country: string; client_input_hardware: boolean; client_output_hardware: boolean; client_input_muted: boolean; @@ -2279,6 +2339,8 @@ declare class ClientEntry { protected _speaking: boolean; protected _listener_initialized: boolean; protected _audio_handle: connection.voice.VoiceClient; + protected _audio_volume: number; + protected _audio_muted: boolean; channelTree: ChannelTree; constructor(clientId: number, clientName, properties?: ClientProperties); set_audio_handle(handle: connection.voice.VoiceClient); @@ -2289,8 +2351,11 @@ declare class ClientEntry { clientNickName(); clientUid(); clientId(); + is_muted(); + set_muted(flag: boolean, update_icon: boolean, force?: boolean); protected initializeListener(); protected assignment_context(): contextmenu.MenuEntry[]; + open_text_chat(); showContextMenu(x: number, y: number, on_close?: () => void); // @ts-ignore get tag(): JQuery; @@ -2309,11 +2374,9 @@ declare class ClientEntry { }[]); update_displayed_client_groups(); updateClientVariables(); - private chat_name(); - chat(create?: boolean): ChatEntry; - initialize_chat(handle?: ChatEntry); updateClientIcon(); updateGroupIcon(group: Group); + update_group_icon_order(); assignedServerGroupIds(): number[]; assignedChannelGroup(): number; groupAssigned(group: Group): boolean; @@ -2321,6 +2384,7 @@ declare class ClientEntry { calculateOnlineTime(): number; avatarId?(): string; update_family_index(); + log_data(): log.server.base.Client; } declare class LocalClientEntry extends ClientEntry { handle: ConnectionHandler; @@ -2445,6 +2509,9 @@ declare class ModalProperties { trigger_tab: boolean; full_size?: boolean; } +declare let _global_modal_count; +declare let _global_modal_last: HTMLElement; +declare let _global_modal_last_time: number; declare class Modal { private _htmlTag: JQuery; properties: ModalProperties; @@ -2488,6 +2555,302 @@ declare interface JQuery { } declare var TabFunctions; +/* File: shared/js/ui/frames/chat_frame.ts */ +declare namespace chat { + export enum InfoFrameMode { + NONE, + CHANNEL_CHAT, + PRIVATE_CHAT, + CLIENT_INFO + } + export class InfoFrame { + private readonly handle: Frame; + private _html_tag: JQuery; + private _mode: InfoFrameMode; + private _value_ping: JQuery; + private _ping_updater: number; + constructor(handle: Frame); + html_tag(): JQuery; + private _build_html_tag(); + update_ping(); + update_channel_talk(); + update_channel_text(); + update_chat_counter(); + current_mode(): InfoFrameMode; + set_mode(mode: InfoFrameMode); + } + export class ChatBox { + private _html_tag: JQuery; + private _html_input: JQuery; + private _enabled: boolean; + private __callback_text_changed; + private __callback_key_down; + private __callback_paste; + callback_text: (text: string) => any; + constructor(); + html_tag(): JQuery; + private _initialize_listener(); + private _build_html_tag(); + private _callback_text_changed(event); + private _text(element: HTMLElement); + private htmlEscape(message: string): string; + private _callback_paste(event: ClipboardEvent); + private _callback_key_down(event: KeyboardEvent); + set_enabled(flag: boolean); + is_enabled(); + focus_input(); + } + export namespace helpers { + export function process_urls(message: string): Promise; + export namespace history { + export function load_history(key: string): Promise; + export function save_history(key: string, value: any): Promise; + } + export namespace date { + export function same_day(a: number | Date, b: number | Date); + } + } + export namespace format { + export namespace date { + export enum ColloquialFormat { + YESTERDAY, + TODAY, + GENERAL + } + export function date_format(date: Date, now: Date, ignore_settings?: boolean): ColloquialFormat; + export function format_date_general(date: Date, hours?: boolean): string; + export function format_date_colloquial(date: Date, current_timestamp: Date): { + result: string; + format: ColloquialFormat; + }; + export function format_chat_time(date: Date): { + result: string; + next_update: number; + }; + } + export namespace time { + export function format_online_time(secs: number): string; + } + } + type PrivateConversationViewEntry = { + html_tag: JQuery; + }; + type PrivateConversationMessageData = { + message_id: string; + message: string; + sender: | ; + sender_name: string; + sender_unique_id: string; + sender_client_id: number; + timestamp: number; + }; + type PrivateConversationViewMessage = PrivateConversationMessageData & PrivateConversationViewEntry & { + time_update_id: number; + }; + type PrivateConversationViewSpacer = PrivateConversationViewEntry; + export enum PrivateConversationState { + OPEN, + CLOSED, + DISCONNECTED + } + type DisplayedMessage = { + timestamp: number; + message: PrivateConversationViewMessage | PrivateConversationViewEntry; + message_type: | ; + 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; + _scroll_position: number | undefined; + _html_message_container: JQuery; + client_unique_id: string; + client_id: number; + client_name: string; + private _displayed_messages: DisplayedMessage[]; + private _displayed_messages_length: number; + private _spacer_unread_message: DisplayedMessage; + constructor(handle: PrivateConverations, client_unique_id: string, client_name: string, client_id: number); + private history_key(); + private load_history(); + private save_history(); + entry_tag(): JQuery; + private _2d_flat(array: T[][]): T[]; + messages_tags(): JQuery[]; + append_message(message: string, sender: { + type: | ; + name: string; + unique_id: string; + client_id: number; + }, timestamp?: Date, save_history?: boolean); + private _displayed_message_first_tag(message: DisplayedMessage); + private _destroy_displayed_message(message: DisplayedMessage, update_pointers: boolean); + clear_messages(save?: boolean); + fix_scroll(animate: boolean); + private _update_message_timestamp(); + private _destroy_message(message: PrivateConversationViewMessage); + private _build_message(message: PrivateConversationMessageData): PrivateConversationViewMessage; + private _build_spacer(message: string, type: | | | | | ): PrivateConversationViewSpacer; + private _register_displayed_message(message: DisplayedMessage); + private _destory_view_entry(entry: PrivateConversationViewEntry); + private _build_entry_tag(); + close_conversation(); + set_client_name(name: string); + set_unread_flag(flag: boolean, update_chat_counter?: boolean); + is_unread(): boolean; + private _append_state_change(state: | | ); + state(): PrivateConversationState; + set_state(state: PrivateConversationState); + set_text_callback(callback: (text: string) => any, update_enabled_state?: boolean); + chat_enabled(); + append_error(message: string, date?: number); + call_message(message: string); + } + 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 _html_no_chats: JQuery; + private _conversations: PrivateConveration[]; + private _current_conversation: PrivateConveration; + private _select_read_timer: number; + constructor(handle: Frame); + html_tag(): JQuery; + conversations(): PrivateConveration[]; + create_conversation(client_uid: string, client_name: string, client_id: number): PrivateConveration; + delete_conversation(conv: PrivateConveration, update_chat_couner?: boolean); + find_conversation(partner: { + name: string; + unique_id: string; + client_id: number; + }, mode: { + create: boolean; + attach: boolean; + }): PrivateConveration | undefined; + clear_conversations(); + set_selected_conversation(conv: PrivateConveration | undefined); + update_chatbox_state(); + private _build_html_tag(); + try_input_focus(); + } + export namespace channel { + 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 _html_tag: JQuery; + private _container_messages: JQuery; + private _container_new_message: JQuery; + private _container_no_permissions: JQuery; + private _view_max_messages; + private _view_older_messages: ViewEntry; + private _has_older_messages: boolean; + private _view_entries: ViewEntry[]; + private _last_messages: MessageData[]; + private _last_messages_timestamp: number; + private _first_unread_message: Message; + private _first_unread_message_pointer: ViewEntry; + private _scroll_position: number | undefined; + constructor(handle: ConversationManager, channel_id: number); + html_tag(): JQuery; + destroy(); + private _build_html_tag(); + mark_read(); + private _mark_read(); + private _generate_view_message(data: MessageData): Message; + private _generate_view_spacer(message: string, type: | | | ): ViewEntry; + last_messages_timestamp(): number; + fetch_last_messages(); + fetch_older_messages(); + register_new_message(message: MessageData); + fix_scroll(animate: boolean); + fix_view_size(); + chat_available(): boolean; + text_send_failed(error: CommandResult | any); + } + 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; + constructor(handle: Frame); + initialize_needed_listener(); + html_tag(): JQuery; + update_chat_box(); + private _build_html_tag(); + set_current_channel(channel_id: number, update_info_frame?: boolean); + current_channel(): number; + delete_conversation(channel_id: number); + reset(); + conversation(channel_id: number): Conversation; + on_show(); + } + } + export class ClientInfo { + readonly handle: Frame; + private _html_tag: JQuery; + private _current_client: ClientEntry | undefined; + private _online_time_updater: number; + previous_frame_content: FrameContent; + constructor(handle: Frame); + html_tag(): JQuery; + private _build_html_tag(); + current_client(): ClientEntry; + set_current_client(client: ClientEntry | undefined, enforce?: boolean); + } + export enum FrameContent { + NONE, + PRIVATE_CHAT, + CHANNEL_CHAT, + CLIENT_INFO + } + 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 _channel_conversations: channel.ConversationManager; + constructor(handle: ConnectionHandler); + html_tag(): JQuery; + info_frame(): InfoFrame; + private _build_html_tag(); + private_conversations(): PrivateConverations; + channel_conversations(): channel.ConversationManager; + client_info(): ClientInfo; + private _clear(); + show_private_conversations(); + show_channel_conversations(); + show_client_info(client: ClientEntry); + set_content(type: FrameContent); + } +} + /* File: shared/js/ui/frames/chat.ts */ declare enum ChatType { GENERAL, @@ -2576,9 +2939,11 @@ declare let server_connections: ServerConnectionManager; declare class ServerConnectionManager { private connection_handlers: ConnectionHandler[]; private active_handler: ConnectionHandler | undefined; + private _container_log_server: JQuery; private _container_channel_tree: JQuery; + private _container_hostbanner: JQuery; private _container_select_info: JQuery; - private _container_chat_box: JQuery; + private _container_chat: JQuery; private _tag: JQuery; private _tag_connection_entries: JQuery; private _tag_buttons_scoll: JQuery; @@ -2609,11 +2974,13 @@ declare class ControlBar { private _button_subscribe_all: boolean; private _button_query_visible: boolean; private connection_handler: ConnectionHandler | undefined; + private _button_hostbanner: JQuery; htmlTag: JQuery; constructor(htmlTag: JQuery); initialize_connection_handler_state(handler?: ConnectionHandler); set_connection_handler(handler?: ConnectionHandler); apply_server_state(); + apply_server_hostbutton(); apply_server_voice_state(); current_connection_handler(); initialise(); @@ -2641,6 +3008,7 @@ declare class ControlBar { private on_toggle_query_view(); private on_open_settings(); private on_open_connect(); + private on_open_connect_new_tab(); update_connection_state(); private on_execute_disconnect(); private on_token_use(); @@ -2656,6 +3024,94 @@ declare class ControlBar { private on_open_playlist_manage(); } +/* File: shared/js/ui/frames/hostbanner.ts */ +declare class Hostbanner { + readonly html_tag: JQuery; + readonly client: ConnectionHandler; + private updater: NodeJS.Timer; + constructor(client: ConnectionHandler); + update(); + private generate_tag?(): Promise; +} + +/* File: shared/js/ui/frames/MenuBar.ts */ +declare namespace top_menu { + export interface HRItem { + } + export interface MenuItem { + append_item(label: string): MenuItem; + append_hr(): HRItem; + delete_item(item: MenuItem | HRItem); + items(): (MenuItem | 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 MenuBarDriver { + initialize(); + append_item(label: string): MenuItem; + delete_item(item: MenuItem); + items(): MenuItem[]; + flush_changes(); + } + export function driver(): MenuBarDriver; + export function set_driver(driver: MenuBarDriver); + export interface NativeActions { + open_dev_tools(); + reload_page(); + check_native_update(); + open_change_log(); + quit(); + } + export let native_actions: NativeActions; + namespace html { + export class HTMLHrItem implements top_menu.HRItem { + readonly html_tag: JQuery; + constructor(); + } + export 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; + private _items: (MenuItem | HRItem)[]; + private _label: string; + private _callback_click: () => any; + constructor(label: string, mode: | ); + append_item(label: string): top_menu.MenuItem; + append_hr(): HRItem; + delete_item(item: top_menu.MenuItem | top_menu.HRItem); + disabled(value?: boolean): boolean; + items(): (top_menu.MenuItem | top_menu.HRItem)[]; + label(value?: string): string; + visible(value?: boolean): boolean; + click(callback: () => any): this; + icon(klass?: string | Promise | Icon): string; + } + export class HTMLMenuBarDriver implements MenuBarDriver { + private static _instance: HTMLMenuBarDriver; + public static instance(): HTMLMenuBarDriver; + readonly html_tag: JQuery; + private _items: MenuItem[]; + constructor(); + append_item(label: string): top_menu.MenuItem; + delete_item(item: MenuItem); + items(): top_menu.MenuItem[]; + flush_changes(); + initialize(); + } + } + export function rebuild_bookmarks(); + export function update_state(); + namespace native { + export function initialize(); + } + export function initialize(); +} + /* File: shared/js/ui/frames/SelectedItemInfo.ts */ declare abstract class InfoManagerBase { private timers: NodeJS.Timer[]; @@ -2681,7 +3137,6 @@ declare class InfoBar; private _current_manager: InfoManagerBase; private managers: InfoManagerBase[]; - private banner_manager: Hostbanner; constructor(client: ConnectionHandler); get_tag(): JQuery; handle_resize(); @@ -2689,7 +3144,6 @@ declare class InfoBar; - readonly client: ConnectionHandler; - private updater: NodeJS.Timer; - private _hostbanner_url: string; - constructor(client: ConnectionHandler, htmlTag: JQuery); - update(); - handle_resize(); - private generate_tag?(): Promise>; -} declare class ClientInfoManager extends InfoManager { available(object: V): boolean; createFrame<_>(handle: InfoBar<_>, client: ClientEntry, html_tag: JQuery); @@ -2744,6 +3188,202 @@ declare class MusicInfoManager extends ClientInfoManager { finalizeFrame(object: ClientEntry, frame: JQuery); } +/* File: shared/js/ui/frames/server_log.ts */ +declare namespace log { + export namespace server { + export enum Type { + CONNECTION_BEGIN, + CONNECTION_HOSTNAME_RESOLVE, + CONNECTION_HOSTNAME_RESOLVE_ERROR, + CONNECTION_HOSTNAME_RESOLVED, + CONNECTION_LOGIN, + CONNECTION_CONNECTED, + CONNECTION_FAILED, + CONNECTION_VOICE_SETUP_FAILED, + GLOBAL_MESSAGE, + SERVER_WELCOME_MESSAGE, + SERVER_HOST_MESSAGE, + CLIENT_VIEW_ENTER, + CLIENT_VIEW_LEAVE, + CLIENT_VIEW_MOVE, + CLIENT_NICKNAME_CHANGED, + CLIENT_NICKNAME_CHANGE_FAILED, + CLIENT_SERVER_GROUP_ADD, + CLIENT_SERVER_GROUP_REMOVE, + CLIENT_CHANNEL_GROUP_CHANGE, + CHANNEL_CREATE, + CHANNEL_DELETE, + ERROR_CUSTOM, + ERROR_PERMISSION, + RECONNECT_SCHEDULED, + RECONNECT_EXECUTE, + RECONNECT_CANCELED + } + 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 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 type ReconnectScheduled = { + timeout: number; + }; + export type ReconnectCanceled = {}; + export type ReconnectExecute = {}; + export type ErrorPermission = { + permission: PermissionInfo; + }; + export type WelcomeMessage = { + 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; + }; + export type ClientNicknameChanged = { + own_client: boolean; + client: base.Client; + old_name: string; + new_name: string; + }; + export type ClientNicknameChangeFailed = { + reason: string; + }; + } + export type LogMessage = { + type: Type; + timestamp: number; + data: any; + }; + export interface TypeInfo { + : event.ConnectBegin; + : event.GlobalMessage; + : event.ErrorCustom; + : event.ErrorPermission; + : event.ConnectionHostnameResolved; + : event.ConnectionHostnameResolve; + : event.ConnectionHostnameResolveError; + : event.ConnectionFailed; + : event.ConnectionLogin; + : event.ConnectionConnected; + : event.ConnectionVoiceSetupFailed; + : event.ReconnectScheduled; + : event.ReconnectCanceled; + : event.ReconnectExecute; + : event.WelcomeMessage; + : event.WelcomeMessage; + : event.ClientEnter; + : event.ClientMove; + : event.ClientLeave; + : event.ClientNicknameChangeFailed; + : event.ClientNicknameChanged; + : event.ChannelCreate; + : event.ChannelDelete; + } + type MessageBuilderOptions = {}; + type MessageBuilder = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | undefined; + export const MessageBuilders: { + [key: string]: MessageBuilder; + }; + } + export class ServerLog { + private readonly handle: ConnectionHandler; + private history_length: number; + private _log: server.LogMessage[]; + private _html_tag: JQuery; + private _log_container: JQuery; + private auto_follow: boolean; + private _ignore_event: number; + constructor(handle: ConnectionHandler); + log(type: T, data: server.TypeInfo[T]); + html_tag(): JQuery; + private append_log(message: server.LogMessage); + } +} +declare namespace log { + export namespace server { + namespace impl { } + } +} + /* File: shared/js/ui/htmltags.ts */ declare namespace htmltags { export interface ClientProperties { @@ -2759,7 +3399,9 @@ declare namespace htmltags { add_braces?: boolean; } export function generate_client(properties: ClientProperties): string; + export function generate_client_object(properties: ClientProperties): JQuery; export function generate_channel(properties: ChannelProperties): string; + export function generate_channel_object(properties: ChannelProperties): JQuery; export namespace callbacks { export function callback_context_client(element: JQuery); export function callback_context_channel(element: JQuery); @@ -2767,6 +3409,16 @@ declare namespace htmltags { namespace bbcodes { } } +/* File: shared/js/ui/modal/ModalAbout.ts */ +declare namespace Modals { + export function spawnAbout(); +} + +/* File: shared/js/ui/modal/ModalAvatar.ts */ +declare namespace Modals { + export function spawnAvatarUpload(callback_data: (data: ArrayBuffer | undefined | null) => any); +} + /* File: shared/js/ui/modal/ModalAvatarList.ts */ declare namespace Modals { export const human_file_size; @@ -2831,8 +3483,47 @@ declare namespace Modals { } /* File: shared/js/ui/modal/ModalConnect.ts */ +declare namespace connection_log { + export type ConnectionData = { + name: string; + icon_id: number; + country: string; + clients_online: number; + clients_total: number; + flag_password: boolean; + password_hash: string; + }; + export type ConnectionEntry = ConnectionData & { + address: { + hostname: string; + port: number; + }; + total_connection: number; + first_timestamp: number; + last_timestamp: number; + }; + export function log_connect(address: { + hostname: string; + port: number; + }); + export function update_address_info(address: { + hostname: string; + port: number; + }, data: ConnectionData); + export function update_address_password(address: { + hostname: string; + port: number; + }, password_hash: string); + export function history(): ConnectionEntry[]; + export function delete_entry(address: { + hostname: string; + port: number; + }); +} declare namespace Modals { - export function spawnConnectModal(defaultHost?: { + export function spawnConnectModal(options: { + default_connect_new_tab?: boolean; + }, defaultHost?: { url: string; enforce: boolean; }, connect_profile?: { @@ -2847,12 +3538,23 @@ declare namespace Modals { export function createChannelModal(connection: ConnectionHandler, channel: ChannelEntry | undefined, parent: ChannelEntry | undefined, permissions: PermissionManager, callback: (properties?: ChannelProperties, permissions?: PermissionValue[]) => any); } +/* File: shared/js/ui/modal/ModalGroupAssignment.ts */ +declare namespace Modals { + export function createServerGroupAssignmentModal(client: ClientEntry, callback: (group: Group, flag: boolean) => Promise); +} + /* File: shared/js/ui/modal/ModalIconSelect.ts */ declare namespace Modals { export function spawnIconSelect(client: ConnectionHandler, callback_icon?: (id: number) => any, selected_icon?: number); export function spawnIconUpload(client: ConnectionHandler); } +/* File: shared/js/ui/modal/ModalIdentity.ts */ +declare namespace Modals { + export function spawnTeamSpeakIdentityImprove(identity: profiles.identities.TeaSpeakIdentity): Modal; + export function spawnTeamSpeakIdentityImport(callback: (identity: profiles.identities.TeaSpeakIdentity) => any): Modal; +} + /* File: shared/js/ui/modal/ModalInvite.ts */ declare namespace Modals { export function spawnInviteEditor(connection: ConnectionHandler); @@ -2907,14 +3609,14 @@ declare namespace Modals { export function createServerModal(server: ServerEntry, callback: (properties?: ServerProperties) => any); } -/* File: shared/js/ui/modal/ModalServerGroupDialog.ts */ -declare namespace Modals { - export function createServerGroupAssignmentModal(client: ClientEntry, callback: (group: Group, flag: boolean) => Promise); -} - /* File: shared/js/ui/modal/ModalSettings.ts */ declare namespace Modals { - export function spawnSettingsModal(): Modal; + export function spawnSettingsModal(default_page?: string); +} + +/* File: shared/js/ui/modal/ModalSettingsOld.ts */ +declare namespace Modals { + export function spawnSettingsModal_old(): Modal; } /* File: shared/js/ui/modal/ModalYesNo.ts */ @@ -2925,54 +3627,336 @@ declare namespace Modals { }); } -/* File: shared/js/ui/modal/permission/HTMLPermissionEditor.ts */ -declare namespace unused { - namespace PermissionEditor { - 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; +/* File: shared/js/ui/modal/permission/CanvasPermissionEditor.ts */ +declare namespace pe { + 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_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 interface PermissionValue { - remove: boolean; - granted?: number; - value?: number; - flag_skip?: boolean; - flag_negate?: boolean; + 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; + on_mouse_enter?: () => RepaintMode; + on_mouse_leave?: () => RepaintMode; + on_click?: (event: InteractionClickEvent) => RepaintMode; + mouse_cursor?: string; + set_full_draw?: () => any; + disabled?: boolean; + } + export abstract class DrawableObject { + abstract draw(context: CanvasRenderingContext2D, full: boolean); + private _object_full_draw; + private _width: number; + set_width(value: number); + request_full_draw(); + pop_full_draw(); + width(); + abstract height(); + private _transforms: DOMMatrix[]; + protected push_transform(context: CanvasRenderingContext2D); + protected pop_transform(context: CanvasRenderingContext2D); + protected original_x(context: CanvasRenderingContext2D, x: number); + protected original_y(context: CanvasRenderingContext2D, y: number); + protected colors: scheme.ColorScheme; + set_color_scheme(scheme: scheme.ColorScheme); + protected manager: PermissionEditor; + set_manager(manager: PermissionEditor); + abstract initialize(); + abstract finalize(); + } + export class PermissionGroup extends DrawableObject { + public static readonly HEIGHT; + public static readonly ARROW_SIZE; + group: GroupedPermissions; + _sub_elements: PermissionGroup[]; + _element_permissions: PermissionList; + collapsed; + private _listener_colaps: InteractionListener; + constructor(group: GroupedPermissions); + draw(context: CanvasRenderingContext2D, full: boolean); + set_width(value: number); + set_color_scheme(scheme: scheme.ColorScheme); + set_manager(manager: PermissionEditor); + height(); + initialize(); + finalize(); + collapse_group(); + expend_group(); + } + export class PermissionList extends DrawableObject { + permissions: PermissionEntry[]; + constructor(permissions: PermissionInfo[]); + set_width(value: number); + draw(context: CanvasRenderingContext2D, full: boolean); + height(); + set_color_scheme(scheme: scheme.ColorScheme); + set_manager(manager: PermissionEditor); + initialize(); + finalize(); + handle_hide(); + } + export class PermissionEntry extends DrawableObject { + public static readonly HEIGHT; + public static readonly HALF_HEIGHT; + public static readonly CHECKBOX_HEIGHT; + public static readonly COLUMN_PADDING; + public static readonly COLUMN_VALUE; + public static readonly COLUMN_GRANTED; + public static readonly COLUMN_NEGATE; + public static readonly COLUMN_SKIP; + private _permission: PermissionInfo; + hidden: boolean; + granted: number; + value: number; + flag_skip: boolean; + flag_negate: boolean; + private _prev_selected; + selected: boolean; + flag_skip_hovered; + flag_negate_hovered; + flag_value_hovered; + flag_grant_hovered; + 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); + set_icon_id_image(image: HTMLImageElement | undefined); + permission(); + draw(ctx: CanvasRenderingContext2D, full: boolean); + handle_hide(); + private _draw_icon_field(ctx: CanvasRenderingContext2D, scheme: scheme.CheckBox, x: number, y: number, width: number, hovered: boolean, image: HTMLImageElement); + private _draw_number_field(ctx: CanvasRenderingContext2D, scheme: scheme.TextField, x: number, y: number, width: number, value: number, hovered: boolean); + private _draw_checkbox_field(ctx: CanvasRenderingContext2D, scheme: scheme.CheckBox, x: number, y: number, height: number, checked: boolean, hovered: boolean); + height(); + set_width(value: number); + initialize(); + finalize(); + private _spawn_number_edit(x: number, y: number, width: number, height: number, color: scheme.TextField, value: number, callback: (new_value?: number) => any); + trigger_value_assign(); + trigger_grant_assign(); + } + export class InteractionManager { + private _listeners: InteractionListener[]; + private _entered_listeners: InteractionListener[]; + register_listener(listener: InteractionListener); + remove_listener(listener: InteractionListener); + process_mouse_move(new_x: number, new_y: number): { + repaint: RepaintMode; + cursor: string; + }; + private process_click_event(x: number, y: number, event: InteractionClickEvent): RepaintMode; + process_click(x: number, y: number): RepaintMode; + process_dblclick(x: number, y: number): RepaintMode; + process_context_menu(js_event: MouseEvent, x: number, y: number): RepaintMode; + } + export class PermissionEditor { + private static readonly PERMISSION_HEIGHT; + private static readonly PERMISSION_GROUP_HEIGHT; + readonly grouped_permissions: GroupedPermissions[]; + readonly canvas: HTMLCanvasElement; + readonly canvas_container: HTMLDivElement; + private _max_height: number; + private _permission_count: number; + private _permission_group_count: number; + private _canvas_context: CanvasRenderingContext2D; + private _selected_entry: PermissionEntry; + private _draw_requested: boolean; + private _draw_requested_full: boolean; + private _elements: PermissionGroup[]; + private _intersect_manager: InteractionManager; + private _permission_entry_map: { + [key: number]: PermissionEntry; + }; + mouse: { + x: number; + y: number; + }; + constructor(permissions: GroupedPermissions[]); + private _handle_repaint(mode: RepaintMode); + request_draw(full?: boolean); + draw(full?: boolean); + private initialize(); + intercept_manager(); + set_selected_entry(entry?: PermissionEntry); + permission_entries(): PermissionEntry[]; + collapse_all(); + expend_all(); } - export type change_listener_t = (permission: PermissionInfo, value?: PermissionEditor.PermissionValue) => Promise; } - export enum PermissionEditorMode { - VISIBLE, - NO_PERMISSION, - UNSET - } - export class PermissionEditor { - readonly permissions: GroupedPermissions[]; - container: JQuery; + export class CanvasPermissionEditor extends Modals.AbstractPermissionEditor { + private container: JQuery; private mode_container_permissions: JQuery; private mode_container_error_permission: JQuery; private mode_container_unset: JQuery; private permission_value_map: { [key: number]: PermissionValue; }; - private permission_map: { - [key: number]: PermissionEditor.PermissionEntry; - }; - private listener_change: PermissionEditor.change_listener_t; - private listener_update: () => any; - constructor(permissions: GroupedPermissions[]); - build_tag(); + private entry_editor: ui.PermissionEditor; + icon_resolver: (id: number) => Promise; + icon_selector: (current_id: number) => Promise; + constructor(); + initialize(permissions: GroupedPermissions[]); + html_tag(); + private build_tag(); set_permissions(permissions?: PermissionValue[]); - set_listener(listener?: PermissionEditor.change_listener_t); - set_listener_update(listener?: () => any); - trigger_update(); + set_mode(mode: Modals.PermissionEditorMode); + update_ui(); + } +} + +/* File: shared/js/ui/modal/permission/HTMLPermissionEditor.ts */ +declare namespace pe { + export 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; + private _mask; + 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; + constructor(handle: HTMLPermissionEditor, group: HTMLPermissionGroup, permission: PermissionInfo, index: number); + private static build_checkbox(): { + tag: JQuery; + input: JQuery; + }; + private static number_filter_re; + private static number_filter; + private build_tag(); + private _trigger_value_assign(); + private _trigger_grant_assign(); + hide(); + show(); + is_filtered(): boolean; + set_filtered(flag: boolean); + is_set(): boolean; + value(value: number | undefined, skip?: boolean, negate?: boolean); + granted(value: number | undefined); + reset(); + private _reset_value(); + private _reset_grant(); + private _update_active_class(); + } + export 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); + private _build_tag(); + update_visibility(); + collapse(); + expend(); + } + export class HTMLPermissionEditor extends Modals.AbstractPermissionEditor { + container: JQuery; + private mode_container_permissions: JQuery; + private mode_container_error_permission: JQuery; + private mode_container_unset: JQuery; + private filter_input: JQuery; + private filter_grant: JQuery; + private button_toggle: JQuery; + private even_list: ({ + visible(): boolean; + set_even(flag: boolean); + })[]; + private permission_map: Array; + private permission_groups: HTMLPermissionGroup[]; + constructor(); + initialize(permissions: GroupedPermissions[]); + private update_filter(); + private build_tag(); + html_tag(): JQuery; + set_permissions(u_permissions?: PermissionValue[]); set_mode(mode: PermissionEditorMode); + trigger_change(permission: PermissionInfo, value?: Modals.PermissionEditor.PermissionValue): Promise; + collapse_all(); + expend_all(); + update_view(); + set_toggle_button(callback: () => string, initial: string); } } @@ -2981,7 +3965,7 @@ declare interface JQuery { dropdown: any; } declare namespace Modals { - namespace PermissionEditor { + export namespace PermissionEditor { export interface PermissionEntry { tag: JQuery; tag_value: JQuery; @@ -2999,255 +3983,48 @@ declare namespace Modals { flag_skip?: boolean; flag_negate?: boolean; } - export type change_listener_t = (permission: PermissionInfo, value?: PermissionEditor.PermissionValue) => Promise; + export type change_listener_t = (permission: PermissionInfo, value?: PermissionEditor.PermissionValue) => Promise; } export enum PermissionEditorMode { VISIBLE, NO_PERMISSION, UNSET } - export class PermissionEditor { - readonly permissions: GroupedPermissions[]; - container: JQuery; - private mode_container_permissions: JQuery; - private mode_container_error_permission: JQuery; - private mode_container_unset: JQuery; - private permission_value_map: { - [key: number]: PermissionValue; - }; - private listener_change: PermissionEditor.change_listener_t; - private listener_update: () => any; - private entry_editor: ui.PermissionEditor; + export abstract class AbstractPermissionEditor { + protected _permissions: GroupedPermissions[]; + protected _listener_update: () => any; + protected _listener_change: PermissionEditor.change_listener_t; + protected _toggle_callback: () => string; icon_resolver: (id: number) => Promise; icon_selector: (current_id: number) => Promise; - constructor(permissions: GroupedPermissions[]); - build_tag(); - set_permissions(permissions?: PermissionValue[]); + protected constructor(); + abstract set_mode(mode: PermissionEditorMode); + abstract initialize(permissions: GroupedPermissions[]); + abstract html_tag(): JQuery; + abstract set_permissions(permissions?: PermissionValue[]); set_listener(listener?: PermissionEditor.change_listener_t); set_listener_update(listener?: () => any); trigger_update(); - set_mode(mode: PermissionEditorMode); - update_ui(); + abstract set_toggle_button(callback: () => string, initial: string); } - export function spawnPermissionEdit(connection: ConnectionHandler): Modal; -} - -/* File: shared/js/ui/modal/permission/PermissionEditor.ts */ -declare 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_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; - on_mouse_enter?: () => RepaintMode; - on_mouse_leave?: () => RepaintMode; - on_click?: (event: InteractionClickEvent) => RepaintMode; - mouse_cursor?: string; - set_full_draw?: () => any; - disabled?: boolean; - } - export abstract class DrawableObject { - abstract draw(context: CanvasRenderingContext2D, full: boolean); - private _object_full_draw; - private _width: number; - set_width(value: number); - request_full_draw(); - pop_full_draw(); - width(); - abstract height(); - private _transforms: DOMMatrix[]; - protected push_transform(context: CanvasRenderingContext2D); - protected pop_transform(context: CanvasRenderingContext2D); - protected original_x(context: CanvasRenderingContext2D, x: number); - protected original_y(context: CanvasRenderingContext2D, y: number); - protected colors: scheme.ColorScheme; - set_color_scheme(scheme: scheme.ColorScheme); - protected manager: PermissionEditor; - set_manager(manager: PermissionEditor); - abstract initialize(); - abstract finalize(); - } - export class PermissionGroup extends DrawableObject { - public static readonly HEIGHT; - public static readonly ARROW_SIZE; - group: GroupedPermissions; - _sub_elements: PermissionGroup[]; - _element_permissions: PermissionList; - collapsed; - private _listener_colaps: InteractionListener; - constructor(group: GroupedPermissions); - draw(context: CanvasRenderingContext2D, full: boolean); - set_width(value: number); - set_color_scheme(scheme: scheme.ColorScheme); - set_manager(manager: PermissionEditor); - height(); - initialize(); - finalize(); - collapse_group(); - expend_group(); - } - export class PermissionList extends DrawableObject { - permissions: PermissionEntry[]; - constructor(permissions: PermissionInfo[]); - set_width(value: number); - draw(context: CanvasRenderingContext2D, full: boolean); - height(); - set_color_scheme(scheme: scheme.ColorScheme); - set_manager(manager: PermissionEditor); - initialize(); - finalize(); - handle_hide(); - } - export class PermissionEntry extends DrawableObject { - public static readonly HEIGHT; - public static readonly HALF_HEIGHT; - public static readonly CHECKBOX_HEIGHT; - public static readonly COLUMN_PADDING; - public static readonly COLUMN_VALUE; - public static readonly COLUMN_GRANTED; - public static readonly COLUMN_NEGATE; - public static readonly COLUMN_SKIP; - private _permission: PermissionInfo; - hidden: boolean; - granted: number; - value: number; - flag_skip: boolean; - flag_negate: boolean; - private _prev_selected; - selected: boolean; - flag_skip_hovered; - flag_negate_hovered; - flag_value_hovered; - flag_grant_hovered; - 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); - set_icon_id_image(image: HTMLImageElement | undefined); - permission(); - draw(ctx: CanvasRenderingContext2D, full: boolean); - handle_hide(); - private _draw_icon_field(ctx: CanvasRenderingContext2D, scheme: scheme.CheckBox, x: number, y: number, width: number, hovered: boolean, image: HTMLImageElement); - private _draw_number_field(ctx: CanvasRenderingContext2D, scheme: scheme.TextField, x: number, y: number, width: number, value: number, hovered: boolean); - private _draw_checkbox_field(ctx: CanvasRenderingContext2D, scheme: scheme.CheckBox, x: number, y: number, height: number, checked: boolean, hovered: boolean); - height(); - set_width(value: number); - initialize(); - finalize(); - private _spawn_number_edit(x: number, y: number, width: number, height: number, color: scheme.TextField, value: number, callback: (new_value?: number) => any); - trigger_value_assign(); - trigger_grant_assign(); - } - export class InteractionManager { - private _listeners: InteractionListener[]; - private _entered_listeners: InteractionListener[]; - register_listener(listener: InteractionListener); - remove_listener(listener: InteractionListener); - process_mouse_move(new_x: number, new_y: number): { - repaint: RepaintMode; - cursor: string; - }; - private process_click_event(x: number, y: number, event: InteractionClickEvent): RepaintMode; - process_click(x: number, y: number): RepaintMode; - process_dblclick(x: number, y: number): RepaintMode; - process_context_menu(js_event: MouseEvent, x: number, y: number): RepaintMode; - } - export class PermissionEditor { - private static readonly PERMISSION_HEIGHT; - private static readonly PERMISSION_GROUP_HEIGHT; - readonly grouped_permissions: GroupedPermissions[]; - readonly canvas: HTMLCanvasElement; - readonly canvas_container: HTMLDivElement; - private _max_height: number; - private _permission_count: number; - private _permission_group_count: number; - private _canvas_context: CanvasRenderingContext2D; - private _selected_entry: PermissionEntry; - private _draw_requested: boolean; - private _draw_requested_full: boolean; - private _elements: PermissionGroup[]; - private _intersect_manager: InteractionManager; - private _permission_entry_map: { - [key: number]: PermissionEntry; - }; - mouse: { - x: number; - y: number; - }; - constructor(permissions: GroupedPermissions[]); - private _handle_repaint(mode: RepaintMode); - request_draw(full?: boolean); - draw(full?: boolean); - private initialize(); - intercept_manager(); - set_selected_entry(entry?: PermissionEntry); - permission_entries(): PermissionEntry[]; - collapse_all(); - expend_all(); + 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 { + : OptionsServerGroup; + : OptionsChannelGroup; + : OptionsClientPermissions; + : OptionsChannelPermissions; + : OptionsClientChannelPermissions; } + export function _space(); + export function spawnPermissionEdit(connection: ConnectionHandler, selected_tab?: T, options?: OptionMap[T]): Modal; } /* File: shared/js/ui/server.ts */ @@ -3268,6 +4045,7 @@ declare class ServerProperties { virtualserver_reserved_slots: number; virtualserver_password: string; virtualserver_flag_password: boolean; + virtualserver_ask_for_privilegekey: boolean; virtualserver_welcomemessage: string; virtualserver_hostmessage: string; virtualserver_hostmessage_mode: number; @@ -3289,6 +4067,7 @@ declare class ServerProperties { virtualserver_antiflood_points_tick_reduce: number; virtualserver_antiflood_points_needed_command_block: number; virtualserver_antiflood_points_needed_ip_block: number; + virtualserver_country_code: string; virtualserver_complain_autoban_count: number; virtualserver_complain_autoban_time: number; virtualserver_complain_remove_time: number; @@ -3405,6 +4184,7 @@ declare namespace audio { export namespace recorder { export interface InputDevice { unique_id: string; + driver: string; name: string; default_input: boolean; supported: boolean; @@ -3480,6 +4260,11 @@ declare namespace audio { disable_filter(type: filter.Type); enable_filter(type: filter.Type); } + export interface LevelMeter { + device(): InputDevice; + set_observer(callback: (value: number) => any); + destory(); + } } } diff --git a/modules/renderer/index.ts b/modules/renderer/index.ts index 2f59f21..39969cb 100644 --- a/modules/renderer/index.ts +++ b/modules/renderer/index.ts @@ -170,17 +170,15 @@ const module_loader_setup = async () => { window["require_setup"] = _mod => { if(!_mod || !_mod.paths) return; - console.dir(_mod); - _mod.paths.push(...native_paths); - const org_req = _mod.__proto__.require; + const original_require = _mod.__proto__.require; if(!_mod.proxied) { - _mod.require = function a(m) { - let stack = new Error().stack; - if(stack.startsWith("Error")) - stack = stack.substr(6); - //console.log("require \"%s\"\nStack:\n%s", m, stack); - return org_req.apply(_mod, [m]); + _mod.require = (path: string) => { + if(path.endsWith("imports/imports_shared")) { + console.log("Proxy require for %s. Using 'window' as result.", path); + return window; + } + return original_require.apply(_mod, [path]); }; _mod.proxied = true; } @@ -202,16 +200,24 @@ const load_modules = async () => { console.log("Loading native extensions..."); try { try { - require("./ppt"); + require("./version"); } catch(error) { - console.error("Failed to load ppt"); + console.error("Failed to load version extension"); console.dir(error); throw error; } try { - require("./version"); + const helper = require("./icon-helper"); + await helper.initialize(); } catch(error) { - console.error("Failed to load version extension"); + console.error("Failed to load the icon helper extension"); + console.dir(error); + throw error; + } + try { + require("./ppt"); + } catch(error) { + console.error("Failed to load ppt"); console.dir(error); throw error; } @@ -236,6 +242,13 @@ const load_modules = async () => { console.dir(error); throw error; } + try { + require("./menu"); + } catch(error) { + console.error("Failed to load menu extension"); + console.dir(error); + throw error; + } try { require("./context-menu"); } catch(error) { diff --git a/modules/renderer/menu.ts b/modules/renderer/menu.ts new file mode 100644 index 0000000..b5b52c4 --- /dev/null +++ b/modules/renderer/menu.ts @@ -0,0 +1,251 @@ +import {class_to_image} from "./icon-helper"; + +window["require_setup"](module); + +import * as electron from "electron"; +import {top_menu as dtop_menu, Icon} from "./imports/imports_shared"; + +namespace _top_menu { + import ipcRenderer = electron.ipcRenderer; + namespace native { + import ipcRenderer = electron.ipcRenderer; + let _item_index = 1; + + abstract class NativeMenuBase { + protected _handle: NativeMenuBar; + protected _click: () => any; + id: string; + + protected constructor(handle: NativeMenuBar, id?: string) { + this._handle = handle; + this.id = id || ("item_" + (_item_index++)); + } + + abstract build() : electron.MenuItemConstructorOptions; + abstract items(): (dtop_menu.MenuItem | dtop_menu.HRItem)[]; + + trigger_click() { + if(this._click) + this._click(); + } + } + + class NativeMenuItem extends NativeMenuBase implements dtop_menu.MenuItem { + private _items: (NativeMenuItem | NativeHrItem)[] = []; + private _label: string; + private _enabled: boolean = true; + private _visible: boolean = true; + + private _icon_data: string; + + constructor(handle: NativeMenuBar) { + super(handle); + + } + + append_hr(): dtop_menu.HRItem { + const item = new NativeHrItem(this._handle); + this._items.push(item); + return item; + } + + append_item(label: string): dtop_menu.MenuItem { + const item = new NativeMenuItem(this._handle); + item.label(label); + this._items.push(item); + return item; + } + + click(callback: () => any): this { + this._click = callback; + return this; + } + + delete_item(item: dtop_menu.MenuItem | dtop_menu.HRItem) { + const i_index = this._items.indexOf(item as any); + if(i_index < 0) return; + this._items.splice(i_index, 1); + } + + disabled(value?: boolean): boolean { + if(typeof(value) === "boolean") + this._enabled = !value; + return !this._enabled; + } + + icon(klass?: string | Promise | Icon): string { + if(typeof(klass) === "string") { + const buffer = class_to_image(klass); + if(buffer) + this._icon_data = buffer.toDataURL(); + } + return ""; + } + + items(): (dtop_menu.MenuItem | dtop_menu.HRItem)[] { + return this._items; + } + + label(value?: string): string { + if(typeof(value) === "string") + this._label = value; + return this._label; + } + + visible(value?: boolean): boolean { + if(typeof(value) === "boolean") + this._visible = value; + return this._visible; + } + + build(): Electron.MenuItemConstructorOptions { + return { + id: this.id, + + label: this._label || "", + + submenu: this._items.length > 0 ? this._items.map(e => e.build()) : undefined, + enabled: this._enabled, + visible: this._visible, + + icon: this._icon_data + } + } + } + + class NativeHrItem extends NativeMenuBase implements dtop_menu.HRItem { + constructor(handle: NativeMenuBar) { + super(handle); + } + + build(): Electron.MenuItemConstructorOptions { + return { + type: 'separator', + id: this.id + } + } + + items(): (dtop_menu.MenuItem | dtop_menu.HRItem)[] { + return []; + } + } + + function is_similar_deep(a, b) { + if(typeof(a) !== typeof(b)) + return false; + if(typeof(a) !== "object") + return a === b; + + const aProps = Object.keys(a); + const bProps = Object.keys(b); + + if (aProps.length != bProps.length) + return false; + + for (let i = 0; i < aProps.length; i++) { + const propName = aProps[i]; + + if(!is_similar_deep(a[propName], b[propName])) + return false; + } + + return true; + } + + + export class NativeMenuBar implements dtop_menu.MenuBarDriver { + private static _instance: NativeMenuBar; + + private menu: electron.Menu; + private _items: NativeMenuItem[] = []; + private _current_menu: electron.MenuItemConstructorOptions[]; + + public static instance() : NativeMenuBar { + if(!this._instance) + this._instance = new NativeMenuBar(); + return this._instance; + } + + append_item(label: string): dtop_menu.MenuItem { + const item = new NativeMenuItem(this); + item.label(label); + this._items.push(item); + return item; + } + + delete_item(item: dtop_menu.MenuItem) { + const i_index = this._items.indexOf(item as any); + if(i_index < 0) return; + this._items.splice(i_index, 1); + } + + flush_changes() { + const target_menu = this.build_menu(); + if(is_similar_deep(target_menu, this._current_menu)) + return; + + this._current_menu = target_menu; + ipcRenderer.send('top-menu', target_menu); + } + + private build_menu() : electron.MenuItemConstructorOptions[] { + return this._items.map(e => e.build()); + } + + items(): dtop_menu.MenuItem[] { + return this._items; + } + + initialize() { + this.menu = new electron.remote.Menu(); + ipcRenderer.on('top-menu', (event, clicked_item) => { + console.log("Item %o clicked", clicked_item); + const check_item = (item: NativeMenuBase) => { + if(item.id == clicked_item) { + item.trigger_click(); + return true; + } + for(const child of item.items()) + if(check_item(child as NativeMenuBase)) + return true; + }; + + for(const item of this._items) + if(check_item(item)) + return; + }); + } + } + } + + //Global variable + // @ts-ignore + top_menu.set_driver(native.NativeMenuBar.instance()); + + + const call_basic_action = (name: string, ...args: any[]) => ipcRenderer.send('basic-action', name, ...args); + top_menu.native_actions = { + open_change_log() { + call_basic_action("open-changelog"); + }, + + check_native_update() { + call_basic_action("check-native-update"); + }, + + quit() { + call_basic_action("quit"); + }, + + open_dev_tools() { + call_basic_action("open-dev-tools"); + }, + + reload_page() { + call_basic_action("reload-window") + } + }; +} + + +export {}; \ No newline at end of file diff --git a/modules/renderer/ppt.ts b/modules/renderer/ppt.ts index ad709e4..4f0a3d8 100644 --- a/modules/renderer/ppt.ts +++ b/modules/renderer/ppt.ts @@ -92,7 +92,7 @@ namespace _ppt { current_state.code = event.key_code; for(const hook of key_hooks) { - if(hook.key_code != event.key_code) continue; + if(hook.key_code && 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; diff --git a/native/serverconnection/CMakeLists.txt b/native/serverconnection/CMakeLists.txt index 2c11c3b..bac3280 100644 --- a/native/serverconnection/CMakeLists.txt +++ b/native/serverconnection/CMakeLists.txt @@ -1,5 +1,6 @@ set(MODULE_NAME "teaclient_connection") +#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer -static-libasan -lasan -lubsan") set(SOURCE_FILES src/logger.cpp src/EventLoop.cpp diff --git a/native/serverconnection/exports/exports.d.ts b/native/serverconnection/exports/exports.d.ts index 96f2882..a610002 100644 --- a/native/serverconnection/exports/exports.d.ts +++ b/native/serverconnection/exports/exports.d.ts @@ -69,6 +69,9 @@ declare module "teaclient_connection" { send_command(command: string, arguments: any[], switches: string[]); send_voice_data(buffer: Uint8Array, codec_id: number, header: boolean); send_voice_data_raw(buffer: Float32Array, channels: number, sample_rate: number, header: boolean); + + /* ping in microseconds */ + current_ping() : number; } export function spawn_server_connection() : NativeServerConnection; @@ -223,12 +226,15 @@ declare module "teaclient_connection" { export interface AudioRecorder { get_device() : number; - set_device(device: number); /* Recorder needs to be started afterwards */ + set_device(device: number, callback: (flag: boolean | string) => void); /* Recorder needs to be started afterwards */ - start(); + start(callback: (flag: boolean) => void); started() : boolean; stop(); + get_volume() : number; + set_volume(volume: number); + create_consumer() : AudioConsumer; get_consumers() : AudioConsumer[]; delete_consumer(consumer: AudioConsumer); diff --git a/native/serverconnection/src/audio/AudioInput.cpp b/native/serverconnection/src/audio/AudioInput.cpp index 0e28489..0574ecf 100644 --- a/native/serverconnection/src/audio/AudioInput.cpp +++ b/native/serverconnection/src/audio/AudioInput.cpp @@ -21,10 +21,10 @@ AudioConsumer::AudioConsumer(tc::audio::AudioInput *handle, size_t channel_count void AudioConsumer::handle_framed_data(const void *buffer, size_t samples) { unique_lock read_callback_lock(this->on_read_lock); - if(!this->on_read) - return; auto function = this->on_read; /* copy */ read_callback_lock.unlock(); + if(!function) + return; function(buffer, samples); } @@ -52,8 +52,10 @@ bool AudioInput::open_device(std::string& error, PaDeviceIndex index) { this->close_device(); this->_current_device_index = index; - this->_current_device = Pa_GetDeviceInfo(index); + if(index == paNoDevice) + return true; + this->_current_device = Pa_GetDeviceInfo(index); if(!this->_current_device) { this->_current_device_index = paNoDevice; error = "failed to get device info"; @@ -141,9 +143,22 @@ int AudioInput::_audio_callback(const void *a, void *b, unsigned long c, const P int AudioInput::audio_callback(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags) { if (!input) /* hmmm.. suspicious */ return 0; + + if(this->_volume != 1) { + auto ptr = (float*) input; + auto left = frameCount * this->_channel_count; + while(left-- > 0) + *(ptr++) *= this->_volume; + } + + auto begin = chrono::system_clock::now(); for(const auto& consumer : this->consumers()) { consumer->process_data(input, frameCount); } - + auto end = chrono::system_clock::now(); + auto ms = chrono::duration_cast(end - begin).count(); + if(ms > 5) { + log_warn(category::audio, tr("Processing of audio input needed {}ms. This could be an issue!"), chrono::duration_cast(end - begin).count()); + } return 0; } \ No newline at end of file diff --git a/native/serverconnection/src/audio/AudioInput.h b/native/serverconnection/src/audio/AudioInput.h index d809668..d441272 100644 --- a/native/serverconnection/src/audio/AudioInput.h +++ b/native/serverconnection/src/audio/AudioInput.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "AudioSamples.h" namespace tc { @@ -23,7 +24,7 @@ namespace tc { size_t const frame_size = 0; - std::mutex on_read_lock; /* locked to access the function */ + spin_lock on_read_lock; /* locked to access the function */ std::function on_read; private: AudioConsumer(AudioInput* handle, size_t channel_count, size_t sample_rate, size_t frame_size); diff --git a/native/serverconnection/src/audio/AudioOutput.cpp b/native/serverconnection/src/audio/AudioOutput.cpp index 1b2e562..9221077 100644 --- a/native/serverconnection/src/audio/AudioOutput.cpp +++ b/native/serverconnection/src/audio/AudioOutput.cpp @@ -86,7 +86,7 @@ ssize_t AudioOutputSource::enqueue_samples(const std::shared_ptrsample_buffers.clear(); break; case overflow_strategy::discard_buffer_half: - this->sample_buffers.erase(this->sample_buffers.begin(), this->sample_buffers.begin() + (int) (this->sample_buffers.size() / 2)); + this->sample_buffers.erase(this->sample_buffers.begin(), this->sample_buffers.begin() + (int) ceil(this->sample_buffers.size() / 2)); break; case overflow_strategy::ignore: break; diff --git a/native/serverconnection/src/audio/AudioReframer.cpp b/native/serverconnection/src/audio/AudioReframer.cpp index c87bd14..ffae477 100644 --- a/native/serverconnection/src/audio/AudioReframer.cpp +++ b/native/serverconnection/src/audio/AudioReframer.cpp @@ -36,8 +36,10 @@ void Reframer::process(const void *source, size_t samples) { } } + auto _on_frame = this->on_frame; while(samples > this->_frame_size) { - this->on_frame(source); + if(_on_frame) + _on_frame(source); samples -= this->_frame_size; source = (char*) source + this->_frame_size * this->_channels * 4; } diff --git a/native/serverconnection/src/audio/js/AudioConsumer.cpp b/native/serverconnection/src/audio/js/AudioConsumer.cpp index e618fbf..c3c7075 100644 --- a/native/serverconnection/src/audio/js/AudioConsumer.cpp +++ b/native/serverconnection/src/audio/js/AudioConsumer.cpp @@ -40,7 +40,9 @@ AudioConsumerWrapper::AudioConsumerWrapper(AudioRecorderWrapper* h, const std::s handle->on_read = [&](const void* buffer, size_t length){ this->process_data(buffer, length); }; } - //this->_recorder->js_ref(); /* FML FIXME: Mem leak! (In general the consumer live is related to the recorder handle) */ +#ifdef DO_DEADLOCK_REF + this->_recorder->js_ref(); /* FML Mem leak! (In general the consumer live is related to the recorder handle, but for nodejs testing we want to keep this reference ) */ +#endif } AudioConsumerWrapper::~AudioConsumerWrapper() { @@ -53,8 +55,10 @@ AudioConsumerWrapper::~AudioConsumerWrapper() { this->_handle = nullptr; } +#ifdef DO_DEADLOCK_REF if(this->_recorder) this->_recorder->js_unref(); +#endif } void AudioConsumerWrapper::do_wrap(const v8::Local &obj) { diff --git a/native/serverconnection/src/audio/js/AudioConsumer.h b/native/serverconnection/src/audio/js/AudioConsumer.h index 5954925..197e20f 100644 --- a/native/serverconnection/src/audio/js/AudioConsumer.h +++ b/native/serverconnection/src/audio/js/AudioConsumer.h @@ -77,7 +77,7 @@ namespace tc { void do_wrap(const v8::Local& /* object */); void unbind(); /* called with execute_lock locked */ - void process_data(const void* /* buffer */, size_t /* samples */); /* TODO: Lock the execute_lock! */ + void process_data(const void* /* buffer */, size_t /* samples */); struct DataEntry { void* buffer = nullptr; @@ -96,7 +96,6 @@ namespace tc { Nan::callback_t<> _call_ended; Nan::callback_t<> _call_started; /* - * callback_data: (buffer: Float32Array) => any; callback_ended: () => any; */ diff --git a/native/serverconnection/src/audio/js/AudioFilter.cpp b/native/serverconnection/src/audio/js/AudioFilter.cpp index 4b601b0..c456df1 100644 --- a/native/serverconnection/src/audio/js/AudioFilter.cpp +++ b/native/serverconnection/src/audio/js/AudioFilter.cpp @@ -68,6 +68,8 @@ AudioFilterWrapper::~AudioFilterWrapper() { auto threshold_filter = dynamic_pointer_cast(this->_filter); if(threshold_filter) threshold_filter->on_analyze = nullptr; + + this->_callback_analyzed.Reset(); } void AudioFilterWrapper::do_wrap(const v8::Local &obj) { diff --git a/native/serverconnection/src/audio/js/AudioRecorder.cpp b/native/serverconnection/src/audio/js/AudioRecorder.cpp index 8e9ad61..69c036b 100644 --- a/native/serverconnection/src/audio/js/AudioRecorder.cpp +++ b/native/serverconnection/src/audio/js/AudioRecorder.cpp @@ -34,6 +34,9 @@ NAN_MODULE_INIT(AudioRecorderWrapper::Init) { Nan::SetPrototypeMethod(klass, "started", AudioRecorderWrapper::_started); Nan::SetPrototypeMethod(klass, "stop", AudioRecorderWrapper::_stop); + Nan::SetPrototypeMethod(klass, "get_volume", AudioRecorderWrapper::_get_volume); + Nan::SetPrototypeMethod(klass, "set_volume", AudioRecorderWrapper::_set_volume); + Nan::SetPrototypeMethod(klass, "get_consumers", AudioRecorderWrapper::_get_consumers); Nan::SetPrototypeMethod(klass, "create_consumer", AudioRecorderWrapper::_create_consumer); Nan::SetPrototypeMethod(klass, "delete_consumer", AudioRecorderWrapper::_delete_consumer); @@ -126,28 +129,69 @@ NAN_METHOD(AudioRecorderWrapper::_set_device) { auto handle = ObjectWrap::Unwrap(info.Holder()); auto input = handle->_input; - if(info.Length() != 1 || !info[0]->IsNumber()) { - Nan::ThrowError("invalid argument"); + if(info.Length() != 2 || !info[0]->IsNumber() || !info[1]->IsFunction()) { + Nan::ThrowError("invalid arguments"); return; } auto device_id = info[0]->Int32Value(Nan::GetCurrentContext()).FromMaybe(0); - string error; - if(!input->open_device(error, device_id)) { - Nan::ThrowError(Nan::New("failed to open device (" + error + ")").ToLocalChecked()); - return; - } + unique_ptr> _callback = make_unique>(info[1].As()); + unique_ptr> _recorder = make_unique>(info.Holder()); + + auto _async_callback = Nan::async_callback([call = std::move(_callback), recorder = move(_recorder)](bool result, std::string error) mutable { + Nan::HandleScope scope; + auto callback_function = call->Get(Nan::GetCurrentContext()->GetIsolate()); + + v8::Local argv[1]; + if(result) + argv[0] = v8::Boolean::New(Nan::GetCurrentContext()->GetIsolate(), result); + else + argv[0] = Nan::NewOneByteString((uint8_t*) error.data(), error.length()).ToLocalChecked(); + callback_function->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, argv); + + recorder->Reset(); + call->Reset(); + }).option_destroyed_execute(true); + + std::thread([_async_callback, input, device_id]{ + string error; + auto flag = input->open_device(error, device_id); + _async_callback(std::forward(flag), std::forward(error)); + }).detach(); } NAN_METHOD(AudioRecorderWrapper::_start) { - auto handle = ObjectWrap::Unwrap(info.Holder()); - auto input = handle->_input; - - if(!input->record()) { - Nan::ThrowError("failed to start"); + if(info.Length() != 1) { + Nan::ThrowError("missing callback"); return; } + + if(!info[0]->IsFunction()) { + Nan::ThrowError("not a function"); + return; + } + + unique_ptr> _callback = make_unique>(info[0].As()); + unique_ptr> _recorder = make_unique>(info.Holder()); + + auto _async_callback = Nan::async_callback([call = std::move(_callback), recorder = move(_recorder)](bool result) mutable { + Nan::HandleScope scope; + auto callback_function = call->Get(Nan::GetCurrentContext()->GetIsolate()); + + v8::Local argv[1]; + argv[0] = v8::Boolean::New(Nan::GetCurrentContext()->GetIsolate(), result); + callback_function->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, argv); + + recorder->Reset(); + call->Reset(); + }).option_destroyed_execute(true); + + auto handle = ObjectWrap::Unwrap(info.Holder()); + auto input = handle->_input; + std::thread([_async_callback, input]{ + _async_callback(input->record()); + }).detach(); } NAN_METHOD(AudioRecorderWrapper::_started) { @@ -203,4 +247,20 @@ NAN_METHOD(AudioRecorderWrapper::_delete_consumer) { auto consumer = ObjectWrap::Unwrap(info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked()); handle->delete_consumer(consumer); +} + +NAN_METHOD(AudioRecorderWrapper::_set_volume) { + auto handle = ObjectWrap::Unwrap(info.Holder()); + + if(info.Length() != 1 || !info[0]->IsNumber()) { + Nan::ThrowError("invalid argument"); + return; + } + + handle->_input->set_volume(info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0)); +} + +NAN_METHOD(AudioRecorderWrapper::_get_volume) { + auto handle = ObjectWrap::Unwrap(info.Holder()); + info.GetReturnValue().Set(handle->_input->volume()); } \ No newline at end of file diff --git a/native/serverconnection/src/audio/js/AudioRecorder.h b/native/serverconnection/src/audio/js/AudioRecorder.h index 7215878..4d3f12d 100644 --- a/native/serverconnection/src/audio/js/AudioRecorder.h +++ b/native/serverconnection/src/audio/js/AudioRecorder.h @@ -38,6 +38,9 @@ namespace tc { static NAN_METHOD(_get_consumers); static NAN_METHOD(_delete_consumer); + static NAN_METHOD(_set_volume); + static NAN_METHOD(_get_volume); + std::shared_ptr create_consumer(); void delete_consumer(const AudioConsumerWrapper*); diff --git a/native/serverconnection/src/connection/ProtocolHandler.cpp b/native/serverconnection/src/connection/ProtocolHandler.cpp index eadebb2..b5523d5 100644 --- a/native/serverconnection/src/connection/ProtocolHandler.cpp +++ b/native/serverconnection/src/connection/ProtocolHandler.cpp @@ -444,7 +444,7 @@ bool ProtocolHandler::create_datagram_packets(std::vector &result } else { packet->applyPacketId(this->_packet_id_manager); } - log_trace(category::connection, tr("Packet {} got packet id {}"), packet->type().name(), packet->packetId()); + //log_trace(category::connection, tr("Packet {} got packet id {}"), packet->type().name(), packet->packetId()); } if(!this->crypt_handler.progressPacketOut(packet.get(), error, false)) { log_error(category::connection, tr("Failed to encrypt packet: {}"), error); diff --git a/native/serverconnection/src/connection/ProtocolHandler.h b/native/serverconnection/src/connection/ProtocolHandler.h index 646a435..d775133 100644 --- a/native/serverconnection/src/connection/ProtocolHandler.h +++ b/native/serverconnection/src/connection/ProtocolHandler.h @@ -75,6 +75,8 @@ namespace tc { ecc_key& get_identity_key() { return this->crypto.identity; } + inline std::chrono::microseconds current_ping() { return this->ping.value; } + connection_state::value connection_state = connection_state::INITIALIZING; server_type::value server_type = server_type::TEASPEAK; private: diff --git a/native/serverconnection/src/connection/ProtocolHandlerPackets.cpp b/native/serverconnection/src/connection/ProtocolHandlerPackets.cpp index 6341d7c..c595ed9 100644 --- a/native/serverconnection/src/connection/ProtocolHandlerPackets.cpp +++ b/native/serverconnection/src/connection/ProtocolHandlerPackets.cpp @@ -60,29 +60,6 @@ void ProtocolHandler::handlePacketCommand(const std::shared_ptr &packet) { this->handle->voice_connection->process_packet(packet); - - /* - if(packet->type() == PacketTypeInfo::Voice) { - if(packet->data().length() < 5) { - //TODO log invalid voice packet - return; - } - auto container = make_unique(); - container->packet_id = be2le16(&packet->data()[0]); - container->client_id = be2le16(&packet->data()[2]); - container->codec_id = (uint8_t) packet->data()[4]; - container->flag_head = packet->hasFlag(PacketFlag::Compressed); - container->voice_data = packet->data().length() > 5 ? packet->data().range(5) : pipes::buffer{}; - - { - lock_guard lock(this->handle->pending_voice_lock); - this->handle->pending_voice.push_back(move(container)); - } - this->handle->execute_pending_voice.call(true); - } else { - //TODO implement whisper - } - */ } void ProtocolHandler::handlePacketPing(const std::shared_ptr &packet) { diff --git a/native/serverconnection/src/connection/ServerConnection.cpp b/native/serverconnection/src/connection/ServerConnection.cpp index a7a9c9c..190b2fc 100644 --- a/native/serverconnection/src/connection/ServerConnection.cpp +++ b/native/serverconnection/src/connection/ServerConnection.cpp @@ -58,6 +58,7 @@ NAN_MODULE_INIT(ServerConnection::Init) { Nan::SetPrototypeMethod(klass, "send_command", ServerConnection::_send_command); Nan::SetPrototypeMethod(klass, "send_voice_data", ServerConnection::_send_voice_data); Nan::SetPrototypeMethod(klass, "send_voice_data_raw", ServerConnection::_send_voice_data_raw); + Nan::SetPrototypeMethod(klass, "current_ping", ServerConnection::_current_ping); constructor().Reset(Nan::GetFunction(klass).ToLocalChecked()); } @@ -641,4 +642,13 @@ void ServerConnection::_execute_callback_disconnect(const std::string &reason) { arguments[0] = Nan::New(reason).ToLocalChecked(); callback->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, arguments); +} + +NAN_METHOD(ServerConnection::_current_ping) { + auto connection = ObjectWrap::Unwrap(info.Holder()); + auto& phandler = connection->protocol_handler; + if(phandler) + info.GetReturnValue().Set((uint32_t) chrono::floor(phandler->current_ping()).count()); + else + info.GetReturnValue().Set(-1); } \ No newline at end of file diff --git a/native/serverconnection/src/connection/ServerConnection.h b/native/serverconnection/src/connection/ServerConnection.h index 543334f..69fded3 100644 --- a/native/serverconnection/src/connection/ServerConnection.h +++ b/native/serverconnection/src/connection/ServerConnection.h @@ -85,6 +85,7 @@ namespace tc { static NAN_METHOD(_send_voice_data); static NAN_METHOD(_send_voice_data_raw); static NAN_METHOD(_error_message); + static NAN_METHOD(_current_ping); std::unique_ptr callback_connect; std::unique_ptr callback_disconnect; diff --git a/native/serverconnection/src/connection/audio/VoiceClient.cpp b/native/serverconnection/src/connection/audio/VoiceClient.cpp index 9b44e74..44a5562 100644 --- a/native/serverconnection/src/connection/audio/VoiceClient.cpp +++ b/native/serverconnection/src/connection/audio/VoiceClient.cpp @@ -208,12 +208,27 @@ VoiceClientWrap::~VoiceClientWrap() {} VoiceClient::VoiceClient(const std::shared_ptr&, uint16_t client_id) : _client_id(client_id) { this->output_source = global_audio_output->create_source(); - this->output_source->overflow_strategy = audio::overflow_strategy::discard_buffer_half; - this->output_source->max_latency = (size_t) ceil(this->output_source->sample_rate * 0.12); + this->output_source->overflow_strategy = audio::overflow_strategy::ignore; + this->output_source->max_latency = (size_t) ceil(this->output_source->sample_rate * 1); this->output_source->min_buffer = (size_t) ceil(this->output_source->sample_rate * 0.025); this->output_source->on_underflow = [&]{ - this->set_state(state::stopped); + if(this->_state == state::stopping) + this->set_state(state::stopped); + else if(this->_state != state::stopped) { + if(this->_last_received_packet + chrono::seconds(1) < chrono::system_clock::now()) { + this->set_state(state::stopped); + log_warn(category::audio, tr("Client {} has a audio buffer underflow and not received any data for one second. Stopping replay."), this->_client_id); + } else { + if(this->_state != state::buffering) { + log_warn(category::audio, tr("Client {} has a audio buffer underflow. Buffer again."), this->_client_id); + this->set_state(state::buffering); + } + } + } + }; + this->output_source->on_overflow = [&](size_t count){ + log_warn(category::audio, tr("Client {} has a audio buffer overflow of {}."), this->_client_id, count); }; } @@ -265,6 +280,7 @@ void VoiceClient::process_packet(uint16_t packet_id, const pipes::buffer_view& b encoded_buffer->buffer = buffer.own_buffer(); encoded_buffer->head = head; + this->_last_received_packet = encoded_buffer->receive_timestamp; { lock_guard lock(this->audio_decode_queue_lock); this->audio_decode_queue.push_back(move(encoded_buffer)); @@ -356,7 +372,7 @@ void VoiceClient::process_encoded_buffer(const std::unique_ptr &b diff = buffer->packet_id - codec_data->last_packet_id; } - if(codec_data->last_packet_timestamp + chrono::seconds(1) < buffer->receive_timestamp) + if(codec_data->last_packet_timestamp + chrono::seconds(1) < buffer->receive_timestamp || this->_state >= state::stopping) diff = 0xFFFF; const auto old_packet_id = codec_data->last_packet_id; @@ -427,6 +443,7 @@ void VoiceClient::process_encoded_buffer(const std::unique_ptr &b } auto enqueued = this->output_source->enqueue_samples(target_buffer, resampled_samples); + if(enqueued != resampled_samples) + log_warn(category::voice_connection, tr("Failed to enqueue all samples for client {}. Enqueued {} of {}"), this->_client_id, enqueued, resampled_samples); this->set_state(state::playing); - //cout << "Enqueued " << enqueued << " samples" << endl; } \ No newline at end of file diff --git a/native/serverconnection/src/connection/audio/VoiceClient.h b/native/serverconnection/src/connection/audio/VoiceClient.h index 649f70d..6788849 100644 --- a/native/serverconnection/src/connection/audio/VoiceClient.h +++ b/native/serverconnection/src/connection/audio/VoiceClient.h @@ -112,6 +112,7 @@ namespace tc { uint16_t _client_id; float _volume = 1.f; + std::chrono::system_clock::time_point _last_received_packet; state::value _state = state::stopped; inline void set_state(state::value value) { if(value == this->_state) diff --git a/native/serverconnection/src/connection/audio/VoiceConnection.cpp b/native/serverconnection/src/connection/audio/VoiceConnection.cpp index 28519a9..145b682 100644 --- a/native/serverconnection/src/connection/audio/VoiceConnection.cpp +++ b/native/serverconnection/src/connection/audio/VoiceConnection.cpp @@ -299,6 +299,7 @@ void VoiceConnection::process_packet(const std::shared_ptrhas_flag(PacketFlag::Compressed); //container->voice_data = packet->data().length() > 5 ? packet->data().range(5) : pipes::buffer{}; + //log_info(category::voice_connection, tr("Received voice packet from {}. Packet ID: {}"), client_id, packet_id); auto client = this->find_client(client_id); if(!client) { log_warn(category::voice_connection, tr("Received voice packet from unknown client {}. Dropping packet!"), client_id); diff --git a/native/serverconnection/test/js/main.ts b/native/serverconnection/test/js/main.ts index bd9106b..2d89c18 100644 --- a/native/serverconnection/test/js/main.ts +++ b/native/serverconnection/test/js/main.ts @@ -1,7 +1,12 @@ /// +console.log("HELLO WORLD"); module.paths.push("../../build/linux_x64"); module.paths.push("../../build/win32_64"); +//LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libasan.so.5 +const os = require('os'); +//process.dlopen(module, '/usr/lib/x86_64-linux-gnu/libasan.so.5', +// os.constants.dlopen.RTLD_NOW); import * as fs from "fs"; const original_require = require; @@ -118,8 +123,8 @@ const do_connect = () => { timeout: 5000, remote_port: 9987, //remote_host: "188.40.240.20", /* twerion */ - remote_host: "localhost", - //remote_host: "ts.teaspeak.de", + //remote_host: "localhost", + remote_host: "ts.teaspeak.de", //remote_host: "51.68.181.92", //remote_host: "94.130.236.135", //remote_host: "54.36.232.11", /* the beast */ @@ -160,11 +165,14 @@ const do_connect = () => { console.log(">> Audio end"); }; + connection._voice_connection.set_audio_source(consumer); + /* consumer.callback_data = buffer => { console.log("Sending voice data"); connection.send_voice_data_raw(buffer, consumer.channels, consumer.sample_rate, true); //stream.write_data_rated(buffer.buffer, true, consumer.sample_rate); }; + */ } }, @@ -173,12 +181,15 @@ const do_connect = () => { }); connection.callback_voice_data = (buffer, client_id, codec_id, flag_head, packet_id) => { + console.log("Having data!"); connection.send_voice_data(buffer, codec_id, flag_head); - } + }; connection.callback_command = (command, arguments1, switches) => { console.log("Command: %s", command); } + + connection._voice_connection.register_client(7); }; do_connect(); diff --git a/package.json b/package.json index 4732d4f..d6e6bbc 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "crash_handler": "electron . crash-handler", "test": "echo \"Error: no test specified\" && exit 1", "start": "electron --js-flags='--expose-gc' --debug --dev-tools --disable-hardware-acceleration .", - "start-d": "electron . --debug -t --gdb -su http://dev.clientapi.teaspeak.de/", + "start-d": "electron . --disable-hardware-acceleration --debug -t -su http://dev.clientapi.teaspeak.de/", "start-d1": "electron . --disable-hardware-acceleration --debug -t --gdb -su http://clientapi.teaspeak.de/ --updater-ui-loader_type=0", "start-n": "electron . -t --disable-hardware-acceleration --no-single-instance -u=https://clientapi.teaspeak.de/ -d --updater-ui-loader_type=0", "start-01": "electron . --updater-channel=test -u=http://dev.clientapi.teaspeak.de/ -d --updater-ui-loader_type=0 --updater-local-version=1.0.1", @@ -48,6 +48,7 @@ "aws4": "^1.8.0", "electron": "5.0.6", "electron-installer-windows": "^1.1.1", + "electron-navigation": "^1.5.8", "electron-rebuild": "^1.8.5", "electron-winstaller": "^2.7.0", "electron-wix-msi": "^2.1.1",