Adjustments for the new client

This commit is contained in:
WolverinDEV 2019-08-21 10:00:27 +02:00
parent ef7e5d5f66
commit ea375bc07e
46 changed files with 2201 additions and 1099 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,28 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="index.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<title>TeaSpeak forum login</title>
</head>
<body>
<div class="inner-container">
<div class="box">
<h1>Login</h1>
<div id="login">
<a class="error">some error code</a>
<input type="text" placeholder="Username" id="user"/>
<input type="password" placeholder="Password" id="pass"/>
<button id="btn_login" target="#">Login</button>
<p>Create a account on <a href="https://forum.teaspeak.de">forum.teaspeak.de</a></p>
</div>
<div id="success">
<a> Successful logged in!</a><br>
<a>You will be redirected in 3 seconds</a>
</div>
</div>
</div>
<script>const exports = {};</script>
<script src="index.js"></script>
</body>
</html>

View File

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

View File

@ -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<UserData> {
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);
}
//<i class="fa fa-circle-o-notch fa-spin" id="login-loader"></i>
$("#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 = (<any>event.target).href;
require("electron").shell.openExternal(link);
});
}

View File

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

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<title>TeaClient - URL preview</title>
<style>
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
</style>
<link rel="stylesheet" type="text/css" href="./index.css">
</head>
<body>
<div id="nav-body-ctrls">
<!-- address -->
</div>
<div id="nav-body-tabs">
<!-- tabs -->
</div>
<div id="nav-body-views">
<!-- view -->
</div>
<script>
let exports = {};
</script>
<script src="./index.js"></script>
</body>
</html>

View File

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

View File

@ -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 =
"<div style='position: fixed; top: 0; bottom: 0; left: 0; right: 0; z-index: 99999999999999999999999999;'>" +
"<div style='\n" +
"font-family: \"Open Sans\"," +
"sans-serif;\n" +
"width: 100%;\n" +
"margin: 0;\n" +
"height: 40px;\n" +
"font-size: 17px;\n" +
"font-weight: 400;\n" +
"padding: .33em .5em;\n" +
"color: #5c5e60;\n" +
"position: fixed;\n" +
"background-color: white;\n" +
"box-shadow: 0 1px 3px 2px rgba(0,0,0,0.15);" +
"display: flex;\n" +
"flex-direction: row;\n" +
"justify-content: center;" +
"align-items: center;'" +
">" +
"<div style='margin-right: .67em;display: inline-block;line-height: 1.3;text-align: center'>You're in TeaWeb website preview mode. Click <a href='#' class='button-open'>here</a> to open the website in the browser</div>" +
"</div>" +
"<div style='display: table-cell;width: 1.6em;'>" +
"<a style='font-size: 14px;\n" +
"top: 13px;\n" +
"right: 25px;\n" +
"width: 15px;\n" +
"height: 15px;\n" +
"opacity: .3;\n" +
"color: #000;\n" +
"cursor: pointer;\n" +
"position: absolute;\n" +
"text-align: center;\n" +
"line-height: 15px;\n" +
"z-index: 1000;\n" +
"text-decoration: none;'" +
"class='button-close'>" +
"✖" +
"</a>" +
"</div>" +
"</div>";
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) {
(<HTMLElement>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) {
(<HTMLElement>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) =>
"<div style='background-color: whitesmoke; padding: 40px; margin: 20px; font-family: consolas,serif;'>" +
"<h2 align=center>Oops, this page failed to load correctly.</h2>" +
"<p align=center><i>ERROR [ " + error_code + ", " + error_desc + " ]</i></p>" +
'<br/><hr/>' +
'<h4>Try this</h4>' +
'<li type=circle>Check your spelling - <b>"' + url + '".</b></li><br/>' +
'<li type=circle><a href="javascript:location.reload();">Refresh</a> the page.</li><br/>' +
'<li type=circle>Perform a <a href=javascript:location.href="https://www.google.com/search?q=' + url + '">search</a> instead.</li><br/>' +
"</div>";
__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();
};

View File

@ -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<void>;
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) => {

View File

@ -1,78 +0,0 @@
const log_prefix = "[TeaSpeak::Preview] ";
const object =
"<div style='position: fixed; top: 0; bottom: 0; left: 0; right: 0; z-index: 99999999999999999999999999;'>" +
"<div style='\n" +
"font-family: \"Open Sans\"," +
"sans-serif;\n" +
"width: 100%;\n" +
"margin: 0;\n" +
"height: 40px;\n" +
"font-size: 17px;\n" +
"font-weight: 400;\n" +
"padding: .33em .5em;\n" +
"color: #5c5e60;\n" +
"position: fixed;\n" +
"background-color: white;\n" +
"box-shadow: 0 1px 3px 2px rgba(0,0,0,0.15);" +
"display: flex;\n" +
"flex-direction: row;\n" +
"justify-content: center;" +
"align-items: center;'" +
">" +
"<div style='margin-right: .67em;display: inline-block;line-height: 1.3;text-align: center'>You're in TeaWeb website preview mode. Click <a href='#' class='button-open'>here</a> to open the website in the browser</div>" +
"</div>" +
"<div style='display: table-cell;width: 1.6em;'>" +
"<a style='font-size: 14px;\n" +
"top: 13px;\n" +
"right: 25px;\n" +
"width: 15px;\n" +
"height: 15px;\n" +
"opacity: .3;\n" +
"color: #000;\n" +
"cursor: pointer;\n" +
"position: absolute;\n" +
"text-align: center;\n" +
"line-height: 15px;\n" +
"z-index: 1000;\n" +
"text-decoration: none;'" +
"class='button-close'>" +
"✖" +
"</a>" +
"</div>" +
"</div>";
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) {
(<HTMLElement>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) {
(<HTMLElement>element).onclick = event => {
console.info(log_prefix + "Opening URL with default browser");
require("electron").ipcRenderer.send('preview-action', {
action: 'open-url',
url: document.documentURI
});
};
}
}
}

