diff --git a/github b/github index 08b8d25..605c418 160000 --- a/github +++ b/github @@ -1 +1 @@ -Subproject commit 08b8d258af3fb887707511625542376e2f222067 +Subproject commit 605c418ac7e5b158426cee96712e7bbc2033e0b6 diff --git a/modules/core/main_window.ts b/modules/core/main_window.ts index 1a7d6f9..8110756 100644 --- a/modules/core/main_window.ts +++ b/modules/core/main_window.ts @@ -73,6 +73,10 @@ function spawn_main_window(entry_point: string) { }); main_window.webContents.on('new-window', (event, url_str, frameName, disposition, options, additionalFeatures) => { + if(frameName.startsWith("__modal_external__")) { + return; + } + event.preventDefault(); try { let url: URL; diff --git a/modules/core/ui-loader/graphical.ts b/modules/core/ui-loader/graphical.ts index f2df4cf..999f025 100644 --- a/modules/core/ui-loader/graphical.ts +++ b/modules/core/ui-loader/graphical.ts @@ -112,8 +112,9 @@ export namespace ui { gui.setMenu(null); gui.loadURL(url.pathToFileURL(path.join(path.dirname(module.filename), "ui", "loading_screen.html")).toString()) gui.on('closed', () => { - if(resolve) + if(resolve) { resolve(); + } gui = undefined; cleanup(); }); diff --git a/modules/core/ui-loader/loader.ts b/modules/core/ui-loader/loader.ts index 0154635..dd2b339 100644 --- a/modules/core/ui-loader/loader.ts +++ b/modules/core/ui-loader/loader.ts @@ -383,7 +383,7 @@ async function load_files_from_dev_server(channel: string, stats_update: (messag const max_simultaneously_downloads = 8; let pending_files: VersionedFile[] = files.slice(0); - let current_downloads: {[key: string]:Promise} = {}; + let current_downloads: {[key: string]: Promise} = {}; const update_download_status = () => { const indicator = (pending_files.length + Object.keys(current_downloads).length) / files.length; @@ -426,6 +426,11 @@ async function load_files_from_dev_server(channel: string, stats_update: (messag /* generate_tmp has already been called an its the file destination */ return path.join(await generate_tmp(), "index.html"); /* entry point */ } + +async function stream_files_from_dev_server(channel: string, stats_update: (message: string, index: number) => any) : Promise { + return remote_url() + "index.html"; +} + async function load_bundles_ui_pack(channel: string, stats_update: (message: string, index: number) => any) : Promise { stats_update("Query local UI pack info", .33); const bundles_ui = await client_shipped_ui(); @@ -564,7 +569,8 @@ async function load_cached_or_remote_ui_pack(channel: string, stats_update: (mes enum UILoaderMethod { PACK, BUNDLED_PACK, - RAW_FILES + RAW_FILES, + DEVELOP_SERVER } export async function load_files(channel: string, stats_update: (message: string, index: number) => any) : Promise { @@ -580,6 +586,9 @@ export async function load_files(channel: string, stats_update: (message: string case UILoaderMethod.RAW_FILES: return await load_files_from_dev_server(channel, stats_update); + + case UILoaderMethod.DEVELOP_SERVER: + return await stream_files_from_dev_server(channel, stats_update); } } diff --git a/modules/core/ui-loader/local_ui_cache.ts b/modules/core/ui-loader/local_ui_cache.ts index 16b42cb..44994a9 100644 --- a/modules/core/ui-loader/local_ui_cache.ts +++ b/modules/core/ui-loader/local_ui_cache.ts @@ -1,5 +1,4 @@ import * as path from "path"; -import * as util from "util"; import * as fs from "fs-extra"; import * as electron from "electron"; @@ -81,22 +80,24 @@ async function load_() : Promise { const file = path.join(cache_path(), "data.json"); try { - if(!(await fs.pathExists(file))) + if(!(await fs.pathExists(file))) { return ui_cache_; + } const data = await fs.readJSON(file) as CacheFile; - if(!data) + if(!data) { throw "invalid data object"; - else if(typeof data["version"] !== "number") + } else if(typeof data["version"] !== "number") { throw "invalid versions tag"; - else if(data["version"] !== 2) { + } else if(data["version"] !== 2) { console.warn("UI cache file contains an old version. Ignoring file and may override with newer version."); return ui_cache_; } /* validating data */ - if(!Array.isArray(data.cached_ui_packs)) + if(!Array.isArray(data.cached_ui_packs)) { throw "Invalid 'cached_ui_packs' entry within the UI cache file"; + } return (ui_cache_ = data as CacheFile); } catch(error) { diff --git a/modules/core/ui-loader/ui/loading_screen.html b/modules/core/ui-loader/ui/loading_screen.html index f65b17b..2973ccc 100644 --- a/modules/core/ui-loader/ui/loading_screen.html +++ b/modules/core/ui-loader/ui/loading_screen.html @@ -27,7 +27,6 @@ justify-content: center; -ms-overflow-style: none; - -webkit-app-region: drag; } img { @@ -75,7 +74,7 @@ background: whitesmoke; border: none; - width: 0%; + width: 0; height: 100%; } @@ -98,8 +97,8 @@
Loading... Please wait! diff --git a/modules/core/ui-loader/ui/preload_page.html b/modules/core/ui-loader/ui/preload_page.html index bd2be1b..6bd70da 100644 --- a/modules/core/ui-loader/ui/preload_page.html +++ b/modules/core/ui-loader/ui/preload_page.html @@ -8,7 +8,7 @@ const target_file = remote.getGlobal("browser-root"); console.log("Navigate to %s", target_file); - if(fs.existsSync(target_file)) + if(fs.existsSync(target_file) || target_file.startsWith("http://") || target_file.startsWith("https://")) window.location.href = target_file; else { console.error("Failed to find target file!"); diff --git a/modules/renderer-manifest/index.ts b/modules/renderer-manifest/index.ts new file mode 100644 index 0000000..948dd33 --- /dev/null +++ b/modules/renderer-manifest/index.ts @@ -0,0 +1,60 @@ +/* --------------- bootstrap --------------- */ +import * as RequireProxy from "../renderer/RequireProxy"; +import * as path from "path"; + +RequireProxy.initialize(path.join(__dirname, "backend-impl")); + +/* --------------- entry point --------------- */ +import * as loader from "tc-loader"; +import {Stage} from "tc-loader"; +import {Arguments, process_args} from "../shared/process-arguments"; +import {remote} from "electron"; + +export function initialize(manifestTarget: string) { + console.log("Initializing native client for manifest target %s", manifestTarget); + + const _impl = message => { + if(!process_args.has_flag(Arguments.DEBUG)) { + console.error("Displaying critical error: %o", message); + message = message.replace(/
/i, "\n"); + + const win = remote.getCurrentWindow(); + win.webContents.openDevTools(); + + remote.dialog.showMessageBox({ + type: "error", + buttons: ["exit"], + title: "A critical error happened!", + message: message + }); + + } else { + console.error("Received critical error: %o", message); + console.error("Ignoring error due to the debug mode"); + } + }; + + if(window.impl_display_critical_error) + window.impl_display_critical_error = _impl; + else + window.displayCriticalError = _impl; + + loader.register_task(loader.Stage.JAVASCRIPT, { + name: "teaclient jquery", + function: async () => { + window.$ = require("jquery"); + window.jQuery = window.$; + Object.assign(window.$, window.jsrender = require('jsrender')); + }, + priority: 80 + }); + + loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { + name: "handler initialize", + priority: 100, + function: async () => { + await import("../renderer/Logger"); + await import("../renderer/PersistentLocalStorage"); + } + }) +} \ No newline at end of file diff --git a/modules/renderer/ExternalModalHandler.ts b/modules/renderer/ExternalModalHandler.ts new file mode 100644 index 0000000..3a60a15 --- /dev/null +++ b/modules/renderer/ExternalModalHandler.ts @@ -0,0 +1,84 @@ +import {AbstractExternalModalController} from "tc-shared/ui/react-elements/external-modal/Controller"; +import {setExternalModalControllerFactory} from "tc-shared/ui/react-elements/external-modal"; +import * as ipc from "tc-shared/ipc/BrowserIPC"; +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import * as loader from "tc-loader"; +import {Stage} from "tc-loader"; +import {BrowserWindow, remote} from "electron"; +import {tr} from "tc-shared/i18n/localize"; +import * as path from "path"; + +class ExternalModalController extends AbstractExternalModalController { + private window: BrowserWindow; + + constructor(a, b, c) { + super(a, b, c); + } + + protected async spawnWindow(): Promise { + if(this.window) { + return true; + } + + this.window = new remote.BrowserWindow({ + parent: remote.getCurrentWindow(), + autoHideMenuBar: true, + + webPreferences: { + nodeIntegration: true + }, + icon: path.join(__dirname, "..", "..", "resources", "logo.ico"), + minWidth: 600, + minHeight: 300 + }); + + const parameters = { + "loader-target": "manifest", + "chunk": "modal-external", + "modal-target": this.modalType, + "ipc-channel": this.ipcChannel.channelId, + "ipc-address": ipc.getInstance().getLocalAddress(), + //"disableGlobalContextMenu": is_debug ? 1 : 0, + //"loader-abort": is_debug ? 1 : 0, + }; + + const baseUrl = location.origin + location.pathname + "?"; + const url = baseUrl + Object.keys(parameters).map(e => e + "=" + encodeURIComponent(parameters[e])).join("&"); + try { + await this.window.loadURL(url); + } catch (error) { + log.warn(LogCategory.GENERAL, tr("Failed to load external modal main page: %o"), error); + this.window.close(); + this.window = undefined; + return false; + } + + this.window.show(); + this.window.on("closed", () => { + this.window = undefined; + this.handleWindowClosed(); + }); + + return true; + } + + protected destroyWindow(): void { + if(this.window) { + this.window.close(); + this.window = undefined; + } + } + + protected focusWindow(): void { + this.window?.focus(); + } +} + +loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { + priority: 50, + name: "external modal controller factory setup", + function: async () => { + setExternalModalControllerFactory((modal, events, userData) => new ExternalModalController(modal, events, userData)); + } +}); \ No newline at end of file diff --git a/modules/renderer/icon-helper.ts b/modules/renderer/IconHelper.ts similarity index 56% rename from modules/renderer/icon-helper.ts rename to modules/renderer/IconHelper.ts index 1ba0a85..cdd704e 100644 --- a/modules/renderer/icon-helper.ts +++ b/modules/renderer/IconHelper.ts @@ -1,4 +1,6 @@ import * as electron from "electron"; +import * as loader from "tc-loader"; +import {Stage} from "tc-loader"; import NativeImage = electron.NativeImage; let _div: JQuery; @@ -31,30 +33,34 @@ export function class_to_image(klass: string) : NativeImage { }); } -export async function initialize() { - if(!_div) { - _div = $(document.createElement("div")); - _div.css('display', 'none'); - _div.appendTo(document.body); +loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { + priority: 100, + name: "native icon sprite loader", + function: async () => { + if(!_div) { + _div = $(document.createElement("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); } - - 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; diff --git a/modules/renderer/logger.ts b/modules/renderer/Logger.ts similarity index 100% rename from modules/renderer/logger.ts rename to modules/renderer/Logger.ts diff --git a/modules/renderer/menu.ts b/modules/renderer/MenuBarHandler.ts similarity index 98% rename from modules/renderer/menu.ts rename to modules/renderer/MenuBarHandler.ts index f8b9f66..29be765 100644 --- a/modules/renderer/menu.ts +++ b/modules/renderer/MenuBarHandler.ts @@ -1,4 +1,4 @@ -import {class_to_image} from "./icon-helper"; +import {class_to_image} from "./IconHelper"; import * as electron from "electron"; import * as mbar from "tc-shared/ui/frames/MenuBar"; import {Arguments, process_args} from "../shared/process-arguments"; diff --git a/modules/renderer/PersistentLocalStorage.ts b/modules/renderer/PersistentLocalStorage.ts index b6a8df3..8f05184 100644 --- a/modules/renderer/PersistentLocalStorage.ts +++ b/modules/renderer/PersistentLocalStorage.ts @@ -19,9 +19,7 @@ export async function initialize() { try { const data = await fs.readFile(path.join(SETTINGS_DIR, file)); - const decoded = JSON.parse(data.toString() || "{}"); - - _local_storage[key] = decoded; + _local_storage[key] = JSON.parse(data.toString() || "{}"); } catch(error) { const target_file = path.join(SETTINGS_DIR, file + "." + Date.now() + ".broken"); console.error("Failed to load settings for %s: %o. Moving settings so the file does not get overridden. Target file: %s", key, error, target_file); diff --git a/modules/renderer/ppt.ts b/modules/renderer/PushToTalkHandler.ts similarity index 100% rename from modules/renderer/ppt.ts rename to modules/renderer/PushToTalkHandler.ts diff --git a/modules/renderer/require-handler.ts b/modules/renderer/RequireProxy.ts similarity index 85% rename from modules/renderer/require-handler.ts rename to modules/renderer/RequireProxy.ts index 0cd8709..abf0ce7 100644 --- a/modules/renderer/require-handler.ts +++ b/modules/renderer/RequireProxy.ts @@ -34,9 +34,13 @@ function proxied_load(request: string, parent?: NodeJS.Module) { } function shared_backend_loader(request: string) { - if(!request.startsWith("tc-backend/")) throw "invalid target"; - const target = request.substr(11); + if(!request.startsWith("tc-backend/")) + throw "invalid target"; + if(!backend_root) + throw "backend is not available in this context"; + + const target = request.substr(11); return require(path.join(backend_root, target)); } @@ -94,18 +98,27 @@ overrides.push({ name: "shared loader", test: /^tc-shared\/.*/, callback: request => { + if(request.endsWith("/")) + return require(request + "index"); + const webpack_path = path.dirname("shared/js/" + request.substr(10)); //FIXME: Get the prefix from a variable! const loader = require("tc-loader"); const mapping = loader.module_mapping().find(e => e.application === "client-app"); //FIXME: Variable name! if(!mapping) throw "missing mapping"; - const entries = mapping.modules.filter(e => e.context === webpack_path); + const entries = mapping.modules.filter(e => e.context.startsWith(webpack_path)); if(!entries.length) throw "unknown target path"; const basename = path.basename(request, path.extname(request)); const entry = entries.find(e => path.basename(e.resource, path.extname(e.resource)) === basename); - if(!entry) throw "unknown import"; + if(!entry) { + if(basename.indexOf(".") === -1 && !request.endsWith("/")) + return require(request + "/index"); + + debugger; + throw "unknown import (" + request + ")"; + } return window["shared-require"](entry.id); } diff --git a/modules/renderer/app_backend.ts b/modules/renderer/SingleInstanceHandler.ts similarity index 100% rename from modules/renderer/app_backend.ts rename to modules/renderer/SingleInstanceHandler.ts diff --git a/modules/renderer/backend-impl/ppt.ts b/modules/renderer/backend-impl/ppt.ts index 8bd023c..a6d1e0c 100644 --- a/modules/renderer/backend-impl/ppt.ts +++ b/modules/renderer/backend-impl/ppt.ts @@ -1,4 +1,4 @@ -import * as handler from "../ppt"; +import * as handler from "../PushToTalkHandler"; export const initialize = handler.initialize; export const finalize = handler.finalize; diff --git a/modules/renderer/context-menu.ts b/modules/renderer/context-menu.ts index 696805d..7904b88 100644 --- a/modules/renderer/context-menu.ts +++ b/modules/renderer/context-menu.ts @@ -1,4 +1,4 @@ -import {class_to_image} from "./icon-helper"; +import {class_to_image} from "./IconHelper"; import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; import * as electron from "electron"; const remote = electron.remote; diff --git a/modules/renderer/index.ts b/modules/renderer/index.ts index 17d7c5a..0eecf5d 100644 --- a/modules/renderer/index.ts +++ b/modules/renderer/index.ts @@ -1,5 +1,5 @@ /* --------------- bootstrap --------------- */ -import * as rh from "./require-handler"; +import * as RequireProxy from "./RequireProxy"; import * as crash_handler from "../crash_handler"; import * as path from "path"; @@ -9,6 +9,15 @@ import * as path from "path"; crash_handler.initialize_handler("renderer", is_electron_run); } +RequireProxy.initialize(path.join(__dirname, "backend-impl")); + +/* --------------- main initialize --------------- */ +import {Arguments, parse_arguments, process_args} from "../shared/process-arguments"; +import * as electron from "electron"; +import {remote} from "electron"; +import * as loader from "tc-loader"; +import ipcRenderer = electron.ipcRenderer; + /* some decls */ declare global { interface Window { @@ -23,14 +32,6 @@ declare global { open_connected_question: () => Promise; } } -rh.initialize(path.join(__dirname, "backend-impl")); - -/* --------------- main initialize --------------- */ -import {Arguments, parse_arguments, process_args} from "../shared/process-arguments"; -import * as electron from "electron"; -import {remote} from "electron"; -import * as loader from "tc-loader"; -import ipcRenderer = electron.ipcRenderer; /* we use out own jquery resource */ loader.register_task(loader.Stage.JAVASCRIPT, { @@ -153,12 +154,13 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { function: async () => { /* all files which replaces a native driver */ try { - require("./version"); - require("./menu"); - require("./context-menu"); - require("./app_backend"); - require("./icon-helper").initialize(); - require("./connection/FileTransfer"); + await import("./version"); + await import("./MenuBarHandler"); + await import("./context-menu"); + await import("./SingleInstanceHandler"); + await import("./IconHelper"); + await import("./connection/FileTransfer"); + await import("./ExternalModalHandler"); } catch (error) { console.log(error); window.displayCriticalError("Failed to load native extensions: " + error); diff --git a/modules/renderer/version.ts b/modules/renderer/version.ts index 6047c25..a5fe13f 100644 --- a/modules/renderer/version.ts +++ b/modules/renderer/version.ts @@ -19,4 +19,6 @@ namespace forum { } } -//window["forum"] = forum; \ No newline at end of file +//window["forum"] = forum; + +export = {}; \ No newline at end of file diff --git a/native/serverconnection/src/audio/driver/PortAudioRecord.cpp b/native/serverconnection/src/audio/driver/PortAudioRecord.cpp index 001b8e0..1364ea8 100644 --- a/native/serverconnection/src/audio/driver/PortAudioRecord.cpp +++ b/native/serverconnection/src/audio/driver/PortAudioRecord.cpp @@ -70,6 +70,8 @@ bool PortAudioRecord::impl_start(std::string &error) { log_critical(category::audio, tr("Failed to close opened pa stream. This will cause memory leaks. Error: {}/{}"), err, Pa_GetErrorText(err)); return false; } + + log_debug(category::audio, tr("Opened audio record stream for {} ({})"), this->info->name, Pa_GetHostApiInfo(this->info->hostApi)->name); return true; } @@ -80,6 +82,8 @@ void PortAudioRecord::impl_stop() { auto error = Pa_CloseStream(this->stream); if(error != paNoError) log_error(category::audio, tr("Failed to close PA stream: {}"), error); + else + log_debug(category::audio, tr("Closed audio record stream for {} ({})"), this->info->name, Pa_GetHostApiInfo(this->info->hostApi)->name); this->stream = nullptr; } diff --git a/native/serverconnection/src/bindings.cpp b/native/serverconnection/src/bindings.cpp index 2e4be85..6230084 100644 --- a/native/serverconnection/src/bindings.cpp +++ b/native/serverconnection/src/bindings.cpp @@ -1,22 +1,18 @@ #include #include #include -#include #include #include #include #include #include "logger.h" -#include "include/NanException.h" #include "include/NanEventCallback.h" #include "connection/ServerConnection.h" #include "connection/audio/VoiceConnection.h" #include "connection/audio/VoiceClient.h" #include "connection/ft/FileTransferManager.h" #include "connection/ft/FileTransferObject.h" -#include "audio/AudioOutput.h" -#include "audio/driver/AudioDriver.h" #include "audio/js/AudioOutputStream.h" #include "audio/js/AudioPlayer.h" #include "audio/js/AudioRecorder.h" @@ -93,20 +89,29 @@ NAN_MODULE_INIT(init) { { auto data = (uint8_t*) "Hello World"; auto hash_result = digest::sha1(std::string("Hello World")); - if(hash_result.length() != 20) + if(hash_result.length() != 20) { Nan::ThrowError("digest::sha1 test failed"); + return; + } } + { auto data = (uint8_t*) "Hello World"; uint8_t result[SHA_DIGEST_LENGTH]; digest::tomcrypt::sha1((char*) data, 11, result); auto hash_result = std::string((const char*) result, SHA_DIGEST_LENGTH); - log_error(category::connection, tr("Hash result: {}"), hash_result.length()); + if(hash_result.length() != SHA_DIGEST_LENGTH) { + Nan::ThrowError("digest::tomcrypt::sha1 test failed"); + return; + } } string error; tc::audio::initialize(); //TODO: Notify JS when initialized? + node::AtExit([](auto){ + tc::audio::finalize(); + }, nullptr); logger::info(category::general, "Loading crypt modules"); std::string descriptors = "LTGE"; @@ -238,7 +243,6 @@ NAN_MODULE_INIT(init) { Nan::GetFunction(Nan::New(TransferFileTarget::create)).ToLocalChecked() ); - //spawn_file_connection destroy_file_connection JSTransfer::Init(ft_namespace); Nan::Set(ft_namespace, Nan::New("spawn_connection").ToLocalChecked(), Nan::GetFunction(Nan::New(JSTransfer::NewInstance)).ToLocalChecked()); Nan::Set(ft_namespace, Nan::New("destroy_connection").ToLocalChecked(), Nan::GetFunction(Nan::New(JSTransfer::destory_transfer)).ToLocalChecked()); diff --git a/package.json b/package.json index 1ed0d14..e596db4 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,16 @@ { "name": "TeaClient", - "version": "1.4.8", + "version": "1.4.9", "description": "", "main": "main.js", "scripts": { "crash_handler": "electron . crash-handler", "test": "echo \"Error: no test specified\" && exit 1", - "start": "electron --js-flags='--expose-gc' --debug --dev-tools --disable-hardware-acceleration .", - "start-d": "electron . --disable-hardware-acceleration --debug -t -u http://clientapi.teaspeak.dev/", - "start-wd": "electron . --disable-hardware-acceleration --debug -t -s -u http://localhost/TeaWeb/client-api/environment/", "start-d1": "electron . --disable-hardware-acceleration --debug -t --gdb -s -u=http://clientapi.teaspeak.dev/ --updater-ui-loader_type=0", "start-n": "electron . -t --disable-hardware-acceleration --no-single-instance -u=https://clientapi.teaspeak.de/ -d", "start-nd": "electron . -t --disable-hardware-acceleration --no-single-instance -u=http://clientapi.teaspeak.dev/ -d", "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-s": "electron . --disable-hardware-acceleration --gdb --debug --updater-ui-loader_type=2 --updater-ui-ignore-version -t -u http://localhost:8081/", + "start-s": "electron . --disable-hardware-acceleration --gdb --debug --updater-ui-loader_type=3 --updater-ui-ignore-version -t -u http://localhost:8081/", "dtest": "electron . dtest", "compile-sass": "sass --update .:.", "compile-tsc": "tsc",