Committing updates and pushed version
This commit is contained in:
parent
7087514df1
commit
a636515de8
2
github
2
github
@ -1 +1 @@
|
|||||||
Subproject commit 9c3cc6d05838a03a5827836b300f8bc8e71b26d2
|
Subproject commit 7c087d46ad75ff641d5862a57ff13f3e860cc8a4
|
@ -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
|
||||||
|
@ -10,6 +10,7 @@ import * as loader from "./../ui-loader";
|
|||||||
import * as url from "url";
|
import * as url from "url";
|
||||||
import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window";
|
import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window";
|
||||||
import {referenceApp, dereferenceApp} from "../AppInstance";
|
import {referenceApp, dereferenceApp} from "../AppInstance";
|
||||||
|
import {closeURLPreview, openURLPreview} from "../url-preview";
|
||||||
|
|
||||||
// Keep a global reference of the window object, if you don't, the window will
|
// Keep a global reference of the window object, if you don't, the window will
|
||||||
// be closed automatically when the JavaScript object is garbage collected.
|
// be closed automatically when the JavaScript object is garbage collected.
|
||||||
@ -49,7 +50,7 @@ function spawnMainWindow(rendererEntryPoint: string) {
|
|||||||
|
|
||||||
mainWindow.on('closed', () => {
|
mainWindow.on('closed', () => {
|
||||||
app.releaseSingleInstanceLock();
|
app.releaseSingleInstanceLock();
|
||||||
require("../url-preview").close();
|
closeURLPreview();
|
||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
|
|
||||||
dereferenceApp();
|
dereferenceApp();
|
||||||
@ -94,8 +95,7 @@ function spawnMainWindow(rendererEntryPoint: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log("Got new window " + frameName);
|
console.log("Got new window " + frameName);
|
||||||
const url_preview = require("./url-preview");
|
openURLPreview(url_str).then(() => {});
|
||||||
url_preview.open_preview(url_str);
|
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
console.error("Failed to open preview window for URL %s: %o", url_str, 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);
|
dialog.showErrorBox("Failed to open preview", "Failed to open preview URL: " + url_str + "\nError: " + error);
|
||||||
|
@ -4,7 +4,7 @@ import {ProxiedClass} from "../../shared/proxy/Definitions";
|
|||||||
import {BrowserWindow, dialog} from "electron";
|
import {BrowserWindow, dialog} from "electron";
|
||||||
import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window";
|
import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window";
|
||||||
import {Arguments, processArguments} from "../../shared/process-arguments";
|
import {Arguments, processArguments} from "../../shared/process-arguments";
|
||||||
import {open_preview} from "../url-preview";
|
import {openURLPreview} from "../url-preview";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
class ProxyImplementation extends ProxiedClass<ExternalModal> implements ExternalModal {
|
class ProxyImplementation extends ProxiedClass<ExternalModal> implements ExternalModal {
|
||||||
@ -71,15 +71,16 @@ class ProxyImplementation extends ProxiedClass<ExternalModal> implements Externa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open_preview(url.toString());
|
openURLPreview(url.toString());
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
console.error("Failed to open preview window for URL %s: %o", url_str, 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);
|
dialog.showErrorBox("Failed to open preview", "Failed to open preview URL: " + url_str + "\nError: " + error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if(processArguments.has_flag(Arguments.DEV_TOOLS))
|
if(processArguments.has_flag(Arguments.DEV_TOOLS)) {
|
||||||
this.windowInstance.webContents.openDevTools();
|
this.windowInstance.webContents.openDevTools();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.windowInstance.loadURL(url);
|
await this.windowInstance.loadURL(url);
|
||||||
|
@ -1,34 +1,26 @@
|
|||||||
import * as electron from "electron";
|
import * as electron from "electron";
|
||||||
import ipcMain = electron.ipcMain;
|
import ipcMain = electron.ipcMain;
|
||||||
import BrowserWindow = electron.BrowserWindow;
|
import BrowserWindow = electron.BrowserWindow;
|
||||||
|
import {NativeMenuBarEntry} from "../../shared/MenuBarDefinitions";
|
||||||
|
import {Menu, MenuItemConstructorOptions} from "electron";
|
||||||
|
|
||||||
ipcMain.on('top-menu', (event, menu_template: electron.MenuItemConstructorOptions[]) => {
|
ipcMain.on("menu-bar", (event, menuBar: NativeMenuBarEntry[]) => {
|
||||||
const window = BrowserWindow.fromWebContents(event.sender);
|
const window = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
|
||||||
const process_template = (item: electron.MenuItemConstructorOptions) => {
|
|
||||||
if(typeof(item.icon) === "string" && item.icon.startsWith("data:"))
|
|
||||||
item.icon = electron.nativeImage.createFromDataURL(item.icon);
|
|
||||||
|
|
||||||
item.click = () => window.webContents.send('top-menu', item.id);
|
|
||||||
for(const i of item.submenu as electron.MenuItemConstructorOptions[] || []) {
|
|
||||||
process_template(i);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for(const m of menu_template)
|
|
||||||
process_template(m);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const menu = new electron.Menu();
|
const processEntry = (entry: NativeMenuBarEntry): MenuItemConstructorOptions => {
|
||||||
for(const m of menu_template) {
|
return {
|
||||||
try {
|
type: entry.type === "separator" ? "separator" : entry.children?.length ? "submenu" : "normal",
|
||||||
menu.append(new electron.MenuItem(m));
|
label: entry.label,
|
||||||
} catch(error) {
|
icon: entry.icon ? electron.nativeImage.createFromDataURL(entry.icon).resize({ height: 16, width: 16 }) : undefined,
|
||||||
console.error("Failed to build menu entry: %o\nSource: %o", error, m);
|
enabled: !entry.disabled,
|
||||||
|
click: entry.uniqueId && (() => event.sender.send("menu-bar", "item-click", entry.uniqueId)),
|
||||||
|
submenu: entry.children?.map(processEntry)
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
window.setMenu(menu_template.length == 0 ? undefined : menu);
|
|
||||||
} catch(error) {
|
window.setMenu(Menu.buildFromTemplate(menuBar.map(processEntry)));
|
||||||
console.error("Failed to set window menu: %o", error);
|
} catch (error) {
|
||||||
|
console.error("failed to set menu bar for %s: %o", window.getTitle(), error);
|
||||||
}
|
}
|
||||||
});
|
});
|
@ -5,7 +5,7 @@ import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window";
|
|||||||
let global_window: electron.BrowserWindow;
|
let global_window: electron.BrowserWindow;
|
||||||
let global_window_promise: Promise<void>;
|
let global_window_promise: Promise<void>;
|
||||||
|
|
||||||
export async function close() {
|
export async function closeURLPreview() {
|
||||||
while(global_window_promise) {
|
while(global_window_promise) {
|
||||||
try {
|
try {
|
||||||
await global_window_promise;
|
await global_window_promise;
|
||||||
@ -20,13 +20,14 @@ export async function close() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function open_preview(url: string) {
|
export async function openURLPreview(url: string) {
|
||||||
while(global_window_promise) {
|
while(global_window_promise) {
|
||||||
try {
|
try {
|
||||||
await global_window_promise;
|
await global_window_promise;
|
||||||
break;
|
break;
|
||||||
} catch(error) {} /* error will be already logged */
|
} catch(error) {} /* error will be already logged */
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!global_window) {
|
if(!global_window) {
|
||||||
global_window_promise = (async () => {
|
global_window_promise = (async () => {
|
||||||
global_window = new electron.BrowserWindow({
|
global_window = new electron.BrowserWindow({
|
||||||
|
@ -1,64 +1,123 @@
|
|||||||
import {ContextMenuEntry, ContextMenuFactory, setGlobalContextMenuFactory} from "tc-shared/ui/ContextMenu";
|
import {ContextMenuEntry, ContextMenuFactory, setGlobalContextMenuFactory} from "tc-shared/ui/ContextMenu";
|
||||||
import * as electron from "electron";
|
import * as electron from "electron";
|
||||||
import {MenuItemConstructorOptions} from "electron";
|
import {MenuItemConstructorOptions} from "electron";
|
||||||
import {clientIconClassToImage} from "./IconHelper";
|
import {clientIconClassToImage, remoteIconDatafier, RemoteIconWrapper} from "./IconHelper";
|
||||||
|
import {getIconManager, RemoteIconInfo} from "tc-shared/file/Icons";
|
||||||
const {Menu} = electron.remote;
|
const {Menu} = electron.remote;
|
||||||
|
|
||||||
let currentMenu: electron.Menu;
|
let currentMenu: ContextMenuInstance;
|
||||||
|
class ContextMenuInstance {
|
||||||
|
private readonly closeCallback: () => void | undefined;
|
||||||
|
private readonly menuOptions: MenuItemConstructorOptions[];
|
||||||
|
private currentMenu: electron.Menu;
|
||||||
|
|
||||||
function mapMenuEntry(entry: ContextMenuEntry) : MenuItemConstructorOptions {
|
private wrappedIcons: RemoteIconWrapper[] = [];
|
||||||
switch (entry.type) {
|
private wrappedIconListeners: (() => void)[] = [];
|
||||||
case "normal":
|
|
||||||
return {
|
|
||||||
type: "normal",
|
|
||||||
label: typeof entry.label === "string" ? entry.label : entry.label.text,
|
|
||||||
enabled: entry.enabled,
|
|
||||||
visible: entry.visible,
|
|
||||||
click: entry.click,
|
|
||||||
icon: typeof entry.icon === "string" ? clientIconClassToImage(entry.icon) : undefined,
|
|
||||||
id: entry.uniqueId,
|
|
||||||
submenu: entry.subMenu ? entry.subMenu.map(mapMenuEntry).filter(e => !!e) : undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
case "checkbox":
|
constructor(entries: ContextMenuEntry[], closeCallback: () => void | undefined) {
|
||||||
return {
|
this.closeCallback = closeCallback;
|
||||||
type: "normal",
|
this.menuOptions = entries.map(e => this.wrapEntry(e)).filter(e => !!e);
|
||||||
label: typeof entry.label === "string" ? entry.label : entry.label.text,
|
}
|
||||||
enabled: entry.enabled,
|
|
||||||
visible: entry.visible,
|
|
||||||
click: entry.click,
|
|
||||||
id: entry.uniqueId,
|
|
||||||
|
|
||||||
checked: entry.checked
|
destroy() {
|
||||||
};
|
this.currentMenu?.closePopup();
|
||||||
|
this.currentMenu = undefined;
|
||||||
|
|
||||||
case "separator":
|
this.wrappedIconListeners.forEach(callback => callback());
|
||||||
return {
|
this.wrappedIcons.forEach(icon => remoteIconDatafier.unrefIcon(icon));
|
||||||
type: "separator"
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
this.wrappedIcons = [];
|
||||||
return undefined;
|
this.wrappedIconListeners = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn(pageX: number, pageY: number) {
|
||||||
|
this.currentMenu = Menu.buildFromTemplate(this.menuOptions);
|
||||||
|
this.currentMenu.popup({
|
||||||
|
callback: () => {
|
||||||
|
if(this.closeCallback) {
|
||||||
|
this.closeCallback();
|
||||||
|
}
|
||||||
|
currentMenu = undefined;
|
||||||
|
},
|
||||||
|
x: pageX,
|
||||||
|
y: pageY,
|
||||||
|
window: electron.remote.BrowserWindow.getFocusedWindow()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private wrapEntry(entry: ContextMenuEntry) : MenuItemConstructorOptions {
|
||||||
|
if(typeof entry.visible === "boolean" && !entry.visible) { return undefined; }
|
||||||
|
|
||||||
|
let options: MenuItemConstructorOptions;
|
||||||
|
let icon: string | RemoteIconInfo | undefined;
|
||||||
|
|
||||||
|
switch (entry.type) {
|
||||||
|
case "normal":
|
||||||
|
icon = entry.icon;
|
||||||
|
options = {
|
||||||
|
type: entry.subMenu?.length ? "submenu" : "normal",
|
||||||
|
label: typeof entry.label === "string" ? entry.label : entry.label.text,
|
||||||
|
enabled: entry.enabled,
|
||||||
|
click: entry.click,
|
||||||
|
id: entry.uniqueId,
|
||||||
|
submenu: entry.subMenu?.length ? entry.subMenu.map(e => this.wrapEntry(e)).filter(e => !!e) : undefined
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "checkbox":
|
||||||
|
icon = entry.icon;
|
||||||
|
options = {
|
||||||
|
type: "checkbox",
|
||||||
|
label: typeof entry.label === "string" ? entry.label : entry.label.text,
|
||||||
|
enabled: entry.enabled,
|
||||||
|
click: entry.click,
|
||||||
|
id: entry.uniqueId,
|
||||||
|
checked: entry.checked
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "separator":
|
||||||
|
return {
|
||||||
|
type: "separator"
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof icon === "object") {
|
||||||
|
const remoteIcon = getIconManager().resolveIcon(icon.iconId, icon.serverUniqueId, icon.handlerId);
|
||||||
|
const wrapped = remoteIconDatafier.resolveIcon(remoteIcon);
|
||||||
|
remoteIconDatafier.unrefIcon(wrapped);
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Sadly we can't update the icon on the fly, so we've to live with whatever we have
|
||||||
|
this.wrappedIcons.push(wrapped);
|
||||||
|
this.wrappedIconListeners.push(wrapped.onDataUrlChange(dataUrl => {
|
||||||
|
options.icon = electron.nativeImage.createFromDataURL(dataUrl);
|
||||||
|
}));
|
||||||
|
*/
|
||||||
|
|
||||||
|
if(wrapped.getDataUrl()) {
|
||||||
|
options.icon = electron.remote.nativeImage.createFromDataURL(wrapped.getDataUrl());
|
||||||
|
}
|
||||||
|
} else if(typeof icon === "string") {
|
||||||
|
options.icon = clientIconClassToImage(icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setGlobalContextMenuFactory(new class implements ContextMenuFactory {
|
setGlobalContextMenuFactory(new class implements ContextMenuFactory {
|
||||||
closeContextMenu() {
|
closeContextMenu() {
|
||||||
currentMenu?.closePopup();
|
currentMenu?.destroy();
|
||||||
currentMenu = undefined;
|
currentMenu = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
spawnContextMenu(position: { pageX: number; pageY: number }, entries: ContextMenuEntry[], callbackClose?: () => void) {
|
spawnContextMenu(position: { pageX: number; pageY: number }, entries: ContextMenuEntry[], callbackClose?: () => void) {
|
||||||
this.closeContextMenu();
|
this.closeContextMenu();
|
||||||
currentMenu = Menu.buildFromTemplate(entries.map(mapMenuEntry).filter(e => !!e));
|
currentMenu = new ContextMenuInstance(entries, callbackClose);
|
||||||
currentMenu.popup({
|
currentMenu.spawn(position.pageX, position.pageY);
|
||||||
callback: () => {
|
|
||||||
callbackClose();
|
|
||||||
currentMenu = undefined;
|
|
||||||
},
|
|
||||||
x: position.pageX,
|
|
||||||
y: position.pageY,
|
|
||||||
window: electron.remote.BrowserWindow.getFocusedWindow()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
@ -6,9 +6,10 @@ import {
|
|||||||
spriteUrl as kClientSpriteUrl,
|
spriteUrl as kClientSpriteUrl,
|
||||||
spriteWidth as kClientSpriteWidth,
|
spriteWidth as kClientSpriteWidth,
|
||||||
spriteHeight as kClientSpriteHeight,
|
spriteHeight as kClientSpriteHeight,
|
||||||
spriteEntries as kClientSpriteEntries
|
spriteEntries as kClientSpriteEntries, ClientIcon
|
||||||
} from "svg-sprites/client-icons";
|
} from "svg-sprites/client-icons";
|
||||||
import {NativeImage} from "electron";
|
import {NativeImage} from "electron";
|
||||||
|
import {RemoteIcon} from "tc-shared/file/Icons";
|
||||||
|
|
||||||
let nativeSprite: NativeImage;
|
let nativeSprite: NativeImage;
|
||||||
|
|
||||||
@ -24,6 +25,134 @@ export function clientIconClassToImage(klass: string) : NativeImage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class RemoteIconDatafier {
|
||||||
|
private cachedIcons: {[key: string]:{ refCount: number, icon: RemoteIconWrapper }} = {};
|
||||||
|
private cleanupTimer;
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
clearTimeout(this.cleanupTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveIcon(icon: RemoteIcon) : RemoteIconWrapper {
|
||||||
|
const uniqueId = icon.iconId + "-" + icon.serverUniqueId;
|
||||||
|
if(!this.cachedIcons[uniqueId]) {
|
||||||
|
this.cachedIcons[uniqueId] = {
|
||||||
|
refCount: 0,
|
||||||
|
icon: new RemoteIconWrapper(uniqueId, icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = this.cachedIcons[uniqueId];
|
||||||
|
cache.refCount++;
|
||||||
|
return cache.icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
unrefIcon(icon: RemoteIconWrapper) {
|
||||||
|
const cache = this.cachedIcons[icon.uniqueId];
|
||||||
|
if(!cache) { return; }
|
||||||
|
|
||||||
|
cache.refCount--;
|
||||||
|
if(cache.refCount <= 0) {
|
||||||
|
if(this.cleanupTimer) {
|
||||||
|
clearTimeout(this.cleanupTimer);
|
||||||
|
}
|
||||||
|
this.cleanupTimer = setTimeout(() => this.cleanupIcons(), 10 * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanupIcons() {
|
||||||
|
this.cleanupTimer = undefined;
|
||||||
|
for(const key of Object.keys(this.cachedIcons)) {
|
||||||
|
if(this.cachedIcons[key].refCount <= 0) {
|
||||||
|
this.cachedIcons[key].icon.destroy();
|
||||||
|
delete this.cachedIcons[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const remoteIconDatafier = new RemoteIconDatafier();
|
||||||
|
|
||||||
|
export class RemoteIconWrapper {
|
||||||
|
readonly callbackUpdated: ((newUrl: string) => void)[] = [];
|
||||||
|
readonly uniqueId: string;
|
||||||
|
|
||||||
|
private readonly icon: RemoteIcon;
|
||||||
|
private readonly callbackStateChanged: () => void;
|
||||||
|
private dataUrl: string | undefined;
|
||||||
|
private currentImageUrl: string;
|
||||||
|
|
||||||
|
constructor(uniqueId: string, icon: RemoteIcon) {
|
||||||
|
this.icon = icon;
|
||||||
|
this.uniqueId = uniqueId;
|
||||||
|
this.callbackStateChanged = this.handleIconStateChanged.bind(this);
|
||||||
|
|
||||||
|
this.icon.events.on("notify_state_changed", this.callbackStateChanged);
|
||||||
|
this.handleIconStateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.icon.events.off("notify_state_changed", this.callbackStateChanged);
|
||||||
|
this.currentImageUrl = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDataUrl() : string | undefined { return this.dataUrl; }
|
||||||
|
|
||||||
|
onDataUrlChange(callback: (newUrl: string) => void) : () => void {
|
||||||
|
this.callbackUpdated.push(callback);
|
||||||
|
return () => {
|
||||||
|
const index = this.callbackUpdated.indexOf(callback);
|
||||||
|
if(index !== -1) { this.callbackUpdated.splice(index, 1); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleIconStateChanged() {
|
||||||
|
if(this.icon.getState() === "loaded") {
|
||||||
|
const imageUrl = this.icon.getImageUrl();
|
||||||
|
this.currentImageUrl = this.icon.getImageUrl();
|
||||||
|
|
||||||
|
const image = new Image();
|
||||||
|
image.src = imageUrl;
|
||||||
|
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
image.onload = resolve;
|
||||||
|
image.onerror = reject;
|
||||||
|
}).then(() => {
|
||||||
|
if(this.currentImageUrl !== imageUrl) { return; }
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
if(image.naturalWidth > 1000 || image.naturalHeight > 1000) {
|
||||||
|
throw "image dimensions are too large";
|
||||||
|
} else {
|
||||||
|
canvas.width = image.naturalWidth;
|
||||||
|
canvas.height = image.naturalHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.getContext("2d").drawImage(image, 0, 0);
|
||||||
|
|
||||||
|
this.setDataUrl(imageUrl, canvas.toDataURL());
|
||||||
|
}).catch(() => {
|
||||||
|
this.setDataUrl(imageUrl, clientIconClassToImage(ClientIcon.Error).toDataURL());
|
||||||
|
});
|
||||||
|
} else if(this.icon.getState() === "error") {
|
||||||
|
this.setDataUrl(undefined, clientIconClassToImage(ClientIcon.Error).toDataURL());
|
||||||
|
} else {
|
||||||
|
this.setDataUrl(undefined, undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setDataUrl(sourceImageUrl: string | undefined, dataUrl: string) {
|
||||||
|
if(sourceImageUrl && this.currentImageUrl !== sourceImageUrl) { return; }
|
||||||
|
this.currentImageUrl = undefined; /* no image is loading any more */
|
||||||
|
|
||||||
|
if(this.dataUrl === dataUrl) { return; }
|
||||||
|
|
||||||
|
this.dataUrl = dataUrl;
|
||||||
|
this.callbackUpdated.forEach(callback => callback(dataUrl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
priority: 100,
|
priority: 100,
|
||||||
name: "native icon sprite loader",
|
name: "native icon sprite loader",
|
||||||
@ -40,7 +169,7 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
|||||||
canvas.height = kClientSpriteHeight;
|
canvas.height = kClientSpriteHeight;
|
||||||
canvas.getContext("2d").drawImage(image, 0, 0);
|
canvas.getContext("2d").drawImage(image, 0, 0);
|
||||||
|
|
||||||
nativeSprite = electron.remote.nativeImage.createFromDataURL( canvas.toDataURL());
|
nativeSprite = electron.remote.nativeImage.createFromDataURL(canvas.toDataURL());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
99
modules/renderer/MenuBar.ts
Normal file
99
modules/renderer/MenuBar.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import * as electron from "electron";
|
||||||
|
import {MenuBarDriver, MenuBarEntry} from "tc-shared/ui/frames/menu-bar";
|
||||||
|
import {IpcRendererEvent} from "electron";
|
||||||
|
import {getIconManager} from "tc-shared/file/Icons";
|
||||||
|
import {clientIconClassToImage, remoteIconDatafier, RemoteIconWrapper} from "./IconHelper";
|
||||||
|
import {NativeMenuBarEntry} from "../shared/MenuBarDefinitions";
|
||||||
|
|
||||||
|
let uniqueEntryIdIndex = 0;
|
||||||
|
export class NativeMenuBarDriver implements MenuBarDriver {
|
||||||
|
private readonly ipcChannelListener;
|
||||||
|
|
||||||
|
private menuEntries: NativeMenuBarEntry[] = [];
|
||||||
|
private remoteIconReferences: RemoteIconWrapper[] = [];
|
||||||
|
private remoteIconListeners: (() => void)[] = [];
|
||||||
|
private callbacks: {[key: string]: () => void} = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.ipcChannelListener = this.handleMenuBarEvent.bind(this);
|
||||||
|
|
||||||
|
electron.ipcRenderer.on("menu-bar", this.ipcChannelListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
electron.ipcRenderer.off("menu-bar", this.ipcChannelListener);
|
||||||
|
this.internalClearEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
private internalClearEntries() {
|
||||||
|
this.callbacks = {};
|
||||||
|
this.menuEntries = [];
|
||||||
|
|
||||||
|
this.remoteIconListeners.forEach(callback => callback());
|
||||||
|
this.remoteIconListeners = [];
|
||||||
|
|
||||||
|
this.remoteIconReferences.forEach(icon => remoteIconDatafier.unrefIcon(icon));
|
||||||
|
this.remoteIconReferences = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
clearEntries() {
|
||||||
|
this.internalClearEntries()
|
||||||
|
electron.ipcRenderer.send("menu-bar", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
setEntries(entries: MenuBarEntry[]) {
|
||||||
|
this.internalClearEntries();
|
||||||
|
this.menuEntries = entries.map(e => this.wrapEntry(e)).filter(e => !!e);
|
||||||
|
electron.ipcRenderer.send("menu-bar", this.menuEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
private wrapEntry(entry: MenuBarEntry) : NativeMenuBarEntry {
|
||||||
|
if(entry.type === "separator") {
|
||||||
|
return { type: "separator", uniqueId: entry.uniqueId || "item-" + (++uniqueEntryIdIndex) };
|
||||||
|
} else if(entry.type === "normal") {
|
||||||
|
if(typeof entry.visible === "boolean" && !entry.visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = {
|
||||||
|
type: "normal",
|
||||||
|
uniqueId: entry.uniqueId || "item-" + (++uniqueEntryIdIndex),
|
||||||
|
label: entry.label,
|
||||||
|
disabled: entry.disabled,
|
||||||
|
children: entry.children?.map(e => this.wrapEntry(e)).filter(e => !!e)
|
||||||
|
} as NativeMenuBarEntry;
|
||||||
|
|
||||||
|
if(entry.click) {
|
||||||
|
this.callbacks[result.uniqueId] = entry.click;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof entry.icon === "object") {
|
||||||
|
/* we've a remote icon */
|
||||||
|
const remoteIcon = getIconManager().resolveIcon(entry.icon.iconId, entry.icon.serverUniqueId, entry.icon.handlerId);
|
||||||
|
const wrapped = remoteIconDatafier.resolveIcon(remoteIcon);
|
||||||
|
this.remoteIconReferences.push(wrapped);
|
||||||
|
|
||||||
|
wrapped.onDataUrlChange(url => {
|
||||||
|
result.icon = url;
|
||||||
|
electron.ipcRenderer.send("menu-bar", this.menuEntries);
|
||||||
|
});
|
||||||
|
result.icon = wrapped.getDataUrl();
|
||||||
|
} else if(typeof entry.icon === "string") {
|
||||||
|
result.icon = clientIconClassToImage(entry.icon).toDataURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMenuBarEvent(_event: IpcRendererEvent, eventType: string, ...args) {
|
||||||
|
if(eventType === "item-click") {
|
||||||
|
const callback = this.callbacks[args[0]];
|
||||||
|
if(typeof callback === "function") {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,246 +0,0 @@
|
|||||||
import {clientIconClassToImage} from "./IconHelper";
|
|
||||||
import * as electron from "electron";
|
|
||||||
import * as mbar from "tc-shared/ui/frames/MenuBar";
|
|
||||||
import {Arguments, processArguments} from "../shared/process-arguments";
|
|
||||||
|
|
||||||
import ipcRenderer = electron.ipcRenderer;
|
|
||||||
import {LocalIcon} from "tc-shared/file/Icons";
|
|
||||||
namespace native {
|
|
||||||
import ipcRenderer = electron.ipcRenderer;
|
|
||||||
let _item_index = 1;
|
|
||||||
|
|
||||||
abstract class NativeMenuBase {
|
|
||||||
protected _handle: NativeMenuBar;
|
|
||||||
protected _click: () => any;
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
protected constructor(handle: NativeMenuBar, id?: string) {
|
|
||||||
this._handle = handle;
|
|
||||||
this.id = id || ("item_" + (_item_index++));
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract build() : electron.MenuItemConstructorOptions;
|
|
||||||
abstract items(): (mbar.MenuItem | mbar.HRItem)[];
|
|
||||||
|
|
||||||
trigger_click() {
|
|
||||||
if(this._click)
|
|
||||||
this._click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NativeMenuItem extends NativeMenuBase implements mbar.MenuItem {
|
|
||||||
private _items: (NativeMenuItem | NativeHrItem)[] = [];
|
|
||||||
private _label: string;
|
|
||||||
private _enabled: boolean = true;
|
|
||||||
private _visible: boolean = true;
|
|
||||||
|
|
||||||
private _icon_data: string;
|
|
||||||
|
|
||||||
constructor(handle: NativeMenuBar) {
|
|
||||||
super(handle);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
append_hr(): mbar.HRItem {
|
|
||||||
const item = new NativeHrItem(this._handle);
|
|
||||||
this._items.push(item);
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
append_item(label: string): mbar.MenuItem {
|
|
||||||
const item = new NativeMenuItem(this._handle);
|
|
||||||
item.label(label);
|
|
||||||
this._items.push(item);
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
click(callback: () => any): this {
|
|
||||||
this._click = callback;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete_item(item: mbar.MenuItem | mbar.HRItem) {
|
|
||||||
const i_index = this._items.indexOf(item as any);
|
|
||||||
if(i_index < 0) return;
|
|
||||||
this._items.splice(i_index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
disabled(value?: boolean): boolean {
|
|
||||||
if(typeof(value) === "boolean")
|
|
||||||
this._enabled = !value;
|
|
||||||
return !this._enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
icon(klass?: string | Promise<LocalIcon> | LocalIcon): string {
|
|
||||||
if(typeof(klass) === "string") {
|
|
||||||
const buffer = clientIconClassToImage(klass);
|
|
||||||
if(buffer)
|
|
||||||
this._icon_data = buffer.toDataURL();
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
items(): (mbar.MenuItem | mbar.HRItem)[] {
|
|
||||||
return this._items;
|
|
||||||
}
|
|
||||||
|
|
||||||
label(value?: string): string {
|
|
||||||
if(typeof(value) === "string")
|
|
||||||
this._label = value;
|
|
||||||
return this._label;
|
|
||||||
}
|
|
||||||
|
|
||||||
visible(value?: boolean): boolean {
|
|
||||||
if(typeof(value) === "boolean")
|
|
||||||
this._visible = value;
|
|
||||||
return this._visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
build(): Electron.MenuItemConstructorOptions {
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
|
|
||||||
label: this._label || "",
|
|
||||||
|
|
||||||
submenu: this._items.length > 0 ? this._items.map(e => e.build()) : undefined,
|
|
||||||
enabled: this._enabled,
|
|
||||||
visible: this._visible,
|
|
||||||
|
|
||||||
icon: this._icon_data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NativeHrItem extends NativeMenuBase implements mbar.HRItem {
|
|
||||||
constructor(handle: NativeMenuBar) {
|
|
||||||
super(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
build(): Electron.MenuItemConstructorOptions {
|
|
||||||
return {
|
|
||||||
type: 'separator',
|
|
||||||
id: this.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items(): (mbar.MenuItem | mbar.HRItem)[] {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function is_similar_deep(a, b) {
|
|
||||||
if(typeof(a) !== typeof(b))
|
|
||||||
return false;
|
|
||||||
if(typeof(a) !== "object")
|
|
||||||
return a === b;
|
|
||||||
|
|
||||||
const aProps = Object.keys(a);
|
|
||||||
const bProps = Object.keys(b);
|
|
||||||
|
|
||||||
if (aProps.length != bProps.length)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
for (let i = 0; i < aProps.length; i++) {
|
|
||||||
const propName = aProps[i];
|
|
||||||
|
|
||||||
if(!is_similar_deep(a[propName], b[propName]))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class NativeMenuBar implements mbar.MenuBarDriver {
|
|
||||||
private static _instance: NativeMenuBar;
|
|
||||||
|
|
||||||
private menu: electron.Menu;
|
|
||||||
private _items: NativeMenuItem[] = [];
|
|
||||||
private _current_menu: electron.MenuItemConstructorOptions[];
|
|
||||||
|
|
||||||
public static instance() : NativeMenuBar {
|
|
||||||
if(!this._instance)
|
|
||||||
this._instance = new NativeMenuBar();
|
|
||||||
return this._instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
append_item(label: string): mbar.MenuItem {
|
|
||||||
const item = new NativeMenuItem(this);
|
|
||||||
item.label(label);
|
|
||||||
this._items.push(item);
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete_item(item: mbar.MenuItem) {
|
|
||||||
const i_index = this._items.indexOf(item as any);
|
|
||||||
if(i_index < 0) return;
|
|
||||||
this._items.splice(i_index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
flush_changes() {
|
|
||||||
const target_menu = this.build_menu();
|
|
||||||
if(is_similar_deep(target_menu, this._current_menu))
|
|
||||||
return;
|
|
||||||
|
|
||||||
this._current_menu = target_menu;
|
|
||||||
ipcRenderer.send('top-menu', target_menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
private build_menu() : electron.MenuItemConstructorOptions[] {
|
|
||||||
return this._items.map(e => e.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
items(): mbar.MenuItem[] {
|
|
||||||
return this._items;
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize() {
|
|
||||||
this.menu = new electron.remote.Menu();
|
|
||||||
ipcRenderer.on('top-menu', (event, clicked_item) => {
|
|
||||||
console.log("Item %o clicked", clicked_item);
|
|
||||||
const check_item = (item: NativeMenuBase) => {
|
|
||||||
if(item.id == clicked_item) {
|
|
||||||
item.trigger_click();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
for(const child of item.items())
|
|
||||||
if(check_item(child as NativeMenuBase))
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
for(const item of this._items)
|
|
||||||
if(check_item(item))
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mbar.set_driver(native.NativeMenuBar.instance());
|
|
||||||
// @ts-ignore
|
|
||||||
mbar.native_actions = {
|
|
||||||
open_change_log() {
|
|
||||||
call_basic_action("open-changelog");
|
|
||||||
},
|
|
||||||
|
|
||||||
check_native_update() {
|
|
||||||
call_basic_action("check-native-update");
|
|
||||||
},
|
|
||||||
|
|
||||||
quit() {
|
|
||||||
call_basic_action("quit");
|
|
||||||
},
|
|
||||||
|
|
||||||
open_dev_tools() {
|
|
||||||
call_basic_action("open-dev-tools");
|
|
||||||
},
|
|
||||||
|
|
||||||
reload_page() {
|
|
||||||
call_basic_action("reload-window")
|
|
||||||
},
|
|
||||||
|
|
||||||
show_dev_tools() { return processArguments.has_flag(Arguments.DEV_TOOLS); }
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const call_basic_action = (name: string, ...args: any[]) => ipcRenderer.send('basic-action', name, ...args);
|
|
@ -16,6 +16,7 @@ import {NativeFilter, NStateFilter, NThresholdFilter, NVoiceLevelFilter} from ".
|
|||||||
import {IDevice} from "tc-shared/audio/recorder";
|
import {IDevice} from "tc-shared/audio/recorder";
|
||||||
import {LogCategory, logWarn} from "tc-shared/log";
|
import {LogCategory, logWarn} from "tc-shared/log";
|
||||||
import NativeFilterMode = audio.record.FilterMode;
|
import NativeFilterMode = audio.record.FilterMode;
|
||||||
|
import {Settings, settings} from "tc-shared/settings";
|
||||||
|
|
||||||
export class NativeInput implements AbstractInput {
|
export class NativeInput implements AbstractInput {
|
||||||
static readonly instances = [] as NativeInput[];
|
static readonly instances = [] as NativeInput[];
|
||||||
@ -37,8 +38,7 @@ export class NativeInput implements AbstractInput {
|
|||||||
this.nativeHandle = audio.record.create_recorder();
|
this.nativeHandle = audio.record.create_recorder();
|
||||||
|
|
||||||
this.nativeConsumer = this.nativeHandle.create_consumer();
|
this.nativeConsumer = this.nativeHandle.create_consumer();
|
||||||
this.nativeConsumer.toggle_rnnoise(true);
|
this.nativeConsumer.toggle_rnnoise(settings.static_global(Settings.KEY_RNNOISE_FILTER));
|
||||||
(window as any).consumer = this.nativeConsumer; /* FIXME! */
|
|
||||||
|
|
||||||
this.nativeConsumer.callback_ended = () => {
|
this.nativeConsumer.callback_ended = () => {
|
||||||
this.filtered = true;
|
this.filtered = true;
|
||||||
@ -245,7 +245,7 @@ export class NativeLevelMeter implements LevelMeter {
|
|||||||
this.nativeRecorder = audio.record.create_recorder();
|
this.nativeRecorder = audio.record.create_recorder();
|
||||||
this.nativeConsumer = this.nativeRecorder.create_consumer();
|
this.nativeConsumer = this.nativeRecorder.create_consumer();
|
||||||
|
|
||||||
this.nativeConsumer.toggle_rnnoise(true); /* FIXME! */
|
this.nativeConsumer.toggle_rnnoise(settings.static_global(Settings.KEY_RNNOISE_FILTER));
|
||||||
|
|
||||||
this.nativeFilter = this.nativeConsumer.create_filter_threshold(.5);
|
this.nativeFilter = this.nativeConsumer.create_filter_threshold(.5);
|
||||||
this.nativeFilter.set_attack_smooth(.75);
|
this.nativeFilter.set_attack_smooth(.75);
|
||||||
|
30
modules/renderer/backend-impl/Backend.ts
Normal file
30
modules/renderer/backend-impl/Backend.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import {NativeClientBackend} from "tc-shared/backend/NativeClient";
|
||||||
|
import {ipcRenderer} from "electron";
|
||||||
|
import {Arguments, processArguments} from "../../shared/process-arguments";
|
||||||
|
|
||||||
|
const call_basic_action = (name: string, ...args: any[]) => ipcRenderer.send('basic-action', name, ...args);
|
||||||
|
export class NativeClientBackendImpl implements NativeClientBackend {
|
||||||
|
openChangeLog(): void {
|
||||||
|
call_basic_action("open-changelog");
|
||||||
|
}
|
||||||
|
|
||||||
|
openClientUpdater(): void {
|
||||||
|
call_basic_action("check-native-update");
|
||||||
|
}
|
||||||
|
|
||||||
|
openDeveloperTools(): void {
|
||||||
|
call_basic_action("open-dev-tools");
|
||||||
|
}
|
||||||
|
|
||||||
|
quit(): void {
|
||||||
|
call_basic_action("quit");
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadWindow(): void {
|
||||||
|
call_basic_action("reload-window")
|
||||||
|
}
|
||||||
|
|
||||||
|
showDeveloperOptions(): boolean {
|
||||||
|
return processArguments.has_flag(Arguments.DEV_TOOLS);
|
||||||
|
}
|
||||||
|
}
|
4
modules/renderer/hooks/Backend.ts
Normal file
4
modules/renderer/hooks/Backend.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import {setBackend} from "tc-shared/backend";
|
||||||
|
import {NativeClientBackendImpl} from "../backend-impl/Backend";
|
||||||
|
|
||||||
|
setBackend(new NativeClientBackendImpl());
|
4
modules/renderer/hooks/MenuBar.ts
Normal file
4
modules/renderer/hooks/MenuBar.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import {setMenuBarDriver} from "tc-shared/ui/frames/menu-bar";
|
||||||
|
import {NativeMenuBarDriver} from "../MenuBar";
|
||||||
|
|
||||||
|
setMenuBarDriver(new NativeMenuBarDriver());
|
@ -52,7 +52,7 @@ loader.register_task(loader.Stage.JAVASCRIPT, {
|
|||||||
priority: 80
|
priority: 80
|
||||||
});
|
});
|
||||||
|
|
||||||
loader.register_task(loader.Stage.INITIALIZING, {
|
loader.register_task(loader.Stage.SETUP, {
|
||||||
name: "teaclient initialize persistent storage",
|
name: "teaclient initialize persistent storage",
|
||||||
function: async () => {
|
function: async () => {
|
||||||
const storage = require("./PersistentLocalStorage");
|
const storage = require("./PersistentLocalStorage");
|
||||||
@ -154,7 +154,7 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
|||||||
/* all files which replaces a native driver */
|
/* all files which replaces a native driver */
|
||||||
try {
|
try {
|
||||||
await import("./version");
|
await import("./version");
|
||||||
await import("./MenuBarHandler");
|
await import("./MenuBar");
|
||||||
await import("./ContextMenu");
|
await import("./ContextMenu");
|
||||||
await import("./SingleInstanceHandler");
|
await import("./SingleInstanceHandler");
|
||||||
await import("./IconHelper");
|
await import("./IconHelper");
|
||||||
@ -164,6 +164,8 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
|||||||
await import("./hooks/ExternalModal");
|
await import("./hooks/ExternalModal");
|
||||||
await import("./hooks/ServerConnection");
|
await import("./hooks/ServerConnection");
|
||||||
await import("./hooks/ChangeLogClient");
|
await import("./hooks/ChangeLogClient");
|
||||||
|
await import("./hooks/Backend");
|
||||||
|
await import("./hooks/MenuBar");
|
||||||
|
|
||||||
await import("./UnloadHandler");
|
await import("./UnloadHandler");
|
||||||
await import("./WindowsTrayHandler");
|
await import("./WindowsTrayHandler");
|
||||||
|
10
modules/shared/MenuBarDefinitions.ts
Normal file
10
modules/shared/MenuBarDefinitions.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import {NativeImage} from "electron";
|
||||||
|
|
||||||
|
export interface NativeMenuBarEntry {
|
||||||
|
uniqueId: string,
|
||||||
|
type: "separator" | "normal",
|
||||||
|
label?: string,
|
||||||
|
icon?: string,
|
||||||
|
disabled?: boolean,
|
||||||
|
children?: NativeMenuBarEntry[]
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "TeaClient",
|
"name": "TeaClient",
|
||||||
"version": "1.4.12",
|
"version": "1.4.13",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user