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 }