From 869f2fd54ca82825f4a893a995a8b8987939221a Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Fri, 22 May 2020 10:25:19 +0200 Subject: [PATCH] Refactor main.ts --- src/Application.ts | 79 ++++++++ src/Meta.ts | 25 ++- src/Window.ts | 78 ++++++++ src/main.ts | 288 +-------------------------- src/windows/MainWindow.ts | 149 ++++++++++++++ src/windows/ServiceSettingsWindow.ts | 58 ++++++ src/windows/SettingsWindow.ts | 50 +++++ 7 files changed, 442 insertions(+), 285 deletions(-) create mode 100644 src/Application.ts create mode 100644 src/Window.ts create mode 100644 src/windows/MainWindow.ts create mode 100644 src/windows/ServiceSettingsWindow.ts create mode 100644 src/windows/SettingsWindow.ts diff --git a/src/Application.ts b/src/Application.ts new file mode 100644 index 0000000..eb05399 --- /dev/null +++ b/src/Application.ts @@ -0,0 +1,79 @@ +import {app, Menu, shell, Tray} from "electron"; +import Meta from "./Meta"; +import Config from "./Config"; +import Updater from "./Updater"; +import MainWindow from "./windows/MainWindow"; + +export default class Application { + private readonly devMode: boolean; + private readonly config: Config; + private readonly updater: Updater; + private readonly mainWindow: MainWindow; + private tray?: Tray; + + constructor(devMode: boolean) { + this.devMode = devMode; + this.config = new Config(); + this.updater = new Updater(this.config); + this.mainWindow = new MainWindow(this); + } + + public async start(): Promise { + console.log('Starting app'); + + this.setupSystemTray(); + this.mainWindow.setup(); + this.setupElectronTweaks(); + + // Check for updates + this.updater.checkAndPromptForUpdates(this.mainWindow.getWindow()).then(() => { + console.log('Update check successful.'); + }).catch(console.error); + + console.log('App started'); + } + + public async stop(): Promise { + this.mainWindow.teardown(); + } + + public getConfig(): Config { + return this.config; + } + + public getUpdater(): Updater { + return this.updater; + } + + public isDevMode(): boolean { + return this.devMode; + } + + private setupElectronTweaks() { + // Open external links in default OS browser + app.on('web-contents-created', (e, contents) => { + if (contents.getType() === 'webview') { + console.log('Setting external links to open in default OS browser'); + contents.on('new-window', (e, url) => { + e.preventDefault(); + if (url.startsWith('https://')) { + shell.openExternal(url).catch(console.error); + } + }); + } + }); + } + + private setupSystemTray() { + console.log('Loading system Tray'); + this.tray = new Tray(Meta.ICON_PATH); + this.tray.setToolTip('Tabs'); + this.tray.setContextMenu(Menu.buildFromTemplate([ + {label: 'Tabs', enabled: false}, + {label: 'Open Tabs', click: () => this.mainWindow.getWindow().show()}, + {type: 'separator'}, + {label: 'Quit', role: 'quit'} + ])); + this.tray.on('click', () => this.mainWindow.toggle()); + } +} \ No newline at end of file diff --git a/src/Meta.ts b/src/Meta.ts index 3f0a83a..570f8b6 100644 --- a/src/Meta.ts +++ b/src/Meta.ts @@ -1,7 +1,18 @@ import Service from "./Service"; +import path from "path"; +import fs from "fs"; export default class Meta { public static readonly title = 'Tabs'; + + // Paths + public static readonly RESOURCES_PATH = path.resolve(__dirname, '../resources'); + public static readonly ICON_PATH = path.resolve(Meta.RESOURCES_PATH, 'logo.png'); + + // Icons + public static readonly BRAND_ICONS = Meta.listIcons('brands'); + public static readonly SOLID_ICONS = Meta.listIcons('solid'); + private static devMode?: boolean; public static isDevMode() { @@ -20,4 +31,16 @@ export default class Meta { } return this.title + ' - ' + service.name + suffix; } -} \ No newline at end of file + + private static listIcons(set: string) { + console.log('Loading icon set', set); + const directory = path.resolve(Meta.RESOURCES_PATH, 'icons/' + set); + const icons: { name: string; faIcon: string }[] = []; + const dir = set === 'brands' ? 'fab' : 'fas'; + fs.readdirSync(directory).forEach(i => icons.push({ + name: i.split('.svg')[0], + faIcon: dir + ' fa-' + i.split('.svg')[0], + })); + return icons; + } +} diff --git a/src/Window.ts b/src/Window.ts new file mode 100644 index 0000000..44e721c --- /dev/null +++ b/src/Window.ts @@ -0,0 +1,78 @@ +import {BrowserWindow, BrowserWindowConstructorOptions, ipcMain, IpcMainEvent} from "electron"; +import Application from "./Application"; +import Config from "./Config"; + +export default abstract class Window { + private readonly listeners: { [channel: string]: ((event: IpcMainEvent, ...args: any[]) => void)[] } = {}; + private readonly onCloseListeners: (() => void)[] = []; + + protected readonly application: Application; + protected readonly config: Config; + protected readonly parent?: Window; + protected window?: BrowserWindow; + + protected constructor(application: Application, parent?: Window) { + this.application = application; + this.parent = parent; + this.config = this.application.getConfig(); + } + + + public setup(options: BrowserWindowConstructorOptions) { + console.log('Creating window', this.constructor.name); + + if (this.parent) { + options.parent = this.parent.getWindow(); + } + + this.window = new BrowserWindow(options); + this.window.on('close', () => { + this.teardown(); + this.window = undefined; + }); + } + + public teardown() { + console.log('Tearing down window', this.constructor.name); + + for (const listener of this.onCloseListeners) { + listener(); + } + + for (const channel in this.listeners) { + for (const listener of this.listeners[channel]) { + ipcMain.removeListener(channel, listener); + } + } + + this.window = undefined; + } + + protected onIpc(channel: string, listener: (event: IpcMainEvent, ...args: any[]) => void): this { + ipcMain.on(channel, listener); + if (!this.listeners[channel]) this.listeners[channel] = []; + this.listeners[channel].push(listener); + return this; + } + + public onClose(listener: () => void) { + this.onCloseListeners.push(listener); + } + + public toggle() { + if (this.window) { + if (!this.window.isFocused()) { + console.log('Showing window', this.constructor.name); + this.window.show(); + } else { + console.log('Hiding window', this.constructor.name); + this.window.hide(); + } + } + } + + public getWindow(): BrowserWindow { + if (!this.window) throw Error('Window not initialized.'); + return this.window; + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index b82e435..708b699 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,296 +1,16 @@ -import fs from "fs"; -import path from "path"; import SingleInstance from "single-instance"; -import {app, BrowserWindow, ipcMain, Menu, shell, Tray} from "electron"; +import {app} from "electron"; import Meta from "./Meta"; -import Config from "./Config"; -import Service from "./Service"; -import Updater from "./Updater"; -import Event = Electron.Event; - -const resourcesDir = path.resolve(__dirname, '../resources'); -const iconPath = path.resolve(resourcesDir, 'logo.png'); - -const config = new Config(); -const updater = new Updater(config); - -const devMode = Meta.isDevMode(); - -// Load icons -const brandIcons = listIcons('brands'); -const solidIcons = listIcons('solid'); - -let selectedService = 0; - -let tray: Tray; -let window: BrowserWindow | null; -let serviceSettingsWindow: BrowserWindow | null, settingsWindow: BrowserWindow | null; - -function toggleMainWindow() { - if (window != null) { - if (!window.isFocused()) { - console.log('Showing main window'); - window.show(); - } else { - console.log('Hiding main window'); - window.hide(); - } - } -} - -async function createWindow() { - // Check for updates - updater.checkAndPromptForUpdates(window!).then(() => { - console.log('Update check successful.'); - }).catch(console.error); - - // System tray - console.log('Loading system Tray'); - tray = new Tray(iconPath); - tray.setToolTip('Tabs'); - tray.setContextMenu(Menu.buildFromTemplate([ - {label: 'Tabs', enabled: false}, - {label: 'Open Tabs', click: () => window!.show()}, - {type: 'separator'}, - {label: 'Quit', role: 'quit'} - ])); - tray.on('click', () => toggleMainWindow()); - - // Create the browser window. - console.log('Creating main window'); - window = new BrowserWindow({ - webPreferences: { - nodeIntegration: true, - enableRemoteModule: true, - webviewTag: true, - }, - autoHideMenuBar: true, - icon: iconPath, - title: Meta.title, - }); - window.maximize(); - window.on('closed', () => { - window = null; - }); - - if (devMode) { - window.webContents.openDevTools({ - mode: 'right' - }); - } - - // Open external links in default OS browser - app.on('web-contents-created', (e, contents) => { - if (contents.getType() === 'webview') { - console.log('Setting external links to open in default OS browser'); - contents.on('new-window', (e, url) => { - e.preventDefault(); - if (url.startsWith('https://')) { - shell.openExternal(url); - } - }); - } - }); - - // Sync data - window.webContents.on('dom-ready', sendData); - - // Load navigation view - window.loadFile(path.resolve(resourcesDir, 'index.html')) - .catch(console.error); - - // Load active service - ipcMain.on('setActiveService', (event, index) => { - setActiveService(index); - }); - - // Set a service's favicon - ipcMain.on('setServiceFavicon', (event, index, favicon) => { - console.log('Setting service', index, 'favicon', favicon); - config.services[index].favicon = favicon; - config.save(); - }); - - // Open add service window - ipcMain.on('openServiceSettings', (e, serviceId) => { - if (!serviceSettingsWindow) { - console.log('Opening service settings', serviceId); - serviceSettingsWindow = new BrowserWindow({ - webPreferences: { - nodeIntegration: true, - enableRemoteModule: true, - webviewTag: true, - }, - parent: window!, - modal: true, - autoHideMenuBar: true, - height: 850, - }); - serviceSettingsWindow.on('close', () => { - serviceSettingsWindow = null; - }); - if (devMode) { - serviceSettingsWindow.webContents.openDevTools({ - mode: 'right' - }); - } - let syncListener: () => void; - ipcMain.on('sync-settings', syncListener = () => { - serviceSettingsWindow!.webContents.send('syncIcons', brandIcons, solidIcons); - serviceSettingsWindow!.webContents.send('loadService', serviceId, config.services[serviceId]); - }); - serviceSettingsWindow.on('close', () => { - ipcMain.removeListener('sync-settings', syncListener); - }); - serviceSettingsWindow.loadFile(path.resolve(resourcesDir, 'service-settings.html')) - .catch(console.error); - } - }); - - ipcMain.on('saveService', (e, id, data) => { - console.log('Saving service', id, data); - const newService = new Service(data); - if (typeof id === 'number') { - config.services[id] = newService; - } else { - config.services.push(newService); - id = config.services.indexOf(newService); - } - config.save(); - - window!.webContents.send('updateService', id, newService); - }); - - ipcMain.on('deleteService', (e, id) => { - console.log('Deleting service', id); - delete config.services[id]; - config.save(); - - window!.webContents.send('deleteService', id); - }); - - ipcMain.on('reorderService', (e, serviceId, targetId) => { - console.log('Reordering services', serviceId, targetId); - - const oldServices = config.services; - config.services = []; - - for (let i = 0; i < targetId; i++) { - if (i !== serviceId) { - config.services.push(oldServices[i]); - } - } - config.services.push(oldServices[serviceId]); - for (let i = targetId; i < oldServices.length; i++) { - if (i !== serviceId) { - config.services.push(oldServices[i]); - } - } - - e.reply('reorderService', serviceId, targetId); - config.save(); - }); - - ipcMain.on('updateServicePermissions', (e, serviceId, permissions) => { - config.services[serviceId].permissions = permissions; - config.save(); - }); - - ipcMain.on('updateWindowTitle', (event, serviceId, viewTitle) => { - if (serviceId === null) { - window!.setTitle(Meta.title); - } else { - const service = config.services[serviceId]; - window!.setTitle(Meta.getTitleForService(service, viewTitle)); - } - }); - - // Open add service window - ipcMain.on('openSettings', (e) => { - if (!settingsWindow) { - console.log('Opening settings'); - settingsWindow = new BrowserWindow({ - webPreferences: { - nodeIntegration: true, - enableRemoteModule: true, - webviewTag: true, - }, - parent: window!, - modal: true, - autoHideMenuBar: true, - height: 850, - }); - settingsWindow.on('close', () => { - settingsWindow = null; - }); - if (devMode) { - settingsWindow.webContents.openDevTools({ - mode: 'right' - }); - } - let syncListener: () => void; - ipcMain.on('syncSettings', syncListener = () => { - settingsWindow!.webContents.send('current-version', updater.getCurrentVersion()); - settingsWindow!.webContents.send('config', config); - }); - - let checkForUpdatesListener: () => void; - ipcMain.on('checkForUpdates', checkForUpdatesListener = () => { - updater.checkForUpdates().then(updateInfo => { - settingsWindow!.webContents.send('updateStatus', typeof updateInfo === 'object', updateInfo); - }).catch(console.error); - }); - - let saveConfigListener: (e: Event, data: any) => void; - ipcMain.on('save-config', saveConfigListener = (e: Event, data: any) => { - config.update(data); - config.save(); - sendData(); - }); - - settingsWindow.on('close', () => { - ipcMain.removeListener('syncSettings', syncListener); - ipcMain.removeListener('checkForUpdates', checkForUpdatesListener); - ipcMain.removeListener('save-config', saveConfigListener); - }); - settingsWindow.loadFile(path.resolve(resourcesDir, 'settings.html')) - .catch(console.error); - } - }); - - console.log('> App started'); -} - -function sendData() { - console.log('Syncing data'); - window!.webContents.send('data', Meta.title, brandIcons, solidIcons, selectedService, path.resolve(resourcesDir, 'empty.html'), config); -} - -function setActiveService(index: number) { - console.log('Selected service is now', index); - selectedService = index; -} - -function listIcons(set: string) { - console.log('Loading icon set', set); - const directory = path.resolve(resourcesDir, 'icons/' + set); - const icons: { name: string; faIcon: string }[] = []; - const dir = set === 'brands' ? 'fab' : 'fas'; - fs.readdirSync(directory).forEach(i => icons.push({ - name: i.split('.svg')[0], - faIcon: dir + ' fa-' + i.split('.svg')[0], - })); - return icons; -} +import Application from "./Application"; +const application = new Application(Meta.isDevMode()); // Check if application is already running const lock = new SingleInstance('tabs-app'); lock.lock().then(() => { - console.log('Starting app'); app.on('ready', () => { - createWindow().catch(console.error); + application.start().catch(console.error); }); }).catch(err => { console.error(err); diff --git a/src/windows/MainWindow.ts b/src/windows/MainWindow.ts new file mode 100644 index 0000000..d602ccf --- /dev/null +++ b/src/windows/MainWindow.ts @@ -0,0 +1,149 @@ +import path from "path"; +import {ipcMain} from "electron"; +import ServiceSettingsWindow from "./ServiceSettingsWindow"; +import SettingsWindow from "./SettingsWindow"; +import Application from "../Application"; +import Meta from "../Meta"; +import Window from "../Window"; + +export default class MainWindow extends Window { + private activeService: number = 0; + private serviceSettingsWindow?: ServiceSettingsWindow; + private settingsWindow?: SettingsWindow; + + constructor(application: Application) { + super(application); + } + + public setup() { + super.setup({ + webPreferences: { + nodeIntegration: true, + enableRemoteModule: true, + webviewTag: true, + }, + autoHideMenuBar: true, + icon: Meta.ICON_PATH, + title: Meta.title, + }); + + const window = this.getWindow(); + + window.maximize(); + + if (this.application.isDevMode()) { + window.webContents.openDevTools({ + mode: 'right' + }); + } + + // Sync data + window.webContents.on('dom-ready', () => { + this.syncData(); + }); + + // Load active service + this.onIpc('setActiveService', (event, index) => { + this.setActiveService(index); + }); + + // Set a service's favicon + this.onIpc('setServiceFavicon', (event, index, favicon) => { + console.log('Setting service', index, 'favicon', favicon); + this.config.services[index].favicon = favicon; + this.config.save(); + }); + + // Reorder services + this.onIpc('reorderService', (event, serviceId, targetId) => { + console.log('Reordering services', serviceId, targetId); + + const oldServices = this.config.services; + this.config.services = []; + + for (let i = 0; i < targetId; i++) { + if (i !== serviceId) { + this.config.services.push(oldServices[i]); + } + } + this.config.services.push(oldServices[serviceId]); + for (let i = targetId; i < oldServices.length; i++) { + if (i !== serviceId) { + this.config.services.push(oldServices[i]); + } + } + + event.reply('reorderService', serviceId, targetId); + this.config.save(); + }); + + // Delete service + this.onIpc('deleteService', (e, id) => { + console.log('Deleting service', id); + delete this.config.services[id]; + this.config.save(); + + window.webContents.send('deleteService', id); + }); + + // Update service permissions + ipcMain.on('updateServicePermissions', (e, serviceId, permissions) => { + this.config.services[serviceId].permissions = permissions; + this.config.save(); + }); + + // Update window title + ipcMain.on('updateWindowTitle', (event, serviceId, viewTitle) => { + if (serviceId === null) { + window.setTitle(Meta.title); + } else { + const service = this.config.services[serviceId]; + window.setTitle(Meta.getTitleForService(service, viewTitle)); + } + }); + + // Open service settings window + ipcMain.on('openServiceSettings', (e, serviceId) => { + if (!this.serviceSettingsWindow) { + console.log('Opening service settings', serviceId); + this.serviceSettingsWindow = new ServiceSettingsWindow(this.application, this, serviceId); + this.serviceSettingsWindow.setup(); + this.serviceSettingsWindow.onClose(() => { + this.serviceSettingsWindow = undefined; + }) + } + }); + + // Open settings window + ipcMain.on('openSettings', () => { + if (!this.settingsWindow) { + console.log('Opening settings'); + this.settingsWindow = new SettingsWindow(this.application, this); + this.settingsWindow.setup(); + this.settingsWindow.onClose(() => { + this.settingsWindow = undefined; + }); + } + }); + + // Load navigation view + window.loadFile(path.resolve(Meta.RESOURCES_PATH, 'index.html')) + .catch(console.error); + } + + public syncData() { + this.getWindow().webContents.send('data', + Meta.title, + Meta.BRAND_ICONS, + Meta.SOLID_ICONS, + this.activeService, + path.resolve(Meta.RESOURCES_PATH, 'empty.html'), + this.config + ); + } + + private setActiveService(index: number) { + console.log('Set active service', index); + this.activeService = index; + } +} \ No newline at end of file diff --git a/src/windows/ServiceSettingsWindow.ts b/src/windows/ServiceSettingsWindow.ts new file mode 100644 index 0000000..94324f8 --- /dev/null +++ b/src/windows/ServiceSettingsWindow.ts @@ -0,0 +1,58 @@ +import path from "path"; +import Window from "../Window"; +import Application from "../Application"; +import Meta from "../Meta"; +import Service from "../Service"; + +export default class ServiceSettingsWindow extends Window { + private readonly serviceId: number; + + constructor(application: Application, parent: Window, serviceId: number) { + super(application, parent); + this.serviceId = serviceId; + } + + public setup() { + super.setup({ + webPreferences: { + nodeIntegration: true, + enableRemoteModule: true, + webviewTag: true, + }, + modal: true, + autoHideMenuBar: true, + height: 850, + }); + + const window = this.getWindow(); + + if (this.application.isDevMode()) { + window.webContents.openDevTools({ + mode: 'right' + }); + } + + this.onIpc('sync-settings', () => { + window.webContents.send('syncIcons', Meta.BRAND_ICONS, Meta.SOLID_ICONS); + window.webContents.send('loadService', this.serviceId, this.config.services[this.serviceId]); + }); + + this.onIpc('saveService', (e, id, data) => { + console.log('Saving service', id, data); + const newService = new Service(data); + if (typeof id === 'number') { + this.config.services[id] = newService; + } else { + this.config.services.push(newService); + id = this.config.services.indexOf(newService); + } + this.config.save(); + + this.parent?.getWindow().webContents.send('updateService', id, newService); + }); + + window.loadFile(path.resolve(Meta.RESOURCES_PATH, 'service-settings.html')) + .catch(console.error); + } + +} \ No newline at end of file diff --git a/src/windows/SettingsWindow.ts b/src/windows/SettingsWindow.ts new file mode 100644 index 0000000..d274003 --- /dev/null +++ b/src/windows/SettingsWindow.ts @@ -0,0 +1,50 @@ +import {Event} from "electron"; +import path from "path"; +import Window from "../Window"; +import MainWindow from "./MainWindow"; +import Meta from "../Meta"; + +export default class SettingsWindow extends Window { + public setup() { + super.setup({ + webPreferences: { + nodeIntegration: true, + enableRemoteModule: true, + webviewTag: true, + }, + modal: true, + autoHideMenuBar: true, + height: 850, + }); + + const window = this.getWindow(); + + if (this.application.isDevMode()) { + window.webContents.openDevTools({ + mode: 'right' + }); + } + + this.onIpc('syncSettings', () => { + window.webContents.send('current-version', this.application.getUpdater().getCurrentVersion()); + window.webContents.send('config', this.config); + }); + + this.onIpc('checkForUpdates', () => { + this.application.getUpdater().checkForUpdates().then(updateInfo => { + window.webContents.send('updateStatus', typeof updateInfo === 'object', updateInfo); + }).catch(console.error); + }); + + this.onIpc('save-config', (e: Event, data: any) => { + this.config.update(data); + this.config.save(); + if (this.parent instanceof MainWindow) { + this.parent.syncData(); + } + }); + + window.loadFile(path.resolve(Meta.RESOURCES_PATH, 'settings.html')) + .catch(console.error); + } +} \ No newline at end of file