/ app / index.js
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  }