View File

@ -1,7 +1,7 @@
/// <reference path="../imports/imports_shared.d.ts" />
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<void> {
async start(): Promise<audio.recorder.InputStartResult> {
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<audio.recorder.LevelMeter> {
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;
}
}
}

View File

@ -56,7 +56,8 @@ namespace _transfer {
status: 200,
statusText: "success",
headers: {
"X-media-bytes": base64ArrayBuffer(buffer)
"X-media-bytes": base64_encode_ab(buffer)
}
}));
}

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

251
modules/renderer/menu.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -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<chrono::milliseconds>(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<chrono::milliseconds>(end - begin).count());
}
return 0;
}

View File

@ -6,6 +6,7 @@
#include <iostream>
#include <functional>
#include <portaudio.h>
#include <misc/spin_lock.h>
#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<void(const void* /* buffer */, size_t /* samples */)> on_read;
private:
AudioConsumer(AudioInput* handle, size_t channel_count, size_t sample_rate, size_t frame_size);

View File

@ -86,7 +86,7 @@ ssize_t AudioOutputSource::enqueue_samples(const std::shared_ptr<tc::audio::Samp
this->sample_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;

View File

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

View File

@ -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<v8::Object> &obj) {

View File

@ -77,7 +77,7 @@ namespace tc {
void do_wrap(const v8::Local<v8::Object>& /* 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;
*/

View File

@ -68,6 +68,8 @@ AudioFilterWrapper::~AudioFilterWrapper() {
auto threshold_filter = dynamic_pointer_cast<filter::ThresholdFilter>(this->_filter);
if(threshold_filter)
threshold_filter->on_analyze = nullptr;
this->_callback_analyzed.Reset();
}
void AudioFilterWrapper::do_wrap(const v8::Local<v8::Object> &obj) {

View File

@ -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<AudioRecorderWrapper>(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<v8::String>("failed to open device (" + error + ")").ToLocalChecked());
return;
}
unique_ptr<Nan::Persistent<v8::Function>> _callback = make_unique<Nan::Persistent<v8::Function>>(info[1].As<v8::Function>());
unique_ptr<Nan::Persistent<v8::Object>> _recorder = make_unique<Nan::Persistent<v8::Object>>(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<v8::Value> 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<bool>(flag), std::forward<std::string>(error));
}).detach();
}
NAN_METHOD(AudioRecorderWrapper::_start) {
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(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<Nan::Persistent<v8::Function>> _callback = make_unique<Nan::Persistent<v8::Function>>(info[0].As<v8::Function>());
unique_ptr<Nan::Persistent<v8::Object>> _recorder = make_unique<Nan::Persistent<v8::Object>>(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<v8::Value> 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<AudioRecorderWrapper>(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<AudioConsumerWrapper>(info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked());
handle->delete_consumer(consumer);
}
NAN_METHOD(AudioRecorderWrapper::_set_volume) {
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(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<AudioRecorderWrapper>(info.Holder());
info.GetReturnValue().Set(handle->_input->volume());
}

View File

@ -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<AudioConsumerWrapper> create_consumer();
void delete_consumer(const AudioConsumerWrapper*);

View File

@ -444,7 +444,7 @@ bool ProtocolHandler::create_datagram_packets(std::vector<pipes::buffer> &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);

View File

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

View File

@ -60,29 +60,6 @@ void ProtocolHandler::handlePacketCommand(const std::shared_ptr<ts::protocol::Se
void ProtocolHandler::handlePacketVoice(const std::shared_ptr<ts::protocol::ServerPacket> &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<ServerConnection::VoicePacket>();
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<ts::protocol::ServerPacket> &packet) {

View File

@ -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<v8::String>(reason).ToLocalChecked();
callback->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, arguments);
}
NAN_METHOD(ServerConnection::_current_ping) {
auto connection = ObjectWrap::Unwrap<ServerConnection>(info.Holder());
auto& phandler = connection->protocol_handler;
if(phandler)
info.GetReturnValue().Set((uint32_t) chrono::floor<microseconds>(phandler->current_ping()).count());
else
info.GetReturnValue().Set(-1);
}

View File

@ -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<Nan::Callback> callback_connect;
std::unique_ptr<Nan::Callback> callback_disconnect;

View File

@ -208,12 +208,27 @@ VoiceClientWrap::~VoiceClientWrap() {}
VoiceClient::VoiceClient(const std::shared_ptr<VoiceConnection>&, 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<EncodedBuffer> &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<EncodedBuffer> &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;
}

View File

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

View File

@ -299,6 +299,7 @@ void VoiceConnection::process_packet(const std::shared_ptr<ts::protocol::ServerP
auto flag_head = packet->has_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);

View File

@ -1,7 +1,12 @@
/// <reference path="../../exports/exports.d.ts" />
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();

View File

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