diff --git a/ChangeLog.md b/ChangeLog.md index fb42f94c..6276d7bf 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,4 +1,8 @@ # Changelog: +* **08.08.20** + - Added a watch to gether mode + - Added API support for the popout able browsers for the native client + * **05.08.20** - Putting the CSS files within the assets. No extra load needed any more - Revoked the async file loading limit diff --git a/loader/app/loader/loader.ts b/loader/app/loader/loader.ts index 54904c68..bfc6362c 100644 --- a/loader/app/loader/loader.ts +++ b/loader/app/loader/loader.ts @@ -129,6 +129,11 @@ export function finished() { export function running() { return typeof(currentStage) !== "undefined"; } export function register_task(stage: Stage, task: Task) { + if(!task.function) { + debugger; + throw "tried to register a loader task without a function"; + } + if(currentStage > stage) { if(config.error) console.warn("Register loading task, but it had already been finished. Executing task anyways!"); diff --git a/loader/app/targets/app.ts b/loader/app/targets/app.ts index a3957961..deb76aba 100644 --- a/loader/app/targets/app.ts +++ b/loader/app/targets/app.ts @@ -1,6 +1,6 @@ import "./shared"; import * as loader from "../loader/loader"; -import {ApplicationLoader, config, SourcePath} from "../loader/loader"; +import {ApplicationLoader, SourcePath} from "../loader/loader"; import {script_name} from "../loader/utils"; import {loadManifest, loadManifestTarget} from "../maifest"; @@ -10,8 +10,6 @@ declare global { } } -const node_require: typeof require = window.require; - function cache_tag() { const ui = ui_version(); return "?_ts=" + (!!ui && ui !== "unknown" ? ui : Date.now()); @@ -168,18 +166,18 @@ loader.register_task(loader.Stage.SETUP, { export default class implements ApplicationLoader { execute() { /* TeaClient */ - if(node_require) { + if(window.require) { if(__build.target !== "client") { 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; - const path = node_require("path"); - const remote = node_require('electron').remote; + const path = __non_webpack_require__("path"); + const remote = __non_webpack_require__('electron').remote; const render_entry = path.join(remote.app.getAppPath(), "/modules/", "renderer"); - const render = node_require(render_entry); + const render = __non_webpack_require__(render_entry); loader.register_task(loader.Stage.INITIALIZING, { name: "teaclient initialize", diff --git a/loader/app/targets/maifest-target.ts b/loader/app/targets/maifest-target.ts index 9b399be7..4fe8d7b7 100644 --- a/loader/app/targets/maifest-target.ts +++ b/loader/app/targets/maifest-target.ts @@ -5,6 +5,8 @@ import {loadManifest, loadManifestTarget} from "../maifest"; import {getUrlParameter} from "../loader/utils"; export default class implements ApplicationLoader { + + execute() { loader.register_task(Stage.SETUP, { function: async taskId => { @@ -27,24 +29,28 @@ export default class implements ApplicationLoader { 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"); @@ -52,6 +58,7 @@ export default class implements ApplicationLoader { body.append(container); } + /* tooltip container */ { const container = document.createElement("div"); @@ -78,6 +85,26 @@ export default class implements ApplicationLoader { priority: 10 }); + if(__build.target === "client") { + loader.register_task(Stage.SETUP, { + name: "native setup", + function: async () => { + const path = __non_webpack_require__("path"); + const remote = __non_webpack_require__('electron').remote; + + const render_entry = path.join(remote.app.getAppPath(), "/modules/", "renderer-manifest", "index"); + const render = __non_webpack_require__(render_entry); + + loader.register_task(loader.Stage.SETUP, { + name: "teaclient setup", + function: async () => await render.initialize(getUrlParameter("chunk")), + priority: 40 + }); + }, + priority: 50 + }); + } + loader.execute_managed(); } } \ No newline at end of file diff --git a/loader/css/loader.scss b/loader/css/loader.scss index f789876d..faf9afba 100644 --- a/loader/css/loader.scss +++ b/loader/css/loader.scss @@ -13,7 +13,7 @@ $setup-time: 80s / 24; /* 24 frames / sec; the initial sequence is 80 seconds */ user-select: none; - z-index: 10000; + z-index: 10000000; display: flex; flex-direction: column; diff --git a/loader/css/overlay.scss b/loader/css/overlay.scss index f348ba1a..e32aab12 100644 --- a/loader/css/overlay.scss +++ b/loader/css/overlay.scss @@ -1,5 +1,5 @@ #overlay-no-js, #critical-load { - z-index: 10000; + z-index: 100000000; display: none; position: fixed; diff --git a/shared/js/events.ts b/shared/js/events.ts index 4821995b..74df49fa 100644 --- a/shared/js/events.ts +++ b/shared/js/events.ts @@ -21,13 +21,13 @@ export class SingletonEvent implements Event() : SingletonEvents[T] { return; } } -export interface EventReceiver { +export interface EventReceiver { fire(event_type: T, data?: Events[T], overrideTypeKey?: boolean); fire_async(event_type: T, data?: Events[T], callback?: () => void); } const event_annotation_key = guid(); -export class Registry implements EventReceiver { +export class Registry implements EventReceiver { private readonly registryUuid; private handler: {[key: string]: ((event) => void)[]} = {}; @@ -309,12 +309,6 @@ export function ReactEventHandler, Event } } -export namespace sidebar { - export interface music { - - } -} - export namespace modal { export type BotStatusType = "name" | "description" | "volume" | "country_code" | "channel_commander" | "priority_speaker"; export type PlaylistStatusType = "replay_mode" | "finished" | "delete_played" | "max_size" | "notify_song_change"; diff --git a/shared/js/i18n/localize.ts b/shared/js/i18n/localize.ts index a2a6cfbb..e38b963c 100644 --- a/shared/js/i18n/localize.ts +++ b/shared/js/i18n/localize.ts @@ -2,7 +2,6 @@ import * as log from "tc-shared/log"; import {LogCategory} from "tc-shared/log"; import {guid} from "tc-shared/crypto/uid"; import {Settings, StaticSettings} from "tc-shared/settings"; -import {createErrorModal} from "tc-shared/ui/elements/Modal"; import * as loader from "tc-loader"; import {formatMessage, formatMessageString} from "tc-shared/ui/frames/chat"; @@ -309,7 +308,9 @@ export async function initialize() { } catch (error) { console.error(tr("Failed to initialize selected translation: %o"), error); const show_error = () => { - createErrorModal(tr("Translation System"), tra("Failed to load current selected translation file.{:br:}File: {0}{:br:}Error: {1}{:br:}{:br:}Using default fallback translations.", cfg.current_translation_url, error)).open() + import("../ui/elements/Modal").then(Modal => { + Modal.createErrorModal(tr("Translation System"), tra("Failed to load current selected translation file.{:br:}File: {0}{:br:}Error: {1}{:br:}{:br:}Using default fallback translations.", cfg.current_translation_url, error)).open() + }); }; if(loader.running()) loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { diff --git a/shared/js/log.ts b/shared/js/log.ts index 57af41f1..93d0f129 100644 --- a/shared/js/log.ts +++ b/shared/js/log.ts @@ -152,6 +152,27 @@ export function error(category: LogCategory, message: string, ...optionalParams: log(LogType.ERROR, category, message, ...optionalParams); } +/* methods for direct import */ +export function logTrace(category: LogCategory, message: string, ...optionalParams: any[]) { + log(LogType.TRACE, category, message, ...optionalParams); +} + +export function logDebug(category: LogCategory, message: string, ...optionalParams: any[]) { + log(LogType.DEBUG, category, message, ...optionalParams); +} + +export function logInfo(category: LogCategory, message: string, ...optionalParams: any[]) { + log(LogType.INFO, category, message, ...optionalParams); +} + +export function logWarn(category: LogCategory, message: string, ...optionalParams: any[]) { + log(LogType.WARNING, category, message, ...optionalParams); +} + +export function logError(category: LogCategory, message: string, ...optionalParams: any[]) { + log(LogType.ERROR, category, message, ...optionalParams); +} + export function group(level: LogType, category: LogCategory, name: string, ...optionalParams: any[]) : Group { name = "[%s] " + name; optionalParams.unshift(category_mapping.get(category)); diff --git a/shared/js/ui/frames/chat.ts b/shared/js/ui/frames/chat.ts index a2920b04..f369f919 100644 --- a/shared/js/ui/frames/chat.ts +++ b/shared/js/ui/frames/chat.ts @@ -284,12 +284,14 @@ export function format_time(time: number, default_value: string) { return result.length > 0 ? result.substring(1) : default_value; } -let _icon_size_style: JQuery; +let _icon_size_style: HTMLStyleElement; export function set_icon_size(size: string) { - if(!_icon_size_style) - _icon_size_style = $.spawn("style").appendTo($("#style")); + if(!_icon_size_style) { + _icon_size_style = document.createElement("style"); + document.head.append(_icon_size_style); + } - _icon_size_style.text("\n" + + _icon_size_style.innerText = ("\n" + ".chat-emoji {\n" + " height: " + size + "!important;\n" + " width: " + size + "!important;\n" + diff --git a/shared/js/ui/react-elements/external-modal/Controller.ts b/shared/js/ui/react-elements/external-modal/Controller.ts index 7ea45739..6b0b6e1a 100644 --- a/shared/js/ui/react-elements/external-modal/Controller.ts +++ b/shared/js/ui/react-elements/external-modal/Controller.ts @@ -2,7 +2,6 @@ import * as log from "tc-shared/log"; import {LogCategory} from "tc-shared/log"; import * as ipc from "tc-shared/ipc/BrowserIPC"; import {ChannelMessage} from "tc-shared/ipc/BrowserIPC"; -import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; import {Registry} from "tc-shared/events"; import { EventControllerBase, @@ -11,21 +10,17 @@ import { } from "tc-shared/ui/react-elements/external-modal/IPCMessage"; import {ModalController, ModalEvents, ModalOptions, ModalState} from "tc-shared/ui/react-elements/Modal"; -export class ExternalModalController extends EventControllerBase<"controller"> implements ModalController { +export abstract class AbstractExternalModalController extends EventControllerBase<"controller"> implements ModalController { public readonly modalType: string; public readonly userData: any; - private modalState: ModalState = ModalState.DESTROYED; private readonly modalEvents: Registry; + private modalState: ModalState = ModalState.DESTROYED; - private currentWindow: Window; + private readonly documentUnloadListener: () => void; private callbackWindowInitialized: (error?: string) => void; - private readonly documentQuitListener: () => void; - private windowClosedTestInterval: number = 0; - private windowClosedTimeout: number; - - constructor(modal: string, localEventRegistry: Registry, userData: any) { + protected constructor(modal: string, localEventRegistry: Registry, userData: any) { super(localEventRegistry); this.modalEvents = new Registry(); @@ -36,7 +31,7 @@ export class ExternalModalController extends EventControllerBase<"controller"> i this.ipcChannel = ipc.getInstance().createChannel(); this.ipcChannel.messageHandler = this.handleIPCMessage.bind(this); - this.documentQuitListener = () => this.currentWindow?.close(); + this.documentUnloadListener = () => this.destroy(); } getOptions(): Readonly { @@ -51,66 +46,21 @@ export class ExternalModalController extends EventControllerBase<"controller"> i return this.modalState; } - private trySpawnWindow() : Window | null { - const parameters = { - "loader-target": "manifest", - "chunk": "modal-external", - "modal-target": this.modalType, - "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("&"), - this.modalType, - Object.keys(features).map(e => e + "=" + features[e]).join(",") - ); - } + protected abstract spawnWindow() : Promise; + protected abstract focusWindow() : void; + protected abstract destroyWindow() : void; async show() { - if(this.currentWindow) { - this.currentWindow.focus(); + if(this.modalState === ModalState.SHOWN) { + this.focusWindow(); return; } + this.modalState = ModalState.SHOWN; - 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.modalType), callback => { - if(!callback) { - reject("user aborted"); - return; - } - - this.currentWindow = this.trySpawnWindow(); - if(this.currentWindow) { - reject(tr("Failed to spawn window")); - } else { - resolve(); - } - }).close_listener.push(() => reject(tr("user aborted"))); - }) - } - - if(!this.currentWindow) { - /* some shitty popup blocker or whatever */ + if(!await this.spawnWindow()) { + this.modalState = ModalState.DESTROYED; throw tr("failed to create window"); } - window.addEventListener("unload", this.documentQuitListener); try { await new Promise((resolve, reject) => { @@ -126,50 +76,25 @@ export class ExternalModalController extends EventControllerBase<"controller"> i }; }); } catch (e) { - this.currentWindow?.close(); - this.currentWindow = undefined; + this.modalState = ModalState.DESTROYED; + this.doDestroyWindow(); throw e; } - this.currentWindow.onbeforeunload = () => { - clearInterval(this.windowClosedTestInterval); - - this.windowClosedTimeout = Date.now() + 5000; - this.windowClosedTestInterval = setInterval(() => { - if(!this.currentWindow) { - clearInterval(this.windowClosedTestInterval); - this.windowClosedTestInterval = 0; - return; - } - - if(this.currentWindow.closed || Date.now() > this.windowClosedTimeout) { - window.removeEventListener("unload", this.documentQuitListener); - this.currentWindow = undefined; - this.destroy(); /* TODO: Test if we should do this */ - } - }, 100); - }; - - this.modalState = ModalState.SHOWN; + window.addEventListener("unload", this.documentUnloadListener); this.modalEvents.fire("open"); } - private destroyPopUp() { - if(this.currentWindow) { - clearInterval(this.windowClosedTestInterval); - this.windowClosedTestInterval = 0; - - window.removeEventListener("beforeunload", this.documentQuitListener); - this.currentWindow.close(); - this.currentWindow = undefined; - } + private doDestroyWindow() { + this.destroyWindow(); + window.removeEventListener("beforeunload", this.documentUnloadListener); } async hide() { if(this.modalState == ModalState.DESTROYED || this.modalState === ModalState.HIDDEN) return; - this.destroyPopUp(); + this.doDestroyWindow(); this.modalState = ModalState.HIDDEN; this.modalEvents.fire("close"); } @@ -178,7 +103,7 @@ export class ExternalModalController extends EventControllerBase<"controller"> i if(this.modalState === ModalState.DESTROYED) return; - this.destroyPopUp(); + this.doDestroyWindow(); if(this.ipcChannel) ipc.getInstance().deleteChannel(this.ipcChannel); @@ -187,6 +112,10 @@ export class ExternalModalController extends EventControllerBase<"controller"> i this.modalEvents.fire("destroy"); } + protected handleWindowClosed() { + this.destroy(); + } + protected handleIPCMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) { if(broadcast) return; @@ -195,14 +124,6 @@ export class ExternalModalController extends EventControllerBase<"controller"> i log.debug(LogCategory.IPC, tr("Remote window connected with id %s"), remoteId); this.ipcRemoteId = remoteId; } else if(this.ipcRemoteId !== remoteId) { - if(this.windowClosedTestInterval > 0) { - clearInterval(this.windowClosedTestInterval); - this.windowClosedTestInterval = 0; - - log.debug(LogCategory.IPC, tr("Remote window got reconnected. Client reloaded it.")); - } else { - log.warn(LogCategory.IPC, tr("Remote window got a new id. Maybe a reload?")); - } this.ipcRemoteId = remoteId; } diff --git a/shared/js/ui/react-elements/external-modal/IPCMessage.ts b/shared/js/ui/react-elements/external-modal/IPCMessage.ts index 58036614..8cb9df49 100644 --- a/shared/js/ui/react-elements/external-modal/IPCMessage.ts +++ b/shared/js/ui/react-elements/external-modal/IPCMessage.ts @@ -33,18 +33,18 @@ export abstract class EventControllerBase protected ipcChannel: IPCChannel; protected ipcRemoteId: string; - protected readonly localEventRegistry: Registry; - private readonly localEventReceiver: EventReceiver; + protected readonly localEventRegistry: Registry; + private readonly localEventReceiver: EventReceiver; private omitEventType: string = undefined; private omitEventData: any; private eventFiredListeners: {[key: string]:{ callback: () => void, timeout: number }} = {}; - protected constructor(localEventRegistry: Registry) { + protected constructor(localEventRegistry: Registry) { this.localEventRegistry = localEventRegistry; let refThis = this; - this.localEventReceiver = new class implements EventReceiver<{}> { + this.localEventReceiver = new class implements EventReceiver { fire(eventType: T, data?: any[T], overrideTypeKey?: boolean) { if(refThis.omitEventType === eventType && refThis.omitEventData === data) { refThis.omitEventType = undefined; @@ -70,7 +70,7 @@ export abstract class EventControllerBase } } }; - this.localEventRegistry.connectAll(this.localEventReceiver as any); + this.localEventRegistry.connectAll(this.localEventReceiver); } protected handleIPCMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) { @@ -112,7 +112,7 @@ export abstract class EventControllerBase } protected destroyIPC() { - this.localEventRegistry.disconnectAll(this.localEventReceiver as any); + this.localEventRegistry.disconnectAll(this.localEventReceiver); this.ipcChannel = undefined; this.ipcRemoteId = undefined; this.eventFiredListeners = {}; diff --git a/shared/js/ui/react-elements/external-modal/index.ts b/shared/js/ui/react-elements/external-modal/index.ts index c9c06c02..2b174996 100644 --- a/shared/js/ui/react-elements/external-modal/index.ts +++ b/shared/js/ui/react-elements/external-modal/index.ts @@ -1,6 +1,17 @@ import {Registry} from "tc-shared/events"; -import {ExternalModalController} from "tc-shared/ui/react-elements/external-modal/Controller"; +import {ModalController} from "tc-shared/ui/react-elements/Modal"; +import "./Controller"; /* we've to reference him here, else the client would not */ -export function spawnExternalModal(modal: string, events: Registry, userData: any) : ExternalModalController { - return new ExternalModalController(modal, events as any, userData); +export type ControllerFactory = (modal: string, events: Registry, userData: any) => ModalController; +let modalControllerFactory: ControllerFactory; + +export function setExternalModalControllerFactory(factory: ControllerFactory) { + modalControllerFactory = factory; +} + +export function spawnExternalModal(modal: string, events: Registry, userData: any) : ModalController { + if(typeof modalControllerFactory === "undefined") + throw tr("No external modal factory has been set"); + + return modalControllerFactory(modal, events as any, userData); } \ No newline at end of file diff --git a/shared/js/video-viewer/Controller.ts b/shared/js/video-viewer/Controller.ts index 5de8c685..0ec99729 100644 --- a/shared/js/video-viewer/Controller.ts +++ b/shared/js/video-viewer/Controller.ts @@ -1,5 +1,5 @@ import * as log from "tc-shared/log"; -import {LogCategory} from "tc-shared/log"; +import {LogCategory, logError} from "tc-shared/log"; import {spawnExternalModal} from "tc-shared/ui/react-elements/external-modal"; import {EventHandler, Registry} from "tc-shared/events"; import {VideoViewerEvents} from "./Definitions"; @@ -66,7 +66,6 @@ class VideoViewer { this.events.fire_async("notify_following", { watcherId: undefined }); this.events.fire_async("notify_video", { url: url }); - this.modal.show(); } async open() { @@ -313,6 +312,7 @@ let currentVideoViewer: VideoViewer; export function openVideoViewer(connection: ConnectionHandler, url: string) { if(currentVideoViewer?.connection === connection) { currentVideoViewer.setWatchingVideo(url); + currentVideoViewer.open(); /* draw focus */ return; } else if(currentVideoViewer) { currentVideoViewer.destroy(); @@ -323,9 +323,13 @@ export function openVideoViewer(connection: ConnectionHandler, url: string) { currentVideoViewer.events.on("notify_destroy", () => { currentVideoViewer = undefined; }); - currentVideoViewer.open(); + currentVideoViewer.open().catch(error => { + logError(LogCategory.GENERAL, tr("Failed to open video viewer: %o"), error); + currentVideoViewer.destroy(); + currentVideoViewer = undefined; + }); } -window.onunload = () => { +window.onbeforeunload = () => { currentVideoViewer?.destroy(); }; \ No newline at end of file diff --git a/shared/js/video-viewer/Definitions.ts b/shared/js/video-viewer/Definitions.ts index 4dde3b4b..722752e7 100644 --- a/shared/js/video-viewer/Definitions.ts +++ b/shared/js/video-viewer/Definitions.ts @@ -1,19 +1,19 @@ -interface PlayerStatusPlaying { +export interface PlayerStatusPlaying { status: "playing"; timestampPlay: number; timestampBuffer: number; } -interface PlayerStatusBuffering { +export interface PlayerStatusBuffering { status: "buffering"; } -interface PlayerStatusStopped { +export interface PlayerStatusStopped { status: "stopped"; } -interface PlayerStatusPaused { +export interface PlayerStatusPaused { status: "paused"; } diff --git a/shared/js/video-viewer/Renderer.tsx b/shared/js/video-viewer/Renderer.tsx index 9ee5bc0a..b1a8b058 100644 --- a/shared/js/video-viewer/Renderer.tsx +++ b/shared/js/video-viewer/Renderer.tsx @@ -91,10 +91,10 @@ const WatcherInfo = React.memo((props: { events: Registry, wa let renderedAvatar; if(clientInfo === "loading") { - renderedAvatar = ; + renderedAvatar = ; } else { const avatar = getGlobalAvatarManagerFactory().getManager(props.handlerId).resolveClientAvatar({ id: clientInfo.clientId, clientUniqueId: clientInfo.uniqueId }); - renderedAvatar = ; + renderedAvatar = ; } let renderedClientName; @@ -154,9 +154,7 @@ const WatcherInfo = React.memo((props: { events: Registry, wa }} >
-
- {renderedAvatar} -
+ {renderedAvatar}
diff --git a/tools/dtsgen/import_organizer.ts b/tools/dtsgen/import_organizer.ts index faa5e1b4..0555d9d2 100644 --- a/tools/dtsgen/import_organizer.ts +++ b/tools/dtsgen/import_organizer.ts @@ -40,7 +40,7 @@ function eliminate_imports(node: ts.Node, ctx: ts.TransformationContext, data: I case SyntaxKind.ImportDeclaration: const import_decl = node as ts.ImportDeclaration; const clause = import_decl.importClause; - if(!clause.namedBindings) return node; + if(!clause?.namedBindings) return node; let new_binding; if(clause.namedBindings.kind === SyntaxKind.NamedImports) { @@ -260,6 +260,9 @@ function analyze_type_node(node: ts.TypeNode | ts.LeftHandSideExpression, data: analyze_type_node(ct.type, data); break; + case SyntaxKind.TupleType: + break; + default: throw "Unknown type " + SyntaxKind[node.kind] + ". Extend me :)"; } diff --git a/web/app/ExternalModalFactory.ts b/web/app/ExternalModalFactory.ts new file mode 100644 index 00000000..bacae447 --- /dev/null +++ b/web/app/ExternalModalFactory.ts @@ -0,0 +1,133 @@ +import {AbstractExternalModalController} from "tc-shared/ui/react-elements/external-modal/Controller"; +import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; +import * as ipc from "tc-shared/ipc/BrowserIPC"; +import * as loader from "tc-loader"; +import {Stage} from "tc-loader"; +import {setExternalModalControllerFactory} from "tc-shared/ui/react-elements/external-modal"; +import {ChannelMessage} from "tc-shared/ipc/BrowserIPC"; +import {LogCategory, logDebug, logWarn} from "tc-shared/log"; + +class ExternalModalController extends AbstractExternalModalController { + private currentWindow: Window; + private windowClosedTestInterval: number = 0; + private windowClosedTimeout: number; + + constructor(a, b, c) { + super(a, b, c); + } + + protected async spawnWindow() : Promise { + if(this.currentWindow) + return true; + + this.currentWindow = this.trySpawnWindow0(); + 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.modalType), callback => { + if(!callback) { + reject("user aborted"); + return; + } + + this.currentWindow = this.trySpawnWindow0(); + if(window) { + reject(tr("Failed to spawn window")); + } else { + resolve(); + } + }).close_listener.push(() => reject(tr("user aborted"))); + }); + } + + if(!this.currentWindow) + return false; + + this.currentWindow.onbeforeunload = () => { + clearInterval(this.windowClosedTestInterval); + + this.windowClosedTimeout = Date.now() + 5000; + this.windowClosedTestInterval = setInterval(() => { + if(!this.currentWindow) { + clearInterval(this.windowClosedTestInterval); + this.windowClosedTestInterval = 0; + return; + } + + if(this.currentWindow.closed || Date.now() > this.windowClosedTimeout) { + clearInterval(this.windowClosedTestInterval); + this.windowClosedTestInterval = 0; + this.handleWindowClosed(); + } + }, 100); + }; + + return true; + } + + protected destroyWindow() { + clearInterval(this.windowClosedTestInterval); + this.windowClosedTestInterval = 0; + + if(this.currentWindow) { + this.currentWindow.close(); + this.currentWindow = undefined; + } + } + + protected focusWindow() { + this.currentWindow?.focus(); + } + + private trySpawnWindow0() : Window | null { + const parameters = { + "loader-target": "manifest", + "chunk": "modal-external", + "modal-target": this.modalType, + "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("&"), + this.modalType, + Object.keys(features).map(e => e + "=" + features[e]).join(",") + ); + } + + protected handleIPCMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) { + if(!broadcast && this.ipcRemoteId !== remoteId) { + if(this.windowClosedTestInterval > 0) { + clearInterval(this.windowClosedTestInterval); + this.windowClosedTestInterval = 0; + + logDebug(LogCategory.IPC, tr("Remote window got reconnected. Client reloaded it.")); + } else { + logWarn(LogCategory.IPC, tr("Remote window got a new id. Maybe a reload?")); + } + } + + super.handleIPCMessage(remoteId, broadcast, message); + } +} + +loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { + priority: 50, + name: "external modal controller factory setup", + function: async () => { + setExternalModalControllerFactory((modal, events, userData) => new ExternalModalController(modal, events, userData)); + } +}); \ No newline at end of file diff --git a/web/app/index.ts b/web/app/index.ts index bcd2e814..72300ecb 100644 --- a/web/app/index.ts +++ b/web/app/index.ts @@ -1,5 +1,6 @@ import "webrtc-adapter"; import "./index.scss"; import "./FileTransfer"; +import "./ExternalModalFactory"; export = require("tc-shared/main"); \ No newline at end of file