mirror of
https://github.com/finos/SymphonyElectron.git
synced 2025-02-16 18:25:04 -06:00
Typescript - Complete application menu
This commit is contained in:
parent
a4858f336b
commit
5e31833b91
329
src/browser/app-menu.ts
Normal file
329
src/browser/app-menu.ts
Normal file
@ -0,0 +1,329 @@
|
||||
import { app, Menu, session, shell } from 'electron';
|
||||
|
||||
import { isMac, isWindowsOS } from '../common/env';
|
||||
import { i18n } from '../common/i18n';
|
||||
import { logger } from '../common/logger';
|
||||
import { autoLaunchInstance as autoLaunch } from './auto-launch-controller';
|
||||
import { config, IConfig } from './config-handler';
|
||||
import { exportCrashDumps, exportLogs } from './reports';
|
||||
import { windowHandler } from './window-handler';
|
||||
import { updateAlwaysOnTop } from './window-utils';
|
||||
|
||||
export const menuSections = {
|
||||
about: 'about',
|
||||
edit: 'edit',
|
||||
view: 'view',
|
||||
window: 'window',
|
||||
help: 'help', // tslint:disable-line
|
||||
};
|
||||
|
||||
const windowsAccelerator = Object.assign({
|
||||
close: 'Ctrl+W',
|
||||
copy: 'Ctrl+C',
|
||||
cut: 'Ctrl+X',
|
||||
minimize: 'Ctrl+M',
|
||||
paste: 'Ctrl+V',
|
||||
pasteandmatchstyle: 'Ctrl+Shift+V',
|
||||
redo: 'Ctrl+Y',
|
||||
resetzoom: 'Ctrl+0',
|
||||
selectall: 'Ctrl+A',
|
||||
togglefullscreen: 'F11',
|
||||
undo: 'Ctrl+Z',
|
||||
zoomin: 'Ctrl+Shift+Plus',
|
||||
zoomout: 'Ctrl+-',
|
||||
});
|
||||
|
||||
let {
|
||||
minimizeOnClose,
|
||||
launchOnStartup,
|
||||
alwaysOnTop: isAlwaysOnTop,
|
||||
bringToFront,
|
||||
memoryRefresh,
|
||||
} = config.getConfigFields([
|
||||
'minimizeOnClose',
|
||||
'launchOnStartup',
|
||||
'alwaysOnTop',
|
||||
'bringToFront',
|
||||
'memoryRefresh',
|
||||
]) as IConfig;
|
||||
|
||||
const menuItemsArray = Object.keys(menuSections)
|
||||
.map((key) => menuSections[ key ])
|
||||
.filter((value) => isMac ?
|
||||
true : value !== menuSections.about);
|
||||
|
||||
export class AppMenu {
|
||||
private menu: Electron.Menu | undefined;
|
||||
private menuList: Electron.MenuItemConstructorOptions[];
|
||||
|
||||
constructor() {
|
||||
this.menuList = [];
|
||||
this.buildMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the menu items for all the menu
|
||||
*/
|
||||
public buildMenu(): void {
|
||||
this.menuList = menuItemsArray.reduce((map: Electron.MenuItemConstructorOptions, key: string) => {
|
||||
map[ key ] = this.buildMenuKey(key);
|
||||
return map;
|
||||
}, this.menuList || {});
|
||||
|
||||
const template = Object.keys(this.menuList)
|
||||
.map((key) => this.menuList[ key ]);
|
||||
|
||||
this.menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(this.menu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays popup application at the x,y coordinates
|
||||
*
|
||||
* @param window {Electron.BrowserWindow}
|
||||
*/
|
||||
public popupMenu(window: Electron.BrowserWindow): void {
|
||||
if (this.menu) {
|
||||
this.menu.popup({ window, x: 20, y: 15 });
|
||||
} else {
|
||||
logger.error(`app-menu: tried popup menu, but failed menu not defined`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds menu items based on key provided
|
||||
*
|
||||
* @param key {string}
|
||||
*/
|
||||
public buildMenuKey(key: string): Electron.MenuItemConstructorOptions {
|
||||
switch (key) {
|
||||
case menuSections.about:
|
||||
return this.buildAboutMenu();
|
||||
case menuSections.edit:
|
||||
return this.buildEditMenu();
|
||||
case menuSections.view:
|
||||
return this.buildViewMenu();
|
||||
case menuSections.window:
|
||||
return this.buildWindowMenu();
|
||||
case menuSections.help:
|
||||
return this.buildHelpMenu();
|
||||
default:
|
||||
throw new Error(`app-menu: Invalid ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds menu items for about symphony section
|
||||
*/
|
||||
private buildAboutMenu(): Electron.MenuItemConstructorOptions {
|
||||
return {
|
||||
id: menuSections.about,
|
||||
label: app.getName(),
|
||||
submenu: [
|
||||
{ label: i18n.t('About Symphony'), role: 'about' },
|
||||
this.buildSeparator(),
|
||||
{ label: i18n.t('Services'), role: 'services' },
|
||||
this.buildSeparator(),
|
||||
{ label: i18n.t('Hide Symphony'), role: 'hide' },
|
||||
{ label: i18n.t('Hide Others'), role: 'hideothers' },
|
||||
{ label: i18n.t('Show All'), role: 'unhide' },
|
||||
this.buildSeparator(),
|
||||
{ label: i18n.t('Quit Symphony'), role: 'quit' },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds menu items for edit section
|
||||
*/
|
||||
private buildEditMenu(): Electron.MenuItemConstructorOptions {
|
||||
const menu = {
|
||||
label: i18n.t('Edit'),
|
||||
submenu:
|
||||
[
|
||||
this.assignRoleOrLabel('undo', i18n.t('Undo')),
|
||||
this.assignRoleOrLabel('redo', i18n.t('Redo')),
|
||||
this.buildSeparator(),
|
||||
this.assignRoleOrLabel('cut', i18n.t('Cut')),
|
||||
this.assignRoleOrLabel('copy', i18n.t('Copy')),
|
||||
this.assignRoleOrLabel('paste', i18n.t('Paste')),
|
||||
this.assignRoleOrLabel('pasteandmatchstyle', i18n.t('Paste and Match Style')),
|
||||
this.assignRoleOrLabel('delete', i18n.t('Delete')),
|
||||
this.assignRoleOrLabel('selectall', i18n.t('Select All')),
|
||||
],
|
||||
};
|
||||
|
||||
if (isMac) {
|
||||
menu.submenu.push(this.buildSeparator(), {
|
||||
label: i18n.t('Speech'),
|
||||
submenu: [
|
||||
{ label: i18n.t('Start Speaking'), role: 'startspeaking' },
|
||||
{ label: i18n.t('Stop Speaking'), role: 'stopspeaking' },
|
||||
],
|
||||
});
|
||||
}
|
||||
return menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds menu items for view section
|
||||
*/
|
||||
private buildViewMenu(): Electron.MenuItemConstructorOptions {
|
||||
return {
|
||||
label: i18n.t('View'),
|
||||
submenu: [ {
|
||||
accelerator: 'CmdOrCtrl+R',
|
||||
click: (_item, focusedWindow) => focusedWindow ? focusedWindow.reload() : null,
|
||||
label: i18n.t('Reload'),
|
||||
},
|
||||
this.buildSeparator(),
|
||||
this.assignRoleOrLabel('resetzoom', i18n.t('Actual Size')),
|
||||
this.assignRoleOrLabel('zoomin', i18n.t('Zoom In')),
|
||||
this.assignRoleOrLabel('zoomout', i18n.t('Zoom Out')),
|
||||
this.buildSeparator(),
|
||||
this.assignRoleOrLabel('togglefullscreen', i18n.t('Toggle Full Screen')),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds menu items for window section
|
||||
*/
|
||||
private buildWindowMenu(): Electron.MenuItemConstructorOptions {
|
||||
return {
|
||||
label: i18n.t('Window'),
|
||||
role: 'window',
|
||||
submenu: [
|
||||
this.assignRoleOrLabel('minimize', i18n.t('Minimize')),
|
||||
this.assignRoleOrLabel('close', i18n.t('Close')),
|
||||
this.buildSeparator(),
|
||||
{
|
||||
checked: launchOnStartup,
|
||||
click: async (item) => {
|
||||
if (item.checked) {
|
||||
await autoLaunch.enableAutoLaunch();
|
||||
} else {
|
||||
await autoLaunch.disableAutoLaunch();
|
||||
}
|
||||
launchOnStartup = item.checked;
|
||||
config.updateUserConfig({ launchOnStartup });
|
||||
},
|
||||
label: i18n.t('Auto Launch On Startup'),
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
checked: isAlwaysOnTop,
|
||||
click: (item) => {
|
||||
isAlwaysOnTop = item.checked;
|
||||
updateAlwaysOnTop(item.checked, true);
|
||||
config.updateUserConfig({ alwaysOnTop: item.checked });
|
||||
},
|
||||
label: i18n.t('Always on Top'),
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
checked: minimizeOnClose,
|
||||
click: (item) => {
|
||||
minimizeOnClose = item.checked;
|
||||
config.updateUserConfig({ minimizeOnClose });
|
||||
},
|
||||
label: i18n.t('Minimize on Close'),
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
checked: bringToFront,
|
||||
click: (item) => {
|
||||
bringToFront = item.checked;
|
||||
config.updateUserConfig({ bringToFront });
|
||||
},
|
||||
label: isWindowsOS
|
||||
? i18n.t('Flash Notification in Taskbar')
|
||||
: i18n.t('Bring to Front on Notifications'),
|
||||
type: 'checkbox',
|
||||
},
|
||||
this.buildSeparator(),
|
||||
{
|
||||
checked: memoryRefresh,
|
||||
click: (item) => {
|
||||
memoryRefresh = item.checked;
|
||||
config.updateUserConfig({ memoryRefresh });
|
||||
},
|
||||
label: i18n.t('Refresh app when idle'),
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
click: (_item, focusedWindow) => {
|
||||
if (focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
const defaultSession = session.defaultSession;
|
||||
if (defaultSession) {
|
||||
defaultSession.clearCache(() => {
|
||||
focusedWindow.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
label: i18n.t('Clear cache and Reload'),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds menu items for help section
|
||||
*/
|
||||
private buildHelpMenu(): Electron.MenuItemConstructorOptions {
|
||||
return {
|
||||
label: i18n.t('Help'),
|
||||
role: 'help',
|
||||
submenu:
|
||||
[ {
|
||||
click: () => shell.openExternal(i18n.t('Help Url')),
|
||||
label: i18n.t('Symphony Help'),
|
||||
}, {
|
||||
click: () => shell.openExternal(i18n.t('Symphony Url')),
|
||||
label: i18n.t('Learn More'),
|
||||
}, {
|
||||
label: i18n.t('Troubleshooting'),
|
||||
submenu: [ {
|
||||
click: async () => exportLogs(),
|
||||
label: isMac ? i18n.t('Show Logs in Finder') : i18n.t('Show Logs in Explorer'),
|
||||
}, {
|
||||
click: () => exportCrashDumps(),
|
||||
label: isMac ? i18n.t('Show crash dump in Finder') : i18n.t('Show crash dump in Explorer'),
|
||||
}, {
|
||||
click: () => windowHandler.createMoreInfoWindow(),
|
||||
label: i18n.t('More Information'),
|
||||
} ],
|
||||
} ],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds menu item separator
|
||||
*/
|
||||
private buildSeparator(): Electron.MenuItemConstructorOptions {
|
||||
return { type: 'separator' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets respective accelerators w.r.t roles for the menu template
|
||||
*
|
||||
* @param role {String} The action of the menu item
|
||||
* @param label {String} Menu item name
|
||||
* @return {Object}
|
||||
* @return {Object}.role The action of the menu item
|
||||
* @return {Object}.accelerator keyboard shortcuts and modifiers
|
||||
*/
|
||||
private assignRoleOrLabel(role: string, label: string) {
|
||||
if (isMac) {
|
||||
return label ? { role, label } : { role };
|
||||
}
|
||||
|
||||
if (isWindowsOS) {
|
||||
return label ? { role, label, accelerator: windowsAccelerator[ role ] || '' }
|
||||
: { role, accelerator: windowsAccelerator[ role ] || '' };
|
||||
}
|
||||
|
||||
return label ? { role, label } : { role };
|
||||
}
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
import AutoLaunch = require('auto-launch');
|
||||
import { BrowserWindow, dialog } from 'electron';
|
||||
|
||||
import { isMac } from '../common/env';
|
||||
import { i18n } from '../common/i18n';
|
||||
import { logger } from '../common/logger';
|
||||
import { config, IConfig } from './config-handler';
|
||||
|
||||
const { autoLaunchPath }: IConfig = config.getGlobalConfigFields([ 'autoLaunchPath' ]);
|
||||
@ -34,23 +37,47 @@ class AutoLaunchController extends AutoLaunch {
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable auto launch
|
||||
* Enable auto launch and displays error dialog on failure
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async enableAutoLaunch(): Promise<void> {
|
||||
// log.send(logLevels.INFO, `Enabling auto launch!`);
|
||||
return await this.enable();
|
||||
logger.info(`Enabling auto launch!`);
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
await this.enable()
|
||||
.catch((err) => {
|
||||
const title = 'Error setting AutoLaunch configuration';
|
||||
logger.error(`auto-launch-controller: ${title}: failed to enable auto launch error: ${err}`);
|
||||
if (focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
dialog.showMessageBox(focusedWindow, {
|
||||
message: i18n.t(title) + ': ' + err,
|
||||
title: i18n.t(title),
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable auto launch
|
||||
* Disable auto launch and displays error dialog on failure
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async disableAutoLaunch(): Promise<void> {
|
||||
// log.send(logLevels.INFO, `Disabling auto launch!`);
|
||||
return await this.disable();
|
||||
logger.info(`Disabling auto launch!`);
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
await this.disable()
|
||||
.catch((err) => {
|
||||
const title = 'Error setting AutoLaunch configuration';
|
||||
logger.error(`auto-launch-controller: ${title}: failed to disable auto launch error: ${err}`);
|
||||
if (focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
dialog.showMessageBox(focusedWindow, {
|
||||
message: i18n.t(title) + ': ' + err,
|
||||
title: i18n.t(title),
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -121,8 +121,8 @@ class Config {
|
||||
*
|
||||
* @param data {IConfig}
|
||||
*/
|
||||
public updateUserConfig(data: IConfig): void {
|
||||
this.userConfig = { ...data, ...this.userConfig };
|
||||
public updateUserConfig(data: Partial<IConfig>): void {
|
||||
this.userConfig = { ...this.userConfig, ...data };
|
||||
fs.writeFileSync(this.userConfigPath, JSON.stringify(this.userConfig), { encoding: 'utf8' });
|
||||
}
|
||||
|
||||
|
@ -3,42 +3,14 @@ import { BrowserWindow, ipcMain } from 'electron';
|
||||
import { apiCmds, apiName, IApiArgs } from '../common/api-interface';
|
||||
import { logger } from '../common/logger';
|
||||
import { windowHandler } from './window-handler';
|
||||
import { setDataUrl, showBadgeCount } from './window-utils';
|
||||
|
||||
const checkValidWindow = true;
|
||||
|
||||
/**
|
||||
* Ensure events comes from a window that we have created.
|
||||
* @param {EventEmitter} event node emitter event to be tested
|
||||
* @return {Boolean} returns true if exists otherwise false
|
||||
*/
|
||||
function isValidWindow(event: Electron.Event): boolean {
|
||||
if (!checkValidWindow) {
|
||||
return true;
|
||||
}
|
||||
let result = false;
|
||||
if (event && event.sender) {
|
||||
// validate that event sender is from window we created
|
||||
const browserWin = BrowserWindow.fromWebContents(event.sender);
|
||||
// @ts-ignore
|
||||
const winKey = event.sender.browserWindowOptions && event.sender.browserWindowOptions.winKey;
|
||||
|
||||
result = windowHandler.hasWindow(winKey, browserWin);
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
logger.warn('invalid window try to perform action, ignoring action', event.sender);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
import { isValidWindow, setDataUrl, showBadgeCount } from './window-utils';
|
||||
|
||||
/**
|
||||
* Handle API related ipc messages from renderers. Only messages from windows
|
||||
* we have created are allowed.
|
||||
*/
|
||||
ipcMain.on(apiName.symphonyApi, (event: Electron.Event, arg: IApiArgs) => {
|
||||
if (!isValidWindow(event)) {
|
||||
if (!isValidWindow(BrowserWindow.fromWebContents(event.sender))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -105,15 +77,16 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.Event, arg: IApiArgs) => {
|
||||
if (Array.isArray(arg.sources) && typeof arg.id === 'number') {
|
||||
openScreenPickerWindow(event.sender, arg.sources, arg.id);
|
||||
}
|
||||
break;
|
||||
case ApiCmds.popupMenu: {
|
||||
let browserWin = electron.BrowserWindow.fromWebContents(event.sender);
|
||||
break;*/
|
||||
case apiCmds.popupMenu: {
|
||||
const browserWin = BrowserWindow.fromWebContents(event.sender);
|
||||
if (browserWin && !browserWin.isDestroyed()) {
|
||||
windowMgr.getMenu().popup(browserWin, {x: 20, y: 15, async: true});
|
||||
const appMenu = windowHandler.getApplicationMenu();
|
||||
if (appMenu) appMenu.popupMenu(browserWin);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ApiCmds.optimizeMemoryConsumption:
|
||||
/*case ApiCmds.optimizeMemoryConsumption:
|
||||
if (typeof arg.memory === 'object'
|
||||
&& typeof arg.cpuUsage === 'object'
|
||||
&& typeof arg.memory.workingSetSize === 'number') {
|
||||
|
78
src/browser/reports.ts
Normal file
78
src/browser/reports.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { app, BrowserWindow, dialog, shell } from 'electron';
|
||||
import * as fs from 'fs';
|
||||
|
||||
import * as electron from 'electron';
|
||||
import { isMac } from '../common/env';
|
||||
import { i18n } from '../common/i18n';
|
||||
import { generateArchiveForDirectory } from '../common/utils';
|
||||
|
||||
export const exportLogs = (): void => {
|
||||
const FILE_EXTENSIONS = [ '.log' ];
|
||||
const MAC_LOGS_PATH = '/Library/Logs/Symphony/';
|
||||
const WINDOWS_LOGS_PATH = '\\AppData\\Roaming\\Symphony\\logs';
|
||||
|
||||
const logsPath = isMac ? MAC_LOGS_PATH : WINDOWS_LOGS_PATH;
|
||||
const source = app.getPath('home') + logsPath;
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
|
||||
if (!fs.existsSync(source) && focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
dialog.showMessageBox(focusedWindow, {
|
||||
message: i18n.t('No logs are available to share'),
|
||||
title: i18n.t('Failed!'),
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const destPath = isMac ? '/logs_symphony_' : '\\logs_symphony_';
|
||||
const timestamp = new Date().getTime();
|
||||
const destination = app.getPath('downloads') + destPath + timestamp + '.zip';
|
||||
|
||||
generateArchiveForDirectory(source, destination, FILE_EXTENSIONS)
|
||||
.then(() => {
|
||||
shell.showItemInFolder(destination);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
dialog.showMessageBox(focusedWindow, {
|
||||
message: `${i18n.t('Unable to generate logs due to ')} ${err}`,
|
||||
title: i18n.t('Failed!'),
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const exportCrashDumps = (): void => {
|
||||
const FILE_EXTENSIONS = isMac ? [ '.dmp' ] : [ '.dmp', '.txt' ];
|
||||
const crashesDirectory = (electron.crashReporter as any).getCrashesDirectory();
|
||||
const source = isMac ? crashesDirectory + '/completed' : crashesDirectory;
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
|
||||
if (!fs.existsSync(source) || fs.readdirSync(source).length === 0 && focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
electron.dialog.showMessageBox(focusedWindow as BrowserWindow, {
|
||||
message: i18n.t('No crashes available to share'),
|
||||
title: i18n.t('Failed!'),
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const destPath = isMac ? '/crashes_symphony_' : '\\crashes_symphony_';
|
||||
const timestamp = new Date().getTime();
|
||||
|
||||
const destination = electron.app.getPath('downloads') + destPath + timestamp + '.zip';
|
||||
|
||||
generateArchiveForDirectory(source, destination, FILE_EXTENSIONS)
|
||||
.then(() => {
|
||||
electron.shell.showItemInFolder(destination);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
electron.dialog.showMessageBox(focusedWindow, {
|
||||
message: `${i18n.t('Unable to generate crash reports due to ')} ${err}`,
|
||||
title: i18n.t('Failed!'),
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
@ -3,7 +3,9 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as url from 'url';
|
||||
|
||||
import { isWindowsOS } from '../common/env';
|
||||
import { getCommandLineArgs, getGuid } from '../common/utils';
|
||||
import { AppMenu } from './app-menu';
|
||||
import { config, IConfig } from './config-handler';
|
||||
import { createComponentWindow } from './window-utils';
|
||||
|
||||
@ -13,6 +15,11 @@ interface ICustomBrowserWindowConstructorOpts extends Electron.BrowserWindowCons
|
||||
winKey: string;
|
||||
}
|
||||
|
||||
export interface ICustomBrowserWindow extends Electron.BrowserWindow {
|
||||
winName: string;
|
||||
notificationObj?: object;
|
||||
}
|
||||
|
||||
export class WindowHandler {
|
||||
|
||||
/**
|
||||
@ -70,20 +77,27 @@ export class WindowHandler {
|
||||
return url.format(parsedUrl);
|
||||
}
|
||||
|
||||
private appMenu: AppMenu | null;
|
||||
private readonly windowOpts: ICustomBrowserWindowConstructorOpts;
|
||||
private readonly globalConfig: IConfig;
|
||||
// Window reference
|
||||
private readonly windows: object;
|
||||
private mainWindow: Electron.BrowserWindow | null;
|
||||
private mainWindow: ICustomBrowserWindow | null;
|
||||
private loadingWindow: Electron.BrowserWindow | null;
|
||||
private aboutAppWindow: Electron.BrowserWindow | null;
|
||||
private moreInfoWindow: Electron.BrowserWindow | null;
|
||||
private isAutoReload: boolean;
|
||||
|
||||
constructor(opts?: Electron.BrowserViewConstructorOptions) {
|
||||
this.windows = {};
|
||||
this.windowOpts = { ...WindowHandler.getMainWindowOpts(), ...opts };
|
||||
this.isAutoReload = false;
|
||||
this.appMenu = null;
|
||||
// Window references
|
||||
this.mainWindow = null;
|
||||
this.loadingWindow = null;
|
||||
this.aboutAppWindow = null;
|
||||
this.moreInfoWindow = null;
|
||||
this.globalConfig = config.getGlobalConfigFields([ 'url', 'crashReporter' ]);
|
||||
|
||||
try {
|
||||
@ -98,7 +112,8 @@ export class WindowHandler {
|
||||
* Starting point of the app
|
||||
*/
|
||||
public createApplication() {
|
||||
this.mainWindow = new BrowserWindow(this.windowOpts);
|
||||
this.mainWindow = new BrowserWindow(this.windowOpts) as ICustomBrowserWindow;
|
||||
this.mainWindow.winName = 'main';
|
||||
|
||||
const urlFromCmd = getCommandLineArgs(process.argv, '--url=', false);
|
||||
this.mainWindow.loadURL(urlFromCmd && urlFromCmd.substr(6) || WindowHandler.validateURL(this.globalConfig.url));
|
||||
@ -107,12 +122,15 @@ export class WindowHandler {
|
||||
this.loadingWindow.destroy();
|
||||
this.loadingWindow = null;
|
||||
}
|
||||
if (this.mainWindow) {
|
||||
if (!this.mainWindow) return;
|
||||
if (isWindowsOS && this.mainWindow && config.getConfigFields([ 'isCustomTitleBar' ])) {
|
||||
this.mainWindow.webContents.insertCSS(
|
||||
fs.readFileSync(path.join(__dirname, '..', '/renderer/styles/title-bar.css'), 'utf8').toString(),
|
||||
);
|
||||
this.mainWindow.show();
|
||||
this.mainWindow.webContents.send('initiate-custom-title-bar');
|
||||
}
|
||||
this.mainWindow.show();
|
||||
this.appMenu = new AppMenu();
|
||||
this.createAboutAppWindow();
|
||||
});
|
||||
this.addWindow(this.windowOpts.winKey, this.mainWindow);
|
||||
@ -122,10 +140,46 @@ export class WindowHandler {
|
||||
/**
|
||||
* Gets the main window
|
||||
*/
|
||||
public getMainWindow(): Electron.BrowserWindow | null {
|
||||
public getMainWindow(): ICustomBrowserWindow | null {
|
||||
return this.mainWindow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the window that we have created
|
||||
*
|
||||
* @return {Electron.BrowserWindow}
|
||||
*
|
||||
*/
|
||||
public getAllWindows(): object {
|
||||
return this.windows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the application menu
|
||||
*/
|
||||
public getApplicationMenu(): AppMenu | null {
|
||||
return this.appMenu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets is auto reload when the application
|
||||
* is auto reloaded for optimizing memory
|
||||
*
|
||||
* @param shouldAutoReload {boolean}
|
||||
*/
|
||||
public setIsAutoReload(shouldAutoReload: boolean) {
|
||||
this.isAutoReload = shouldAutoReload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets is auto reload
|
||||
*
|
||||
* @return isAutoReload {boolean}
|
||||
*/
|
||||
public getIsAutoReload(): boolean {
|
||||
return this.isAutoReload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the window and a key has a window
|
||||
* @param key {string}
|
||||
@ -152,7 +206,7 @@ export class WindowHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a about app window
|
||||
* Creates a about app window
|
||||
*/
|
||||
public createAboutAppWindow() {
|
||||
this.aboutAppWindow = createComponentWindow('about-app');
|
||||
@ -163,6 +217,18 @@ export class WindowHandler {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a more info window
|
||||
*/
|
||||
public createMoreInfoWindow() {
|
||||
this.moreInfoWindow = createComponentWindow('more-info-window');
|
||||
this.moreInfoWindow.webContents.once('did-finish-load', () => {
|
||||
if (this.aboutAppWindow) {
|
||||
this.aboutAppWindow.webContents.send('more-info-window');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores information of all the window we have created
|
||||
* @param key {string}
|
||||
|
@ -2,9 +2,11 @@ import { app, BrowserWindow, nativeImage } from 'electron';
|
||||
import * as path from 'path';
|
||||
import * as url from 'url';
|
||||
|
||||
import { isMac } from '../common/env';
|
||||
import { isMac, isWindowsOS } from '../common/env';
|
||||
import { logger } from '../common/logger';
|
||||
import { windowHandler } from './window-handler';
|
||||
import { ICustomBrowserWindow, windowHandler } from './window-handler';
|
||||
|
||||
const checkValidWindow = true;
|
||||
|
||||
/**
|
||||
* Creates components windows
|
||||
@ -50,6 +52,7 @@ export function createComponentWindow(
|
||||
|
||||
/**
|
||||
* Prevents window from navigating
|
||||
*
|
||||
* @param browserWindow
|
||||
*/
|
||||
export function preventWindowNavigation(browserWindow: Electron.BrowserWindow) {
|
||||
@ -65,6 +68,7 @@ export function preventWindowNavigation(browserWindow: Electron.BrowserWindow) {
|
||||
|
||||
/**
|
||||
* Shows the badge count
|
||||
*
|
||||
* @param count {number}
|
||||
*/
|
||||
export function showBadgeCount(count: number): void {
|
||||
@ -95,6 +99,7 @@ export function showBadgeCount(count: number): void {
|
||||
|
||||
/**
|
||||
* Sets the data url
|
||||
*
|
||||
* @param dataUrl
|
||||
* @param count
|
||||
*/
|
||||
@ -106,4 +111,83 @@ export function setDataUrl(dataUrl: string, count: number): void {
|
||||
const desc = 'Symphony has ' + count + ' unread messages';
|
||||
mainWindow.setOverlayIcon(img, desc);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets always on top property based on isAlwaysOnTop
|
||||
*
|
||||
* @param isAlwaysOnTop
|
||||
* @param shouldActivateMainWindow
|
||||
*/
|
||||
export function updateAlwaysOnTop(isAlwaysOnTop: boolean, shouldActivateMainWindow: boolean = true) {
|
||||
const browserWins: ICustomBrowserWindow[] = BrowserWindow.getAllWindows() as ICustomBrowserWindow[];
|
||||
if (browserWins.length > 0) {
|
||||
browserWins
|
||||
.filter((browser) => typeof browser.notificationObj !== 'object')
|
||||
.forEach((browser) => browser.setAlwaysOnTop(isAlwaysOnTop));
|
||||
|
||||
// An issue where changing the alwaysOnTop property
|
||||
// focus the pop-out window
|
||||
// Issue - Electron-209/470
|
||||
const mainWindow = windowHandler.getMainWindow();
|
||||
if (mainWindow && mainWindow.winName && shouldActivateMainWindow) {
|
||||
activate(mainWindow.winName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries finding a window we have created with given name. If found, then
|
||||
* brings to front and gives focus.
|
||||
*
|
||||
* @param {string} windowName Name of target window. Note: main window has
|
||||
* name 'main'.
|
||||
* @param {Boolean} shouldFocus whether to get window to focus or just show
|
||||
* without giving focus
|
||||
*/
|
||||
function activate(windowName: string, shouldFocus: boolean = true): void {
|
||||
|
||||
// Electron-136: don't activate when the app is reloaded programmatically
|
||||
if (windowHandler.getIsAutoReload()) return;
|
||||
|
||||
const windows = windowHandler.getAllWindows();
|
||||
for (const key in windows) {
|
||||
if (windows.hasOwnProperty(key)) {
|
||||
const window = windows[ key ];
|
||||
if (window && !window.isDestroyed() && window.winName === windowName) {
|
||||
|
||||
// Bring the window to the top without focusing
|
||||
// Flash task bar icon in Windows for windows
|
||||
if (!shouldFocus) {
|
||||
window.moveTop();
|
||||
return isWindowsOS ? window.flashFrame(true) : null;
|
||||
}
|
||||
|
||||
return window.isMinimized() ? window.restore() : window.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure events comes from a window that we have created.
|
||||
* @param {BrowserWindow} browserWin node emitter event to be tested
|
||||
* @return {Boolean} returns true if exists otherwise false
|
||||
*/
|
||||
export function isValidWindow(browserWin: Electron.BrowserWindow): boolean {
|
||||
if (!checkValidWindow) {
|
||||
return true;
|
||||
}
|
||||
let result: boolean = false;
|
||||
if (browserWin && !browserWin.isDestroyed()) {
|
||||
// @ts-ignore
|
||||
const winKey = browserWin.webContents.browserWindowOptions && browserWin.webContents.browserWindowOptions.winKey;
|
||||
result = windowHandler.hasWindow(winKey, browserWin);
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
logger.warn('invalid window try to perform action, ignoring action');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
@ -3,6 +3,6 @@ export const isDevEnv = process.env.ELECTRON_DEV ?
|
||||
export const isElectronQA = !!process.env.ELECTRON_QA;
|
||||
|
||||
export const isMac = (process.platform === 'darwin');
|
||||
export const isWindowsOS = (process.platform === 'win32');
|
||||
export const isWindowsOS = true;
|
||||
|
||||
export const isNodeEnv = !!process.env.NODE_ENV;
|
@ -41,7 +41,6 @@ class Translation {
|
||||
* @param data {object}
|
||||
*/
|
||||
public t(value: string, data?: object): string {
|
||||
console.log(process.type);
|
||||
if (this.loadedResource && this.loadedResource[this.locale]) {
|
||||
return formatString(Translation.translate(value, this.loadedResource[this.locale]));
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
// regex match the semver (semantic version) this checks for the pattern X.Y.Z
|
||||
// ex-valid v1.2.0, 1.2.0, 2.3.4-r51
|
||||
import archiver from 'archiver';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const semver = /^v?(?:\d+)(\.(?:[x*]|\d+)(\.(?:[x*]|\d+)(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?)?)?$/i;
|
||||
const patch = /-([0-9A-Za-z-.]+)/;
|
||||
|
||||
@ -195,4 +199,50 @@ const throttle = (func: (...args) => void, wait: number): (...args) => void => {
|
||||
};
|
||||
};
|
||||
|
||||
export { compareVersions, getCommandLineArgs, getGuid, pick, formatString, throttle };
|
||||
/**
|
||||
* Archives files in the source directory
|
||||
* that matches the given file extension
|
||||
*
|
||||
* @param source {String} source path
|
||||
* @param destination {String} destination path
|
||||
* @param fileExtensions {Array} array of file ext
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
const generateArchiveForDirectory = (source: string, destination: string, fileExtensions: string[]): Promise<void> => {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const output = fs.createWriteStream(destination);
|
||||
const archive = archiver('zip', { zlib: { level: 9 } });
|
||||
|
||||
output.on('close', () => {
|
||||
return resolve();
|
||||
});
|
||||
|
||||
archive.on('error', (err) => {
|
||||
return reject(err);
|
||||
});
|
||||
|
||||
archive.pipe(output);
|
||||
|
||||
const files = fs.readdirSync(source);
|
||||
files
|
||||
.filter((file) => fileExtensions.indexOf(path.extname(file)) !== -1)
|
||||
.forEach((file) => {
|
||||
switch (path.extname(file)) {
|
||||
case '.log':
|
||||
archive.file(source + '/' + file, { name: 'logs/' + file });
|
||||
break;
|
||||
case '.dmp':
|
||||
case '.txt': // on Windows .txt files will be created as part of crash dump
|
||||
archive.file(source + '/' + file, { name: 'crashes/' + file });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
archive.finalize();
|
||||
});
|
||||
};
|
||||
|
||||
export { compareVersions, getCommandLineArgs, getGuid, pick, formatString, throttle, generateArchiveForDirectory };
|
@ -65,6 +65,7 @@
|
||||
"Loading Error": "Loading Error",
|
||||
"Minimize": "Minimize",
|
||||
"Minimize on Close": "Minimize on Close",
|
||||
"More Information": "More Information",
|
||||
"Native": "Native",
|
||||
"No crashes available to share": "No crashes available to share",
|
||||
"No logs are available to share": "No logs are available to share",
|
||||
|
@ -65,6 +65,7 @@
|
||||
"Loading Error": "読み込みエラー",
|
||||
"Minimize": "最小化",
|
||||
"Minimize on Close": "閉じるで最小化",
|
||||
"More Information": "詳しくは",
|
||||
"Native": "Native",
|
||||
"No crashes available to share": "共有できるクラッシュはありません",
|
||||
"No logs are available to share": "共有できるログはありません",
|
||||
|
30
src/renderer/more-info.tsx
Normal file
30
src/renderer/more-info.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* Window that display app version and copyright info
|
||||
*/
|
||||
export default class MoreInfo extends React.Component<{}, {}> {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
/**
|
||||
* main render function
|
||||
*/
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div className='MoreInfo'>
|
||||
<span><b>Version Information</b></span>
|
||||
<span className='MoreInfo-electron'>{process.versions.electron}</span>
|
||||
<span className='MoreInfo-chrome'>{process.versions.chrome}</span>
|
||||
<span className='MoreInfo-v8'>{process.versions.v8}</span>
|
||||
<span className='MoreInfo-node'>{process.versions.node}</span>
|
||||
<span className='MoreInfo-openssl'>{process.versions.openssl}</span>
|
||||
<span className='MoreInfo-zlib'>{process.versions.zlib}</span>
|
||||
<span className='MoreInfo-uv'>{process.versions.uv}</span>
|
||||
<span className='MoreInfo-ares'>{process.versions.ares}</span>
|
||||
<span className='MoreInfo-http_parser'>{process.versions.http_parser}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ import * as ReactDOM from 'react-dom';
|
||||
|
||||
import AboutBox from './about-app';
|
||||
import LoadingScreen from './loading-screen';
|
||||
import MoreInfo from './more-info';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', load);
|
||||
|
||||
@ -21,6 +22,9 @@ function load() {
|
||||
case 'loading-screen':
|
||||
component = LoadingScreen;
|
||||
break;
|
||||
case 'more-info-window':
|
||||
component = MoreInfo;
|
||||
break;
|
||||
}
|
||||
const element = React.createElement(component);
|
||||
ReactDOM.render(element, document.getElementById('Root'));
|
||||
|
@ -1,19 +1,10 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
|
||||
import WindowsTitleBar from '../renderer/windows-title-bar';
|
||||
import { SSFApi } from './ssf-api';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', load);
|
||||
|
||||
/**
|
||||
* Injects custom title bar to the document body
|
||||
*/
|
||||
function load() {
|
||||
const element = React.createElement(WindowsTitleBar);
|
||||
ReactDOM.render(element, document.body);
|
||||
}
|
||||
|
||||
createAPI();
|
||||
|
||||
/**
|
||||
@ -35,4 +26,16 @@ function createAPI() {
|
||||
//
|
||||
// @ts-ignore
|
||||
window.ssf = new SSFApi();
|
||||
}
|
||||
}
|
||||
|
||||
// When the window is completely loaded
|
||||
ipcRenderer.on('page-load', () => {
|
||||
const element = React.createElement(WindowsTitleBar);
|
||||
ReactDOM.render(element, document.body);
|
||||
});
|
||||
|
||||
// Creates a custom tile bar for Windows
|
||||
ipcRenderer.on('initiate-custom-title-bar', () => {
|
||||
const element = React.createElement(WindowsTitleBar);
|
||||
ReactDOM.render(element, document.body);
|
||||
});
|
@ -26,7 +26,6 @@ export class SSFApi {
|
||||
* note: for windws the number displayed will be 1 to 99 and 99+
|
||||
*/
|
||||
public setBadgeCount(count: number): void {
|
||||
console.log(count);
|
||||
throttleSetBadgeCount(count);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user