From 5437c28b25c04290d72821ec5aa4cba89c640054 Mon Sep 17 00:00:00 2001 From: rhysd Date: Mon, 17 Apr 2017 18:40:55 +0900 Subject: [PATCH 1/5] implement account switcher --- main/account_switcher.ts | 66 ++++++++++++++ main/app.ts | 32 ++++--- main/default_menu.ts | 189 +++++++++++++++++++++++++++++++++++++++ renderer/index.ts | 16 +++- renderer/ipc.ts | 2 +- typings/ipc.d.ts | 1 + 6 files changed, 290 insertions(+), 16 deletions(-) create mode 100644 main/account_switcher.ts create mode 100644 main/default_menu.ts diff --git a/main/account_switcher.ts b/main/account_switcher.ts new file mode 100644 index 0000000..c09213d --- /dev/null +++ b/main/account_switcher.ts @@ -0,0 +1,66 @@ +import {EventEmitter} from 'events'; +import {Menu, MenuItem, session} from 'electron'; +import log from './log'; +import {Account} from './config'; + +export function partitionForAccount(account: Account) { + return `persist:mstdn:${account.name}:${account.host}`; +} + +export default class AccountSwitcher extends EventEmitter { + accounts: Account[]; + current: Account; + + constructor(private win: Electron.BrowserWindow, accounts: Account[]) { + super(); + const submenu = [] as Electron.MenuItemOptions[]; + + for (const account of accounts.filter(a => a.name !== '')) { + let name = account.name; + if (!name.startsWith('@')) { + name = '@' + name; + } + name += '@' + account.host; + submenu.push({ + label: name, + type: 'radio', + checked: false, + click: () => this.switchAccountTo(account), + }); + } + + this.accounts = accounts; + if (accounts.length === 0) { + return; + } + + submenu[0].checked = true; + this.current = accounts[0]; + + const item = new MenuItem({ + label: 'Accounts', + type: 'submenu', + submenu, + }); + + const menu = Menu.getApplicationMenu(); + // Insert item before 'Help' + menu.insert(menu.items.length - 1, item); + Menu.setApplicationMenu(menu); + } + + switchAccountTo(account: Account) { + if (this.current.name === account.name && this.current.host === account.host) { + log.debug('Current account is already @' + account.name); + return; + } + log.debug('Switch to account', account); + this.emit('will-switch', account); + + log.error('TODO: Switch partition'); + + this.win.webContents.send('mstdn:change-account', account); + this.emit('did-switch', account); + this.current = account; + } +} diff --git a/main/app.ts b/main/app.ts index a7683ed..2acd6be 100644 --- a/main/app.ts +++ b/main/app.ts @@ -3,7 +3,9 @@ import {app, BrowserWindow, globalShortcut, Tray, shell, dialog, Menu} from 'ele import windowState = require('electron-window-state'); import * as menubar from 'menubar'; import log from './log'; -import {Config, Account} from './config'; +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'; @@ -11,27 +13,26 @@ const APP_ICON = path.join(__dirname, '..', 'resources', 'icon', 'icon.png'); const PRELOAD_JS = path.join(__dirname, '..', 'renderer', 'preload.js'); export class App { - private account: Account; + 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.'); } - this.account = this.config.accounts[0]; - } + 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()); - open() { if (!IS_DARWIN) { // Users can still access menu bar with pressing Alt key. this.win.setMenu(Menu.getApplicationMenu()); } - this.win.webContents.on('dom-ready', () => { - this.win.show(); - }); - this.win.loadURL(`https://${this.account.host}${this.account.default_page}`); this.win.webContents.on('will-navigate', (e, url) => { - if (!url.startsWith(`https://${this.account.host}`)) { + if (!url.startsWith(`https://${this.switcher.current.host}`)) { e.preventDefault(); shell.openExternal(url); } @@ -40,6 +41,11 @@ export class App { 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 @@ -91,6 +97,7 @@ function startNormalWindow(config: Config): Promise { nodeIntegration: false, sandbox: true, preload: PRELOAD_JS, + partition: partitionForAccount(config.accounts[0]), }, }); win.once('ready-to-show', () => { @@ -142,7 +149,6 @@ function startNormalWindow(config: Config): Promise { tray.on('double-click', toggleWindow); if (IS_DARWIN) { tray.setHighlightMode('never'); - app.dock.setIcon(APP_ICON); } resolve(win); @@ -163,13 +169,15 @@ function startMenuBar(config: Config): Promise { height: state.height, alwaysOnTop: IS_DEBUG || !!config.always_on_top, tooltip: 'Mstdn', - autoHideMenuBar: true, 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()); diff --git a/main/default_menu.ts b/main/default_menu.ts new file mode 100644 index 0000000..fddf259 --- /dev/null +++ b/main/default_menu.ts @@ -0,0 +1,189 @@ +import * as path from 'path'; +import {Menu, shell, app} from 'electron'; + +export default function defaultMenu() { + const template: Electron.MenuItemOptions[] = [ + { + label: 'Edit', + submenu: [ + { + label: 'Edit Config', + click() { + shell.openItem(path.join(app.getPath('userData'), 'config.json')); + } + }, + { + type: 'separator' + }, + { + role: 'undo' + }, + { + role: 'redo' + }, + { + type: 'separator' + }, + { + role: 'cut' + }, + { + role: 'copy' + }, + { + role: 'paste' + }, + { + role: 'pasteandmatchstyle' + }, + { + role: 'delete' + }, + { + role: 'selectall' + } + ] + }, + { + label: 'View', + submenu: [ + { + role: 'reload' + }, + { + role: 'toggledevtools' + }, + { + type: 'separator' + }, + { + role: 'resetzoom' + }, + { + role: 'zoomin' + }, + { + role: 'zoomout' + }, + { + type: 'separator' + }, + { + role: 'togglefullscreen' + } + ] + }, + { + role: 'window', + submenu: [ + { + role: 'minimize' + }, + { + role: 'close' + } + ] + }, + { + role: 'help', + submenu: [ + { + label: 'Learn More', + click () { + shell.openExternal('https://github.com/rhysd/Mstdn#readme'); + } + }, + { + label: 'Search Issues', + click () { + shell.openExternal('https://github.com/rhysd/Mstdn/issues'); + } + } + ] + } + ]; + + if (process.platform === 'darwin') { + template.unshift({ + label: 'Mstdn', + submenu: [ + { + role: 'about' + }, + { + type: 'separator' + }, + { + role: 'services', + submenu: [] + }, + { + type: 'separator' + }, + { + role: 'hide' + }, + { + role: 'hideothers' + }, + { + role: 'unhide' + }, + { + type: 'separator' + }, + { + role: 'quit' + } + ] + }); + + (template[1].submenu as Electron.MenuItemOptions[]).push( + { + type: 'separator' + }, + { + label: 'Speech', + submenu: [ + { + role: 'startspeaking' + }, + { + role: 'stopspeaking' + } + ] + } + ); + + template[3].submenu = [ + { + role: 'close' + }, + { + role: 'minimize' + }, + { + role: 'zoom' + }, + { + type: 'separator' + }, + { + role: 'front' + } + ]; + } else { + template.unshift( + { + label: 'File', + submenu: [ + { + role: 'quit' + } + ] + } + ); + } + + return Menu.buildFromTemplate(template); +} diff --git a/renderer/index.ts b/renderer/index.ts index d9135a2..f4edf45 100644 --- a/renderer/index.ts +++ b/renderer/index.ts @@ -1,5 +1,5 @@ import * as Mousetrap from 'mousetrap'; -import {Config} from '../main/config'; +import {Config, Account} from '../main/config'; import * as Ipc from './ipc'; import log from './log'; @@ -52,8 +52,18 @@ function setupKeybinds(keybinds: {[key: string]: string}, host: string) { } } -Ipc.on('mstdn:config', (config: Config) => { - // TODO: Temporary. It should be fixed on supporting multi-account. +let config: Config | null = null; + +Ipc.on('mstdn:config', (c: Config) => { + config = c; const host = config.accounts[0].host; setupKeybinds(config.keymaps, host); }); + +Ipc.on('mstdn:change-account', (account: Account) => { + if (config === null) { + log.error('FATAL: config is null at receiving mstdn:change-account'); + return; + } + setupKeybinds(config.keymaps, account.host); +}) diff --git a/renderer/ipc.ts b/renderer/ipc.ts index a954c5c..b5389e8 100644 --- a/renderer/ipc.ts +++ b/renderer/ipc.ts @@ -5,7 +5,7 @@ const ipc = electron.ipcRenderer; export function on(channel: IpcChannel, callback: (...args: any[]) => void) { ipc.on(channel, (...args: any[]) => { - log.debug('IPC: Received from:', channel, args); + log.info('IPC: Received from:', channel, args); callback(...args); }); } diff --git a/typings/ipc.d.ts b/typings/ipc.d.ts index becc519..2312831 100644 --- a/typings/ipc.d.ts +++ b/typings/ipc.d.ts @@ -1,3 +1,4 @@ type IpcChannel = 'mstdn:config' + | 'mstdn:change-account' ; From d9bdb4571dd004ce4d70a5a4064699777482b790 Mon Sep 17 00:00:00 2001 From: rhysd Date: Tue, 18 Apr 2017 15:48:50 +0900 Subject: [PATCH 2/5] use App as mediator and separate browser window as Window class because window needs to be reopened for switching account --- main/account_switcher.ts | 2 +- main/app.ts | 195 ++--------------------------------- main/window.ts | 214 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 224 insertions(+), 187 deletions(-) create mode 100644 main/window.ts diff --git a/main/account_switcher.ts b/main/account_switcher.ts index c09213d..a4ac1aa 100644 --- a/main/account_switcher.ts +++ b/main/account_switcher.ts @@ -1,5 +1,5 @@ import {EventEmitter} from 'events'; -import {Menu, MenuItem, session} from 'electron'; +import {Menu, MenuItem} from 'electron'; import log from './log'; import {Account} from './config'; diff --git a/main/app.ts b/main/app.ts index 2acd6be..10dcd97 100644 --- a/main/app.ts +++ b/main/app.ts @@ -1,21 +1,18 @@ 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 {app, Menu} from 'electron'; import log from './log'; import {Config} from './config'; -import AccountSwitcher, {partitionForAccount} from './account_switcher'; +import AccountSwitcher from './account_switcher'; import defaultMenu from './default_menu'; +import Window from './window'; -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) { + constructor(private win: Window, private config: Config) { if (config.accounts.length === 0) { throw new Error('No account found. Please check the config.'); } @@ -23,191 +20,17 @@ export class App { app.dock.setIcon(APP_ICON); } Menu.setApplicationMenu(defaultMenu()); - this.switcher = new AccountSwitcher(win, this.config.accounts); + this.switcher = new AccountSwitcher(win.browser, 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); - }); - }); + const url = `https://${this.switcher.current.host}${this.switcher.current.default_page}`; + this.win.open(url); + log.debug('Open URL: ', url); } } -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(); - }); - }); + return Window.create(config).then(win => new App(win, config)); } diff --git a/main/window.ts b/main/window.ts new file mode 100644 index 0000000..e1eb65c --- /dev/null +++ b/main/window.ts @@ -0,0 +1,214 @@ +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 {Config, Account} from './config'; +import {partitionForAccount} from './account_switcher'; +import log from './log'; + +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 default class Window { + static create(config: Config) { + return (config.normal_window ? startNormalWindow : startMenuBar)(config); + } + + constructor( + public browser: Electron.BrowserWindow, + public account: Account, + public menubar: Menubar.MenubarApp | null, + ) { + if (!IS_DARWIN) { + // Users can still access menu bar with pressing Alt key. + browser.setMenu(Menu.getApplicationMenu()); + } + + browser.webContents.on('will-navigate', (e, url) => { + if (!url.startsWith(`https://${this.account.host}`)) { + e.preventDefault(); + shell.openExternal(url); + } + }); + browser.webContents.on('new-window', (e, url) => { + e.preventDefault(); + shell.openExternal(url); + }); + + browser.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); + }); + }); + } + + open(url: string) { + this.browser.loadURL(url); + } + + isMenubar() { + return this.browser !== null; + } + + close() { + log.error('TODO: close window'); + } +} + +function trayIcon(color: string) { + return path.join(__dirname, '..', 'resources', 'icon', `tray-icon-${ + color === 'white' ? 'white' : 'black' + }@2x.png`); +} + +function startNormalWindow(config: Config): Promise { + log.debug('Setup a normal window'); + return new Promise(resolve => { + const state = windowState({ + defaultWidth: 600, + defaultHeight: 800, + }); + const account = config.accounts[0]; + 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(account), + }, + }); + 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(new Window(win, account, null)); + }); +} + +function startMenuBar(config: Config): Promise { + log.debug('Setup a menubar window'); + return new Promise(resolve => { + const state = windowState({ + defaultWidth: 350, + defaultHeight: 420, + }); + const account = config.accounts[0]; + 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(account), + }, + }); + 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(new Window(mb.window, account, mb)); + }); + mb.once('after-close', () => { + app.quit(); + }); + }); +} + From 8395159007910d7626cfe8c05479dfa156e59963 Mon Sep 17 00:00:00 2001 From: rhysd Date: Tue, 18 Apr 2017 15:56:46 +0900 Subject: [PATCH 3/5] tslint --- main/default_menu.ts | 4 ++-- main/window.ts | 2 +- renderer/index.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/main/default_menu.ts b/main/default_menu.ts index fddf259..6fbcbcd 100644 --- a/main/default_menu.ts +++ b/main/default_menu.ts @@ -89,13 +89,13 @@ export default function defaultMenu() { submenu: [ { label: 'Learn More', - click () { + click() { shell.openExternal('https://github.com/rhysd/Mstdn#readme'); } }, { label: 'Search Issues', - click () { + click() { shell.openExternal('https://github.com/rhysd/Mstdn/issues'); } } diff --git a/main/window.ts b/main/window.ts index e1eb65c..19d4fd6 100644 --- a/main/window.ts +++ b/main/window.ts @@ -61,7 +61,7 @@ export default class Window { } isMenubar() { - return this.browser !== null; + return this.menubar !== null; } close() { diff --git a/renderer/index.ts b/renderer/index.ts index f4edf45..0039a04 100644 --- a/renderer/index.ts +++ b/renderer/index.ts @@ -39,7 +39,7 @@ function setupKeybinds(keybinds: {[key: string]: string}, host: string) { }); } else { const func = ShortcutActions[action]; - if (func === undefined) { + if (!func) { log.error('Unknown shortcut action:', action); continue; } @@ -66,4 +66,4 @@ Ipc.on('mstdn:change-account', (account: Account) => { return; } setupKeybinds(config.keymaps, account.host); -}) +}); From bf680a4b89b0dcd9d96ce7590122142d453441ed Mon Sep 17 00:00:00 2001 From: rhysd Date: Tue, 18 Apr 2017 20:29:07 +0900 Subject: [PATCH 4/5] recreate browser window to switch session partition --- main/account_switcher.ts | 13 ++++------- main/app.ts | 25 +++++++++++++++----- main/index.ts | 8 +++++-- main/window.ts | 49 ++++++++++++++++++++++++++-------------- renderer/index.ts | 12 ++-------- 5 files changed, 63 insertions(+), 44 deletions(-) diff --git a/main/account_switcher.ts b/main/account_switcher.ts index a4ac1aa..f3f62e6 100644 --- a/main/account_switcher.ts +++ b/main/account_switcher.ts @@ -11,7 +11,7 @@ export default class AccountSwitcher extends EventEmitter { accounts: Account[]; current: Account; - constructor(private win: Electron.BrowserWindow, accounts: Account[]) { + constructor(accounts: Account[]) { super(); const submenu = [] as Electron.MenuItemOptions[]; @@ -25,7 +25,7 @@ export default class AccountSwitcher extends EventEmitter { label: name, type: 'radio', checked: false, - click: () => this.switchAccountTo(account), + click: () => this.switchTo(account), }); } @@ -49,18 +49,13 @@ export default class AccountSwitcher extends EventEmitter { Menu.setApplicationMenu(menu); } - switchAccountTo(account: Account) { + switchTo(account: Account) { if (this.current.name === account.name && this.current.host === account.host) { log.debug('Current account is already @' + account.name); return; } log.debug('Switch to account', account); - this.emit('will-switch', account); - - log.error('TODO: Switch partition'); - - this.win.webContents.send('mstdn:change-account', account); - this.emit('did-switch', account); + this.emit('switch', account, this.current); this.current = account; } } diff --git a/main/app.ts b/main/app.ts index 10dcd97..4e1bbb2 100644 --- a/main/app.ts +++ b/main/app.ts @@ -1,7 +1,7 @@ import * as path from 'path'; -import {app, Menu} from 'electron'; +import {app, Menu, globalShortcut} from 'electron'; import log from './log'; -import {Config} from './config'; +import {Config, Account} from './config'; import AccountSwitcher from './account_switcher'; import defaultMenu from './default_menu'; import Window from './window'; @@ -20,17 +20,30 @@ export class App { app.dock.setIcon(APP_ICON); } Menu.setApplicationMenu(defaultMenu()); - this.switcher = new AccountSwitcher(win.browser, this.config.accounts); - this.switcher.on('did-switch', () => this.open()); + this.switcher = new AccountSwitcher(this.config.accounts); + this.switcher.on('switch', this.onAccountSwitch); } - open() { + start() { const url = `https://${this.switcher.current.host}${this.switcher.current.default_page}`; this.win.open(url); log.debug('Open URL: ', url); } + + private onAccountSwitch = (next: Account) => { + this.win.close(); + if (this.config.hot_key) { + globalShortcut.unregister(this.config.hot_key); + } + Window.create(next, this.config, this.win.menubar) .then(win => { + this.win = win; + this.start(); + }); + } } export default function startApp(config: Config) { - return Window.create(config).then(win => new App(win, config)); + const default_account = config.accounts[0]; + return Window.create(default_account, config) + .then(win => new App(win, config)); } diff --git a/main/index.ts b/main/index.ts index a4d5a70..f1da477 100644 --- a/main/index.ts +++ b/main/index.ts @@ -3,6 +3,10 @@ import log from './log'; import startApp from './app'; import loadConfig from './config'; +// default_app sets app.on('all-window-closed', () => app.quit()) before +// loading this application. We need to disable the callback. +app.removeAllListeners(); + const appReady = new Promise(resolve => app.once('ready', resolve)); process.on('unhandledRejection', (reason: string) => { @@ -16,7 +20,7 @@ app.on('will-quit', () => { Promise.all([ loadConfig(), appReady -]).then(([config, _]) => startApp(config)).then(win => { - win.open(); +]).then(([config, _]) => startApp(config)).then(mstdn => { + mstdn.start(); log.debug('Application launched!'); }); diff --git a/main/window.ts b/main/window.ts index 19d4fd6..8fafa09 100644 --- a/main/window.ts +++ b/main/window.ts @@ -12,12 +12,17 @@ const APP_ICON = path.join(__dirname, '..', 'resources', 'icon', 'icon.png'); const PRELOAD_JS = path.join(__dirname, '..', 'renderer', 'preload.js'); export default class Window { - static create(config: Config) { - return (config.normal_window ? startNormalWindow : startMenuBar)(config); + static create(account: Account, config: Config, mb: Menubar.MenubarApp | null = null) { + if (config.normal_window) { + return startNormalWindow(account, config); + } else { + return startMenuBar(account, config, mb); + } } constructor( public browser: Electron.BrowserWindow, + public state: any /*XXX: ElectronWindowState.WindowState */, public account: Account, public menubar: Menubar.MenubarApp | null, ) { @@ -60,12 +65,16 @@ export default class Window { this.browser.loadURL(url); } - isMenubar() { - return this.menubar !== null; - } - close() { - log.error('TODO: close window'); + this.state.unmanage(); + this.browser.webContents.removeAllListeners(); + this.browser.removeAllListeners(); + if (this.menubar) { + // Note: + // menubar.windowClear() won't be called because all listners was removed + delete this.menubar.window; + } + this.browser.close(); } } @@ -75,14 +84,13 @@ function trayIcon(color: string) { }@2x.png`); } -function startNormalWindow(config: Config): Promise { +function startNormalWindow(account: Account, config: Config): Promise { log.debug('Setup a normal window'); return new Promise(resolve => { const state = windowState({ defaultWidth: 600, defaultHeight: 800, }); - const account = config.accounts[0]; const win = new BrowserWindow({ width: state.width, height: state.height, @@ -129,7 +137,7 @@ function startNormalWindow(config: Config): Promise { win.webContents.on('dom-ready', () => { log.debug('Send config to renderer procress'); - win.webContents.send('mstdn:config', config); + win.webContents.send('mstdn:config', config, account); }); win.webContents.once('dom-ready', () => { log.debug('Normal window application was launched'); @@ -150,20 +158,19 @@ function startNormalWindow(config: Config): Promise { tray.setHighlightMode('never'); } - resolve(new Window(win, account, null)); + resolve(new Window(win, state, account, null)); }); } -function startMenuBar(config: Config): Promise { +function startMenuBar(account: Account, config: Config, bar: Menubar.MenubarApp | null): Promise { log.debug('Setup a menubar window'); return new Promise(resolve => { const state = windowState({ defaultWidth: 350, defaultHeight: 420, }); - const account = config.accounts[0]; const icon = trayIcon(config.icon_color); - const mb = menubar({ + const mb = bar || menubar({ icon, width: state.width, height: state.height, @@ -180,7 +187,6 @@ function startMenuBar(config: Config): Promise { partition: partitionForAccount(account), }, }); - mb.once('ready', () => mb.showWindow()); mb.once('after-create-window', () => { log.debug('Menubar application was launched'); if (config.hot_key) { @@ -200,15 +206,24 @@ function startMenuBar(config: Config): Promise { } mb.window.webContents.on('dom-ready', () => { log.debug('Send config to renderer procress'); - mb.window.webContents.send('mstdn:config', config); + mb.window.webContents.send('mstdn:config', config, account); }); state.manage(mb.window); - resolve(new Window(mb.window, account, mb)); + resolve(new Window(mb.window, state, account, mb)); }); mb.once('after-close', () => { app.quit(); }); + if (bar) { + log.debug('recreate menubar window with different partition:', account); + const pref = mb.getOption('webPreferences'); + pref.partition = partitionForAccount(account); + mb.setOption('webPreferences', pref); + mb.showWindow(); + } else { + mb.once('ready', () => mb.showWindow()); + } }); } diff --git a/renderer/index.ts b/renderer/index.ts index 0039a04..f78c6d2 100644 --- a/renderer/index.ts +++ b/renderer/index.ts @@ -54,16 +54,8 @@ function setupKeybinds(keybinds: {[key: string]: string}, host: string) { let config: Config | null = null; -Ipc.on('mstdn:config', (c: Config) => { +Ipc.on('mstdn:config', (c: Config, a: Account) => { config = c; - const host = config.accounts[0].host; + const host = a.host; setupKeybinds(config.keymaps, host); }); - -Ipc.on('mstdn:change-account', (account: Account) => { - if (config === null) { - log.error('FATAL: config is null at receiving mstdn:change-account'); - return; - } - setupKeybinds(config.keymaps, account.host); -}); From 07652e1ad4910ed1677d230f437056bbcac63166 Mon Sep 17 00:00:00 2001 From: rhysd Date: Tue, 18 Apr 2017 20:39:47 +0900 Subject: [PATCH 5/5] more logs --- main/app.ts | 7 +++++-- main/window.ts | 9 ++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/main/app.ts b/main/app.ts index 4e1bbb2..c413cb5 100644 --- a/main/app.ts +++ b/main/app.ts @@ -25,17 +25,20 @@ export class App { } start() { - const url = `https://${this.switcher.current.host}${this.switcher.current.default_page}`; + const a = this.switcher.current; + const url = `https://${a.host}${a.default_page}`; this.win.open(url); - log.debug('Open URL: ', url); + log.debug('Application started', a, url); } private onAccountSwitch = (next: Account) => { this.win.close(); if (this.config.hot_key) { + log.debug('Disable global shortcut for switching account'); globalShortcut.unregister(this.config.hot_key); } Window.create(next, this.config, this.win.menubar) .then(win => { + log.debug('Window was recreated again', next); this.win = win; this.start(); }); diff --git a/main/window.ts b/main/window.ts index 8fafa09..0e9178b 100644 --- a/main/window.ts +++ b/main/window.ts @@ -36,19 +36,23 @@ export default class Window { e.preventDefault(); shell.openExternal(url); } + log.debug('Opened URL with external browser (will-navigate)', url); }); browser.webContents.on('new-window', (e, url) => { e.preventDefault(); shell.openExternal(url); + log.debug('Opened URL with external browser (new-window)', url); }); browser.webContents.session.setPermissionRequestHandler((contents, permission, callback) => { if (permission !== 'geolocation' && permission !== 'media') { // Granted + log.debug('Permission was granted', permission); callback(true); return; } + log.debug('Create dialog for user permission', permission); dialog.showMessageBox({ type: 'question', buttons: ['Accept', 'Reject'], @@ -62,10 +66,12 @@ export default class Window { } open(url: string) { + log.debug('Open URL:', url); this.browser.loadURL(url); } close() { + log.debug('Closing window:', this.account); this.state.unmanage(); this.browser.webContents.removeAllListeners(); this.browser.removeAllListeners(); @@ -216,12 +222,13 @@ function startMenuBar(account: Account, config: Config, bar: Menubar.MenubarApp app.quit(); }); if (bar) { - log.debug('recreate menubar window with different partition:', account); + log.debug('Recreate menubar window with different partition:', account); const pref = mb.getOption('webPreferences'); pref.partition = partitionForAccount(account); mb.setOption('webPreferences', pref); mb.showWindow(); } else { + log.debug('New menubar instance was created:', account); mb.once('ready', () => mb.showWindow()); } });