Improved the external modal support
This commit is contained in:
parent
ea6e37e4f1
commit
bc9f313aeb
@ -122,5 +122,5 @@ function deploy_client() {
|
|||||||
#install_npm
|
#install_npm
|
||||||
#compile_scripts
|
#compile_scripts
|
||||||
#compile_native
|
#compile_native
|
||||||
#package_client
|
package_client
|
||||||
deploy_client
|
#deploy_client
|
||||||
|
111
modules/core/render-backend/ExternalModal.ts
Normal file
111
modules/core/render-backend/ExternalModal.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import {ObjectProxyServer} from "../../shared/proxy/Server";
|
||||||
|
import {ExternalModal, kIPCChannelExternalModal} from "../../shared/ipc/ExternalModal";
|
||||||
|
import {ProxiedClass} from "../../shared/proxy/Definitions";
|
||||||
|
import {BrowserWindow, dialog} from "electron";
|
||||||
|
import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window";
|
||||||
|
import {Arguments, process_args} from "../../shared/process-arguments";
|
||||||
|
import {open_preview} from "../url-preview";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
class ProxyImplementation extends ProxiedClass<ExternalModal> implements ExternalModal {
|
||||||
|
private windowInstance: BrowserWindow;
|
||||||
|
|
||||||
|
public constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
async focus(): Promise<any> {
|
||||||
|
this.windowInstance?.focusOnWebView();
|
||||||
|
}
|
||||||
|
|
||||||
|
async minimize(): Promise<any> {
|
||||||
|
this.windowInstance?.minimize();
|
||||||
|
}
|
||||||
|
|
||||||
|
async spawnWindow(modalTarget: string, url: string): Promise<boolean> {
|
||||||
|
if(this.windowInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.windowInstance = new BrowserWindow({
|
||||||
|
/* parent: remote.getCurrentWindow(), */ /* do not link them together */
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true,
|
||||||
|
},
|
||||||
|
icon: path.join(__dirname, "..", "..", "resources", "logo.ico"),
|
||||||
|
minWidth: 600,
|
||||||
|
minHeight: 300,
|
||||||
|
|
||||||
|
frame: false,
|
||||||
|
transparent: true,
|
||||||
|
|
||||||
|
show: true
|
||||||
|
});
|
||||||
|
|
||||||
|
loadWindowBounds("modal-" + modalTarget, this.windowInstance).then(() => {
|
||||||
|
startTrackWindowBounds("modal-" + modalTarget, this.windowInstance);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.windowInstance.webContents.on('new-window', (event, url_str, frameName, disposition, options, additionalFeatures) => {
|
||||||
|
console.error("Open: %O", frameName);
|
||||||
|
if(frameName.startsWith("__modal_external__")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
try {
|
||||||
|
let url: URL;
|
||||||
|
try {
|
||||||
|
url = new URL(url_str);
|
||||||
|
} catch(error) {
|
||||||
|
throw "failed to parse URL";
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let protocol = url.protocol.endsWith(":") ? url.protocol.substring(0, url.protocol.length - 1) : url.protocol;
|
||||||
|
if(protocol !== "https" && protocol !== "http") {
|
||||||
|
throw "invalid protocol (" + protocol + "). HTTP(S) are only supported!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open_preview(url.toString());
|
||||||
|
} catch(error) {
|
||||||
|
console.error("Failed to open preview window for URL %s: %o", url_str, error);
|
||||||
|
dialog.showErrorBox("Failed to open preview", "Failed to open preview URL: " + url_str + "\nError: " + error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(process_args.has_flag(Arguments.DEV_TOOLS))
|
||||||
|
this.windowInstance.webContents.openDevTools();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.windowInstance.loadURL(url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load external modal main page: %o", error);
|
||||||
|
this.windowInstance.close();
|
||||||
|
this.windowInstance = undefined;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.windowInstance.on("closed", () => {
|
||||||
|
this.windowInstance = undefined;
|
||||||
|
this.events.onClose();
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if(!this.windowInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.windowInstance.close();
|
||||||
|
this.windowInstance = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = new ObjectProxyServer<ExternalModal>(kIPCChannelExternalModal, ProxyImplementation);
|
||||||
|
server.initialize();
|
@ -8,6 +8,10 @@ import {open as open_changelog} from "../app-updater/changelog";
|
|||||||
import * as updater from "../app-updater";
|
import * as updater from "../app-updater";
|
||||||
import {execute_connect_urls} from "../instance_handler";
|
import {execute_connect_urls} from "../instance_handler";
|
||||||
import {process_args} from "../../shared/process-arguments";
|
import {process_args} from "../../shared/process-arguments";
|
||||||
|
import {open_preview} from "../url-preview";
|
||||||
|
import {dialog} from "electron";
|
||||||
|
|
||||||
|
import "./ExternalModal";
|
||||||
|
|
||||||
ipcMain.on('basic-action', (event, action, ...args: any[]) => {
|
ipcMain.on('basic-action', (event, action, ...args: any[]) => {
|
||||||
const window = BrowserWindow.fromWebContents(event.sender);
|
const window = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
/* --------------- bootstrap --------------- */
|
/* --------------- bootstrap --------------- */
|
||||||
import * as RequireProxy from "../renderer/RequireProxy";
|
import * as RequireProxy from "../renderer/RequireProxy";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
RequireProxy.initialize(path.join(__dirname, "backend-impl"));
|
||||||
|
|
||||||
/* --------------- entry point --------------- */
|
/* --------------- entry point --------------- */
|
||||||
import * as loader from "tc-loader";
|
import * as loader from "tc-loader";
|
||||||
import {Stage} from "tc-loader";
|
import {Stage} from "tc-loader";
|
||||||
import {Arguments, process_args} from "../shared/process-arguments";
|
import {Arguments, process_args} from "../shared/process-arguments";
|
||||||
import {remote} from "electron";
|
import {remote} from "electron";
|
||||||
|
|
||||||
RequireProxy.initialize(path.join(__dirname, "backend-impl"));
|
|
||||||
|
|
||||||
export function initialize(manifestTarget: string) {
|
export function initialize(manifestTarget: string) {
|
||||||
console.log("Initializing native client for manifest target %s", manifestTarget);
|
console.log("Initializing native client for manifest target %s", manifestTarget);
|
||||||
|
|
||||||
|
@ -1,50 +1,25 @@
|
|||||||
import {AbstractExternalModalController} from "tc-shared/ui/react-elements/external-modal/Controller";
|
import {AbstractExternalModalController} from "tc-shared/ui/react-elements/external-modal/Controller";
|
||||||
import * as ipc from "tc-shared/ipc/BrowserIPC";
|
|
||||||
import * as log from "tc-shared/log";
|
|
||||||
import {LogCategory} from "tc-shared/log";
|
|
||||||
import {BrowserWindow, remote} from "electron";
|
|
||||||
import {tr} from "tc-shared/i18n/localize";
|
|
||||||
import * as path from "path";
|
|
||||||
import {Arguments, process_args} from "../shared/process-arguments";
|
|
||||||
import {Popout2ControllerMessages, PopoutIPCMessage} from "tc-shared/ui/react-elements/external-modal/IPCMessage";
|
import {Popout2ControllerMessages, PopoutIPCMessage} from "tc-shared/ui/react-elements/external-modal/IPCMessage";
|
||||||
import {loadWindowBounds, startTrackWindowBounds} from "../shared/window";
|
import {ExternalModal, kIPCChannelExternalModal} from "../shared/ipc/ExternalModal";
|
||||||
|
import {ObjectProxyClient} from "../shared/proxy/Client";
|
||||||
|
import * as ipc from "tc-shared/ipc/BrowserIPC";
|
||||||
|
import {ProxiedClass} from "../shared/proxy/Definitions";
|
||||||
|
|
||||||
|
const modalClient = new ObjectProxyClient<ExternalModal>(kIPCChannelExternalModal);
|
||||||
|
modalClient.initialize();
|
||||||
|
|
||||||
export class ExternalModalController extends AbstractExternalModalController {
|
export class ExternalModalController extends AbstractExternalModalController {
|
||||||
private window: BrowserWindow;
|
private handle: ProxiedClass<ExternalModal> & ExternalModal;
|
||||||
|
|
||||||
constructor(a, b, c) {
|
constructor(a, b, c) {
|
||||||
super(a, b, c);
|
super(a, b, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async spawnWindow(): Promise<boolean> {
|
protected async spawnWindow(): Promise<boolean> {
|
||||||
if(this.window) {
|
if(!this.handle) {
|
||||||
return true;
|
this.handle = await modalClient.createNewInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.window = new remote.BrowserWindow({
|
|
||||||
/* parent: remote.getCurrentWindow(), */ /* do not link them together */
|
|
||||||
autoHideMenuBar: true,
|
|
||||||
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: true,
|
|
||||||
},
|
|
||||||
icon: path.join(__dirname, "..", "..", "resources", "logo.ico"),
|
|
||||||
minWidth: 600,
|
|
||||||
minHeight: 300,
|
|
||||||
|
|
||||||
frame: false,
|
|
||||||
transparent: true,
|
|
||||||
|
|
||||||
show: true
|
|
||||||
});
|
|
||||||
|
|
||||||
loadWindowBounds("modal-" + this.modalType, this.window).then(() => {
|
|
||||||
startTrackWindowBounds("modal-" + this.modalType, this.window);
|
|
||||||
});
|
|
||||||
|
|
||||||
if(process_args.has_flag(Arguments.DEV_TOOLS))
|
|
||||||
this.window.webContents.openDevTools();
|
|
||||||
|
|
||||||
const parameters = {
|
const parameters = {
|
||||||
"loader-target": "manifest",
|
"loader-target": "manifest",
|
||||||
"chunk": "modal-external",
|
"chunk": "modal-external",
|
||||||
@ -57,32 +32,17 @@ export class ExternalModalController extends AbstractExternalModalController {
|
|||||||
|
|
||||||
const baseUrl = location.origin + location.pathname + "?";
|
const baseUrl = location.origin + location.pathname + "?";
|
||||||
const url = baseUrl + Object.keys(parameters).map(e => e + "=" + encodeURIComponent(parameters[e])).join("&");
|
const url = baseUrl + Object.keys(parameters).map(e => e + "=" + encodeURIComponent(parameters[e])).join("&");
|
||||||
try {
|
|
||||||
await this.window.loadURL(url);
|
|
||||||
} catch (error) {
|
|
||||||
log.warn(LogCategory.GENERAL, tr("Failed to load external modal main page: %o"), error);
|
|
||||||
this.window.close();
|
|
||||||
this.window = undefined;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.window.on("closed", () => {
|
return await this.handle.spawnWindow(this.modalType, url);
|
||||||
this.window = undefined;
|
|
||||||
this.handleWindowClosed();
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected destroyWindow(): void {
|
protected destroyWindow(): void {
|
||||||
if(this.window) {
|
this.handle?.destroy();
|
||||||
this.window.close();
|
this.handle = undefined;
|
||||||
this.window = undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected focusWindow(): void {
|
protected focusWindow(): void {
|
||||||
this.window?.focus();
|
this.handle?.focus().then(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected handleTypedIPCMessage<T extends Popout2ControllerMessages>(type: T, payload: PopoutIPCMessage[T]) {
|
protected handleTypedIPCMessage<T extends Popout2ControllerMessages>(type: T, payload: PopoutIPCMessage[T]) {
|
||||||
@ -97,7 +57,7 @@ export class ExternalModalController extends AbstractExternalModalController {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "minimize":
|
case "minimize":
|
||||||
this.window?.minimize();
|
this.handle?.minimize().then(() => {});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -4,7 +4,7 @@ import {tr} from "tc-shared/i18n/localize";
|
|||||||
import {Arguments, process_args} from "../shared/process-arguments";
|
import {Arguments, process_args} from "../shared/process-arguments";
|
||||||
import {remote} from "electron";
|
import {remote} from "electron";
|
||||||
|
|
||||||
const unloadListener = event => {
|
window.onbeforeunload = event => {
|
||||||
if(settings.static(Settings.KEY_DISABLE_UNLOAD_DIALOG))
|
if(settings.static(Settings.KEY_DISABLE_UNLOAD_DIALOG))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -15,12 +15,12 @@ const unloadListener = event => {
|
|||||||
const dp = server_connections.all_connections().map(e => {
|
const dp = server_connections.all_connections().map(e => {
|
||||||
if(e.serverConnection.connected())
|
if(e.serverConnection.connected())
|
||||||
return e.serverConnection.disconnect(tr("client closed"))
|
return e.serverConnection.disconnect(tr("client closed"))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.warn(tr("Failed to disconnect from server %s on client close: %o"),
|
console.warn(tr("Failed to disconnect from server %s on client close: %o"),
|
||||||
e.serverConnection.remote_address().host + ":" + e.serverConnection.remote_address().port,
|
e.serverConnection.remote_address().host + ":" + e.serverConnection.remote_address().port,
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -49,12 +49,12 @@ const unloadListener = event => {
|
|||||||
}).then(result => {
|
}).then(result => {
|
||||||
if(result.response === 0) {
|
if(result.response === 0) {
|
||||||
/* prevent quitting because we try to disconnect */
|
/* prevent quitting because we try to disconnect */
|
||||||
window.removeEventListener("beforeunload", unloadListener);
|
window.onbeforeunload = e => e.preventDefault();
|
||||||
do_exit(true);
|
do_exit(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", unloadListener);
|
event.preventDefault();
|
||||||
|
event.returnValue = "question";
|
||||||
|
}
|
12
modules/shared/ipc/ExternalModal.ts
Normal file
12
modules/shared/ipc/ExternalModal.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export const kIPCChannelExternalModal = "external-modal";
|
||||||
|
|
||||||
|
export interface ExternalModal {
|
||||||
|
readonly events: {
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnWindow(modalTarget: string, url: string) : Promise<boolean>;
|
||||||
|
|
||||||
|
minimize() : Promise<any>;
|
||||||
|
focus() : Promise<any>;
|
||||||
|
}
|
119
modules/shared/proxy/Client.ts
Normal file
119
modules/shared/proxy/Client.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import {ipcRenderer, IpcRendererEvent, remote} from "electron";
|
||||||
|
import {tr} from "tc-shared/i18n/localize";
|
||||||
|
import {LogCategory, logError, logWarn} from "tc-shared/log";
|
||||||
|
import {ProxiedClass, ProxyInterface} from "./Definitions";
|
||||||
|
|
||||||
|
export class ObjectProxyClient<ObjectType extends ProxyInterface<ObjectType>> {
|
||||||
|
private readonly ipcChannel: string;
|
||||||
|
private readonly handleIPCMessageBinding;
|
||||||
|
|
||||||
|
private eventInvokers: {[key: string]: { fireEvent: (type: string, ...args: any) => void }} = {};
|
||||||
|
|
||||||
|
constructor(ipcChannel: string) {
|
||||||
|
this.ipcChannel = ipcChannel;
|
||||||
|
this.handleIPCMessageBinding = this.handleIPCMessage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
ipcRenderer.on(this.ipcChannel, this.handleIPCMessageBinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
ipcRenderer.off(this.ipcChannel, this.handleIPCMessageBinding);
|
||||||
|
/* TODO: Destroy all client instances? */
|
||||||
|
}
|
||||||
|
|
||||||
|
async createNewInstance() : Promise<ObjectType & ProxiedClass<ObjectType>> {
|
||||||
|
let object = {
|
||||||
|
objectId: undefined as string
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await ipcRenderer.invoke(this.ipcChannel, "create");
|
||||||
|
if(result.status !== "success") {
|
||||||
|
if(result.status === "error") {
|
||||||
|
throw result.message || tr("failed to create a new instance");
|
||||||
|
} else {
|
||||||
|
throw tr("failed to create a new object instance ({})", result.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
object.objectId = result.instanceId;
|
||||||
|
|
||||||
|
const ipcChannel = this.ipcChannel;
|
||||||
|
|
||||||
|
const events = this.generateEvents(object.objectId);
|
||||||
|
return new Proxy(object, {
|
||||||
|
get(target, key: PropertyKey) {
|
||||||
|
if(key === "ownerWindowId") {
|
||||||
|
return remote.getCurrentWindow().id;
|
||||||
|
} else if(key === "instanceId") {
|
||||||
|
return object.objectId;
|
||||||
|
} else if(key === "destroy") {
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.invoke(ipcChannel, "destroy", target.objectId);
|
||||||
|
events.destroy();
|
||||||
|
};
|
||||||
|
} else if(key === "events") {
|
||||||
|
return events;
|
||||||
|
} else if(key === "then" || key === "catch") {
|
||||||
|
/* typescript for some reason has an issue if then and catch return anything */
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (...args: any) => ipcRenderer.invoke(ipcChannel, "invoke", target.objectId, key, ...args);
|
||||||
|
},
|
||||||
|
|
||||||
|
set(): boolean {
|
||||||
|
throw "class is a ready only interface";
|
||||||
|
}
|
||||||
|
}) as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateEvents(objectId: string) : { destroy() } {
|
||||||
|
const eventInvokers = this.eventInvokers;
|
||||||
|
const registeredEvents = {};
|
||||||
|
|
||||||
|
eventInvokers[objectId] = {
|
||||||
|
fireEvent(event: string, ...args: any) {
|
||||||
|
if(typeof registeredEvents[event] === "undefined")
|
||||||
|
return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
registeredEvents[event](...args);
|
||||||
|
} catch (error) {
|
||||||
|
logError(LogCategory.IPC, tr("Failed to invoke event %s on %s: %o"), event, objectId, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Proxy({ }, {
|
||||||
|
set(target, key: PropertyKey, value: any): boolean {
|
||||||
|
registeredEvents[key] = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
get(target, key: PropertyKey): any {
|
||||||
|
if(key === "destroy") {
|
||||||
|
return () => delete eventInvokers[objectId];
|
||||||
|
} else if(typeof registeredEvents[key] === "function") {
|
||||||
|
return () => { throw tr("events can only be invoked via IPC") };
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleIPCMessage(event: IpcRendererEvent, ...args: any[]) {
|
||||||
|
const actionType = args[0];
|
||||||
|
|
||||||
|
if(actionType === "notify-event") {
|
||||||
|
const invoker = this.eventInvokers[args[1]];
|
||||||
|
if(typeof invoker !== "object") {
|
||||||
|
logWarn(LogCategory.IPC, tr("Received event %s for unknown object instance on channel %s"), args[2], args[1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
invoker.fireEvent(args[2], ...args.slice(3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
modules/shared/proxy/Definitions.ts
Normal file
35
modules/shared/proxy/Definitions.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
export type ProxiedEvents<EventObject> = {
|
||||||
|
[Q in keyof EventObject]: EventObject[Q] extends (...args: any) => void ? (...args: Parameters<EventObject[Q]>) => void : never
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FunctionalInterface<ObjectType> = {
|
||||||
|
[P in keyof ObjectType]: ObjectType[P] extends (...args: any) => Promise<any> ? (...args: any) => Promise<any> :
|
||||||
|
P extends "events" ? ObjectType[P] extends ProxiedEvents<ObjectType[P]> ? ProxiedEvents<ObjectType[P]> : never : never
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProxiedClassProperties = { instanceId: string, ownerWindowId: number, events: any };
|
||||||
|
|
||||||
|
export type ProxyInterface<ObjectType> = FunctionalInterface<ObjectType>;
|
||||||
|
export type ProxyClass<ObjectType> = { new(props: ProxiedClassProperties): ProxyInterface<ObjectType> & ProxiedClass<ObjectType> };
|
||||||
|
|
||||||
|
export abstract class ProxiedClass<Interface extends { events?: ProxiedEvents<Interface["events"]> }> {
|
||||||
|
public readonly ownerWindowId: number;
|
||||||
|
public readonly instanceId: string;
|
||||||
|
|
||||||
|
public readonly events: ProxiedEvents<Interface["events"]>;
|
||||||
|
|
||||||
|
public constructor(props: ProxiedClassProperties) {
|
||||||
|
this.ownerWindowId = props.ownerWindowId;
|
||||||
|
this.instanceId = props.instanceId;
|
||||||
|
this.events = props.events;
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateUUID() {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
|
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
83
modules/shared/proxy/Server.ts
Normal file
83
modules/shared/proxy/Server.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import {BrowserWindow, ipcMain, IpcMainEvent} from "electron";
|
||||||
|
import {generateUUID, ProxiedClass, ProxyClass, ProxyInterface} from "./Definitions";
|
||||||
|
|
||||||
|
export class ObjectProxyServer<ObjectType extends ProxyInterface<ObjectType>> {
|
||||||
|
private readonly ipcChannel: string;
|
||||||
|
private readonly klass: ProxyClass<ObjectType>;
|
||||||
|
private readonly instances: { [key: string]: ProxyInterface<ObjectType> & ProxiedClass<ObjectType> } = {};
|
||||||
|
|
||||||
|
private readonly handleIPCMessageBinding;
|
||||||
|
|
||||||
|
constructor(ipcChannel: string, klass: ProxyClass<ObjectType>) {
|
||||||
|
this.klass = klass;
|
||||||
|
this.ipcChannel = ipcChannel;
|
||||||
|
|
||||||
|
this.handleIPCMessageBinding = this.handleIPCMessage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
ipcMain.handle(this.ipcChannel, this.handleIPCMessageBinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
ipcMain.removeHandler(this.ipcChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleIPCMessage(event: IpcMainEvent, ...args: any[]) {
|
||||||
|
const actionType = args[0];
|
||||||
|
|
||||||
|
if(actionType === "create") {
|
||||||
|
let instance: ProxiedClass<ObjectType> & ProxyInterface<ObjectType>;
|
||||||
|
try {
|
||||||
|
const instanceId = generateUUID();
|
||||||
|
instance = new this.klass({
|
||||||
|
ownerWindowId: event.sender.id,
|
||||||
|
instanceId: instanceId,
|
||||||
|
events: this.generateEventProxy(instanceId, event.sender.id)
|
||||||
|
});
|
||||||
|
this.instances[instance.instanceId] = instance;
|
||||||
|
} catch (error) {
|
||||||
|
event.returnValue = { "status": "error", message: "create-error" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { "status": "success", instanceId: instance.instanceId };
|
||||||
|
} else {
|
||||||
|
const instance = this.instances[args[1]];
|
||||||
|
|
||||||
|
if(!instance) {
|
||||||
|
throw "instance-unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(actionType === "destroy") {
|
||||||
|
delete this.instances[args[1]];
|
||||||
|
instance.destroy();
|
||||||
|
} else if(actionType === "invoke") {
|
||||||
|
if(typeof instance[args[2]] !== "function") {
|
||||||
|
throw "function-unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance[args[2]](...args.slice(3));
|
||||||
|
} else {
|
||||||
|
console.warn("Received an invalid action: %s", actionType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateEventProxy(instanceId: string, owningWindowId: number) : {} {
|
||||||
|
const ipcChannel = this.ipcChannel;
|
||||||
|
return new Proxy({ }, {
|
||||||
|
get(target: { }, event: PropertyKey, receiver: any): any {
|
||||||
|
return (...args: any) => {
|
||||||
|
const window = BrowserWindow.fromId(owningWindowId);
|
||||||
|
if(!window) return;
|
||||||
|
|
||||||
|
window.webContents.send(ipcChannel, "notify-event", instanceId, event, ...args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set(): boolean {
|
||||||
|
throw "the events are read only for the implementation";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
30
modules/shared/proxy/Test.ts
Normal file
30
modules/shared/proxy/Test.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import {ProxiedClass} from "./Definitions";
|
||||||
|
import {ObjectProxyClient} from "./Client";
|
||||||
|
import {ObjectProxyServer} from "./Server";
|
||||||
|
|
||||||
|
interface TextModal {
|
||||||
|
readonly events: {
|
||||||
|
onHide: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
sayHi() : Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextModalImpl extends ProxiedClass<TextModal> implements TextModal {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sayHi(): Promise<void> {
|
||||||
|
this.events.onHide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let server = new ObjectProxyServer<TextModal>("", TextModalImpl);
|
||||||
|
let client = new ObjectProxyClient<TextModal>("");
|
||||||
|
|
||||||
|
const instance = await client.createNewInstance();
|
||||||
|
await instance.sayHi();
|
||||||
|
instance.events.onHide = () => {};
|
||||||
|
}
|
@ -8,7 +8,7 @@
|
|||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"start-d1": "electron . --disable-hardware-acceleration --debug -t --gdb -s -u=http://clientapi.teaspeak.dev/ --updater-ui-loader_type=0",
|
"start-d1": "electron . --disable-hardware-acceleration --debug -t --gdb -s -u=http://clientapi.teaspeak.dev/ --updater-ui-loader_type=0",
|
||||||
"start-n": "electron . -t --disable-hardware-acceleration --no-single-instance -u=https://clientapi.teaspeak.de/ -d",
|
"start-n": "electron . -t --disable-hardware-acceleration --no-single-instance -u=https://clientapi.teaspeak.de/ -d",
|
||||||
"start-nd": "electron . -t --disable-hardware-acceleration --no-single-instance -u=http://clientapi.teaspeak.dev/ -d",
|
"start-nd": "electron . -t --disable-hardware-acceleration --no-single-instance -u=http://clientapi.teaspeak.dev/ -d --updater-ui-loader_type=0",
|
||||||
"start-01": "electron . --updater-channel=test -u=http://dev.clientapi.teaspeak.de/ -d --updater-ui-loader_type=0 --updater-local-version=1.0.1",
|
"start-01": "electron . --updater-channel=test -u=http://dev.clientapi.teaspeak.de/ -d --updater-ui-loader_type=0 --updater-local-version=1.0.1",
|
||||||
"start-devel-download": "electron . --disable-hardware-acceleration --gdb --debug --updater-ui-loader_type=2 --updater-ui-ignore-version -t -u http://localhost:8081/",
|
"start-devel-download": "electron . --disable-hardware-acceleration --gdb --debug --updater-ui-loader_type=2 --updater-ui-ignore-version -t -u http://localhost:8081/",
|
||||||
"start-s": "electron . --disable-hardware-acceleration --gdb --debug --updater-ui-loader_type=3 --updater-ui-ignore-version -t -u http://localhost:8081/",
|
"start-s": "electron . --disable-hardware-acceleration --gdb --debug --updater-ui-loader_type=3 --updater-ui-ignore-version -t -u http://localhost:8081/",
|
||||||
|
Loading…
Reference in New Issue
Block a user