Typescript - Completed window actions & snackbar, Updated i18n module

This commit is contained in:
Kiran Niranjan 2018-12-08 21:17:09 +05:30
parent 7771ea5b41
commit 3f799f10cc
17 changed files with 402 additions and 124 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

@ -3,6 +3,7 @@
"module": "commonjs",
"pretty": true,
"target": "ES2016",
"resolveJsonModule": true,
"jsx": "react",
"lib": [
"es2016",

View File

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