Adjustments for the new client
This commit is contained in:
parent
ef7e5d5f66
commit
ea375bc07e
@ -1,7 +1,6 @@
|
|||||||
// Quit when all windows are closed.
|
// Quit when all windows are closed.
|
||||||
import * as electron from "electron";
|
import * as electron from "electron";
|
||||||
import * as app_updater from "./app-updater";
|
import * as app_updater from "./app-updater";
|
||||||
import * as forum from "./teaspeak-forum";
|
|
||||||
|
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import MessageBoxOptions = electron.MessageBoxOptions;
|
import MessageBoxOptions = electron.MessageBoxOptions;
|
||||||
@ -9,6 +8,7 @@ import MessageBoxOptions = electron.MessageBoxOptions;
|
|||||||
import {process_args, parse_arguments, Arguments} from "../shared/process-arguments";
|
import {process_args, parse_arguments, Arguments} from "../shared/process-arguments";
|
||||||
import {open as open_changelog} from "./app-updater/changelog";
|
import {open as open_changelog} from "./app-updater/changelog";
|
||||||
import * as crash_handler from "../crash_handler";
|
import * as crash_handler from "../crash_handler";
|
||||||
|
import {open_preview} from "./url-preview";
|
||||||
|
|
||||||
async function execute_app() {
|
async function execute_app() {
|
||||||
/* legacy, will be removed soon */
|
/* 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 {
|
try {
|
||||||
{
|
{
|
||||||
const version = await app_updater.current_version();
|
const version = await app_updater.current_version();
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {BrowserWindow, Menu, MenuItem, MessageBoxOptions, app, dialog} from "electron";
|
import {BrowserWindow, Menu, MenuItem, MessageBoxOptions, app, dialog} from "electron";
|
||||||
import * as electron from "electron";
|
import * as electron from "electron";
|
||||||
import * as winmgr from "./window";
|
import * as winmgr from "./window";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
export let prevent_instant_close: boolean = true;
|
export let prevent_instant_close: boolean = true;
|
||||||
export function set_prevent_instant_close(flag: boolean) {
|
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 {Arguments, parse_arguments, process_args} from "../shared/process-arguments";
|
||||||
import * as updater from "./app-updater";
|
import * as updater from "./app-updater";
|
||||||
import * as loader from "./ui-loader";
|
import * as loader from "./ui-loader";
|
||||||
import {open as open_changelog} from "./app-updater/changelog";
|
|
||||||
import * as crash_handler from "../crash_handler";
|
import * as crash_handler from "../crash_handler";
|
||||||
|
|
||||||
// Keep a global reference of the window object, if you don't, the window will
|
// 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.
|
// be closed automatically when the JavaScript object is garbage collected.
|
||||||
export let main_window: BrowserWindow = null;
|
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) {
|
function spawn_main_window(entry_point: string) {
|
||||||
// Create the browser window.
|
// Create the browser window.
|
||||||
console.log("Spawning main window");
|
console.log("Spawning main window");
|
||||||
@ -112,17 +32,15 @@ function spawn_main_window(entry_point: string) {
|
|||||||
nodeIntegrationInWorker: true,
|
nodeIntegrationInWorker: true,
|
||||||
nodeIntegration: 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 => {
|
main_window.webContents.on('devtools-closed', event => {
|
||||||
console.log("Dev tools destroyed!");
|
console.log("Dev tools destroyed!");
|
||||||
});
|
});
|
||||||
|
|
||||||
main_window.on('closed', () => {
|
main_window.on('closed', () => {
|
||||||
|
require("./url-preview").close();
|
||||||
main_window = null;
|
main_window = null;
|
||||||
prevent_instant_close = false;
|
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) => {
|
main_window.webContents.on('new-window', (event, url, frameName, disposition, options, additionalFeatures) => {
|
||||||
console.log("Got new window " + frameName);
|
console.log("Got new window " + frameName);
|
||||||
if (frameName === 'teaforo-login') {
|
const url_preview = require("./url-preview");
|
||||||
// open window as modal
|
url_preview.open_preview(url);
|
||||||
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);
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -245,6 +150,10 @@ export function execute() {
|
|||||||
|
|
||||||
Menu.setApplicationMenu(null);
|
Menu.setApplicationMenu(null);
|
||||||
init_listener();
|
init_listener();
|
||||||
|
|
||||||
|
console.log("Setting up render backend");
|
||||||
|
require("./render-backend");
|
||||||
|
|
||||||
console.log("Spawn loading screen");
|
console.log("Spawn loading screen");
|
||||||
loader.ui.execute_loader().then(async (entry_point: string) => {
|
loader.ui.execute_loader().then(async (entry_point: string) => {
|
||||||
/* test if the updater may have an update found */
|
/* test if the updater may have an update found */
|
||||||
|
22
modules/core/render-backend/index.ts
Normal file
22
modules/core/render-backend/index.ts
Normal 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();
|
||||||
|
}
|
||||||
|
});
|
34
modules/core/render-backend/menu.ts
Normal file
34
modules/core/render-backend/menu.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
@ -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 */
|
|
@ -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"}
|
|
@ -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>
|
|
@ -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;
|
|
||||||
}
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
86
modules/core/url-preview/html/index.css
Normal file
86
modules/core/url-preview/html/index.css
Normal 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
|
||||||
|
}
|
34
modules/core/url-preview/html/index.html
Normal file
34
modules/core/url-preview/html/index.html
Normal 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>
|
78
modules/core/url-preview/html/index.ts
Normal file
78
modules/core/url-preview/html/index.ts
Normal 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));
|
118
modules/core/url-preview/html/inject.ts
Normal file
118
modules/core/url-preview/html/inject.ts
Normal 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();
|
||||||
|
};
|
@ -1,36 +1,85 @@
|
|||||||
import * as electron from "electron";
|
import * as electron from "electron";
|
||||||
import * as fs from "fs";
|
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import * as winmgr from "../window";
|
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) {
|
export async function open_preview(url: string) {
|
||||||
console.log("Open URL as preview: %s", url);
|
while(global_window_promise) {
|
||||||
const window = new electron.BrowserWindow({
|
try {
|
||||||
webPreferences: {
|
await global_window_promise;
|
||||||
webSecurity: true,
|
break;
|
||||||
nodeIntegration: false,
|
} catch(error) {} /* error will be already logged */
|
||||||
nodeIntegrationInWorker: false,
|
}
|
||||||
allowRunningInsecureContent: false,
|
if(!global_window) {
|
||||||
},
|
global_window_promise = (async () => {
|
||||||
skipTaskbar: true,
|
global_window = new electron.BrowserWindow({
|
||||||
center: true,
|
webPreferences: {
|
||||||
});
|
nodeIntegration: true,
|
||||||
await winmgr.apply_bounds('url-preview', window);
|
webviewTag: true
|
||||||
winmgr.track_bounds('url-preview', window);
|
},
|
||||||
window.setMenu(null);
|
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(() => {
|
try {
|
||||||
//window.webContents.openDevTools();
|
await winmgr.apply_bounds('url-preview', global_window);
|
||||||
});
|
winmgr.track_bounds('url-preview', global_window);
|
||||||
|
|
||||||
//FIXME try catch?
|
await new Promise((resolve, reject) => {
|
||||||
const inject_file = path.join(path.dirname(module.filename), "inject.js");
|
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();
|
global_window.show();
|
||||||
window.webContents.once('dom-ready', e => {
|
})();
|
||||||
const code_inject = fs.readFileSync(inject_file).toString();
|
try {
|
||||||
window.webContents.executeJavaScript(code_inject, true);
|
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) => {
|
electron.ipcMain.on('preview-action', (event, args) => {
|
||||||
|
@ -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
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
/// <reference path="../imports/imports_shared.d.ts" />
|
|
||||||
window["require_setup"](module);
|
window["require_setup"](module);
|
||||||
|
|
||||||
import {audio as naudio} from "teaclient_connection";
|
import {audio as naudio} from "teaclient_connection";
|
||||||
|
import {audio, tr} from "../imports/imports_shared";
|
||||||
|
|
||||||
export namespace _audio.recorder {
|
export namespace _audio.recorder {
|
||||||
import InputDevice = audio.recorder.InputDevice;
|
import InputDevice = audio.recorder.InputDevice;
|
||||||
@ -20,8 +20,9 @@ export namespace _audio.recorder {
|
|||||||
default_input: e.input_default,
|
default_input: e.input_default,
|
||||||
supported: e.input_supported,
|
supported: e.input_supported,
|
||||||
name: e.name,
|
name: e.name,
|
||||||
|
driver: e.driver,
|
||||||
sample_rate: 44100, /* TODO! */
|
sample_rate: 44100, /* TODO! */
|
||||||
device_index: e.device_index
|
device_index: e.device_index,
|
||||||
} as NativeDevice
|
} as NativeDevice
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -67,7 +68,6 @@ export namespace _audio.recorder {
|
|||||||
get(): any {
|
get(): any {
|
||||||
return this._callback_level;
|
return this._callback_level;
|
||||||
}, set(v: any): void {
|
}, set(v: any): void {
|
||||||
console.log("SET CALLBACK LEVEL! %o", v);
|
|
||||||
if(v === this._callback_level)
|
if(v === this._callback_level)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -272,9 +272,25 @@ export namespace _audio.recorder {
|
|||||||
|
|
||||||
const device = _device as NativeDevice; /* TODO: test for? */
|
const device = _device as NativeDevice; /* TODO: test for? */
|
||||||
this._current_device = _device;
|
this._current_device = _device;
|
||||||
this.handle.set_device(device ? device.device_index : -1);
|
|
||||||
try {
|
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) {
|
} catch(error) {
|
||||||
console.warn(tr("Failed to start playback on new input device (%o)"), error);
|
console.warn(tr("Failed to start playback on new input device (%o)"), error);
|
||||||
throw error;
|
throw error;
|
||||||
@ -345,7 +361,7 @@ export namespace _audio.recorder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<audio.recorder.InputStartResult> {
|
||||||
try {
|
try {
|
||||||
await this.stop();
|
await this.stop();
|
||||||
} catch(error) {
|
} 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)
|
for(const filter of this.filters)
|
||||||
if(filter.is_enabled())
|
if(filter.is_enabled())
|
||||||
filter.initialize();
|
filter.initialize();
|
||||||
|
return audio.recorder.InputStartResult.EOK;
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
this._current_state = audio.recorder.InputState.PAUSED;
|
this._current_state = audio.recorder.InputState.PAUSED;
|
||||||
throw error;
|
throw error;
|
||||||
@ -386,6 +410,93 @@ export namespace _audio.recorder {
|
|||||||
this.callback_end();
|
this.callback_end();
|
||||||
this._current_state = audio.recorder.InputState.PAUSED;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,8 @@ namespace _transfer {
|
|||||||
status: 200,
|
status: 200,
|
||||||
statusText: "success",
|
statusText: "success",
|
||||||
headers: {
|
headers: {
|
||||||
"X-media-bytes": base64ArrayBuffer(buffer)
|
"X-media-bytes": base64_encode_ab(buffer)
|
||||||
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -284,6 +284,12 @@ export namespace _connection {
|
|||||||
});
|
});
|
||||||
return this._command_handler_default.proxy_command_promise(promise, options);
|
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");
|
console.log("Spawning native connection");
|
||||||
return new native.ServerConnection(handle); /* will be overridden by the client */
|
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);
|
Object.assign(window["connection"] || (window["connection"] = {}), _connection);
|
@ -1,3 +1,5 @@
|
|||||||
|
import {class_to_image} from "./icon-helper";
|
||||||
|
|
||||||
window["require_setup"](module);
|
window["require_setup"](module);
|
||||||
|
|
||||||
import * as electron from "electron";
|
import * as electron from "electron";
|
||||||
@ -5,14 +7,11 @@ const remote = electron.remote;
|
|||||||
const {Menu, MenuItem} = remote;
|
const {Menu, MenuItem} = remote;
|
||||||
|
|
||||||
import {isFunction} from "util";
|
import {isFunction} from "util";
|
||||||
import NativeImage = electron.NativeImage;
|
|
||||||
|
|
||||||
class ElectronContextMenu implements contextmenu.ContextMenuProvider {
|
class ElectronContextMenu implements contextmenu.ContextMenuProvider {
|
||||||
private _close_listeners: (() => any)[] = [];
|
private _close_listeners: (() => any)[] = [];
|
||||||
private _current_menu: electron.Menu;
|
private _current_menu: electron.Menu;
|
||||||
|
|
||||||
private _icon_mash_url: string;
|
|
||||||
private _icon_mask_img: NativeImage;
|
|
||||||
private _div: JQuery;
|
private _div: JQuery;
|
||||||
|
|
||||||
despawn_context_menu() {
|
despawn_context_menu() {
|
||||||
@ -21,72 +20,22 @@ class ElectronContextMenu implements contextmenu.ContextMenuProvider {
|
|||||||
this._current_menu.closePopup();
|
this._current_menu.closePopup();
|
||||||
this._current_menu = undefined;
|
this._current_menu = undefined;
|
||||||
|
|
||||||
for(const listener of this._close_listeners)
|
for(const listener of this._close_listeners) {
|
||||||
listener();
|
if(listener) {
|
||||||
|
listener();
|
||||||
|
}
|
||||||
|
}
|
||||||
this._close_listeners = [];
|
this._close_listeners = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
finalize() {
|
finalize() {
|
||||||
this._icon_mask_img = undefined;
|
|
||||||
this._icon_mash_url = undefined;
|
|
||||||
if(this._div) this._div.detach();
|
if(this._div) this._div.detach();
|
||||||
this._div = undefined;
|
this._div = undefined;
|
||||||
this._cache_klass_map = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize() {
|
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 _entry_id = 0;
|
||||||
private build_menu(entry: contextmenu.MenuEntry) : electron.MenuItem {
|
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,
|
label: (isFunction(entry.name) ? (entry.name as (() => string))() : entry.name) as string,
|
||||||
type: "normal",
|
type: "normal",
|
||||||
click: click_callback,
|
click: click_callback,
|
||||||
icon: this.class_to_image(entry.icon_class),
|
icon: class_to_image(entry.icon_class),
|
||||||
visible: entry.visible,
|
visible: entry.visible,
|
||||||
enabled: !entry.disabled
|
enabled: !entry.disabled
|
||||||
});
|
});
|
||||||
@ -128,7 +77,7 @@ class ElectronContextMenu implements contextmenu.ContextMenuProvider {
|
|||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
checked: !!entry.checkbox_checked,
|
checked: !!entry.checkbox_checked,
|
||||||
click: click_callback,
|
click: click_callback,
|
||||||
icon: this.class_to_image(entry.icon_class),
|
icon: class_to_image(entry.icon_class),
|
||||||
visible: entry.visible,
|
visible: entry.visible,
|
||||||
enabled: !entry.disabled
|
enabled: !entry.disabled
|
||||||
});
|
});
|
||||||
@ -146,7 +95,7 @@ class ElectronContextMenu implements contextmenu.ContextMenuProvider {
|
|||||||
type: "submenu",
|
type: "submenu",
|
||||||
submenu: sub_menu,
|
submenu: sub_menu,
|
||||||
click: click_callback,
|
click: click_callback,
|
||||||
icon: this.class_to_image(entry.icon_class),
|
icon: class_to_image(entry.icon_class),
|
||||||
visible: entry.visible,
|
visible: entry.visible,
|
||||||
enabled: !entry.disabled
|
enabled: !entry.disabled
|
||||||
});
|
});
|
||||||
|
63
modules/renderer/icon-helper.ts
Normal file
63
modules/renderer/icon-helper.ts
Normal 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;
|
||||||
|
}
|
1395
modules/renderer/imports/.copy_imports_shared.d.ts
vendored
1395
modules/renderer/imports/.copy_imports_shared.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@ -170,17 +170,15 @@ const module_loader_setup = async () => {
|
|||||||
window["require_setup"] = _mod => {
|
window["require_setup"] = _mod => {
|
||||||
if(!_mod || !_mod.paths) return;
|
if(!_mod || !_mod.paths) return;
|
||||||
|
|
||||||
console.dir(_mod);
|
|
||||||
|
|
||||||
_mod.paths.push(...native_paths);
|
_mod.paths.push(...native_paths);
|
||||||
const org_req = _mod.__proto__.require;
|
const original_require = _mod.__proto__.require;
|
||||||
if(!_mod.proxied) {
|
if(!_mod.proxied) {
|
||||||
_mod.require = function a(m) {
|
_mod.require = (path: string) => {
|
||||||
let stack = new Error().stack;
|
if(path.endsWith("imports/imports_shared")) {
|
||||||
if(stack.startsWith("Error"))
|
console.log("Proxy require for %s. Using 'window' as result.", path);
|
||||||
stack = stack.substr(6);
|
return window;
|
||||||
//console.log("require \"%s\"\nStack:\n%s", m, stack);
|
}
|
||||||
return org_req.apply(_mod, [m]);
|
return original_require.apply(_mod, [path]);
|
||||||
};
|
};
|
||||||
_mod.proxied = true;
|
_mod.proxied = true;
|
||||||
}
|
}
|
||||||
@ -202,16 +200,24 @@ const load_modules = async () => {
|
|||||||
console.log("Loading native extensions...");
|
console.log("Loading native extensions...");
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
require("./ppt");
|
require("./version");
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
console.error("Failed to load ppt");
|
console.error("Failed to load version extension");
|
||||||
console.dir(error);
|
console.dir(error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
require("./version");
|
const helper = require("./icon-helper");
|
||||||
|
await helper.initialize();
|
||||||
} catch(error) {
|
} 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);
|
console.dir(error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -236,6 +242,13 @@ const load_modules = async () => {
|
|||||||
console.dir(error);
|
console.dir(error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
require("./menu");
|
||||||
|
} catch(error) {
|
||||||
|
console.error("Failed to load menu extension");
|
||||||
|
console.dir(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
require("./context-menu");
|
require("./context-menu");
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
|
251
modules/renderer/menu.ts
Normal file
251
modules/renderer/menu.ts
Normal 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 {};
|
@ -92,7 +92,7 @@ namespace _ppt {
|
|||||||
current_state.code = event.key_code;
|
current_state.code = event.key_code;
|
||||||
|
|
||||||
for(const hook of key_hooks) {
|
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_alt != event.key_alt) continue;
|
||||||
if(hook.key_ctrl != event.key_ctrl) continue;
|
if(hook.key_ctrl != event.key_ctrl) continue;
|
||||||
if(hook.key_shift != event.key_shift) continue;
|
if(hook.key_shift != event.key_shift) continue;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
set(MODULE_NAME "teaclient_connection")
|
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
|
set(SOURCE_FILES
|
||||||
src/logger.cpp
|
src/logger.cpp
|
||||||
src/EventLoop.cpp
|
src/EventLoop.cpp
|
||||||
|
10
native/serverconnection/exports/exports.d.ts
vendored
10
native/serverconnection/exports/exports.d.ts
vendored
@ -69,6 +69,9 @@ declare module "teaclient_connection" {
|
|||||||
send_command(command: string, arguments: any[], switches: string[]);
|
send_command(command: string, arguments: any[], switches: string[]);
|
||||||
send_voice_data(buffer: Uint8Array, codec_id: number, header: boolean);
|
send_voice_data(buffer: Uint8Array, codec_id: number, header: boolean);
|
||||||
send_voice_data_raw(buffer: Float32Array, channels: number, sample_rate: 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;
|
export function spawn_server_connection() : NativeServerConnection;
|
||||||
@ -223,12 +226,15 @@ declare module "teaclient_connection" {
|
|||||||
|
|
||||||
export interface AudioRecorder {
|
export interface AudioRecorder {
|
||||||
get_device() : number;
|
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;
|
started() : boolean;
|
||||||
stop();
|
stop();
|
||||||
|
|
||||||
|
get_volume() : number;
|
||||||
|
set_volume(volume: number);
|
||||||
|
|
||||||
create_consumer() : AudioConsumer;
|
create_consumer() : AudioConsumer;
|
||||||
get_consumers() : AudioConsumer[];
|
get_consumers() : AudioConsumer[];
|
||||||
delete_consumer(consumer: AudioConsumer);
|
delete_consumer(consumer: AudioConsumer);
|
||||||
|
@ -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) {
|
void AudioConsumer::handle_framed_data(const void *buffer, size_t samples) {
|
||||||
unique_lock read_callback_lock(this->on_read_lock);
|
unique_lock read_callback_lock(this->on_read_lock);
|
||||||
if(!this->on_read)
|
|
||||||
return;
|
|
||||||
auto function = this->on_read; /* copy */
|
auto function = this->on_read; /* copy */
|
||||||
read_callback_lock.unlock();
|
read_callback_lock.unlock();
|
||||||
|
if(!function)
|
||||||
|
return;
|
||||||
|
|
||||||
function(buffer, samples);
|
function(buffer, samples);
|
||||||
}
|
}
|
||||||
@ -52,8 +52,10 @@ bool AudioInput::open_device(std::string& error, PaDeviceIndex index) {
|
|||||||
|
|
||||||
this->close_device();
|
this->close_device();
|
||||||
this->_current_device_index = index;
|
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) {
|
if(!this->_current_device) {
|
||||||
this->_current_device_index = paNoDevice;
|
this->_current_device_index = paNoDevice;
|
||||||
error = "failed to get device info";
|
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) {
|
int AudioInput::audio_callback(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags) {
|
||||||
if (!input) /* hmmm.. suspicious */
|
if (!input) /* hmmm.. suspicious */
|
||||||
return 0;
|
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()) {
|
for(const auto& consumer : this->consumers()) {
|
||||||
consumer->process_data(input, frameCount);
|
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;
|
return 0;
|
||||||
}
|
}
|
@ -6,6 +6,7 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <portaudio.h>
|
#include <portaudio.h>
|
||||||
|
#include <misc/spin_lock.h>
|
||||||
#include "AudioSamples.h"
|
#include "AudioSamples.h"
|
||||||
|
|
||||||
namespace tc {
|
namespace tc {
|
||||||
@ -23,7 +24,7 @@ namespace tc {
|
|||||||
|
|
||||||
size_t const frame_size = 0;
|
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;
|
std::function<void(const void* /* buffer */, size_t /* samples */)> on_read;
|
||||||
private:
|
private:
|
||||||
AudioConsumer(AudioInput* handle, size_t channel_count, size_t sample_rate, size_t frame_size);
|
AudioConsumer(AudioInput* handle, size_t channel_count, size_t sample_rate, size_t frame_size);
|
||||||
|
@ -86,7 +86,7 @@ ssize_t AudioOutputSource::enqueue_samples(const std::shared_ptr<tc::audio::Samp
|
|||||||
this->sample_buffers.clear();
|
this->sample_buffers.clear();
|
||||||
break;
|
break;
|
||||||
case overflow_strategy::discard_buffer_half:
|
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;
|
break;
|
||||||
case overflow_strategy::ignore:
|
case overflow_strategy::ignore:
|
||||||
break;
|
break;
|
||||||
|
@ -36,8 +36,10 @@ void Reframer::process(const void *source, size_t samples) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto _on_frame = this->on_frame;
|
||||||
while(samples > this->_frame_size) {
|
while(samples > this->_frame_size) {
|
||||||
this->on_frame(source);
|
if(_on_frame)
|
||||||
|
_on_frame(source);
|
||||||
samples -= this->_frame_size;
|
samples -= this->_frame_size;
|
||||||
source = (char*) source + this->_frame_size * this->_channels * 4;
|
source = (char*) source + this->_frame_size * this->_channels * 4;
|
||||||
}
|
}
|
||||||
|
@ -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); };
|
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() {
|
AudioConsumerWrapper::~AudioConsumerWrapper() {
|
||||||
@ -53,8 +55,10 @@ AudioConsumerWrapper::~AudioConsumerWrapper() {
|
|||||||
this->_handle = nullptr;
|
this->_handle = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef DO_DEADLOCK_REF
|
||||||
if(this->_recorder)
|
if(this->_recorder)
|
||||||
this->_recorder->js_unref();
|
this->_recorder->js_unref();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioConsumerWrapper::do_wrap(const v8::Local<v8::Object> &obj) {
|
void AudioConsumerWrapper::do_wrap(const v8::Local<v8::Object> &obj) {
|
||||||
|
@ -77,7 +77,7 @@ namespace tc {
|
|||||||
void do_wrap(const v8::Local<v8::Object>& /* object */);
|
void do_wrap(const v8::Local<v8::Object>& /* object */);
|
||||||
|
|
||||||
void unbind(); /* called with execute_lock locked */
|
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 {
|
struct DataEntry {
|
||||||
void* buffer = nullptr;
|
void* buffer = nullptr;
|
||||||
@ -96,7 +96,6 @@ namespace tc {
|
|||||||
Nan::callback_t<> _call_ended;
|
Nan::callback_t<> _call_ended;
|
||||||
Nan::callback_t<> _call_started;
|
Nan::callback_t<> _call_started;
|
||||||
/*
|
/*
|
||||||
*
|
|
||||||
callback_data: (buffer: Float32Array) => any;
|
callback_data: (buffer: Float32Array) => any;
|
||||||
callback_ended: () => any;
|
callback_ended: () => any;
|
||||||
*/
|
*/
|
||||||
|
@ -68,6 +68,8 @@ AudioFilterWrapper::~AudioFilterWrapper() {
|
|||||||
auto threshold_filter = dynamic_pointer_cast<filter::ThresholdFilter>(this->_filter);
|
auto threshold_filter = dynamic_pointer_cast<filter::ThresholdFilter>(this->_filter);
|
||||||
if(threshold_filter)
|
if(threshold_filter)
|
||||||
threshold_filter->on_analyze = nullptr;
|
threshold_filter->on_analyze = nullptr;
|
||||||
|
|
||||||
|
this->_callback_analyzed.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioFilterWrapper::do_wrap(const v8::Local<v8::Object> &obj) {
|
void AudioFilterWrapper::do_wrap(const v8::Local<v8::Object> &obj) {
|
||||||
|
@ -34,6 +34,9 @@ NAN_MODULE_INIT(AudioRecorderWrapper::Init) {
|
|||||||
Nan::SetPrototypeMethod(klass, "started", AudioRecorderWrapper::_started);
|
Nan::SetPrototypeMethod(klass, "started", AudioRecorderWrapper::_started);
|
||||||
Nan::SetPrototypeMethod(klass, "stop", AudioRecorderWrapper::_stop);
|
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, "get_consumers", AudioRecorderWrapper::_get_consumers);
|
||||||
Nan::SetPrototypeMethod(klass, "create_consumer", AudioRecorderWrapper::_create_consumer);
|
Nan::SetPrototypeMethod(klass, "create_consumer", AudioRecorderWrapper::_create_consumer);
|
||||||
Nan::SetPrototypeMethod(klass, "delete_consumer", AudioRecorderWrapper::_delete_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 handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
||||||
auto input = handle->_input;
|
auto input = handle->_input;
|
||||||
|
|
||||||
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
if(info.Length() != 2 || !info[0]->IsNumber() || !info[1]->IsFunction()) {
|
||||||
Nan::ThrowError("invalid argument");
|
Nan::ThrowError("invalid arguments");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto device_id = info[0]->Int32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
auto device_id = info[0]->Int32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||||
|
|
||||||
string error;
|
unique_ptr<Nan::Persistent<v8::Function>> _callback = make_unique<Nan::Persistent<v8::Function>>(info[1].As<v8::Function>());
|
||||||
if(!input->open_device(error, device_id)) {
|
unique_ptr<Nan::Persistent<v8::Object>> _recorder = make_unique<Nan::Persistent<v8::Object>>(info.Holder());
|
||||||
Nan::ThrowError(Nan::New<v8::String>("failed to open device (" + error + ")").ToLocalChecked());
|
|
||||||
return;
|
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) {
|
NAN_METHOD(AudioRecorderWrapper::_start) {
|
||||||
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
if(info.Length() != 1) {
|
||||||
auto input = handle->_input;
|
Nan::ThrowError("missing callback");
|
||||||
|
|
||||||
if(!input->record()) {
|
|
||||||
Nan::ThrowError("failed to start");
|
|
||||||
return;
|
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) {
|
NAN_METHOD(AudioRecorderWrapper::_started) {
|
||||||
@ -203,4 +247,20 @@ NAN_METHOD(AudioRecorderWrapper::_delete_consumer) {
|
|||||||
|
|
||||||
auto consumer = ObjectWrap::Unwrap<AudioConsumerWrapper>(info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked());
|
auto consumer = ObjectWrap::Unwrap<AudioConsumerWrapper>(info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked());
|
||||||
handle->delete_consumer(consumer);
|
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());
|
||||||
}
|
}
|
@ -38,6 +38,9 @@ namespace tc {
|
|||||||
static NAN_METHOD(_get_consumers);
|
static NAN_METHOD(_get_consumers);
|
||||||
static NAN_METHOD(_delete_consumer);
|
static NAN_METHOD(_delete_consumer);
|
||||||
|
|
||||||
|
static NAN_METHOD(_set_volume);
|
||||||
|
static NAN_METHOD(_get_volume);
|
||||||
|
|
||||||
std::shared_ptr<AudioConsumerWrapper> create_consumer();
|
std::shared_ptr<AudioConsumerWrapper> create_consumer();
|
||||||
void delete_consumer(const AudioConsumerWrapper*);
|
void delete_consumer(const AudioConsumerWrapper*);
|
||||||
|
|
||||||
|
@ -444,7 +444,7 @@ bool ProtocolHandler::create_datagram_packets(std::vector<pipes::buffer> &result
|
|||||||
} else {
|
} else {
|
||||||
packet->applyPacketId(this->_packet_id_manager);
|
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)) {
|
if(!this->crypt_handler.progressPacketOut(packet.get(), error, false)) {
|
||||||
log_error(category::connection, tr("Failed to encrypt packet: {}"), error);
|
log_error(category::connection, tr("Failed to encrypt packet: {}"), error);
|
||||||
|
@ -75,6 +75,8 @@ namespace tc {
|
|||||||
|
|
||||||
ecc_key& get_identity_key() { return this->crypto.identity; }
|
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;
|
connection_state::value connection_state = connection_state::INITIALIZING;
|
||||||
server_type::value server_type = server_type::TEASPEAK;
|
server_type::value server_type = server_type::TEASPEAK;
|
||||||
private:
|
private:
|
||||||
|
@ -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) {
|
void ProtocolHandler::handlePacketVoice(const std::shared_ptr<ts::protocol::ServerPacket> &packet) {
|
||||||
this->handle->voice_connection->process_packet(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) {
|
void ProtocolHandler::handlePacketPing(const std::shared_ptr<ts::protocol::ServerPacket> &packet) {
|
||||||
|
@ -58,6 +58,7 @@ NAN_MODULE_INIT(ServerConnection::Init) {
|
|||||||
Nan::SetPrototypeMethod(klass, "send_command", ServerConnection::_send_command);
|
Nan::SetPrototypeMethod(klass, "send_command", ServerConnection::_send_command);
|
||||||
Nan::SetPrototypeMethod(klass, "send_voice_data", ServerConnection::_send_voice_data);
|
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, "send_voice_data_raw", ServerConnection::_send_voice_data_raw);
|
||||||
|
Nan::SetPrototypeMethod(klass, "current_ping", ServerConnection::_current_ping);
|
||||||
|
|
||||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
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();
|
arguments[0] = Nan::New<v8::String>(reason).ToLocalChecked();
|
||||||
callback->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, arguments);
|
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);
|
||||||
}
|
}
|
@ -85,6 +85,7 @@ namespace tc {
|
|||||||
static NAN_METHOD(_send_voice_data);
|
static NAN_METHOD(_send_voice_data);
|
||||||
static NAN_METHOD(_send_voice_data_raw);
|
static NAN_METHOD(_send_voice_data_raw);
|
||||||
static NAN_METHOD(_error_message);
|
static NAN_METHOD(_error_message);
|
||||||
|
static NAN_METHOD(_current_ping);
|
||||||
|
|
||||||
std::unique_ptr<Nan::Callback> callback_connect;
|
std::unique_ptr<Nan::Callback> callback_connect;
|
||||||
std::unique_ptr<Nan::Callback> callback_disconnect;
|
std::unique_ptr<Nan::Callback> callback_disconnect;
|
||||||
|
@ -208,12 +208,27 @@ VoiceClientWrap::~VoiceClientWrap() {}
|
|||||||
|
|
||||||
VoiceClient::VoiceClient(const std::shared_ptr<VoiceConnection>&, uint16_t client_id) : _client_id(client_id) {
|
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 = global_audio_output->create_source();
|
||||||
this->output_source->overflow_strategy = audio::overflow_strategy::discard_buffer_half;
|
this->output_source->overflow_strategy = audio::overflow_strategy::ignore;
|
||||||
this->output_source->max_latency = (size_t) ceil(this->output_source->sample_rate * 0.12);
|
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->min_buffer = (size_t) ceil(this->output_source->sample_rate * 0.025);
|
||||||
|
|
||||||
this->output_source->on_underflow = [&]{
|
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->buffer = buffer.own_buffer();
|
||||||
encoded_buffer->head = head;
|
encoded_buffer->head = head;
|
||||||
|
|
||||||
|
this->_last_received_packet = encoded_buffer->receive_timestamp;
|
||||||
{
|
{
|
||||||
lock_guard lock(this->audio_decode_queue_lock);
|
lock_guard lock(this->audio_decode_queue_lock);
|
||||||
this->audio_decode_queue.push_back(move(encoded_buffer));
|
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;
|
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;
|
diff = 0xFFFF;
|
||||||
|
|
||||||
const auto old_packet_id = codec_data->last_packet_id;
|
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);
|
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);
|
this->set_state(state::playing);
|
||||||
//cout << "Enqueued " << enqueued << " samples" << endl;
|
|
||||||
}
|
}
|
@ -112,6 +112,7 @@ namespace tc {
|
|||||||
uint16_t _client_id;
|
uint16_t _client_id;
|
||||||
float _volume = 1.f;
|
float _volume = 1.f;
|
||||||
|
|
||||||
|
std::chrono::system_clock::time_point _last_received_packet;
|
||||||
state::value _state = state::stopped;
|
state::value _state = state::stopped;
|
||||||
inline void set_state(state::value value) {
|
inline void set_state(state::value value) {
|
||||||
if(value == this->_state)
|
if(value == this->_state)
|
||||||
|
@ -299,6 +299,7 @@ void VoiceConnection::process_packet(const std::shared_ptr<ts::protocol::ServerP
|
|||||||
auto flag_head = packet->has_flag(PacketFlag::Compressed);
|
auto flag_head = packet->has_flag(PacketFlag::Compressed);
|
||||||
//container->voice_data = packet->data().length() > 5 ? packet->data().range(5) : pipes::buffer{};
|
//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);
|
auto client = this->find_client(client_id);
|
||||||
if(!client) {
|
if(!client) {
|
||||||
log_warn(category::voice_connection, tr("Received voice packet from unknown client {}. Dropping packet!"), client_id);
|
log_warn(category::voice_connection, tr("Received voice packet from unknown client {}. Dropping packet!"), client_id);
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
/// <reference path="../../exports/exports.d.ts" />
|
/// <reference path="../../exports/exports.d.ts" />
|
||||||
|
console.log("HELLO WORLD");
|
||||||
module.paths.push("../../build/linux_x64");
|
module.paths.push("../../build/linux_x64");
|
||||||
module.paths.push("../../build/win32_64");
|
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";
|
import * as fs from "fs";
|
||||||
|
|
||||||
const original_require = require;
|
const original_require = require;
|
||||||
@ -118,8 +123,8 @@ const do_connect = () => {
|
|||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
remote_port: 9987,
|
remote_port: 9987,
|
||||||
//remote_host: "188.40.240.20", /* twerion */
|
//remote_host: "188.40.240.20", /* twerion */
|
||||||
remote_host: "localhost",
|
//remote_host: "localhost",
|
||||||
//remote_host: "ts.teaspeak.de",
|
remote_host: "ts.teaspeak.de",
|
||||||
//remote_host: "51.68.181.92",
|
//remote_host: "51.68.181.92",
|
||||||
//remote_host: "94.130.236.135",
|
//remote_host: "94.130.236.135",
|
||||||
//remote_host: "54.36.232.11", /* the beast */
|
//remote_host: "54.36.232.11", /* the beast */
|
||||||
@ -160,11 +165,14 @@ const do_connect = () => {
|
|||||||
console.log(">> Audio end");
|
console.log(">> Audio end");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
connection._voice_connection.set_audio_source(consumer);
|
||||||
|
/*
|
||||||
consumer.callback_data = buffer => {
|
consumer.callback_data = buffer => {
|
||||||
console.log("Sending voice data");
|
console.log("Sending voice data");
|
||||||
connection.send_voice_data_raw(buffer, consumer.channels, consumer.sample_rate, true);
|
connection.send_voice_data_raw(buffer, consumer.channels, consumer.sample_rate, true);
|
||||||
//stream.write_data_rated(buffer.buffer, true, consumer.sample_rate);
|
//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) => {
|
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.send_voice_data(buffer, codec_id, flag_head);
|
||||||
}
|
};
|
||||||
|
|
||||||
connection.callback_command = (command, arguments1, switches) => {
|
connection.callback_command = (command, arguments1, switches) => {
|
||||||
console.log("Command: %s", command);
|
console.log("Command: %s", command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connection._voice_connection.register_client(7);
|
||||||
};
|
};
|
||||||
do_connect();
|
do_connect();
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"crash_handler": "electron . crash-handler",
|
"crash_handler": "electron . crash-handler",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"start": "electron --js-flags='--expose-gc' --debug --dev-tools --disable-hardware-acceleration .",
|
"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-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-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",
|
"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",
|
"aws4": "^1.8.0",
|
||||||
"electron": "5.0.6",
|
"electron": "5.0.6",
|
||||||
"electron-installer-windows": "^1.1.1",
|
"electron-installer-windows": "^1.1.1",
|
||||||
|
"electron-navigation": "^1.5.8",
|
||||||
"electron-rebuild": "^1.8.5",
|
"electron-rebuild": "^1.8.5",
|
||||||
"electron-winstaller": "^2.7.0",
|
"electron-winstaller": "^2.7.0",
|
||||||
"electron-wix-msi": "^2.1.1",
|
"electron-wix-msi": "^2.1.1",
|
||||||
|
Loading…
Reference in New Issue
Block a user