Added the basics for popoutable windows and a CSS style editor
parent
6332b0fc0e
commit
e377a77e32
|
@ -1,4 +1,10 @@
|
||||||
# Changelog:
|
# Changelog:
|
||||||
|
* **20.07.20**
|
||||||
|
- Some general project cleanup
|
||||||
|
- Heavily improved the IPC internal API
|
||||||
|
- Added a basic API for popout able modals
|
||||||
|
- Added a CSS variable editor
|
||||||
|
|
||||||
* **18.07.20**
|
* **18.07.20**
|
||||||
- Rewrote the channel conversation UI and manager
|
- Rewrote the channel conversation UI and manager
|
||||||
- Several bug fixes like the scrollbar
|
- Several bug fixes like the scrollbar
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import * as loader from "./loader/loader";
|
import * as loader from "./loader/loader";
|
||||||
import {Stage} from "./loader/loader";
|
import {Stage} from "./loader/loader";
|
||||||
|
import {getUrlParameter} from "./loader/utils";
|
||||||
|
|
||||||
let overlay: HTMLDivElement;
|
let overlay: HTMLDivElement;
|
||||||
let setupContainer: HTMLDivElement;
|
let setupContainer: HTMLDivElement;
|
||||||
|
@ -92,10 +93,14 @@ export function abort() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function finalize() {
|
export function finalize() {
|
||||||
finalizing = true;
|
if(getUrlParameter("loader-abort") === "1") {
|
||||||
|
abort();
|
||||||
|
} else {
|
||||||
|
finalizing = true;
|
||||||
|
|
||||||
if(loaderStageContainer)
|
if(loaderStageContainer)
|
||||||
loaderStageContainer.innerText = "app loaded successfully (" + (Date.now() - initializeTimestamp) + "ms)";
|
loaderStageContainer.innerText = "app loaded successfully (" + (Date.now() - initializeTimestamp) + "ms)";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const StageNames = {};
|
const StageNames = {};
|
||||||
|
|
|
@ -2,14 +2,24 @@ import "core-js/stable";
|
||||||
import "./polifill";
|
import "./polifill";
|
||||||
|
|
||||||
import * as loader from "./loader/loader";
|
import * as loader from "./loader/loader";
|
||||||
|
import {ApplicationLoader} from "./loader/loader";
|
||||||
|
import {getUrlParameter} from "./loader/utils";
|
||||||
|
|
||||||
window["loader"] = loader;
|
window["loader"] = loader;
|
||||||
/* let the loader register himself at the window first */
|
/* let the loader register himself at the window first */
|
||||||
|
|
||||||
import * as AppLoader from "./targets/app";
|
const target = getUrlParameter("loader-target") || "app";
|
||||||
setTimeout(AppLoader.run, 0);
|
console.error("Loading app with loader \"%s\"", target);
|
||||||
|
|
||||||
import * as EmptyLoader from "./targets/empty";
|
let appLoader: ApplicationLoader;
|
||||||
//setTimeout(EmptyLoader.run, 0);
|
if(target === "empty") {
|
||||||
|
appLoader = new (require("./targets/empty").default);
|
||||||
|
} else if(target === "manifest") {
|
||||||
|
appLoader = new (require("./targets/maifest-target").default);
|
||||||
|
} else {
|
||||||
|
appLoader = new (require("./targets/app").default);
|
||||||
|
}
|
||||||
|
setTimeout(() => appLoader.execute(), 0);
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,11 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
const tr: typeof window.tr;
|
const tr: typeof window.tr;
|
||||||
const tra: typeof window.tr;
|
const tra: typeof window.tra;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplicationLoader {
|
||||||
|
execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
import {SourcePath} from "./loader";
|
import {SourcePath} from "./loader";
|
||||||
import {Options} from "./script_loader";
|
import {Options} from "./script_loader";
|
||||||
|
|
||||||
|
export const getUrlParameter = key => {
|
||||||
|
const match = location.search.match(new RegExp("(.*[?&]|^)" + key + "=([^&]+)($|&.*)"));
|
||||||
|
if(!match)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
return match[2];
|
||||||
|
};
|
||||||
|
|
||||||
export class LoadSyntaxError {
|
export class LoadSyntaxError {
|
||||||
readonly source: any;
|
readonly source: any;
|
||||||
constructor(source: any) {
|
constructor(source: any) {
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import * as loader from "./loader/loader";
|
||||||
|
import {config} from "./loader/loader";
|
||||||
|
import {script_name} from "./loader/utils";
|
||||||
|
|
||||||
|
export interface TeaManifest {
|
||||||
|
version: number;
|
||||||
|
|
||||||
|
chunks: {
|
||||||
|
[key: string]: {
|
||||||
|
files: {
|
||||||
|
hash: string,
|
||||||
|
file: string
|
||||||
|
}[],
|
||||||
|
modules: {
|
||||||
|
id: string,
|
||||||
|
context: string,
|
||||||
|
resource: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let manifest: TeaManifest;
|
||||||
|
export async function loadManifest() : Promise<TeaManifest> {
|
||||||
|
if(manifest) {
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(config.baseUrl + "js/manifest.json");
|
||||||
|
if(!response.ok) throw response.status + " " + response.statusText;
|
||||||
|
|
||||||
|
manifest = await response.json();
|
||||||
|
} catch(error) {
|
||||||
|
console.error("Failed to load javascript manifest: %o", error);
|
||||||
|
loader.critical_error("Failed to load manifest.json", error);
|
||||||
|
throw "failed to load manifest.json";
|
||||||
|
}
|
||||||
|
if(manifest.version !== 2)
|
||||||
|
throw "invalid manifest version";
|
||||||
|
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadManifestTarget(chunkName: string, taskId: number) {
|
||||||
|
if(typeof manifest.chunks[chunkName] !== "object") {
|
||||||
|
loader.critical_error("Missing entry chunk in manifest.json", "Chunk " + chunkName + " is missing.");
|
||||||
|
throw "missing entry chunk";
|
||||||
|
}
|
||||||
|
loader.module_mapping().push({
|
||||||
|
application: chunkName,
|
||||||
|
modules: manifest.chunks[chunkName].modules
|
||||||
|
});
|
||||||
|
|
||||||
|
await loader.scripts.load_multiple(manifest.chunks[chunkName].files.map(e => "js/" + e.file), {
|
||||||
|
cache_tag: undefined,
|
||||||
|
max_parallel_requests: -1
|
||||||
|
}, (script, state) => {
|
||||||
|
if(state !== "loading")
|
||||||
|
return;
|
||||||
|
|
||||||
|
loader.setCurrentTaskName(taskId, script_name(script, false));
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import "./shared";
|
import "./shared";
|
||||||
import * as loader from "../loader/loader";
|
import * as loader from "../loader/loader";
|
||||||
import {config, SourcePath} from "../loader/loader";
|
import {ApplicationLoader, config, SourcePath} from "../loader/loader";
|
||||||
import {script_name} from "../loader/utils";
|
import {script_name} from "../loader/utils";
|
||||||
import { detect as detectBrowser } from "detect-browser";
|
import {loadManifest, loadManifestTarget} from "../maifest";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -31,22 +31,6 @@ export function ui_version() {
|
||||||
return _ui_version;
|
return _ui_version;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Manifest {
|
|
||||||
version: number;
|
|
||||||
|
|
||||||
chunks: {[key: string]: {
|
|
||||||
files: {
|
|
||||||
hash: string,
|
|
||||||
file: string
|
|
||||||
}[],
|
|
||||||
modules: {
|
|
||||||
id: string,
|
|
||||||
context: string,
|
|
||||||
resource: string
|
|
||||||
}[]
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
const LoaderTaskCallback = taskId => (script: SourcePath, state) => {
|
const LoaderTaskCallback = taskId => (script: SourcePath, state) => {
|
||||||
if(state !== "loading")
|
if(state !== "loading")
|
||||||
return;
|
return;
|
||||||
|
@ -62,33 +46,8 @@ const loader_javascript = {
|
||||||
}
|
}
|
||||||
|
|
||||||
loader.setCurrentTaskName(taskId, "manifest");
|
loader.setCurrentTaskName(taskId, "manifest");
|
||||||
let manifest: Manifest;
|
await loadManifest();
|
||||||
try {
|
await loadManifestTarget(__build.entry_chunk_name, taskId);
|
||||||
const response = await fetch(config.baseUrl + "js/manifest.json");
|
|
||||||
if(!response.ok) throw response.status + " " + response.statusText;
|
|
||||||
|
|
||||||
manifest = await response.json();
|
|
||||||
} catch(error) {
|
|
||||||
console.error("Failed to load javascript manifest: %o", error);
|
|
||||||
loader.critical_error("Failed to load manifest.json", error);
|
|
||||||
throw "failed to load manifest.json";
|
|
||||||
}
|
|
||||||
if(manifest.version !== 2)
|
|
||||||
throw "invalid manifest version";
|
|
||||||
|
|
||||||
const chunk_name = __build.entry_chunk_name;
|
|
||||||
if(typeof manifest.chunks[chunk_name] !== "object") {
|
|
||||||
loader.critical_error("Missing entry chunk in manifest.json", "Chunk " + chunk_name + " is missing.");
|
|
||||||
throw "missing entry chunk";
|
|
||||||
}
|
|
||||||
loader.module_mapping().push({
|
|
||||||
application: chunk_name,
|
|
||||||
modules: manifest.chunks[chunk_name].modules
|
|
||||||
});
|
|
||||||
await loader.scripts.load_multiple(manifest.chunks[chunk_name].files.map(e => "js/" + e.file), {
|
|
||||||
cache_tag: undefined,
|
|
||||||
max_parallel_requests: -1
|
|
||||||
}, LoaderTaskCallback(taskId));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -286,37 +245,36 @@ loader.register_task(loader.Stage.SETUP, {
|
||||||
priority: 100
|
priority: 100
|
||||||
});
|
});
|
||||||
|
|
||||||
export function run() {
|
export default class implements ApplicationLoader {
|
||||||
/* TeaClient */
|
execute() {
|
||||||
if(node_require) {
|
/* TeaClient */
|
||||||
if(__build.target !== "client") {
|
if(node_require) {
|
||||||
loader.critical_error("App seems not to be compiled for the client.", "This app has been compiled for " + __build.target);
|
if(__build.target !== "client") {
|
||||||
return;
|
loader.critical_error("App seems not to be compiled for the client.", "This app has been compiled for " + __build.target);
|
||||||
}
|
return;
|
||||||
window.native_client = true;
|
}
|
||||||
|
window.native_client = true;
|
||||||
|
|
||||||
const path = node_require("path");
|
const path = node_require("path");
|
||||||
const remote = node_require('electron').remote;
|
const remote = node_require('electron').remote;
|
||||||
|
|
||||||
const render_entry = path.join(remote.app.getAppPath(), "/modules/", "renderer");
|
const render_entry = path.join(remote.app.getAppPath(), "/modules/", "renderer");
|
||||||
const render = node_require(render_entry);
|
const render = node_require(render_entry);
|
||||||
|
|
||||||
loader.register_task(loader.Stage.INITIALIZING, {
|
loader.register_task(loader.Stage.INITIALIZING, {
|
||||||
name: "teaclient initialize",
|
name: "teaclient initialize",
|
||||||
function: render.initialize,
|
function: render.initialize,
|
||||||
priority: 40
|
priority: 40
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if(__build.target !== "web") {
|
if(__build.target !== "web") {
|
||||||
loader.critical_error("App seems not to be compiled for the web.", "This app has been compiled for " + __build.target);
|
loader.critical_error("App seems not to be compiled for the web.", "This app has been compiled for " + __build.target);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.native_client = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.native_client = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!loader.running()) {
|
|
||||||
/* we know that we want to load the app */
|
|
||||||
loader.execute_managed();
|
loader.execute_managed();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,27 +1,29 @@
|
||||||
import "./shared";
|
import "./shared";
|
||||||
import * as loader from "../loader/loader";
|
import * as loader from "../loader/loader";
|
||||||
import {Stage} from "../loader/loader";
|
import {ApplicationLoader, Stage} from "../loader/loader";
|
||||||
|
|
||||||
export function run() {
|
export default class implements ApplicationLoader {
|
||||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
execute() {
|
||||||
name: "doing nothing",
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
priority: 1,
|
name: "doing nothing",
|
||||||
function: async taskId => {
|
priority: 1,
|
||||||
console.log("Doing nothing");
|
function: async taskId => {
|
||||||
|
console.log("Doing nothing");
|
||||||
|
|
||||||
for(let index of [1, 2, 3]) {
|
for(let index of [1, 2, 3]) {
|
||||||
await new Promise(resolve => {
|
await new Promise(resolve => {
|
||||||
const callback = () => {
|
const callback = () => {
|
||||||
document.removeEventListener("click", resolve);
|
document.removeEventListener("click", resolve);
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("click", callback);
|
document.addEventListener("click", callback);
|
||||||
});
|
});
|
||||||
loader.setCurrentTaskName(taskId, "try again (" + index + ")");
|
loader.setCurrentTaskName(taskId, "try again (" + index + ")");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
loader.execute_managed();
|
loader.execute_managed();
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
import "./shared";
|
||||||
|
import * as loader from "../loader/loader";
|
||||||
|
import {ApplicationLoader, Stage} from "../loader/loader";
|
||||||
|
import {loadManifest, loadManifestTarget} from "../maifest";
|
||||||
|
import {getUrlParameter} from "../loader/utils";
|
||||||
|
|
||||||
|
export default class implements ApplicationLoader {
|
||||||
|
execute() {
|
||||||
|
loader.register_task(Stage.SETUP, {
|
||||||
|
function: async taskId => {
|
||||||
|
/* sadly still a need in general :/ */
|
||||||
|
await loader.scripts.load_multiple(["vendor/jquery/jquery.min.js"], { });
|
||||||
|
|
||||||
|
await loadManifest();
|
||||||
|
|
||||||
|
const entryChunk = getUrlParameter("chunk");
|
||||||
|
if(!entryChunk) {
|
||||||
|
loader.critical_error("Missing entry chunk parameter");
|
||||||
|
throw "Missing entry chunk parameter";
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadManifestTarget(entryChunk, taskId);
|
||||||
|
},
|
||||||
|
name: "Manifest loader",
|
||||||
|
priority: 100
|
||||||
|
});
|
||||||
|
|
||||||
|
/* required sadly */
|
||||||
|
|
||||||
|
loader.register_task(loader.Stage.SETUP, {
|
||||||
|
name: "page setup",
|
||||||
|
function: async () => {
|
||||||
|
const body = document.body;
|
||||||
|
/* top menu */
|
||||||
|
{
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.setAttribute('id', "top-menu-bar");
|
||||||
|
body.append(container);
|
||||||
|
}
|
||||||
|
/* template containers */
|
||||||
|
{
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.setAttribute('id', "templates");
|
||||||
|
body.append(container);
|
||||||
|
}
|
||||||
|
/* sounds container */
|
||||||
|
{
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.setAttribute('id', "sounds");
|
||||||
|
body.append(container);
|
||||||
|
}
|
||||||
|
/* mouse move container */
|
||||||
|
{
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.setAttribute('id', "mouse-move");
|
||||||
|
|
||||||
|
body.append(container);
|
||||||
|
}
|
||||||
|
/* tooltip container */
|
||||||
|
{
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.setAttribute('id', "global-tooltip");
|
||||||
|
|
||||||
|
container.append(document.createElement("a"));
|
||||||
|
|
||||||
|
body.append(container);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
priority: 10
|
||||||
|
});
|
||||||
|
|
||||||
|
loader.register_task(loader.Stage.TEMPLATES, {
|
||||||
|
name: "templates",
|
||||||
|
function: async () => {
|
||||||
|
await loader.templates.load_multiple([
|
||||||
|
"templates.html"
|
||||||
|
], {
|
||||||
|
cache_tag: "?22",
|
||||||
|
max_parallel_requests: -1
|
||||||
|
});
|
||||||
|
},
|
||||||
|
priority: 10
|
||||||
|
});
|
||||||
|
|
||||||
|
loader.execute_managed();
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,7 +32,7 @@ if(__build.target === "web") {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* directly disable all context menus */ //disableGlobalContextMenu
|
/* directly disable all context menus */
|
||||||
if(!location.search.match(/(.*[?&]|^)disableGlobalContextMenu=1($|&.*)/)) {
|
if(!location.search.match(/(.*[?&]|^)disableGlobalContextMenu=1($|&.*)/)) {
|
||||||
document.addEventListener("contextmenu", event => event.preventDefault());
|
document.addEventListener("contextmenu", event => event.preventDefault());
|
||||||
}
|
}
|
|
@ -1,6 +1,13 @@
|
||||||
body {
|
body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
*, :before, :after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@import "loader";
|
@import "loader";
|
||||||
|
|
|
@ -1141,6 +1141,11 @@
|
||||||
"protobufjs": "^6.8.6"
|
"protobufjs": "^6.8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@icons/material": {
|
||||||
|
"version": "0.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz",
|
||||||
|
"integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw=="
|
||||||
|
},
|
||||||
"@nodelib/fs.scandir": {
|
"@nodelib/fs.scandir": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz",
|
||||||
|
@ -1448,6 +1453,16 @@
|
||||||
"csstype": "^2.2.0"
|
"csstype": "^2.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-color": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-EswbYJDF1kkrx93/YU+BbBtb46CCtDMvTiGmcOa/c5PETnwTiSWoseJ1oSWeRl/4rUXkhME9bVURvvPg0W5YQw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/reactcss": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/react-dom": {
|
"@types/react-dom": {
|
||||||
"version": "16.9.5",
|
"version": "16.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.5.tgz",
|
||||||
|
@ -1457,6 +1472,15 @@
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/reactcss": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-d2gQQ0IL6hXLnoRfVYZukQNWHuVsE75DzFTLPUuyyEhJS8G2VvlE+qfQQ91SJjaMqlURRCNIsX7Jcsw6cEuJlA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/relateurl": {
|
"@types/relateurl": {
|
||||||
"version": "0.2.28",
|
"version": "0.2.28",
|
||||||
"resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.28.tgz",
|
"resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.28.tgz",
|
||||||
|
@ -1776,6 +1800,14 @@
|
||||||
"integrity": "sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==",
|
"integrity": "sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"add-dom-event-listener": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==",
|
||||||
|
"requires": {
|
||||||
|
"object-assign": "4.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"agent-base": {
|
"agent-base": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz",
|
||||||
|
@ -2132,6 +2164,11 @@
|
||||||
"integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
|
"integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"asap": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||||
|
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
|
||||||
|
},
|
||||||
"asn1": {
|
"asn1": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
|
||||||
|
@ -2328,6 +2365,27 @@
|
||||||
"object.assign": "^4.1.0"
|
"object.assign": "^4.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"babel-runtime": {
|
||||||
|
"version": "6.26.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
||||||
|
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
|
||||||
|
"requires": {
|
||||||
|
"core-js": "^2.4.0",
|
||||||
|
"regenerator-runtime": "^0.11.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"core-js": {
|
||||||
|
"version": "2.6.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
|
||||||
|
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg=="
|
||||||
|
},
|
||||||
|
"regenerator-runtime": {
|
||||||
|
"version": "0.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
|
||||||
|
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"bach": {
|
"bach": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz",
|
||||||
|
@ -3133,6 +3191,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"classnames": {
|
||||||
|
"version": "2.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
|
||||||
|
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
|
||||||
|
},
|
||||||
"clean-css": {
|
"clean-css": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
|
||||||
|
@ -3312,12 +3375,25 @@
|
||||||
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
|
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"component-classes": {
|
||||||
|
"version": "1.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/component-classes/-/component-classes-1.2.6.tgz",
|
||||||
|
"integrity": "sha1-xkI5TDYYpNiwuJGe/Mu9kw5c1pE=",
|
||||||
|
"requires": {
|
||||||
|
"component-indexof": "0.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"component-emitter": {
|
"component-emitter": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
||||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
|
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"component-indexof": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/component-indexof/-/component-indexof-0.0.3.tgz",
|
||||||
|
"integrity": "sha1-EdCRMSI5648yyPJa6csAL/6NPCQ="
|
||||||
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
@ -3511,6 +3587,16 @@
|
||||||
"sha.js": "^2.4.8"
|
"sha.js": "^2.4.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"create-react-class": {
|
||||||
|
"version": "15.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz",
|
||||||
|
"integrity": "sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==",
|
||||||
|
"requires": {
|
||||||
|
"fbjs": "^0.8.9",
|
||||||
|
"loose-envify": "^1.3.1",
|
||||||
|
"object-assign": "^4.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"cross-spawn": {
|
"cross-spawn": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
|
||||||
|
@ -3546,6 +3632,15 @@
|
||||||
"integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
|
"integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"css-animation": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-animation/-/css-animation-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-/48+/BaEaHRY6kNQ2OIPzKf9A6g8WjZYjhiNDNuIVbsm5tXCGIAsHDjB4Xu1C4vXJtUWZo26O68OQkDpNBaPog==",
|
||||||
|
"requires": {
|
||||||
|
"babel-runtime": "6.x",
|
||||||
|
"component-classes": "^1.2.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"css-loader": {
|
"css-loader": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz",
|
||||||
|
@ -3956,6 +4051,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dom-align": {
|
||||||
|
"version": "1.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.12.0.tgz",
|
||||||
|
"integrity": "sha512-YkoezQuhp3SLFGdOlr5xkqZ640iXrnHAwVYcDg8ZKRUtO7mSzSC2BA5V0VuyAwPSJA4CLIc6EDDJh4bEsD2+zA=="
|
||||||
|
},
|
||||||
"dom-converter": {
|
"dom-converter": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
|
||||||
|
@ -4142,6 +4242,24 @@
|
||||||
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
|
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"encoding": {
|
||||||
|
"version": "0.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||||
|
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||||
|
"requires": {
|
||||||
|
"iconv-lite": "^0.6.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": {
|
||||||
|
"version": "0.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
|
||||||
|
"integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
|
||||||
|
"requires": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"end-of-stream": {
|
"end-of-stream": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||||
|
@ -4735,6 +4853,27 @@
|
||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fbjs": {
|
||||||
|
"version": "0.8.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz",
|
||||||
|
"integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=",
|
||||||
|
"requires": {
|
||||||
|
"core-js": "^1.0.0",
|
||||||
|
"isomorphic-fetch": "^2.1.1",
|
||||||
|
"loose-envify": "^1.0.0",
|
||||||
|
"object-assign": "^4.1.0",
|
||||||
|
"promise": "^7.1.1",
|
||||||
|
"setimmediate": "^1.0.5",
|
||||||
|
"ua-parser-js": "^0.7.18"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"core-js": {
|
||||||
|
"version": "1.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
|
||||||
|
"integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"figgy-pudding": {
|
"figgy-pudding": {
|
||||||
"version": "3.5.2",
|
"version": "3.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz",
|
||||||
|
@ -7487,8 +7626,7 @@
|
||||||
"is-stream": {
|
"is-stream": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
|
||||||
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
|
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"is-stream-ended": {
|
"is-stream-ended": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
|
@ -7561,6 +7699,26 @@
|
||||||
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
|
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"isomorphic-fetch": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
|
||||||
|
"integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
|
||||||
|
"requires": {
|
||||||
|
"node-fetch": "^1.0.1",
|
||||||
|
"whatwg-fetch": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"node-fetch": {
|
||||||
|
"version": "1.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
|
||||||
|
"integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
|
||||||
|
"requires": {
|
||||||
|
"encoding": "^0.1.11",
|
||||||
|
"is-stream": "^1.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"isstream": {
|
"isstream": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||||
|
@ -7897,8 +8055,7 @@
|
||||||
"lodash": {
|
"lodash": {
|
||||||
"version": "4.17.19",
|
"version": "4.17.19",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
|
||||||
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
|
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"lodash.at": {
|
"lodash.at": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
|
@ -8422,6 +8579,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"material-colors": {
|
||||||
|
"version": "1.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz",
|
||||||
|
"integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg=="
|
||||||
|
},
|
||||||
"md5.js": {
|
"md5.js": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
||||||
|
@ -9555,8 +9717,7 @@
|
||||||
"performance-now": {
|
"performance-now": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||||
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
|
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"picomatch": {
|
"picomatch": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
|
@ -9764,6 +9925,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||||
},
|
},
|
||||||
|
"promise": {
|
||||||
|
"version": "7.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
|
||||||
|
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
|
||||||
|
"requires": {
|
||||||
|
"asap": "~2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"promise-inflight": {
|
"promise-inflight": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
|
||||||
|
@ -9921,6 +10090,14 @@
|
||||||
"integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==",
|
"integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"raf": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||||
|
"requires": {
|
||||||
|
"performance-now": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"randombytes": {
|
"randombytes": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||||
|
@ -9980,6 +10157,68 @@
|
||||||
"strip-json-comments": "~2.0.1"
|
"strip-json-comments": "~2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"rc-align": {
|
||||||
|
"version": "2.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/rc-align/-/rc-align-2.4.5.tgz",
|
||||||
|
"integrity": "sha512-nv9wYUYdfyfK+qskThf4BQUSIadeI/dCsfaMZfNEoxm9HwOIioQ+LyqmMK6jWHAZQgOzMLaqawhuBXlF63vgjw==",
|
||||||
|
"requires": {
|
||||||
|
"babel-runtime": "^6.26.0",
|
||||||
|
"dom-align": "^1.7.0",
|
||||||
|
"prop-types": "^15.5.8",
|
||||||
|
"rc-util": "^4.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rc-animate": {
|
||||||
|
"version": "2.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rc-animate/-/rc-animate-2.11.1.tgz",
|
||||||
|
"integrity": "sha512-1NyuCGFJG/0Y+9RKh5y/i/AalUCA51opyyS/jO2seELpgymZm2u9QV3xwODwEuzkmeQ1BDPxMLmYLcTJedPlkQ==",
|
||||||
|
"requires": {
|
||||||
|
"babel-runtime": "6.x",
|
||||||
|
"classnames": "^2.2.6",
|
||||||
|
"css-animation": "^1.3.2",
|
||||||
|
"prop-types": "15.x",
|
||||||
|
"raf": "^3.4.0",
|
||||||
|
"rc-util": "^4.15.3",
|
||||||
|
"react-lifecycles-compat": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rc-color-picker": {
|
||||||
|
"version": "1.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/rc-color-picker/-/rc-color-picker-1.2.6.tgz",
|
||||||
|
"integrity": "sha512-AaC9Pg7qCHSy5M4eVbqDIaNb2FC4SEw82GOHB2C4R/+vF2FVa/r5XA+Igg5+zLPmAvBLhz9tL4MAfkRA8yWNJw==",
|
||||||
|
"requires": {
|
||||||
|
"classnames": "^2.2.5",
|
||||||
|
"prop-types": "^15.5.8",
|
||||||
|
"rc-trigger": "1.x",
|
||||||
|
"rc-util": "^4.0.2",
|
||||||
|
"tinycolor2": "^1.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rc-trigger": {
|
||||||
|
"version": "1.11.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-1.11.5.tgz",
|
||||||
|
"integrity": "sha512-MBuUPw1nFzA4K7jQOwb7uvFaZFjXGd00EofUYiZ+l/fgKVq8wnLC0lkv36kwqM7vfKyftRo2sh7cWVpdPuNnnw==",
|
||||||
|
"requires": {
|
||||||
|
"babel-runtime": "6.x",
|
||||||
|
"create-react-class": "15.x",
|
||||||
|
"prop-types": "15.x",
|
||||||
|
"rc-align": "2.x",
|
||||||
|
"rc-animate": "2.x",
|
||||||
|
"rc-util": "4.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rc-util": {
|
||||||
|
"version": "4.21.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.21.1.tgz",
|
||||||
|
"integrity": "sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg==",
|
||||||
|
"requires": {
|
||||||
|
"add-dom-event-listener": "^1.1.0",
|
||||||
|
"prop-types": "^15.5.10",
|
||||||
|
"react-is": "^16.12.0",
|
||||||
|
"react-lifecycles-compat": "^3.0.4",
|
||||||
|
"shallowequal": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react": {
|
"react": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz",
|
||||||
|
@ -9990,6 +10229,19 @@
|
||||||
"prop-types": "^15.6.2"
|
"prop-types": "^15.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-color": {
|
||||||
|
"version": "2.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.18.1.tgz",
|
||||||
|
"integrity": "sha512-X5XpyJS6ncplZs74ak0JJoqPi+33Nzpv5RYWWxn17bslih+X7OlgmfpmGC1fNvdkK7/SGWYf1JJdn7D2n5gSuQ==",
|
||||||
|
"requires": {
|
||||||
|
"@icons/material": "^0.2.4",
|
||||||
|
"lodash": "^4.17.11",
|
||||||
|
"material-colors": "^1.2.1",
|
||||||
|
"prop-types": "^15.5.10",
|
||||||
|
"reactcss": "^1.2.0",
|
||||||
|
"tinycolor2": "^1.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-dom": {
|
"react-dom": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz",
|
||||||
|
@ -10006,6 +10258,19 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
},
|
},
|
||||||
|
"react-lifecycles-compat": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||||
|
},
|
||||||
|
"reactcss": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==",
|
||||||
|
"requires": {
|
||||||
|
"lodash": "^4.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"read-pkg": {
|
"read-pkg": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
|
||||||
|
@ -10802,8 +11067,7 @@
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"sass": {
|
"sass": {
|
||||||
"version": "1.22.10",
|
"version": "1.22.10",
|
||||||
|
@ -11265,8 +11529,7 @@
|
||||||
"setimmediate": {
|
"setimmediate": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||||
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
|
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"setprototypeof": {
|
"setprototypeof": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
|
@ -11311,6 +11574,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"shallowequal": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
|
||||||
|
},
|
||||||
"shebang-command": {
|
"shebang-command": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||||
|
@ -11988,6 +12256,11 @@
|
||||||
"setimmediate": "^1.0.4"
|
"setimmediate": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tinycolor2": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz",
|
||||||
|
"integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g="
|
||||||
|
},
|
||||||
"to-absolute-glob": {
|
"to-absolute-glob": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz",
|
||||||
|
@ -12521,6 +12794,11 @@
|
||||||
"integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
|
"integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"ua-parser-js": {
|
||||||
|
"version": "0.7.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz",
|
||||||
|
"integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ=="
|
||||||
|
},
|
||||||
"uglify-js": {
|
"uglify-js": {
|
||||||
"version": "3.8.1",
|
"version": "3.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.8.1.tgz",
|
||||||
|
@ -14225,6 +14503,11 @@
|
||||||
"sdp": "^2.12.0"
|
"sdp": "^2.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"whatwg-fetch": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-SdGPoQMMnzVYThUbSrEvqTlkvC1Ux27NehaJ/GUHBfNrh5Mjg+1/uRyFMwVnxO2MrikMWvWAqUGgQOfVU4hT7w=="
|
||||||
|
},
|
||||||
"which": {
|
"which": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
"@types/lodash": "^4.14.149",
|
"@types/lodash": "^4.14.149",
|
||||||
"@types/moment": "^2.13.0",
|
"@types/moment": "^2.13.0",
|
||||||
"@types/node": "^12.7.2",
|
"@types/node": "^12.7.2",
|
||||||
|
"@types/react-color": "^3.0.4",
|
||||||
"@types/react-dom": "^16.9.5",
|
"@types/react-dom": "^16.9.5",
|
||||||
"@types/remarkable": "^1.7.4",
|
"@types/remarkable": "^1.7.4",
|
||||||
"@types/sha256": "^0.2.0",
|
"@types/sha256": "^0.2.0",
|
||||||
|
|
|
@ -7,6 +7,27 @@ $client_info_avatar_size: 10em;
|
||||||
$bot_thumbnail_width: 16em;
|
$bot_thumbnail_width: 16em;
|
||||||
$bot_thumbnail_height: 9em;
|
$bot_thumbnail_height: 9em;
|
||||||
|
|
||||||
|
html:root {
|
||||||
|
--side-info-background: #2e2e2e;
|
||||||
|
--side-info-shadow: rgba(0, 0, 0, 0.25);
|
||||||
|
--side-info-title: #8b8b8b;
|
||||||
|
|
||||||
|
--side-info-indicator: #dab8b4;
|
||||||
|
--side-info-indicator-border: #6a0e0e;
|
||||||
|
--side-info-indicator-background: #ca3e22;
|
||||||
|
|
||||||
|
--side-info-value-background: #373737;
|
||||||
|
--side-info-value: #5a5a5a;
|
||||||
|
|
||||||
|
--side-info-ping-very-good: #3f7538;
|
||||||
|
--side-info-ping-good: #365632;
|
||||||
|
--side-info-ping-medium: #777f2c;
|
||||||
|
--side-info-ping-poor: #7f5122;
|
||||||
|
--side-info-ping-very-poor: #7f2222;
|
||||||
|
|
||||||
|
--side-info-bot-add-song: #3f7538;
|
||||||
|
}
|
||||||
|
|
||||||
.container-chat-frame {
|
.container-chat-frame {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
|
@ -29,13 +50,13 @@ $bot_thumbnail_height: 9em;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
|
|
||||||
background-color: #2e2e2e;
|
background-color: var(--side-info-background);
|
||||||
border-top-left-radius: 5px;
|
border-top-left-radius: 5px;
|
||||||
border-top-right-radius: 5px;
|
border-top-right-radius: 5px;
|
||||||
|
|
||||||
-moz-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.25);
|
-moz-box-shadow: inset 0 0 5px var(--side-info-shadow);
|
||||||
-webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.25);
|
-webkit-box-shadow: inset 0 0 5px var(--side-info-shadow);
|
||||||
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.25);
|
box-shadow: inset 0 0 5px var(--side-info-shadow);
|
||||||
|
|
||||||
.lane {
|
.lane {
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
|
@ -90,22 +111,22 @@ $bot_thumbnail_height: 9em;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
display: block;
|
display: block;
|
||||||
color: #8b8b8b;
|
color: var(--side-info-title);
|
||||||
|
|
||||||
.container-indicator {
|
.container-indicator {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
|
|
||||||
background: #ca3e22;
|
background: var(--side-info-indicator-background);
|
||||||
border: 1px solid #6a0e0e;
|
border: 1px solid var(--side-info-indicator-border);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
vertical-align: text-top;
|
vertical-align: text-top;
|
||||||
|
|
||||||
color: #dab8b4;
|
color: var(--side-info-indicator);
|
||||||
|
|
||||||
font-size: .66em;
|
font-size: .66em;
|
||||||
height: 1.3em;
|
height: 1.3em;
|
||||||
|
@ -117,8 +138,8 @@ $bot_thumbnail_height: 9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value {
|
.value {
|
||||||
color: #5a5a5a;
|
color: var(--side-info-value);
|
||||||
background-color: #373737;
|
background-color: var(--side-info-value-background);
|
||||||
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
|
@ -138,19 +159,19 @@ $bot_thumbnail_height: 9em;
|
||||||
&.value-ping {
|
&.value-ping {
|
||||||
//very-good good medium poor very-poor
|
//very-good good medium poor very-poor
|
||||||
&.very-good {
|
&.very-good {
|
||||||
color: #3f7538;
|
color: var(--side-info-ping-very-good);
|
||||||
}
|
}
|
||||||
&.good {
|
&.good {
|
||||||
color: #365632;
|
color: var(--side-info-ping-good);
|
||||||
}
|
}
|
||||||
&.medium {
|
&.medium {
|
||||||
color: #777f2c;
|
color: var(--side-info-ping-medium);
|
||||||
}
|
}
|
||||||
&.poor {
|
&.poor {
|
||||||
color: #7f5122;
|
color: var(--side-info-ping-poor);
|
||||||
}
|
}
|
||||||
&.very-poor {
|
&.very-poor {
|
||||||
color: #7f2222;
|
color: var(--side-info-ping-very-poor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,21 +180,21 @@ $bot_thumbnail_height: 9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.bot-add-song {
|
&.bot-add-song {
|
||||||
color: #3f7538;
|
color: var(--side-info-bot-add-song);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.small-value {
|
.small-value {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: #5a5a5a;
|
color: var(--side-info-value);
|
||||||
font-size: .66em;
|
font-size: .66em;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
margin-top: -.2em;
|
margin-top: -.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
color: #5a5a5a;
|
color: var(--side-info-value);
|
||||||
background-color: #373737;
|
background-color: var(--side-info-value-background);
|
||||||
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
|
|
|
@ -71,11 +71,6 @@ html:root {
|
||||||
--text: #999;
|
--text: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
*, :before, :after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
|
@ -85,6 +80,7 @@ html {
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
background-color: gray;
|
background-color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
|
@ -156,43 +152,6 @@ a[href] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* code hightliting */
|
|
||||||
.tag-hljs-inline-code, .tag-hljs-code {
|
|
||||||
display: block;
|
|
||||||
margin: 3px;
|
|
||||||
|
|
||||||
font-size: 80%;
|
|
||||||
border-radius: .2em;
|
|
||||||
font-family: Monaco, Menlo, Consolas, "Roboto Mono", "Andale Mono", "Ubuntu Mono", monospace;
|
|
||||||
box-decoration-break: clone;
|
|
||||||
|
|
||||||
&.tag-hljs-inline-code {
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
> .hljs {
|
|
||||||
padding: 0 .25em!important;
|
|
||||||
}
|
|
||||||
|
|
||||||
white-space: pre-wrap;
|
|
||||||
margin: 0 0 -0.1em;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
&.tag-hljs-code {
|
|
||||||
word-wrap: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
@include chat-scrollbar-horizontal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* fix tailing new line after code blocks */
|
|
||||||
.message > {
|
|
||||||
.tag-hljs-code + br {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tooltip */
|
/* tooltip */
|
||||||
#global-tooltip {
|
#global-tooltip {
|
||||||
color: #999999;
|
color: #999999;
|
||||||
|
|
|
@ -1,725 +0,0 @@
|
||||||
import * as log from "tc-shared/log";
|
|
||||||
import {LogCategory} from "tc-shared/log";
|
|
||||||
|
|
||||||
export interface BroadcastMessage {
|
|
||||||
timestamp: number;
|
|
||||||
receiver: string;
|
|
||||||
sender: string;
|
|
||||||
|
|
||||||
type: string;
|
|
||||||
data: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
function uuidv4() {
|
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
||||||
const r = Math.random() * 16 | 0;
|
|
||||||
const v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
||||||
return v.toString(16);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProcessQuery {
|
|
||||||
timestamp: number
|
|
||||||
query_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChannelMessage {
|
|
||||||
channel_id: string;
|
|
||||||
type: string;
|
|
||||||
data: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProcessQueryResponse {
|
|
||||||
request_timestamp: number
|
|
||||||
request_query_id: string;
|
|
||||||
|
|
||||||
device_id: string;
|
|
||||||
protocol: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CertificateAcceptCallback {
|
|
||||||
request_id: string;
|
|
||||||
}
|
|
||||||
export interface CertificateAcceptSucceeded { }
|
|
||||||
|
|
||||||
export abstract class BasicIPCHandler {
|
|
||||||
protected static readonly BROADCAST_UNIQUE_ID = "00000000-0000-4000-0000-000000000000";
|
|
||||||
protected static readonly PROTOCOL_VERSION = 1;
|
|
||||||
|
|
||||||
protected _channels: Channel[] = [];
|
|
||||||
protected unique_id;
|
|
||||||
|
|
||||||
protected constructor() { }
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
this.unique_id = uuidv4(); /* lets get an unique identifier */
|
|
||||||
}
|
|
||||||
|
|
||||||
get_local_address() { return this.unique_id; }
|
|
||||||
|
|
||||||
abstract send_message(type: string, data: any, target?: string);
|
|
||||||
|
|
||||||
protected handle_message(message: BroadcastMessage) {
|
|
||||||
//log.trace(LogCategory.IPC, tr("Received message %o"), message);
|
|
||||||
|
|
||||||
if(message.receiver === BasicIPCHandler.BROADCAST_UNIQUE_ID) {
|
|
||||||
if(message.type == "process-query") {
|
|
||||||
log.debug(LogCategory.IPC, tr("Received a device query from %s."), message.sender);
|
|
||||||
this.send_message("process-query-response", {
|
|
||||||
request_query_id: (<ProcessQuery>message.data).query_id,
|
|
||||||
request_timestamp: (<ProcessQuery>message.data).timestamp,
|
|
||||||
|
|
||||||
device_id: this.unique_id,
|
|
||||||
protocol: BasicIPCHandler.PROTOCOL_VERSION
|
|
||||||
} as ProcessQueryResponse, message.sender);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if(message.receiver === this.unique_id) {
|
|
||||||
if(message.type == "process-query-response") {
|
|
||||||
const response: ProcessQueryResponse = message.data;
|
|
||||||
if(this._query_results[response.request_query_id])
|
|
||||||
this._query_results[response.request_query_id].push(response);
|
|
||||||
else {
|
|
||||||
log.warn(LogCategory.IPC, tr("Received a query response for an unknown request."));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if(message.type == "certificate-accept-callback") {
|
|
||||||
const data: CertificateAcceptCallback = message.data;
|
|
||||||
if(!this._cert_accept_callbacks[data.request_id]) {
|
|
||||||
log.warn(LogCategory.IPC, tr("Received certificate accept callback for an unknown request ID."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._cert_accept_callbacks[data.request_id]();
|
|
||||||
delete this._cert_accept_callbacks[data.request_id];
|
|
||||||
|
|
||||||
this.send_message("certificate-accept-succeeded", {
|
|
||||||
|
|
||||||
} as CertificateAcceptSucceeded, message.sender);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if(message.type == "certificate-accept-succeeded") {
|
|
||||||
if(!this._cert_accept_succeeded[message.sender]) {
|
|
||||||
log.warn(LogCategory.IPC, tr("Received certificate accept succeeded, but haven't a callback."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._cert_accept_succeeded[message.sender]();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(message.type === "channel") {
|
|
||||||
const data: ChannelMessage = message.data;
|
|
||||||
|
|
||||||
let channel_invoked = false;
|
|
||||||
for(const channel of this._channels)
|
|
||||||
if(channel.channel_id === data.channel_id && (typeof(channel.target_id) === "undefined" || channel.target_id === message.sender)) {
|
|
||||||
if(channel.message_handler)
|
|
||||||
channel.message_handler(message.sender, message.receiver === BasicIPCHandler.BROADCAST_UNIQUE_ID, data);
|
|
||||||
channel_invoked = true;
|
|
||||||
}
|
|
||||||
if(!channel_invoked) {
|
|
||||||
console.warn(tr("Received channel message for unknown channel (%s)"), data.channel_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
create_channel(target_id?: string, channel_id?: string) {
|
|
||||||
let channel: Channel = {
|
|
||||||
target_id: target_id,
|
|
||||||
channel_id: channel_id || uuidv4(),
|
|
||||||
message_handler: undefined,
|
|
||||||
send_message: (type: string, data: any, target?: string) => {
|
|
||||||
if(typeof target !== "undefined") {
|
|
||||||
if(typeof channel.target_id === "string" && target != channel.target_id)
|
|
||||||
throw "target id does not match channel target";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.send_message("channel", {
|
|
||||||
type: type,
|
|
||||||
data: data,
|
|
||||||
channel_id: channel.channel_id
|
|
||||||
} as ChannelMessage, target || channel.target_id || BasicIPCHandler.BROADCAST_UNIQUE_ID);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this._channels.push(channel);
|
|
||||||
return channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
channels() : Channel[] { return this._channels; }
|
|
||||||
|
|
||||||
delete_channel(channel: Channel) {
|
|
||||||
this._channels = this._channels.filter(e => e !== channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _query_results: {[key: string]:ProcessQueryResponse[]} = {};
|
|
||||||
async query_processes(timeout?: number) : Promise<ProcessQueryResponse[]> {
|
|
||||||
const query_id = uuidv4();
|
|
||||||
this._query_results[query_id] = [];
|
|
||||||
|
|
||||||
this.send_message("process-query", {
|
|
||||||
query_id: query_id,
|
|
||||||
timestamp: Date.now()
|
|
||||||
} as ProcessQuery);
|
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, timeout || 250));
|
|
||||||
const result = this._query_results[query_id];
|
|
||||||
delete this._query_results[query_id];
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _cert_accept_callbacks: {[key: string]:(() => any)} = {};
|
|
||||||
register_certificate_accept_callback(callback: () => any) : string {
|
|
||||||
const id = uuidv4();
|
|
||||||
this._cert_accept_callbacks[id] = callback;
|
|
||||||
return this.unique_id + ":" + id;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _cert_accept_succeeded: {[sender: string]:(() => any)} = {};
|
|
||||||
post_certificate_accpected(id: string, timeout?: number) : Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const data = id.split(":");
|
|
||||||
const timeout_id = setTimeout(() => {
|
|
||||||
delete this._cert_accept_succeeded[data[0]];
|
|
||||||
clearTimeout(timeout_id);
|
|
||||||
reject("timeout");
|
|
||||||
}, timeout || 250);
|
|
||||||
this._cert_accept_succeeded[data[0]] = () => {
|
|
||||||
delete this._cert_accept_succeeded[data[0]];
|
|
||||||
clearTimeout(timeout_id);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
this.send_message("certificate-accept-callback", {
|
|
||||||
request_id: data[1]
|
|
||||||
} as CertificateAcceptCallback, data[0]);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Channel {
|
|
||||||
readonly channel_id: string;
|
|
||||||
target_id?: string;
|
|
||||||
|
|
||||||
message_handler: (remote_id: string, broadcast: boolean, message: ChannelMessage) => any;
|
|
||||||
send_message(type: string, message: any, target?: string);
|
|
||||||
}
|
|
||||||
|
|
||||||
class BroadcastChannelIPC extends BasicIPCHandler {
|
|
||||||
private static readonly CHANNEL_NAME = "TeaSpeak-Web";
|
|
||||||
|
|
||||||
private channel: BroadcastChannel;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
super.setup();
|
|
||||||
|
|
||||||
this.channel = new BroadcastChannel(BroadcastChannelIPC.CHANNEL_NAME);
|
|
||||||
this.channel.onmessage = this.on_message.bind(this);
|
|
||||||
this.channel.onmessageerror = this.on_error.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private on_message(event: MessageEvent) {
|
|
||||||
if(typeof(event.data) !== "string") {
|
|
||||||
log.warn(LogCategory.IPC, tr("Received message with an invalid type (%s): %o"), typeof(event.data), event.data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let message: BroadcastMessage;
|
|
||||||
try {
|
|
||||||
message = JSON.parse(event.data);
|
|
||||||
} catch(error) {
|
|
||||||
log.error(LogCategory.IPC, tr("Received an invalid encoded message: %o"), event.data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
super.handle_message(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private on_error(event: MessageEvent) {
|
|
||||||
log.warn(LogCategory.IPC, tr("Received error: %o"), event);
|
|
||||||
}
|
|
||||||
|
|
||||||
send_message(type: string, data: any, target?: string) {
|
|
||||||
const message: BroadcastMessage = {} as any;
|
|
||||||
|
|
||||||
message.sender = this.unique_id;
|
|
||||||
message.receiver = target ? target : BasicIPCHandler.BROADCAST_UNIQUE_ID;
|
|
||||||
message.timestamp = Date.now();
|
|
||||||
message.type = type;
|
|
||||||
message.data = data;
|
|
||||||
|
|
||||||
this.channel.postMessage(JSON.stringify(message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace connect {
|
|
||||||
export type ConnectRequestData = {
|
|
||||||
address: string;
|
|
||||||
|
|
||||||
profile?: string;
|
|
||||||
username?: string;
|
|
||||||
password?: {
|
|
||||||
value: string;
|
|
||||||
hashed: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConnectOffer {
|
|
||||||
request_id: string;
|
|
||||||
data: ConnectRequestData;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConnectOfferAnswer {
|
|
||||||
request_id: string;
|
|
||||||
accepted: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConnectExecute {
|
|
||||||
request_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConnectExecuted {
|
|
||||||
request_id: string;
|
|
||||||
succeeded: boolean;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The connect process:
|
|
||||||
* 1. Broadcast an offer
|
|
||||||
* 2. Wait 50ms for all offer responses or until the first one respond with "ok"
|
|
||||||
* 3. Select (if possible) on accepted offer and execute the connect
|
|
||||||
*/
|
|
||||||
export class ConnectHandler {
|
|
||||||
private static readonly CHANNEL_NAME = "connect";
|
|
||||||
|
|
||||||
readonly ipc_handler: BasicIPCHandler;
|
|
||||||
private ipc_channel: Channel;
|
|
||||||
|
|
||||||
public callback_available: (data: ConnectRequestData) => boolean = () => false;
|
|
||||||
public callback_execute: (data: ConnectRequestData) => boolean | string = () => false;
|
|
||||||
|
|
||||||
|
|
||||||
private _pending_connect_offers: {
|
|
||||||
id: string;
|
|
||||||
data: ConnectRequestData;
|
|
||||||
timeout: number;
|
|
||||||
|
|
||||||
remote_handler: string;
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
private _pending_connects_requests: {
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
data: ConnectRequestData;
|
|
||||||
timeout: number;
|
|
||||||
|
|
||||||
callback_success: () => any;
|
|
||||||
callback_failed: (message: string) => any;
|
|
||||||
callback_avail: () => Promise<boolean>;
|
|
||||||
|
|
||||||
remote_handler?: string;
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
constructor(ipc_handler: BasicIPCHandler) {
|
|
||||||
this.ipc_handler = ipc_handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setup() {
|
|
||||||
this.ipc_channel = this.ipc_handler.create_channel(undefined, ConnectHandler.CHANNEL_NAME);
|
|
||||||
this.ipc_channel.message_handler = this.on_message.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private on_message(sender: string, broadcast: boolean, message: ChannelMessage) {
|
|
||||||
if(broadcast) {
|
|
||||||
if(message.type == "offer") {
|
|
||||||
const data = message.data as ConnectOffer;
|
|
||||||
|
|
||||||
const response = {
|
|
||||||
accepted: this.callback_available(data.data),
|
|
||||||
request_id: data.request_id
|
|
||||||
} as ConnectOfferAnswer;
|
|
||||||
|
|
||||||
if(response.accepted) {
|
|
||||||
log.debug(LogCategory.IPC, tr("Received new connect offer from %s: %s"), sender, data.request_id);
|
|
||||||
|
|
||||||
const ld = {
|
|
||||||
remote_handler: sender,
|
|
||||||
data: data.data,
|
|
||||||
id: data.request_id,
|
|
||||||
timeout: 0
|
|
||||||
};
|
|
||||||
this._pending_connect_offers.push(ld);
|
|
||||||
ld.timeout = setTimeout(() => {
|
|
||||||
log.debug(LogCategory.IPC, tr("Dropping connect request %s, because we never received an execute."), ld.id);
|
|
||||||
this._pending_connect_offers.remove(ld);
|
|
||||||
}, 120 * 1000) as any;
|
|
||||||
}
|
|
||||||
this.ipc_channel.send_message("offer-answer", response, sender);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(message.type == "offer-answer") {
|
|
||||||
const data = message.data as ConnectOfferAnswer;
|
|
||||||
const request = this._pending_connects_requests.find(e => e.id === data.request_id);
|
|
||||||
if(!request) {
|
|
||||||
log.warn(LogCategory.IPC, tr("Received connect offer answer with unknown request id (%s)."), data.request_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(!data.accepted) {
|
|
||||||
log.debug(LogCategory.IPC, tr("Client %s rejected the connect offer (%s)."), sender, request.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(request.remote_handler) {
|
|
||||||
log.debug(LogCategory.IPC, tr("Client %s accepted the connect offer (%s), but offer has already been accepted."), sender, request.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug(LogCategory.IPC, tr("Client %s accepted the connect offer (%s). Request local acceptance."), sender, request.id);
|
|
||||||
request.remote_handler = sender;
|
|
||||||
clearTimeout(request.timeout);
|
|
||||||
|
|
||||||
request.callback_avail().then(flag => {
|
|
||||||
if(!flag) {
|
|
||||||
request.callback_failed("local avail rejected");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug(LogCategory.IPC, tr("Executing connect with client %s"), request.remote_handler);
|
|
||||||
this.ipc_channel.send_message("execute", {
|
|
||||||
request_id: request.id
|
|
||||||
} as ConnectExecute, request.remote_handler);
|
|
||||||
request.timeout = setTimeout(() => {
|
|
||||||
request.callback_failed("connect execute timeout");
|
|
||||||
}, 1000) as any;
|
|
||||||
}).catch(error => {
|
|
||||||
log.error(LogCategory.IPC, tr("Local avail callback caused an error: %o"), error);
|
|
||||||
request.callback_failed(tr("local avail callback caused an error"));
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
else if(message.type == "executed") {
|
|
||||||
const data = message.data as ConnectExecuted;
|
|
||||||
const request = this._pending_connects_requests.find(e => e.id === data.request_id);
|
|
||||||
if(!request) {
|
|
||||||
log.warn(LogCategory.IPC, tr("Received connect executed with unknown request id (%s)."), data.request_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(request.remote_handler != sender) {
|
|
||||||
log.warn(LogCategory.IPC, tr("Received connect executed for request %s, but from wrong client: %s (expected %s)"), data.request_id, sender, request.remote_handler);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug(LogCategory.IPC, tr("Received connect executed response from client %s for request %s. Succeeded: %o (%s)"), sender, data.request_id, data.succeeded, data.message);
|
|
||||||
clearTimeout(request.timeout);
|
|
||||||
if(data.succeeded)
|
|
||||||
request.callback_success();
|
|
||||||
else
|
|
||||||
request.callback_failed(data.message);
|
|
||||||
}
|
|
||||||
else if(message.type == "execute") {
|
|
||||||
const data = message.data as ConnectExecute;
|
|
||||||
const request = this._pending_connect_offers.find(e => e.id === data.request_id);
|
|
||||||
if(!request) {
|
|
||||||
log.warn(LogCategory.IPC, tr("Received connect execute with unknown request id (%s)."), data.request_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(request.remote_handler != sender) {
|
|
||||||
log.warn(LogCategory.IPC, tr("Received connect execute for request %s, but from wrong client: %s (expected %s)"), data.request_id, sender, request.remote_handler);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
clearTimeout(request.timeout);
|
|
||||||
this._pending_connect_offers.remove(request);
|
|
||||||
|
|
||||||
log.debug(LogCategory.IPC, tr("Executing connect for %s"), data.request_id);
|
|
||||||
const cr = this.callback_execute(request.data);
|
|
||||||
|
|
||||||
const response = {
|
|
||||||
request_id: data.request_id,
|
|
||||||
|
|
||||||
succeeded: typeof(cr) !== "string" && cr,
|
|
||||||
message: typeof(cr) === "string" ? cr : "",
|
|
||||||
} as ConnectExecuted;
|
|
||||||
this.ipc_channel.send_message("executed", response, request.remote_handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
post_connect_request(data: ConnectRequestData, callback_avail: () => Promise<boolean>) : Promise<void> {
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
const pd = {
|
|
||||||
data: data,
|
|
||||||
id: uuidv4(),
|
|
||||||
timeout: 0,
|
|
||||||
|
|
||||||
callback_success: () => {
|
|
||||||
this._pending_connects_requests.remove(pd);
|
|
||||||
clearTimeout(pd.timeout);
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
|
|
||||||
callback_failed: error => {
|
|
||||||
this._pending_connects_requests.remove(pd);
|
|
||||||
clearTimeout(pd.timeout);
|
|
||||||
reject(error);
|
|
||||||
},
|
|
||||||
|
|
||||||
callback_avail: callback_avail,
|
|
||||||
};
|
|
||||||
this._pending_connects_requests.push(pd);
|
|
||||||
|
|
||||||
this.ipc_channel.send_message("offer", {
|
|
||||||
request_id: pd.id,
|
|
||||||
data: pd.data
|
|
||||||
} as ConnectOffer);
|
|
||||||
pd.timeout = setTimeout(() => {
|
|
||||||
pd.callback_failed("received no response to offer");
|
|
||||||
}, 50) as any;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace mproxy {
|
|
||||||
export interface MethodProxyInvokeData {
|
|
||||||
method_name: string;
|
|
||||||
arguments: any[];
|
|
||||||
promise_id: string;
|
|
||||||
}
|
|
||||||
export interface MethodProxyResultData {
|
|
||||||
promise_id: string;
|
|
||||||
result: any;
|
|
||||||
success: boolean;
|
|
||||||
}
|
|
||||||
export interface MethodProxyCallback {
|
|
||||||
promise: Promise<any>;
|
|
||||||
promise_id: string;
|
|
||||||
|
|
||||||
resolve: (object: any) => any;
|
|
||||||
reject: (object: any) => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MethodProxyConnectParameters = {
|
|
||||||
channel_id: string;
|
|
||||||
client_id: string;
|
|
||||||
}
|
|
||||||
export abstract class MethodProxy {
|
|
||||||
readonly ipc_handler: BasicIPCHandler;
|
|
||||||
private _ipc_channel: Channel;
|
|
||||||
private _ipc_parameters: MethodProxyConnectParameters;
|
|
||||||
|
|
||||||
private readonly _local: boolean;
|
|
||||||
private readonly _slave: boolean;
|
|
||||||
|
|
||||||
private _connected: boolean;
|
|
||||||
private _proxied_methods: {[key: string]:() => Promise<any>} = {};
|
|
||||||
private _proxied_callbacks: {[key: string]:MethodProxyCallback} = {};
|
|
||||||
|
|
||||||
protected constructor(ipc_handler: BasicIPCHandler, connect_params?: MethodProxyConnectParameters) {
|
|
||||||
this.ipc_handler = ipc_handler;
|
|
||||||
this._ipc_parameters = connect_params;
|
|
||||||
this._connected = false;
|
|
||||||
this._slave = typeof(connect_params) !== "undefined";
|
|
||||||
this._local = typeof(connect_params) !== "undefined" && connect_params.channel_id === "local" && connect_params.client_id === "local";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected setup() {
|
|
||||||
if(this._local) {
|
|
||||||
this._connected = true;
|
|
||||||
this.on_connected();
|
|
||||||
} else {
|
|
||||||
if(this._slave)
|
|
||||||
this._ipc_channel = this.ipc_handler.create_channel(this._ipc_parameters.client_id, this._ipc_parameters.channel_id);
|
|
||||||
else
|
|
||||||
this._ipc_channel = this.ipc_handler.create_channel();
|
|
||||||
|
|
||||||
this._ipc_channel.message_handler = this._handle_message.bind(this);
|
|
||||||
if(this._slave)
|
|
||||||
this._ipc_channel.send_message("initialize", {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected finalize() {
|
|
||||||
if(!this._local) {
|
|
||||||
if(this._connected)
|
|
||||||
this._ipc_channel.send_message("finalize", {});
|
|
||||||
|
|
||||||
this.ipc_handler.delete_channel(this._ipc_channel);
|
|
||||||
this._ipc_channel = undefined;
|
|
||||||
}
|
|
||||||
for(const promise of Object.values(this._proxied_callbacks))
|
|
||||||
promise.reject("disconnected");
|
|
||||||
this._proxied_callbacks = {};
|
|
||||||
|
|
||||||
this._connected = false;
|
|
||||||
this.on_disconnected();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected register_method<R>(method: (...args: any[]) => Promise<R> | string) {
|
|
||||||
let method_name: string;
|
|
||||||
if(typeof method === "function") {
|
|
||||||
log.debug(LogCategory.IPC, tr("Registering method proxy for %s"), method.name);
|
|
||||||
method_name = method.name;
|
|
||||||
} else {
|
|
||||||
log.debug(LogCategory.IPC, tr("Registering method proxy for %s"), method);
|
|
||||||
method_name = method;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!this[method_name])
|
|
||||||
throw "method is missing in current object";
|
|
||||||
|
|
||||||
this._proxied_methods[method_name] = this[method_name];
|
|
||||||
if(!this._local) {
|
|
||||||
this[method_name] = (...args: any[]) => {
|
|
||||||
if(!this._connected)
|
|
||||||
return Promise.reject("not connected");
|
|
||||||
|
|
||||||
const proxy_callback = {
|
|
||||||
promise_id: uuidv4()
|
|
||||||
} as MethodProxyCallback;
|
|
||||||
this._proxied_callbacks[proxy_callback.promise_id] = proxy_callback;
|
|
||||||
proxy_callback.promise = new Promise((resolve, reject) => {
|
|
||||||
proxy_callback.resolve = resolve;
|
|
||||||
proxy_callback.reject = reject;
|
|
||||||
});
|
|
||||||
|
|
||||||
this._ipc_channel.send_message("invoke", {
|
|
||||||
promise_id: proxy_callback.promise_id,
|
|
||||||
arguments: [...args],
|
|
||||||
method_name: method_name
|
|
||||||
} as MethodProxyInvokeData);
|
|
||||||
return proxy_callback.promise;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handle_message(remote_id: string, boradcast: boolean, message: ChannelMessage) {
|
|
||||||
if(message.type === "finalize") {
|
|
||||||
this._handle_finalize();
|
|
||||||
} else if(message.type === "initialize") {
|
|
||||||
this._handle_remote_callback(remote_id);
|
|
||||||
} else if(message.type === "invoke") {
|
|
||||||
this._handle_invoke(message.data);
|
|
||||||
} else if(message.type === "result") {
|
|
||||||
this._handle_result(message.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handle_finalize() {
|
|
||||||
this.on_disconnected();
|
|
||||||
this.finalize();
|
|
||||||
this._connected = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handle_remote_callback(remote_id: string) {
|
|
||||||
if(!this._ipc_channel.target_id) {
|
|
||||||
if(this._slave)
|
|
||||||
throw "initialize wrong state!";
|
|
||||||
|
|
||||||
this._ipc_channel.target_id = remote_id; /* now we're able to send messages */
|
|
||||||
this.on_connected();
|
|
||||||
this._ipc_channel.send_message("initialize", true);
|
|
||||||
} else {
|
|
||||||
if(!this._slave)
|
|
||||||
throw "initialize wrong state!";
|
|
||||||
|
|
||||||
this.on_connected();
|
|
||||||
}
|
|
||||||
this._connected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _send_result(promise_id: string, success: boolean, message: any) {
|
|
||||||
this._ipc_channel.send_message("result", {
|
|
||||||
promise_id: promise_id,
|
|
||||||
result: message,
|
|
||||||
success: success
|
|
||||||
} as MethodProxyResultData);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handle_invoke(data: MethodProxyInvokeData) {
|
|
||||||
if(this._proxied_methods[data.method_name])
|
|
||||||
throw "we could not invoke a local proxied method!";
|
|
||||||
|
|
||||||
if(!this[data.method_name]) {
|
|
||||||
this._send_result(data.promise_id, false, "missing method");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
log.info(LogCategory.IPC, tr("Invoking method %s with arguments: %o"), data.method_name, data.arguments);
|
|
||||||
|
|
||||||
const promise = this[data.method_name](...data.arguments);
|
|
||||||
promise.then(result => {
|
|
||||||
log.info(LogCategory.IPC, tr("Result: %o"), result);
|
|
||||||
this._send_result(data.promise_id, true, result);
|
|
||||||
}).catch(error => {
|
|
||||||
this._send_result(data.promise_id, false, error);
|
|
||||||
});
|
|
||||||
} catch(error) {
|
|
||||||
this._send_result(data.promise_id, false, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handle_result(data: MethodProxyResultData) {
|
|
||||||
if(!this._proxied_callbacks[data.promise_id]) {
|
|
||||||
console.warn(tr("Received proxy method result for unknown promise"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const callback = this._proxied_callbacks[data.promise_id];
|
|
||||||
delete this._proxied_callbacks[data.promise_id];
|
|
||||||
|
|
||||||
if(data.success)
|
|
||||||
callback.resolve(data.result);
|
|
||||||
else
|
|
||||||
callback.reject(data.result);
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_connect_parameters() : MethodProxyConnectParameters {
|
|
||||||
if(this._slave)
|
|
||||||
throw "only masters can generate connect parameters!";
|
|
||||||
if(!this._ipc_channel)
|
|
||||||
throw "please call setup() before";
|
|
||||||
|
|
||||||
return {
|
|
||||||
channel_id: this._ipc_channel.channel_id,
|
|
||||||
client_id: this.ipc_handler.get_local_address()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
is_slave() { return this._local || this._slave; } /* the popout modal */
|
|
||||||
is_master() { return this._local || !this._slave; } /* the host (teaweb application) */
|
|
||||||
|
|
||||||
protected abstract on_connected();
|
|
||||||
protected abstract on_disconnected();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let handler: BasicIPCHandler;
|
|
||||||
let connect_handler: connect.ConnectHandler;
|
|
||||||
|
|
||||||
export function setup() {
|
|
||||||
if(!supported())
|
|
||||||
return;
|
|
||||||
|
|
||||||
handler = new BroadcastChannelIPC();
|
|
||||||
handler.setup();
|
|
||||||
|
|
||||||
connect_handler = new connect.ConnectHandler(handler);
|
|
||||||
connect_handler.setup();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function get_handler() {
|
|
||||||
return handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function get_connect_handler() {
|
|
||||||
return connect_handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function supported() {
|
|
||||||
/* ios does not support this */
|
|
||||||
return typeof(window.BroadcastChannel) !== "undefined";
|
|
||||||
}
|
|
|
@ -18,7 +18,7 @@ import * as htmltags from "./ui/htmltags";
|
||||||
import {ChannelEntry} from "tc-shared/ui/channel";
|
import {ChannelEntry} from "tc-shared/ui/channel";
|
||||||
import {InputStartResult, InputState} from "tc-shared/voice/RecorderBase";
|
import {InputStartResult, InputState} from "tc-shared/voice/RecorderBase";
|
||||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||||
import * as bipc from "./BrowserIPC";
|
import * as bipc from "./ipc/BrowserIPC";
|
||||||
import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
|
import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
|
||||||
import {Frame} from "tc-shared/ui/frames/chat_frame";
|
import {Frame} from "tc-shared/ui/frames/chat_frame";
|
||||||
import {Hostbanner} from "tc-shared/ui/frames/hostbanner";
|
import {Hostbanner} from "tc-shared/ui/frames/hostbanner";
|
||||||
|
@ -34,6 +34,7 @@ import {FileManager} from "tc-shared/file/FileManager";
|
||||||
import {FileTransferState, TransferProvider} from "tc-shared/file/Transfer";
|
import {FileTransferState, TransferProvider} from "tc-shared/file/Transfer";
|
||||||
import {traj} from "tc-shared/i18n/localize";
|
import {traj} from "tc-shared/i18n/localize";
|
||||||
import {md5} from "tc-shared/crypto/md5";
|
import {md5} from "tc-shared/crypto/md5";
|
||||||
|
import {guid} from "tc-shared/crypto/uid";
|
||||||
|
|
||||||
export enum DisconnectReason {
|
export enum DisconnectReason {
|
||||||
HANDLER_DESTROYED,
|
HANDLER_DESTROYED,
|
||||||
|
@ -126,6 +127,8 @@ export interface ConnectParameters {
|
||||||
|
|
||||||
declare const native_client;
|
declare const native_client;
|
||||||
export class ConnectionHandler {
|
export class ConnectionHandler {
|
||||||
|
readonly handlerId: string;
|
||||||
|
|
||||||
private readonly event_registry: Registry<ConnectionEvents>;
|
private readonly event_registry: Registry<ConnectionEvents>;
|
||||||
channelTree: ChannelTree;
|
channelTree: ChannelTree;
|
||||||
|
|
||||||
|
@ -172,8 +175,9 @@ export class ConnectionHandler {
|
||||||
log: ServerLog;
|
log: ServerLog;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.handlerId = guid();
|
||||||
this.event_registry = new Registry<ConnectionEvents>();
|
this.event_registry = new Registry<ConnectionEvents>();
|
||||||
this.event_registry.enable_debug("connection-handler");
|
this.event_registry.enableDebug("connection-handler");
|
||||||
|
|
||||||
this.settings = new ServerSettings();
|
this.settings = new ServerSettings();
|
||||||
|
|
||||||
|
@ -435,7 +439,7 @@ export class ConnectionHandler {
|
||||||
if(popup)
|
if(popup)
|
||||||
popup.close();
|
popup.close();
|
||||||
|
|
||||||
properties["certificate_callback"] = bipc.get_handler().register_certificate_accept_callback(() => {
|
properties["certificate_callback"] = bipc.getInstance().register_certificate_accept_callback(() => {
|
||||||
log.info(LogCategory.GENERAL, tr("Received notification that the certificate has been accepted! Attempting reconnect!"));
|
log.info(LogCategory.GENERAL, tr("Received notification that the certificate has been accepted! Attempting reconnect!"));
|
||||||
if(this._certificate_modal)
|
if(this._certificate_modal)
|
||||||
this._certificate_modal.close();
|
this._certificate_modal.close();
|
||||||
|
@ -448,7 +452,7 @@ export class ConnectionHandler {
|
||||||
});
|
});
|
||||||
|
|
||||||
const url = build_url(document.location.origin + pathname + "/popup/certaccept/", "", properties);
|
const url = build_url(document.location.origin + pathname + "/popup/certaccept/", "", properties);
|
||||||
const features_string = [...Object.keys(features)].map(e => e + "=" + features[e]).reduce((a, b) => a + "," + b);
|
const features_string = Object.keys(features).map(e => e + "=" + features[e]).join(",");
|
||||||
popup = window.open(url, "TeaWeb certificate accept", features_string);
|
popup = window.open(url, "TeaWeb certificate accept", features_string);
|
||||||
try {
|
try {
|
||||||
popup.focus();
|
popup.focus();
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import * as loader from "tc-loader";
|
||||||
|
import {Stage} from "tc-loader";
|
||||||
|
|
||||||
|
import * as ipc from "./ipc/BrowserIPC";
|
||||||
|
import * as i18n from "./i18n/localize";
|
||||||
|
|
||||||
|
import "./proto";
|
||||||
|
|
||||||
|
import {spawnVideoPopout} from "tc-shared/video-viewer/Controller";
|
||||||
|
|
||||||
|
console.error("Hello World from devel main");
|
||||||
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
|
name: "setup",
|
||||||
|
priority: 10,
|
||||||
|
function: async () => {
|
||||||
|
await i18n.initialize();
|
||||||
|
ipc.setup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loader.register_task(Stage.LOADED, {
|
||||||
|
name: "invoke",
|
||||||
|
priority: 10,
|
||||||
|
function: async () => {
|
||||||
|
console.error("Spawning video popup");
|
||||||
|
spawnVideoPopout();
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,4 +1,3 @@
|
||||||
import {ClientEvents, MusicClientEntry, SongInfo} from "tc-shared/ui/client";
|
|
||||||
import {guid} from "tc-shared/crypto/uid";
|
import {guid} from "tc-shared/crypto/uid";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {useEffect} from "react";
|
import {useEffect} from "react";
|
||||||
|
@ -20,29 +19,34 @@ export class SingletonEvent implements Event<SingletonEvents, "singletone-instan
|
||||||
as<T extends keyof SingletonEvents>() : SingletonEvents[T] { return; }
|
as<T extends keyof SingletonEvents>() : SingletonEvents[T] { return; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EventReceiver<Events> {
|
||||||
|
fire<T extends keyof Events>(event_type: T, data?: Events[T], overrideTypeKey?: boolean);
|
||||||
|
fire_async<T extends keyof Events>(event_type: T, data?: Events[T], callback?: () => void);
|
||||||
|
}
|
||||||
|
|
||||||
const event_annotation_key = guid();
|
const event_annotation_key = guid();
|
||||||
export class Registry<Events> {
|
export class Registry<Events> implements EventReceiver<Events> {
|
||||||
private readonly registry_uuid;
|
private readonly registryUuid;
|
||||||
|
|
||||||
private handler: {[key: string]: ((event) => void)[]} = {};
|
private handler: {[key: string]: ((event) => void)[]} = {};
|
||||||
private connections: {[key: string]:Registry<string>[]} = {};
|
private connections: {[key: string]: EventReceiver<Events>[]} = {};
|
||||||
private event_handler_objects: {
|
private eventHandlerObjects: {
|
||||||
object: any,
|
object: any,
|
||||||
handlers: {[key: string]: ((event) => void)[]}
|
handlers: {[key: string]: ((event) => void)[]}
|
||||||
}[] = [];
|
}[] = [];
|
||||||
private debug_prefix = undefined;
|
private debugPrefix = undefined;
|
||||||
private warn_unhandled_events = false;
|
private warnUnhandledEvents = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.registry_uuid = "evreg_data_" + guid();
|
this.registryUuid = "evreg_data_" + guid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
enable_debug(prefix: string) { this.debug_prefix = prefix || "---"; }
|
enableDebug(prefix: string) { this.debugPrefix = prefix || "---"; }
|
||||||
disable_debug() { this.debug_prefix = undefined; }
|
disableDebug() { this.debugPrefix = undefined; }
|
||||||
|
|
||||||
enable_warn_unhandled_events() { this.warn_unhandled_events = true; }
|
enable_warn_unhandled_events() { this.warnUnhandledEvents = true; }
|
||||||
disable_warn_unhandled_events() { this.warn_unhandled_events = false; }
|
disable_warn_unhandled_events() { this.warnUnhandledEvents = false; }
|
||||||
|
|
||||||
on<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void) : () => void;
|
on<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void) : () => void;
|
||||||
on(events: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => void) : () => void;
|
on(events: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => void) : () => void;
|
||||||
|
@ -50,7 +54,7 @@ export class Registry<Events> {
|
||||||
if(!Array.isArray(events))
|
if(!Array.isArray(events))
|
||||||
events = [events];
|
events = [events];
|
||||||
|
|
||||||
handler[this.registry_uuid] = {
|
handler[this.registryUuid] = {
|
||||||
singleshot: false
|
singleshot: false
|
||||||
};
|
};
|
||||||
for(const event of events) {
|
for(const event of events) {
|
||||||
|
@ -60,6 +64,14 @@ export class Registry<Events> {
|
||||||
return () => this.off(events, handler);
|
return () => this.off(events, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onAll(handler: (event?: Event<Events>) => void) : () => void {
|
||||||
|
handler[this.registryUuid] = {
|
||||||
|
singleshot: false
|
||||||
|
};
|
||||||
|
(this.handler[null as any] || (this.handler[null as any] = [])).push(handler);
|
||||||
|
return () => this.offAll(handler);
|
||||||
|
}
|
||||||
|
|
||||||
/* one */
|
/* one */
|
||||||
one<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void) : () => void;
|
one<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void) : () => void;
|
||||||
one(events: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => void) : () => void;
|
one(events: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => void) : () => void;
|
||||||
|
@ -70,7 +82,7 @@ export class Registry<Events> {
|
||||||
for(const event of events) {
|
for(const event of events) {
|
||||||
const handlers = this.handler[event] || (this.handler[event] = []);
|
const handlers = this.handler[event] || (this.handler[event] = []);
|
||||||
|
|
||||||
handler[this.registry_uuid] = { singleshot: true };
|
handler[this.registryUuid] = { singleshot: true };
|
||||||
handlers.push(handler);
|
handlers.push(handler);
|
||||||
}
|
}
|
||||||
return () => this.off(events, handler);
|
return () => this.off(events, handler);
|
||||||
|
@ -94,6 +106,10 @@ export class Registry<Events> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
offAll(handler: (event?: Event<Events>) => void) {
|
||||||
|
(this.handler[null as any] || []).remove(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* special helper methods for react components */
|
/* special helper methods for react components */
|
||||||
reactUse<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void, condition?: boolean) {
|
reactUse<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void, condition?: boolean) {
|
||||||
|
@ -108,23 +124,28 @@ export class Registry<Events> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
connect<EOther, T extends keyof Events & keyof EOther>(events: T | T[], target: Registry<EOther>) {
|
connectAll<EOther, T extends keyof Events & keyof EOther>(target: EventReceiver<Events>) {
|
||||||
|
(this.connections[null as any] || (this.connections[null as any] = [])).push(target as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect<EOther, T extends keyof Events & keyof EOther>(events: T | T[], target: EventReceiver<Events>) {
|
||||||
for(const event of Array.isArray(events) ? events : [events])
|
for(const event of Array.isArray(events) ? events : [events])
|
||||||
(this.connections[event as string] || (this.connections[event as string] = [])).push(target as any);
|
(this.connections[event as string] || (this.connections[event as string] = [])).push(target as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect<EOther, T extends keyof Events & keyof EOther>(events: T | T[], target: Registry<EOther>) {
|
disconnect<EOther, T extends keyof Events & keyof EOther>(events: T | T[], target: EventReceiver<Events>) {
|
||||||
for(const event of Array.isArray(events) ? events : [events])
|
for(const event of Array.isArray(events) ? events : [events])
|
||||||
(this.connections[event as string] || []).remove(target as any);
|
(this.connections[event as string] || []).remove(target as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect_all<EOther>(target: Registry<EOther>) {
|
disconnectAll<EOther>(target: EventReceiver<Events>) {
|
||||||
|
this.connections[null as any]?.remove(target);
|
||||||
for(const event of Object.keys(this.connections))
|
for(const event of Object.keys(this.connections))
|
||||||
this.connections[event].remove(target as any);
|
this.connections[event].remove(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
fire<T extends keyof Events>(event_type: T, data?: Events[T], overrideTypeKey?: boolean) {
|
fire<T extends keyof Events>(event_type: T, data?: Events[T], overrideTypeKey?: boolean) {
|
||||||
if(this.debug_prefix) console.log("[%s] Trigger event: %s", this.debug_prefix, event_type);
|
if(this.debugPrefix) console.log("[%s] Trigger event: %s", this.debugPrefix, event_type);
|
||||||
|
|
||||||
if(typeof data === "object" && 'type' in data && !overrideTypeKey) {
|
if(typeof data === "object" && 'type' in data && !overrideTypeKey) {
|
||||||
if((data as any).type !== event_type) {
|
if((data as any).type !== event_type) {
|
||||||
|
@ -137,26 +158,35 @@ export class Registry<Events> {
|
||||||
as: function () { return this; }
|
as: function () { return this; }
|
||||||
});
|
});
|
||||||
|
|
||||||
this.fire_event(event_type as string, event);
|
this.fire_event(event_type, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fire_event(type: string, data: any) {
|
private fire_event(type: keyof Events, data: any) {
|
||||||
let invoke_count = 0;
|
let invokeCount = 0;
|
||||||
for(const handler of (this.handler[type]?.slice(0) || [])) {
|
|
||||||
|
const typedHandler = this.handler[type as string] || [];
|
||||||
|
const generalHandler = this.handler[null as string] || [];
|
||||||
|
for(const handler of [...generalHandler, ...typedHandler]) {
|
||||||
handler(data);
|
handler(data);
|
||||||
invoke_count++;
|
invokeCount++;
|
||||||
|
|
||||||
const reg_data = handler[this.registry_uuid];
|
const regData = handler[this.registryUuid];
|
||||||
if(typeof reg_data === "object" && reg_data.singleshot)
|
if(typeof regData === "object" && regData.singleshot)
|
||||||
this.handler[type].remove(handler);
|
this.handler[type as string].remove(handler); /* FIXME: General single shot? */
|
||||||
}
|
}
|
||||||
|
|
||||||
for(const evhandler of (this.connections[type]?.slice(0) || [])) {
|
const typedConnections = this.connections[type as string] || [];
|
||||||
evhandler.fire_event(type, data);
|
const generalConnections = this.connections[null as string] || [];
|
||||||
invoke_count++;
|
for(const evhandler of [...generalConnections, ...typedConnections]) {
|
||||||
|
if('fire_event' in evhandler)
|
||||||
|
/* evhandler is an event registry as well. We don't have to check for any inappropriate keys */
|
||||||
|
(evhandler as any).fire_event(type, data);
|
||||||
|
else
|
||||||
|
evhandler.fire(type, data);
|
||||||
|
invokeCount++;
|
||||||
}
|
}
|
||||||
if(this.warn_unhandled_events && invoke_count === 0) {
|
if(this.warnUnhandledEvents && invokeCount === 0) {
|
||||||
console.warn(tr("Event handler (%s) triggered event %s which has no consumers."), this.debug_prefix, type);
|
console.warn(tr("Event handler (%s) triggered event %s which has no consumers."), this.debugPrefix, type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,7 +202,7 @@ export class Registry<Events> {
|
||||||
destroy() {
|
destroy() {
|
||||||
this.handler = {};
|
this.handler = {};
|
||||||
this.connections = {};
|
this.connections = {};
|
||||||
this.event_handler_objects = [];
|
this.eventHandlerObjects = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
register_handler(handler: any, parentClasses?: boolean) {
|
register_handler(handler: any, parentClasses?: boolean) {
|
||||||
|
@ -213,17 +243,17 @@ export class Registry<Events> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.event_handler_objects.push({
|
this.eventHandlerObjects.push({
|
||||||
handlers: registered_events,
|
handlers: registered_events,
|
||||||
object: handler
|
object: handler
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
unregister_handler(handler: any) {
|
unregister_handler(handler: any) {
|
||||||
const data = this.event_handler_objects.find(e => e.object === handler);
|
const data = this.eventHandlerObjects.find(e => e.object === handler);
|
||||||
if(!data) return;
|
if(!data) return;
|
||||||
|
|
||||||
this.event_handler_objects.remove(data);
|
this.eventHandlerObjects.remove(data);
|
||||||
|
|
||||||
for(const key of Object.keys(data.handlers)) {
|
for(const key of Object.keys(data.handlers)) {
|
||||||
for(const evhandler of data.handlers[key])
|
for(const evhandler of data.handlers[key])
|
||||||
|
@ -278,48 +308,7 @@ export function ReactEventHandler<ObjectClass = React.Component<any, any>, Event
|
||||||
|
|
||||||
export namespace sidebar {
|
export namespace sidebar {
|
||||||
export interface music {
|
export interface music {
|
||||||
"open": {}, /* triggers when frame should be shown */
|
|
||||||
"close": {}, /* triggers when frame will be closed */
|
|
||||||
|
|
||||||
"bot_change": {
|
|
||||||
old: MusicClientEntry | undefined,
|
|
||||||
new: MusicClientEntry | undefined
|
|
||||||
},
|
|
||||||
"bot_property_update": {
|
|
||||||
properties: string[]
|
|
||||||
},
|
|
||||||
|
|
||||||
"action_play": {},
|
|
||||||
"action_pause": {},
|
|
||||||
"action_song_set": { song_id: number },
|
|
||||||
"action_forward": {},
|
|
||||||
"action_rewind": {},
|
|
||||||
"action_forward_ms": {
|
|
||||||
units: number;
|
|
||||||
},
|
|
||||||
"action_rewind_ms": {
|
|
||||||
units: number;
|
|
||||||
},
|
|
||||||
"action_song_add": {},
|
|
||||||
"action_song_delete": { song_id: number },
|
|
||||||
"action_playlist_reload": {},
|
|
||||||
|
|
||||||
"playtime_move_begin": {},
|
|
||||||
"playtime_move_end": {
|
|
||||||
canceled: boolean,
|
|
||||||
target_time?: number
|
|
||||||
},
|
|
||||||
|
|
||||||
"reorder_begin": { song_id: number; entry: JQuery },
|
|
||||||
"reorder_end": { song_id: number; canceled: boolean; entry: JQuery; previous_entry?: number },
|
|
||||||
|
|
||||||
"player_time_update": ClientEvents["music_status_update"],
|
|
||||||
"player_song_change": ClientEvents["music_song_change"],
|
|
||||||
|
|
||||||
"playlist_song_add": ClientEvents["playlist_song_add"] & { insert_effect?: boolean },
|
|
||||||
"playlist_song_remove": ClientEvents["playlist_song_remove"],
|
|
||||||
"playlist_song_reorder": ClientEvents["playlist_song_reorder"],
|
|
||||||
"playlist_song_loaded": ClientEvents["playlist_song_loaded"] & { html_entry?: JQuery },
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -722,13 +711,3 @@ export namespace modal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Some test code
|
|
||||||
/*
|
|
||||||
const eclient = new Registry<ClientEvents>();
|
|
||||||
const emusic = new Registry<sidebar.music>();
|
|
||||||
|
|
||||||
eclient.on("property_update", event => { event.as<"playlist_song_loaded">(); });
|
|
||||||
eclient.connect("playlist_song_loaded", emusic);
|
|
||||||
eclient.connect("playlist_song_loaded", emusic);
|
|
||||||
*/
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
import * as hex from "tc-shared/crypto/hex";
|
||||||
|
|
||||||
|
export const kIPCAvatarChannel = "avatars";
|
||||||
|
export const kDefaultAvatarImage = "img/style/avatar.png";
|
||||||
|
export type AvatarState = "unset" | "loading" | "errored" | "loaded";
|
||||||
|
|
||||||
|
export interface AvatarStateData {
|
||||||
|
"unset": {},
|
||||||
|
"loading": {},
|
||||||
|
"errored": { message: string },
|
||||||
|
"loaded": { url: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AvatarEvents {
|
||||||
|
avatar_changed: {
|
||||||
|
newAvatarHash: string
|
||||||
|
},
|
||||||
|
avatar_state_changed: {
|
||||||
|
oldState: AvatarState,
|
||||||
|
newState: AvatarState,
|
||||||
|
newStateData: AvatarStateData[keyof AvatarStateData]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class ClientAvatar {
|
||||||
|
readonly events: Registry<AvatarEvents>;
|
||||||
|
readonly clientAvatarId: string; /* the base64 unique id thing from a-m */
|
||||||
|
|
||||||
|
private currentAvatarHash: string | "unknown"; /* the client avatars flag */
|
||||||
|
private state: AvatarState = "loading";
|
||||||
|
|
||||||
|
private stateData: AvatarStateData[AvatarState] = {};
|
||||||
|
|
||||||
|
loadingTimestamp: number = 0;
|
||||||
|
|
||||||
|
constructor(clientAvatarId: string) {
|
||||||
|
this.clientAvatarId = clientAvatarId;
|
||||||
|
this.events = new Registry<AvatarEvents>();
|
||||||
|
|
||||||
|
this.events.on("avatar_state_changed", event => { this.state = event.newState; this.stateData = event.newStateData; });
|
||||||
|
this.events.on("avatar_changed", event => this.currentAvatarHash = event.newAvatarHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.setState("unset", {});
|
||||||
|
this.events.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setState<T extends AvatarState>(state: T, data: AvatarStateData[T], force?: boolean) {
|
||||||
|
if(this.state === state && !force)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.destroyStateData(this.state, this.stateData);
|
||||||
|
this.events.fire("avatar_state_changed", { newState: state, oldState: this.state, newStateData: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTypedStateData<T extends AvatarState>(state: T) : AvatarStateData[T] {
|
||||||
|
if(this.state !== state)
|
||||||
|
throw "invalid avatar state";
|
||||||
|
|
||||||
|
return this.stateData as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setUnset() {
|
||||||
|
this.setState("unset", {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public setLoading() {
|
||||||
|
this.setState("loading", {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public setLoaded(data: AvatarStateData["loaded"]) {
|
||||||
|
this.setState("loaded", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setErrored(data: AvatarStateData["errored"]) {
|
||||||
|
this.setState("errored", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async awaitLoaded() {
|
||||||
|
if(this.state !== "loading")
|
||||||
|
return;
|
||||||
|
|
||||||
|
await new Promise(resolve => this.events.on("avatar_state_changed", event => event.newState !== "loading" && resolve()));
|
||||||
|
}
|
||||||
|
|
||||||
|
getState() : AvatarState {
|
||||||
|
return this.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStateData() : AvatarStateData[AvatarState] {
|
||||||
|
return this.stateData;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvatarHash() : string | "unknown" {
|
||||||
|
return this.currentAvatarHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvatarUrl() {
|
||||||
|
if(this.state === "loaded")
|
||||||
|
return this.getTypedStateData("loaded").url || kDefaultAvatarImage;
|
||||||
|
return kDefaultAvatarImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLoadError() {
|
||||||
|
return this.getTypedStateData("errored").message;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract destroyStateData(state: AvatarState, data: AvatarStateData[AvatarState]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class AbstractAvatarManager {
|
||||||
|
abstract resolveAvatar(clientAvatarId: string, avatarHash?: string) : ClientAvatar;
|
||||||
|
abstract resolveClientAvatar(client: { id?: number, database_id?: number, clientUniqueId: string });
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class AbstractAvatarManagerFactory {
|
||||||
|
abstract hasManager(handlerId: string) : boolean;
|
||||||
|
abstract getManager(handlerId: string) : AbstractAvatarManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
let globalAvatarManagerFactory: AbstractAvatarManagerFactory;
|
||||||
|
export function setGlobalAvatarManagerFactory(factory: AbstractAvatarManagerFactory) {
|
||||||
|
if(globalAvatarManagerFactory)
|
||||||
|
throw "global avatar manager factory has already been set";
|
||||||
|
globalAvatarManagerFactory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGlobalAvatarManagerFactory() {
|
||||||
|
return globalAvatarManagerFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uniqueId2AvatarId(unique_id: string) {
|
||||||
|
function str2ab(str) {
|
||||||
|
let buf = new ArrayBuffer(str.length); // 2 bytes for each char
|
||||||
|
let bufView = new Uint8Array(buf);
|
||||||
|
for (let i=0, strLen = str.length; i<strLen; i++) {
|
||||||
|
bufView[i] = str.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let raw = atob(unique_id);
|
||||||
|
let input = hex.encode(str2ab(raw));
|
||||||
|
|
||||||
|
let result: string = "";
|
||||||
|
for(let index = 0; index < input.length; index++) {
|
||||||
|
let c = input.charAt(index);
|
||||||
|
let offset: number = 0;
|
||||||
|
if(c >= '0' && c <= '9')
|
||||||
|
offset = c.charCodeAt(0) - '0'.charCodeAt(0);
|
||||||
|
else if(c >= 'A' && c <= 'F')
|
||||||
|
offset = c.charCodeAt(0) - 'A'.charCodeAt(0) + 0x0A;
|
||||||
|
else if(c >= 'a' && c <= 'f')
|
||||||
|
offset = c.charCodeAt(0) - 'a'.charCodeAt(0) + 0x0A;
|
||||||
|
result += String.fromCharCode('a'.charCodeAt(0) + offset);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (e) { //invalid base 64 (like music bot etc)
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import {ServerCommand} from "tc-shared/connection/ConnectionBase";
|
||||||
import {CommandResult, ErrorCode, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
|
import {CommandResult, ErrorCode, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||||
import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler";
|
import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler";
|
||||||
import {IconManager} from "tc-shared/file/Icons";
|
import {IconManager} from "tc-shared/file/Icons";
|
||||||
import {AvatarManager} from "tc-shared/file/Avatars";
|
import {AvatarManager} from "tc-shared/file/LocalAvatars";
|
||||||
import {
|
import {
|
||||||
CancelReason,
|
CancelReason,
|
||||||
FileDownloadTransfer,
|
FileDownloadTransfer,
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import * as log from "tc-shared/log";
|
import * as log from "tc-shared/log";
|
||||||
import {LogCategory} from "tc-shared/log";
|
import {LogCategory} from "tc-shared/log";
|
||||||
import * as hex from "tc-shared/crypto/hex";
|
import * as ipc from "../ipc/BrowserIPC";
|
||||||
|
import {ChannelMessage} from "../ipc/BrowserIPC";
|
||||||
|
import * as loader from "tc-loader";
|
||||||
|
import {Stage} from "tc-loader";
|
||||||
import {image_type, ImageCache, media_image_type} from "tc-shared/file/ImageCache";
|
import {image_type, ImageCache, media_image_type} from "tc-shared/file/ImageCache";
|
||||||
import {FileManager} from "tc-shared/file/FileManager";
|
import {FileManager} from "tc-shared/file/FileManager";
|
||||||
import {
|
import {
|
||||||
|
@ -12,69 +15,40 @@ import {
|
||||||
} from "tc-shared/file/Transfer";
|
} from "tc-shared/file/Transfer";
|
||||||
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
|
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||||
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
|
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
|
||||||
import {Registry} from "tc-shared/events";
|
|
||||||
import {ClientEntry} from "tc-shared/ui/client";
|
import {ClientEntry} from "tc-shared/ui/client";
|
||||||
import {tr} from "tc-shared/i18n/localize";
|
import {tr} from "tc-shared/i18n/localize";
|
||||||
|
import {
|
||||||
|
AbstractAvatarManager,
|
||||||
|
AbstractAvatarManagerFactory,
|
||||||
|
AvatarState,
|
||||||
|
AvatarStateData,
|
||||||
|
ClientAvatar,
|
||||||
|
kIPCAvatarChannel,
|
||||||
|
setGlobalAvatarManagerFactory,
|
||||||
|
uniqueId2AvatarId
|
||||||
|
} from "tc-shared/file/Avatars";
|
||||||
|
import {IPCChannel} from "tc-shared/ipc/BrowserIPC";
|
||||||
|
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||||
|
|
||||||
/* FIXME: Retry avatar download after some time! */
|
/* FIXME: Retry avatar download after some time! */
|
||||||
|
|
||||||
const DefaultAvatarImage = "img/style/avatar.png";
|
class LocalClientAvatar extends ClientAvatar {
|
||||||
|
protected destroyStateData(state: AvatarState, data: AvatarStateData[AvatarState]) {
|
||||||
export type AvatarState = "unset" | "loading" | "errored" | "loaded";
|
if(state === "loaded") {
|
||||||
|
const tdata = data as AvatarStateData["loaded"];
|
||||||
interface AvatarEvents {
|
URL.revokeObjectURL(tdata.url);
|
||||||
avatar_changed: {},
|
}
|
||||||
avatar_state_changed: { oldState: AvatarState, newState: AvatarState }
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ClientAvatar {
|
|
||||||
readonly events: Registry<AvatarEvents>;
|
|
||||||
readonly clientAvatarId: string; /* the base64 unique id thing from a-m */
|
|
||||||
|
|
||||||
currentAvatarHash: string | "unknown"; /* the client avatars flag */
|
|
||||||
state: AvatarState = "loading";
|
|
||||||
|
|
||||||
/* only set when state is unset, loaded or errored */
|
|
||||||
avatarUrl?: string;
|
|
||||||
loadError?: string;
|
|
||||||
|
|
||||||
loadingTimestamp: number = 0;
|
|
||||||
|
|
||||||
constructor(client_avatar_id: string) {
|
|
||||||
this.clientAvatarId = client_avatar_id;
|
|
||||||
this.events = new Registry<AvatarEvents>();
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(state: AvatarState) {
|
|
||||||
if(state === this.state)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const oldState = this.state;
|
|
||||||
this.state = state;
|
|
||||||
this.events.fire("avatar_state_changed", { newState: state, oldState: oldState });
|
|
||||||
}
|
|
||||||
|
|
||||||
async awaitLoaded() {
|
|
||||||
if(this.state !== "loading")
|
|
||||||
return;
|
|
||||||
|
|
||||||
await new Promise(resolve => this.events.on("avatar_state_changed", event => event.newState !== "loading" && resolve()));
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyUrl() {
|
|
||||||
URL.revokeObjectURL(this.avatarUrl);
|
|
||||||
this.avatarUrl = DefaultAvatarImage;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AvatarManager {
|
export class AvatarManager extends AbstractAvatarManager {
|
||||||
handle: FileManager;
|
handle: FileManager;
|
||||||
|
|
||||||
private static cache: ImageCache;
|
private static cache: ImageCache;
|
||||||
|
|
||||||
|
|
||||||
private cachedAvatars: {[avatarId: string]: ClientAvatar} = {};
|
private cachedAvatars: {[avatarId: string]: LocalClientAvatar} = {};
|
||||||
constructor(handle: FileManager) {
|
constructor(handle: FileManager) {
|
||||||
|
super();
|
||||||
this.handle = handle;
|
this.handle = handle;
|
||||||
|
|
||||||
if(!AvatarManager.cache)
|
if(!AvatarManager.cache)
|
||||||
|
@ -82,7 +56,7 @@ export class AvatarManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
Object.values(this.cachedAvatars).forEach(e => e.destroyUrl());
|
Object.values(this.cachedAvatars).forEach(e => e.destroy());
|
||||||
this.cachedAvatars = {};
|
this.cachedAvatars = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,14 +70,13 @@ export class AvatarManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async executeAvatarLoad0(avatar: ClientAvatar) {
|
private async executeAvatarLoad0(avatar: LocalClientAvatar) {
|
||||||
if(avatar.currentAvatarHash === "") {
|
if(avatar.getAvatarHash() === "") {
|
||||||
avatar.destroyUrl();
|
avatar.setUnset();
|
||||||
avatar.setState("unset");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialAvatarHash = avatar.currentAvatarHash;
|
let initialAvatarHash = avatar.getAvatarHash();
|
||||||
let avatarResponse: Response;
|
let avatarResponse: Response;
|
||||||
|
|
||||||
/* try to lookup our cache for the avatar */
|
/* try to lookup our cache for the avatar */
|
||||||
|
@ -118,18 +91,19 @@ export class AvatarManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
let cachedAvatarHash = response.headers.has("X-avatar-version") ? response.headers.get("X-avatar-version") : undefined;
|
let cachedAvatarHash = response.headers.has("X-avatar-version") ? response.headers.get("X-avatar-version") : undefined;
|
||||||
if(avatar.currentAvatarHash !== "unknown") {
|
if(avatar.getAvatarHash() !== "unknown") {
|
||||||
if(cachedAvatarHash === undefined) {
|
if(cachedAvatarHash === undefined) {
|
||||||
log.debug(LogCategory.FILE_TRANSFER, tr("Invalidating cached avatar for %s (Version miss match. Cached: unset, Current: %s)"), avatar.clientAvatarId, avatar.currentAvatarHash);
|
log.debug(LogCategory.FILE_TRANSFER, tr("Invalidating cached avatar for %s (Version miss match. Cached: unset, Current: %s)"), avatar.clientAvatarId, avatar.getAvatarHash());
|
||||||
await AvatarManager.cache.delete('avatar_' + avatar.clientAvatarId);
|
await AvatarManager.cache.delete('avatar_' + avatar.clientAvatarId);
|
||||||
break cache_lookup;
|
break cache_lookup;
|
||||||
} else if(cachedAvatarHash !== avatar.currentAvatarHash) {
|
} else if(cachedAvatarHash !== avatar.getAvatarHash()) {
|
||||||
log.debug(LogCategory.FILE_TRANSFER, tr("Invalidating cached avatar for %s (Version miss match. Cached: %s, Current: %s)"), avatar.clientAvatarId, cachedAvatarHash, avatar.currentAvatarHash);
|
log.debug(LogCategory.FILE_TRANSFER, tr("Invalidating cached avatar for %s (Version miss match. Cached: %s, Current: %s)"), avatar.clientAvatarId, cachedAvatarHash, avatar.getAvatarHash());
|
||||||
await AvatarManager.cache.delete('avatar_' + avatar.clientAvatarId);
|
await AvatarManager.cache.delete('avatar_' + avatar.clientAvatarId);
|
||||||
break cache_lookup;
|
break cache_lookup;
|
||||||
}
|
}
|
||||||
} else if(cachedAvatarHash) {
|
} else if(cachedAvatarHash) {
|
||||||
avatar.currentAvatarHash = cachedAvatarHash;
|
avatar.events.fire("avatar_changed", { newAvatarHash: cachedAvatarHash });
|
||||||
|
initialAvatarHash = cachedAvatarHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
avatarResponse = response;
|
avatarResponse = response;
|
||||||
|
@ -154,11 +128,12 @@ export class AvatarManager {
|
||||||
const commandResult = error.commandResult;
|
const commandResult = error.commandResult;
|
||||||
if(commandResult instanceof CommandResult) {
|
if(commandResult instanceof CommandResult) {
|
||||||
if(commandResult.id === ErrorID.FILE_NOT_FOUND) {
|
if(commandResult.id === ErrorID.FILE_NOT_FOUND) {
|
||||||
if(avatar.currentAvatarHash !== initialAvatarHash)
|
if(avatar.getAvatarHash() !== initialAvatarHash) {
|
||||||
|
log.debug(LogCategory.GENERAL, tr("Ignoring avatar not found since the avatar itself got updated. Out version: %s, current version: %s"), initialAvatarHash, avatar.getAvatarHash());
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
avatar.destroyUrl();
|
avatar.setUnset();
|
||||||
avatar.setState("unset");
|
|
||||||
return;
|
return;
|
||||||
} else if(commandResult.id === ErrorID.PERMISSION_ERROR) {
|
} else if(commandResult.id === ErrorID.PERMISSION_ERROR) {
|
||||||
throw tr("No permissions to download the avatar");
|
throw tr("No permissions to download the avatar");
|
||||||
|
@ -192,11 +167,13 @@ export class AvatarManager {
|
||||||
const type = image_type(headers.get('X-media-bytes'));
|
const type = image_type(headers.get('X-media-bytes'));
|
||||||
const media = media_image_type(type);
|
const media = media_image_type(type);
|
||||||
|
|
||||||
if(avatar.currentAvatarHash !== initialAvatarHash)
|
if(avatar.getAvatarHash() !== initialAvatarHash) {
|
||||||
|
log.debug(LogCategory.GENERAL, tr("Ignoring avatar not found since the avatar itself got updated. Out version: %s, current version: %s"), initialAvatarHash, avatar.getAvatarHash());
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await AvatarManager.cache.put_cache('avatar_' + avatar.clientAvatarId, transferResponse.getResponse().clone(), "image/" + media, {
|
await AvatarManager.cache.put_cache('avatar_' + avatar.clientAvatarId, transferResponse.getResponse().clone(), "image/" + media, {
|
||||||
"X-avatar-version": avatar.currentAvatarHash
|
"X-avatar-version": avatar.getAvatarHash()
|
||||||
});
|
});
|
||||||
|
|
||||||
avatarResponse = transferResponse.getResponse();
|
avatarResponse = transferResponse.getResponse();
|
||||||
|
@ -217,51 +194,49 @@ export class AvatarManager {
|
||||||
const blob = await avatarResponse.blob();
|
const blob = await avatarResponse.blob();
|
||||||
|
|
||||||
/* ensure we're still up to date */
|
/* ensure we're still up to date */
|
||||||
if(avatar.currentAvatarHash !== initialAvatarHash)
|
if(avatar.getAvatarHash() !== initialAvatarHash) {
|
||||||
|
log.debug(LogCategory.GENERAL, tr("Ignoring avatar not found since the avatar itself got updated. Out version: %s, current version: %s"), initialAvatarHash, avatar.getAvatarHash());
|
||||||
return;
|
return;
|
||||||
|
|
||||||
avatar.destroyUrl();
|
|
||||||
if(blob.type !== "image/" + media) {
|
|
||||||
avatar.avatarUrl = URL.createObjectURL(blob.slice(0, blob.size, "image/" + media));
|
|
||||||
} else {
|
|
||||||
avatar.avatarUrl = URL.createObjectURL(blob);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
avatar.setState("loaded");
|
if(blob.type !== "image/" + media) {
|
||||||
|
avatar.setLoaded({ url: URL.createObjectURL(blob.slice(0, blob.size, "image/" + media)) });
|
||||||
|
} else {
|
||||||
|
avatar.setLoaded({ url: URL.createObjectURL(blob) });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private executeAvatarLoad(avatar: ClientAvatar) {
|
private executeAvatarLoad(avatar: LocalClientAvatar) {
|
||||||
const avatar_hash = avatar.currentAvatarHash;
|
const avatarHash = avatar.getAvatarHash();
|
||||||
|
|
||||||
avatar.setState("loading");
|
avatar.setLoading();
|
||||||
avatar.loadingTimestamp = Date.now();
|
avatar.loadingTimestamp = Date.now();
|
||||||
this.executeAvatarLoad0(avatar).catch(error => {
|
this.executeAvatarLoad0(avatar).catch(error => {
|
||||||
if(avatar.currentAvatarHash !== avatar_hash)
|
if(avatar.getAvatarHash() !== avatarHash) {
|
||||||
|
log.debug(LogCategory.GENERAL, tr("Ignoring avatar not found since the avatar itself got updated. Out version: %s, current version: %s"), avatarHash, avatar.getAvatarHash());
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if(typeof error === "string") {
|
|
||||||
avatar.loadError = error;
|
|
||||||
} else if(error instanceof Error) {
|
|
||||||
avatar.loadError = error.message;
|
|
||||||
} else {
|
|
||||||
log.error(LogCategory.FILE_TRANSFER, tr("Failed to load avatar %s (hash: %s): %o"), avatar.clientAvatarId, avatar_hash, error);
|
|
||||||
avatar.loadError = tr("lookup the console");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
avatar.destroyUrl(); /* if there were any image previously */
|
if(typeof error === "string") {
|
||||||
avatar.setState("errored");
|
avatar.setErrored({ message: error });
|
||||||
|
} else if(error instanceof Error) {
|
||||||
|
avatar.setErrored({ message: error.message });
|
||||||
|
} else {
|
||||||
|
log.error(LogCategory.FILE_TRANSFER, tr("Failed to load avatar %s (hash: %s): %o"), avatar.clientAvatarId, avatarHash, error);
|
||||||
|
avatar.setErrored({ message: tr("lookup the console") });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
update_cache(clientAvatarId: string, clientAvatarHash: string) {
|
updateCache(clientAvatarId: string, clientAvatarHash: string) {
|
||||||
AvatarManager.cache.setup().then(async () => {
|
AvatarManager.cache.setup().then(async () => {
|
||||||
const cached = this.cachedAvatars[clientAvatarId];
|
const cached = this.cachedAvatars[clientAvatarId];
|
||||||
if(cached) {
|
if(cached) {
|
||||||
if(cached.currentAvatarHash === clientAvatarHash)
|
if(cached.getAvatarHash() === clientAvatarHash)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
log.info(LogCategory.GENERAL, tr("Deleting cached avatar for client %s. Cached version: %s; New version: %s"), cached.currentAvatarHash, clientAvatarHash);
|
log.info(LogCategory.GENERAL, tr("Deleting cached avatar for client %s. Cached version: %s; New version: %s"), cached.getAvatarHash(), clientAvatarHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await AvatarManager.cache.resolve_cached('avatar_' + clientAvatarId);
|
const response = await AvatarManager.cache.resolve_cached('avatar_' + clientAvatarId);
|
||||||
|
@ -275,26 +250,25 @@ export class AvatarManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(cached) {
|
if(cached) {
|
||||||
cached.currentAvatarHash = clientAvatarHash;
|
cached.events.fire("avatar_changed", { newAvatarHash: clientAvatarHash });
|
||||||
cached.events.fire("avatar_changed");
|
|
||||||
this.executeAvatarLoad(cached);
|
this.executeAvatarLoad(cached);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveAvatar(clientAvatarId: string, avatarHash?: string, cacheOnly?: boolean) {
|
resolveAvatar(clientAvatarId: string, avatarHash?: string, cacheOnly?: boolean) : ClientAvatar {
|
||||||
let avatar = this.cachedAvatars[clientAvatarId];
|
let avatar = this.cachedAvatars[clientAvatarId];
|
||||||
if(!avatar) {
|
if(!avatar) {
|
||||||
if(cacheOnly)
|
if(cacheOnly)
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
||||||
avatar = new ClientAvatar(clientAvatarId);
|
avatar = new LocalClientAvatar(clientAvatarId);
|
||||||
this.cachedAvatars[clientAvatarId] = avatar;
|
this.cachedAvatars[clientAvatarId] = avatar;
|
||||||
} else if(typeof avatarHash !== "string" || avatar.currentAvatarHash === avatarHash) {
|
} else if(typeof avatarHash !== "string" || avatar.getAvatarHash() === avatarHash) {
|
||||||
return avatar;
|
return avatar;
|
||||||
}
|
}
|
||||||
|
|
||||||
avatar.currentAvatarHash = typeof avatarHash === "string" ? avatarHash : "unknown";
|
avatar.events.fire("avatar_changed", { newAvatarHash: typeof avatarHash === "string" ? avatarHash : "unknown" });
|
||||||
this.executeAvatarLoad(avatar);
|
this.executeAvatarLoad(avatar);
|
||||||
|
|
||||||
return avatar;
|
return avatar;
|
||||||
|
@ -317,7 +291,7 @@ export class AvatarManager {
|
||||||
return this.resolveAvatar(uniqueId2AvatarId(client.clientUniqueId), clientHandle?.properties.client_flag_avatar);
|
return this.resolveAvatar(uniqueId2AvatarId(client.clientUniqueId), clientHandle?.properties.client_flag_avatar);
|
||||||
}
|
}
|
||||||
|
|
||||||
private generate_default_image() : JQuery {
|
private static generate_default_image() : JQuery {
|
||||||
return $.spawn("img").attr("src", "img/style/avatar.png").css({width: '100%', height: '100%'});
|
return $.spawn("img").attr("src", "img/style/avatar.png").css({width: '100%', height: '100%'});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,7 +311,7 @@ export class AvatarManager {
|
||||||
|
|
||||||
const container = $.spawn("div").addClass("avatar");
|
const container = $.spawn("div").addClass("avatar");
|
||||||
if(client_handle && !client_handle.properties.client_flag_avatar)
|
if(client_handle && !client_handle.properties.client_flag_avatar)
|
||||||
return container.append(this.generate_default_image());
|
return container.append(AvatarManager.generate_default_image());
|
||||||
|
|
||||||
|
|
||||||
const clientAvatarId = client_handle ? client_handle.avatarId() : uniqueId2AvatarId(client_unique_id);
|
const clientAvatarId = client_handle ? client_handle.avatarId() : uniqueId2AvatarId(client_unique_id);
|
||||||
|
@ -346,11 +320,11 @@ export class AvatarManager {
|
||||||
|
|
||||||
|
|
||||||
const updateJQueryTag = () => {
|
const updateJQueryTag = () => {
|
||||||
const image = $.spawn("img").attr("src", avatar.avatarUrl).css({width: '100%', height: '100%'});
|
const image = $.spawn("img").attr("src", avatar.getAvatarUrl()).css({width: '100%', height: '100%'});
|
||||||
container.append(image);
|
container.append(image);
|
||||||
};
|
};
|
||||||
|
|
||||||
if(avatar.state !== "loading") {
|
if(avatar.getState() !== "loading") {
|
||||||
/* Test if we're may able to load the client avatar sync without a loading screen */
|
/* Test if we're may able to load the client avatar sync without a loading screen */
|
||||||
updateJQueryTag();
|
updateJQueryTag();
|
||||||
return container;
|
return container;
|
||||||
|
@ -362,7 +336,7 @@ export class AvatarManager {
|
||||||
avatar.awaitLoaded().then(updateJQueryTag);
|
avatar.awaitLoaded().then(updateJQueryTag);
|
||||||
image_loading.appendTo(container);
|
image_loading.appendTo(container);
|
||||||
} else {
|
} else {
|
||||||
this.generate_default_image().appendTo(container);
|
AvatarManager.generate_default_image().appendTo(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
|
@ -378,34 +352,102 @@ export class AvatarManager {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export function uniqueId2AvatarId(unique_id: string) {
|
/* FIXME: unsubscribe if the other client isn't alive any mnore */
|
||||||
function str2ab(str) {
|
class LocalAvatarManagerFactory extends AbstractAvatarManagerFactory {
|
||||||
let buf = new ArrayBuffer(str.length); // 2 bytes for each char
|
private ipcChannel: IPCChannel;
|
||||||
let bufView = new Uint8Array(buf);
|
|
||||||
for (let i=0, strLen = str.length; i<strLen; i++) {
|
private subscribedAvatars: {[key: string]: { avatar: ClientAvatar, remoteAvatarId: string, unregisterCallback: () => void }[]} = {};
|
||||||
bufView[i] = str.charCodeAt(i);
|
|
||||||
}
|
constructor() {
|
||||||
return buf;
|
super();
|
||||||
|
|
||||||
|
this.ipcChannel = ipc.getInstance().createChannel(undefined, kIPCAvatarChannel);
|
||||||
|
this.ipcChannel.messageHandler = this.handleIpcMessage.bind(this);
|
||||||
|
|
||||||
|
server_connections.events().on("notify_handler_created", event => this.handleHandlerCreated(event.handler));
|
||||||
|
server_connections.events().on("notify_handler_deleted", event => this.handleHandlerDestroyed(event.handler));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
getManager(handlerId: string): AbstractAvatarManager {
|
||||||
let raw = atob(unique_id);
|
return server_connections.findConnection(handlerId)?.fileManager.avatars;
|
||||||
let input = hex.encode(str2ab(raw));
|
}
|
||||||
|
|
||||||
let result: string = "";
|
hasManager(handlerId: string): boolean {
|
||||||
for(let index = 0; index < input.length; index++) {
|
return this.getManager(handlerId) !== undefined;
|
||||||
let c = input.charAt(index);
|
}
|
||||||
let offset: number = 0;
|
|
||||||
if(c >= '0' && c <= '9')
|
private handleHandlerCreated(handler: ConnectionHandler) {
|
||||||
offset = c.charCodeAt(0) - '0'.charCodeAt(0);
|
this.ipcChannel.sendMessage("notify-handler-created", { handler: handler.handlerId });
|
||||||
else if(c >= 'A' && c <= 'F')
|
}
|
||||||
offset = c.charCodeAt(0) - 'A'.charCodeAt(0) + 0x0A;
|
|
||||||
else if(c >= 'a' && c <= 'f')
|
private handleHandlerDestroyed(handler: ConnectionHandler) {
|
||||||
offset = c.charCodeAt(0) - 'a'.charCodeAt(0) + 0x0A;
|
this.ipcChannel.sendMessage("notify-handler-destroyed", { handler: handler.handlerId });
|
||||||
result += String.fromCharCode('a'.charCodeAt(0) + offset);
|
const subscriptions = this.subscribedAvatars[handler.handlerId] || [];
|
||||||
|
delete this.subscribedAvatars[handler.handlerId];
|
||||||
|
|
||||||
|
subscriptions.forEach(e => e.unregisterCallback());
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleIpcMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) {
|
||||||
|
if(broadcast)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(message.type === "query-handlers") {
|
||||||
|
this.ipcChannel.sendMessage("notify-handlers", {
|
||||||
|
handlers: server_connections.all_connections().map(e => e.handlerId)
|
||||||
|
}, remoteId);
|
||||||
|
return;
|
||||||
|
} else if(message.type === "load-avatar") {
|
||||||
|
const sendResponse = properties => {
|
||||||
|
this.ipcChannel.sendMessage("load-avatar-result", {
|
||||||
|
avatarId: message.data.avatarId,
|
||||||
|
handlerId: message.data.handlerId,
|
||||||
|
...properties
|
||||||
|
}, remoteId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const avatarId = message.data.avatarId;
|
||||||
|
const handlerId = message.data.handlerId;
|
||||||
|
const manager = this.getManager(handlerId);
|
||||||
|
if(!manager) {
|
||||||
|
sendResponse({ success: false, message: tr("Invalid handler") });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatar: ClientAvatar;
|
||||||
|
if(message.data.keyType === "client") {
|
||||||
|
avatar = manager.resolveClientAvatar({
|
||||||
|
id: message.data.clientId,
|
||||||
|
clientUniqueId: message.data.clientUniqueId,
|
||||||
|
database_id: message.data.clientDatabaseId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
avatar = manager.resolveAvatar(message.data.clientAvatarId, message.data.avatarVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscribedAvatars = this.subscribedAvatars[handlerId] || (this.subscribedAvatars[handlerId] = []);
|
||||||
|
const oldSubscribedAvatarIndex = subscribedAvatars.findIndex(e => e.remoteAvatarId === avatarId);
|
||||||
|
if(oldSubscribedAvatarIndex !== -1) {
|
||||||
|
const [ subscription ] = subscribedAvatars.splice(oldSubscribedAvatarIndex, 1);
|
||||||
|
subscription.unregisterCallback();
|
||||||
|
}
|
||||||
|
subscribedAvatars.push({
|
||||||
|
avatar: avatar,
|
||||||
|
remoteAvatarId: avatarId,
|
||||||
|
unregisterCallback: avatar.events.onAll(event => {
|
||||||
|
this.ipcChannel.sendMessage("avatar-event", { handlerId: handlerId, avatarId: avatarId, event: event }, remoteId);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
sendResponse({ success: true, state: avatar.getState(), stateData: avatar.getStateData(), hash: avatar.getAvatarHash() });
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
} catch (e) { //invalid base 64 (like music bot etc)
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loader.register_task(Stage.LOADED, {
|
||||||
|
name: "Avatar init",
|
||||||
|
function: async () => {
|
||||||
|
setGlobalAvatarManagerFactory(new LocalAvatarManagerFactory());
|
||||||
|
},
|
||||||
|
priority: 5
|
||||||
|
});
|
|
@ -0,0 +1,227 @@
|
||||||
|
import * as ipc from "../ipc/BrowserIPC";
|
||||||
|
import * as loader from "tc-loader";
|
||||||
|
import {Stage} from "tc-loader";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AbstractAvatarManager,
|
||||||
|
AbstractAvatarManagerFactory, AvatarState, AvatarStateData, ClientAvatar,
|
||||||
|
kIPCAvatarChannel,
|
||||||
|
setGlobalAvatarManagerFactory, uniqueId2AvatarId
|
||||||
|
} from "tc-shared/file/Avatars";
|
||||||
|
import {IPCChannel} from "tc-shared/ipc/BrowserIPC";
|
||||||
|
import {Settings} from "tc-shared/settings";
|
||||||
|
import {ChannelMessage} from "../ipc/BrowserIPC";
|
||||||
|
import {guid} from "tc-shared/crypto/uid";
|
||||||
|
|
||||||
|
function isEquivalent(a, b) {
|
||||||
|
// Create arrays of property names
|
||||||
|
const aProps = Object.getOwnPropertyNames(a);
|
||||||
|
const bProps = Object.getOwnPropertyNames(b);
|
||||||
|
|
||||||
|
// If number of properties is different,
|
||||||
|
// objects are not equivalent
|
||||||
|
if (aProps.length != bProps.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < aProps.length; i++) {
|
||||||
|
const propName = aProps[i];
|
||||||
|
|
||||||
|
// If values of same property are not equal,
|
||||||
|
// objects are not equivalent
|
||||||
|
if (a[propName] !== b[propName]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we made it this far, objects
|
||||||
|
// are considered equivalent
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemoteAvatar extends ClientAvatar {
|
||||||
|
readonly avatarId: string;
|
||||||
|
readonly type: "avatar" | "client-avatar";
|
||||||
|
|
||||||
|
constructor(clientAvatarId: string, type: "avatar" | "client-avatar") {
|
||||||
|
super(clientAvatarId);
|
||||||
|
|
||||||
|
this.avatarId = guid();
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected destroyStateData(state: AvatarState, data: AvatarStateData[AvatarState]) {}
|
||||||
|
|
||||||
|
public updateStateFromRemote(state: AvatarState, data: AvatarStateData[AvatarState]) {
|
||||||
|
if(this.getState() === state && isEquivalent(this.getStateData(), data))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.setState(state, data, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemoteAvatarManager extends AbstractAvatarManager {
|
||||||
|
readonly handlerId: string;
|
||||||
|
readonly ipcChannel: IPCChannel;
|
||||||
|
private knownAvatars: RemoteAvatar[] = [];
|
||||||
|
|
||||||
|
constructor(handlerId: string, ipcChannel: IPCChannel) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.ipcChannel = ipcChannel;
|
||||||
|
this.handlerId = handlerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.knownAvatars.forEach(e => e.destroy());
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveAvatar(clientAvatarId: string, avatarHash?: string): ClientAvatar {
|
||||||
|
const sendRequest = (avatar: RemoteAvatar) => this.ipcChannel.sendMessage("load-avatar", {
|
||||||
|
avatarId: avatar.avatarId,
|
||||||
|
handlerId: this.handlerId,
|
||||||
|
|
||||||
|
keyType: "avatar",
|
||||||
|
clientAvatarId: avatar.clientAvatarId,
|
||||||
|
avatarVersion: avatarHash
|
||||||
|
});
|
||||||
|
|
||||||
|
const cachedAvatar = this.knownAvatars.find(e => e.type === "avatar" && e.avatarId === clientAvatarId);
|
||||||
|
if(cachedAvatar) {
|
||||||
|
if(cachedAvatar.getAvatarHash() !== avatarHash)
|
||||||
|
sendRequest(cachedAvatar); /* update */
|
||||||
|
return cachedAvatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatar = new RemoteAvatar(clientAvatarId, "avatar");
|
||||||
|
avatar.setLoading();
|
||||||
|
this.knownAvatars.push(avatar);
|
||||||
|
sendRequest(avatar);
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveClientAvatar(client: { id?: number; database_id?: number; clientUniqueId: string }) {
|
||||||
|
const sendRequest = (avatar: RemoteAvatar) => this.ipcChannel.sendMessage("load-avatar", {
|
||||||
|
avatarId: avatar.avatarId,
|
||||||
|
handlerId: this.handlerId,
|
||||||
|
|
||||||
|
keyType: "client",
|
||||||
|
clientId: client.id,
|
||||||
|
clientUniqueId: client.clientUniqueId,
|
||||||
|
clientDatabaseId: client.database_id
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientAvatarId = uniqueId2AvatarId(client.clientUniqueId);
|
||||||
|
const cachedAvatar = this.knownAvatars.find(e => e.type === "client-avatar" && e.clientAvatarId === clientAvatarId);
|
||||||
|
if(cachedAvatar) {
|
||||||
|
//sendRequest(cachedAvatar); /* just update in case */
|
||||||
|
return cachedAvatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatar = new RemoteAvatar(clientAvatarId, "client-avatar");
|
||||||
|
avatar.setLoading();
|
||||||
|
this.knownAvatars.push(avatar);
|
||||||
|
sendRequest(avatar);
|
||||||
|
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAvatarLoadCallback(data: any) {
|
||||||
|
const avatar = this.knownAvatars.find(e => e.avatarId === data.avatarId);
|
||||||
|
if(!avatar) return;
|
||||||
|
|
||||||
|
if(!(data.success === true)) {
|
||||||
|
avatar.setErrored({ message: data.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(avatar.getAvatarHash() !== data.hash)
|
||||||
|
avatar.events.fire("avatar_changed", { newAvatarHash: data.hash });
|
||||||
|
|
||||||
|
avatar.updateStateFromRemote(data.state, data.stateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAvatarEvent(data: any) {
|
||||||
|
const avatar = this.knownAvatars.find(e => e.avatarId === data.avatarId);
|
||||||
|
if(!avatar) return;
|
||||||
|
|
||||||
|
avatar.events.fire(data.event.type, data.event, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemoteAvatarManagerFactory extends AbstractAvatarManagerFactory {
|
||||||
|
private readonly ipcChannel: IPCChannel;
|
||||||
|
private manager: {[key: string]: RemoteAvatarManager} = {};
|
||||||
|
|
||||||
|
private callbackHandlerQueried: () => void;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.ipcChannel = ipc.getInstance().createChannel(Settings.instance.static(Settings.KEY_IPC_REMOTE_ADDRESS, "invalid"), kIPCAvatarChannel);
|
||||||
|
this.ipcChannel.messageHandler = this.handleIpcMessage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
this.ipcChannel.sendMessage("query-handlers", {});
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
this.callbackHandlerQueried = undefined;
|
||||||
|
reject(tr("handler query timeout"));
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
this.callbackHandlerQueried = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getManager(handlerId: string): AbstractAvatarManager {
|
||||||
|
return this.manager[handlerId];
|
||||||
|
}
|
||||||
|
|
||||||
|
hasManager(handlerId: string): boolean {
|
||||||
|
return typeof this.manager[handlerId] !== "undefined";
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleIpcMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) {
|
||||||
|
if(broadcast) {
|
||||||
|
if(message.type === "notify-handler-destroyed") {
|
||||||
|
const manager = this.manager[message.data.handler];
|
||||||
|
delete this.manager[message.data.handler];
|
||||||
|
manager?.destroy();
|
||||||
|
} else if(message.type === "notify-handler-created") {
|
||||||
|
this.manager[message.data.handler] = new RemoteAvatarManager(message.data.handler, this.ipcChannel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(message.type === "notify-handlers") {
|
||||||
|
Object.values(this.manager).forEach(e => e.destroy());
|
||||||
|
this.manager = {};
|
||||||
|
|
||||||
|
for(const handlerId of message.data.handlers)
|
||||||
|
this.manager[handlerId] = new RemoteAvatarManager(handlerId, this.ipcChannel);
|
||||||
|
|
||||||
|
if(this.callbackHandlerQueried)
|
||||||
|
this.callbackHandlerQueried();
|
||||||
|
} else if(message.type === "load-avatar-result") {
|
||||||
|
const manager = this.manager[message.data.handlerId];
|
||||||
|
manager?.handleAvatarLoadCallback(message.data);
|
||||||
|
} else if(message.type === "avatar-event") {
|
||||||
|
const manager = this.manager[message.data.handlerId];
|
||||||
|
manager?.handleAvatarEvent(message.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
|
priority: 10,
|
||||||
|
name: "IPC avatar init",
|
||||||
|
function: async () => {
|
||||||
|
let factory = new RemoteAvatarManagerFactory();
|
||||||
|
await factory.initialize();
|
||||||
|
setGlobalAvatarManagerFactory(factory);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,287 @@
|
||||||
|
import * as log from "tc-shared/log";
|
||||||
|
import {LogCategory} from "tc-shared/log";
|
||||||
|
import {ConnectHandler} from "tc-shared/ipc/ConnectHandler";
|
||||||
|
|
||||||
|
export interface BroadcastMessage {
|
||||||
|
timestamp: number;
|
||||||
|
receiver: string;
|
||||||
|
sender: string;
|
||||||
|
|
||||||
|
type: string;
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function uuidv4() {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
|
const r = Math.random() * 16 | 0;
|
||||||
|
const v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProcessQuery {
|
||||||
|
timestamp: number
|
||||||
|
query_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelMessage {
|
||||||
|
channel_id: string;
|
||||||
|
type: string;
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessQueryResponse {
|
||||||
|
request_timestamp: number
|
||||||
|
request_query_id: string;
|
||||||
|
|
||||||
|
device_id: string;
|
||||||
|
protocol: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CertificateAcceptCallback {
|
||||||
|
request_id: string;
|
||||||
|
}
|
||||||
|
export interface CertificateAcceptSucceeded { }
|
||||||
|
|
||||||
|
export abstract class BasicIPCHandler {
|
||||||
|
protected static readonly BROADCAST_UNIQUE_ID = "00000000-0000-4000-0000-000000000000";
|
||||||
|
protected static readonly PROTOCOL_VERSION = 1;
|
||||||
|
|
||||||
|
protected _channels: IPCChannel[] = [];
|
||||||
|
protected unique_id;
|
||||||
|
|
||||||
|
protected constructor() { }
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.unique_id = uuidv4(); /* lets get an unique identifier */
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocalAddress() { return this.unique_id; }
|
||||||
|
|
||||||
|
abstract sendMessage(type: string, data: any, target?: string);
|
||||||
|
|
||||||
|
protected handleMessage(message: BroadcastMessage) {
|
||||||
|
//log.trace(LogCategory.IPC, tr("Received message %o"), message);
|
||||||
|
|
||||||
|
if(message.receiver === BasicIPCHandler.BROADCAST_UNIQUE_ID) {
|
||||||
|
if(message.type == "process-query") {
|
||||||
|
log.debug(LogCategory.IPC, tr("Received a device query from %s."), message.sender);
|
||||||
|
this.sendMessage("process-query-response", {
|
||||||
|
request_query_id: (<ProcessQuery>message.data).query_id,
|
||||||
|
request_timestamp: (<ProcessQuery>message.data).timestamp,
|
||||||
|
|
||||||
|
device_id: this.unique_id,
|
||||||
|
protocol: BasicIPCHandler.PROTOCOL_VERSION
|
||||||
|
} as ProcessQueryResponse, message.sender);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if(message.receiver === this.unique_id) {
|
||||||
|
if(message.type == "process-query-response") {
|
||||||
|
const response: ProcessQueryResponse = message.data;
|
||||||
|
if(this._query_results[response.request_query_id])
|
||||||
|
this._query_results[response.request_query_id].push(response);
|
||||||
|
else {
|
||||||
|
log.warn(LogCategory.IPC, tr("Received a query response for an unknown request."));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if(message.type == "certificate-accept-callback") {
|
||||||
|
const data: CertificateAcceptCallback = message.data;
|
||||||
|
if(!this._cert_accept_callbacks[data.request_id]) {
|
||||||
|
log.warn(LogCategory.IPC, tr("Received certificate accept callback for an unknown request ID."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._cert_accept_callbacks[data.request_id]();
|
||||||
|
delete this._cert_accept_callbacks[data.request_id];
|
||||||
|
|
||||||
|
this.sendMessage("certificate-accept-succeeded", {
|
||||||
|
|
||||||
|
} as CertificateAcceptSucceeded, message.sender);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if(message.type == "certificate-accept-succeeded") {
|
||||||
|
if(!this._cert_accept_succeeded[message.sender]) {
|
||||||
|
log.warn(LogCategory.IPC, tr("Received certificate accept succeeded, but haven't a callback."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._cert_accept_succeeded[message.sender]();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(message.type === "channel") {
|
||||||
|
const data: ChannelMessage = message.data;
|
||||||
|
|
||||||
|
let channel_invoked = false;
|
||||||
|
for(const channel of this._channels)
|
||||||
|
if(channel.channelId === data.channel_id && (typeof(channel.targetClientId) === "undefined" || channel.targetClientId === message.sender)) {
|
||||||
|
if(channel.messageHandler)
|
||||||
|
channel.messageHandler(message.sender, message.receiver === BasicIPCHandler.BROADCAST_UNIQUE_ID, data);
|
||||||
|
channel_invoked = true;
|
||||||
|
}
|
||||||
|
if(!channel_invoked) {
|
||||||
|
debugger;
|
||||||
|
console.warn(tr("Received channel message for unknown channel (%s)"), data.channel_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createChannel(targetId?: string, channelId?: string) : IPCChannel {
|
||||||
|
let channel: IPCChannel = {
|
||||||
|
targetClientId: targetId,
|
||||||
|
channelId: channelId || uuidv4(),
|
||||||
|
messageHandler: undefined,
|
||||||
|
sendMessage: (type: string, data: any, target?: string) => {
|
||||||
|
if(typeof target !== "undefined") {
|
||||||
|
if(typeof channel.targetClientId === "string" && target != channel.targetClientId)
|
||||||
|
throw "target id does not match channel target";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendMessage("channel", {
|
||||||
|
type: type,
|
||||||
|
data: data,
|
||||||
|
channel_id: channel.channelId
|
||||||
|
} as ChannelMessage, target || channel.targetClientId || BasicIPCHandler.BROADCAST_UNIQUE_ID);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this._channels.push(channel);
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
channels() : IPCChannel[] { return this._channels; }
|
||||||
|
|
||||||
|
deleteChannel(channel: IPCChannel) {
|
||||||
|
this._channels = this._channels.filter(e => e !== channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _query_results: {[key: string]:ProcessQueryResponse[]} = {};
|
||||||
|
async queryProcesses(timeout?: number) : Promise<ProcessQueryResponse[]> {
|
||||||
|
const query_id = uuidv4();
|
||||||
|
this._query_results[query_id] = [];
|
||||||
|
|
||||||
|
this.sendMessage("process-query", {
|
||||||
|
query_id: query_id,
|
||||||
|
timestamp: Date.now()
|
||||||
|
} as ProcessQuery);
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, timeout || 250));
|
||||||
|
const result = this._query_results[query_id];
|
||||||
|
delete this._query_results[query_id];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _cert_accept_callbacks: {[key: string]:(() => any)} = {};
|
||||||
|
register_certificate_accept_callback(callback: () => any) : string {
|
||||||
|
const id = uuidv4();
|
||||||
|
this._cert_accept_callbacks[id] = callback;
|
||||||
|
return this.unique_id + ":" + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _cert_accept_succeeded: {[sender: string]:(() => any)} = {};
|
||||||
|
post_certificate_accpected(id: string, timeout?: number) : Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const data = id.split(":");
|
||||||
|
const timeout_id = setTimeout(() => {
|
||||||
|
delete this._cert_accept_succeeded[data[0]];
|
||||||
|
clearTimeout(timeout_id);
|
||||||
|
reject("timeout");
|
||||||
|
}, timeout || 250);
|
||||||
|
this._cert_accept_succeeded[data[0]] = () => {
|
||||||
|
delete this._cert_accept_succeeded[data[0]];
|
||||||
|
clearTimeout(timeout_id);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
this.sendMessage("certificate-accept-callback", {
|
||||||
|
request_id: data[1]
|
||||||
|
} as CertificateAcceptCallback, data[0]);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPCChannel {
|
||||||
|
readonly channelId: string;
|
||||||
|
targetClientId?: string;
|
||||||
|
|
||||||
|
messageHandler: (remoteId: string, broadcast: boolean, message: ChannelMessage) => void;
|
||||||
|
sendMessage(type: string, message: any, target?: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
class BroadcastChannelIPC extends BasicIPCHandler {
|
||||||
|
private static readonly CHANNEL_NAME = "TeaSpeak-Web";
|
||||||
|
|
||||||
|
private channel: BroadcastChannel;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
super.setup();
|
||||||
|
|
||||||
|
this.channel = new BroadcastChannel(BroadcastChannelIPC.CHANNEL_NAME);
|
||||||
|
this.channel.onmessage = this.onMessage.bind(this);
|
||||||
|
this.channel.onmessageerror = this.onError.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onMessage(event: MessageEvent) {
|
||||||
|
if(typeof(event.data) !== "string") {
|
||||||
|
log.warn(LogCategory.IPC, tr("Received message with an invalid type (%s): %o"), typeof(event.data), event.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message: BroadcastMessage;
|
||||||
|
try {
|
||||||
|
message = JSON.parse(event.data);
|
||||||
|
} catch(error) {
|
||||||
|
log.error(LogCategory.IPC, tr("Received an invalid encoded message: %o"), event.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
super.handleMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onError(event: MessageEvent) {
|
||||||
|
log.warn(LogCategory.IPC, tr("Received error: %o"), event);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(type: string, data: any, target?: string) {
|
||||||
|
const message: BroadcastMessage = {} as any;
|
||||||
|
|
||||||
|
message.sender = this.unique_id;
|
||||||
|
message.receiver = target ? target : BasicIPCHandler.BROADCAST_UNIQUE_ID;
|
||||||
|
message.timestamp = Date.now();
|
||||||
|
message.type = type;
|
||||||
|
message.data = data;
|
||||||
|
|
||||||
|
this.channel.postMessage(JSON.stringify(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let handler: BasicIPCHandler;
|
||||||
|
let connect_handler: ConnectHandler;
|
||||||
|
|
||||||
|
export function setup() {
|
||||||
|
if(!supported())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(handler)
|
||||||
|
throw "bipc already started";
|
||||||
|
|
||||||
|
handler = new BroadcastChannelIPC();
|
||||||
|
handler.setup();
|
||||||
|
|
||||||
|
connect_handler = new ConnectHandler(handler);
|
||||||
|
connect_handler.setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInstance() {
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInstanceConnectHandler() {
|
||||||
|
return connect_handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function supported() {
|
||||||
|
/* ios does not support this */
|
||||||
|
return typeof(window.BroadcastChannel) !== "undefined";
|
||||||
|
}
|
|
@ -0,0 +1,230 @@
|
||||||
|
import * as log from "tc-shared/log";
|
||||||
|
import {LogCategory} from "tc-shared/log";
|
||||||
|
import {BasicIPCHandler, IPCChannel, ChannelMessage} from "tc-shared/ipc/BrowserIPC";
|
||||||
|
import {guid} from "tc-shared/crypto/uid";
|
||||||
|
|
||||||
|
export type ConnectRequestData = {
|
||||||
|
address: string;
|
||||||
|
|
||||||
|
profile?: string;
|
||||||
|
username?: string;
|
||||||
|
password?: {
|
||||||
|
value: string;
|
||||||
|
hashed: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConnectOffer {
|
||||||
|
request_id: string;
|
||||||
|
data: ConnectRequestData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConnectOfferAnswer {
|
||||||
|
request_id: string;
|
||||||
|
accepted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConnectExecute {
|
||||||
|
request_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConnectExecuted {
|
||||||
|
request_id: string;
|
||||||
|
succeeded: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The connect process:
|
||||||
|
* 1. Broadcast an offer
|
||||||
|
* 2. Wait 50ms for all offer responses or until the first one respond with "ok"
|
||||||
|
* 3. Select (if possible) on accepted offer and execute the connect
|
||||||
|
*/
|
||||||
|
export class ConnectHandler {
|
||||||
|
private static readonly CHANNEL_NAME = "connect";
|
||||||
|
|
||||||
|
readonly ipc_handler: BasicIPCHandler;
|
||||||
|
private ipc_channel: IPCChannel;
|
||||||
|
|
||||||
|
public callback_available: (data: ConnectRequestData) => boolean = () => false;
|
||||||
|
public callback_execute: (data: ConnectRequestData) => boolean | string = () => false;
|
||||||
|
|
||||||
|
|
||||||
|
private _pending_connect_offers: {
|
||||||
|
id: string;
|
||||||
|
data: ConnectRequestData;
|
||||||
|
timeout: number;
|
||||||
|
|
||||||
|
remote_handler: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
private _pending_connects_requests: {
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
data: ConnectRequestData;
|
||||||
|
timeout: number;
|
||||||
|
|
||||||
|
callback_success: () => any;
|
||||||
|
callback_failed: (message: string) => any;
|
||||||
|
callback_avail: () => Promise<boolean>;
|
||||||
|
|
||||||
|
remote_handler?: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
constructor(ipc_handler: BasicIPCHandler) {
|
||||||
|
this.ipc_handler = ipc_handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setup() {
|
||||||
|
this.ipc_channel = this.ipc_handler.createChannel(undefined, ConnectHandler.CHANNEL_NAME);
|
||||||
|
this.ipc_channel.messageHandler = this.onMessage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onMessage(sender: string, broadcast: boolean, message: ChannelMessage) {
|
||||||
|
if(broadcast) {
|
||||||
|
if(message.type == "offer") {
|
||||||
|
const data = message.data as ConnectOffer;
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
accepted: this.callback_available(data.data),
|
||||||
|
request_id: data.request_id
|
||||||
|
} as ConnectOfferAnswer;
|
||||||
|
|
||||||
|
if(response.accepted) {
|
||||||
|
log.debug(LogCategory.IPC, tr("Received new connect offer from %s: %s"), sender, data.request_id);
|
||||||
|
|
||||||
|
const ld = {
|
||||||
|
remote_handler: sender,
|
||||||
|
data: data.data,
|
||||||
|
id: data.request_id,
|
||||||
|
timeout: 0
|
||||||
|
};
|
||||||
|
this._pending_connect_offers.push(ld);
|
||||||
|
ld.timeout = setTimeout(() => {
|
||||||
|
log.debug(LogCategory.IPC, tr("Dropping connect request %s, because we never received an execute."), ld.id);
|
||||||
|
this._pending_connect_offers.remove(ld);
|
||||||
|
}, 120 * 1000) as any;
|
||||||
|
}
|
||||||
|
this.ipc_channel.sendMessage("offer-answer", response, sender);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(message.type == "offer-answer") {
|
||||||
|
const data = message.data as ConnectOfferAnswer;
|
||||||
|
const request = this._pending_connects_requests.find(e => e.id === data.request_id);
|
||||||
|
if(!request) {
|
||||||
|
log.warn(LogCategory.IPC, tr("Received connect offer answer with unknown request id (%s)."), data.request_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!data.accepted) {
|
||||||
|
log.debug(LogCategory.IPC, tr("Client %s rejected the connect offer (%s)."), sender, request.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(request.remote_handler) {
|
||||||
|
log.debug(LogCategory.IPC, tr("Client %s accepted the connect offer (%s), but offer has already been accepted."), sender, request.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(LogCategory.IPC, tr("Client %s accepted the connect offer (%s). Request local acceptance."), sender, request.id);
|
||||||
|
request.remote_handler = sender;
|
||||||
|
clearTimeout(request.timeout);
|
||||||
|
|
||||||
|
request.callback_avail().then(flag => {
|
||||||
|
if(!flag) {
|
||||||
|
request.callback_failed("local avail rejected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(LogCategory.IPC, tr("Executing connect with client %s"), request.remote_handler);
|
||||||
|
this.ipc_channel.sendMessage("execute", {
|
||||||
|
request_id: request.id
|
||||||
|
} as ConnectExecute, request.remote_handler);
|
||||||
|
request.timeout = setTimeout(() => {
|
||||||
|
request.callback_failed("connect execute timeout");
|
||||||
|
}, 1000) as any;
|
||||||
|
}).catch(error => {
|
||||||
|
log.error(LogCategory.IPC, tr("Local avail callback caused an error: %o"), error);
|
||||||
|
request.callback_failed(tr("local avail callback caused an error"));
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
else if(message.type == "executed") {
|
||||||
|
const data = message.data as ConnectExecuted;
|
||||||
|
const request = this._pending_connects_requests.find(e => e.id === data.request_id);
|
||||||
|
if(!request) {
|
||||||
|
log.warn(LogCategory.IPC, tr("Received connect executed with unknown request id (%s)."), data.request_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(request.remote_handler != sender) {
|
||||||
|
log.warn(LogCategory.IPC, tr("Received connect executed for request %s, but from wrong client: %s (expected %s)"), data.request_id, sender, request.remote_handler);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(LogCategory.IPC, tr("Received connect executed response from client %s for request %s. Succeeded: %o (%s)"), sender, data.request_id, data.succeeded, data.message);
|
||||||
|
clearTimeout(request.timeout);
|
||||||
|
if(data.succeeded)
|
||||||
|
request.callback_success();
|
||||||
|
else
|
||||||
|
request.callback_failed(data.message);
|
||||||
|
}
|
||||||
|
else if(message.type == "execute") {
|
||||||
|
const data = message.data as ConnectExecute;
|
||||||
|
const request = this._pending_connect_offers.find(e => e.id === data.request_id);
|
||||||
|
if(!request) {
|
||||||
|
log.warn(LogCategory.IPC, tr("Received connect execute with unknown request id (%s)."), data.request_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(request.remote_handler != sender) {
|
||||||
|
log.warn(LogCategory.IPC, tr("Received connect execute for request %s, but from wrong client: %s (expected %s)"), data.request_id, sender, request.remote_handler);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearTimeout(request.timeout);
|
||||||
|
this._pending_connect_offers.remove(request);
|
||||||
|
|
||||||
|
log.debug(LogCategory.IPC, tr("Executing connect for %s"), data.request_id);
|
||||||
|
const cr = this.callback_execute(request.data);
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
request_id: data.request_id,
|
||||||
|
|
||||||
|
succeeded: typeof(cr) !== "string" && cr,
|
||||||
|
message: typeof(cr) === "string" ? cr : "",
|
||||||
|
} as ConnectExecuted;
|
||||||
|
this.ipc_channel.sendMessage("executed", response, request.remote_handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post_connect_request(data: ConnectRequestData, callback_avail: () => Promise<boolean>) : Promise<void> {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const pd = {
|
||||||
|
data: data,
|
||||||
|
id: guid(),
|
||||||
|
timeout: 0,
|
||||||
|
|
||||||
|
callback_success: () => {
|
||||||
|
this._pending_connects_requests.remove(pd);
|
||||||
|
clearTimeout(pd.timeout);
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
callback_failed: error => {
|
||||||
|
this._pending_connects_requests.remove(pd);
|
||||||
|
clearTimeout(pd.timeout);
|
||||||
|
reject(error);
|
||||||
|
},
|
||||||
|
|
||||||
|
callback_avail: callback_avail,
|
||||||
|
};
|
||||||
|
this._pending_connects_requests.push(pd);
|
||||||
|
|
||||||
|
this.ipc_channel.sendMessage("offer", {
|
||||||
|
request_id: pd.id,
|
||||||
|
data: pd.data
|
||||||
|
} as ConnectOffer);
|
||||||
|
pd.timeout = setTimeout(() => {
|
||||||
|
pd.callback_failed("received no response to offer");
|
||||||
|
}, 50) as any;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,216 @@
|
||||||
|
import * as log from "tc-shared/log";
|
||||||
|
import {LogCategory} from "tc-shared/log";
|
||||||
|
import {BasicIPCHandler, IPCChannel, ChannelMessage} from "tc-shared/ipc/BrowserIPC";
|
||||||
|
|
||||||
|
export interface MethodProxyInvokeData {
|
||||||
|
method_name: string;
|
||||||
|
arguments: any[];
|
||||||
|
promise_id: string;
|
||||||
|
}
|
||||||
|
export interface MethodProxyResultData {
|
||||||
|
promise_id: string;
|
||||||
|
result: any;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
export interface MethodProxyCallback {
|
||||||
|
promise: Promise<any>;
|
||||||
|
promise_id: string;
|
||||||
|
|
||||||
|
resolve: (object: any) => any;
|
||||||
|
reject: (object: any) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MethodProxyConnectParameters = {
|
||||||
|
channel_id: string;
|
||||||
|
client_id: string;
|
||||||
|
}
|
||||||
|
export abstract class MethodProxy {
|
||||||
|
readonly ipc_handler: BasicIPCHandler;
|
||||||
|
private _ipc_channel: IPCChannel;
|
||||||
|
private _ipc_parameters: MethodProxyConnectParameters;
|
||||||
|
|
||||||
|
private readonly _local: boolean;
|
||||||
|
private readonly _slave: boolean;
|
||||||
|
|
||||||
|
private _connected: boolean;
|
||||||
|
private _proxied_methods: {[key: string]:() => Promise<any>} = {};
|
||||||
|
private _proxied_callbacks: {[key: string]:MethodProxyCallback} = {};
|
||||||
|
|
||||||
|
protected constructor(ipc_handler: BasicIPCHandler, connect_params?: MethodProxyConnectParameters) {
|
||||||
|
this.ipc_handler = ipc_handler;
|
||||||
|
this._ipc_parameters = connect_params;
|
||||||
|
this._connected = false;
|
||||||
|
this._slave = typeof(connect_params) !== "undefined";
|
||||||
|
this._local = typeof(connect_params) !== "undefined" && connect_params.channel_id === "local" && connect_params.client_id === "local";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setup() {
|
||||||
|
if(this._local) {
|
||||||
|
this._connected = true;
|
||||||
|
this.on_connected();
|
||||||
|
} else {
|
||||||
|
if(this._slave)
|
||||||
|
this._ipc_channel = this.ipc_handler.createChannel(this._ipc_parameters.client_id, this._ipc_parameters.channel_id);
|
||||||
|
else
|
||||||
|
this._ipc_channel = this.ipc_handler.createChannel();
|
||||||
|
|
||||||
|
this._ipc_channel.messageHandler = this._handle_message.bind(this);
|
||||||
|
if(this._slave)
|
||||||
|
this._ipc_channel.sendMessage("initialize", {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected finalize() {
|
||||||
|
if(!this._local) {
|
||||||
|
if(this._connected)
|
||||||
|
this._ipc_channel.sendMessage("finalize", {});
|
||||||
|
|
||||||
|
this.ipc_handler.deleteChannel(this._ipc_channel);
|
||||||
|
this._ipc_channel = undefined;
|
||||||
|
}
|
||||||
|
for(const promise of Object.values(this._proxied_callbacks))
|
||||||
|
promise.reject("disconnected");
|
||||||
|
this._proxied_callbacks = {};
|
||||||
|
|
||||||
|
this._connected = false;
|
||||||
|
this.on_disconnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected register_method<R>(method: (...args: any[]) => Promise<R> | string) {
|
||||||
|
let method_name: string;
|
||||||
|
if(typeof method === "function") {
|
||||||
|
log.debug(LogCategory.IPC, tr("Registering method proxy for %s"), method.name);
|
||||||
|
method_name = method.name;
|
||||||
|
} else {
|
||||||
|
log.debug(LogCategory.IPC, tr("Registering method proxy for %s"), method);
|
||||||
|
method_name = method;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this[method_name])
|
||||||
|
throw "method is missing in current object";
|
||||||
|
|
||||||
|
this._proxied_methods[method_name] = this[method_name];
|
||||||
|
if(!this._local) {
|
||||||
|
this[method_name] = (...args: any[]) => {
|
||||||
|
if(!this._connected)
|
||||||
|
return Promise.reject("not connected");
|
||||||
|
|
||||||
|
const proxy_callback = {
|
||||||
|
promise_id: uuidv4()
|
||||||
|
} as MethodProxyCallback;
|
||||||
|
this._proxied_callbacks[proxy_callback.promise_id] = proxy_callback;
|
||||||
|
proxy_callback.promise = new Promise((resolve, reject) => {
|
||||||
|
proxy_callback.resolve = resolve;
|
||||||
|
proxy_callback.reject = reject;
|
||||||
|
});
|
||||||
|
|
||||||
|
this._ipc_channel.sendMessage("invoke", {
|
||||||
|
promise_id: proxy_callback.promise_id,
|
||||||
|
arguments: [...args],
|
||||||
|
method_name: method_name
|
||||||
|
} as MethodProxyInvokeData);
|
||||||
|
return proxy_callback.promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handle_message(remote_id: string, boradcast: boolean, message: ChannelMessage) {
|
||||||
|
if(message.type === "finalize") {
|
||||||
|
this._handle_finalize();
|
||||||
|
} else if(message.type === "initialize") {
|
||||||
|
this._handle_remote_callback(remote_id);
|
||||||
|
} else if(message.type === "invoke") {
|
||||||
|
this._handle_invoke(message.data);
|
||||||
|
} else if(message.type === "result") {
|
||||||
|
this._handle_result(message.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handle_finalize() {
|
||||||
|
this.on_disconnected();
|
||||||
|
this.finalize();
|
||||||
|
this._connected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handle_remote_callback(remote_id: string) {
|
||||||
|
if(!this._ipc_channel.targetClientId) {
|
||||||
|
if(this._slave)
|
||||||
|
throw "initialize wrong state!";
|
||||||
|
|
||||||
|
this._ipc_channel.targetClientId = remote_id; /* now we're able to send messages */
|
||||||
|
this.on_connected();
|
||||||
|
this._ipc_channel.sendMessage("initialize", true);
|
||||||
|
} else {
|
||||||
|
if(!this._slave)
|
||||||
|
throw "initialize wrong state!";
|
||||||
|
|
||||||
|
this.on_connected();
|
||||||
|
}
|
||||||
|
this._connected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _send_result(promise_id: string, success: boolean, message: any) {
|
||||||
|
this._ipc_channel.sendMessage("result", {
|
||||||
|
promise_id: promise_id,
|
||||||
|
result: message,
|
||||||
|
success: success
|
||||||
|
} as MethodProxyResultData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handle_invoke(data: MethodProxyInvokeData) {
|
||||||
|
if(this._proxied_methods[data.method_name])
|
||||||
|
throw "we could not invoke a local proxied method!";
|
||||||
|
|
||||||
|
if(!this[data.method_name]) {
|
||||||
|
this._send_result(data.promise_id, false, "missing method");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info(LogCategory.IPC, tr("Invoking method %s with arguments: %o"), data.method_name, data.arguments);
|
||||||
|
|
||||||
|
const promise = this[data.method_name](...data.arguments);
|
||||||
|
promise.then(result => {
|
||||||
|
log.info(LogCategory.IPC, tr("Result: %o"), result);
|
||||||
|
this._send_result(data.promise_id, true, result);
|
||||||
|
}).catch(error => {
|
||||||
|
this._send_result(data.promise_id, false, error);
|
||||||
|
});
|
||||||
|
} catch(error) {
|
||||||
|
this._send_result(data.promise_id, false, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handle_result(data: MethodProxyResultData) {
|
||||||
|
if(!this._proxied_callbacks[data.promise_id]) {
|
||||||
|
console.warn(tr("Received proxy method result for unknown promise"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const callback = this._proxied_callbacks[data.promise_id];
|
||||||
|
delete this._proxied_callbacks[data.promise_id];
|
||||||
|
|
||||||
|
if(data.success)
|
||||||
|
callback.resolve(data.result);
|
||||||
|
else
|
||||||
|
callback.reject(data.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_connect_parameters() : MethodProxyConnectParameters {
|
||||||
|
if(this._slave)
|
||||||
|
throw "only masters can generate connect parameters!";
|
||||||
|
if(!this._ipc_channel)
|
||||||
|
throw "please call setup() before";
|
||||||
|
|
||||||
|
return {
|
||||||
|
channel_id: this._ipc_channel.channelId,
|
||||||
|
client_id: this.ipc_handler.getLocalAddress()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
is_slave() { return this._local || this._slave; } /* the popout modal */
|
||||||
|
is_master() { return this._local || !this._slave; } /* the host (teaweb application) */
|
||||||
|
|
||||||
|
protected abstract on_connected();
|
||||||
|
protected abstract on_disconnected();
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import {settings, Settings} from "tc-shared/settings";
|
||||||
import * as profiles from "tc-shared/profiles/ConnectionProfile";
|
import * as profiles from "tc-shared/profiles/ConnectionProfile";
|
||||||
import * as log from "tc-shared/log";
|
import * as log from "tc-shared/log";
|
||||||
import {LogCategory} from "tc-shared/log";
|
import {LogCategory} from "tc-shared/log";
|
||||||
import * as bipc from "./BrowserIPC";
|
import * as bipc from "./ipc/BrowserIPC";
|
||||||
import * as sound from "./sound/Sounds";
|
import * as sound from "./sound/Sounds";
|
||||||
import * as i18n from "./i18n/localize";
|
import * as i18n from "./i18n/localize";
|
||||||
import {tra} from "./i18n/localize";
|
import {tra} from "./i18n/localize";
|
||||||
|
@ -39,7 +39,10 @@ import ContextMenuEvent = JQuery.ContextMenuEvent;
|
||||||
import "./proto";
|
import "./proto";
|
||||||
import "./ui/elements/ContextDivider";
|
import "./ui/elements/ContextDivider";
|
||||||
import "./ui/elements/Tab";
|
import "./ui/elements/Tab";
|
||||||
import "./connection/CommandHandler"; /* else it might not get bundled because only the backends are accessing it */
|
import "./connection/CommandHandler";
|
||||||
|
import {ConnectRequestData} from "tc-shared/ipc/ConnectHandler";
|
||||||
|
import {spawnVideoPopout} from "tc-shared/video-viewer/Controller";
|
||||||
|
import {spawnModalCssVariableEditor} from "tc-shared/ui/modal/css-editor/Controller"; /* else it might not get bundled because only the backends are accessing it */
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -130,8 +133,6 @@ function setup_jsrender() : boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initialize() {
|
async function initialize() {
|
||||||
Settings.initialize();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await i18n.initialize();
|
await i18n.initialize();
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
|
@ -251,7 +252,7 @@ interface Window {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function handle_connect_request(properties: bipc.connect.ConnectRequestData, connection: ConnectionHandler) {
|
export function handle_connect_request(properties: ConnectRequestData, connection: ConnectionHandler) {
|
||||||
const profile_uuid = properties.profile || (profiles.default_profile() || {id: 'default'}).id;
|
const profile_uuid = properties.profile || (profiles.default_profile() || {id: 'default'}).id;
|
||||||
const profile = profiles.find_profile(profile_uuid) || profiles.default_profile();
|
const profile = profiles.find_profile(profile_uuid) || profiles.default_profile();
|
||||||
const username = properties.username || profile.connect_username();
|
const username = properties.username || profile.connect_username();
|
||||||
|
@ -497,7 +498,7 @@ function main() {
|
||||||
modal.close_listener.push(() => settings.changeGlobal(Settings.KEY_USER_IS_NEW, false));
|
modal.close_listener.push(() => settings.changeGlobal(Settings.KEY_USER_IS_NEW, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
(window as any).spawnFileTransferModal = spawnFileTransferModal;
|
(window as any).spawnVideoPopout = spawnModalCssVariableEditor;
|
||||||
}
|
}
|
||||||
|
|
||||||
const task_teaweb_starter: loader.Task = {
|
const task_teaweb_starter: loader.Task = {
|
||||||
|
@ -527,7 +528,7 @@ const task_connect_handler: loader.Task = {
|
||||||
name: "Connect handler",
|
name: "Connect handler",
|
||||||
function: async () => {
|
function: async () => {
|
||||||
const address = settings.static(Settings.KEY_CONNECT_ADDRESS, "");
|
const address = settings.static(Settings.KEY_CONNECT_ADDRESS, "");
|
||||||
const chandler = bipc.get_connect_handler();
|
const chandler = bipc.getInstanceConnectHandler();
|
||||||
if(settings.static(Settings.KEY_FLAG_CONNECT_DEFAULT, false) && address) {
|
if(settings.static(Settings.KEY_FLAG_CONNECT_DEFAULT, false) && address) {
|
||||||
const connect_data = {
|
const connect_data = {
|
||||||
address: address,
|
address: address,
|
||||||
|
@ -599,7 +600,7 @@ const task_certificate_callback: loader.Task = {
|
||||||
log.info(LogCategory.IPC, tr("Using this instance as certificate callback. ID: %s"), certificate_accept);
|
log.info(LogCategory.IPC, tr("Using this instance as certificate callback. ID: %s"), certificate_accept);
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
await bipc.get_handler().post_certificate_accpected(certificate_accept);
|
await bipc.getInstance().post_certificate_accpected(certificate_accept);
|
||||||
} catch(e) {} //FIXME remove!
|
} catch(e) {} //FIXME remove!
|
||||||
log.info(LogCategory.IPC, tr("Other instance has acknowledged out work. Closing this window."));
|
log.info(LogCategory.IPC, tr("Other instance has acknowledged out work. Closing this window."));
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
import * as log from "tc-shared/log";
|
||||||
import {LogCategory} from "tc-shared/log";
|
import {LogCategory} from "tc-shared/log";
|
||||||
import * as loader from "tc-loader";
|
import * as loader from "tc-loader";
|
||||||
import * as log from "tc-shared/log";
|
import {Stage} from "tc-loader";
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
|
|
||||||
type ConfigValueTypes = boolean | number | string;
|
type ConfigValueTypes = boolean | number | string;
|
||||||
|
@ -456,6 +457,11 @@ export class Settings extends StaticSettings {
|
||||||
/* defaultValue: <users download directory> */
|
/* defaultValue: <users download directory> */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static readonly KEY_IPC_REMOTE_ADDRESS: SettingsKey<string> = {
|
||||||
|
key: "ipc-address",
|
||||||
|
valueType: "string"
|
||||||
|
};
|
||||||
|
|
||||||
static readonly FN_LOG_ENABLED: (category: string) => SettingsKey<boolean> = category => {
|
static readonly FN_LOG_ENABLED: (category: string) => SettingsKey<boolean> = category => {
|
||||||
return {
|
return {
|
||||||
key: "log." + category.toLowerCase() + ".enabled",
|
key: "log." + category.toLowerCase() + ".enabled",
|
||||||
|
@ -709,3 +715,9 @@ export class ServerSettings extends SettingsBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
export let settings: Settings = null;
|
export let settings: Settings = null;
|
||||||
|
|
||||||
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
|
priority: 1000,
|
||||||
|
name: "Settings initialize",
|
||||||
|
function: async () => Settings.initialize()
|
||||||
|
})
|
|
@ -0,0 +1,36 @@
|
||||||
|
@import "../../../css/static/mixin";
|
||||||
|
|
||||||
|
/* code hightliting */
|
||||||
|
.inlineCode, .code {
|
||||||
|
display: block;
|
||||||
|
margin: 3px;
|
||||||
|
|
||||||
|
font-size: 80%;
|
||||||
|
border-radius: .2em;
|
||||||
|
font-family: Monaco, Menlo, Consolas, "Roboto Mono", "Andale Mono", "Ubuntu Mono", monospace;
|
||||||
|
box-decoration-break: clone;
|
||||||
|
|
||||||
|
&.inlineCode {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
> .hljs {
|
||||||
|
padding: 0 .25em!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
white-space: pre-wrap;
|
||||||
|
margin: 0 0 -0.1em;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
&.code {
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
@include chat-scrollbar-horizontal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fix tailing new line after code blocks */
|
||||||
|
.code + br {
|
||||||
|
display: none;
|
||||||
|
}
|
|
@ -68,6 +68,8 @@ registerLanguage("x86asm", import("highlight.js/lib/languages/x86asm"));
|
||||||
registerLanguage("xml", import("highlight.js/lib/languages/xml"));
|
registerLanguage("xml", import("highlight.js/lib/languages/xml"));
|
||||||
registerLanguage("yaml", import("highlight.js/lib/languages/yaml"));
|
registerLanguage("yaml", import("highlight.js/lib/languages/yaml"));
|
||||||
|
|
||||||
|
const cssStyle = require("./highlight.scss");
|
||||||
|
|
||||||
interface HighlightResult {
|
interface HighlightResult {
|
||||||
relevance : number
|
relevance : number
|
||||||
value : string
|
value : string
|
||||||
|
@ -91,7 +93,7 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
}
|
}
|
||||||
|
|
||||||
render(element: TagElement): React.ReactNode {
|
render(element: TagElement): React.ReactNode {
|
||||||
const klass = element.tagNormalized != 'code' ? "tag-hljs-inline-code" : "tag-hljs-code";
|
const klass = element.tagNormalized != 'code' ? cssStyle.inlineCode : cssStyle.code;
|
||||||
const language = (element.options || "").replace("\"", "'").toLowerCase();
|
const language = (element.options || "").replace("\"", "'").toLowerCase();
|
||||||
|
|
||||||
let lines = rendererText.renderContent(element).join("").split("\n");
|
let lines = rendererText.renderContent(element).join("").split("\n");
|
||||||
|
|
|
@ -2,9 +2,9 @@ import TextRenderer from "vendor/xbbcode/renderer/text";
|
||||||
import ReactRenderer from "vendor/xbbcode/renderer/react";
|
import ReactRenderer from "vendor/xbbcode/renderer/react";
|
||||||
import HTMLRenderer from "vendor/xbbcode/renderer/html";
|
import HTMLRenderer from "vendor/xbbcode/renderer/html";
|
||||||
|
|
||||||
import "./emoji";
|
|
||||||
import "./highlight";
|
|
||||||
|
|
||||||
export const rendererText = new TextRenderer();
|
export const rendererText = new TextRenderer();
|
||||||
export const rendererReact = new ReactRenderer();
|
export const rendererReact = new ReactRenderer();
|
||||||
export const rendererHTML = new HTMLRenderer(rendererReact);
|
export const rendererHTML = new HTMLRenderer(rendererReact);
|
||||||
|
|
||||||
|
import "./emoji";
|
||||||
|
import "./highlight";
|
|
@ -787,7 +787,7 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(update_avatar)
|
if(update_avatar)
|
||||||
this.channelTree.client.fileManager.avatars.update_cache(this.avatarId(), this.properties.client_flag_avatar);
|
this.channelTree.client.fileManager.avatars.updateCache(this.avatarId(), this.properties.client_flag_avatar);
|
||||||
|
|
||||||
/* devel-block(log-client-property-updates) */
|
/* devel-block(log-client-property-updates) */
|
||||||
group.end();
|
group.end();
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {formatMessage} from "tc-shared/ui/frames/chat";
|
||||||
import {control_bar_instance} from "tc-shared/ui/frames/control-bar";
|
import {control_bar_instance} from "tc-shared/ui/frames/control-bar";
|
||||||
import {icon_cache_loader, IconManager, LocalIcon} from "tc-shared/file/Icons";
|
import {icon_cache_loader, IconManager, LocalIcon} from "tc-shared/file/Icons";
|
||||||
import {spawnPermissionEditorModal} from "tc-shared/ui/modal/permission/ModalPermissionEditor";
|
import {spawnPermissionEditorModal} from "tc-shared/ui/modal/permission/ModalPermissionEditor";
|
||||||
|
import {spawnModalCssVariableEditor} from "tc-shared/ui/modal/css-editor/Controller";
|
||||||
|
|
||||||
export interface HRItem { }
|
export interface HRItem { }
|
||||||
|
|
||||||
|
@ -521,6 +522,11 @@ export function initialize() {
|
||||||
_state_updater["tools.qc"] = { item: item, conditions: [condition_connected]};
|
_state_updater["tools.qc"] = { item: item, conditions: [condition_connected]};
|
||||||
menu.append_hr();
|
menu.append_hr();
|
||||||
|
|
||||||
|
if(__build.target === "web") {
|
||||||
|
item = menu.append_item(tr("Modify CSS variables"));
|
||||||
|
item.click(() => spawnModalCssVariableEditor());
|
||||||
|
}
|
||||||
|
|
||||||
item = menu.append_item(tr("Settings"));
|
item = menu.append_item(tr("Settings"));
|
||||||
item.icon("client-settings");
|
item.icon("client-settings");
|
||||||
item.click(() => spawnSettingsModal());
|
item.click(() => spawnSettingsModal());
|
||||||
|
|
|
@ -26,7 +26,7 @@ export class ConnectionManager {
|
||||||
|
|
||||||
constructor(tag: JQuery) {
|
constructor(tag: JQuery) {
|
||||||
this.event_registry = new Registry<ConnectionManagerEvents>();
|
this.event_registry = new Registry<ConnectionManagerEvents>();
|
||||||
this.event_registry.enable_debug("connection-manager");
|
this.event_registry.enableDebug("connection-manager");
|
||||||
|
|
||||||
this._tag = tag;
|
this._tag = tag;
|
||||||
|
|
||||||
|
@ -129,6 +129,10 @@ export class ConnectionManager {
|
||||||
top_menu.update_state(); //FIXME: Top menu should listen to our events!
|
top_menu.update_state(); //FIXME: Top menu should listen to our events!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findConnection(handlerId: string) : ConnectionHandler | undefined {
|
||||||
|
return this.connection_handlers.find(e => e.handlerId === handlerId);
|
||||||
|
}
|
||||||
|
|
||||||
active_connection() : ConnectionHandler | undefined {
|
active_connection() : ConnectionHandler | undefined {
|
||||||
return this.active_handler;
|
return this.active_handler;
|
||||||
}
|
}
|
||||||
|
|
|
@ -406,7 +406,7 @@ export class ControlBar extends React.Component<ControlBarProperties, {}> {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.event_registry = new Registry<InternalControlBarEvents>();
|
this.event_registry = new Registry<InternalControlBarEvents>();
|
||||||
this.event_registry.enable_debug("control-bar");
|
this.event_registry.enableDebug("control-bar");
|
||||||
initialize(this.event_registry);
|
initialize(this.event_registry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,21 @@
|
||||||
@import "../../../../css/static/mixin";
|
@import "../../../../css/static/mixin";
|
||||||
@import "../../../../css/static/properties";
|
@import "../../../../css/static/properties";
|
||||||
|
|
||||||
|
html:root {
|
||||||
|
--chatbox-emoji-hover-background: #454545;
|
||||||
|
--chatbox-input-background: #464646;
|
||||||
|
--chatbox-input-background-disabled: #3d3d3d;
|
||||||
|
|
||||||
|
--chatbox-input-border: #353535;
|
||||||
|
--chatbox-input-border-hover: #474747;
|
||||||
|
--chatbox-input-border-focus: #585858;
|
||||||
|
|
||||||
|
--chatbox-input: #a9a9a9;
|
||||||
|
--chatbox-input-planceholder: #363535;
|
||||||
|
--chatbox-input-disabled: #4d4d4d;
|
||||||
|
--chatbox-input-help: #555555;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@include user-select(none);
|
@include user-select(none);
|
||||||
|
|
||||||
|
@ -47,7 +62,7 @@
|
||||||
border-radius: .25em;
|
border-radius: .25em;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #454545;
|
background-color: var(--chatbox-emoji-hover-background);
|
||||||
}
|
}
|
||||||
@include transition(background-color $button_hover_animation_time ease-in-out);
|
@include transition(background-color $button_hover_animation_time ease-in-out);
|
||||||
|
|
||||||
|
@ -118,8 +133,8 @@
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #464646;
|
background-color: var(--chatbox-input-background);
|
||||||
border: 2px solid #353535; /* background color (like no border) */
|
border: 2px solid var(--chatbox-input-border); /* background color (like no border) */
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
@ -147,13 +162,13 @@
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
color: #a9a9a9;
|
color: var(--chatbox-input);
|
||||||
|
|
||||||
@include chat-scrollbar-vertical();
|
@include chat-scrollbar-vertical();
|
||||||
|
|
||||||
&:empty::before {
|
&:empty::before {
|
||||||
content: attr(placeholder);
|
content: attr(placeholder);
|
||||||
color: #5e5e5e;
|
color: var(--chatbox-input-planceholder);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:empty:focus::before {
|
&:empty:focus::before {
|
||||||
|
@ -163,11 +178,11 @@
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
.textarea {
|
.textarea {
|
||||||
background-color: #3d3d3d;
|
background-color: var(--chatbox-input-background-disabled);
|
||||||
border-width: 0;
|
border-width: 0;
|
||||||
|
|
||||||
&:empty::before {
|
&:empty::before {
|
||||||
color: #4d4d4d;
|
color: var(--chatbox-input-disabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,16 +190,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@include placeholder(textarea) {
|
@include placeholder(textarea) {
|
||||||
color: #363535;
|
color: var(--chatbox-input-planceholder);
|
||||||
font-style: oblique;
|
font-style: oblique;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: #474747;
|
border-color: var(--chatbox-input-border-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-within {
|
&:focus-within {
|
||||||
border-color: #585858;
|
border-color: var(--chatbox-input-border-focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
@include transition(border-color $button_hover_animation_time ease-in-out);
|
@include transition(border-color $button_hover_animation_time ease-in-out);
|
||||||
|
@ -197,7 +212,7 @@
|
||||||
min-height: unset;
|
min-height: unset;
|
||||||
height: initial;
|
height: initial;
|
||||||
|
|
||||||
color: #555555;
|
color: var(--chatbox-input-help);
|
||||||
font-size: .8em;
|
font-size: .8em;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin: -3px 2px 2px 2.5em;
|
margin: -3px 2px 2px 2.5em;
|
||||||
|
|
|
@ -260,6 +260,7 @@ const TextInput = (props: { events: Registry<ChatBoxEvents>, enabled?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ChatBoxProperties {
|
export interface ChatBoxProperties {
|
||||||
|
className?: string;
|
||||||
onSubmit?: (text: string) => void;
|
onSubmit?: (text: string) => void;
|
||||||
onType?: () => void;
|
onType?: () => void;
|
||||||
}
|
}
|
||||||
|
@ -298,7 +299,7 @@ export class ChatBox extends React.Component<ChatBoxProperties, ChatBoxState> {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = { enabled: false };
|
this.state = { enabled: false };
|
||||||
this.events.enable_debug("chat-box");
|
this.events.enableDebug("chat-box");
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
|
@ -312,7 +313,7 @@ export class ChatBox extends React.Component<ChatBoxProperties, ChatBoxState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div className={cssStyle.container}>
|
return <div className={cssStyle.container + " " + this.props.className}>
|
||||||
<div className={cssStyle.chatbox}>
|
<div className={cssStyle.chatbox}>
|
||||||
<EmojiButton events={this.events} />
|
<EmojiButton events={this.events} />
|
||||||
<TextInput events={this.events} placeholder={tr("Type your message here...")} />
|
<TextInput events={this.events} placeholder={tr("Type your message here...")} />
|
||||||
|
|
|
@ -109,6 +109,9 @@ export interface ConversationUIEvents {
|
||||||
action_jump_to_present: { chatId: string },
|
action_jump_to_present: { chatId: string },
|
||||||
action_focus_chat: {},
|
action_focus_chat: {},
|
||||||
|
|
||||||
|
query_selected_chat: {},
|
||||||
|
/* will cause a notify_selected_chat */
|
||||||
|
|
||||||
query_conversation_state: { chatId: string }, /* will cause a notify_conversation_state */
|
query_conversation_state: { chatId: string }, /* will cause a notify_conversation_state */
|
||||||
notify_conversation_state: { chatId: string } & ChatStateData,
|
notify_conversation_state: { chatId: string } & ChatStateData,
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
} from "tc-shared/ui/frames/side/ConversationDefinitions";
|
} from "tc-shared/ui/frames/side/ConversationDefinitions";
|
||||||
import {ConversationPanel} from "tc-shared/ui/frames/side/ConversationUI";
|
import {ConversationPanel} from "tc-shared/ui/frames/side/ConversationUI";
|
||||||
import {preprocessChatMessageForSend} from "tc-shared/text/chat";
|
import {preprocessChatMessageForSend} from "tc-shared/text/chat";
|
||||||
|
import {spawnExternalModal} from "tc-shared/ui/react-elements/external-modal";
|
||||||
|
|
||||||
const kMaxChatFrameMessageSize = 50; /* max 100 messages, since the server does not support more than 100 messages queried at once */
|
const kMaxChatFrameMessageSize = 50; /* max 100 messages, since the server does not support more than 100 messages queried at once */
|
||||||
|
|
||||||
|
@ -676,10 +677,19 @@ export class ConversationManager extends AbstractChatManager<ConversationUIEvent
|
||||||
|
|
||||||
ReactDOM.render(React.createElement(ConversationPanel, {
|
ReactDOM.render(React.createElement(ConversationPanel, {
|
||||||
events: this.uiEvents,
|
events: this.uiEvents,
|
||||||
handler: this.connection,
|
handlerId: this.connection.handlerId,
|
||||||
noFirstMessageOverlay: false,
|
noFirstMessageOverlay: false,
|
||||||
messagesDeletable: true
|
messagesDeletable: true
|
||||||
}), this.htmlTag);
|
}), this.htmlTag);
|
||||||
|
/*
|
||||||
|
spawnExternalModal("conversation", this.uiEvents, {
|
||||||
|
handlerId: this.connection.handlerId,
|
||||||
|
noFirstMessageOverlay: false,
|
||||||
|
messagesDeletable: true
|
||||||
|
}).open().then(() => {
|
||||||
|
console.error("Opened");
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
this.uiEvents.on("action_select_chat", event => this.selectedConversation_ = parseInt(event.chatId));
|
this.uiEvents.on("action_select_chat", event => this.selectedConversation_ = parseInt(event.chatId));
|
||||||
this.uiEvents.on("notify_destroy", connection.events().on("notify_connection_state_changed", event => {
|
this.uiEvents.on("notify_destroy", connection.events().on("notify_connection_state_changed", event => {
|
||||||
|
@ -827,6 +837,11 @@ export class ConversationManager extends AbstractChatManager<ConversationUIEvent
|
||||||
conversation.deleteMessage(event.uniqueId);
|
conversation.deleteMessage(event.uniqueId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EventHandler<ConversationUIEvents>("query_selected_chat")
|
||||||
|
private handleQuerySelectedChat(event: ConversationUIEvents["query_selected_chat"]) {
|
||||||
|
this.uiEvents.fire_async("notify_selected_chat", { chatId: isNaN(this.selectedConversation_) ? "unselected" : this.selectedConversation_ + ""})
|
||||||
|
}
|
||||||
|
|
||||||
@EventHandler<ConversationUIEvents>("notify_selected_chat")
|
@EventHandler<ConversationUIEvents>("notify_selected_chat")
|
||||||
private handleNotifySelectedChat(event: ConversationUIEvents["notify_selected_chat"]) {
|
private handleNotifySelectedChat(event: ConversationUIEvents["notify_selected_chat"]) {
|
||||||
this.selectedConversation_ = parseInt(event.chatId);
|
this.selectedConversation_ = parseInt(event.chatId);
|
||||||
|
|
|
@ -6,6 +6,43 @@ $client_info_avatar_size: 10em;
|
||||||
$bot_thumbnail_width: 16em;
|
$bot_thumbnail_width: 16em;
|
||||||
$bot_thumbnail_height: 9em;
|
$bot_thumbnail_height: 9em;
|
||||||
|
|
||||||
|
html:root {
|
||||||
|
--chat-background: #353535;
|
||||||
|
|
||||||
|
--chat-event-new-message: #8b8b8b;
|
||||||
|
|
||||||
|
--chat-loader-background: #252525;
|
||||||
|
--chat-loader: #565353;
|
||||||
|
|
||||||
|
--chat-loader-background-hover: #232326;
|
||||||
|
--chat-loader-hover: #5b5757;
|
||||||
|
|
||||||
|
--chat-partner-typing: #4d4d4d;
|
||||||
|
|
||||||
|
--chat-message-background: #303030;
|
||||||
|
--chat-message-timestamp: #5d5b5b;
|
||||||
|
--chat-message: #b5b5b5;
|
||||||
|
|
||||||
|
--chat-message-table-border: #1e2025;
|
||||||
|
--chat-message-table-row-background: #303036;
|
||||||
|
--chat-message-table-row-even-background: #25252a;
|
||||||
|
|
||||||
|
--chat-message-quote-border: #737373;
|
||||||
|
--chat-message-quote: #737373;
|
||||||
|
|
||||||
|
--chat-timestamp: #565353;
|
||||||
|
--chat-overlay: #5a5a5a;
|
||||||
|
--chat-unread: #bc1515;
|
||||||
|
|
||||||
|
--chat-container-switch: #535353;
|
||||||
|
|
||||||
|
--chat-event-general: #524e4e;
|
||||||
|
--chat-event-message-send-failed: #ac5353;
|
||||||
|
--chat-event-close: #adad1f;
|
||||||
|
--chat-event-disconnect: #a82424;
|
||||||
|
--chat-event-reconnect: #1B7E1B;
|
||||||
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -15,6 +52,7 @@ $bot_thumbnail_height: 9em;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
min-width: 250px;
|
min-width: 250px;
|
||||||
|
background: var(--chat-background);
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@ -44,10 +82,10 @@ $bot_thumbnail_height: 9em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: #8b8b8b;
|
color: var(--chat-event-new-message);
|
||||||
|
|
||||||
background: #353535; /* if we dont support gradients */
|
background: var(--chat-background); /* if we dont support gradients */
|
||||||
background: linear-gradient(rgba(53, 53, 53, 0) 10%, #353535 70%);
|
background: linear-gradient(rgba(53, 53, 53, 0) 10%, var(--chat-background) 70%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
@ -67,7 +105,8 @@ $bot_thumbnail_height: 9em;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
background: linear-gradient(rgba(53, 53, 53, 0) 10%, #353535 70%);
|
background: var(--chat-background); /* if we dont support gradients */
|
||||||
|
background: linear-gradient(rgba(53, 53, 53, 0) 10%, var(--chat-background) 70%);
|
||||||
|
|
||||||
.inner {
|
.inner {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@ -78,8 +117,8 @@ $bot_thumbnail_height: 9em;
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
background: #252525;
|
background: var(--chat-loader-background);
|
||||||
color: #565353;
|
color: var(--chat-loader);
|
||||||
|
|
||||||
margin-left: 4.5em;
|
margin-left: 4.5em;
|
||||||
margin-right: 2em;
|
margin-right: 2em;
|
||||||
|
@ -92,8 +131,8 @@ $bot_thumbnail_height: 9em;
|
||||||
@include transition(background-color ease-in-out $button_hover_animation_time);
|
@include transition(background-color ease-in-out $button_hover_animation_time);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #232326;
|
background-color: var(--chat-loader-background-hover);
|
||||||
color: #5b5757;
|
color: var(--chat-loader-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +156,7 @@ $bot_thumbnail_height: 9em;
|
||||||
padding-left: .6em;
|
padding-left: .6em;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
||||||
color: #4d4d4d;
|
color: var(--chat-partner-typing);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
@include transition(.25s ease-in-out);
|
@include transition(.25s ease-in-out);
|
||||||
|
@ -189,7 +228,7 @@ $bot_thumbnail_height: 9em;
|
||||||
|
|
||||||
@include user-select(text);
|
@include user-select(text);
|
||||||
|
|
||||||
background: #303030;
|
background: var(--chat-message-background);
|
||||||
border-radius: 6px 6px 6px 6px;
|
border-radius: 6px 6px 6px 6px;
|
||||||
|
|
||||||
margin-top: .5em;
|
margin-top: .5em;
|
||||||
|
@ -213,7 +252,7 @@ $bot_thumbnail_height: 9em;
|
||||||
display: inline;
|
display: inline;
|
||||||
|
|
||||||
font-size: 0.66em;
|
font-size: 0.66em;
|
||||||
color: #5d5b5b;
|
color: var(--chat-message-timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete {
|
.delete {
|
||||||
|
@ -238,7 +277,7 @@ $bot_thumbnail_height: 9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
color: #b5b5b5;
|
color: var(--chat-message);
|
||||||
line-height: 1.1em;
|
line-height: 1.1em;
|
||||||
|
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
@ -261,15 +300,15 @@ $bot_thumbnail_height: 9em;
|
||||||
|
|
||||||
table {
|
table {
|
||||||
th, td {
|
th, td {
|
||||||
border-color: #1e2025;
|
border-color: var(--chat-message-table-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
background-color: #303036;
|
background-color: var(--chat-message-table-row-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
tr:nth-child(2n) {
|
tr:nth-child(2n) {
|
||||||
background-color: #25252a;
|
background-color: var(--chat-message-table-row-even-background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,9 +343,9 @@ $bot_thumbnail_height: 9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.xbbcode-tag-quote) {
|
:global(.xbbcode-tag-quote) {
|
||||||
border-color: #737373;
|
border-color: var(--chat-message-quote-border);
|
||||||
padding-left: .5em;
|
padding-left: .5em;
|
||||||
color: #737373;
|
color: var(--chat-message-quote);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,7 +359,7 @@ $bot_thumbnail_height: 9em;
|
||||||
|
|
||||||
margin-left: calc(-.5em - 1em);
|
margin-left: calc(-.5em - 1em);
|
||||||
border-top: .5em solid transparent;
|
border-top: .5em solid transparent;
|
||||||
border-right: .75em solid #303030;
|
border-right: .75em solid var(--chat-message-background);
|
||||||
border-bottom: .5em solid transparent;
|
border-bottom: .5em solid transparent;
|
||||||
|
|
||||||
top: 1.25em;
|
top: 1.25em;
|
||||||
|
@ -331,7 +370,7 @@ $bot_thumbnail_height: 9em;
|
||||||
.containerTimestamp {
|
.containerTimestamp {
|
||||||
margin-left: 2.5em;
|
margin-left: 2.5em;
|
||||||
|
|
||||||
color: #565353;
|
color: var(--chat-timestamp);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -349,8 +388,8 @@ $bot_thumbnail_height: 9em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: #5a5a5a;
|
color: var(--chat-overlay);
|
||||||
background: #353535;
|
background: var(--chat-background);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -362,7 +401,7 @@ $bot_thumbnail_height: 9em;
|
||||||
margin-right: .5em;
|
margin-right: .5em;
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #bc1515;
|
color: var(--chat-unread);
|
||||||
}
|
}
|
||||||
|
|
||||||
.jumpToPresentPlaceholder {
|
.jumpToPresentPlaceholder {
|
||||||
|
@ -379,10 +418,10 @@ $bot_thumbnail_height: 9em;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
color: #535353;
|
color: var(--chat-container-switch);
|
||||||
|
|
||||||
a {
|
a {
|
||||||
background: #353535;
|
background: var(--chat-background);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
|
@ -398,7 +437,7 @@ $bot_thumbnail_height: 9em;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
||||||
height: .1em;
|
height: .1em;
|
||||||
background-color: #535353;
|
background-color: var(--chat-container-switch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -408,27 +447,27 @@ $bot_thumbnail_height: 9em;
|
||||||
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #524e4e;
|
color: var(--chat-event-general);
|
||||||
|
|
||||||
a {
|
a {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.containerMessageSendFailed {
|
&.containerMessageSendFailed {
|
||||||
color: #ac5353;
|
color: var(--chat-event-message-send-failed);
|
||||||
margin-bottom: .5em;
|
margin-bottom: .5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.actionClose {
|
&.actionClose {
|
||||||
color: #adad1f;
|
color: var(--chat-event-close);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.actionDisconnect {
|
&.actionDisconnect {
|
||||||
color: #a82424;
|
color: var(--chat-event-disconnect);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.actionReconnect {
|
&.actionReconnect {
|
||||||
color: hsl(120, 65%, 30%);
|
color: var(--chat-event-reconnect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,9 +1,7 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
|
import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
|
||||||
import {ChatBox} from "tc-shared/ui/frames/side/ChatBox";
|
import {ChatBox} from "tc-shared/ui/frames/side/ChatBox";
|
||||||
import {generate_client} from "tc-shared/ui/htmltags";
|
|
||||||
import {Ref, useEffect, useRef, useState} from "react";
|
import {Ref, useEffect, useRef, useState} from "react";
|
||||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
|
||||||
import {AvatarRenderer} from "tc-shared/ui/react-elements/Avatar";
|
import {AvatarRenderer} from "tc-shared/ui/react-elements/Avatar";
|
||||||
import {format} from "tc-shared/ui/frames/side/chat_helper";
|
import {format} from "tc-shared/ui/frames/side/chat_helper";
|
||||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
|
@ -23,6 +21,7 @@ import {
|
||||||
} from "tc-shared/ui/frames/side/ConversationDefinitions";
|
} from "tc-shared/ui/frames/side/ConversationDefinitions";
|
||||||
import {TimestampRenderer} from "tc-shared/ui/react-elements/TimestampRenderer";
|
import {TimestampRenderer} from "tc-shared/ui/react-elements/TimestampRenderer";
|
||||||
import {BBCodeRenderer} from "tc-shared/text/bbcode";
|
import {BBCodeRenderer} from "tc-shared/text/bbcode";
|
||||||
|
import {getGlobalAvatarManagerFactory} from "tc-shared/file/Avatars";
|
||||||
|
|
||||||
const cssStyle = require("./ConversationUI.scss");
|
const cssStyle = require("./ConversationUI.scss");
|
||||||
|
|
||||||
|
@ -32,7 +31,7 @@ const ChatEventMessageRenderer = React.memo((props: {
|
||||||
message: ChatMessage,
|
message: ChatMessage,
|
||||||
callbackDelete?: () => void,
|
callbackDelete?: () => void,
|
||||||
events: Registry<ConversationUIEvents>,
|
events: Registry<ConversationUIEvents>,
|
||||||
handler: ConnectionHandler,
|
handlerId: string,
|
||||||
|
|
||||||
refHTMLElement?: Ref<HTMLDivElement>
|
refHTMLElement?: Ref<HTMLDivElement>
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -46,24 +45,30 @@ const ChatEventMessageRenderer = React.memo((props: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const avatar = getGlobalAvatarManagerFactory().getManager(props.handlerId)?.resolveClientAvatar({ clientUniqueId: props.message.sender_unique_id, database_id: props.message.sender_database_id });
|
||||||
return (
|
return (
|
||||||
<div className={cssStyle.containerMessage} ref={props.refHTMLElement}>
|
<div className={cssStyle.containerMessage} ref={props.refHTMLElement}>
|
||||||
<div className={cssStyle.avatar}>
|
<div className={cssStyle.avatar}>
|
||||||
<AvatarRenderer
|
<AvatarRenderer
|
||||||
className={cssStyle.imageContainer}
|
className={cssStyle.imageContainer}
|
||||||
alt={""}
|
alt={""}
|
||||||
avatar={props.handler.fileManager.avatars.resolveClientAvatar({ clientUniqueId: props.message.sender_unique_id, database_id: props.message.sender_database_id })} />
|
avatar={avatar} />
|
||||||
</div>
|
</div>
|
||||||
<div className={cssStyle.message}>
|
<div className={cssStyle.message}>
|
||||||
<div className={cssStyle.info}>
|
<div className={cssStyle.info}>
|
||||||
{deleteButton}
|
{deleteButton}
|
||||||
<a className={cssStyle.sender} dangerouslySetInnerHTML={{ __html: generate_client({
|
{/*
|
||||||
client_database_id: props.message.sender_database_id,
|
<a className={cssStyle.sender} dangerouslySetInnerHTML={{ __html: generate_client({
|
||||||
client_id: -1,
|
client_database_id: props.message.sender_database_id,
|
||||||
client_name: props.message.sender_name,
|
client_id: -1,
|
||||||
client_unique_id: props.message.sender_unique_id,
|
client_name: props.message.sender_name,
|
||||||
add_braces: false
|
client_unique_id: props.message.sender_unique_id,
|
||||||
})}} />
|
add_braces: false
|
||||||
|
})}} />
|
||||||
|
*/}
|
||||||
|
<a className={cssStyle.sender}>
|
||||||
|
<div className={"htmltag-client"}>{props.message.sender_name}</div>
|
||||||
|
</a>
|
||||||
<span> </span> { /* Only for copy purposes */}
|
<span> </span> { /* Only for copy purposes */}
|
||||||
<a className={cssStyle.timestamp}>
|
<a className={cssStyle.timestamp}>
|
||||||
<TimestampRenderer timestamp={props.message.timestamp} />
|
<TimestampRenderer timestamp={props.message.timestamp} />
|
||||||
|
@ -322,7 +327,7 @@ const PartnerTypingIndicator = (props: { events: Registry<ConversationUIEvents>,
|
||||||
|
|
||||||
interface ConversationMessagesProperties {
|
interface ConversationMessagesProperties {
|
||||||
events: Registry<ConversationUIEvents>;
|
events: Registry<ConversationUIEvents>;
|
||||||
handler: ConnectionHandler;
|
handlerId: string;
|
||||||
|
|
||||||
noFirstMessageOverlay?: boolean
|
noFirstMessageOverlay?: boolean
|
||||||
messagesDeletable?: boolean;
|
messagesDeletable?: boolean;
|
||||||
|
@ -555,6 +560,7 @@ class ConversationMessages extends React.PureComponent<ConversationMessagesPrope
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
|
this.props.events.fire("query_selected_chat");
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -604,7 +610,7 @@ class ConversationMessages extends React.PureComponent<ConversationMessagesPrope
|
||||||
message={event.message}
|
message={event.message}
|
||||||
events={this.props.events}
|
events={this.props.events}
|
||||||
callbackDelete={this.props.messagesDeletable ? () => this.props.events.fire("action_delete_message", { chatId: this.currentChatId, uniqueId: event.uniqueId }) : undefined}
|
callbackDelete={this.props.messagesDeletable ? () => this.props.events.fire("action_delete_message", { chatId: this.currentChatId, uniqueId: event.uniqueId }) : undefined}
|
||||||
handler={this.props.handler}
|
handlerId={this.props.handlerId}
|
||||||
refHTMLElement={reference}
|
refHTMLElement={reference}
|
||||||
/>);
|
/>);
|
||||||
break;
|
break;
|
||||||
|
@ -850,7 +856,7 @@ class ConversationMessages extends React.PureComponent<ConversationMessagesPrope
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConversationPanel = React.memo((props: { events: Registry<ConversationUIEvents>, handler: ConnectionHandler, messagesDeletable: boolean, noFirstMessageOverlay: boolean }) => {
|
export const ConversationPanel = React.memo((props: { events: Registry<ConversationUIEvents>, handlerId: string, messagesDeletable: boolean, noFirstMessageOverlay: boolean }) => {
|
||||||
const currentChat = useRef({ id: "unselected" });
|
const currentChat = useRef({ id: "unselected" });
|
||||||
const chatEnabled = useRef(false);
|
const chatEnabled = useRef(false);
|
||||||
|
|
||||||
|
@ -882,7 +888,7 @@ export const ConversationPanel = React.memo((props: { events: Registry<Conversat
|
||||||
});
|
});
|
||||||
|
|
||||||
return <div className={cssStyle.panel}>
|
return <div className={cssStyle.panel}>
|
||||||
<ConversationMessages events={props.events} handler={props.handler} messagesDeletable={props.messagesDeletable} noFirstMessageOverlay={props.noFirstMessageOverlay} />
|
<ConversationMessages events={props.events} handlerId={props.handlerId} messagesDeletable={props.messagesDeletable} noFirstMessageOverlay={props.noFirstMessageOverlay} />
|
||||||
<ChatBox
|
<ChatBox
|
||||||
ref={refChatBox}
|
ref={refChatBox}
|
||||||
onSubmit={text => props.events.fire("action_send_message", { chatId: currentChat.current.id, text: text }) }
|
onSubmit={text => props.events.fire("action_send_message", { chatId: currentChat.current.id, text: text }) }
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import {AbstractModal} from "tc-shared/ui/react-elements/Modal";
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
import {ConversationUIEvents} from "tc-shared/ui/frames/side/ConversationDefinitions";
|
||||||
|
import {ConversationPanel} from "tc-shared/ui/frames/side/ConversationUI";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
class PopoutConversationUI extends AbstractModal {
|
||||||
|
private readonly events: Registry<ConversationUIEvents>;
|
||||||
|
private readonly userData: any;
|
||||||
|
|
||||||
|
constructor(events: Registry<ConversationUIEvents>, userData: any) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.userData = userData;
|
||||||
|
this.events = events;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBody() {
|
||||||
|
return <ConversationPanel
|
||||||
|
handlerId={this.userData.handlerId}
|
||||||
|
events={this.events}
|
||||||
|
messagesDeletable={this.userData.messagesDeletable}
|
||||||
|
noFirstMessageOverlay={this.userData.noFirstMessageOverlay} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
title() {
|
||||||
|
return "Conversations";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export = PopoutConversationUI;
|
|
@ -330,7 +330,7 @@ export class PrivateConversationManager extends AbstractChatManager<PrivateConve
|
||||||
this.htmlTag.style.height = "100%";
|
this.htmlTag.style.height = "100%";
|
||||||
|
|
||||||
this.uiEvents.register_handler(this, true);
|
this.uiEvents.register_handler(this, true);
|
||||||
this.uiEvents.enable_debug("private-conversations");
|
this.uiEvents.enableDebug("private-conversations");
|
||||||
|
|
||||||
ReactDOM.render(React.createElement(PrivateConversationsPanel, { events: this.uiEvents, handler: this.connection }), this.htmlTag);
|
ReactDOM.render(React.createElement(PrivateConversationsPanel, { events: this.uiEvents, handler: this.connection }), this.htmlTag);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,22 @@
|
||||||
@import "../../../../css/static/mixin";
|
@import "../../../../css/static/mixin";
|
||||||
@import "../../../../css/static/properties";
|
@import "../../../../css/static/properties";
|
||||||
|
|
||||||
|
html:root {
|
||||||
|
--chat-private-no-chats: #5a5a5a;
|
||||||
|
--chat-private-border: #313132;
|
||||||
|
|
||||||
|
--chat-private-unread-background: #a81414;
|
||||||
|
--chat-private-unread-shadow: rgba(0, 0, 0, 0.20);
|
||||||
|
|
||||||
|
--chat-private-name: #ccc;
|
||||||
|
--chat-private-timestamp: #555353;
|
||||||
|
|
||||||
|
--chat-private-close-background: #5a5a5a;
|
||||||
|
|
||||||
|
--chat-private-hovered-background: #393939;
|
||||||
|
--chat-private-selected-background: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
width: 2px!important;
|
width: 2px!important;
|
||||||
min-width: 2px!important;
|
min-width: 2px!important;
|
||||||
|
@ -39,7 +55,7 @@
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: #5a5a5a;
|
color: var(--chat-private-no-chats);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +68,7 @@
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 1px solid #313132;
|
border-bottom: 1px solid var(--chat-private-border);
|
||||||
|
|
||||||
.containerAvatar {
|
.containerAvatar {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
@ -78,15 +94,15 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background-color: #a81414;
|
background-color: var(--chat-private-unread-background);
|
||||||
width: 7px;
|
width: 7px;
|
||||||
height: 7px;
|
height: 7px;
|
||||||
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
||||||
-webkit-box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.20);
|
-webkit-box-shadow: 0 0 1px 1px var(--chat-private-unread-shadow);
|
||||||
-moz-box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.20);
|
-moz-box-shadow: 0 0 1px 1px var(--chat-private-unread-shadow);
|
||||||
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.20);
|
box-shadow: 0 0 1px 1px var(--chat-private-unread-shadow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,14 +128,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
color: #ccc;
|
color: var(--chat-private-name);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
margin-bottom: -.4em;
|
margin-bottom: -.4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timestamp {
|
.timestamp {
|
||||||
color: #555353;
|
color: var(--chat-private-timestamp);
|
||||||
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: .66em;
|
font-size: .66em;
|
||||||
|
@ -150,7 +166,7 @@
|
||||||
content: ' ';
|
content: ' ';
|
||||||
height: .5em;
|
height: .5em;
|
||||||
width: .05em;
|
width: .05em;
|
||||||
background-color: #5a5a5a;
|
background-color: var(--chat-private-close-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
|
@ -163,11 +179,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #393939;
|
background-color: var(--chat-private-hovered-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
background-color: #2c2c2c;
|
background-color: var(--chat-private-selected-background);
|
||||||
}
|
}
|
||||||
@include transition(background-color $button_hover_animation_time ease-in-out);
|
@include transition(background-color $button_hover_animation_time ease-in-out);
|
||||||
}
|
}
|
|
@ -201,6 +201,6 @@ const OpenConversationsPanel = React.memo((props: { events: Registry<PrivateConv
|
||||||
export const PrivateConversationsPanel = (props: { events: Registry<PrivateConversationUIEvents>, handler: ConnectionHandler }) => (
|
export const PrivateConversationsPanel = (props: { events: Registry<PrivateConversationUIEvents>, handler: ConnectionHandler }) => (
|
||||||
<ContextDivider id={"seperator-conversation-list-messages"} direction={"horizontal"} defaultValue={25} separatorClassName={cssStyle.divider}>
|
<ContextDivider id={"seperator-conversation-list-messages"} direction={"horizontal"} defaultValue={25} separatorClassName={cssStyle.divider}>
|
||||||
<OpenConversationsPanel events={props.events} connection={props.handler} />
|
<OpenConversationsPanel events={props.events} connection={props.handler} />
|
||||||
<ConversationPanel events={props.events as any} handler={props.handler} noFirstMessageOverlay={true} messagesDeletable={false} />
|
<ConversationPanel events={props.events as any} handlerId={props.handler.handlerId} noFirstMessageOverlay={true} messagesDeletable={false} />
|
||||||
</ContextDivider>
|
</ContextDivider>
|
||||||
);
|
);
|
|
@ -1,5 +1,4 @@
|
||||||
import {Frame, FrameContent} from "tc-shared/ui/frames/chat_frame";
|
import {Frame, FrameContent} from "tc-shared/ui/frames/chat_frame";
|
||||||
import * as events from "tc-shared/events";
|
|
||||||
import {ClientEvents, MusicClientEntry, SongInfo} from "tc-shared/ui/client";
|
import {ClientEvents, MusicClientEntry, SongInfo} from "tc-shared/ui/client";
|
||||||
import {voice} from "tc-shared/connection/ConnectionBase";
|
import {voice} from "tc-shared/connection/ConnectionBase";
|
||||||
import PlayerState = voice.PlayerState;
|
import PlayerState = voice.PlayerState;
|
||||||
|
@ -8,6 +7,52 @@ import {CommandResult, ErrorID, PlaylistSong} from "tc-shared/connection/ServerC
|
||||||
import {createErrorModal, createInputModal} from "tc-shared/ui/elements/Modal";
|
import {createErrorModal, createInputModal} from "tc-shared/ui/elements/Modal";
|
||||||
import * as log from "tc-shared/log";
|
import * as log from "tc-shared/log";
|
||||||
import * as image_preview from "../image_preview";
|
import * as image_preview from "../image_preview";
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
|
||||||
|
export interface MusicSidebarEvents {
|
||||||
|
"open": {}, /* triggers when frame should be shown */
|
||||||
|
"close": {}, /* triggers when frame will be closed */
|
||||||
|
|
||||||
|
"bot_change": {
|
||||||
|
old: MusicClientEntry | undefined,
|
||||||
|
new: MusicClientEntry | undefined
|
||||||
|
},
|
||||||
|
"bot_property_update": {
|
||||||
|
properties: string[]
|
||||||
|
},
|
||||||
|
|
||||||
|
"action_play": {},
|
||||||
|
"action_pause": {},
|
||||||
|
"action_song_set": { song_id: number },
|
||||||
|
"action_forward": {},
|
||||||
|
"action_rewind": {},
|
||||||
|
"action_forward_ms": {
|
||||||
|
units: number;
|
||||||
|
},
|
||||||
|
"action_rewind_ms": {
|
||||||
|
units: number;
|
||||||
|
},
|
||||||
|
"action_song_add": {},
|
||||||
|
"action_song_delete": { song_id: number },
|
||||||
|
"action_playlist_reload": {},
|
||||||
|
|
||||||
|
"playtime_move_begin": {},
|
||||||
|
"playtime_move_end": {
|
||||||
|
canceled: boolean,
|
||||||
|
target_time?: number
|
||||||
|
},
|
||||||
|
|
||||||
|
"reorder_begin": { song_id: number; entry: JQuery },
|
||||||
|
"reorder_end": { song_id: number; canceled: boolean; entry: JQuery; previous_entry?: number },
|
||||||
|
|
||||||
|
"player_time_update": ClientEvents["music_status_update"],
|
||||||
|
"player_song_change": ClientEvents["music_song_change"],
|
||||||
|
|
||||||
|
"playlist_song_add": ClientEvents["playlist_song_add"] & { insert_effect?: boolean },
|
||||||
|
"playlist_song_remove": ClientEvents["playlist_song_remove"],
|
||||||
|
"playlist_song_reorder": ClientEvents["playlist_song_reorder"],
|
||||||
|
"playlist_song_loaded": ClientEvents["playlist_song_loaded"] & { html_entry?: JQuery },
|
||||||
|
}
|
||||||
|
|
||||||
interface LoadedSongData {
|
interface LoadedSongData {
|
||||||
description: string;
|
description: string;
|
||||||
|
@ -21,7 +66,7 @@ interface LoadedSongData {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MusicInfo {
|
export class MusicInfo {
|
||||||
readonly events: events.Registry<events.sidebar.music>;
|
readonly events: Registry<MusicSidebarEvents>;
|
||||||
readonly handle: Frame;
|
readonly handle: Frame;
|
||||||
|
|
||||||
private _html_tag: JQuery;
|
private _html_tag: JQuery;
|
||||||
|
@ -47,10 +92,10 @@ export class MusicInfo {
|
||||||
previous_frame_content: FrameContent;
|
previous_frame_content: FrameContent;
|
||||||
|
|
||||||
constructor(handle: Frame) {
|
constructor(handle: Frame) {
|
||||||
this.events = new events.Registry<events.sidebar.music>();
|
this.events = new Registry<MusicSidebarEvents>();
|
||||||
this.handle = handle;
|
this.handle = handle;
|
||||||
|
|
||||||
this.events.enable_debug("music-info");
|
this.events.enableDebug("music-info");
|
||||||
this.initialize_listener();
|
this.initialize_listener();
|
||||||
this._build_html_tag();
|
this._build_html_tag();
|
||||||
|
|
||||||
|
@ -333,7 +378,7 @@ export class MusicInfo {
|
||||||
event.old.events.off(callback_property);
|
event.old.events.off(callback_property);
|
||||||
event.old.events.off(callback_time_update);
|
event.old.events.off(callback_time_update);
|
||||||
event.old.events.off(callback_song_change);
|
event.old.events.off(callback_song_change);
|
||||||
event.old.events.disconnect_all(this.events);
|
event.old.events.disconnectAll(this.events);
|
||||||
}
|
}
|
||||||
if(event.new) {
|
if(event.new) {
|
||||||
event.new.events.on("notify_properties_updated", callback_property);
|
event.new.events.on("notify_properties_updated", callback_property);
|
||||||
|
|
|
@ -240,7 +240,7 @@ class ModalGroupCreate extends Modal {
|
||||||
constructor(connection: ConnectionHandler, target: "server" | "channel", defaultSourceGroup: number) {
|
constructor(connection: ConnectionHandler, target: "server" | "channel", defaultSourceGroup: number) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.events.enable_debug("group-create");
|
this.events.enableDebug("group-create");
|
||||||
this.defaultSourceGroup = defaultSourceGroup;
|
this.defaultSourceGroup = defaultSourceGroup;
|
||||||
this.target = target;
|
this.target = target;
|
||||||
initializeGroupCreateController(connection, this.events, this.target);
|
initializeGroupCreateController(connection, this.events, this.target);
|
||||||
|
|
|
@ -15,7 +15,7 @@ import * as htmltags from "tc-shared/ui/htmltags";
|
||||||
|
|
||||||
export function openMusicManage(client: ConnectionHandler, bot: MusicClientEntry) {
|
export function openMusicManage(client: ConnectionHandler, bot: MusicClientEntry) {
|
||||||
const ev_registry = new Registry<modal.music_manage>();
|
const ev_registry = new Registry<modal.music_manage>();
|
||||||
ev_registry.enable_debug("music-manage");
|
ev_registry.enableDebug("music-manage");
|
||||||
//dummy_controller(ev_registry);
|
//dummy_controller(ev_registry);
|
||||||
permission_controller(ev_registry, bot, client);
|
permission_controller(ev_registry, bot, client);
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ export function openModalNewcomer() : Modal {
|
||||||
});
|
});
|
||||||
|
|
||||||
const event_registry = new Registry<emodal.newcomer>();
|
const event_registry = new Registry<emodal.newcomer>();
|
||||||
event_registry.enable_debug("newcomer");
|
event_registry.enableDebug("newcomer");
|
||||||
|
|
||||||
modal.htmlTag.find(".modal-body").addClass("modal-newcomer");
|
modal.htmlTag.find(".modal-body").addClass("modal-newcomer");
|
||||||
|
|
||||||
|
@ -126,7 +126,7 @@ function initializeStepFinish(tag: JQuery, event_registry: Registry<emodal.newco
|
||||||
|
|
||||||
function initializeStepIdentity(tag: JQuery, event_registry: Registry<emodal.newcomer>) {
|
function initializeStepIdentity(tag: JQuery, event_registry: Registry<emodal.newcomer>) {
|
||||||
const profile_events = new Registry<emodal.settings.profiles>();
|
const profile_events = new Registry<emodal.settings.profiles>();
|
||||||
profile_events.enable_debug("settings-identity");
|
profile_events.enableDebug("settings-identity");
|
||||||
modal_settings.initialize_identity_profiles_controller(profile_events);
|
modal_settings.initialize_identity_profiles_controller(profile_events);
|
||||||
modal_settings.initialize_identity_profiles_view(tag, profile_events, { forum_setuppable: false });
|
modal_settings.initialize_identity_profiles_view(tag, profile_events, { forum_setuppable: false });
|
||||||
|
|
||||||
|
|
|
@ -436,7 +436,7 @@ function settings_general_chat(container: JQuery, modal: Modal) {
|
||||||
|
|
||||||
function settings_audio_microphone(container: JQuery, modal: Modal) {
|
function settings_audio_microphone(container: JQuery, modal: Modal) {
|
||||||
const registry = new Registry<events.modal.settings.microphone>();
|
const registry = new Registry<events.modal.settings.microphone>();
|
||||||
registry.enable_debug("settings-microphone");
|
registry.enableDebug("settings-microphone");
|
||||||
modal_settings.initialize_audio_microphone_controller(registry);
|
modal_settings.initialize_audio_microphone_controller(registry);
|
||||||
modal_settings.initialize_audio_microphone_view(container, registry);
|
modal_settings.initialize_audio_microphone_view(container, registry);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,187 @@
|
||||||
|
import * as loader from "tc-loader";
|
||||||
|
import {Stage} from "tc-loader";
|
||||||
|
import {CssEditorEvents, CssVariable} from "tc-shared/ui/modal/css-editor/Definitions";
|
||||||
|
import {spawnExternalModal} from "tc-shared/ui/react-elements/external-modal";
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
|
||||||
|
interface CustomVariable {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CssVariableManager {
|
||||||
|
private customVariables: {[key: string]: CustomVariable} = {};
|
||||||
|
private htmlTag: HTMLStyleElement;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
this.htmlTag = document.createElement("style");
|
||||||
|
document.body.appendChild(this.htmlTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllCssVariables() : CssVariable[] {
|
||||||
|
let variables: {[key: string]: CssVariable} = {};
|
||||||
|
|
||||||
|
const ownStyleSheets = Array.from(document.styleSheets)
|
||||||
|
.filter(sheet => sheet.href === null || sheet.href.startsWith(window.location.origin)) as CSSStyleSheet[];
|
||||||
|
for(const sheet of ownStyleSheets) {
|
||||||
|
for(const rule of sheet.cssRules) {
|
||||||
|
if(!(rule instanceof CSSStyleRule))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if(rule.selectorText !== "html:root" && rule.selectorText !== ":root")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for(const entry of rule.style) {
|
||||||
|
if(!entry.startsWith("--"))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if(variables[entry])
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const customVariable = this.customVariables[entry];
|
||||||
|
variables[entry] = {
|
||||||
|
name: entry,
|
||||||
|
defaultValue: rule.style.getPropertyValue(entry).trim(),
|
||||||
|
customValue: customVariable?.value,
|
||||||
|
overwriteValue: !!customVariable?.enabled
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
setVariable(name: string, value: string) {
|
||||||
|
const customVariable = this.customVariables[name] || (this.customVariables[name] = { name: name, value: undefined, enabled: false });
|
||||||
|
customVariable.enabled = true;
|
||||||
|
customVariable.value = value;
|
||||||
|
this.updateCustomVariables();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleCustomVariable(name: string, flag: boolean, value?: string) {
|
||||||
|
let customVariable = this.customVariables[name];
|
||||||
|
if(!customVariable) {
|
||||||
|
if(!flag)
|
||||||
|
return;
|
||||||
|
|
||||||
|
customVariable = this.customVariables[name] = { name: name, value: value, enabled: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
customVariable.enabled = flag;
|
||||||
|
if(flag && typeof value === "string")
|
||||||
|
customVariable.value = value;
|
||||||
|
this.updateCustomVariables();
|
||||||
|
}
|
||||||
|
|
||||||
|
exportConfig() {
|
||||||
|
return JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
variables: this.customVariables
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
importConfig(config: string) {
|
||||||
|
const data = JSON.parse(config);
|
||||||
|
if(data.version !== 1)
|
||||||
|
throw "unsupported config version";
|
||||||
|
|
||||||
|
this.customVariables = data.variables;
|
||||||
|
this.updateCustomVariables();
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.customVariables = {};
|
||||||
|
this.updateCustomVariables();
|
||||||
|
}
|
||||||
|
|
||||||
|
randomize() {
|
||||||
|
this.customVariables = {};
|
||||||
|
this.getAllCssVariables().forEach(e => {
|
||||||
|
this.customVariables[e.name] = {
|
||||||
|
enabled: true,
|
||||||
|
value: "#" + Math.floor(Math.random() * 0xFFFFFF).toString(16),
|
||||||
|
name: e.name
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.updateCustomVariables();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateCustomVariables() {
|
||||||
|
let text = "html:root {\n";
|
||||||
|
for(const variable of Object.values(this.customVariables))
|
||||||
|
text += " " + variable.name + ": " + variable.value + ";\n";
|
||||||
|
text += "}";
|
||||||
|
this.htmlTag.textContent = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let cssVariableManager: CssVariableManager;
|
||||||
|
|
||||||
|
export function spawnModalCssVariableEditor() {
|
||||||
|
/* FIXME: Disable for the native client! */
|
||||||
|
const events = new Registry<CssEditorEvents>();
|
||||||
|
cssVariableEditorController(events);
|
||||||
|
|
||||||
|
const modal = spawnExternalModal("css-editor", events, {});
|
||||||
|
modal.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cssVariableEditorController(events: Registry<CssEditorEvents>) {
|
||||||
|
events.on("query_css_variables", () => {
|
||||||
|
events.fire_async("notify_css_variables", {
|
||||||
|
variables: cssVariableManager.getAllCssVariables()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
events.on("action_override_toggle", event => {
|
||||||
|
cssVariableManager.toggleCustomVariable(event.variableName, event.enabled, event.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
events.on("action_change_override_value", event => {
|
||||||
|
cssVariableManager.setVariable(event.variableName, event.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
events.on("action_export", () => {
|
||||||
|
events.fire_async("notify_export_result", {
|
||||||
|
config: cssVariableManager.exportConfig()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
events.on("action_import", event => {
|
||||||
|
try {
|
||||||
|
cssVariableManager.importConfig(event.config);
|
||||||
|
events.fire_async("notify_import_result", { success: true });
|
||||||
|
events.fire_async("action_select_entry", { variable: undefined });
|
||||||
|
events.fire_async("query_css_variables");
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to import CSS variable values: %o", error);
|
||||||
|
events.fire_async("notify_import_result", { success: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
events.on("action_reset", () => {
|
||||||
|
cssVariableManager.reset();
|
||||||
|
events.fire_async("action_select_entry", { variable: undefined });
|
||||||
|
events.fire_async("query_css_variables");
|
||||||
|
});
|
||||||
|
|
||||||
|
events.on("action_randomize", () => {
|
||||||
|
cssVariableManager.randomize();
|
||||||
|
events.fire_async("action_select_entry", { variable: undefined });
|
||||||
|
events.fire_async("query_css_variables");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
|
priority: 10,
|
||||||
|
name: "CSS Variable setup",
|
||||||
|
function: async () => {
|
||||||
|
cssVariableManager = new CssVariableManager();
|
||||||
|
cssVariableManager.initialize();
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,29 @@
|
||||||
|
export interface CssVariable {
|
||||||
|
name: string;
|
||||||
|
defaultValue: string;
|
||||||
|
overwriteValue: boolean;
|
||||||
|
|
||||||
|
customValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CssEditorUserData {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CssEditorEvents {
|
||||||
|
action_set_filter: { filter: string | undefined },
|
||||||
|
action_select_entry: { variable: CssVariable },
|
||||||
|
action_override_toggle: { variableName: string, enabled: boolean, value?: string }
|
||||||
|
action_change_override_value: { variableName: string, value: string },
|
||||||
|
action_reset: { },
|
||||||
|
action_randomize: {},
|
||||||
|
|
||||||
|
action_export: {},
|
||||||
|
action_import: { config: string }
|
||||||
|
|
||||||
|
query_css_variables: {},
|
||||||
|
notify_css_variables: { variables: CssVariable[] }
|
||||||
|
|
||||||
|
notify_export_result: { config: string },
|
||||||
|
notify_import_result: { success: boolean }
|
||||||
|
}
|
|
@ -0,0 +1,259 @@
|
||||||
|
@import "../../../../css/static/mixin";
|
||||||
|
@import "../../../../css/static/properties";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
background: #19191b;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
min-height: 10em;
|
||||||
|
min-width: 20em;
|
||||||
|
|
||||||
|
.containerList, .containerEdit {
|
||||||
|
width: 50%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
a {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e0e0e0;
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
font-size: 1.05em;
|
||||||
|
min-width: 5em;
|
||||||
|
|
||||||
|
line-height: normal;
|
||||||
|
|
||||||
|
@include text-dotdotdot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.containerList {
|
||||||
|
.list {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
min-height: 8em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
border-radius: .2em;
|
||||||
|
border: 1px solid #1f2122;
|
||||||
|
background-color: #28292b;
|
||||||
|
|
||||||
|
.search {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
padding: 0 .5em;
|
||||||
|
border-top: 1px solid #1f2122;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
min-width: 5em;
|
||||||
|
|
||||||
|
margin-left: 1em;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
min-height: 5em;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
@include chat-scrollbar-vertical();
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2em;
|
||||||
|
|
||||||
|
color: #4d4d4d;
|
||||||
|
background-color: #28292b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variable {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
min-width: 4em;
|
||||||
|
padding-left: .5em;
|
||||||
|
padding-right: .5em;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
@include text-dotdotdot();
|
||||||
|
color: #999;
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
align-self: center;
|
||||||
|
margin-right: .5em;
|
||||||
|
|
||||||
|
height: 1em;
|
||||||
|
width: 1em;
|
||||||
|
|
||||||
|
border-right: .1em;
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
|
.color {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
background-color: #28292b; /* default value if the value is not a color or broken */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #2c2d2f;
|
||||||
|
|
||||||
|
.preview .color {
|
||||||
|
background-color: #2c2d2f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: #1a1a1b;
|
||||||
|
|
||||||
|
.preview .color {
|
||||||
|
background-color: #1a1a1b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.containerEdit {
|
||||||
|
margin-left: 2em;
|
||||||
|
|
||||||
|
.detail {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
|
||||||
|
.title, .value {
|
||||||
|
@include text-dotdotdot();
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #557edc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: #999;
|
||||||
|
|
||||||
|
&.color {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
.colorButton {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
margin-left: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
min-width: 5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorButton {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
padding: .5em;
|
||||||
|
border-radius: .2em;
|
||||||
|
border: 1px solid #111112;
|
||||||
|
background-color: #121213;
|
||||||
|
|
||||||
|
height: 2em;
|
||||||
|
width: 2em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
input {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
margin-top: auto;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
margin-left: 1em;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonReset {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,421 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import {CssEditorEvents, CssEditorUserData, CssVariable} from "tc-shared/ui/modal/css-editor/Definitions";
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
import {AbstractModal} from "tc-shared/ui/react-elements/Modal";
|
||||||
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
|
import {BoxedInputField, FlatInputField} from "tc-shared/ui/react-elements/InputField";
|
||||||
|
import {useState} from "react";
|
||||||
|
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||||
|
import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
|
||||||
|
import 'rc-color-picker/assets/index.css';
|
||||||
|
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||||
|
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
|
||||||
|
|
||||||
|
const cssStyle = require("./Renderer.scss");
|
||||||
|
|
||||||
|
const CssVariableRenderer = React.memo((props: { events: Registry<CssEditorEvents>, variable: CssVariable, selected: boolean }) => {
|
||||||
|
const [ selected, setSelected ] = useState(props.selected);
|
||||||
|
const [ override, setOverride ] = useState(props.variable.overwriteValue);
|
||||||
|
const [ overrideColor, setOverrideColor ] = useState(props.variable.customValue);
|
||||||
|
|
||||||
|
props.events.reactUse("action_select_entry", event => setSelected(event.variable === props.variable));
|
||||||
|
props.events.reactUse("action_override_toggle", event => {
|
||||||
|
if(event.variableName !== props.variable.name)
|
||||||
|
return;
|
||||||
|
|
||||||
|
setOverride(event.enabled);
|
||||||
|
if(event.enabled)
|
||||||
|
setOverrideColor(event.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
props.events.reactUse("action_change_override_value", event => {
|
||||||
|
if(event.variableName !== props.variable.name)
|
||||||
|
return;
|
||||||
|
|
||||||
|
setOverrideColor(event.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cssStyle.variable + " " + (selected ? cssStyle.selected : "")}
|
||||||
|
onClick={() => {
|
||||||
|
if(selected)
|
||||||
|
return;
|
||||||
|
|
||||||
|
props.events.fire("action_select_entry", { variable: props.variable })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={cssStyle.preview}>
|
||||||
|
<div
|
||||||
|
className={cssStyle.color}
|
||||||
|
style={{ backgroundColor: props.variable.defaultValue }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.preview}>
|
||||||
|
<div
|
||||||
|
className={cssStyle.color}
|
||||||
|
style={{ backgroundColor: override ? overrideColor : undefined }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<a>{props.variable.name}</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const CssVariableListBodyRenderer = (props: { events: Registry<CssEditorEvents> }) => {
|
||||||
|
const [ variables, setVariables ] = useState<"loading" | CssVariable[]>(() => {
|
||||||
|
props.events.fire_async("query_css_variables");
|
||||||
|
return "loading";
|
||||||
|
});
|
||||||
|
|
||||||
|
const [ filter, setFilter ] = useState(undefined);
|
||||||
|
const [ selectedVariable, setSelectedVariable ] = useState(undefined);
|
||||||
|
|
||||||
|
props.events.reactUse("action_select_entry", event => setSelectedVariable(event.variable));
|
||||||
|
props.events.reactUse("query_css_variables", () => setVariables("loading"));
|
||||||
|
|
||||||
|
let content;
|
||||||
|
if(variables === "loading") {
|
||||||
|
content = (
|
||||||
|
<div className={cssStyle.overlay} key={"loading"}>
|
||||||
|
<a>
|
||||||
|
<Translatable>Loading</Translatable>
|
||||||
|
<LoadingDots />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
content = [];
|
||||||
|
for(const variable of variables) {
|
||||||
|
if(filter && variable.name.toLowerCase().indexOf(filter) === -1)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
content.push(<CssVariableRenderer
|
||||||
|
key={"variable-" + variable.name}
|
||||||
|
events={props.events}
|
||||||
|
variable={variable}
|
||||||
|
selected={selectedVariable === variable.name}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(content.length === 0) {
|
||||||
|
content.push(
|
||||||
|
<div className={cssStyle.overlay} key={"no-match"}>
|
||||||
|
<a><Translatable>No variable matched your filter</Translatable></a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
props.events.reactUse("action_set_filter", event => setFilter(event.filter?.toLowerCase()));
|
||||||
|
props.events.reactUse("notify_css_variables", event => setVariables(event.variables));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.body} onKeyPress={event => {
|
||||||
|
if(variables === "loading")
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* TODO: This isn't working since the div isn't focused properly yet */
|
||||||
|
let offset = 0;
|
||||||
|
if(event.key === "ArrowDown") {
|
||||||
|
offset = 1;
|
||||||
|
} else if(event.key === "ArrowUp") {
|
||||||
|
offset = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(offset !== 0) {
|
||||||
|
const selectIndex = variables.findIndex(e => e === selectedVariable);
|
||||||
|
if(selectIndex === -1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const variable = variables[selectIndex + offset];
|
||||||
|
if(!variable)
|
||||||
|
return;
|
||||||
|
|
||||||
|
props.events.fire("action_select_entry", { variable: variable });
|
||||||
|
}
|
||||||
|
}} tabIndex={0}>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CssVariableListSearchRenderer = (props: { events: Registry<CssEditorEvents> }) => {
|
||||||
|
const [ isLoading, setLoading ] = useState(true);
|
||||||
|
|
||||||
|
props.events.reactUse("notify_css_variables", () => setLoading(false));
|
||||||
|
props.events.reactUse("query_css_variables", () => setLoading(true));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.search}>
|
||||||
|
<FlatInputField
|
||||||
|
label={<Translatable>Filter variables</Translatable>}
|
||||||
|
labelType={"floating"}
|
||||||
|
className={cssStyle.input}
|
||||||
|
onInput={text => props.events.fire("action_set_filter", { filter: text })}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CssVariableListRenderer = (props: { events: Registry<CssEditorEvents> }) => (
|
||||||
|
<div className={cssStyle.containerList}>
|
||||||
|
<div className={cssStyle.header}>
|
||||||
|
<a><Translatable>CSS Variable list</Translatable></a>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.list} onKeyPress={event => console.error(event.key)}>
|
||||||
|
<CssVariableListBodyRenderer events={props.events} />
|
||||||
|
<CssVariableListSearchRenderer events={props.events} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const SelectedVariableInfo = (props: { events: Registry<CssEditorEvents> }) => {
|
||||||
|
const [ selectedVariable, setSelectedVariable ] = useState<CssVariable>(undefined);
|
||||||
|
props.events.reactUse("action_select_entry", event => setSelectedVariable(event.variable));
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<div className={cssStyle.detail}>
|
||||||
|
<div className={cssStyle.title}>
|
||||||
|
<Translatable>Name</Translatable>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.value}>
|
||||||
|
<BoxedInputField
|
||||||
|
editable={false}
|
||||||
|
value={selectedVariable ? selectedVariable.name : "-"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.detail}>
|
||||||
|
<div className={cssStyle.title}>
|
||||||
|
<Translatable>Default Value</Translatable>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.value}>
|
||||||
|
<BoxedInputField
|
||||||
|
editable={false}
|
||||||
|
value={selectedVariable ? selectedVariable.defaultValue : "-"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OverrideVariableInfo = (props: { events: Registry<CssEditorEvents> }) => {
|
||||||
|
const [ selectedVariable, setSelectedVariable ] = useState<CssVariable>(undefined);
|
||||||
|
const [ overwriteValue, setOverwriteValue ] = useState<string>(undefined);
|
||||||
|
const [ overwriteEnabled, setOverwriteEnabled ] = useState(false);
|
||||||
|
|
||||||
|
props.events.reactUse("action_select_entry", event => {
|
||||||
|
setSelectedVariable(event.variable);
|
||||||
|
setOverwriteEnabled(event.variable?.overwriteValue);
|
||||||
|
setOverwriteValue(event.variable?.customValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
props.events.reactUse("action_override_toggle", event => {
|
||||||
|
if(event.variableName !== selectedVariable?.name)
|
||||||
|
return;
|
||||||
|
|
||||||
|
selectedVariable.overwriteValue = event.enabled;
|
||||||
|
setOverwriteEnabled(event.enabled);
|
||||||
|
if(event.enabled)
|
||||||
|
setOverwriteValue(event.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
props.events.reactUse("action_change_override_value", event => {
|
||||||
|
if(event.variableName !== selectedVariable?.name)
|
||||||
|
return;
|
||||||
|
|
||||||
|
setOverwriteValue(event.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<div className={cssStyle.detail}>
|
||||||
|
<div className={cssStyle.title}>
|
||||||
|
<Translatable>Override Value</Translatable>
|
||||||
|
<Checkbox
|
||||||
|
value={overwriteEnabled}
|
||||||
|
disabled={!selectedVariable}
|
||||||
|
onChange={value => {
|
||||||
|
props.events.fire("action_override_toggle", {
|
||||||
|
variableName: selectedVariable.name,
|
||||||
|
value: typeof selectedVariable.customValue === "string" ? selectedVariable.customValue : selectedVariable.defaultValue,
|
||||||
|
enabled: value
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.value + " " + cssStyle.color}>
|
||||||
|
<BoxedInputField
|
||||||
|
className={cssStyle.input}
|
||||||
|
disabled={!overwriteEnabled}
|
||||||
|
value={overwriteValue || " "}
|
||||||
|
onInput={text => {
|
||||||
|
selectedVariable.customValue = text;
|
||||||
|
props.events.fire("action_change_override_value", { value: text, variableName: selectedVariable.name });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CssVariableColorPicker events={props.events} selectedVariable={selectedVariable} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CssVariableColorPicker = (props: { events: Registry<CssEditorEvents>, selectedVariable: CssVariable }) => {
|
||||||
|
const [ overwriteValue, setOverwriteValue ] = useState<string>(undefined);
|
||||||
|
const [ overwriteEnabled, setOverwriteEnabled ] = useState(false);
|
||||||
|
|
||||||
|
props.events.reactUse("action_override_toggle", event => {
|
||||||
|
if(event.variableName !== props.selectedVariable?.name)
|
||||||
|
return;
|
||||||
|
|
||||||
|
props.selectedVariable.overwriteValue = event.enabled;
|
||||||
|
setOverwriteEnabled(event.enabled);
|
||||||
|
if(event.enabled)
|
||||||
|
setOverwriteValue(event.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
props.events.reactUse("action_change_override_value", event => {
|
||||||
|
if(event.variableName !== props.selectedVariable?.name || 'cpInvoker' in event)
|
||||||
|
return;
|
||||||
|
|
||||||
|
setOverwriteValue(event.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
let currentInput: string;
|
||||||
|
let inputTimeout: number;
|
||||||
|
return (
|
||||||
|
<label className={cssStyle.colorButton} >
|
||||||
|
<input
|
||||||
|
disabled={!overwriteEnabled}
|
||||||
|
type={"color"}
|
||||||
|
value={overwriteValue}
|
||||||
|
onChange={event => {
|
||||||
|
currentInput = event.target.value;
|
||||||
|
if(inputTimeout)
|
||||||
|
return;
|
||||||
|
|
||||||
|
inputTimeout = setTimeout(() => {
|
||||||
|
inputTimeout = undefined;
|
||||||
|
props.events.fire("action_change_override_value", { value: currentInput, variableName: props.selectedVariable.name });
|
||||||
|
}, 150);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<a className="rainbow-letter" style={{ borderBottomColor: overwriteValue }}>C</a>
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
const ControlButtons = (props: { events: Registry<CssEditorEvents> }) => {
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.buttons}>
|
||||||
|
<Button
|
||||||
|
color={"blue"}
|
||||||
|
type={"normal"}
|
||||||
|
className={cssStyle.button}
|
||||||
|
onClick={() => props.events.fire("action_randomize")}
|
||||||
|
><Translatable>Randomize</Translatable></Button>
|
||||||
|
<Button
|
||||||
|
color={"red"}
|
||||||
|
type={"normal"}
|
||||||
|
className={cssStyle.button + " " + cssStyle.buttonReset}
|
||||||
|
onClick={() => props.events.fire("action_reset")}
|
||||||
|
><Translatable>Reset</Translatable></Button>
|
||||||
|
<Button
|
||||||
|
color={"blue"}
|
||||||
|
type={"normal"}
|
||||||
|
className={cssStyle.button}
|
||||||
|
onClick={() => props.events.fire("action_export")}
|
||||||
|
><Translatable>Export</Translatable></Button>
|
||||||
|
<Button
|
||||||
|
color={"green"}
|
||||||
|
type={"normal"}
|
||||||
|
className={cssStyle.button}
|
||||||
|
onClick={() => requestFileAsText().then(content => {
|
||||||
|
props.events.fire("action_import", { config: content })
|
||||||
|
})}
|
||||||
|
><Translatable>Import</Translatable></Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
const CssVariableEditor = (props: { events: Registry<CssEditorEvents> }) => {
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.containerEdit}>
|
||||||
|
<div className={cssStyle.header}>
|
||||||
|
<a><Translatable>Variable details</Translatable></a>
|
||||||
|
</div>
|
||||||
|
<SelectedVariableInfo events={props.events} />
|
||||||
|
<OverrideVariableInfo events={props.events} />
|
||||||
|
<ControlButtons events={props.events} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
const downloadTextAsFile = (text, name) => {
|
||||||
|
const element = document.createElement("a");
|
||||||
|
element.text = "download";
|
||||||
|
element.href = "data:test/plain;charset=utf-8," + encodeURIComponent(text);
|
||||||
|
element.download = name;
|
||||||
|
element.style.display = "none";
|
||||||
|
|
||||||
|
document.body.appendChild(element);
|
||||||
|
element.click();
|
||||||
|
element.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestFileAsText = async (): Promise<string> => {
|
||||||
|
const element = document.createElement("input");
|
||||||
|
element.style.display = "none";
|
||||||
|
element.type = "file";
|
||||||
|
|
||||||
|
document.body.appendChild(element);
|
||||||
|
element.click();
|
||||||
|
await new Promise(resolve => {
|
||||||
|
element.onchange = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(element.files.length !== 1)
|
||||||
|
return undefined;
|
||||||
|
const file = element.files[0];
|
||||||
|
element.remove();
|
||||||
|
|
||||||
|
return await file.text();
|
||||||
|
};
|
||||||
|
|
||||||
|
class PopoutConversationUI extends AbstractModal {
|
||||||
|
private readonly events: Registry<CssEditorEvents>;
|
||||||
|
private readonly userData: CssEditorUserData;
|
||||||
|
|
||||||
|
constructor(events: Registry<CssEditorEvents>, userData: CssEditorUserData) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.userData = userData;
|
||||||
|
this.events = events;
|
||||||
|
|
||||||
|
this.events.on("notify_export_result", event => {
|
||||||
|
createInfoModal(tr("Config exported successfully"), tr("The config has been exported successfully.")).open();
|
||||||
|
downloadTextAsFile(event.config, "teaweb-style.json");
|
||||||
|
});
|
||||||
|
this.events.on("notify_import_result", event => {
|
||||||
|
if(event.success)
|
||||||
|
createInfoModal(tr("Config imported successfully"), tr("The config has been imported successfully.")).open();
|
||||||
|
else
|
||||||
|
createErrorModal(tr("Config imported failed"), tr("The config import has been failed.")).open();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBody() {
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.container}>
|
||||||
|
<CssVariableListRenderer events={this.events} />
|
||||||
|
<CssVariableEditor events={this.events} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
title() {
|
||||||
|
return "CSS Variable editor";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export = PopoutConversationUI;
|
|
@ -281,8 +281,8 @@ class PermissionEditorModal extends Modal {
|
||||||
this.defaultTab = defaultTab;
|
this.defaultTab = defaultTab;
|
||||||
this.defaultTabValues = defaultTabValues || {};
|
this.defaultTabValues = defaultTabValues || {};
|
||||||
|
|
||||||
this.modalEvents.enable_debug("modal-permissions");
|
this.modalEvents.enableDebug("modal-permissions");
|
||||||
this.editorEvents.enable_debug("permissions-editor");
|
this.editorEvents.enableDebug("permissions-editor");
|
||||||
|
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
initializePermissionModalResultHandlers(this.modalEvents);
|
initializePermissionModalResultHandlers(this.modalEvents);
|
||||||
|
|
|
@ -190,8 +190,8 @@ class FileTransferModal extends Modal {
|
||||||
|
|
||||||
this.defaultChannelId = defaultChannelId;
|
this.defaultChannelId = defaultChannelId;
|
||||||
|
|
||||||
this.remoteBrowseEvents.enable_debug("remote-file-browser");
|
this.remoteBrowseEvents.enableDebug("remote-file-browser");
|
||||||
this.transferInfoEvents.enable_debug("transfer-info");
|
this.transferInfoEvents.enableDebug("transfer-info");
|
||||||
|
|
||||||
initializeRemoteFileBrowserController(server_connections.active_connection(), this.remoteBrowseEvents);
|
initializeRemoteFileBrowserController(server_connections.active_connection(), this.remoteBrowseEvents);
|
||||||
initializeTransferInfoController(server_connections.active_connection(), this.transferInfoEvents);
|
initializeTransferInfoController(server_connections.active_connection(), this.transferInfoEvents);
|
||||||
|
|
|
@ -8,51 +8,54 @@ export const AvatarRenderer = React.memo((props: { avatar: ClientAvatar, classNa
|
||||||
let [ revision, setRevision ] = useState(0);
|
let [ revision, setRevision ] = useState(0);
|
||||||
|
|
||||||
let image;
|
let image;
|
||||||
switch (props.avatar.state) {
|
switch (props.avatar?.getState()) {
|
||||||
case "unset":
|
case "unset":
|
||||||
image = <img
|
image = <img
|
||||||
key={"default"}
|
key={"default"}
|
||||||
title={tr("default avatar")}
|
title={tr("default avatar")}
|
||||||
alt={typeof props.alt === "string" ? props.alt : tr("default avatar")}
|
alt={typeof props.alt === "string" ? props.alt : tr("default avatar")}
|
||||||
src={props.avatar.avatarUrl}
|
src={props.avatar.getAvatarUrl()}
|
||||||
style={ImageStyle}
|
style={ImageStyle}
|
||||||
onClick={event => {
|
onClick={event => {
|
||||||
if(event.isDefaultPrevented())
|
if(event.isDefaultPrevented())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
image_preview.preview_image(props.avatar.avatarUrl, undefined);
|
image_preview.preview_image(props.avatar.getAvatarUrl(), undefined);
|
||||||
}}
|
}}
|
||||||
/>;
|
/>;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "loaded":
|
case "loaded":
|
||||||
image = <img
|
image = <img
|
||||||
key={"user-" + props.avatar.currentAvatarHash}
|
key={"user-" + props.avatar.getAvatarHash()}
|
||||||
alt={typeof props.alt === "string" ? props.alt : tr("user avatar")}
|
alt={typeof props.alt === "string" ? props.alt : tr("user avatar")}
|
||||||
title={tr("user avatar")}
|
title={tr("user avatar")}
|
||||||
src={props.avatar.avatarUrl}
|
src={props.avatar.getAvatarUrl()}
|
||||||
style={ImageStyle}
|
style={ImageStyle}
|
||||||
onClick={event => {
|
onClick={event => {
|
||||||
if(event.isDefaultPrevented())
|
if(event.isDefaultPrevented())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
image_preview.preview_image(props.avatar.avatarUrl, undefined);
|
image_preview.preview_image(props.avatar.getAvatarUrl(), undefined);
|
||||||
}}
|
}}
|
||||||
/>;
|
/>;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "errored":
|
case "errored":
|
||||||
image = <img key={"error"} alt={typeof props.alt === "string" ? props.alt : tr("error")} title={tr("avatar failed to load:\n") + props.avatar.loadError} src={props.avatar.avatarUrl} style={ImageStyle} />;
|
image = <img key={"error"} alt={typeof props.alt === "string" ? props.alt : tr("error")} title={tr("avatar failed to load:\n") + props.avatar.getLoadError()} src={props.avatar.getAvatarUrl()} style={ImageStyle} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "loading":
|
case "loading":
|
||||||
image = <img key={"loading"} alt={typeof props.alt === "string" ? props.alt : tr("loading")} title={tr("loading avatar")} src={"img/loading_image.svg"} style={ImageStyle} />;
|
image = <img key={"loading"} alt={typeof props.alt === "string" ? props.alt : tr("loading")} title={tr("loading avatar")} src={"img/loading_image.svg"} style={ImageStyle} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case undefined:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
props.avatar.events.reactUse("avatar_state_changed", () => setRevision(revision + 1));
|
props.avatar?.events.reactUse("avatar_state_changed", () => setRevision(revision + 1));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={props.className} style={{ overflow: "hidden" }}>
|
<div className={props.className} style={{ overflow: "hidden" }}>
|
||||||
|
|
|
@ -5,7 +5,7 @@ html:root {
|
||||||
--checkbox-checkmark: #46c0ec;
|
--checkbox-checkmark: #46c0ec;
|
||||||
|
|
||||||
--checkbox-background: #272626;
|
--checkbox-background: #272626;
|
||||||
--checkbox-disabled-background: #46c0ec;
|
--checkbox-disabled-background: #1a1a1e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
|
|
|
@ -6,6 +6,8 @@ export interface CheckboxProperties {
|
||||||
label?: ReactElement | string;
|
label?: ReactElement | string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onChange?: (value: boolean) => void;
|
onChange?: (value: boolean) => void;
|
||||||
|
|
||||||
|
value?: boolean;
|
||||||
initialValue?: boolean;
|
initialValue?: boolean;
|
||||||
|
|
||||||
children?: never;
|
children?: never;
|
||||||
|
@ -20,21 +22,18 @@ export class Checkbox extends React.Component<CheckboxProperties, CheckboxState>
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = { };
|
||||||
checked: this.props.initialValue,
|
|
||||||
disabled: this.props.disabled
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const disabled = typeof this.state.disabled === "boolean" ? this.state.disabled : this.props.disabled;
|
const disabled = typeof this.state.disabled === "boolean" ? this.state.disabled : this.props.disabled;
|
||||||
const checked = typeof this.state.checked === "boolean" ? this.state.checked : this.props.initialValue;
|
const checked = typeof this.props.value === "boolean" ? this.props.value : typeof this.state.checked === "boolean" ? this.state.checked : this.props.initialValue;
|
||||||
const disabledClass = disabled ? cssStyle.disabled : "";
|
const disabledClass = disabled ? cssStyle.disabled : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className={cssStyle.labelCheckbox + " " + disabledClass}>
|
<label className={cssStyle.labelCheckbox + " " + disabledClass}>
|
||||||
<div className={cssStyle.checkbox + " " + disabledClass}>
|
<div className={cssStyle.checkbox + " " + disabledClass}>
|
||||||
<input type={"checkbox"} checked={checked} disabled={disabled} onChange={() => this.onStateChange()} />
|
<input type={"checkbox"} checked={checked} disabled={disabled} onChange={event => this.onStateChange(event)} />
|
||||||
<div className={cssStyle.mark} />
|
<div className={cssStyle.mark} />
|
||||||
</div>
|
</div>
|
||||||
{this.props.label ? <a>{this.props.label}</a> : undefined}
|
{this.props.label ? <a>{this.props.label}</a> : undefined}
|
||||||
|
@ -42,9 +41,9 @@ export class Checkbox extends React.Component<CheckboxProperties, CheckboxState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onStateChange() {
|
private onStateChange(event: React.ChangeEvent) {
|
||||||
if(this.props.onChange)
|
if(this.props.onChange)
|
||||||
this.props.onChange(!this.state.checked);
|
this.props.onChange((event.target as HTMLInputElement).checked);
|
||||||
|
|
||||||
this.setState({ checked: !this.state.checked });
|
this.setState({ checked: !this.state.checked });
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,9 @@ html:root {
|
||||||
|
|
||||||
--boxed-input-field-disabled-background: #1a1819;
|
--boxed-input-field-disabled-background: #1a1819;
|
||||||
|
|
||||||
--boxed-input-field-focus-border: #111112;
|
--boxed-input-field-focus-border: #284262;
|
||||||
--boxed-input-field-focus-background: #121213;
|
--boxed-input-field-focus-background: #131b22;
|
||||||
--boxed-input-field-focus-text: #b3b3b3;
|
--boxed-input-field-focus-text: #e1e2e3;
|
||||||
|
|
||||||
--boxed-input-field-invalid-border: #721c1c;
|
--boxed-input-field-invalid-border: #721c1c;
|
||||||
--boxed-input-field-invalid-background: #180d0d;
|
--boxed-input-field-invalid-background: #180d0d;
|
||||||
|
@ -73,17 +73,19 @@ html:root {
|
||||||
background-image: unset!important;
|
background-image: unset!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus, &:focus-within {
|
&.editable {
|
||||||
background-color: var(--boxed-input-field-focus-background);
|
&:focus, &:focus-within {
|
||||||
border-color: var(--boxed-input-field-focus-border);
|
background-color: var(--boxed-input-field-focus-background);
|
||||||
|
border-color: var(--boxed-input-field-focus-border);
|
||||||
|
|
||||||
color: var(--boxed-input-field-focus-text);
|
color: var(--boxed-input-field-focus-text);
|
||||||
|
|
||||||
.prefix {
|
.prefix {
|
||||||
width: 0;
|
width: 0;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,7 +99,7 @@ html:root {
|
||||||
outline: none;
|
outline: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
//color: #b3b3b3;
|
color: inherit;
|
||||||
min-width: 2em;
|
min-width: 2em;
|
||||||
|
|
||||||
&.editable {
|
&.editable {
|
||||||
|
|
|
@ -10,6 +10,7 @@ export interface BoxedInputFieldProperties {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
|
|
||||||
|
value?: string;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
|
|
||||||
rightIcon?: () => ReactElement;
|
rightIcon?: () => ReactElement;
|
||||||
|
@ -52,9 +53,10 @@ export class BoxedInputField extends React.Component<BoxedInputFieldProperties,
|
||||||
draggable={false}
|
draggable={false}
|
||||||
className={
|
className={
|
||||||
cssStyle.containerBoxed + " " +
|
cssStyle.containerBoxed + " " +
|
||||||
cssStyle["size-" + (this.props.size || "normal")] +
|
cssStyle["size-" + (this.props.size || "normal")] + " " +
|
||||||
(this.state.disabled || this.props.disabled ? cssStyle.disabled : "") + " " +
|
(this.state.disabled || this.props.disabled ? cssStyle.disabled : "") + " " +
|
||||||
(this.state.isInvalid || this.props.isInvalid ? cssStyle.isInvalid : "") + " " +
|
(this.state.isInvalid || this.props.isInvalid ? cssStyle.isInvalid : "") + " " +
|
||||||
|
(typeof this.props.editable !== "boolean" || this.props.editable ? cssStyle.editable : "") + " " +
|
||||||
(this.props.leftIcon ? "" : cssStyle.noLeftIcon) + " " +
|
(this.props.leftIcon ? "" : cssStyle.noLeftIcon) + " " +
|
||||||
(this.props.rightIcon ? "" : cssStyle.noRightIcon) + " " +
|
(this.props.rightIcon ? "" : cssStyle.noRightIcon) + " " +
|
||||||
this.props.className
|
this.props.className
|
||||||
|
@ -69,10 +71,10 @@ export class BoxedInputField extends React.Component<BoxedInputFieldProperties,
|
||||||
<span key={"custom-input"} className={cssStyle.inputBox + " " + (this.props.editable ? cssStyle.editable : "")} onClick={this.props.onFocus}>{this.props.inputBox()}</span> :
|
<span key={"custom-input"} className={cssStyle.inputBox + " " + (this.props.editable ? cssStyle.editable : "")} onClick={this.props.onFocus}>{this.props.inputBox()}</span> :
|
||||||
<input key={"input"}
|
<input key={"input"}
|
||||||
ref={this.refInput}
|
ref={this.refInput}
|
||||||
value={this.state.value}
|
value={this.props.value || this.state.value}
|
||||||
defaultValue={this.state.defaultValue || this.props.defaultValue}
|
defaultValue={this.state.defaultValue || this.props.defaultValue}
|
||||||
placeholder={this.props.placeholder}
|
placeholder={this.props.placeholder}
|
||||||
readOnly={typeof this.props.editable === "boolean" ? this.props.editable : false}
|
readOnly={typeof this.props.editable === "boolean" ? !this.props.editable : false}
|
||||||
disabled={this.state.disabled || this.props.disabled}
|
disabled={this.state.disabled || this.props.disabled}
|
||||||
onInput={this.props.onInput && (event => this.props.onInput(event.currentTarget.value))}
|
onInput={this.props.onInput && (event => this.props.onInput(event.currentTarget.value))}
|
||||||
onKeyDown={e => this.onKeyDown(e)}
|
onKeyDown={e => this.onKeyDown(e)}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
export const LoadingDots = (props: { maxDots?: number, speed?: number }) => {
|
export const LoadingDots = (props: { maxDots?: number, speed?: number, textOnly?: boolean }) => {
|
||||||
let { maxDots, speed } = props;
|
let { maxDots, speed } = props;
|
||||||
if(!maxDots || maxDots < 1)
|
if(!maxDots || maxDots < 1)
|
||||||
maxDots = 3;
|
maxDots = 3;
|
||||||
|
@ -16,5 +16,8 @@ export const LoadingDots = (props: { maxDots?: number, speed?: number }) => {
|
||||||
let result = ".";
|
let result = ".";
|
||||||
for(let index = 0; index < dots % maxDots; index++)
|
for(let index = 0; index < dots % maxDots; index++)
|
||||||
result += ".";
|
result += ".";
|
||||||
|
|
||||||
|
if(props.textOnly)
|
||||||
|
return <>{result}</>;
|
||||||
return <div style={{ width: (maxDots / 3) + "em", display: "inline-block", textAlign: "left" }}>{result}</div>;
|
return <div style={{ width: (maxDots / 3) + "em", display: "inline-block", textAlign: "left" }}>{result}</div>;
|
||||||
};
|
};
|
|
@ -101,26 +101,30 @@ export class ModalController<InstanceType extends Modal = Modal> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class Modal {
|
export abstract class AbstractModal {
|
||||||
private __modal_controller: ModalController;
|
protected constructor() {}
|
||||||
public constructor() {}
|
|
||||||
|
|
||||||
type() : ModalType { return "none"; }
|
|
||||||
abstract renderBody() : ReactElement;
|
abstract renderBody() : ReactElement;
|
||||||
abstract title() : string | React.ReactElement<Translatable>;
|
abstract title() : string | React.ReactElement<Translatable>;
|
||||||
|
|
||||||
|
protected onInitialize() {}
|
||||||
|
protected onDestroy() {}
|
||||||
|
|
||||||
|
protected onClose() {}
|
||||||
|
protected onOpen() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class Modal extends AbstractModal {
|
||||||
|
private __modal_controller: ModalController;
|
||||||
|
|
||||||
|
type() : ModalType { return "none"; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will only return a modal controller when the modal has not been destroyed
|
* Will only return a modal controller when the modal has not been destroyed
|
||||||
*/
|
*/
|
||||||
modalController() : ModalController | undefined {
|
modalController() : ModalController | undefined {
|
||||||
return this.__modal_controller;
|
return this.__modal_controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onInitialize() {}
|
|
||||||
protected onDestroy() {}
|
|
||||||
|
|
||||||
protected onClose() {}
|
|
||||||
protected onOpen() {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ModalImpl extends React.PureComponent<{ controller: ModalController }, { show: boolean }> {
|
class ModalImpl extends React.PureComponent<{ controller: ModalController }, { show: boolean }> {
|
||||||
|
|
|
@ -1,6 +1,28 @@
|
||||||
@import "../../../css/static/properties";
|
@import "../../../css/static/properties";
|
||||||
@import "../../../css/static/mixin";
|
@import "../../../css/static/mixin";
|
||||||
|
|
||||||
|
html:root {
|
||||||
|
--switch-background: #1c1c1c;
|
||||||
|
|
||||||
|
--switch-thumb-background: #3d3a3a;
|
||||||
|
--switch-thumb-shadow: rgba(0, 0, 0, 0.27);
|
||||||
|
|
||||||
|
--switch-dot-background: #a5a5a5;
|
||||||
|
--switch-dot-shadow: rgba(165, 165, 165, 0.4);
|
||||||
|
|
||||||
|
--switch-dot-checked-background: #46c0ec;
|
||||||
|
--switch-dot-checked-shadow: #46c0ec;
|
||||||
|
|
||||||
|
--switch-thumb-disabled-background: #252424;
|
||||||
|
--switch-thumb-disabled-shadow: #2f2d2d;
|
||||||
|
|
||||||
|
--switch-dot-disabled-background: #808080;
|
||||||
|
--switch-dot-disabled-shadow: rgba(102, 102, 102, 0.4);
|
||||||
|
|
||||||
|
--switch-dot-checked-disabled-background: #138db9;
|
||||||
|
--switch-dot-checked-disabled-shadow: #138db9;
|
||||||
|
}
|
||||||
|
|
||||||
/* general switch look */
|
/* general switch look */
|
||||||
.switch {
|
.switch {
|
||||||
$ball_outer_width: 1.5em; /* 1.5? */
|
$ball_outer_width: 1.5em; /* 1.5? */
|
||||||
|
@ -47,7 +69,7 @@
|
||||||
right: -$slider_border_size;
|
right: -$slider_border_size;
|
||||||
bottom: -$slider_border_size;
|
bottom: -$slider_border_size;
|
||||||
|
|
||||||
background-color: #1c1c1c;
|
background-color: var(--switch-background);
|
||||||
|
|
||||||
border: $slider_border_size solid #262628;
|
border: $slider_border_size solid #262628;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
@ -62,12 +84,12 @@
|
||||||
left: - $ball_outer_width / 2;
|
left: - $ball_outer_width / 2;
|
||||||
bottom: -($ball_outer_width - $slider_height) / 2;
|
bottom: -($ball_outer_width - $slider_height) / 2;
|
||||||
|
|
||||||
background-color: #3d3a3a;
|
background-color: var(--switch-thumb-background);
|
||||||
|
|
||||||
@include transition(.4s);
|
@include transition(.4s);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
||||||
box-shadow: 0 0 .2em 1px rgba(0, 0, 0, 0.27);
|
box-shadow: 0 0 .2em 1px var(--switch-thumb-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dot {
|
.dot {
|
||||||
|
@ -79,8 +101,8 @@
|
||||||
left: -($ball_inner_width / 2);
|
left: -($ball_inner_width / 2);
|
||||||
bottom: $slider_height / 2 - $ball_inner_width / 2;
|
bottom: $slider_height / 2 - $ball_inner_width / 2;
|
||||||
|
|
||||||
background-color: #a5a5a5;
|
background-color: var(--switch-dot-background);
|
||||||
box-shadow: 0 0 1em 1px rgba(165, 165, 165, 0.4);
|
box-shadow: 0 0 1em 1px var(--switch-dot-shadow);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
||||||
@include transition(.4s);
|
@include transition(.4s);
|
||||||
|
@ -99,8 +121,8 @@
|
||||||
|
|
||||||
.dot {
|
.dot {
|
||||||
@include transform(translateX($slider_width));
|
@include transform(translateX($slider_width));
|
||||||
background-color: #46c0ec;
|
background-color: var(--switch-dot-checked-background);
|
||||||
box-shadow: 0 0 1em 1px #46c0ec;
|
box-shadow: 0 0 1em 1px var(--switch-dot-checked-shadow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,23 +139,23 @@
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
.dot {
|
.dot {
|
||||||
background-color: #808080;
|
background-color: var(--switch-dot-disabled-background);
|
||||||
box-shadow: 0 0 1em 1px rgba(102, 102, 102, 0.4);
|
box-shadow: 0 0 1em 1px var(--switch-dot-disabled-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
input:checked + .slider {
|
input:checked + .slider {
|
||||||
.dot {
|
.dot {
|
||||||
background-color: #138db9;
|
background-color: var(--switch-dot-checked-disabled-background);
|
||||||
box-shadow: 0 0 1em 1px #138db9;
|
box-shadow: 0 0 1em 1px var(--switch-dot-checked-disabled-shadow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
background-color: #252424;
|
background-color: var(--switch-thumb-disabled-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider:before {
|
.slider:before {
|
||||||
background-color: #2f2d2d;
|
background-color: var(--switch-thumb-disabled-shadow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,10 +2,12 @@
|
||||||
|
|
||||||
html:root {
|
html:root {
|
||||||
--tooltip-background-color: #232222;
|
--tooltip-background-color: #232222;
|
||||||
|
--tooltip-color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
background-color: var(--tooltip-background-color);
|
background-color: var(--tooltip-background-color);
|
||||||
|
color: var(--tooltip-color);
|
||||||
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1000000;
|
z-index: 1000000;
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
import * as ipc from "tc-shared/ipc/BrowserIPC";
|
||||||
|
import {ChannelMessage, IPCChannel} from "tc-shared/ipc/BrowserIPC";
|
||||||
|
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
import {
|
||||||
|
EventControllerBase,
|
||||||
|
Popout2ControllerMessages,
|
||||||
|
PopoutIPCMessage
|
||||||
|
} from "tc-shared/ui/react-elements/external-modal/IPCMessage";
|
||||||
|
|
||||||
|
export class ExternalModalController extends EventControllerBase<"controller"> {
|
||||||
|
readonly modal: string;
|
||||||
|
readonly userData: any;
|
||||||
|
|
||||||
|
private currentWindow: Window;
|
||||||
|
private callbackWindowInitialized: (error?: string) => void;
|
||||||
|
|
||||||
|
private documentQuitListener: () => void;
|
||||||
|
|
||||||
|
constructor(modal: string, localEventRegistry: Registry<any>, userData: any) {
|
||||||
|
super(localEventRegistry);
|
||||||
|
|
||||||
|
this.modal = modal;
|
||||||
|
this.userData = userData;
|
||||||
|
|
||||||
|
this.ipcChannel = ipc.getInstance().createChannel();
|
||||||
|
this.ipcChannel.messageHandler = this.handleIPCMessage.bind(this);
|
||||||
|
|
||||||
|
this.documentQuitListener = () => this.currentWindow?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private trySpawnWindow() {
|
||||||
|
const parameters = {
|
||||||
|
"loader-target": "manifest",
|
||||||
|
"chunk": "modal-external",
|
||||||
|
"modal-target": this.modal,
|
||||||
|
"ipc-channel": this.ipcChannel.channelId,
|
||||||
|
"ipc-address": ipc.getInstance().getLocalAddress(),
|
||||||
|
"disableGlobalContextMenu": __build.mode === "debug" ? 1 : 0,
|
||||||
|
"loader-abort": __build.mode === "debug" ? 1 : 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const features = {
|
||||||
|
/*
|
||||||
|
status: "no",
|
||||||
|
location: "no",
|
||||||
|
toolbar: "no",
|
||||||
|
menubar: "no",
|
||||||
|
width: 600,
|
||||||
|
height: 400
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
|
||||||
|
let baseUrl = location.origin + location.pathname + "?";
|
||||||
|
return window.open(
|
||||||
|
baseUrl + Object.keys(parameters).map(e => e + "=" + encodeURIComponent(parameters[e])).join("&"),
|
||||||
|
"External Modal",
|
||||||
|
Object.keys(features).map(e => e + "=" + features[e]).join(",")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async open() {
|
||||||
|
this.currentWindow = this.trySpawnWindow();
|
||||||
|
if(!this.currentWindow) {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
spawnYesNo(tr("Would you like to open the popup?"), tra("Would you like to open popup {}?", this.modal), callback => {
|
||||||
|
if(!callback) {
|
||||||
|
reject("user aborted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentWindow = this.trySpawnWindow();
|
||||||
|
if(this.currentWindow) {
|
||||||
|
reject("Failed to spawn window");
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}).close_listener.push(() => reject("user aborted"));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.currentWindow) {
|
||||||
|
/* some shitty popup blocker or whatever */
|
||||||
|
throw "failed to create window";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentWindow.onclose = () => {
|
||||||
|
/* TODO: General handle */
|
||||||
|
window.removeEventListener("beforeunload", this.documentQuitListener);
|
||||||
|
};
|
||||||
|
window.addEventListener("beforeunload", this.documentQuitListener);
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
this.callbackWindowInitialized = undefined;
|
||||||
|
reject("window haven't called back");
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
this.callbackWindowInitialized = error => {
|
||||||
|
this.callbackWindowInitialized = undefined;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
error ? reject(error) : resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleIPCMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) {
|
||||||
|
if(broadcast)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(this.ipcRemoteId !== remoteId) {
|
||||||
|
this.ipcRemoteId = remoteId;
|
||||||
|
} else if(this.ipcRemoteId !== remoteId) {
|
||||||
|
console.warn("Remote window got a new id. Maybe reload?");
|
||||||
|
this.ipcRemoteId = remoteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
super.handleIPCMessage(remoteId, broadcast, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleTypedIPCMessage<T extends Popout2ControllerMessages>(type: T, payload: PopoutIPCMessage[T]) {
|
||||||
|
super.handleTypedIPCMessage(type, payload);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "hello-popout": {
|
||||||
|
const tpayload = payload as PopoutIPCMessage["hello-popout"];
|
||||||
|
console.log("Received Hello World from popup with version %s (expected %s).", tpayload.version, __build.version);
|
||||||
|
if(tpayload.version !== __build.version) {
|
||||||
|
this.sendIPCMessage("hello-controller", { accepted: false, message: tr("version miss match") });
|
||||||
|
if(this.callbackWindowInitialized) {
|
||||||
|
this.callbackWindowInitialized(tr("version miss match"));
|
||||||
|
this.callbackWindowInitialized = undefined;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.callbackWindowInitialized) {
|
||||||
|
this.callbackWindowInitialized();
|
||||||
|
this.callbackWindowInitialized = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendIPCMessage("hello-controller", { accepted: true, userData: this.userData });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "fire-event":
|
||||||
|
case "fire-event-callback":
|
||||||
|
/* already handled by out base class */
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn("Received unknown message type from popup window: %s", type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
import {ChannelMessage, IPCChannel} from "tc-shared/ipc/BrowserIPC";
|
||||||
|
import {EventReceiver, Registry} from "tc-shared/events";
|
||||||
|
|
||||||
|
export interface PopoutIPCMessage {
|
||||||
|
"hello-popout": { version: string },
|
||||||
|
"hello-controller": { accepted: boolean, message?: string, userData?: any },
|
||||||
|
|
||||||
|
"fire-event": {
|
||||||
|
type: string;
|
||||||
|
payload: any;
|
||||||
|
callbackId: string;
|
||||||
|
},
|
||||||
|
|
||||||
|
"fire-event-callback": {
|
||||||
|
callbackId: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Controller2PopoutMessages = "hello-controller" | "fire-event" | "fire-event-callback";
|
||||||
|
export type Popout2ControllerMessages = "hello-popout" | "fire-event" | "fire-event-callback";
|
||||||
|
|
||||||
|
interface SendIPCMessage {
|
||||||
|
"controller": Controller2PopoutMessages;
|
||||||
|
"popout": Popout2ControllerMessages;
|
||||||
|
}
|
||||||
|
interface ReceivedIPCMessage {
|
||||||
|
"controller": Popout2ControllerMessages;
|
||||||
|
"popout": Controller2PopoutMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
let callbackIdIndex = 0;
|
||||||
|
export abstract class EventControllerBase<Type extends "controller" | "popout"> {
|
||||||
|
protected ipcChannel: IPCChannel;
|
||||||
|
protected ipcRemoteId: string;
|
||||||
|
|
||||||
|
protected readonly localEventRegistry: Registry<string>;
|
||||||
|
private readonly localEventReceiver: EventReceiver<string>;
|
||||||
|
|
||||||
|
private omitEventType: string = undefined;
|
||||||
|
private omitEventData: any;
|
||||||
|
private eventFiredListeners: {[key: string]:{ callback: () => void, timeout: number }} = {};
|
||||||
|
|
||||||
|
protected constructor(localEventRegistry: Registry<string>) {
|
||||||
|
this.localEventRegistry = localEventRegistry;
|
||||||
|
|
||||||
|
let refThis = this;
|
||||||
|
this.localEventReceiver = new class implements EventReceiver<{}> {
|
||||||
|
fire<T extends keyof {}>(eventType: T, data?: any[T], overrideTypeKey?: boolean) {
|
||||||
|
if(refThis.omitEventType === eventType && refThis.omitEventData === data) {
|
||||||
|
refThis.omitEventType = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refThis.sendIPCMessage("fire-event", { type: eventType, payload: data, callbackId: undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
fire_async<T extends keyof {}>(eventType: T, data?: any[T], callback?: () => void) {
|
||||||
|
const callbackId = callback ? (++callbackIdIndex) + "-ev-cb" : undefined;
|
||||||
|
refThis.sendIPCMessage("fire-event", { type: eventType, payload: data, callbackId: callbackId });
|
||||||
|
if(callbackId) {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
delete refThis.eventFiredListeners[callbackId];
|
||||||
|
callback();
|
||||||
|
}, 2500);
|
||||||
|
|
||||||
|
refThis.eventFiredListeners[callbackId] = {
|
||||||
|
callback: callback,
|
||||||
|
timeout: timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.localEventRegistry.connectAll(this.localEventReceiver as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleIPCMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) {
|
||||||
|
if(this.ipcRemoteId !== remoteId) {
|
||||||
|
console.warn("Received message from unknown end: %s. Expected: %s", remoteId, this.ipcRemoteId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleTypedIPCMessage(message.type as any, message.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sendIPCMessage<T extends SendIPCMessage[Type]>(type: T, payload: PopoutIPCMessage[T]) {
|
||||||
|
this.ipcChannel.sendMessage(type, payload, this.ipcRemoteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleTypedIPCMessage<T extends ReceivedIPCMessage[Type]>(type: T, payload: PopoutIPCMessage[T]) {
|
||||||
|
switch (type) {
|
||||||
|
case "fire-event": {
|
||||||
|
const tpayload = payload as PopoutIPCMessage["fire-event"];
|
||||||
|
this.omitEventData = tpayload.payload;
|
||||||
|
this.omitEventType = tpayload.type;
|
||||||
|
this.localEventRegistry.fire(tpayload.type as any, tpayload.payload);
|
||||||
|
if(tpayload.callbackId)
|
||||||
|
this.sendIPCMessage("fire-event-callback", { callbackId: tpayload.callbackId });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "fire-event-callback": {
|
||||||
|
const tpayload = payload as PopoutIPCMessage["fire-event-callback"];
|
||||||
|
const callback = this.eventFiredListeners[tpayload.callbackId];
|
||||||
|
delete this.eventFiredListeners[tpayload.callbackId];
|
||||||
|
if(callback) {
|
||||||
|
clearTimeout(callback.timeout);
|
||||||
|
callback.callback();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
import {ChannelMessage, getInstance as getIPCInstance, IPCChannel} from "tc-shared/ipc/BrowserIPC";
|
||||||
|
import {Settings, SettingsKey} from "tc-shared/settings";
|
||||||
|
import {
|
||||||
|
Controller2PopoutMessages, EventControllerBase,
|
||||||
|
PopoutIPCMessage
|
||||||
|
} from "tc-shared/ui/react-elements/external-modal/IPCMessage";
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
|
||||||
|
const kSettingIPCChannel: SettingsKey<string> = {
|
||||||
|
key: "ipc-channel",
|
||||||
|
valueType: "string"
|
||||||
|
};
|
||||||
|
|
||||||
|
let controller: PopoutController;
|
||||||
|
export function getPopoutController() {
|
||||||
|
if(!controller)
|
||||||
|
controller = new PopoutController();
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PopoutController extends EventControllerBase<"popout"> {
|
||||||
|
private userData: any;
|
||||||
|
private callbackControllerHello: (accepted: boolean | string) => void;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(new Registry<string>());
|
||||||
|
this.ipcRemoteId = Settings.instance.static(Settings.KEY_IPC_REMOTE_ADDRESS, "invalid");
|
||||||
|
|
||||||
|
this.ipcChannel = getIPCInstance().createChannel(this.ipcRemoteId, Settings.instance.static(kSettingIPCChannel, "invalid"));
|
||||||
|
this.ipcChannel.messageHandler = this.handleIPCMessage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
getEventRegistry() {
|
||||||
|
return this.localEventRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
this.sendIPCMessage("hello-popout", { version: __build.version });
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
this.callbackControllerHello = undefined;
|
||||||
|
reject("controller haven't called back");
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
this.callbackControllerHello = result => {
|
||||||
|
this.callbackControllerHello = undefined;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if(typeof result === "string") {
|
||||||
|
reject(result);
|
||||||
|
} else if(!result) {
|
||||||
|
reject();
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleTypedIPCMessage<T extends Controller2PopoutMessages>(type: T, payload: PopoutIPCMessage[T]) {
|
||||||
|
super.handleTypedIPCMessage(type, payload);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "hello-controller": {
|
||||||
|
const tpayload = payload as PopoutIPCMessage["hello-controller"];
|
||||||
|
console.log("Received Hello World from controller. Window instance accpected: %o", tpayload.accepted);
|
||||||
|
if(!this.callbackControllerHello)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.userData = tpayload.userData;
|
||||||
|
this.callbackControllerHello(tpayload.accepted ? true : tpayload.message || false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "fire-event-callback":
|
||||||
|
case "fire-event":
|
||||||
|
/* handled by out base class */
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn("Received unknown message type from controller: %s", type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserData() {
|
||||||
|
return this.userData;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
import * as log from "tc-shared/log";
|
||||||
|
import {LogCategory} from "tc-shared/log";
|
||||||
|
import * as loader from "tc-loader";
|
||||||
|
import * as ipc from "../../../ipc/BrowserIPC";
|
||||||
|
import * as i18n from "../../../i18n/localize";
|
||||||
|
|
||||||
|
import "tc-shared/file/RemoteAvatars";
|
||||||
|
import "tc-shared/proto";
|
||||||
|
|
||||||
|
import {Stage} from "tc-loader";
|
||||||
|
import {AbstractModal} from "tc-shared/ui/react-elements/Modal";
|
||||||
|
import {Settings, SettingsKey} from "tc-shared/settings";
|
||||||
|
import {getPopoutController} from "./PopoutController";
|
||||||
|
import {findPopoutHandler} from "tc-shared/ui/react-elements/external-modal/PopoutRegistry";
|
||||||
|
import {bodyRenderer, titleRenderer} from "tc-shared/ui/react-elements/external-modal/PopoutRenderer";
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
|
||||||
|
log.info(LogCategory.GENERAL, "Hello World");
|
||||||
|
console.error("External modal said hello!");
|
||||||
|
|
||||||
|
let modalInstance: AbstractModal;
|
||||||
|
let modalClass: new <T>(events: Registry<T>, userData: any) => AbstractModal;
|
||||||
|
|
||||||
|
const kSettingModalTarget: SettingsKey<string> = {
|
||||||
|
key: "modal-target",
|
||||||
|
valueType: "string"
|
||||||
|
};
|
||||||
|
|
||||||
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
|
name: "setup",
|
||||||
|
priority: 110,
|
||||||
|
function: async () => {
|
||||||
|
await i18n.initialize();
|
||||||
|
ipc.setup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
|
name: "main app connect",
|
||||||
|
priority: 100,
|
||||||
|
function: async () => {
|
||||||
|
const ppController = getPopoutController();
|
||||||
|
await ppController.initialize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
|
name: "modal class loader",
|
||||||
|
priority: 10,
|
||||||
|
function: async () => {
|
||||||
|
const modalTarget = Settings.instance.static(kSettingModalTarget, "unknown");
|
||||||
|
console.error("Loading modal class %s", modalTarget);
|
||||||
|
try {
|
||||||
|
const handler = findPopoutHandler(modalTarget);
|
||||||
|
if(!handler) {
|
||||||
|
loader.critical_error("Missing popout handler", "Handler " + modalTarget + " is missing.");
|
||||||
|
throw "missing handler";
|
||||||
|
}
|
||||||
|
|
||||||
|
modalClass = await handler.loadClass();
|
||||||
|
} catch(error) {
|
||||||
|
loader.critical_error("Failed to load modal", "Lookup the console for more detail");
|
||||||
|
console.error("Failed to load modal %s: %o", modalTarget, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loader.register_task(Stage.LOADED, {
|
||||||
|
name: "main app connect",
|
||||||
|
priority: 100,
|
||||||
|
function: async () => {
|
||||||
|
try {
|
||||||
|
modalInstance = new modalClass(getPopoutController().getEventRegistry(), getPopoutController().getUserData());
|
||||||
|
titleRenderer.setInstance(modalInstance);
|
||||||
|
bodyRenderer.setInstance(modalInstance);
|
||||||
|
} catch(error) {
|
||||||
|
loader.critical_error("Failed to invoker modal", "Lookup the console for more detail");
|
||||||
|
console.error("Failed to load modal: %o", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,33 @@
|
||||||
|
import {AbstractModal} from "tc-shared/ui/react-elements/Modal";
|
||||||
|
|
||||||
|
export interface PopoutHandler {
|
||||||
|
name: string;
|
||||||
|
loadClass: <T extends AbstractModal>() => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registeredHandler: {[key: string]: PopoutHandler} = {};
|
||||||
|
|
||||||
|
export function findPopoutHandler(name: string) {
|
||||||
|
return registeredHandler[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerHandler(handler: PopoutHandler) {
|
||||||
|
registeredHandler[handler.name] = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerHandler({
|
||||||
|
name: "video-viewer",
|
||||||
|
loadClass: async () => await import("tc-shared/video-viewer/Renderer")
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
registerHandler({
|
||||||
|
name: "conversation",
|
||||||
|
loadClass: async () => await import("tc-shared/ui/frames/side/PopoutConversationUI")
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
registerHandler({
|
||||||
|
name: "css-editor",
|
||||||
|
loadClass: async () => await import("tc-shared/ui/modal/css-editor/Renderer")
|
||||||
|
});
|
|
@ -0,0 +1,31 @@
|
||||||
|
/* FIXME: Remove this wired import */
|
||||||
|
:global {
|
||||||
|
@import "../../../../css/static/general";
|
||||||
|
@import "../../../../css/static/modal";
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
max-height: 100vh;
|
||||||
|
max-width: 100vw;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ReactDOM from "react-dom";
|
||||||
|
|
||||||
|
import {AbstractModal} from "tc-shared/ui/react-elements/Modal";
|
||||||
|
|
||||||
|
const cssStyle = require("./PopoutRenderer.scss");
|
||||||
|
|
||||||
|
class TitleRenderer {
|
||||||
|
private readonly htmlContainer: HTMLElement;
|
||||||
|
private modalInstance: AbstractModal;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const titleElements = document.getElementsByTagName("title");
|
||||||
|
if(titleElements.length === 0) {
|
||||||
|
this.htmlContainer = document.createElement("title");
|
||||||
|
document.head.appendChild(this.htmlContainer);
|
||||||
|
} else {
|
||||||
|
this.htmlContainer = titleElements[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInstance(instance: AbstractModal) {
|
||||||
|
if(this.modalInstance)
|
||||||
|
ReactDOM.unmountComponentAtNode(this.htmlContainer);
|
||||||
|
this.modalInstance = instance;
|
||||||
|
if(this.modalInstance)
|
||||||
|
ReactDOM.render(<>{this.modalInstance.title()}</>, this.htmlContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const titleRenderer = new TitleRenderer();
|
||||||
|
|
||||||
|
class BodyRenderer {
|
||||||
|
private readonly htmlContainer: HTMLElement;
|
||||||
|
private modalInstance: AbstractModal;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.htmlContainer = document.createElement("div");
|
||||||
|
this.htmlContainer.classList.add(cssStyle.container);
|
||||||
|
|
||||||
|
document.body.appendChild(this.htmlContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
setInstance(instance: AbstractModal) {
|
||||||
|
if(this.modalInstance)
|
||||||
|
ReactDOM.unmountComponentAtNode(this.htmlContainer);
|
||||||
|
this.modalInstance = instance;
|
||||||
|
if(this.modalInstance)
|
||||||
|
ReactDOM.render(<>{this.modalInstance.renderBody()}</>, this.htmlContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const bodyRenderer = new BodyRenderer();
|
|
@ -0,0 +1,7 @@
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
import {ExternalModalController} from "tc-shared/ui/react-elements/external-modal/Controller";
|
||||||
|
|
||||||
|
|
||||||
|
export function spawnExternalModal<EventClass>(modal: string, events: Registry<EventClass>, userData: any) : ExternalModalController {
|
||||||
|
return new ExternalModalController(modal, events as any, userData);
|
||||||
|
}
|
|
@ -228,7 +228,7 @@ export class ChannelTree {
|
||||||
|
|
||||||
constructor(client) {
|
constructor(client) {
|
||||||
this.events = new Registry<ChannelTreeEvents>();
|
this.events = new Registry<ChannelTreeEvents>();
|
||||||
this.events.enable_debug("channel-tree");
|
this.events.enableDebug("channel-tree");
|
||||||
|
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.view = React.createRef();
|
this.view = React.createRef();
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import {spawnExternalModal} from "tc-shared/ui/react-elements/external-modal";
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
import {VideoViewerEvents} from "./Definitions";
|
||||||
|
import {Modal, spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||||
|
import * as React from "react";
|
||||||
|
import {useState} from "react";
|
||||||
|
|
||||||
|
const NumberRenderer = (props: { events: Registry<VideoViewerEvents> }) => {
|
||||||
|
const [ value, setValue ] = useState("unset");
|
||||||
|
|
||||||
|
props.events.reactUse("notify_value", event => setValue(event.value + ""));
|
||||||
|
|
||||||
|
return <>{value}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function spawnVideoPopout() {
|
||||||
|
const registry = new Registry<VideoViewerEvents>();
|
||||||
|
const modalController = spawnExternalModal("video-viewer", registry, {});
|
||||||
|
modalController.open().then(() => {
|
||||||
|
const url = URL.createObjectURL(new Blob(["Hello World"], { type: "plain/text" }));
|
||||||
|
registry.fire("notify_data_url", { url: url });
|
||||||
|
});
|
||||||
|
|
||||||
|
spawnReactModal(class extends Modal {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
title() {
|
||||||
|
return "Hello World";
|
||||||
|
}
|
||||||
|
renderBody() {
|
||||||
|
return <h1>Hello World: <NumberRenderer events={registry} /></h1>;
|
||||||
|
}
|
||||||
|
}).show();
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface VideoViewerEvents {
|
||||||
|
"notify_show": {},
|
||||||
|
|
||||||
|
"notify_value": { value: number },
|
||||||
|
"notify_data_url": { url: string }
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import {AbstractModal} from "tc-shared/ui/react-elements/Modal";
|
||||||
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
|
import * as React from "react";
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
import {VideoViewerEvents} from "./Definitions";
|
||||||
|
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||||
|
import {Slider} from "tc-shared/ui/react-elements/Slider";
|
||||||
|
|
||||||
|
class ModalVideoPopout extends AbstractModal {
|
||||||
|
readonly events: Registry<VideoViewerEvents>;
|
||||||
|
|
||||||
|
constructor(registry: Registry<VideoViewerEvents>, userData: any) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.events = registry;
|
||||||
|
this.events.on("notify_show", () => {
|
||||||
|
console.log("Showed!");
|
||||||
|
});
|
||||||
|
|
||||||
|
this.events.on("notify_data_url", async event => {
|
||||||
|
console.log(event.url);
|
||||||
|
console.log(await (await fetch(event.url)).text());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
title(): string | React.ReactElement<Translatable> {
|
||||||
|
return <>Hello World <LoadingDots textOnly={true} /></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBody(): React.ReactElement {
|
||||||
|
return <div style={{ padding: "10em" }}>
|
||||||
|
<Slider value={100} minValue={0} maxValue={100} stepSize={1} onInput={value => this.events.fire("notify_value", { value: value })} />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export = ModalVideoPopout;
|
||||||
|
|
||||||
|
console.error("Hello World from video popout");
|
|
@ -4,6 +4,7 @@ import trtransformer from "./tools/trgen/ts_transformer";
|
||||||
import {exec} from "child_process";
|
import {exec} from "child_process";
|
||||||
import * as util from "util";
|
import * as util from "util";
|
||||||
import LoaderIndexGenerator = require("./loader/IndexGenerator");
|
import LoaderIndexGenerator = require("./loader/IndexGenerator");
|
||||||
|
import {Configuration} from "webpack";
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
|
@ -56,7 +57,9 @@ const isLoaderFile = (file: string) => {
|
||||||
|
|
||||||
export const config = async (target: "web" | "client") => { return {
|
export const config = async (target: "web" | "client") => { return {
|
||||||
entry: {
|
entry: {
|
||||||
"loader": "./loader/app/index.ts"
|
"loader": "./loader/app/index.ts",
|
||||||
|
"modal-external": "./shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts",
|
||||||
|
"devel-main": "./shared/js/devel_main.ts"
|
||||||
},
|
},
|
||||||
|
|
||||||
devtool: isDevelopment ? "inline-source-map" : undefined,
|
devtool: isDevelopment ? "inline-source-map" : undefined,
|
||||||
|
@ -143,7 +146,8 @@ export const config = async (target: "web" | "client") => { return {
|
||||||
target_file: path.join(__dirname, "dist", "translations.json")
|
target_file: path.join(__dirname, "dist", "translations.json")
|
||||||
})]
|
})]
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
transpileOnly: isDevelopment
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue