/ src / main / aid / index.ts
index.ts
  1  import { CreateWindowOpts, ReleaseFocusOpts, BrowserWindowCommandOptions } from './window'
  2  import { app, BrowserWindow, Menu, Display, screen, shell } from 'electron'
  3  
  4  import { wait } from './utils'
  5  import MacosAutomator from './macos'
  6  import WindowsAutomator from './windows'
  7  
  8  import Constants from '../utils/Constants'
  9  
 10  const dockedWindows: Set<number> = new Set()
 11  
 12  export interface WindowListener {
 13    onWindowCreated: (_window: BrowserWindow) => void
 14    onWindowTitleChanged: (_window: BrowserWindow) => void
 15    onWindowClosed: (_window: BrowserWindow) => void
 16  }
 17  const listeners: WindowListener[] = []
 18  export const addWindowListener = (listener: WindowListener) => {
 19    listeners.push(listener)
 20  }
 21  
 22  export const createWindow = (opts: CreateWindowOpts = {}) => {
 23    // create the browser window
 24    const window = new BrowserWindow({
 25      ...opts,
 26      show: false,
 27      webPreferences: {
 28        ...Constants.DEFAULT_WEB_PREFERENCES
 29      }
 30    })
 31  
 32    // show when ready
 33    window.once('ready-to-show', () => {
 34      if (!opts.keepHidden) {
 35        window.show()
 36      }
 37    })
 38  
 39    // notify listeners
 40    window.on('show', () => {
 41      // docked window
 42      if (opts.showInDock) {
 43        dockedWindows.add(window.id)
 44        if (process.platform === 'darwin') {
 45          app.dock.show()
 46        }
 47      }
 48  
 49      // notify
 50      for (const listener of listeners) {
 51        listener.onWindowCreated(window)
 52      }
 53    })
 54  
 55    // notify listeners
 56    window.webContents.on('page-title-updated', () => {
 57      for (const listener of listeners) {
 58        listener.onWindowTitleChanged(window)
 59      }
 60    })
 61  
 62    // we keep prompt anywhere all the time so we need our own way
 63    window.on('closed', () => {
 64      for (const listener of listeners) {
 65        listener.onWindowClosed(window)
 66      }
 67      undockWindow(window)
 68    })
 69  
 70    // web console to here
 71    window.webContents.on('console-message', ({ level, message, lineNumber, sourceId, frame }) => {
 72      void level
 73      void frame
 74      if (
 75        !message.includes('Electron Security Warning') &&
 76        !message.includes('Third-party cookie will be blocked')
 77      ) {
 78        console.log(`${message} ${sourceId}:${lineNumber}`)
 79      }
 80    })
 81  
 82    // open links in default browser
 83    window.webContents.setWindowOpenHandler(({ url }) => {
 84      shell.openExternal(url)
 85      return { action: 'deny' }
 86    })
 87  
 88    // to log network traffic
 89    // interceptNetwork(window);
 90  
 91    loadWindowUrl(window, opts)
 92  
 93    return window
 94  }
 95  
 96  export function loadWindowUrl(window: BrowserWindow, opts: BrowserWindowCommandOptions) {
 97    let queryParams = ''
 98    if (opts.queryParams) {
 99      queryParams =
100        '?' +
101        Object.keys(opts.queryParams)
102          .map((key) => key + '=' + encodeURIComponent(opts.queryParams[key]))
103          .join('&')
104    }
105  
106    let url: string
107    if (Constants.IS_DEV_ENV) {
108      url = `${Constants.APP_INDEX_URL_DEV}#${opts.hash || ''}${queryParams}`
109    } else {
110      url = `${Constants.APP_INDEX_URL_PROD}#${opts.hash || ''}${queryParams}`
111    }
112  
113    // const url = `${MAIN_WINDOW_VITE_DEV_SERVER_URL}${queryParams}#${opts.hash||''}`;
114    console.log('Load URL:', url)
115    window.loadURL(url)
116  }
117  
118  export const undockWindow = (window: BrowserWindow, preventQuit: boolean = false) => {
119    // helper function
120    const hideDock = () => {
121      if (process.platform === 'darwin' && dockedWindows.size === 0) {
122        app.dock.hide()
123      }
124    }
125  
126    // only if it was there before
127    if (!dockedWindows.has(window.id)) {
128      hideDock()
129      return
130    }
131  
132    // remove
133    dockedWindows.delete(window.id)
134    if (dockedWindows.size) {
135      return
136    }
137  
138    // quit when all windows are closed, except on macOS. There, it's common
139    // for applications and their menu bar to stay active until the user quits
140    // explicitly with Cmd + Q.
141    if (process.platform === 'darwin') {
142      // for an unknown reason app.dock.hide
143      // might not work immediately...
144      hideDock()
145      setTimeout(hideDock, 1000)
146      setTimeout(hideDock, 2500)
147    } else if (!preventQuit) {
148      app.quit()
149    }
150  }
151  
152  // ensure window is on current screen
153  export const ensureOnCurrentScreen = (window: BrowserWindow) => {
154    const cursorScreen = getCurrentScreen()
155    const windowScreen = getWindowScreen(window)
156    if (cursorScreen.id !== windowScreen.id) {
157      // adjust width
158      let windowSize = window.getSize()
159      if (windowSize[0] > cursorScreen.workAreaSize.width) {
160        window.setSize(Math.floor(cursorScreen.workAreaSize.width * 0.8), windowSize[1])
161      }
162  
163      // move
164      windowSize = window.getSize()
165      const { x, y } = getCenteredCoordinates(windowSize[0], windowSize[1])
166      window.setPosition(x, y)
167    }
168  }
169  
170  export const getCurrentScreen = () => {
171    const cursorPoint = screen.getCursorScreenPoint()
172    return screen.getDisplayNearestPoint(cursorPoint)
173  }
174  
175  export const getWindowScreen = (window: BrowserWindow): Display => {
176    const windowPosition = window.getPosition()
177    return screen.getDisplayNearestPoint({ x: windowPosition[0], y: windowPosition[1] })
178  }
179  
180  // get coordinates for a centered window slightly above the center
181  export const getCenteredCoordinates = (w: number, h: number) => {
182    const cursorScreen = getCurrentScreen()
183    const { width, height } = cursorScreen.workAreaSize
184    return {
185      x: cursorScreen.bounds.x + Math.round((width - w) / 2),
186      y: cursorScreen.bounds.y + Math.round(Math.max(height / 5, (height - h) / 3))
187    }
188  }
189  
190  export const releaseFocus = async (opts?: ReleaseFocusOpts) => {
191    // defaults
192    opts = {
193      sourceApp: null,
194      delay: 500,
195      ...opts
196    }
197  
198    // platform specific
199    if (process.platform === 'darwin') {
200      let focused = false
201  
202      // if we have an app then target it
203      if (opts?.sourceApp) {
204        try {
205          console.log(`Releasing focus to ${opts.sourceApp.id} / ${opts.sourceApp.window}`)
206          const macosAutomator = new MacosAutomator()
207          focused = await macosAutomator.focusApp(opts.sourceApp)
208        } catch (error) {
209          console.error('Error while focusing app', error)
210        }
211      }
212  
213      if (!focused) {
214        Menu.sendActionToFirstResponder('hide:')
215      }
216    } else if (process.platform === 'win32') {
217      let focused = false
218  
219      // if we have an app then target it
220      if (opts?.sourceApp?.window) {
221        try {
222          console.log(`Releasing focus to ${opts.sourceApp.window}`)
223          const windowsAutomator = new WindowsAutomator()
224          await windowsAutomator.activateApp(opts.sourceApp.window)
225          focused = true
226        } catch (error) {
227          console.error('Error while focusing app', error)
228        }
229      }
230  
231      if (!focused) {
232        const dummyTransparentWindow = new BrowserWindow({
233          width: 1,
234          height: 1,
235          x: -100,
236          y: -100,
237          skipTaskbar: true,
238          transparent: true,
239          frame: false
240        })
241  
242        dummyTransparentWindow.close()
243      }
244    }
245  
246    // pause
247    if (opts.delay > 0) {
248      await wait(opts.delay)
249    }
250  }