Typescript - Complete application menu

This commit is contained in:
Kiran Niranjan 2018-12-03 23:16:47 +05:30
parent a4858f336b
commit 5e31833b91
16 changed files with 710 additions and 66 deletions

329
src/browser/app-menu.ts Normal file
View 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 };
}
}

View File

@ -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',
});
}
});
}
/**

View File

@ -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' });
}

View File

@ -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
View 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',
});
}
});
};

View File

@ -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}

View File

@ -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;
}

View File

@ -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;

View File

@ -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]));
}

View File

@ -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 };

View File

@ -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",

View File

@ -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": "共有できるログはありません",

View 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>
);
}
}

View File

@ -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'));

View File

@ -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);
});

View File

@ -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);
}