2017-04-20 11:09:57 +09:00
|
|
|
import * as fs from 'fs';
|
2017-04-19 16:16:55 +09:00
|
|
|
import {app, BrowserWindow, shell, dialog, Menu} from 'electron';
|
2017-04-18 15:48:50 +09:00
|
|
|
import windowState = require('electron-window-state');
|
|
|
|
import * as menubar from 'menubar';
|
2017-04-23 23:04:03 +09:00
|
|
|
import {Config, Account, hostUrl} from './config';
|
2017-04-18 15:48:50 +09:00
|
|
|
import {partitionForAccount} from './account_switcher';
|
|
|
|
import log from './log';
|
2017-04-20 11:09:57 +09:00
|
|
|
import {IS_DEBUG, IS_DARWIN, IS_WINDOWS, IS_LINUX, APP_ICON, PRELOAD_JS, USER_CSS, trayIcon} from './common';
|
2017-04-20 00:06:23 +09:00
|
|
|
|
|
|
|
const ELECTRON_ISSUE_9230 = IS_WINDOWS || IS_LINUX;
|
2017-04-18 15:48:50 +09:00
|
|
|
|
|
|
|
export default class Window {
|
2017-04-18 20:29:07 +09:00
|
|
|
static create(account: Account, config: Config, mb: Menubar.MenubarApp | null = null) {
|
2017-04-20 23:39:04 +09:00
|
|
|
return (config.normal_window ? startNormalWindow(account, config) : startMenuBar(account, config, mb))
|
|
|
|
.then(win => {
|
|
|
|
win.browser.webContents.on('dom-ready', () => {
|
|
|
|
applyUserCss(win.browser, config);
|
|
|
|
win.browser.webContents.setZoomFactor(config.zoom_factor);
|
|
|
|
log.debug('Send config to renderer procress');
|
|
|
|
win.browser.webContents.send('mstdn:config', config, account);
|
|
|
|
});
|
|
|
|
return win;
|
|
|
|
});
|
2017-04-18 15:48:50 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
public browser: Electron.BrowserWindow,
|
2017-04-18 20:29:07 +09:00
|
|
|
public state: any /*XXX: ElectronWindowState.WindowState */,
|
2017-04-18 15:48:50 +09:00
|
|
|
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) => {
|
2017-04-23 23:04:03 +09:00
|
|
|
if (!url.startsWith(hostUrl(this.account))) {
|
2017-04-18 15:48:50 +09:00
|
|
|
e.preventDefault();
|
|
|
|
shell.openExternal(url);
|
|
|
|
}
|
2017-04-18 20:39:47 +09:00
|
|
|
log.debug('Opened URL with external browser (will-navigate)', url);
|
2017-04-18 15:48:50 +09:00
|
|
|
});
|
|
|
|
browser.webContents.on('new-window', (e, url) => {
|
2017-04-20 00:06:23 +09:00
|
|
|
if (ELECTRON_ISSUE_9230) {
|
2017-04-19 20:56:54 +09:00
|
|
|
// XXX:
|
2017-04-20 00:06:23 +09:00
|
|
|
// On Windows or Linux, rel="noopener" lets app crash on preventing the event.
|
|
|
|
// Issue: https://github.com/electron/electron/issues/9230
|
2017-04-19 20:56:54 +09:00
|
|
|
return;
|
|
|
|
}
|
2017-04-18 15:48:50 +09:00
|
|
|
e.preventDefault();
|
|
|
|
shell.openExternal(url);
|
2017-04-18 20:39:47 +09:00
|
|
|
log.debug('Opened URL with external browser (new-window)', url);
|
2017-04-18 15:48:50 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
browser.webContents.session.setPermissionRequestHandler((contents, permission, callback) => {
|
2017-04-19 16:42:59 +09:00
|
|
|
const url = contents.getURL();
|
|
|
|
const grantedByDefault =
|
2017-04-23 23:04:03 +09:00
|
|
|
url.startsWith(hostUrl(this.account)) &&
|
2017-04-19 16:42:59 +09:00
|
|
|
permission !== 'geolocation' &&
|
|
|
|
permission !== 'media';
|
|
|
|
|
|
|
|
if (grantedByDefault) {
|
2017-04-18 15:48:50 +09:00
|
|
|
// Granted
|
2017-04-18 20:39:47 +09:00
|
|
|
log.debug('Permission was granted', permission);
|
2017-04-18 15:48:50 +09:00
|
|
|
callback(true);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-04-18 20:39:47 +09:00
|
|
|
log.debug('Create dialog for user permission', permission);
|
2017-04-18 15:48:50 +09:00
|
|
|
dialog.showMessageBox({
|
|
|
|
type: 'question',
|
|
|
|
buttons: ['Accept', 'Reject'],
|
2017-04-19 16:42:59 +09:00
|
|
|
message: `Permission '${permission}' is requested by ${url}`,
|
2017-04-18 15:48:50 +09:00
|
|
|
detail: "Please choose one of 'Accept' or 'Reject'",
|
|
|
|
}, (buttonIndex: number) => {
|
|
|
|
const granted = buttonIndex === 0;
|
|
|
|
callback(granted);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
open(url: string) {
|
2017-04-18 20:39:47 +09:00
|
|
|
log.debug('Open URL:', url);
|
2017-04-20 23:39:04 +09:00
|
|
|
this.browser.webContents.once('did-get-redirect-request', (e: Event, _: string, newUrl: string) => {
|
2017-04-20 23:41:25 +09:00
|
|
|
if (url === newUrl) {
|
|
|
|
return;
|
|
|
|
}
|
2017-04-20 23:39:04 +09:00
|
|
|
log.debug('Redirecting to ' + newUrl + '. Will navigate to login page for user using single user mode');
|
|
|
|
e.preventDefault();
|
2017-04-23 23:04:03 +09:00
|
|
|
this.browser.loadURL(hostUrl(this.account) + '/auth/sign_in');
|
2017-04-20 23:39:04 +09:00
|
|
|
});
|
2017-04-18 15:48:50 +09:00
|
|
|
this.browser.loadURL(url);
|
|
|
|
}
|
|
|
|
|
|
|
|
close() {
|
2017-04-18 20:39:47 +09:00
|
|
|
log.debug('Closing window:', this.account);
|
2017-04-20 12:22:45 +09:00
|
|
|
this.state.saveState();
|
2017-04-18 20:29:07 +09:00
|
|
|
this.state.unmanage();
|
|
|
|
this.browser.webContents.removeAllListeners();
|
|
|
|
this.browser.removeAllListeners();
|
|
|
|
if (this.menubar) {
|
|
|
|
// Note:
|
2017-04-25 16:45:11 +09:00
|
|
|
// menubar.windowClear() won't be called because all listners were removed
|
2017-04-18 20:29:07 +09:00
|
|
|
delete this.menubar.window;
|
|
|
|
}
|
|
|
|
this.browser.close();
|
2017-04-18 15:48:50 +09:00
|
|
|
}
|
2017-04-25 14:58:37 +09:00
|
|
|
|
|
|
|
toggle() {
|
|
|
|
if (this.menubar) {
|
|
|
|
if (this.browser.isFocused()) {
|
|
|
|
log.debug('Toggle window: shown -> hidden');
|
|
|
|
this.menubar.hideWindow();
|
|
|
|
} else {
|
|
|
|
log.debug('Toggle window: hidden -> shown');
|
|
|
|
this.menubar.showWindow();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (this.browser.isFocused()) {
|
|
|
|
log.debug('Toggle window: shown -> hidden');
|
|
|
|
if (IS_DARWIN) {
|
|
|
|
app.hide();
|
2017-04-25 16:45:11 +09:00
|
|
|
} else if (IS_WINDOWS) {
|
|
|
|
this.browser.minimize();
|
2017-04-25 14:58:37 +09:00
|
|
|
} else {
|
|
|
|
this.browser.hide();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
log.debug('Toggle window: hidden -> shown');
|
|
|
|
this.browser.show();
|
|
|
|
if (IS_WINDOWS) {
|
|
|
|
this.browser.focus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-04-18 15:48:50 +09:00
|
|
|
}
|
|
|
|
|
2017-04-20 11:09:57 +09:00
|
|
|
function applyUserCss(win: Electron.BrowserWindow, config: Config) {
|
|
|
|
if (config.chromium_sandbox) {
|
|
|
|
log.debug('User CSS is disabled because Chromium sandbox is enabled');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
fs.readFile(USER_CSS, 'utf8', (err, css) => {
|
|
|
|
if (err) {
|
|
|
|
log.debug('Failed to load user.css: ', err.message);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
win.webContents.insertCSS(css);
|
|
|
|
log.debug('Applied user CSS:', USER_CSS);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-04-18 20:29:07 +09:00
|
|
|
function startNormalWindow(account: Account, config: Config): Promise<Window> {
|
2017-04-18 15:48:50 +09:00
|
|
|
log.debug('Setup a normal window');
|
|
|
|
return new Promise<Window>(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,
|
2017-04-20 11:09:57 +09:00
|
|
|
autoHideMenuBar: !!config.hide_menu,
|
2017-04-18 15:48:50 +09:00
|
|
|
webPreferences: {
|
|
|
|
nodeIntegration: false,
|
2017-04-25 01:27:07 +09:00
|
|
|
sandbox: sandboxFlag(config, account),
|
2017-04-18 15:48:50 +09:00
|
|
|
preload: PRELOAD_JS,
|
|
|
|
partition: partitionForAccount(account),
|
2017-04-20 23:39:04 +09:00
|
|
|
zoomFactor: config.zoom_factor,
|
2017-04-18 15:48:50 +09:00
|
|
|
},
|
|
|
|
});
|
|
|
|
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);
|
|
|
|
|
|
|
|
win.webContents.once('dom-ready', () => {
|
|
|
|
log.debug('Normal window application was launched');
|
|
|
|
if (IS_DEBUG) {
|
|
|
|
win.webContents.openDevTools({mode: 'detach'});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-04-18 20:29:07 +09:00
|
|
|
resolve(new Window(win, state, account, null));
|
2017-04-18 15:48:50 +09:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-04-25 01:27:07 +09:00
|
|
|
function sandboxFlag(config: Config, account: Account) {
|
|
|
|
// XXX:
|
|
|
|
// Electron has a bug that CSP prevents preload script from being loaded.
|
|
|
|
// mstdn.jp enables CSP. So currently we need to disable native sandbox to load preload script.
|
|
|
|
//
|
|
|
|
// Ref: https://github.com/electron/electron/issues/9276
|
|
|
|
//
|
|
|
|
const electronBugIssue9276 = account.host === 'mstdn.jp' || account.host === 'https://mstdn.jp';
|
|
|
|
if (electronBugIssue9276) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return !!config.chromium_sandbox;
|
|
|
|
}
|
|
|
|
|
2017-04-18 20:29:07 +09:00
|
|
|
function startMenuBar(account: Account, config: Config, bar: Menubar.MenubarApp | null): Promise<Window> {
|
2017-04-18 15:48:50 +09:00
|
|
|
log.debug('Setup a menubar window');
|
|
|
|
return new Promise<Window>(resolve => {
|
|
|
|
const state = windowState({
|
2017-04-20 14:00:57 +09:00
|
|
|
defaultWidth: 320,
|
2017-04-18 15:48:50 +09:00
|
|
|
defaultHeight: 420,
|
|
|
|
});
|
|
|
|
const icon = trayIcon(config.icon_color);
|
2017-04-18 20:29:07 +09:00
|
|
|
const mb = bar || menubar({
|
2017-04-18 15:48:50 +09:00
|
|
|
icon,
|
|
|
|
width: state.width,
|
|
|
|
height: state.height,
|
|
|
|
alwaysOnTop: IS_DEBUG || !!config.always_on_top,
|
|
|
|
tooltip: 'Mstdn',
|
2017-04-20 11:09:57 +09:00
|
|
|
autoHideMenuBar: !!config.hide_menu,
|
2017-04-18 15:48:50 +09:00
|
|
|
show: false,
|
|
|
|
showDockIcon: true,
|
|
|
|
webPreferences: {
|
|
|
|
nodeIntegration: false,
|
2017-04-25 01:27:07 +09:00
|
|
|
sandbox: sandboxFlag(config, account),
|
2017-04-18 15:48:50 +09:00
|
|
|
preload: PRELOAD_JS,
|
|
|
|
partition: partitionForAccount(account),
|
2017-04-20 23:39:04 +09:00
|
|
|
zoomFactor: config.zoom_factor,
|
2017-04-18 15:48:50 +09:00
|
|
|
},
|
|
|
|
});
|
|
|
|
mb.once('after-create-window', () => {
|
|
|
|
log.debug('Menubar application was launched');
|
|
|
|
if (IS_DEBUG) {
|
|
|
|
mb.window.webContents.openDevTools({mode: 'detach'});
|
|
|
|
}
|
|
|
|
state.manage(mb.window);
|
|
|
|
|
2017-04-18 20:29:07 +09:00
|
|
|
resolve(new Window(mb.window, state, account, mb));
|
2017-04-18 15:48:50 +09:00
|
|
|
});
|
|
|
|
mb.once('after-close', () => {
|
|
|
|
app.quit();
|
|
|
|
});
|
2017-04-18 20:29:07 +09:00
|
|
|
if (bar) {
|
2017-04-18 20:39:47 +09:00
|
|
|
log.debug('Recreate menubar window with different partition:', account);
|
2017-04-18 20:29:07 +09:00
|
|
|
const pref = mb.getOption('webPreferences');
|
|
|
|
pref.partition = partitionForAccount(account);
|
2017-04-25 23:33:32 +09:00
|
|
|
pref.sandbox = sandboxFlag(config, account);
|
2017-04-18 20:29:07 +09:00
|
|
|
mb.setOption('webPreferences', pref);
|
|
|
|
mb.showWindow();
|
|
|
|
} else {
|
2017-04-18 20:39:47 +09:00
|
|
|
log.debug('New menubar instance was created:', account);
|
2017-04-18 20:29:07 +09:00
|
|
|
mb.once('ready', () => mb.showWindow());
|
|
|
|
}
|
2017-04-18 15:48:50 +09:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|