import * as path from 'path'; import {app, BrowserWindow, globalShortcut, Tray, shell, dialog, Menu} from 'electron'; import windowState = require('electron-window-state'); import * as menubar from 'menubar'; import log from './log'; import {Config} from './config'; import AccountSwitcher, {partitionForAccount} from './account_switcher'; import defaultMenu from './default_menu'; const IS_DEBUG = process.env.NODE_ENV === 'development'; const IS_DARWIN = process.platform === 'darwin'; const APP_ICON = path.join(__dirname, '..', 'resources', 'icon', 'icon.png'); const PRELOAD_JS = path.join(__dirname, '..', 'renderer', 'preload.js'); export class App { private switcher: AccountSwitcher; constructor(private win: Electron.BrowserWindow, private config: Config) { if (config.accounts.length === 0) { throw new Error('No account found. Please check the config.'); } if (IS_DARWIN) { app.dock.setIcon(APP_ICON); } Menu.setApplicationMenu(defaultMenu()); this.switcher = new AccountSwitcher(win, this.config.accounts); this.switcher.on('did-switch', () => this.open()); if (!IS_DARWIN) { // Users can still access menu bar with pressing Alt key. this.win.setMenu(Menu.getApplicationMenu()); } this.win.webContents.on('will-navigate', (e, url) => { if (!url.startsWith(`https://${this.switcher.current.host}`)) { e.preventDefault(); shell.openExternal(url); } }); this.win.webContents.on('new-window', (e, url) => { e.preventDefault(); shell.openExternal(url); }); } open() { this.win.loadURL(`https://${this.switcher.current.host}${this.switcher.current.default_page}`); this.win.webContents.session.setPermissionRequestHandler((contents, permission, callback) => { if (permission !== 'geolocation' && permission !== 'media') { // Granted callback(true); return; } dialog.showMessageBox({ type: 'question', buttons: ['Accept', 'Reject'], message: `Permission '${permission}' is requested by ${contents.getURL()}`, detail: "Please choose one of 'Accept' or 'Reject'", }, (buttonIndex: number) => { const granted = buttonIndex === 0; callback(granted); }); }); } } function trayIcon(color: string) { return path.join(__dirname, '..', 'resources', 'icon', `tray-icon-${ color === 'white' ? 'white' : 'black' }@2x.png`); } export default function startApp(config: Config) { return (config.normal_window ? startNormalWindow : startMenuBar)(config) .then(win => new App(win, config)); } function startNormalWindow(config: Config): Promise { log.debug('Setup a normal window'); return new Promise(resolve => { const state = windowState({ defaultWidth: 600, defaultHeight: 800, }); const win = new BrowserWindow({ width: state.width, height: state.height, x: state.x, y: state.y, icon: APP_ICON, show: false, useContentSize: true, autoHideMenuBar: true, webPreferences: { nodeIntegration: false, sandbox: true, preload: PRELOAD_JS, partition: partitionForAccount(config.accounts[0]), }, }); win.once('ready-to-show', () => { win.show(); }); win.once('closed', () => { app.quit(); }); if (state.isFullScreen) { win.setFullScreen(true); } else if (state.isMaximized) { win.maximize(); } state.manage(win); const toggleWindow = () => { if (win.isFocused()) { log.debug('Toggle window: shown -> hidden'); if (IS_DARWIN) { app.hide(); } else { win.hide(); } } else { log.debug('Toggle window: hidden -> shown'); win.show(); } }; win.webContents.on('dom-ready', () => { log.debug('Send config to renderer procress'); win.webContents.send('mstdn:config', config); }); win.webContents.once('dom-ready', () => { log.debug('Normal window application was launched'); if (config.hot_key) { globalShortcut.register(config.hot_key, toggleWindow); log.debug('Hot key was set to:', config.hot_key); } if (IS_DEBUG) { win.webContents.openDevTools({mode: 'detach'}); } }); const normalIcon = trayIcon(config.icon_color); const tray = new Tray(normalIcon); tray.on('click', toggleWindow); tray.on('double-click', toggleWindow); if (IS_DARWIN) { tray.setHighlightMode('never'); } resolve(win); }); } function startMenuBar(config: Config): Promise { log.debug('Setup a menubar window'); return new Promise(resolve => { const state = windowState({ defaultWidth: 350, defaultHeight: 420, }); const icon = trayIcon(config.icon_color); const mb = menubar({ icon, width: state.width, height: state.height, alwaysOnTop: IS_DEBUG || !!config.always_on_top, tooltip: 'Mstdn', useContentSize: true, autoHideMenuBar: true, show: false, showDockIcon: true, webPreferences: { nodeIntegration: false, sandbox: true, preload: PRELOAD_JS, partition: partitionForAccount(config.accounts[0]), }, }); mb.once('ready', () => mb.showWindow()); mb.once('after-create-window', () => { log.debug('Menubar application was launched'); if (config.hot_key) { globalShortcut.register(config.hot_key, () => { if (mb.window.isFocused()) { log.debug('Toggle window: shown -> hidden'); mb.hideWindow(); } else { log.debug('Toggle window: hidden -> shown'); mb.showWindow(); } }); log.debug('Hot key was set to:', config.hot_key); } if (IS_DEBUG) { mb.window.webContents.openDevTools({mode: 'detach'}); } mb.window.webContents.on('dom-ready', () => { log.debug('Send config to renderer procress'); mb.window.webContents.send('mstdn:config', config); }); state.manage(mb.window); resolve(mb.window); }); mb.once('after-close', () => { app.quit(); }); }); }