mirror of
https://github.com/finos/SymphonyElectron.git
synced 2024-12-26 08:51:22 -06:00
Typescript - Completed window actions & snackbar, Updated i18n module
This commit is contained in:
parent
7771ea5b41
commit
3f799f10cc
@ -55,7 +55,7 @@ const menuItemsArray = Object.keys(menuSections)
|
||||
export class AppMenu {
|
||||
private menu: Electron.Menu | undefined;
|
||||
private menuList: Electron.MenuItemConstructorOptions[];
|
||||
private readonly locale: LocaleType;
|
||||
private locale: LocaleType;
|
||||
|
||||
constructor() {
|
||||
this.menuList = [];
|
||||
@ -85,7 +85,10 @@ export class AppMenu {
|
||||
* @param locale {LocaleType}
|
||||
*/
|
||||
public update(locale: LocaleType): void {
|
||||
if (this.locale !== locale) this.buildMenu();
|
||||
if (this.locale !== locale) {
|
||||
this.buildMenu();
|
||||
this.locale = locale;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -131,15 +134,15 @@ export class AppMenu {
|
||||
id: menuSections.about,
|
||||
label: app.getName(),
|
||||
submenu: [
|
||||
{ label: i18n.t('About Symphony'), role: 'about' },
|
||||
{ label: i18n.t('About Symphony')(), role: 'about' },
|
||||
this.buildSeparator(),
|
||||
{ label: i18n.t('Services'), role: 'services' },
|
||||
{ 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' },
|
||||
{ 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' },
|
||||
{ label: i18n.t('Quit Symphony')(), role: 'quit' },
|
||||
],
|
||||
};
|
||||
}
|
||||
@ -149,27 +152,27 @@ export class AppMenu {
|
||||
*/
|
||||
private buildEditMenu(): Electron.MenuItemConstructorOptions {
|
||||
const menu = {
|
||||
label: i18n.t('Edit'),
|
||||
label: i18n.t('Edit')(),
|
||||
submenu:
|
||||
[
|
||||
this.assignRoleOrLabel('undo', i18n.t('Undo')),
|
||||
this.assignRoleOrLabel('redo', i18n.t('Redo')),
|
||||
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')),
|
||||
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'),
|
||||
label: i18n.t('Speech')(),
|
||||
submenu: [
|
||||
{ label: i18n.t('Start Speaking'), role: 'startspeaking' },
|
||||
{ label: i18n.t('Stop Speaking'), role: 'stopspeaking' },
|
||||
{ label: i18n.t('Start Speaking')(), role: 'startspeaking' },
|
||||
{ label: i18n.t('Stop Speaking')(), role: 'stopspeaking' },
|
||||
],
|
||||
});
|
||||
}
|
||||
@ -181,18 +184,18 @@ export class AppMenu {
|
||||
*/
|
||||
private buildViewMenu(): Electron.MenuItemConstructorOptions {
|
||||
return {
|
||||
label: i18n.t('View'),
|
||||
label: i18n.t('View')(),
|
||||
submenu: [ {
|
||||
accelerator: 'CmdOrCtrl+R',
|
||||
click: (_item, focusedWindow) => focusedWindow ? focusedWindow.reload() : null,
|
||||
label: i18n.t('Reload'),
|
||||
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.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')),
|
||||
this.assignRoleOrLabel('togglefullscreen', i18n.t('Toggle Full Screen')()),
|
||||
],
|
||||
};
|
||||
}
|
||||
@ -202,11 +205,11 @@ export class AppMenu {
|
||||
*/
|
||||
private buildWindowMenu(): Electron.MenuItemConstructorOptions {
|
||||
return {
|
||||
label: i18n.t('Window'),
|
||||
label: i18n.t('Window')(),
|
||||
role: 'window',
|
||||
submenu: [
|
||||
this.assignRoleOrLabel('minimize', i18n.t('Minimize')),
|
||||
this.assignRoleOrLabel('close', i18n.t('Close')),
|
||||
this.assignRoleOrLabel('minimize', i18n.t('Minimize')()),
|
||||
this.assignRoleOrLabel('close', i18n.t('Close')()),
|
||||
this.buildSeparator(),
|
||||
{
|
||||
checked: launchOnStartup,
|
||||
@ -217,49 +220,49 @@ export class AppMenu {
|
||||
await autoLaunch.disableAutoLaunch();
|
||||
}
|
||||
launchOnStartup = item.checked;
|
||||
config.updateUserConfig({ launchOnStartup });
|
||||
await config.updateUserConfig({ launchOnStartup });
|
||||
},
|
||||
label: i18n.t('Auto Launch On Startup'),
|
||||
label: i18n.t('Auto Launch On Startup')(),
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
checked: isAlwaysOnTop,
|
||||
click: (item) => {
|
||||
click: async (item) => {
|
||||
isAlwaysOnTop = item.checked;
|
||||
updateAlwaysOnTop(item.checked, true);
|
||||
config.updateUserConfig({ alwaysOnTop: item.checked });
|
||||
await config.updateUserConfig({ alwaysOnTop: item.checked });
|
||||
},
|
||||
label: i18n.t('Always on Top'),
|
||||
label: i18n.t('Always on Top')(),
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
checked: minimizeOnClose,
|
||||
click: (item) => {
|
||||
click: async (item) => {
|
||||
minimizeOnClose = item.checked;
|
||||
config.updateUserConfig({ minimizeOnClose });
|
||||
await config.updateUserConfig({ minimizeOnClose });
|
||||
},
|
||||
label: i18n.t('Minimize on Close'),
|
||||
label: i18n.t('Minimize on Close')(),
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
checked: bringToFront,
|
||||
click: (item) => {
|
||||
click: async (item) => {
|
||||
bringToFront = item.checked;
|
||||
config.updateUserConfig({ bringToFront });
|
||||
await config.updateUserConfig({ bringToFront });
|
||||
},
|
||||
label: isWindowsOS
|
||||
? i18n.t('Flash Notification in Taskbar')
|
||||
: i18n.t('Bring to Front on Notifications'),
|
||||
? i18n.t('Flash Notification in Taskbar')()
|
||||
: i18n.t('Bring to Front on Notifications')(),
|
||||
type: 'checkbox',
|
||||
},
|
||||
this.buildSeparator(),
|
||||
{
|
||||
checked: memoryRefresh,
|
||||
click: (item) => {
|
||||
click: async (item) => {
|
||||
memoryRefresh = item.checked;
|
||||
config.updateUserConfig({ memoryRefresh });
|
||||
await config.updateUserConfig({ memoryRefresh });
|
||||
},
|
||||
label: i18n.t('Refresh app when idle'),
|
||||
label: i18n.t('Refresh app when idle')(),
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
@ -273,7 +276,7 @@ export class AppMenu {
|
||||
}
|
||||
}
|
||||
},
|
||||
label: i18n.t('Clear cache and Reload'),
|
||||
label: i18n.t('Clear cache and Reload')(),
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -284,26 +287,26 @@ export class AppMenu {
|
||||
*/
|
||||
private buildHelpMenu(): Electron.MenuItemConstructorOptions {
|
||||
return {
|
||||
label: i18n.t('Help'),
|
||||
label: i18n.t('Help')(),
|
||||
role: 'help',
|
||||
submenu:
|
||||
[ {
|
||||
click: () => shell.openExternal(i18n.t('Help Url')),
|
||||
label: i18n.t('Symphony Help'),
|
||||
click: () => shell.openExternal(i18n.t('Help Url')()),
|
||||
label: i18n.t('Symphony Help')(),
|
||||
}, {
|
||||
click: () => shell.openExternal(i18n.t('Symphony Url')),
|
||||
label: i18n.t('Learn More'),
|
||||
click: () => shell.openExternal(i18n.t('Symphony Url')()),
|
||||
label: i18n.t('Learn More')(),
|
||||
}, {
|
||||
label: i18n.t('Troubleshooting'),
|
||||
label: i18n.t('Troubleshooting')(),
|
||||
submenu: [ {
|
||||
click: async () => exportLogs(),
|
||||
label: isMac ? i18n.t('Show Logs in Finder') : i18n.t('Show Logs in Explorer'),
|
||||
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'),
|
||||
label: isMac ? i18n.t('Show crash dump in Finder')() : i18n.t('Show crash dump in Explorer')(),
|
||||
}, {
|
||||
click: () => windowHandler.createMoreInfoWindow(),
|
||||
label: i18n.t('More Information'),
|
||||
label: i18n.t('More Information')(),
|
||||
} ],
|
||||
} ],
|
||||
};
|
||||
|
@ -50,8 +50,8 @@ class AutoLaunchController extends AutoLaunch {
|
||||
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),
|
||||
message: i18n.t(title)() + ': ' + err,
|
||||
title: i18n.t(title)(),
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
@ -72,8 +72,8 @@ class AutoLaunchController extends AutoLaunch {
|
||||
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),
|
||||
message: i18n.t(title)() + ': ' + err,
|
||||
title: i18n.t(title)(),
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { app } from 'electron';
|
||||
import { app, dialog } from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import * as util from 'util';
|
||||
import { isDevEnv, isMac } from '../common/env';
|
||||
import { logger } from '../common/logger';
|
||||
import { compareVersions, pick } from '../common/utils';
|
||||
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
|
||||
const ignoreSettings = [
|
||||
'minimizeOnClose',
|
||||
'launchOnStartup',
|
||||
@ -34,6 +37,7 @@ export interface IConfig {
|
||||
permissions: IPermission;
|
||||
customFlags: ICustomFlag;
|
||||
crashReporter: ICrashReporter;
|
||||
mainWinPos: ICustomRectangle;
|
||||
}
|
||||
|
||||
export interface IPermission {
|
||||
@ -63,6 +67,11 @@ export interface INotificationSetting {
|
||||
display: string;
|
||||
}
|
||||
|
||||
export interface ICustomRectangle extends Electron.Rectangle {
|
||||
isMaximized: boolean;
|
||||
isFullScreen: boolean;
|
||||
}
|
||||
|
||||
class Config {
|
||||
private userConfig: IConfig | {};
|
||||
private globalConfig: IConfig | {};
|
||||
@ -121,9 +130,14 @@ class Config {
|
||||
*
|
||||
* @param data {IConfig}
|
||||
*/
|
||||
public updateUserConfig(data: Partial<IConfig>): void {
|
||||
public async updateUserConfig(data: Partial<IConfig>): Promise<void> {
|
||||
this.userConfig = { ...this.userConfig, ...data };
|
||||
fs.writeFileSync(this.userConfigPath, JSON.stringify(this.userConfig), { encoding: 'utf8' });
|
||||
try {
|
||||
await writeFile(this.userConfigPath, JSON.stringify(this.userConfig), { encoding: 'utf8' });
|
||||
} catch (error) {
|
||||
logger.error(`config-handler: failed to update user config file with ${data}`, error);
|
||||
dialog.showErrorBox('Error updating user config file', 'failed to write user config file with ${}');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -167,9 +181,9 @@ class Config {
|
||||
* If user config doesn't exits?
|
||||
* this creates a new one with { configVersion: current_app_version }
|
||||
*/
|
||||
private readUserConfig() {
|
||||
private async readUserConfig() {
|
||||
if (!fs.existsSync(this.userConfigPath)) {
|
||||
this.updateUserConfig({ configVersion: app.getVersion().toString() } as IConfig);
|
||||
await this.updateUserConfig({ configVersion: app.getVersion().toString() } as IConfig);
|
||||
}
|
||||
this.userConfig = this.parseConfigData(fs.readFileSync(this.userConfigPath, 'utf8'));
|
||||
}
|
||||
|
@ -35,7 +35,6 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.Event, arg: IApiArgs) => {
|
||||
break;*/
|
||||
case apiCmds.setBadgeCount:
|
||||
if (typeof arg.count === 'number') {
|
||||
console.log(arg.count);
|
||||
showBadgeCount(arg.count);
|
||||
}
|
||||
break;
|
||||
|
@ -17,8 +17,8 @@ export const exportLogs = (): void => {
|
||||
|
||||
if (!fs.existsSync(source) && focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
dialog.showMessageBox(focusedWindow, {
|
||||
message: i18n.t('No logs are available to share'),
|
||||
title: i18n.t('Failed!'),
|
||||
message: i18n.t('No logs are available to share')(),
|
||||
title: i18n.t('Failed!')(),
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
@ -34,8 +34,8 @@ export const exportLogs = (): void => {
|
||||
.catch((err) => {
|
||||
if (focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
dialog.showMessageBox(focusedWindow, {
|
||||
message: `${i18n.t('Unable to generate logs due to ')} ${err}`,
|
||||
title: i18n.t('Failed!'),
|
||||
message: `${i18n.t('Unable to generate logs due to ')()} ${err}`,
|
||||
title: i18n.t('Failed!')(),
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
@ -50,8 +50,8 @@ export const exportCrashDumps = (): void => {
|
||||
|
||||
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!'),
|
||||
message: i18n.t('No crashes available to share')(),
|
||||
title: i18n.t('Failed!')(),
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
@ -69,8 +69,8 @@ export const exportCrashDumps = (): void => {
|
||||
.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!'),
|
||||
message: `${i18n.t('Unable to generate crash reports due to ')()} ${err}`,
|
||||
title: i18n.t('Failed!')(),
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ class ScreenSnippet {
|
||||
if (this.child) this.child.kill();
|
||||
try {
|
||||
await this.execCmd(this.captureUtil, this.captureUtilArgs);
|
||||
const { message, data, type } = await this.convertFileToData();
|
||||
const { message, data, type }: IScreenSnippet = await this.convertFileToData();
|
||||
webContents.send('screen-snippet-data', { message, data, type });
|
||||
} catch (error) {
|
||||
logger.error(`screen-snippet: screen snippet process was killed`, error);
|
||||
|
40
src/browser/window-actions.ts
Normal file
40
src/browser/window-actions.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import { throttle } from '../common/utils';
|
||||
import { config } from './config-handler';
|
||||
import { ICustomBrowserWindow } from './window-handler';
|
||||
|
||||
export const saveWindowSettings = (): void => {
|
||||
const browserWindow = BrowserWindow.getFocusedWindow() as ICustomBrowserWindow;
|
||||
|
||||
if (browserWindow && !browserWindow.isDestroyed()) {
|
||||
const [ x, y ] = browserWindow.getPosition();
|
||||
const [ width, height ] = browserWindow.getSize();
|
||||
if (x && y && width && height) {
|
||||
browserWindow.webContents.send('boundChanges', { x, y, width, height });
|
||||
|
||||
if (browserWindow.winName === 'main') {
|
||||
const isMaximized = browserWindow.isMaximized();
|
||||
const isFullScreen = browserWindow.isFullScreen();
|
||||
config.updateUserConfig({ mainWinPos: { x, y, width, height, isMaximized, isFullScreen } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export const enterFullScreen = () => {
|
||||
const browserWindow = BrowserWindow.getFocusedWindow() as ICustomBrowserWindow;
|
||||
if (browserWindow && !browserWindow.isDestroyed() && browserWindow.winName === 'main') {
|
||||
browserWindow.webContents.send('window-enter-full-screen');
|
||||
}
|
||||
};
|
||||
|
||||
export const leaveFullScreen = () => {
|
||||
const browserWindow = BrowserWindow.getFocusedWindow() as ICustomBrowserWindow;
|
||||
if (browserWindow && !browserWindow.isDestroyed() && browserWindow.winName === 'main') {
|
||||
browserWindow.webContents.send('window-leave-full-screen');
|
||||
}
|
||||
};
|
||||
|
||||
export const throttledWindowChanges = throttle(saveWindowSettings, 1000);
|
@ -1,16 +1,17 @@
|
||||
import * as electron from 'electron';
|
||||
import { BrowserWindow, crashReporter } from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as url from 'url';
|
||||
|
||||
import { buildNumber, clientVersion, version } from '../../package.json';
|
||||
import { isWindowsOS } from '../common/env';
|
||||
import { getCommandLineArgs, getGuid } from '../common/utils';
|
||||
import { AppMenu } from './app-menu';
|
||||
import { config, IConfig } from './config-handler';
|
||||
import { enterFullScreen, leaveFullScreen, throttledWindowChanges } from './window-actions';
|
||||
import { createComponentWindow } from './window-utils';
|
||||
|
||||
const { buildNumber, clientVersion, version } = require('../../package.json'); // tslint:disable-line:no-var-requires
|
||||
|
||||
interface ICustomBrowserWindowConstructorOpts extends Electron.BrowserWindowConstructorOptions {
|
||||
winKey: string;
|
||||
}
|
||||
@ -20,28 +21,11 @@ export interface ICustomBrowserWindow extends Electron.BrowserWindow {
|
||||
notificationObj?: object;
|
||||
}
|
||||
|
||||
export class WindowHandler {
|
||||
// Default window width & height
|
||||
const DEFAULT_WIDTH: number = 900;
|
||||
const DEFAULT_HEIGHT: number = 900;
|
||||
|
||||
/**
|
||||
* Main window opts
|
||||
*/
|
||||
private static getMainWindowOpts(): ICustomBrowserWindowConstructorOpts {
|
||||
return {
|
||||
alwaysOnTop: false,
|
||||
frame: true,
|
||||
minHeight: 300,
|
||||
minWidth: 400,
|
||||
show: false,
|
||||
title: 'Symphony',
|
||||
webPreferences: {
|
||||
nativeWindowOpen: true,
|
||||
nodeIntegration: false,
|
||||
preload: path.join(__dirname, '../renderer/preload-main'),
|
||||
sandbox: false,
|
||||
},
|
||||
winKey: getGuid(),
|
||||
};
|
||||
}
|
||||
export class WindowHandler {
|
||||
|
||||
/**
|
||||
* Loading window opts
|
||||
@ -82,6 +66,7 @@ export class WindowHandler {
|
||||
|
||||
private readonly windowOpts: ICustomBrowserWindowConstructorOpts;
|
||||
private readonly globalConfig: IConfig;
|
||||
private readonly config: IConfig;
|
||||
// Window reference
|
||||
private readonly windows: object;
|
||||
private mainWindow: ICustomBrowserWindow | null;
|
||||
@ -90,8 +75,12 @@ export class WindowHandler {
|
||||
private moreInfoWindow: Electron.BrowserWindow | null;
|
||||
|
||||
constructor(opts?: Electron.BrowserViewConstructorOptions) {
|
||||
// Settings
|
||||
this.config = config.getConfigFields([ 'isCustomTitleBar', 'mainWinPos' ]);
|
||||
this.globalConfig = config.getGlobalConfigFields([ 'url', 'crashReporter' ]);
|
||||
|
||||
this.windows = {};
|
||||
this.windowOpts = { ...WindowHandler.getMainWindowOpts(), ...opts };
|
||||
this.windowOpts = { ...this.getMainWindowOpts(), ...opts };
|
||||
this.isAutoReload = false;
|
||||
this.appMenu = null;
|
||||
// Window references
|
||||
@ -99,7 +88,6 @@ export class WindowHandler {
|
||||
this.loadingWindow = null;
|
||||
this.aboutAppWindow = null;
|
||||
this.moreInfoWindow = null;
|
||||
this.globalConfig = config.getGlobalConfigFields([ 'url', 'crashReporter' ]);
|
||||
|
||||
try {
|
||||
const extra = { podUrl: this.globalConfig.url, process: 'main' };
|
||||
@ -113,25 +101,46 @@ export class WindowHandler {
|
||||
* Starting point of the app
|
||||
*/
|
||||
public createApplication() {
|
||||
this.mainWindow = new BrowserWindow(this.windowOpts) as ICustomBrowserWindow;
|
||||
// set window opts with additional config
|
||||
this.mainWindow = new BrowserWindow({
|
||||
...this.windowOpts, ...this.getBounds(this.config.mainWinPos),
|
||||
}) as ICustomBrowserWindow;
|
||||
this.mainWindow.winName = 'main';
|
||||
|
||||
// Event needed to hide native menu bar on Windows 10 as we use custom menu bar
|
||||
this.mainWindow.webContents.once('did-start-loading', () => {
|
||||
if ((this.config.isCustomTitleBar || isWindowsOS) && this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||
this.mainWindow.setMenuBarVisibility(false);
|
||||
}
|
||||
});
|
||||
|
||||
const urlFromCmd = getCommandLineArgs(process.argv, '--url=', false);
|
||||
this.mainWindow.loadURL(urlFromCmd && urlFromCmd.substr(6) || WindowHandler.validateURL(this.globalConfig.url));
|
||||
this.mainWindow.webContents.on('did-finish-load', () => {
|
||||
// close the loading window when
|
||||
// the main windows finished loading
|
||||
if (this.loadingWindow) {
|
||||
this.loadingWindow.destroy();
|
||||
this.loadingWindow = null;
|
||||
}
|
||||
if (!this.mainWindow) return;
|
||||
if (isWindowsOS && this.mainWindow && config.getConfigFields([ 'isCustomTitleBar' ])) {
|
||||
// early exit if the window has already been destroyed
|
||||
if (!this.mainWindow || this.mainWindow.isDestroyed()) return;
|
||||
|
||||
// Injects custom title bar css into the webContents
|
||||
if (isWindowsOS && this.mainWindow && this.config.isCustomTitleBar) {
|
||||
this.mainWindow.webContents.insertCSS(
|
||||
fs.readFileSync(path.join(__dirname, '..', '/renderer/styles/title-bar.css'), 'utf8').toString(),
|
||||
);
|
||||
this.mainWindow.webContents.send('initiate-custom-title-bar');
|
||||
}
|
||||
this.mainWindow.show();
|
||||
this.mainWindow.webContents.insertCSS(
|
||||
fs.readFileSync(path.join(__dirname, '..', '/renderer/styles/snack-bar.css'), 'utf8').toString(),
|
||||
);
|
||||
this.mainWindow.webContents.send('page-load');
|
||||
this.appMenu = new AppMenu();
|
||||
this.monitorWindowActions();
|
||||
// Ready to show the window
|
||||
this.mainWindow.show();
|
||||
});
|
||||
this.mainWindow.webContents.toggleDevTools();
|
||||
this.addWindow(this.windowOpts.winKey, this.mainWindow);
|
||||
@ -171,7 +180,7 @@ export class WindowHandler {
|
||||
* @param window {Electron.BrowserWindow}
|
||||
*/
|
||||
public hasWindow(key: string, window: Electron.BrowserWindow): boolean {
|
||||
const browserWindow = this.windows[key];
|
||||
const browserWindow = this.windows[ key ];
|
||||
return browserWindow && window === browserWindow;
|
||||
}
|
||||
|
||||
@ -220,7 +229,68 @@ export class WindowHandler {
|
||||
* @param browserWindow {Electron.BrowserWindow}
|
||||
*/
|
||||
private addWindow(key: string, browserWindow: Electron.BrowserWindow): void {
|
||||
this.windows[key] = browserWindow;
|
||||
this.windows[ key ] = browserWindow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the main window bounds
|
||||
*/
|
||||
private monitorWindowActions(): void {
|
||||
const eventNames = [ 'move', 'resize', 'maximize', 'unmaximize' ];
|
||||
eventNames.forEach((event: string) => {
|
||||
// @ts-ignore
|
||||
if (this.mainWindow) this.mainWindow.on(event, throttledWindowChanges);
|
||||
});
|
||||
if (this.mainWindow) {
|
||||
this.mainWindow.on('enter-full-screen', enterFullScreen);
|
||||
this.mainWindow.on('leave-full-screen', leaveFullScreen);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the config stored rectangle if it is contained within the workArea of at
|
||||
* least one of the screens else returns the default rectangle value with out x, y
|
||||
* as the default is to center the window
|
||||
*
|
||||
* @param mainWinPos {Electron.Rectangle}
|
||||
* @return {x?: Number, y?: Number, width: Number, height: Number}
|
||||
*/
|
||||
private getBounds(mainWinPos): Partial<Electron.Rectangle> {
|
||||
if (!mainWinPos) return { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT };
|
||||
const displays = electron.screen.getAllDisplays();
|
||||
|
||||
for (let i = 0, len = displays.length; i < len; i++) {
|
||||
const workArea = displays[ i ].workArea;
|
||||
if (mainWinPos.x >= workArea.x && mainWinPos.y >= workArea.y &&
|
||||
((mainWinPos.x + mainWinPos.width) <= (workArea.x + workArea.width)) &&
|
||||
((mainWinPos.y + mainWinPos.height) <= (workArea.y + workArea.height))) {
|
||||
return mainWinPos;
|
||||
}
|
||||
}
|
||||
return { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT };
|
||||
}
|
||||
|
||||
/**
|
||||
* Main window opts
|
||||
*/
|
||||
private getMainWindowOpts(): ICustomBrowserWindowConstructorOpts {
|
||||
// config fields
|
||||
const { isCustomTitleBar } = this.config;
|
||||
return {
|
||||
alwaysOnTop: false,
|
||||
frame: !(isCustomTitleBar && isWindowsOS),
|
||||
minHeight: 300,
|
||||
minWidth: 300,
|
||||
show: false,
|
||||
title: 'Symphony',
|
||||
webPreferences: {
|
||||
nativeWindowOpen: true,
|
||||
nodeIntegration: false,
|
||||
preload: path.join(__dirname, '../renderer/preload-main'),
|
||||
sandbox: false,
|
||||
},
|
||||
winKey: getGuid(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,8 +7,20 @@ const localeCodeRegex = /^([a-z]{2})-([A-Z]{2})$/;
|
||||
|
||||
export type LocaleType = 'en-US' | 'ja-JP';
|
||||
|
||||
type formaterFunction = (...args: any[]) => string;
|
||||
|
||||
class Translation {
|
||||
private static translate = (value: string, resource) => resource[value];
|
||||
/**
|
||||
* Returns translated string with respect to value, resource & name space
|
||||
*
|
||||
* @param value {string} key field in the resources
|
||||
* @param resource {string} current locale resource
|
||||
* @param namespace {string} name space in the resource
|
||||
*/
|
||||
private static translate(value: string, resource: JSON | null, namespace: string | undefined): string {
|
||||
return resource ? Translation.getResource(resource, namespace)[value] : null;
|
||||
}
|
||||
private static getResource = (resource: JSON, namespace: string | undefined): JSON => namespace ? resource[namespace] : resource;
|
||||
private locale: LocaleType = 'en-US';
|
||||
private loadedResource: object = {};
|
||||
|
||||
@ -37,14 +49,18 @@ class Translation {
|
||||
* fetches and returns the translated value
|
||||
*
|
||||
* @param value {string}
|
||||
* @param data {object}
|
||||
* @param namespace {string}
|
||||
* @example t('translate and formats {data} ', namespace)({ data: 'string' })
|
||||
* @returns translate and formats string
|
||||
*/
|
||||
public t(value: string, data?: object): string {
|
||||
if (this.loadedResource && this.loadedResource[this.locale]) {
|
||||
return formatString(Translation.translate(value, this.loadedResource[this.locale]));
|
||||
}
|
||||
const resource = this.loadResource(this.locale);
|
||||
return formatString(resource ? resource[value] : value || value, data);
|
||||
public t(value: string, namespace?: string): formaterFunction {
|
||||
return (...args: any[]): string => {
|
||||
if (this.loadedResource && this.loadedResource[this.locale]) {
|
||||
return formatString(Translation.translate(value, this.loadedResource[this.locale], namespace), args);
|
||||
}
|
||||
const resource = this.loadResource(this.locale);
|
||||
return formatString(Translation.translate(value, resource, namespace) || value, args);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,11 +68,10 @@ class Translation {
|
||||
*
|
||||
* @param locale
|
||||
*/
|
||||
public loadResource(locale: LocaleType): object | null {
|
||||
public loadResource(locale: LocaleType): JSON | null {
|
||||
const resourcePath = path.resolve(__dirname, '..', 'locale', `${locale}.json`);
|
||||
|
||||
if (!fs.existsSync(resourcePath)) {
|
||||
// logger.error(`Translation: locale resource path does not exits ${resourcePath}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,9 @@ export default class AboutApp extends React.Component<{}, IState> {
|
||||
clientVersion: '0',
|
||||
version: 'N/A',
|
||||
};
|
||||
this.updateState = this.updateState.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* main render function
|
||||
*/
|
||||
@ -42,7 +44,7 @@ export default class AboutApp extends React.Component<{}, IState> {
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
ipcRenderer.on('about-app-data', this.updateState.bind(this));
|
||||
ipcRenderer.on('about-app-data', this.updateState);
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
@ -51,6 +53,7 @@ export default class AboutApp extends React.Component<{}, IState> {
|
||||
|
||||
/**
|
||||
* Sets the About app state
|
||||
*
|
||||
* @param _event
|
||||
* @param data {Object} { buildNumber, clientVersion, version }
|
||||
*/
|
||||
|
@ -3,6 +3,7 @@ import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
|
||||
import WindowsTitleBar from '../renderer/windows-title-bar';
|
||||
import SnackBar from './snack-bar';
|
||||
import { SSFApi } from './ssf-api';
|
||||
|
||||
interface ISSFWindow extends Window {
|
||||
@ -37,8 +38,13 @@ createAPI();
|
||||
|
||||
// When the window is completely loaded
|
||||
ipcRenderer.on('page-load', () => {
|
||||
const element = React.createElement(WindowsTitleBar);
|
||||
ReactDOM.render(element, document.body);
|
||||
// injects custom window title bar
|
||||
const titleBar = React.createElement(WindowsTitleBar);
|
||||
ReactDOM.render(titleBar, document.body.appendChild(document.createElement( 'div' )));
|
||||
|
||||
// injects snack bar
|
||||
const snackBar = React.createElement(SnackBar);
|
||||
ReactDOM.render(snackBar, document.body.appendChild(document.createElement( 'div' )));
|
||||
});
|
||||
|
||||
// Creates a custom tile bar for Windows
|
||||
|
69
src/renderer/snack-bar.tsx
Normal file
69
src/renderer/snack-bar.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import * as React from 'react';
|
||||
|
||||
import Timer = NodeJS.Timer;
|
||||
import { i18n } from '../common/i18n';
|
||||
|
||||
interface IState {
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
const SNACKBAR_NAMESPACE = 'SnackBar';
|
||||
|
||||
export default class SnackBar extends React.Component<{}, IState> {
|
||||
private snackBarTimer: Timer | undefined;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
show: false,
|
||||
};
|
||||
this.showSnackBar = this.showSnackBar.bind(this);
|
||||
this.removeSnackBar = this.removeSnackBar.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the custom title bar
|
||||
*/
|
||||
public render(): JSX.Element | undefined {
|
||||
const { show } = this.state;
|
||||
|
||||
return show ? (
|
||||
<div className='SnackBar SnackBar-show'>
|
||||
<span >{i18n.t('Press ', SNACKBAR_NAMESPACE)()}</span>
|
||||
<span className='SnackBar-esc'>{i18n.t('esc', SNACKBAR_NAMESPACE)()}</span>
|
||||
<span >{i18n.t(' to exit full screen', SNACKBAR_NAMESPACE)()}</span>
|
||||
</div>
|
||||
) : <div> </div>;
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
ipcRenderer.on('window-enter-full-screen', this.showSnackBar);
|
||||
ipcRenderer.on('window-leave-full-screen', this.removeSnackBar);
|
||||
}
|
||||
|
||||
public componentWillMount(): void {
|
||||
ipcRenderer.removeListener('window-enter-full-screen', this.showSnackBar);
|
||||
ipcRenderer.removeListener('window-leave-full-screen', this.removeSnackBar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays snackbar for 3sec
|
||||
*/
|
||||
public showSnackBar(): void {
|
||||
this.setState({ show: true });
|
||||
this.snackBarTimer = setTimeout(() => {
|
||||
this.removeSnackBar();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes snackbar
|
||||
*/
|
||||
public removeSnackBar(): void {
|
||||
this.setState({ show: false });
|
||||
if (this.snackBarTimer) {
|
||||
clearTimeout(this.snackBarTimer);
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import {
|
||||
IBadgeCount,
|
||||
IScreenSnippet,
|
||||
} from '../common/api-interface';
|
||||
import { i18n, LocaleType } from '../common/i18n';
|
||||
import { throttle } from '../common/utils';
|
||||
|
||||
interface ILocalObject {
|
||||
@ -90,6 +91,7 @@ export class SSFApi {
|
||||
*/
|
||||
public setLocale(locale): void {
|
||||
if (typeof locale === 'string') {
|
||||
i18n.setLocale(locale as LocaleType);
|
||||
throttledSetLocale(locale);
|
||||
}
|
||||
}
|
||||
|
52
src/renderer/styles/snack-bar.less
Normal file
52
src/renderer/styles/snack-bar.less
Normal file
@ -0,0 +1,52 @@
|
||||
@white: rgba(255,255,255, 1);
|
||||
@grey: rgba(51,51,51, 1);
|
||||
|
||||
@-webkit-keyframes fadein {
|
||||
from {
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
top: 30px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes fadeout {
|
||||
from {
|
||||
top: 30px;
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.snackbar {
|
||||
visibility: hidden;
|
||||
min-width: 250px;
|
||||
margin-left: -135px;
|
||||
background-color: @grey;
|
||||
color: @white;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
padding: 16px;
|
||||
position: fixed;
|
||||
z-index: 2147483647;
|
||||
left: 50%;
|
||||
top: 30px;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
|
||||
&-esc {
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
border: 2px solid @white;
|
||||
background-color: @grey;
|
||||
}
|
||||
|
||||
&-show {
|
||||
visibility: visible;
|
||||
-webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s;
|
||||
animation: fadein 0.5s, fadeout 0.5s 2.5s;
|
||||
}
|
||||
}
|
@ -109,7 +109,7 @@ export default class WindowsTitleBar extends React.Component<{}, IState> {
|
||||
>
|
||||
<div className='title-bar-button-container'>
|
||||
<button
|
||||
title={i18n.t('Menu')}
|
||||
title={i18n.t('Menu')()}
|
||||
className='hamburger-menu-button'
|
||||
onClick={this.eventHandlers.onShowMenu}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
@ -128,7 +128,7 @@ export default class WindowsTitleBar extends React.Component<{}, IState> {
|
||||
<div className='title-bar-button-container'>
|
||||
<button
|
||||
className='title-bar-button'
|
||||
title={i18n.t('Minimize')}
|
||||
title={i18n.t('Minimize')()}
|
||||
onClick={this.eventHandlers.onMinimize}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
>
|
||||
@ -143,7 +143,7 @@ export default class WindowsTitleBar extends React.Component<{}, IState> {
|
||||
<div className='title-bar-button-container'>
|
||||
<button
|
||||
className='title-bar-button'
|
||||
title={i18n.t('Close')}
|
||||
title={i18n.t('Close')()}
|
||||
onClick={this.eventHandlers.onClose}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
>
|
||||
@ -169,7 +169,7 @@ export default class WindowsTitleBar extends React.Component<{}, IState> {
|
||||
return (
|
||||
<button
|
||||
className='title-bar-button'
|
||||
title={i18n.t('Maximize')}
|
||||
title={i18n.t('Maximize')()}
|
||||
onClick={this.eventHandlers.onUnmaximize}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
>
|
||||
@ -185,7 +185,7 @@ export default class WindowsTitleBar extends React.Component<{}, IState> {
|
||||
return (
|
||||
<button
|
||||
className='title-bar-button'
|
||||
title={i18n.t('unMaximize')}
|
||||
title={i18n.t('unMaximize')()}
|
||||
onClick={this.eventHandlers.onMaximize }
|
||||
onMouseDown={this.handleMouseDown}
|
||||
>
|
||||
@ -223,7 +223,6 @@ export default class WindowsTitleBar extends React.Component<{}, IState> {
|
||||
*/
|
||||
public maximize(): void {
|
||||
if (this.isValidWindow()) {
|
||||
console.log(this.window.maximize());
|
||||
this.window.maximize();
|
||||
this.setState({ isMaximized: true });
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
"module": "commonjs",
|
||||
"pretty": true,
|
||||
"target": "ES2016",
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "react",
|
||||
"lib": [
|
||||
"es2016",
|
||||
|
@ -2,6 +2,11 @@
|
||||
"extends": [
|
||||
"tslint:recommended"
|
||||
],
|
||||
"linterOptions": {
|
||||
"exclude": [
|
||||
"**/*.json"
|
||||
]
|
||||
},
|
||||
"rules": {
|
||||
"curly": false,
|
||||
"eofline": false,
|
||||
@ -28,7 +33,7 @@
|
||||
"no-var-requires": true,
|
||||
"only-arrow-functions": true,
|
||||
"no-console": [
|
||||
false,
|
||||
true,
|
||||
"log",
|
||||
"error"
|
||||
],
|
||||
|
Loading…
Reference in New Issue
Block a user