diff --git a/materialious/electron/src/index.ts b/materialious/electron/src/index.ts index 8756e19e..642bc1e3 100644 --- a/materialious/electron/src/index.ts +++ b/materialious/electron/src/index.ts @@ -3,8 +3,7 @@ import { getCapacitorElectronConfig, setupElectronDeepLinking } from '@capacitor-community/electron'; -import type { MenuItemConstructorOptions } from 'electron'; -import { app, ipcMain, MenuItem, session } from 'electron'; +import { app, ipcMain, session } from 'electron'; import electronIsDev from 'electron-is-dev'; import unhandled from 'electron-unhandled'; import { autoUpdater } from 'electron-updater'; @@ -16,20 +15,12 @@ import { ElectronCapacitorApp, setupContentSecurityPolicy, setupReloadWatcher } // Graceful handling of unhandled errors. unhandled(); -// Define our menu template (this is optional) -const trayMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [ - new MenuItem({ label: 'Quit App', role: 'quit' }) -]; - // Get Config options from capacitor.config const capacitorFileConfig: CapacitorElectronConfig = getCapacitorElectronConfig(); // Initialize our app. You can pass menu templates into the app here. // const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig); -const myCapacitorApp = new ElectronCapacitorApp( - capacitorFileConfig, - trayMenuTemplate, -); +const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig); // If deeplinking is enabled then we will set it up here. if (capacitorFileConfig.electron?.deepLinkingEnabled) { diff --git a/materialious/electron/src/setup.ts b/materialious/electron/src/setup.ts index 186b35f8..7f026b1f 100644 --- a/materialious/electron/src/setup.ts +++ b/materialious/electron/src/setup.ts @@ -1,13 +1,23 @@ import type { CapacitorElectronConfig } from '@capacitor-community/electron'; import { - CapacitorSplashScreen, - CapElectronEventEmitter, - setupCapacitorElectronPlugins, + CapacitorSplashScreen, + CapElectronEventEmitter, + setupCapacitorElectronPlugins } from '@capacitor-community/electron'; import { USER_AGENT } from 'bgutils-js'; import chokidar from 'chokidar'; import type { MenuItemConstructorOptions } from 'electron'; -import { app, BrowserWindow, Menu, MenuItem, nativeImage, session, shell, Tray } from 'electron'; +import { + app, + BrowserWindow, + Menu, + MenuItem, + nativeImage, + session, + shell, + Tray, + globalShortcut +} from 'electron'; import electronIsDev from 'electron-is-dev'; import electronServe from 'electron-serve'; import windowStateKeeper from 'electron-window-state'; @@ -15,213 +25,236 @@ import { join } from 'path'; // Define components for a watcher to detect when the webapp is changed so we can reload in Dev mode. const reloadWatcher = { - debouncer: null, - ready: false, - watcher: null, + debouncer: null, + ready: false, + watcher: null }; export function setupReloadWatcher(electronCapacitorApp: ElectronCapacitorApp): void { - reloadWatcher.watcher = chokidar - .watch(join(app.getAppPath(), 'app'), { - ignored: /[/\\]\./, - persistent: true, - }) - .on('ready', () => { - reloadWatcher.ready = true; - }) - .on('all', (_event, _path) => { - if (reloadWatcher.ready) { - clearTimeout(reloadWatcher.debouncer); - reloadWatcher.debouncer = setTimeout(async () => { - electronCapacitorApp.getMainWindow().webContents.reload(); - reloadWatcher.ready = false; - clearTimeout(reloadWatcher.debouncer); - reloadWatcher.debouncer = null; - reloadWatcher.watcher = null; - setupReloadWatcher(electronCapacitorApp); - }, 1500); - } - }); + reloadWatcher.watcher = chokidar + .watch(join(app.getAppPath(), 'app'), { + ignored: /[/\\]\./, + persistent: true + }) + .on('ready', () => { + reloadWatcher.ready = true; + }) + .on('all', (_event, _path) => { + if (reloadWatcher.ready) { + clearTimeout(reloadWatcher.debouncer); + reloadWatcher.debouncer = setTimeout(async () => { + electronCapacitorApp.getMainWindow().webContents.reload(); + reloadWatcher.ready = false; + clearTimeout(reloadWatcher.debouncer); + reloadWatcher.debouncer = null; + reloadWatcher.watcher = null; + setupReloadWatcher(electronCapacitorApp); + }, 1500); + } + }); } // Define our class to manage our app. export class ElectronCapacitorApp { - private MainWindow: BrowserWindow | null = null; - private SplashScreen: CapacitorSplashScreen | null = null; - private TrayIcon: Tray | null = null; - private CapacitorFileConfig: CapacitorElectronConfig; - private TrayMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [ - new MenuItem({ label: 'Quit App', role: 'quit' }), - ]; - private mainWindowState; - private loadWebApp; - private customScheme: string; + private MainWindow: BrowserWindow | null = null; + private SplashScreen: CapacitorSplashScreen | null = null; + private TrayIcon: Tray | null = null; + private CapacitorFileConfig: CapacitorElectronConfig; + private TrayMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [ + new MenuItem({ label: 'Quit App', role: 'quit' }) + ]; + private mainWindowState; + private loadWebApp; + private customScheme: string; - constructor( - capacitorFileConfig: CapacitorElectronConfig, - trayMenuTemplate?: (MenuItemConstructorOptions | MenuItem)[], - ) { - this.CapacitorFileConfig = capacitorFileConfig; + constructor( + capacitorFileConfig: CapacitorElectronConfig, + trayMenuTemplate?: (MenuItemConstructorOptions | MenuItem)[] + ) { + this.CapacitorFileConfig = capacitorFileConfig; - this.customScheme = this.CapacitorFileConfig.electron?.customUrlScheme ?? 'capacitor-electron'; + this.customScheme = this.CapacitorFileConfig.electron?.customUrlScheme ?? 'capacitor-electron'; - if (trayMenuTemplate) { - this.TrayMenuTemplate = trayMenuTemplate; - } + if (trayMenuTemplate) { + this.TrayMenuTemplate = trayMenuTemplate; + } - // Setup our web app loader, this lets us load apps like react, vue, and angular without changing their build chains. - this.loadWebApp = electronServe({ - directory: join(app.getAppPath(), 'app'), - scheme: this.customScheme, - }); - } + // Setup our web app loader, this lets us load apps like react, vue, and angular without changing their build chains. + this.loadWebApp = electronServe({ + directory: join(app.getAppPath(), 'app'), + scheme: this.customScheme + }); + } - // Helper function to load in the app. - private async loadMainWindow(thisRef: any) { - await thisRef.loadWebApp(thisRef.MainWindow); - } + // Helper function to load in the app. + private async loadMainWindow(thisRef: any) { + await thisRef.loadWebApp(thisRef.MainWindow); + } - // Expose the mainWindow ref for use outside of the class. - getMainWindow(): BrowserWindow { - return this.MainWindow; - } + // Expose the mainWindow ref for use outside of the class. + getMainWindow(): BrowserWindow { + return this.MainWindow; + } - getCustomURLScheme(): string { - return this.customScheme; - } + getCustomURLScheme(): string { + return this.customScheme; + } - async init(): Promise { - const icon = nativeImage.createFromPath( - join(app.getAppPath(), 'assets', process.platform === 'win32' ? 'appIcon.ico' : 'appIcon.png') - ); - this.mainWindowState = windowStateKeeper({ - defaultWidth: 1000, - defaultHeight: 800, - }); - // Setup preload script path and construct our main window. - const preloadPath = join(app.getAppPath(), 'build', 'src', 'preload.js'); - this.MainWindow = new BrowserWindow({ - icon, - show: false, - x: this.mainWindowState.x, - y: this.mainWindowState.y, - width: this.mainWindowState.width, - height: this.mainWindowState.height, - webPreferences: { - nodeIntegration: true, - contextIsolation: true, - webSecurity: false, - // Use preload to inject the electron varriant overrides for capacitor plugins. - // preload: join(app.getAppPath(), "node_modules", "@capacitor-community", "electron", "dist", "runtime", "electron-rt.js"), - preload: preloadPath, - }, - }); - this.mainWindowState.manage(this.MainWindow); + async init(): Promise { + const icon = nativeImage.createFromPath( + join(app.getAppPath(), 'assets', process.platform === 'win32' ? 'appIcon.ico' : 'appIcon.png') + ); + this.mainWindowState = windowStateKeeper({ + defaultWidth: 1000, + defaultHeight: 800 + }); + // Setup preload script path and construct our main window. + const preloadPath = join(app.getAppPath(), 'build', 'src', 'preload.js'); + this.MainWindow = new BrowserWindow({ + icon, + show: false, + x: this.mainWindowState.x, + y: this.mainWindowState.y, + width: this.mainWindowState.width, + height: this.mainWindowState.height, + webPreferences: { + nodeIntegration: true, + contextIsolation: true, + webSecurity: false, + // Use preload to inject the electron varriant overrides for capacitor plugins. + // preload: join(app.getAppPath(), "node_modules", "@capacitor-community", "electron", "dist", "runtime", "electron-rt.js"), + preload: preloadPath + } + }); + this.mainWindowState.manage(this.MainWindow); - if (this.CapacitorFileConfig.backgroundColor) { - this.MainWindow.setBackgroundColor(this.CapacitorFileConfig.electron.backgroundColor); - } + if (this.CapacitorFileConfig.backgroundColor) { + this.MainWindow.setBackgroundColor(this.CapacitorFileConfig.electron.backgroundColor); + } - // If we close the main window with the splashscreen enabled we need to destory the ref. - this.MainWindow.on('closed', () => { - if (this.SplashScreen?.getSplashWindow() && !this.SplashScreen.getSplashWindow().isDestroyed()) { - this.SplashScreen.getSplashWindow().close(); - } - }); + // If we close the main window with the splashscreen enabled we need to destory the ref. + this.MainWindow.on('closed', () => { + if ( + this.SplashScreen?.getSplashWindow() && + !this.SplashScreen.getSplashWindow().isDestroyed() + ) { + this.SplashScreen.getSplashWindow().close(); + } + }); - // When the tray icon is enabled, setup the options. - if (this.CapacitorFileConfig.electron?.trayIconAndMenuEnabled) { - this.TrayIcon = new Tray(icon); - this.TrayIcon.on('double-click', () => { - if (this.MainWindow) { - if (this.MainWindow.isVisible()) { - this.MainWindow.hide(); - } else { - this.MainWindow.show(); - this.MainWindow.focus(); - } - } - }); - this.TrayIcon.on('click', () => { - if (this.MainWindow) { - if (this.MainWindow.isVisible()) { - this.MainWindow.hide(); - } else { - this.MainWindow.show(); - this.MainWindow.focus(); - } - } - }); - this.TrayIcon.setToolTip(app.getName()); - this.TrayIcon.setContextMenu(Menu.buildFromTemplate(this.TrayMenuTemplate)); - } + // When the tray icon is enabled, setup the options. + if (this.CapacitorFileConfig.electron?.trayIconAndMenuEnabled) { + this.TrayIcon = new Tray(icon); + this.TrayIcon.on('double-click', () => { + if (this.MainWindow) { + if (this.MainWindow.isVisible()) { + this.MainWindow.hide(); + } else { + this.MainWindow.show(); + this.MainWindow.focus(); + } + } + }); + this.TrayIcon.on('click', () => { + if (this.MainWindow) { + if (this.MainWindow.isVisible()) { + this.MainWindow.hide(); + } else { + this.MainWindow.show(); + this.MainWindow.focus(); + } + } + }); + this.TrayIcon.setToolTip(app.getName()); + this.TrayIcon.setContextMenu(Menu.buildFromTemplate(this.TrayMenuTemplate)); + } - // Disable Menu Bar at the top. - Menu.setApplicationMenu(null); + // Disable Menu Bar at the top. + Menu.setApplicationMenu(null); - // If the splashscreen is enabled, show it first while the main window loads then switch it out for the main window, or just load the main window from the start. - if (this.CapacitorFileConfig.electron?.splashScreenEnabled) { - this.SplashScreen = new CapacitorSplashScreen({ - imageFilePath: join( - app.getAppPath(), - 'assets', - this.CapacitorFileConfig.electron?.splashScreenImageName ?? 'splash.png' - ), - windowWidth: 400, - windowHeight: 400, - }); - this.SplashScreen.init(this.loadMainWindow, this); - } else { - this.loadMainWindow(this); - } + // If the splashscreen is enabled, show it first while the main window loads then switch it out for the main window, or just load the main window from the start. + if (this.CapacitorFileConfig.electron?.splashScreenEnabled) { + this.SplashScreen = new CapacitorSplashScreen({ + imageFilePath: join( + app.getAppPath(), + 'assets', + this.CapacitorFileConfig.electron?.splashScreenImageName ?? 'splash.png' + ), + windowWidth: 400, + windowHeight: 400 + }); + this.SplashScreen.init(this.loadMainWindow, this); + } else { + this.loadMainWindow(this); + } - // Security - this.MainWindow.webContents.setWindowOpenHandler((details) => { - shell.openExternal(details.url); - return { action: 'deny' }; - }); - this.MainWindow.webContents.on('will-navigate', (event, _newURL) => { - if (!this.MainWindow.webContents.getURL().includes(this.customScheme)) { - event.preventDefault(); - } - }); - // Link electron plugins into the system. - setupCapacitorElectronPlugins(); + globalShortcut.register('Control+Shift+I', () => { + this.MainWindow.webContents.toggleDevTools(); + }); - // When the web app is loaded we hide the splashscreen if needed and show the mainwindow. - this.MainWindow.webContents.on('dom-ready', () => { - if (this.CapacitorFileConfig.electron?.splashScreenEnabled) { - this.SplashScreen.getSplashWindow().hide(); - } - if (!this.CapacitorFileConfig.electron?.hideMainWindowOnLaunch) { - this.MainWindow.show(); - } - setTimeout(() => { - if (electronIsDev) { - this.MainWindow.webContents.openDevTools(); - } - CapElectronEventEmitter.emit('CAPELECTRON_DeeplinkListenerInitialized', ''); - }, 400); - }); - } + let zoomFactor = 1.0; + globalShortcut.register('Control+Plus', () => { + zoomFactor += 0.1; + this.MainWindow.webContents.setZoomFactor(zoomFactor); + }); + + globalShortcut.register('Control+Minus', () => { + zoomFactor = Math.max(0.2, zoomFactor - 0.1); + this.MainWindow.webContents.setZoomFactor(zoomFactor); + }); + + globalShortcut.register('Control+0', () => { + zoomFactor = 1.0; + this.MainWindow.webContents.setZoomFactor(zoomFactor); + }); + + // Security + this.MainWindow.webContents.setWindowOpenHandler((details) => { + shell.openExternal(details.url); + return { action: 'deny' }; + }); + this.MainWindow.webContents.on('will-navigate', (event, _newURL) => { + if (!this.MainWindow.webContents.getURL().includes(this.customScheme)) { + event.preventDefault(); + } + }); + // Link electron plugins into the system. + setupCapacitorElectronPlugins(); + + // When the web app is loaded we hide the splashscreen if needed and show the mainwindow. + this.MainWindow.webContents.on('dom-ready', () => { + if (this.CapacitorFileConfig.electron?.splashScreenEnabled) { + this.SplashScreen.getSplashWindow().hide(); + } + if (!this.CapacitorFileConfig.electron?.hideMainWindowOnLaunch) { + this.MainWindow.show(); + } + setTimeout(() => { + if (electronIsDev) { + this.MainWindow.webContents.openDevTools(); + } + CapElectronEventEmitter.emit('CAPELECTRON_DeeplinkListenerInitialized', ''); + }, 400); + }); + } } // Set a CSP up for our application based on the custom scheme export function setupContentSecurityPolicy(customScheme: string): void { - session.defaultSession.webRequest.onHeadersReceived((details, callback) => { - callback({ - responseHeaders: { - ...details.responseHeaders, - }, - }); - }); + session.defaultSession.webRequest.onHeadersReceived((details, callback) => { + callback({ + responseHeaders: { + ...details.responseHeaders + } + }); + }); - session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { - const uri = new URL(details.url); + session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { + const uri = new URL(details.url); - details.requestHeaders['User-Agent'] = USER_AGENT; - details.requestHeaders['origin'] = uri.origin; - details.requestHeaders['host'] = uri.host; + details.requestHeaders['User-Agent'] = USER_AGENT; + details.requestHeaders['origin'] = uri.origin; + details.requestHeaders['host'] = uri.host; - callback({ requestHeaders: details.requestHeaders }); - }); + callback({ requestHeaders: details.requestHeaders }); + }); }