index.js
1 import { app, BrowserWindow, session, Menu, Tray } from 'electron' 2 import path, { sep } from 'node:path' 3 import { fileURLToPath } from 'node:url' 4 5 import * as protocols from './protocols/index.js' 6 import { createActions } from './actions.js' 7 import { registerMenu } from './menu.js' 8 import { attachContextMenus } from './context-menus.js' 9 import { WindowManager } from './window.js' 10 import { createExtensions } from './extensions/index.js' 11 import * as history from './history.js' 12 import { version } from './version.js' 13 14 const IS_DEBUG = process.env.NODE_ENV === 'debug' 15 16 const __dirname = fileURLToPath(new URL('./', import.meta.url)) 17 18 const WEB_PARTITION = 'persist:web-content' 19 const LOGO_FILE = path.join(__dirname, './../build/icon-small.png') 20 21 // Wait for two minutes of use before registering handlers 22 const REGISTRATION_DELAY = 2 * 60 * 1000 23 24 if (IS_DEBUG) { 25 app.on('web-contents-created', (event, webContents) => { 26 webContents.openDevTools() 27 }) 28 } 29 30 if (!IS_DEBUG) { 31 // We shouldn't freeze the process when there's an error. Just ignore it. 32 process.on('uncaughtException', function (error) { 33 console.error(error) 34 }) 35 } 36 37 process.on('SIGINT', () => app.quit()) 38 39 let extensions = null 40 let windowManager = null 41 42 // Enable text to speech. 43 // Requires espeak on Linux 44 app.commandLine.appendSwitch('enable-speech-dispatcher') 45 46 // Smooth scrolling 47 app.commandLine.appendSwitch('enable-smooth-scrolling') 48 49 // Try to use the GPU for video decode on Pinephone 50 app.commandLine.appendSwitch('enable-accelerated-video-decode') 51 app.commandLine.appendSwitch('ignore-gpu-blacklist') 52 53 // Experimental web platform features, such as the FileSystem API 54 app.commandLine.appendSwitch('enable-experimental-web-platform-features') 55 56 init() 57 58 function init () { 59 const gotTheLock = app.requestSingleInstanceLock() 60 61 if (!gotTheLock) { 62 app.quit() 63 return 64 } 65 66 windowManager = new WindowManager({ 67 onSearch: (...args) => history.search(...args), 68 listActions: (...args) => extensions.listActions(...args) 69 }) 70 71 app.on('second-instance', (event, argv, workingDirectory) => { 72 console.log('Got signal from second instance', [...argv]) 73 const urls = urlsFromArgs(argv.slice(1), workingDirectory) 74 urls.map((url) => windowManager.open({ url })) 75 }) 76 77 windowManager.on('open', window => { 78 attachContextMenus({ window, createWindow, extensions }) 79 if (!window.rawFrame) { 80 const asBrowserView = BrowserWindow.fromBrowserView(window.view) 81 asBrowserView.on('focus', () => { 82 window.web.focus() 83 }) 84 } 85 86 window.web.setWindowOpenHandler(({ url, features, disposition }) => { 87 console.log('New window', url, disposition) 88 if ((disposition === 'foreground-tab') || (disposition === 'background-tab')) { 89 createWindow(url) 90 91 return { action: 'deny' } 92 } else { 93 // TODO: Should we override more options here? 94 return { action: 'allow' } 95 } 96 }) 97 98 window.web.on('did-create-window', (window) => { 99 attachContextMenus({ window, createWindow, extensions }) 100 }) 101 }) 102 } 103 104 // This method will be called when Electron has finished 105 // initialization and is ready to create browser windows. 106 // Some APIs can only be used after this event occurs. 107 app.whenReady().then(onready).catch((e) => { 108 console.error(e) 109 process.exit(1) 110 }) 111 112 app.on('activate', () => { 113 // On macOS it's common to re-create a window in the app when the 114 // dock icon is clicked and there are no other windows open. 115 if (BrowserWindow.getAllWindows().length === 0) { 116 windowManager.open() 117 } 118 }) 119 120 app.on('before-quit', () => { 121 windowManager.saveOpened() 122 windowManager.close() 123 protocols.close() 124 }) 125 126 app.on('window-all-closed', () => {}) 127 async function onready () { 128 console.log('Building tray and context menu') 129 const appIcon = new Tray(LOGO_FILE) 130 const contextMenu = Menu.buildFromTemplate([ 131 { label: 'New Window', click: () => createWindow() }, 132 { 133 label: 'Quit', 134 role: 'quit' 135 } 136 ]) 137 // Call this again for Linux because we modified the context menu 138 appIcon.setContextMenu(contextMenu) 139 appIcon.on('click', () => { 140 createWindow() 141 }) 142 143 const webSession = session.fromPartition(WEB_PARTITION) 144 145 const electronSection = /Electron.+ /i 146 const existingAgent = webSession.getUserAgent() 147 const newAgent = existingAgent.replace(electronSection, `AgregoreDesktop/${version} `) 148 149 webSession.setUserAgent(newAgent) 150 session.defaultSession.setUserAgent(newAgent) 151 152 const actions = createActions({ 153 createWindow 154 }) 155 156 console.log('Setting up protocol handlers') 157 158 await protocols.setupProtocols(webSession) 159 160 console.log('Registering context menu') 161 162 await registerMenu(actions) 163 164 function updateBrowserActions (tabId, actions) { 165 windowManager.reloadBrowserActions(tabId) 166 } 167 168 console.log('Initializing extensions') 169 170 extensions = createExtensions({ session: webSession, createWindow, updateBrowserActions }) 171 172 console.log('Extracting internal extensions') 173 174 // Extract any internal extensions if there are updates 175 await extensions.extractInternal() 176 177 console.log('Registering extensions from disk') 178 179 // Register all extensions in the extensions folder from disk 180 await extensions.registerAll() 181 182 // TODO: Better error handling when the extension doesn't exist? 183 history.setGetBackgroundPage(() => { 184 return extensions.getBackgroundPageByName('agregore-history') 185 }) 186 187 console.log('Opening saved windows') 188 189 const opened = await windowManager.openSaved() 190 191 const urls = urlsFromArgs(process.argv.slice(1), process.cwd()) 192 if (urls.length) { 193 for (const url of urls) { 194 windowManager.open({ url }) 195 } 196 } else if (!opened.length) windowManager.open() 197 198 console.log('Waiting for windows to settle') 199 200 await new Promise((resolve) => setTimeout(resolve, REGISTRATION_DELAY)) 201 202 protocols.setAsDefaultProtocolClient() 203 204 console.log('Initialization done') 205 } 206 207 function createWindow (url, options = {}) { 208 console.log('createWindow', url, options) 209 return windowManager.open({ url, ...options }) 210 } 211 212 function urlsFromArgs (argv, workingDir) { 213 const rootURL = new URL(workingDir + sep, 'file://') 214 return argv 215 .filter((arg) => arg.includes('/')) 216 .map((arg) => (arg.includes('://') ? arg : (new URL(arg, rootURL)).href)) 217 }