mirror of
https://github.com/rhysd/Mstdn.git
synced 2025-02-02 06:52:13 +01:00
Merge branch 'multi-account'
This commit is contained in:
commit
e76fdd25aa
61
main/account_switcher.ts
Normal file
61
main/account_switcher.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import {EventEmitter} from 'events';
|
||||||
|
import {Menu, MenuItem} 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(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.switchTo(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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('switch', account, this.current);
|
||||||
|
this.current = account;
|
||||||
|
}
|
||||||
|
}
|
209
main/app.ts
209
main/app.ts
@ -1,205 +1,52 @@
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import {app, BrowserWindow, globalShortcut, Tray, shell, dialog, Menu} from 'electron';
|
import {app, Menu, globalShortcut} from 'electron';
|
||||||
import windowState = require('electron-window-state');
|
|
||||||
import * as menubar from 'menubar';
|
|
||||||
import log from './log';
|
import log from './log';
|
||||||
import {Config, Account} from './config';
|
import {Config, Account} from './config';
|
||||||
|
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 IS_DARWIN = process.platform === 'darwin';
|
||||||
const APP_ICON = path.join(__dirname, '..', 'resources', 'icon', 'icon.png');
|
const APP_ICON = path.join(__dirname, '..', 'resources', 'icon', 'icon.png');
|
||||||
const PRELOAD_JS = path.join(__dirname, '..', 'renderer', 'preload.js');
|
|
||||||
|
|
||||||
export class App {
|
export class App {
|
||||||
private account: Account;
|
private switcher: AccountSwitcher;
|
||||||
|
|
||||||
constructor(private win: Electron.BrowserWindow, private config: Config) {
|
constructor(private win: Window, private config: Config) {
|
||||||
if (config.accounts.length === 0) {
|
if (config.accounts.length === 0) {
|
||||||
throw new Error('No account found. Please check the config.');
|
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(this.config.accounts);
|
||||||
|
this.switcher.on('switch', this.onAccountSwitch);
|
||||||
}
|
}
|
||||||
|
|
||||||
open() {
|
start() {
|
||||||
if (!IS_DARWIN) {
|
const a = this.switcher.current;
|
||||||
// Users can still access menu bar with pressing Alt key.
|
const url = `https://${a.host}${a.default_page}`;
|
||||||
this.win.setMenu(Menu.getApplicationMenu());
|
this.win.open(url);
|
||||||
|
log.debug('Application started', a, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.win.webContents.on('dom-ready', () => {
|
private onAccountSwitch = (next: Account) => {
|
||||||
this.win.show();
|
this.win.close();
|
||||||
});
|
if (this.config.hot_key) {
|
||||||
this.win.loadURL(`https://${this.account.host}${this.account.default_page}`);
|
log.debug('Disable global shortcut for switching account');
|
||||||
this.win.webContents.on('will-navigate', (e, url) => {
|
globalShortcut.unregister(this.config.hot_key);
|
||||||
if (!url.startsWith(`https://${this.account.host}`)) {
|
|
||||||
e.preventDefault();
|
|
||||||
shell.openExternal(url);
|
|
||||||
}
|
}
|
||||||
});
|
Window.create(next, this.config, this.win.menubar) .then(win => {
|
||||||
this.win.webContents.on('new-window', (e, url) => {
|
log.debug('Window was recreated again', next);
|
||||||
e.preventDefault();
|
this.win = win;
|
||||||
shell.openExternal(url);
|
this.start();
|
||||||
});
|
|
||||||
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) {
|
export default function startApp(config: Config) {
|
||||||
return (config.normal_window ? startNormalWindow : startMenuBar)(config)
|
const default_account = config.accounts[0];
|
||||||
|
return Window.create(default_account, config)
|
||||||
.then(win => new App(win, config));
|
.then(win => new App(win, config));
|
||||||
}
|
}
|
||||||
|
|
||||||
function startNormalWindow(config: Config): Promise<Electron.BrowserWindow> {
|
|
||||||
log.debug('Setup a normal window');
|
|
||||||
return new Promise<Electron.BrowserWindow>(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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
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');
|
|
||||||
app.dock.setIcon(APP_ICON);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(win);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function startMenuBar(config: Config): Promise<Electron.BrowserWindow> {
|
|
||||||
log.debug('Setup a menubar window');
|
|
||||||
return new Promise<Electron.BrowserWindow>(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',
|
|
||||||
autoHideMenuBar: true,
|
|
||||||
useContentSize: true,
|
|
||||||
show: false,
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: false,
|
|
||||||
sandbox: true,
|
|
||||||
preload: PRELOAD_JS,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
189
main/default_menu.ts
Normal file
189
main/default_menu.ts
Normal file
@ -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);
|
||||||
|
}
|
@ -3,6 +3,10 @@ import log from './log';
|
|||||||
import startApp from './app';
|
import startApp from './app';
|
||||||
import loadConfig from './config';
|
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<void>(resolve => app.once('ready', resolve));
|
const appReady = new Promise<void>(resolve => app.once('ready', resolve));
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason: string) => {
|
process.on('unhandledRejection', (reason: string) => {
|
||||||
@ -16,7 +20,7 @@ app.on('will-quit', () => {
|
|||||||
Promise.all([
|
Promise.all([
|
||||||
loadConfig(),
|
loadConfig(),
|
||||||
appReady
|
appReady
|
||||||
]).then(([config, _]) => startApp(config)).then(win => {
|
]).then(([config, _]) => startApp(config)).then(mstdn => {
|
||||||
win.open();
|
mstdn.start();
|
||||||
log.debug('Application launched!');
|
log.debug('Application launched!');
|
||||||
});
|
});
|
||||||
|
236
main/window.ts
Normal file
236
main/window.ts
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
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(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,
|
||||||
|
) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
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'],
|
||||||
|
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) {
|
||||||
|
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();
|
||||||
|
if (this.menubar) {
|
||||||
|
// Note:
|
||||||
|
// menubar.windowClear() won't be called because all listners was removed
|
||||||
|
delete this.menubar.window;
|
||||||
|
}
|
||||||
|
this.browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function trayIcon(color: string) {
|
||||||
|
return path.join(__dirname, '..', 'resources', 'icon', `tray-icon-${
|
||||||
|
color === 'white' ? 'white' : 'black'
|
||||||
|
}@2x.png`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startNormalWindow(account: Account, config: Config): Promise<Window> {
|
||||||
|
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,
|
||||||
|
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, account);
|
||||||
|
});
|
||||||
|
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, state, account, null));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startMenuBar(account: Account, config: Config, bar: Menubar.MenubarApp | null): Promise<Window> {
|
||||||
|
log.debug('Setup a menubar window');
|
||||||
|
return new Promise<Window>(resolve => {
|
||||||
|
const state = windowState({
|
||||||
|
defaultWidth: 350,
|
||||||
|
defaultHeight: 420,
|
||||||
|
});
|
||||||
|
const icon = trayIcon(config.icon_color);
|
||||||
|
const mb = bar || 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('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, account);
|
||||||
|
});
|
||||||
|
state.manage(mb.window);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
log.debug('New menubar instance was created:', account);
|
||||||
|
mb.once('ready', () => mb.showWindow());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
import * as Mousetrap from 'mousetrap';
|
import * as Mousetrap from 'mousetrap';
|
||||||
import {Config} from '../main/config';
|
import {Config, Account} from '../main/config';
|
||||||
import * as Ipc from './ipc';
|
import * as Ipc from './ipc';
|
||||||
import log from './log';
|
import log from './log';
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ function setupKeybinds(keybinds: {[key: string]: string}, host: string) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const func = ShortcutActions[action];
|
const func = ShortcutActions[action];
|
||||||
if (func === undefined) {
|
if (!func) {
|
||||||
log.error('Unknown shortcut action:', action);
|
log.error('Unknown shortcut action:', action);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -52,8 +52,10 @@ function setupKeybinds(keybinds: {[key: string]: string}, host: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ipc.on('mstdn:config', (config: Config) => {
|
let config: Config | null = null;
|
||||||
// TODO: Temporary. It should be fixed on supporting multi-account.
|
|
||||||
const host = config.accounts[0].host;
|
Ipc.on('mstdn:config', (c: Config, a: Account) => {
|
||||||
|
config = c;
|
||||||
|
const host = a.host;
|
||||||
setupKeybinds(config.keymaps, host);
|
setupKeybinds(config.keymaps, host);
|
||||||
});
|
});
|
||||||
|
@ -5,7 +5,7 @@ const ipc = electron.ipcRenderer;
|
|||||||
|
|
||||||
export function on(channel: IpcChannel, callback: (...args: any[]) => void) {
|
export function on(channel: IpcChannel, callback: (...args: any[]) => void) {
|
||||||
ipc.on(channel, (...args: any[]) => {
|
ipc.on(channel, (...args: any[]) => {
|
||||||
log.debug('IPC: Received from:', channel, args);
|
log.info('IPC: Received from:', channel, args);
|
||||||
callback(...args);
|
callback(...args);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
1
typings/ipc.d.ts
vendored
1
typings/ipc.d.ts
vendored
@ -1,3 +1,4 @@
|
|||||||
type IpcChannel
|
type IpcChannel
|
||||||
= 'mstdn:config'
|
= 'mstdn:config'
|
||||||
|
| 'mstdn:change-account'
|
||||||
;
|
;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user