diff --git a/src/background.ts b/src/background.ts index 8db9e8a3..5cf60c67 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,38 +1,23 @@ 'use strict' -import * as path from 'path' import { pick } from 'lodash' import { app, ipcMain, protocol, - shell, - BrowserWindow, - Menu, -} from 'electron' -// Electron types -import { - App, - BrowserWindowConstructorOptions, Event, - MenuItemConstructorOptions, } from 'electron' import ContextMenu from 'electron-context-menu' -import { - createProtocol, - installVueDevtools -} from 'vue-cli-plugin-electron-builder/lib' -// tslint:disable-next-line:no-var-requires -const localShortcut = require('electron-localshortcut') + +import Application from './main/Application' +import ApplicationMenu from "./main/ApplicationMenu"; export type PackageJson = typeof import('../package.json'); -import { bugs, homepage } from '../package.json' +import { homepage } from '../package.json' import TheDeskInfo from '../info.json' export type TheDeskInfoObject = typeof TheDeskInfo; -declare const __static: string; - ipcMain.on('thedesk-info', (event: Event) => { event.returnValue = Object.assign({ productName: app.getName(), @@ -59,275 +44,7 @@ if (isDevelopment) { protocol.registerStandardSchemes(['app'], { secure: true }) -interface CreateWindowOptions { - windowName: string - loadPath: string - windowOptions?: BrowserWindowConstructorOptions - singleton?: boolean - lastAction?: (win: BrowserWindow) => void - openDevTools?: boolean -} - -class Application { - public app: App; - public windows: { [key: string]: BrowserWindow } = {}; - - constructor(app: App) { - this.app = app; - ContextMenu() - this.app.on('window-all-closed', () => this.onWindowAllClosed()) - this.app.on('ready', () => this.onReady()); - this.app.on('activate', () => this.onActivated()); - } - - private onWindowAllClosed() { - if (process.platform !== 'darwin') { - this.app.quit() - } - } - - private async onReady() { - if (isDevelopment && !process.env.IS_TEST) { - // Install Vue Devtools - try { - await installVueDevtools() - } catch (e) { - //console.error('Vue Devtools failed to install:', e.toString()) - } - } - if (!process.env.WEBPACK_DEV_SERVER_URL) createProtocol('app') - this.openMainWindow() - } - - private onActivated() { - if (typeof this.windows.main === 'undefined') { - this.openMainWindow() - } - } - - public openMainWindow() { - const opts: CreateWindowOptions = { - windowName: 'main', - loadPath: 'index.html', - windowOptions: { - icon: path.join(__static, 'icon.png'), - width: 800, - height: 600, - autoHideMenuBar: true, - }, - singleton: true, - lastAction: (win) => { - localShortcut.register(win, 'F5', () => this.windows.main.reload()) - }, - openDevTools: !process.env.IS_TEST - } - this.createWindow(opts) - } - - public openAboutWindow() { - const opts: CreateWindowOptions = { - windowName: 'about', - loadPath: 'about.html', - windowOptions: { - width: 296, - height: 432, - resizable: false, - minimizable: false, - maximizable: false, - fullscreenable: false, - autoHideMenuBar: true, - titleBarStyle: 'hiddenInset', - }, - singleton: true, - lastAction: (win) => { - win.setMenuBarVisibility(false) - win.webContents.on('before-input-event', (event, input) => { - if (typeof this.windows.about !== 'undefined') - this.windows.about.webContents.setIgnoreMenuShortcuts((input.meta || input.control) && input.key !== "R" || input.key === "F5") - }) - localShortcut.register(win, 'Esc', () => this.windows.about.destroy()) - }, - openDevTools: !process.env.IS_TEST - } - this.createWindow(opts) - } - - private async createWindow(options: CreateWindowOptions) { - if (typeof this.windows[options.windowName] !== 'undefined') { - this.windows[options.windowName].show() - return - } - let win = new BrowserWindow(options.windowOptions) - win.hide() - win.webContents.on('did-finish-load', () => { - this.windows[options.windowName].show() - }) - - win.on('closed', () => { - delete this.windows[options.windowName] - }) - - let openUrl = (event: Event, url: string) => { - if (isDevelopment && url === process.env.WEBPACK_DEV_SERVER_URL + options.loadPath) { - return - } - event.preventDefault() - shell.openExternal(url, { - activate: false - }, (err) => { - //if (err) console.log(err) - }) - } - win.webContents.on('will-navigate', openUrl) - win.webContents.on('new-window', openUrl) - - if (process.env.WEBPACK_DEV_SERVER_URL) { - // `electron:serve`で起動した時の読み込み - win.loadURL(process.env.WEBPACK_DEV_SERVER_URL + options.loadPath) - } else { - // ビルドしたアプリでの読み込み - win.loadURL(`app://./${options.loadPath}`) - } - - if (isDevelopment && options.openDevTools) win.webContents.openDevTools() - - if (typeof options.lastAction === 'function') options.lastAction(win) - - this.windows[options.windowName] = win - } -} - -class ApplicationMenu { - private app: Application; - - // Mac only menu. prefix `macOnly`. First Item always separator - private macOnlyAppMenu: MenuItemConstructorOptions[] = [ - { type: 'separator' }, - { role: 'services' }, - { type: 'separator' }, - { role: 'hide' }, - { role: 'hideOthers' }, - { role: 'unhide' }, - ]; - private macOnlyEditMenu: MenuItemConstructorOptions[] = [ - { type: 'separator' }, - { - label: 'Speech', - submenu: [ - { role: 'startspeaking' }, - { role: 'stopspeaking' }, - ] - } - ]; - - private get aboutMenuItem(): MenuItemConstructorOptions { - return { - label: process.platform !== 'darwin' ? 'About' : `About ${this.app.app.getName()}`, - click: () => this.app.openAboutWindow(), - } - } - - constructor(app: Application) { - this.app = app; - } - - public setApplicationMenu() { - const menu = Menu.buildFromTemplate(this.buildTemplate(process.platform === 'darwin')) - Menu.setApplicationMenu(menu) - } - - private buildTemplate(isMac: boolean): MenuItemConstructorOptions[] { - return [ - this.AppMenu(isMac), - this.EditMenu(isMac), - this.ViewMenu(), - this.WindowMenu(isMac), - this.HelpMenu(), - ] - } - - private AppMenu(isMac: boolean): MenuItemConstructorOptions { - let appMenu: MenuItemConstructorOptions[] = [ - this.aboutMenuItem, - ...(isMac ? this.macOnlyAppMenu : []), - { type: 'separator' }, - { role: 'quit' }, - ] - return { - label: this.app.app.getName(), - submenu: appMenu, - } - } - - private EditMenu(isMac: boolean): MenuItemConstructorOptions { - let editMenu: MenuItemConstructorOptions[] = [ - { role: 'undo' }, - { role: 'redo' }, - { type: 'separator' }, - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, - { role: 'pasteandmatchstyle' }, - { role: 'delete' }, - { role: 'selectall' }, - ...(isMac ? this.macOnlyEditMenu : []), - ] - return { - label: 'Edit', - submenu: editMenu, - } - } - - private ViewMenu(): MenuItemConstructorOptions { - let viewMenu: MenuItemConstructorOptions[] = [ - { role: 'reload' }, - { role: 'forcereload' }, - { role: 'toggledevtools' }, - { type: 'separator' }, - { role: 'togglefullscreen' }, - ] - return { - label: 'View', - submenu: viewMenu - } - } - - private WindowMenu(isMac: boolean): MenuItemConstructorOptions { - let windowMenu: MenuItemConstructorOptions[] = isMac ? [ - { role: 'close' }, - { role: 'minimize' }, - { role: 'zoom' }, - { type: 'separator' }, - { role: 'front' }, - ] : [ - { role: 'minimize' }, - { role: 'close' }, - ] - return { - label: 'Window', - submenu: windowMenu, - } - } - - private HelpMenu(): MenuItemConstructorOptions { - let helpMenu: MenuItemConstructorOptions[] = [ - { - label: 'Report an issue', - click: () => shell.openExternal(`${bugs.url}/new`), - }, - { - label: 'Learn More', - click: () => shell.openExternal(TheDeskInfo.documentURL), - } - ] - return { - label: 'Help', - submenu: helpMenu, - } - } -} - -const TheDeskVueApp: Application = new Application(app) -const MainMenu: ApplicationMenu = new ApplicationMenu(TheDeskVueApp) -MainMenu.setApplicationMenu() +ContextMenu() +const TheDeskVueApp: Application = Application.shared +TheDeskVueApp.setApplicationMenu(ApplicationMenu.buildTemplate()) \ No newline at end of file diff --git a/src/main/Application.ts b/src/main/Application.ts new file mode 100644 index 00000000..db613a7b --- /dev/null +++ b/src/main/Application.ts @@ -0,0 +1,55 @@ +import { + app, + Menu, +} from 'electron' +import { + createProtocol, + installVueDevtools, +} from 'vue-cli-plugin-electron-builder/lib' +import Window from './Window' + +const isDevelopment = process.env.NODE_ENV !== 'production' + +export default class Application { + private static _instance: Application; + + public static get shared() { + // Do you need arguments? Make it a regular static method instead. + return this._instance || (this._instance = new this()); + } + + private constructor() { + app.on('window-all-closed', () => this.onWindowAllClosed()) + app.on('ready', () => this.onReady()); + app.on('activate', () => this.onActivated()); + } + + public setApplicationMenu(menu: Menu) { + Menu.setApplicationMenu(menu) + } + + private onWindowAllClosed() { + if (process.platform !== 'darwin') { + app.quit() + } + } + + private async onReady() { + if (isDevelopment && !process.env.IS_TEST) { + // Install Vue Devtools + try { + await installVueDevtools() + } catch (e) { + //console.error('Vue Devtools failed to install:', e.toString()) + } + } + if (!process.env.WEBPACK_DEV_SERVER_URL) createProtocol('app') + Window.Main() + } + + private onActivated() { + if (!Window.single.has('main')) { + Window.Main() + } + } +} \ No newline at end of file diff --git a/src/main/ApplicationMenu.ts b/src/main/ApplicationMenu.ts new file mode 100644 index 00000000..30da6f88 --- /dev/null +++ b/src/main/ApplicationMenu.ts @@ -0,0 +1,126 @@ +import { + app, + shell, + Menu, + MenuItemConstructorOptions +} from 'electron'; +import Window from "./Window"; +import { bugs } from '../../package.json'; +import { documentURL } from '../../info.json'; + +const isMac = process.platform === 'darwin' + +export default class ApplicationMenu { + // Mac only menu. prefix `macOnly`. First Item always separator + private static macOnlyAppMenu: MenuItemConstructorOptions[] = [ + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideOthers' }, + { role: 'unhide' }, + ]; + private static macOnlyEditMenu: MenuItemConstructorOptions[] = [ + { type: 'separator' }, + { + label: 'Speech', + submenu: [ + { role: 'startspeaking' }, + { role: 'stopspeaking' }, + ] + } + ]; + private static aboutMenuItem: MenuItemConstructorOptions = { + label: process.platform !== 'darwin' ? 'About' : `About ${app.getName()}`, + click: () => Window.About(), + } + + public static get menuConstruct(): MenuItemConstructorOptions[] { + return [ + this.AppMenu(isMac), + this.EditMenu(isMac), + this.ViewMenu(), + this.WindowMenu(isMac), + this.HelpMenu(), + ] + } + + public static buildTemplate(): Menu { + return Menu.buildFromTemplate(this.menuConstruct); + } + private static AppMenu(isMac: boolean): MenuItemConstructorOptions { + let appMenu: MenuItemConstructorOptions[] = [ + this.aboutMenuItem, + ...(isMac ? this.macOnlyAppMenu : []), + { type: 'separator' }, + { role: 'quit' }, + ]; + return { + label: app.getName(), + submenu: appMenu, + }; + } + private static EditMenu(isMac: boolean): MenuItemConstructorOptions { + let editMenu: MenuItemConstructorOptions[] = [ + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + { role: 'pasteandmatchstyle' }, + { role: 'delete' }, + { role: 'selectall' }, + ...(isMac ? this.macOnlyEditMenu : []), + ]; + return { + label: 'Edit', + submenu: editMenu, + }; + } + private static ViewMenu(): MenuItemConstructorOptions { + let viewMenu: MenuItemConstructorOptions[] = [ + { role: 'reload' }, + { role: 'forcereload' }, + { role: 'toggledevtools' }, + { type: 'separator' }, + { role: 'togglefullscreen' }, + ]; + return { + label: 'View', + submenu: viewMenu + }; + } + private static WindowMenu(isMac: boolean): MenuItemConstructorOptions { + let windowMenu: MenuItemConstructorOptions[] = isMac ? [ + { role: 'close' }, + { role: 'minimize' }, + { role: 'zoom' }, + { type: 'separator' }, + { role: 'front' }, + ] : [ + { role: 'minimize' }, + { role: 'close' }, + ]; + return { + label: 'Window', + submenu: windowMenu, + }; + } + private static HelpMenu(): MenuItemConstructorOptions { + let helpMenu: MenuItemConstructorOptions[] = [ + { + label: 'Report an issue', + click: () => shell.openExternal(`${bugs.url}/new`), + }, + { + label: 'Learn More', + click: () => shell.openExternal(documentURL), + } + ]; + return { + label: 'Help', + submenu: helpMenu, + }; + } +} diff --git a/src/main/Window.ts b/src/main/Window.ts new file mode 100644 index 00000000..b06f09f1 --- /dev/null +++ b/src/main/Window.ts @@ -0,0 +1,119 @@ +import { join } from 'path' + +import { + shell, + BrowserWindow, + BrowserWindowConstructorOptions, + Event, + Input, + app, +} from 'electron' +import { register as shortcutRegister } from 'electron-localshortcut' + +declare const __static: string; +const isDevelopment = process.env.NODE_ENV !== 'production' + +export interface CreateWindowOptions { + windowName: string + loadPath: string + windowOptions?: BrowserWindowConstructorOptions + singleton?: boolean + lastAction?: (win: BrowserWindow) => void + openDevTools?: boolean +} + +export default class Window { + public static single: Map = new Map() + public static list: BrowserWindow[] + + public static Main() { + const opts: CreateWindowOptions = { + windowName: 'main', + loadPath: 'index.html', + windowOptions: { + icon: join(__static, 'icon.png'), + width: 800, + height: 600, + autoHideMenuBar: true, + }, + singleton: true, + lastAction: (win) => { + shortcutRegister(win, 'F5', () => this.single.get('main')!.reload()) + }, + openDevTools: !process.env.IS_TEST + } + + this.createWindow(opts) + } + + public static About() { + const opts: CreateWindowOptions = { + windowName: 'about', + loadPath: 'about.html', + windowOptions: { + width: 296, + height: 432, + resizable: false, + minimizable: false, + maximizable: false, + fullscreenable: false, + autoHideMenuBar: true, + titleBarStyle: 'hiddenInset', + }, + singleton: true, + lastAction: (win) => { + win.setMenuBarVisibility(false) + win.webContents.on('before-input-event', (event: Event, input: Input) => { + this.single.get('about')!.webContents.setIgnoreMenuShortcuts((process.platform === 'darwin' ? input.meta : input.control) && input.key === "r") + }) + shortcutRegister(win, 'Esc', () => this.single.get('about')!.destroy()) + }, + openDevTools: !process.env.IS_TEST + } + this.createWindow(opts) + } + + public static async createWindow(options: CreateWindowOptions) { + if (options.singleton && this.single.has(options.windowName)) { + this.single.get(options.windowName)!.show() + return + } + let win = new BrowserWindow(options.windowOptions) + win.hide() + win.webContents.on('did-finish-load', () => { + win.show() + }) + + win.on('closed', () => { + this.single.delete(options.windowName) + }) + + let openUrl = (event: Event, url: string) => { + if (isDevelopment && url === process.env.WEBPACK_DEV_SERVER_URL + options.loadPath) { + return + } + event.preventDefault() + shell.openExternal(url, { + activate: false + }, (err) => { + //if (err) console.log(err) + }) + } + win.webContents.on('will-navigate', openUrl) + win.webContents.on('new-window', openUrl) + + if (process.env.WEBPACK_DEV_SERVER_URL) { + // `electron:serve`で起動した時の読み込み + win.loadURL(process.env.WEBPACK_DEV_SERVER_URL + options.loadPath) + } else { + // ビルドしたアプリでの読み込み + win.loadURL(`app://./${options.loadPath}`) + } + + if (isDevelopment && options.openDevTools) win.webContents.openDevTools() + + if (typeof options.lastAction === 'function') options.lastAction(win) + + this.single.set(options.windowName, win) + } +} \ No newline at end of file