Encapsulated the external modal from spawning and administrating windows
parent
3d04fa3f0d
commit
0b5f519735
|
@ -1,3 +0,0 @@
|
|||
window.__native_client_init_shared(__webpack_require__);
|
||||
|
||||
import "tc-shared/ui/react-elements/external-modal/PopoutEntrypoint";
|
|
@ -0,0 +1,4 @@
|
|||
window.__native_client_init_shared(__webpack_require__);
|
||||
|
||||
import "../AppMain.scss";
|
||||
import "tc-shared/entry-points/MainApp";
|
|
@ -1,4 +1,2 @@
|
|||
window.__native_client_init_shared(__webpack_require__);
|
||||
|
||||
import "./index.scss";
|
||||
import "tc-shared/main";
|
||||
import "tc-shared/entry-points/ModalWindow";
|
|
@ -10,7 +10,7 @@ export default class implements ApplicationLoader {
|
|||
function: async taskId => {
|
||||
await loadManifest();
|
||||
|
||||
const entryChunk = getUrlParameter("chunk");
|
||||
const entryChunk = getUrlParameter("loader-chunk");
|
||||
if(!entryChunk) {
|
||||
loader.critical_error("Missing entry chunk parameter");
|
||||
throw "Missing entry chunk parameter";
|
||||
|
|
|
@ -24,7 +24,7 @@ export let config: Config;
|
|||
export type Task = {
|
||||
name: string,
|
||||
priority: number, /* tasks with the same priority will be executed in sync */
|
||||
function: () => Promise<void>
|
||||
function: (taskId: number) => Promise<void>
|
||||
};
|
||||
export enum Stage {
|
||||
/*
|
||||
|
@ -86,4 +86,5 @@ export type SourcePath = string | DependSource | string[];
|
|||
export type ErrorHandler = (message: string, detail: string) => void;
|
||||
export function critical_error(message: string, detail?: string);
|
||||
export function critical_error_handler(handler?: ErrorHandler, override?: boolean);
|
||||
export function hide_overlay();
|
||||
export function hide_overlay();
|
||||
export function setCurrentTaskName(taskId: number, name: string);
|
|
@ -19305,9 +19305,9 @@
|
|||
}
|
||||
},
|
||||
"webpack-svg-sprite-generator": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack-svg-sprite-generator/-/webpack-svg-sprite-generator-5.0.2.tgz",
|
||||
"integrity": "sha512-i3b4aKgy2VZI7uYYEteKvnv3+mRtG+UfFSCYvnvoGs4dLJ++pDwbGGZL1XaYXN8qbePsHD88JQEvhfkChtx3aw==",
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/webpack-svg-sprite-generator/-/webpack-svg-sprite-generator-5.0.4.tgz",
|
||||
"integrity": "sha512-j+Bl44VoF/Jv0pz0qfU/zeomoVUmWWZ0CGpaHdLH7xwHiCK4xCB2E+xaaSYSDRxoyNZNJEKESOIX+BzKCW4QYg==",
|
||||
"dev": true
|
||||
},
|
||||
"webrtc-adapter": {
|
||||
|
|
|
@ -13,7 +13,8 @@
|
|||
"webpack-web": "webpack --config webpack-web.config.js",
|
||||
"webpack-client": "webpack --config webpack-client.config.js",
|
||||
"generate-i18n-gtranslate": "node shared/generate_i18n_gtranslate.js",
|
||||
"dev-server": "npm run compile-project-base && webpack serve --config webpack-web.config.js"
|
||||
"dev-server": "npm run compile-project-base && webpack serve --config webpack-web.config.js",
|
||||
"dev-server-client": "npm run compile-project-base && webpack serve --config webpack-client.config.js"
|
||||
},
|
||||
"author": "TeaSpeak (WolverinDEV)",
|
||||
"license": "ISC",
|
||||
|
@ -85,7 +86,7 @@
|
|||
"webpack-bundle-analyzer": "^3.6.1",
|
||||
"webpack-cli": "^4.5.0",
|
||||
"webpack-dev-server": "^3.11.2",
|
||||
"webpack-svg-sprite-generator": "^5.0.2",
|
||||
"webpack-svg-sprite-generator": "^5.0.4",
|
||||
"zip-webpack-plugin": "^4.0.1"
|
||||
},
|
||||
"repository": {
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
@import "mixin";
|
||||
|
||||
:global {
|
||||
|
||||
.modal {
|
||||
color: #999999; /* base color */
|
||||
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
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";
|
||||
|
||||
|
||||
console.error("Hello World from devel main");
|
||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "setup",
|
||||
priority: 10,
|
||||
function: async () => {
|
||||
await i18n.initialize();
|
||||
ipc.setupIpcHandler();
|
||||
}
|
||||
});
|
||||
|
||||
loader.register_task(Stage.LOADED, {
|
||||
name: "invoke",
|
||||
priority: 10,
|
||||
function: async () => {
|
||||
}
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
import "../main";
|
|
@ -0,0 +1 @@
|
|||
import "../ui/react-elements/modal/external/renderer/EntryPoint";
|
|
@ -316,7 +316,7 @@ export function select_translation(repository: TranslationRepository, entry: Rep
|
|||
}
|
||||
|
||||
/* ATTENTION: This method is called before most other library initialisations! */
|
||||
export async function initialize() {
|
||||
export async function initializeI18N() {
|
||||
const rcfg = config.repository_config(); /* initialize */
|
||||
const cfg = config.translation_config();
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ assertMainApplication();
|
|||
let preventWelcomeUI = false;
|
||||
async function initialize() {
|
||||
try {
|
||||
await i18n.initialize();
|
||||
await i18n.initializeI18N();
|
||||
} catch(error) {
|
||||
console.error(tr("Failed to initialized the translation system!\nError: %o"), error);
|
||||
loader.critical_error("Failed to setup the translation system");
|
||||
|
|
|
@ -275,16 +275,10 @@ export namespace AppParameters {
|
|||
description: "Peer address of the apps core",
|
||||
};
|
||||
|
||||
export const KEY_MODAL_IDENTITY_CODE: RegistryKey<string> = {
|
||||
key: "modal-identify",
|
||||
export const KEY_MODAL_IPC_CHANNEL: RegistryKey<string> = {
|
||||
key: "modal-channel",
|
||||
valueType: "string",
|
||||
description: "An authentication code used to register the new process as the modal"
|
||||
};
|
||||
|
||||
export const KEY_MODAL_TARGET: RegistryKey<string> = {
|
||||
key: "modal-target",
|
||||
valueType: "string",
|
||||
description: "Target modal unique id which should be loaded"
|
||||
description: "The modal IPC channel id for communication with the controller"
|
||||
};
|
||||
|
||||
export const KEY_LOAD_DUMMY_ERROR: ValuedRegistryKey<boolean> = {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||
import * as React from "react";
|
||||
import {joinClassList} from "tc-shared/ui/react-elements/Helper";
|
||||
|
||||
const cssStyle = require("./Button.scss");
|
||||
|
||||
|
@ -21,12 +22,14 @@ export interface ButtonState {
|
|||
disabled?: boolean
|
||||
}
|
||||
|
||||
export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
|
||||
protected defaultState(): ButtonState {
|
||||
return {
|
||||
export class Button extends React.Component<ButtonProperties, ButtonState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
disabled: undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
if(this.props.hidden)
|
||||
|
@ -34,7 +37,7 @@ export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
|
|||
|
||||
return (
|
||||
<button
|
||||
className={this.classList(
|
||||
className={joinClassList(
|
||||
cssStyle.button,
|
||||
cssStyle["color-" + this.props.color] || cssStyle["color-default"],
|
||||
cssStyle["type-" + this.props.type] || cssStyle["type-normal"],
|
||||
|
|
|
@ -1,169 +0,0 @@
|
|||
import {LogCategory, logDebug, logTrace} from "../../../log";
|
||||
import * as ipc from "../../../ipc/BrowserIPC";
|
||||
import {ChannelMessage} from "../../../ipc/BrowserIPC";
|
||||
import {Registry} from "tc-events";
|
||||
import {
|
||||
EventControllerBase,
|
||||
kPopoutIPCChannelId,
|
||||
Popout2ControllerMessages,
|
||||
PopoutIPCMessage
|
||||
} from "../../../ui/react-elements/external-modal/IPCMessage";
|
||||
import {ModalEvents, ModalOptions, ModalState} from "../../../ui/react-elements/ModalDefinitions";
|
||||
import {guid} from "tc-shared/crypto/uid";
|
||||
import {ModalInstanceController, ModalInstanceEvents} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
|
||||
export abstract class AbstractExternalModalController extends EventControllerBase<"controller"> implements ModalInstanceController {
|
||||
public readonly modalType: string;
|
||||
public readonly constructorArguments: any[];
|
||||
|
||||
private readonly modalEvents: Registry<ModalInstanceEvents>;
|
||||
private modalState: ModalState = ModalState.DESTROYED;
|
||||
|
||||
private readonly documentUnloadListener: () => void;
|
||||
private callbackWindowInitialized: (error?: string) => void;
|
||||
|
||||
protected constructor(modalType: string, constructorArguments: any[]) {
|
||||
super(guid());
|
||||
this.modalType = modalType;
|
||||
this.constructorArguments = constructorArguments;
|
||||
|
||||
this.modalEvents = new Registry<ModalInstanceEvents>();
|
||||
|
||||
this.ipcChannel = ipc.getIpcInstance().createChannel(kPopoutIPCChannelId);
|
||||
this.ipcChannel.messageHandler = this.handleIPCMessage.bind(this);
|
||||
|
||||
this.documentUnloadListener = () => this.destroy();
|
||||
}
|
||||
|
||||
getOptions(): Readonly<ModalOptions> {
|
||||
return {}; /* FIXME! */
|
||||
}
|
||||
|
||||
getEvents(): Registry<ModalInstanceEvents> {
|
||||
return this.modalEvents;
|
||||
}
|
||||
|
||||
getState(): ModalState {
|
||||
return this.modalState;
|
||||
}
|
||||
|
||||
protected abstract spawnWindow() : Promise<boolean>;
|
||||
protected abstract focusWindow() : void;
|
||||
protected abstract destroyWindow() : void;
|
||||
|
||||
async show() {
|
||||
if(this.modalState === ModalState.SHOWN) {
|
||||
this.focusWindow();
|
||||
return;
|
||||
}
|
||||
this.modalState = ModalState.SHOWN;
|
||||
|
||||
if(!await this.spawnWindow()) {
|
||||
this.modalState = ModalState.DESTROYED;
|
||||
throw tr("failed to create window");
|
||||
}
|
||||
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
this.callbackWindowInitialized = undefined;
|
||||
reject("window haven't called back");
|
||||
}, 15000);
|
||||
|
||||
this.callbackWindowInitialized = error => {
|
||||
this.callbackWindowInitialized = undefined;
|
||||
clearTimeout(timeout);
|
||||
error ? reject(error) : resolve();
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
this.modalState = ModalState.DESTROYED;
|
||||
if(__build.mode !== "debug") {
|
||||
/* do not destroy the window in debug mode in order to debug what happened */
|
||||
this.doDestroyWindow();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
window.addEventListener("unload", this.documentUnloadListener);
|
||||
this.modalEvents.fire("notify_open");
|
||||
}
|
||||
|
||||
private doDestroyWindow() {
|
||||
this.destroyWindow();
|
||||
window.removeEventListener("beforeunload", this.documentUnloadListener);
|
||||
}
|
||||
|
||||
async hide() {
|
||||
if(this.modalState == ModalState.DESTROYED || this.modalState === ModalState.HIDDEN)
|
||||
return;
|
||||
|
||||
this.doDestroyWindow();
|
||||
this.modalState = ModalState.HIDDEN;
|
||||
this.modalEvents.fire("notify_close");
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if(this.modalState === ModalState.DESTROYED) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.doDestroyWindow();
|
||||
if(this.ipcChannel) {
|
||||
ipc.getIpcInstance().deleteChannel(this.ipcChannel);
|
||||
}
|
||||
|
||||
this.destroyIPC();
|
||||
this.modalState = ModalState.DESTROYED;
|
||||
this.modalEvents.fire("notify_destroy");
|
||||
}
|
||||
|
||||
protected handleWindowClosed() {
|
||||
/* no other way currently */
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
protected handleIPCMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) {
|
||||
if(!broadcast && remoteId !== this.ipcRemotePeerId) {
|
||||
logDebug(LogCategory.IPC, tr("Received direct IPC message for popout controller from unknown source: %s"), remoteId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleTypedIPCMessage(remoteId, broadcast, message.type as any, message.data);
|
||||
}
|
||||
|
||||
protected handleTypedIPCMessage<T extends Popout2ControllerMessages>(remoteId: string, isBroadcast: boolean, type: T, payload: PopoutIPCMessage[T]) {
|
||||
super.handleTypedIPCMessage(remoteId, isBroadcast, type, payload);
|
||||
|
||||
if(type === "hello-popout") {
|
||||
const messageHello = payload as PopoutIPCMessage["hello-popout"];
|
||||
if(messageHello.authenticationCode !== this.ipcAuthenticationCode) {
|
||||
/* most likely not for us */
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.ipcRemotePeerId) {
|
||||
logTrace(LogCategory.IPC, tr("Modal popout slave changed from %s to %s. Side reload?"), this.ipcRemotePeerId, remoteId);
|
||||
/* TODO: Send a good by to the old modal */
|
||||
}
|
||||
this.ipcRemotePeerId = remoteId;
|
||||
|
||||
logTrace(LogCategory.IPC, "Received Hello World from popup (peer id %s) with version %s (expected %s).", remoteId, messageHello.version, __build.version);
|
||||
if(messageHello.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, constructorArguments: this.constructorArguments });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
import {IPCChannel} from "../../../ipc/BrowserIPC";
|
||||
|
||||
export const kPopoutIPCChannelId = "popout-channel";
|
||||
|
||||
export interface PopoutIPCMessage {
|
||||
"hello-popout": { version: string, authenticationCode: string },
|
||||
"hello-controller": { accepted: boolean, message?: string, constructorArguments?: any[] },
|
||||
"invoke-modal-action": {
|
||||
action: "close" | "minimize"
|
||||
}
|
||||
}
|
||||
|
||||
export type Controller2PopoutMessages = "hello-controller";
|
||||
export type Popout2ControllerMessages = "hello-popout" | "invoke-modal-action";
|
||||
|
||||
export interface SendIPCMessage {
|
||||
"controller": Controller2PopoutMessages;
|
||||
"popout": Popout2ControllerMessages;
|
||||
}
|
||||
|
||||
export interface ReceivedIPCMessage {
|
||||
"controller": Popout2ControllerMessages;
|
||||
"popout": Controller2PopoutMessages;
|
||||
}
|
||||
|
||||
export abstract class EventControllerBase<Type extends "controller" | "popout"> {
|
||||
protected readonly ipcAuthenticationCode: string;
|
||||
protected ipcRemotePeerId: string;
|
||||
protected ipcChannel: IPCChannel;
|
||||
|
||||
protected constructor(ipcAuthenticationCode: string) {
|
||||
this.ipcAuthenticationCode = ipcAuthenticationCode;
|
||||
}
|
||||
|
||||
protected sendIPCMessage<T extends SendIPCMessage[Type]>(type: T, payload: PopoutIPCMessage[T]) {
|
||||
this.ipcChannel.sendMessage(type, payload, this.ipcRemotePeerId);
|
||||
}
|
||||
|
||||
protected handleTypedIPCMessage<T extends ReceivedIPCMessage[Type]>(remoteId: string, isBroadcast: boolean, type: T, payload: PopoutIPCMessage[T]) {
|
||||
|
||||
}
|
||||
|
||||
protected destroyIPC() {
|
||||
this.ipcChannel = undefined;
|
||||
this.ipcRemotePeerId = undefined;
|
||||
}
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
import {getIpcInstance as getIPCInstance} from "../../../ipc/BrowserIPC";
|
||||
import {AppParameters} from "../../../settings";
|
||||
import {
|
||||
Controller2PopoutMessages,
|
||||
EventControllerBase, kPopoutIPCChannelId,
|
||||
PopoutIPCMessage,
|
||||
} from "../../../ui/react-elements/external-modal/IPCMessage";
|
||||
|
||||
let controller: PopoutController;
|
||||
export function getPopoutController() {
|
||||
if(!controller) {
|
||||
controller = new PopoutController();
|
||||
}
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
|
||||
class PopoutController extends EventControllerBase<"popout"> {
|
||||
private constructorArguments: any[];
|
||||
private callbackControllerHello: (accepted: boolean | string) => void;
|
||||
|
||||
constructor() {
|
||||
super(AppParameters.getValue(AppParameters.KEY_MODAL_IDENTITY_CODE, "invalid"));
|
||||
|
||||
this.ipcChannel = getIPCInstance().createChannel(kPopoutIPCChannelId);
|
||||
this.ipcChannel.messageHandler = (sourcePeerId, broadcast, message) => {
|
||||
this.handleTypedIPCMessage(sourcePeerId, broadcast, message.type as any, message.data);
|
||||
};
|
||||
}
|
||||
|
||||
getConstructorArguments() : any[] {
|
||||
return this.constructorArguments;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.sendIPCMessage("hello-popout", { version: __build.version, authenticationCode: this.ipcAuthenticationCode });
|
||||
|
||||
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>(remoteId: string, isBroadcast: boolean, type: T, payload: PopoutIPCMessage[T]) {
|
||||
super.handleTypedIPCMessage(remoteId, isBroadcast, type, payload);
|
||||
|
||||
switch (type) {
|
||||
case "hello-controller": {
|
||||
const tpayload = payload as PopoutIPCMessage["hello-controller"];
|
||||
this.ipcRemotePeerId = remoteId;
|
||||
console.log("Received Hello World from controller (peer id %s). Window instance accepted: %o", this.ipcRemotePeerId, tpayload.accepted);
|
||||
if(!this.callbackControllerHello) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.constructorArguments = tpayload.constructorArguments;
|
||||
this.callbackControllerHello(tpayload.accepted ? true : tpayload.message || false);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn("Received unknown message type from controller: %s", type);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
doClose() {
|
||||
this.sendIPCMessage("invoke-modal-action", { action: "close" });
|
||||
}
|
||||
|
||||
doMinimize() {
|
||||
this.sendIPCMessage("invoke-modal-action", { action: "minimize" });
|
||||
}
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
import * as loader from "tc-loader";
|
||||
import * as ipc from "../../../ipc/BrowserIPC";
|
||||
import * as i18n from "../../../i18n/localize";
|
||||
import {Stage} from "tc-loader";
|
||||
import {AbstractModal} from "../../../ui/react-elements/ModalDefinitions";
|
||||
import {AppParameters} from "../../../settings";
|
||||
import {getPopoutController} from "./PopoutController";
|
||||
import {setupJSRender} from "../../../ui/jsrender";
|
||||
import {findRegisteredModal} from "tc-shared/ui/react-elements/modal/Registry";
|
||||
import {ModalRenderer} from "tc-shared/ui/react-elements/external-modal/ModalRenderer";
|
||||
import {constructAbstractModalClass} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
import "../../../file/RemoteAvatars";
|
||||
import "../../../file/RemoteIcons";
|
||||
|
||||
let modalRenderer: ModalRenderer;
|
||||
let modalInstance: AbstractModal;
|
||||
let modalClass: new (...args: any[]) => AbstractModal;
|
||||
|
||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "setup",
|
||||
priority: 110,
|
||||
function: async () => {
|
||||
await import("tc-shared/proto");
|
||||
await i18n.initialize();
|
||||
ipc.setupIpcHandler();
|
||||
|
||||
setupJSRender();
|
||||
}
|
||||
});
|
||||
|
||||
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 renderer loader",
|
||||
priority: 10,
|
||||
function: async () => {
|
||||
modalRenderer = new ModalRenderer({
|
||||
close() {
|
||||
getPopoutController().doClose()
|
||||
},
|
||||
minimize() {
|
||||
getPopoutController().doMinimize()
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "modal class loader",
|
||||
priority: 10,
|
||||
function: async () => {
|
||||
const modalTarget = AppParameters.getValue(AppParameters.KEY_MODAL_TARGET, "unknown");
|
||||
console.error("Loading modal class %s", modalTarget);
|
||||
try {
|
||||
const registeredModal = findRegisteredModal(modalTarget as any);
|
||||
if(!registeredModal) {
|
||||
loader.critical_error("Missing popout handler", "Handler " + modalTarget + " is missing.");
|
||||
throw "missing handler";
|
||||
}
|
||||
|
||||
modalClass = (await registeredModal.classLoader()).default;
|
||||
} 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 = constructAbstractModalClass(modalClass, { windowed: true }, getPopoutController().getConstructorArguments());
|
||||
modalRenderer.renderModal(modalInstance);
|
||||
} catch(error) {
|
||||
loader.critical_error("Failed to invoker modal", "Lookup the console for more detail");
|
||||
console.error("Failed to load modal: %o", error);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,17 +0,0 @@
|
|||
import {ModalInstanceController, ModalOptions} from "../modal/Definitions";
|
||||
import "./Controller";
|
||||
|
||||
export type ControllerFactory = (modalType: string, constructorArguments?: any[], options?: ModalOptions) => ModalInstanceController;
|
||||
|
||||
let modalControllerFactory: ControllerFactory;
|
||||
export function setExternalModalControllerFactory(factory: ControllerFactory) {
|
||||
modalControllerFactory = factory;
|
||||
}
|
||||
|
||||
export function spawnExternalModal<EventClass extends { [key: string]: any }>(modalType: string, constructorArguments: any[], options: ModalOptions) : ModalInstanceController {
|
||||
if(typeof modalControllerFactory === "undefined") {
|
||||
throw tr("No external modal factory has been set");
|
||||
}
|
||||
|
||||
return modalControllerFactory(modalType, constructorArguments, options);
|
||||
}
|
|
@ -10,9 +10,11 @@ import {
|
|||
} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
import {Registry} from "tc-events";
|
||||
import {findRegisteredModal, RegisteredModal} from "tc-shared/ui/react-elements/modal/Registry";
|
||||
import {spawnExternalModal} from "tc-shared/ui/react-elements/external-modal";
|
||||
import {InternalModalInstance} from "tc-shared/ui/react-elements/modal/internal";
|
||||
import {assertMainApplication} from "tc-shared/ui/utils";
|
||||
import {InternalModalInstance} from "./internal";
|
||||
import {ExternalModalController} from "./external/Controller";
|
||||
|
||||
assertMainApplication();
|
||||
export class GenericModalController<T extends keyof ModalConstructorArguments> implements ModalController {
|
||||
private readonly events: Registry<ModalEvents>;
|
||||
|
||||
|
@ -66,7 +68,7 @@ export class GenericModalController<T extends keyof ModalConstructorArguments> i
|
|||
|
||||
private createModalInstance() {
|
||||
if(this.popedOut) {
|
||||
this.instance = spawnExternalModal(this.modalType, this.modalConstructorArguments, this.modalOptions);
|
||||
this.instance = new ExternalModalController(this.modalType, this.modalConstructorArguments, this.modalOptions);
|
||||
} else {
|
||||
this.instance = new InternalModalInstance(this.getModalClass(), this.modalConstructorArguments, this.modalOptions);
|
||||
}
|
||||
|
@ -74,6 +76,11 @@ export class GenericModalController<T extends keyof ModalConstructorArguments> i
|
|||
const events = this.instance.getEvents();
|
||||
events.on("notify_destroy", events.on("notify_open", () => this.events.fire("open")));
|
||||
events.on("notify_destroy", events.on("notify_close", () => this.events.fire("close")));
|
||||
events.on("notify_destroy", () => {
|
||||
if(this.instance) {
|
||||
this.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
events.on("action_close", () => this.destroy());
|
||||
events.on("action_popout", () => {
|
||||
|
@ -99,8 +106,9 @@ export class GenericModalController<T extends keyof ModalConstructorArguments> i
|
|||
}
|
||||
|
||||
private destroyModalInstance() {
|
||||
this.instance?.destroy();
|
||||
const instance = this.instance;
|
||||
this.instance = undefined;
|
||||
instance?.destroy();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
|
|
@ -69,10 +69,12 @@ export enum ModalState {
|
|||
}
|
||||
|
||||
export interface ModalInstanceEvents {
|
||||
/* Actions which must be implemented by our modal owner */
|
||||
action_close: {},
|
||||
action_minimize: {},
|
||||
action_popout: {},
|
||||
|
||||
/* State changes we encountered */
|
||||
notify_open: {}
|
||||
notify_minimize: {},
|
||||
notify_close: {},
|
||||
|
|
|
@ -135,6 +135,27 @@ html:root {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
&.windowed {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.modalTitle {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.modalBody {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modalWindowContainer {
|
||||
|
|
|
@ -123,11 +123,12 @@ export class ModalBodyRenderer extends React.PureComponent<{
|
|||
}
|
||||
|
||||
export class ModalFrameRenderer extends React.PureComponent<{
|
||||
windowed: boolean,
|
||||
children: [React.ReactElement<ModalFrameTopRenderer>, React.ReactElement<ModalBodyRenderer>]
|
||||
}> {
|
||||
render() {
|
||||
return (
|
||||
<div className={cssStyle.modalFrame}>
|
||||
<div className={cssStyle.modalFrame + " " + (this.props.windowed ? cssStyle.windowed : "")}>
|
||||
{this.props.children[0]}
|
||||
{this.props.children[1]}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,219 @@
|
|||
import {LogCategory, logError, logInfo, logWarn} from "tc-shared/log";
|
||||
import {getIpcInstance, IPCChannel} from "tc-shared/ipc/BrowserIPC";
|
||||
import {Registry} from "tc-events";
|
||||
import {ModalOptions} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
import {guid} from "tc-shared/crypto/uid";
|
||||
import {ModalInstanceController, ModalInstanceEvents, ModalState} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
import {getWindowManager} from "tc-shared/ui/windows/WindowManager";
|
||||
import {assertMainApplication} from "tc-shared/ui/utils";
|
||||
import {
|
||||
ModalIPCController2Renderer,
|
||||
ModalIPCRenderer2ControllerMessages
|
||||
} from "tc-shared/ui/react-elements/modal/external/Definitions";
|
||||
|
||||
assertMainApplication();
|
||||
export class ExternalModalController implements ModalInstanceController {
|
||||
private readonly modalType: string;
|
||||
private readonly modalOptions: ModalOptions;
|
||||
private readonly constructorArguments: any[];
|
||||
private readonly mainModalId: string;
|
||||
|
||||
private readonly ipcMessageHandler: { [key: string]: (sourcePeerId: string, message: any) => void };
|
||||
private ipcRemotePeerId: string;
|
||||
private ipcChannel: IPCChannel;
|
||||
|
||||
private readonly modalEvents: Registry<ModalInstanceEvents>;
|
||||
private modalInitializeCallback: () => void;
|
||||
|
||||
private windowId: string | undefined;
|
||||
private windowListener: (() => void)[];
|
||||
private windowMutatePromise: Promise<void>;
|
||||
|
||||
constructor(modalType: string, constructorArguments: any[], modalOptions: ModalOptions) {
|
||||
this.modalType = modalType;
|
||||
this.modalOptions = modalOptions;
|
||||
this.constructorArguments = constructorArguments;
|
||||
this.mainModalId = guid();
|
||||
|
||||
this.modalEvents = new Registry<ModalInstanceEvents>();
|
||||
|
||||
this.ipcMessageHandler = {};
|
||||
this.ipcChannel = getIpcInstance().createChannel(guid());
|
||||
this.ipcChannel.messageHandler = (sourcePeerId, broadcast, message) => {
|
||||
if(sourcePeerId !== this.ipcRemotePeerId && message.type !== "hello-renderer") {
|
||||
return;
|
||||
}
|
||||
|
||||
if(typeof this.ipcMessageHandler[message.type] !== "function") {
|
||||
logWarn(LogCategory.IPC, tr("Received remote modal message but we don't know how to handle the message (%s)."), message.type);
|
||||
return;
|
||||
}
|
||||
|
||||
this.ipcMessageHandler[message.type](sourcePeerId, message.data);
|
||||
};
|
||||
|
||||
this.registerIpcMessageHandler("hello-renderer", (sourcePeerId, message) => {
|
||||
if(this.ipcRemotePeerId) {
|
||||
logInfo(LogCategory.IPC, tr("Modal %s got reloaded (Version: %s). Using new peer address %s instead of %s and initializing it."), this.modalType, message.version, this.ipcRemotePeerId, sourcePeerId);
|
||||
this.sendIpcMessage("invalidate-modal-instance", { });
|
||||
} else {
|
||||
logInfo(LogCategory.IPC, tr("Modal %s has called back (Version: %s). Initializing it."), this.modalType, message.version);
|
||||
}
|
||||
|
||||
this.ipcRemotePeerId = sourcePeerId;
|
||||
if(message.version !== __build.version) {
|
||||
this.sendIpcMessage("hello-controller", { accepted: false, message: tr("version miss match") });
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.modalInitializeCallback) {
|
||||
this.modalInitializeCallback();
|
||||
}
|
||||
|
||||
this.sendIpcMessage("hello-controller", {
|
||||
accepted: true,
|
||||
|
||||
modalId: this.mainModalId,
|
||||
modalType: this.modalType,
|
||||
constructorArguments: this.constructorArguments
|
||||
});
|
||||
});
|
||||
|
||||
this.registerIpcMessageHandler("invoke-modal-action", (sourcePeerId, message) => {
|
||||
if(message.modalId !== this.mainModalId) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (message.action) {
|
||||
case "minimize":
|
||||
this.modalEvents.fire("action_minimize");
|
||||
break;
|
||||
|
||||
case "close":
|
||||
this.modalEvents.fire("action_close");
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.hide().then(undefined);
|
||||
this.modalEvents.destroy();
|
||||
}
|
||||
|
||||
getEvents(): Registry<ModalInstanceEvents> {
|
||||
return this.modalEvents;
|
||||
}
|
||||
|
||||
getState(): ModalState {
|
||||
return typeof this.windowId === "string" ? ModalState.SHOWN : ModalState.DESTROYED;
|
||||
}
|
||||
|
||||
async show(): Promise<void> {
|
||||
await this.mutateWindow(async () => {
|
||||
const windowManager = getWindowManager();
|
||||
if(typeof this.windowId === "string") {
|
||||
if(windowManager.isActionSupported(this.windowId, "focus")) {
|
||||
await windowManager.executeAction(this.windowId, "focus");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await windowManager.createWindow({
|
||||
uniqueId: this.modalOptions.uniqueId || this.modalType,
|
||||
loaderTarget: "modal-external",
|
||||
|
||||
windowName: "modal " + this.modalType,
|
||||
|
||||
defaultSize: this.modalOptions.defaultSize,
|
||||
appParameters: {
|
||||
"modal-channel": this.ipcChannel.channelId,
|
||||
}
|
||||
});
|
||||
|
||||
if(result.status === "error-user-rejected") {
|
||||
throw tr("user rejected");
|
||||
} else if(result.status === "error-unknown") {
|
||||
throw result.message;
|
||||
}
|
||||
|
||||
this.windowListener = [];
|
||||
this.windowListener.push(windowManager.getEvents().on("notify_window_destroyed", event => {
|
||||
if(event.windowId !== this.windowId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleWindowDestroyed();
|
||||
}));
|
||||
|
||||
this.windowId = result.windowId;
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
this.modalInitializeCallback = resolve;
|
||||
setTimeout(reject, 15000);
|
||||
});
|
||||
} catch (_) {
|
||||
logError(LogCategory.IPC, tr("Opened modal failed to call back within 15 seconds."));
|
||||
getWindowManager().destroyWindow(this.windowId);
|
||||
} finally {
|
||||
this.modalInitializeCallback = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async hide(): Promise<void> {
|
||||
await this.mutateWindow(async () => {
|
||||
if(typeof this.windowId !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
getWindowManager().destroyWindow(this.windowId);
|
||||
});
|
||||
}
|
||||
|
||||
private async mutateWindow<T>(callback: () => Promise<T>) : Promise<T> {
|
||||
while(this.windowMutatePromise) {
|
||||
await this.windowMutatePromise;
|
||||
}
|
||||
|
||||
return await new Promise((resolveOuter, rejectOuter) => {
|
||||
const promise = callback();
|
||||
this.windowMutatePromise = new Promise<void>(resolve => {
|
||||
promise.then(result => {
|
||||
this.windowMutatePromise = undefined;
|
||||
resolve();
|
||||
|
||||
resolveOuter(result);
|
||||
}).catch(error => {
|
||||
this.windowMutatePromise = undefined;
|
||||
resolve();
|
||||
|
||||
rejectOuter(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private handleWindowDestroyed() {
|
||||
this.windowId = undefined;
|
||||
this.windowListener.forEach(callback => callback());
|
||||
this.modalEvents.fire("notify_destroy");
|
||||
}
|
||||
|
||||
private registerIpcMessageHandler<T extends keyof ModalIPCRenderer2ControllerMessages>(
|
||||
type: T,
|
||||
handler: (sourcePeerId: string, message: ModalIPCRenderer2ControllerMessages[T]) => void
|
||||
) {
|
||||
this.ipcMessageHandler[type] = handler;
|
||||
}
|
||||
|
||||
private sendIpcMessage<T extends keyof ModalIPCController2Renderer>(type: T, message: ModalIPCController2Renderer[T]) {
|
||||
if(!this.ipcRemotePeerId) {
|
||||
logWarn(LogCategory.IPC, tr("Tried to send a modal message but we don't know the modals peer address."));
|
||||
return;
|
||||
}
|
||||
|
||||
this.ipcChannel.sendMessage(type, message, this.ipcRemotePeerId);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
export interface ModalIPCMessages {
|
||||
"hello-renderer": { version: string },
|
||||
"hello-controller": {
|
||||
accepted: true,
|
||||
|
||||
modalId: string,
|
||||
modalType: string,
|
||||
constructorArguments: any[],
|
||||
} | {
|
||||
accepted: false,
|
||||
message: string
|
||||
},
|
||||
/*
|
||||
"create-inline-modal": {
|
||||
modalId: string,
|
||||
modalType: string,
|
||||
constructorArguments: any[],
|
||||
},
|
||||
"destroy-inline-modal": {
|
||||
|
||||
},
|
||||
*/
|
||||
"invoke-modal-action": {
|
||||
modalId: string,
|
||||
action: "close" | "minimize"
|
||||
},
|
||||
|
||||
/* The controller has a new peer which authenticated for the modal */
|
||||
"invalidate-modal-instance": {}
|
||||
}
|
||||
|
||||
export type ModalIPCRenderer2ControllerMessages = Pick<ModalIPCMessages, "hello-renderer" | "invoke-modal-action">;
|
||||
export type ModalIPCController2Renderer = Pick<ModalIPCMessages, "hello-controller" | "invalidate-modal-instance">;
|
||||
|
||||
export type ModalIPCMessage<Messages, T extends keyof Messages = keyof Messages> = {
|
||||
type: T,
|
||||
payload: Messages[T]
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
import {getIpcInstance, IPCChannel} from "tc-shared/ipc/BrowserIPC";
|
||||
import {
|
||||
ModalIPCController2Renderer,
|
||||
ModalIPCRenderer2ControllerMessages
|
||||
} from "../Definitions";
|
||||
import {LogCategory, logDebug, logWarn} from "tc-shared/log";
|
||||
|
||||
export type ModalInstanceInitializeResult = {
|
||||
status: "success",
|
||||
|
||||
modalId: string,
|
||||
modalType: string,
|
||||
constructorArguments: any[],
|
||||
} | {
|
||||
status: "timeout"
|
||||
} | {
|
||||
status: "rejected",
|
||||
message: string
|
||||
};
|
||||
|
||||
export class ModalWindowControllerInstance {
|
||||
private readonly ipcMessageHandler: { [key: string]: (sourcePeerId: string, message: any) => void };
|
||||
private ipcRemotePeerId: string;
|
||||
private ipcChannel: IPCChannel;
|
||||
|
||||
constructor(modalChannelId: string) {
|
||||
this.ipcMessageHandler = {};
|
||||
this.ipcChannel = getIpcInstance().createCoreControlChannel(modalChannelId);
|
||||
this.ipcChannel.messageHandler = (sourcePeerId, broadcast, message) => {
|
||||
if(this.ipcRemotePeerId && sourcePeerId !== this.ipcRemotePeerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(typeof this.ipcMessageHandler[message.type] !== "function") {
|
||||
logWarn(LogCategory.IPC, tr("Received remote controller message but we don't know how to handle the message (%s)."), message.type);
|
||||
return;
|
||||
}
|
||||
|
||||
this.ipcMessageHandler[message.type](sourcePeerId, message.data);
|
||||
};
|
||||
|
||||
this.registerIpcMessageHandler("invalidate-modal-instance", () => {
|
||||
logWarn(LogCategory.IPC, tr("This modal instance has been invalidated."));
|
||||
/* TODO: Show so on the screen. */
|
||||
});
|
||||
}
|
||||
|
||||
async initialize() : Promise<ModalInstanceInitializeResult> {
|
||||
this.sendIpcMessage("hello-renderer", {
|
||||
version: __build.version
|
||||
});
|
||||
|
||||
let controllerResult: ModalIPCController2Renderer["hello-controller"];
|
||||
try {
|
||||
controllerResult = await new Promise((resolve, reject) => {
|
||||
this.registerIpcMessageHandler("hello-controller", (sourcePeerId, message) => {
|
||||
logDebug(LogCategory.IPC, tr("Found remote controller peer id: %s"), sourcePeerId);
|
||||
this.ipcRemotePeerId = sourcePeerId;
|
||||
resolve(message);
|
||||
});
|
||||
|
||||
setTimeout(reject, 15000);
|
||||
});
|
||||
} catch (_) {
|
||||
return { status: "timeout" };
|
||||
}
|
||||
|
||||
if(controllerResult.accepted === true) {
|
||||
return {
|
||||
status: "success",
|
||||
|
||||
modalId: controllerResult.modalId,
|
||||
modalType: controllerResult.modalType,
|
||||
constructorArguments: controllerResult.constructorArguments
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: "rejected",
|
||||
message: controllerResult.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
triggerModalAction(modalId: string, action: "minimize" | "close") {
|
||||
this.sendIpcMessage("invoke-modal-action", {
|
||||
modalId: modalId,
|
||||
action: action
|
||||
});
|
||||
}
|
||||
|
||||
private registerIpcMessageHandler<T extends keyof ModalIPCController2Renderer>(
|
||||
type: T,
|
||||
handler: (sourcePeerId: string, message: ModalIPCController2Renderer[T]) => void
|
||||
) {
|
||||
this.ipcMessageHandler[type] = handler;
|
||||
}
|
||||
|
||||
private sendIpcMessage<T extends keyof ModalIPCRenderer2ControllerMessages>(type: T, message: ModalIPCRenderer2ControllerMessages[T]) {
|
||||
if(!this.ipcRemotePeerId && type !== "hello-renderer") {
|
||||
logWarn(LogCategory.IPC, tr("Tried to send a controller message but we don't know the controllers peer address."));
|
||||
return;
|
||||
}
|
||||
|
||||
this.ipcChannel.sendMessage(type, message, this.ipcRemotePeerId);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
import * as loader from "tc-loader";
|
||||
import {setupIpcHandler} from "tc-shared/ipc/BrowserIPC";
|
||||
import {initializeI18N} from "tc-shared/i18n/localize";
|
||||
import {Stage} from "tc-loader";
|
||||
import {AbstractModal, constructAbstractModalClass} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
import {AppParameters} from "tc-shared/settings";
|
||||
import {setupJSRender} from "tc-shared/ui/jsrender";
|
||||
import {findRegisteredModal} from "tc-shared/ui/react-elements/modal/Registry";
|
||||
import {ModalWindowControllerInstance} from "./Controller";
|
||||
import {LogCategory, logError, logInfo} from "tc-shared/log";
|
||||
import {ModalRenderer} from "./ModalRenderer";
|
||||
|
||||
import "../../../../../file/RemoteAvatars";
|
||||
import "../../../../../file/RemoteIcons";
|
||||
|
||||
let instanceController: ModalWindowControllerInstance;
|
||||
|
||||
let mainModalId: string;
|
||||
let mainModalRenderer: ModalRenderer;
|
||||
let mainModalInstance: AbstractModal;
|
||||
|
||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "setup",
|
||||
priority: 110,
|
||||
function: async () => {
|
||||
await import("tc-shared/proto");
|
||||
await initializeI18N();
|
||||
setupIpcHandler();
|
||||
|
||||
setupJSRender();
|
||||
}
|
||||
});
|
||||
|
||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "modal renderer initialize",
|
||||
priority: 100,
|
||||
function: async () => {
|
||||
mainModalRenderer = new ModalRenderer({
|
||||
close() {
|
||||
instanceController?.triggerModalAction(mainModalId, "close");
|
||||
},
|
||||
minimize() {
|
||||
instanceController?.triggerModalAction(mainModalId, "minimize");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "modal initialize",
|
||||
priority: 10,
|
||||
function: initializeModalRenderer
|
||||
});
|
||||
|
||||
async function initializeModalRenderer(taskId) {
|
||||
loader.setCurrentTaskName(taskId, tr("connecting to controller"));
|
||||
instanceController = new ModalWindowControllerInstance(AppParameters.getValue(AppParameters.KEY_MODAL_IPC_CHANNEL, "invalid"));
|
||||
const result = await instanceController.initialize();
|
||||
if(result.status === "timeout") {
|
||||
loader.critical_error("Modal controller timeout", "Modal controller failed to call back.");
|
||||
throw "modal controller timeout";
|
||||
} else if(result.status === "rejected") {
|
||||
loader.critical_error("Modal controller reject", result.message || tr("unknown why"));
|
||||
throw "modal controller reject";
|
||||
}
|
||||
|
||||
mainModalId = result.modalId;
|
||||
|
||||
loader.setCurrentTaskName(taskId, tr("loading modal class"));
|
||||
let modalClass: new (...args: any[]) => AbstractModal;
|
||||
logInfo(LogCategory.GENERAL, tr("Loading modal class %s"), result.modalType);
|
||||
try {
|
||||
const registeredModal = findRegisteredModal(result.modalType as any);
|
||||
if(!registeredModal) {
|
||||
loader.critical_error(tr("Unknown modal"), tra("Modal {} is unknown", result.modalType));
|
||||
throw "missing modal";
|
||||
}
|
||||
|
||||
modalClass = (await registeredModal.classLoader()).default;
|
||||
} catch(error) {
|
||||
loader.critical_error("Failed to load modal", "Lookup the console for more detail");
|
||||
logError(LogCategory.GENERAL,tr("Failed to load main modal %s: %o"), result.modalType, error);
|
||||
}
|
||||
|
||||
loader.setCurrentTaskName(taskId, tr("initializing modal class"));
|
||||
try {
|
||||
mainModalInstance = constructAbstractModalClass(modalClass, { windowed: true }, result.constructorArguments);
|
||||
mainModalRenderer.renderModal(mainModalInstance);
|
||||
} catch(error) {
|
||||
loader.critical_error("Failed to invoker modal", "Lookup the console for more detail");
|
||||
logError(LogCategory.GENERAL,tr("Failed to load modal: %o"), error);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
@import "../../../../css/static/mixin";
|
||||
@import "../../../../../../css/static/mixin";
|
||||
|
||||
/* FIXME: Remove this wired import */
|
||||
@import "../../../../css/static/general";
|
||||
@import "../../../../css/static/modal";
|
||||
@import "../../../../../../css/static/general";
|
||||
@import "../../../../../../css/static/modal";
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
|
@ -15,4 +15,4 @@ html, body {
|
|||
color: #999;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
|
@ -33,7 +33,7 @@ export class ModalRenderer {
|
|||
|
||||
if(__build.target === "client") {
|
||||
ReactDOM.render(
|
||||
<ModalFrameRenderer>
|
||||
<ModalFrameRenderer windowed={true}>
|
||||
<ModalFrameTopRenderer
|
||||
replacePageTitle={true}
|
||||
modalInstance={modal}
|
|
@ -66,7 +66,7 @@ export class InternalModalInstance implements ModalInstanceController {
|
|||
await new Promise(resolve => {
|
||||
ReactDOM.render(
|
||||
<PageModalRenderer modalInstance={this.modalInstance} onBackdropClicked={this.getCloseCallback()} ref={this.rendererInstance}>
|
||||
<ModalFrameRenderer>
|
||||
<ModalFrameRenderer windowed={false}>
|
||||
<ModalFrameTopRenderer
|
||||
replacePageTitle={false}
|
||||
modalInstance={this.modalInstance}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import {Registry} from "tc-events";
|
||||
|
||||
export type WindowCreateResult = {
|
||||
status: "success",
|
||||
windowId: string
|
||||
} | {
|
||||
status: "error-unknown",
|
||||
message: string
|
||||
} | {
|
||||
status: "error-user-rejected",
|
||||
};
|
||||
|
||||
export interface WindowManagerEvents {
|
||||
notify_window_created: { windowId: string },
|
||||
notify_window_focused: { windowId: string },
|
||||
notify_window_destroyed: { windowId: string },
|
||||
}
|
||||
|
||||
export type WindowAction = "focus" | "maximize" | "minimize";
|
||||
|
||||
export interface WindowSpawnOptions {
|
||||
uniqueId: string,
|
||||
loaderTarget: string,
|
||||
/* if the window hasn't been created within a user gesture the client will be prompted if he wants to open the window */
|
||||
windowName?: string,
|
||||
|
||||
appParameters?: {[key: string]: string},
|
||||
defaultSize?: { width: number, height: number },
|
||||
}
|
||||
|
||||
export interface WindowManager {
|
||||
getEvents() : Registry<WindowManagerEvents>;
|
||||
|
||||
createWindow(options: WindowSpawnOptions) : Promise<WindowCreateResult>;
|
||||
destroyWindow(windowId: string);
|
||||
|
||||
getActiveWindowId() : string | undefined;
|
||||
|
||||
isActionSupported(windowId: string, action: WindowAction) : boolean;
|
||||
executeAction(windowId: string, action: WindowAction) : Promise<void>;
|
||||
}
|
||||
|
||||
let windowManager: WindowManager;
|
||||
export function getWindowManager() : WindowManager {
|
||||
return windowManager;
|
||||
}
|
||||
|
||||
export function setWindowManager(newWindowManager: WindowManager) {
|
||||
windowManager = newWindowManager;
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
import {AbstractExternalModalController} from "tc-shared/ui/react-elements/external-modal/Controller";
|
||||
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||
import {ChannelMessage, getIpcInstance} from "tc-shared/ipc/BrowserIPC";
|
||||
import {LogCategory, logDebug, logWarn} from "tc-shared/log";
|
||||
import {Popout2ControllerMessages, PopoutIPCMessage} from "tc-shared/ui/react-elements/external-modal/IPCMessage";
|
||||
import {tr, tra} from "tc-shared/i18n/localize";
|
||||
import {ModalOptions} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
import {assertMainApplication} from "tc-shared/ui/utils";
|
||||
|
||||
assertMainApplication();
|
||||
|
||||
export class ExternalModalController extends AbstractExternalModalController {
|
||||
private readonly options: ModalOptions;
|
||||
private currentWindow: Window;
|
||||
private windowClosedTestInterval: number = 0;
|
||||
private windowClosedTimeout: number;
|
||||
|
||||
constructor(modalType: string, constructorArguments: any[] | undefined, options: ModalOptions | undefined) {
|
||||
super(modalType, constructorArguments);
|
||||
this.options = options || {};
|
||||
}
|
||||
|
||||
protected async spawnWindow() : Promise<boolean> {
|
||||
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,
|
||||
"modal-identify": this.ipcAuthenticationCode,
|
||||
"ipc-address": getIpcInstance().getApplicationChannelId(),
|
||||
"ipc-core-peer": getIpcInstance().getLocalPeerId(),
|
||||
"disableGlobalContextMenu": __build.mode === "debug" ? 1 : 0,
|
||||
"loader-abort": __build.mode === "debug" ? 1 : 0,
|
||||
};
|
||||
|
||||
const options = this.getOptions();
|
||||
const features = {
|
||||
status: "no",
|
||||
location: "no",
|
||||
toolbar: "no",
|
||||
menubar: "no",
|
||||
resizable: "yes",
|
||||
width: options.defaultSize?.width,
|
||||
height: options.defaultSize?.height
|
||||
};
|
||||
|
||||
let baseUrl = location.origin + location.pathname + "?";
|
||||
return window.open(
|
||||
baseUrl + Object.keys(parameters).map(e => e + "=" + encodeURIComponent(parameters[e])).join("&"),
|
||||
this.options?.uniqueId || this.modalType,
|
||||
Object.keys(features).map(e => e + "=" + features[e]).join(",")
|
||||
);
|
||||
}
|
||||
|
||||
protected handleIPCMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) {
|
||||
if(!broadcast && this.ipcRemotePeerId !== 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);
|
||||
}
|
||||
|
||||
protected handleTypedIPCMessage<T extends Popout2ControllerMessages>(remoteId: string, isBroadcast: boolean, type: T, payload: PopoutIPCMessage[T]) {
|
||||
super.handleTypedIPCMessage(remoteId, isBroadcast, type, payload);
|
||||
|
||||
if(isBroadcast) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "invoke-modal-action":
|
||||
const data = payload as PopoutIPCMessage["invoke-modal-action"];
|
||||
switch (data.action) {
|
||||
case "close":
|
||||
this.destroy();
|
||||
break;
|
||||
|
||||
case "minimize":
|
||||
window.focus();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,7 +7,9 @@ window.addEventListener("beforeunload", event => {
|
|||
}
|
||||
|
||||
const active_connections = server_connections.getAllConnectionHandlers().filter(e => e.connected);
|
||||
if(active_connections.length == 0) return;
|
||||
if(active_connections.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.returnValue = "Are you really sure?<br>You're still connected!";
|
||||
});
|
|
@ -0,0 +1,197 @@
|
|||
import {
|
||||
WindowAction,
|
||||
WindowCreateResult,
|
||||
WindowManager,
|
||||
WindowManagerEvents,
|
||||
WindowSpawnOptions
|
||||
} from "tc-shared/ui/windows/WindowManager";
|
||||
import {assertMainApplication} from "tc-shared/ui/utils";
|
||||
import {Registry} from "tc-events";
|
||||
import {getIpcInstance} from "tc-shared/ipc/BrowserIPC";
|
||||
import _ from "lodash";
|
||||
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||
import {tr, tra} from "tc-shared/i18n/localize";
|
||||
import {guid} from "tc-shared/crypto/uid";
|
||||
|
||||
assertMainApplication();
|
||||
|
||||
type WindowHandle = {
|
||||
window: Window,
|
||||
uniqueId: string,
|
||||
|
||||
destroy: () => void,
|
||||
|
||||
closeTestInterval: number | undefined,
|
||||
closeTestTimeout: number | undefined,
|
||||
}
|
||||
|
||||
export class WebWindowManager implements WindowManager {
|
||||
private readonly events: Registry<WindowManagerEvents>;
|
||||
private registeredWindows: { [key: string]: WindowHandle } = {};
|
||||
|
||||
constructor() {
|
||||
this.events = new Registry<WindowManagerEvents>();
|
||||
/* TODO: Close all active windows on page unload */
|
||||
}
|
||||
|
||||
getEvents(): Registry<WindowManagerEvents> {
|
||||
return this.events;
|
||||
}
|
||||
|
||||
async createWindow(options: WindowSpawnOptions): Promise<WindowCreateResult> {
|
||||
/* Multiple application instance may want to open the same windows */
|
||||
const windowUniqueId = getIpcInstance().getApplicationChannelId() + "-" + options.uniqueId;
|
||||
|
||||
/* If we're opening a window with the same unique id we need to destroy the old handle */
|
||||
for(const windowId of Object.keys(this.registeredWindows)) {
|
||||
if(this.registeredWindows[windowId].uniqueId === windowUniqueId) {
|
||||
this.registeredWindows[windowId].destroy();
|
||||
}
|
||||
}
|
||||
|
||||
let windowInstance = this.tryCreateWindow(options, windowUniqueId);
|
||||
if(!windowInstance) {
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
spawnYesNo(tr("Would you like to open the popup?"), tra("Would you like to open window {}?", options.windowName), callback => {
|
||||
if(!callback) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
windowInstance = this.tryCreateWindow(options, windowUniqueId);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} catch (_) {
|
||||
return { status: "error-user-rejected" };
|
||||
}
|
||||
}
|
||||
|
||||
if(!windowInstance) {
|
||||
return { status: "error-user-rejected" };
|
||||
}
|
||||
|
||||
const windowId = guid();
|
||||
const windowHandle = this.registeredWindows[windowId] = {
|
||||
window: windowInstance,
|
||||
uniqueId: windowUniqueId,
|
||||
|
||||
closeTestInterval: 0,
|
||||
closeTestTimeout: 0,
|
||||
|
||||
destroy: undefined
|
||||
};
|
||||
|
||||
const handleWindowClosed = () => {
|
||||
if(windowHandle.window && !windowHandle.window.closed) {
|
||||
windowHandle.window.onbeforeunload = undefined;
|
||||
windowHandle.window.onunload = undefined;
|
||||
windowHandle.window.onclose = undefined;
|
||||
windowHandle.window.close();
|
||||
}
|
||||
|
||||
clearInterval(windowHandle.closeTestInterval);
|
||||
clearTimeout(windowHandle.closeTestTimeout);
|
||||
if(!this.registeredWindows[windowId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete this.registeredWindows[windowId];
|
||||
this.events.fire("notify_window_destroyed", { windowId: windowId });
|
||||
};
|
||||
|
||||
const testWindowClosed = (timeout: number) => {
|
||||
clearInterval(windowHandle.closeTestInterval);
|
||||
clearTimeout(windowHandle.closeTestTimeout);
|
||||
|
||||
windowHandle.closeTestInterval = setInterval(() => {
|
||||
/* !== is required for compatibility with Opera */
|
||||
if(windowHandle.window.closed !== false) {
|
||||
handleWindowClosed();
|
||||
}
|
||||
}, 100);
|
||||
windowHandle.closeTestTimeout = setTimeout(() => {
|
||||
clearInterval(windowHandle.closeTestInterval);
|
||||
clearTimeout(windowHandle.closeTestTimeout);
|
||||
}, timeout);
|
||||
};
|
||||
|
||||
windowInstance.onbeforeunload = () => testWindowClosed(5000);
|
||||
windowInstance.onunload = () => testWindowClosed(2500);
|
||||
windowInstance.onclose = () => testWindowClosed(2500);
|
||||
|
||||
windowHandle.destroy = () => handleWindowClosed();
|
||||
return { status: "success", windowId: windowId };
|
||||
}
|
||||
|
||||
private tryCreateWindow(options: WindowSpawnOptions, uniqueId: string) : Window | null {
|
||||
const parameters = _.cloneDeep(options.appParameters || {});
|
||||
Object.assign(parameters, {
|
||||
"loader-target": "manifest",
|
||||
"loader-chunk": options.loaderTarget,
|
||||
"loader-abort": __build.mode === "debug" ? 1 : 0,
|
||||
|
||||
"ipc-address": getIpcInstance().getApplicationChannelId(),
|
||||
"ipc-core-peer": getIpcInstance().getLocalPeerId(),
|
||||
|
||||
"disableGlobalContextMenu": __build.mode === "debug" ? 1 : 0,
|
||||
});
|
||||
|
||||
const features = {
|
||||
status: "no",
|
||||
location: "no",
|
||||
toolbar: "no",
|
||||
menubar: "no",
|
||||
resizable: "yes",
|
||||
width: options.defaultSize?.width,
|
||||
height: options.defaultSize?.height
|
||||
};
|
||||
|
||||
let baseUrl = location.origin + location.pathname + "?";
|
||||
return window.open(
|
||||
baseUrl + Object.keys(parameters).map(e => e + "=" + encodeURIComponent(parameters[e])).join("&"),
|
||||
uniqueId,
|
||||
Object.keys(features).map(e => e + "=" + features[e]).join(",")
|
||||
);
|
||||
}
|
||||
|
||||
destroyWindow(windowId: string) {
|
||||
this.registeredWindows[windowId]?.destroy();
|
||||
}
|
||||
|
||||
getActiveWindowId(): string | undefined {
|
||||
/* TODO! */
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async executeAction(windowId: string, action: WindowAction): Promise<void> {
|
||||
const windowHandle = this.registeredWindows[windowId];
|
||||
if(!windowHandle || windowHandle.window.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "focus":
|
||||
window.window.focus();
|
||||
break;
|
||||
|
||||
case "minimize":
|
||||
case "maximize":
|
||||
/* we can't do so */
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
isActionSupported(windowId: string, action: WindowAction) {
|
||||
switch (action) {
|
||||
case "focus":
|
||||
return true;
|
||||
|
||||
case "maximize":
|
||||
case "minimize":
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import "webrtc-adapter";
|
||||
import "webcrypto-liner";
|
||||
|
||||
import "../index.scss";
|
||||
import "../FileTransfer";
|
||||
|
||||
import "../hooks"
|
||||
|
||||
import "../UnloadHandler";
|
||||
|
||||
import "../ui/context-menu";
|
||||
import "../ui/FaviconRenderer";
|
||||
|
||||
import "tc-shared/entry-points/MainApp";
|
|
@ -0,0 +1,4 @@
|
|||
/* This is the entry point file for the external modals */
|
||||
|
||||
import "../ui/context-menu";
|
||||
import "tc-shared/entry-points/ModalWindow";
|
|
@ -1,12 +0,0 @@
|
|||
import * as loader from "tc-loader";
|
||||
import {Stage} from "tc-loader";
|
||||
import {setExternalModalControllerFactory} from "tc-shared/ui/react-elements/external-modal";
|
||||
import {ExternalModalController} from "../ExternalModalFactory";
|
||||
|
||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||
priority: 50,
|
||||
name: "external modal controller factory setup",
|
||||
function: async () => {
|
||||
setExternalModalControllerFactory((modalType, constructorArguments, options) => new ExternalModalController(modalType, constructorArguments, options));
|
||||
}
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
import * as loader from "tc-loader";
|
||||
import {Stage} from "tc-loader";
|
||||
import {setWindowManager} from "tc-shared/ui/windows/WindowManager";
|
||||
import {WebWindowManager} from "../WebWindowManager";
|
||||
|
||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "window manager init",
|
||||
function: async () => setWindowManager(new WebWindowManager()),
|
||||
priority: 100
|
||||
});
|
|
@ -1,9 +1,10 @@
|
|||
import "./AudioBackend";
|
||||
import "./AudioRecorder";
|
||||
import "./Backend";
|
||||
import "./Dns";
|
||||
import "./DNS";
|
||||
import "./MenuBar";
|
||||
import "./ServerConnection";
|
||||
import "./Sounds";
|
||||
import "./Video";
|
||||
import "./KeyBoard";
|
||||
import "./KeyBoard";
|
||||
import "./WindowManager";
|
|
@ -1,4 +0,0 @@
|
|||
/* This is the entry point file for the external modals */
|
||||
|
||||
import "./ui/context-menu";
|
||||
import "tc-shared/ui/react-elements/external-modal/PopoutEntrypoint"
|
|
@ -1,14 +0,0 @@
|
|||
import "webrtc-adapter";
|
||||
import "webcrypto-liner";
|
||||
|
||||
import "./index.scss";
|
||||
import "./FileTransfer";
|
||||
|
||||
import "./hooks"
|
||||
|
||||
import "./UnloadHandler";
|
||||
|
||||
import "./ui/context-menu";
|
||||
import "./ui/FaviconRenderer";
|
||||
|
||||
export = require("tc-shared/main");
|
|
@ -3,16 +3,17 @@ import * as config_base from "./webpack.config";
|
|||
|
||||
export = env => config_base.config(env, "client").then(config => {
|
||||
Object.assign(config.entry, {
|
||||
"client-app": ["./client/app/index.ts"],
|
||||
"modal-external": ["./client/app/EntryPointPopoutModal.ts"]
|
||||
"shared-app": ["./client/app/entry-points/AppMain.ts"],
|
||||
"modal-external": ["./client/app/entry-points/ModalWindow.ts"]
|
||||
});
|
||||
|
||||
Object.assign(config.resolve.alias, {
|
||||
"tc-shared": path.resolve(__dirname, "shared/js"),
|
||||
});
|
||||
|
||||
if(!Array.isArray(config.externals))
|
||||
if(!Array.isArray(config.externals)) {
|
||||
throw "invalid config";
|
||||
}
|
||||
|
||||
config.externals.push(({ context, request }, callback) => {
|
||||
if (request.startsWith("tc-backend/")) {
|
||||
|
|
|
@ -3,8 +3,8 @@ import * as config_base from "./webpack.config";
|
|||
|
||||
export = env => config_base.config(env, "web").then(config => {
|
||||
Object.assign(config.entry, {
|
||||
"shared-app": ["./web/app/index.ts"],
|
||||
"modal-external": ["./web/app/index-external.ts"]
|
||||
"shared-app": ["./web/app/entry-points/AppMain.ts"],
|
||||
"modal-external": ["./web/app/entry-points/ModalWindow.ts"]
|
||||
});
|
||||
|
||||
Object.assign(config.resolve.alias, {
|
||||
|
|
|
@ -118,8 +118,7 @@ export const config = async (env: any, target: "web" | "client"): Promise<Config
|
|||
return {
|
||||
entry: {
|
||||
"loader": "./loader/app/index.ts",
|
||||
"modal-external": "./shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts",
|
||||
//"devel-main": "./shared/js/devel_main.ts"
|
||||
"modal-external": "./shared/js/entry-points/ModalWindow.ts",
|
||||
},
|
||||
|
||||
devtool: isDevelopment ? "inline-source-map" : "source-map",
|
||||
|
|
|
@ -62,6 +62,17 @@ class ManifestGenerator {
|
|||
}
|
||||
|
||||
for(const module of compilation.chunkGraph.getChunkModules(chunk)) {
|
||||
const moduleId = compilation.chunkGraph.getModuleId(module);
|
||||
if(typeof moduleId === "string" && moduleId.startsWith("svg-sprites/")) {
|
||||
/* custom svg sprite handler */
|
||||
modules.push({
|
||||
id: module.id,
|
||||
context: "svg-sprites",
|
||||
resource: moduleId.substring("svg-sprites/".length)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!module.type.startsWith("javascript/")) {
|
||||
continue;
|
||||
}
|
||||
|
@ -70,16 +81,6 @@ class ManifestGenerator {
|
|||
continue;
|
||||
}
|
||||
|
||||
if(module.context.startsWith("svg-sprites/")) {
|
||||
/* custom svg sprite handler */
|
||||
modules.push({
|
||||
id: module.id,
|
||||
context: "svg-sprites",
|
||||
resource: module.context.substring("svg-sprites/".length)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!(module instanceof NormalModule)) {
|
||||
continue;
|
||||
}
|
||||
|
@ -94,7 +95,7 @@ class ManifestGenerator {
|
|||
}
|
||||
|
||||
modules.push({
|
||||
id: compilation.chunkGraph.getModuleId(module),
|
||||
id: moduleId,
|
||||
context: path.relative(this.options.context, module.context).replace(/\\/g, "/"),
|
||||
resource: path.basename(module.resource)
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue