All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Changed the development server port from 5780 to 5173 for updated local development settings. - Reformatted import statements for better readability and consistency. - Simplified the timeout function call in handleWindowControl for improved code clarity.
798 lines
19 KiB
JavaScript
798 lines
19 KiB
JavaScript
import Electrobun, { BrowserWindow, Updater, Utils } from 'electrobun/bun'
|
|
import { applyApplicationMenu, setupApplicationMenuEvents } from './menu.js'
|
|
import {
|
|
applyMacOSWindowEffects,
|
|
MAC_TRAFFIC_LIGHT_OFFSET
|
|
} from './macos-window-effects.js'
|
|
import {
|
|
configureWindowMessaging,
|
|
sendToRenderer,
|
|
sendToWindow
|
|
} from './notify.js'
|
|
import {
|
|
clampWindowToWorkArea,
|
|
isWindowWorkAreaMaximized
|
|
} from './windows-work-area.js'
|
|
import {
|
|
findProtocolUrl,
|
|
setSingleInstanceHandlers
|
|
} from './single-instance.js'
|
|
import { getAppSettings } from './store.js'
|
|
import {
|
|
getWindowSessionFile,
|
|
writeWindowSessionFile,
|
|
writeWindowSessionFileSync
|
|
} from './session.js'
|
|
|
|
const isMacOS = process.platform === 'darwin'
|
|
const isWindows = process.platform === 'win32'
|
|
const WINDOWS_STARTUP_MAXIMIZE_DELAY_MS = 1500
|
|
const DEV_SERVER_PORT = 5173
|
|
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`
|
|
const DEFAULT_DASHBOARD_PATH = '/dashboard/production/overview'
|
|
const PROTOCOL_PREFIX = 'farmcontrol://'
|
|
|
|
const windows = new Map()
|
|
let focusedWindowId = null
|
|
let sharedRpc = null
|
|
let appListenersBound = false
|
|
let persistTimer = null
|
|
|
|
export function getDefaultDashboardPath() {
|
|
return DEFAULT_DASHBOARD_PATH
|
|
}
|
|
|
|
function createWindowId() {
|
|
return `window-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
|
}
|
|
|
|
function getEntry(windowId) {
|
|
return windowId ? windows.get(windowId) : null
|
|
}
|
|
|
|
export function getFocusedWindowId() {
|
|
if (focusedWindowId && windows.has(focusedWindowId)) {
|
|
return focusedWindowId
|
|
}
|
|
const first = windows.keys().next()
|
|
return first.done ? null : first.value
|
|
}
|
|
|
|
export function getWindowById(windowId) {
|
|
return getEntry(windowId)?.window || null
|
|
}
|
|
|
|
export function getMainWindow() {
|
|
return getWindowById(getFocusedWindowId())
|
|
}
|
|
|
|
function dispatchToWindow(windowId, channel, data) {
|
|
const entry = getEntry(windowId)
|
|
const window = entry?.window
|
|
if (!window) return false
|
|
|
|
try {
|
|
const webview = window.webview
|
|
const channelLiteral = JSON.stringify(channel)
|
|
const payloadLiteral = JSON.stringify(data ?? null)
|
|
|
|
if (webview?.executeJavascript) {
|
|
webview.executeJavascript(
|
|
`window.__farmcontrolDispatchRpcMessage?.(${channelLiteral}, ${payloadLiteral})`
|
|
)
|
|
return true
|
|
}
|
|
|
|
const send = webview?.rpc?.send
|
|
if (!send) {
|
|
console.warn(
|
|
`No RPC sender available for channel: ${channel}. Is the window ready?`
|
|
)
|
|
return false
|
|
}
|
|
|
|
send[channel](data)
|
|
return true
|
|
} catch (error) {
|
|
console.warn(`Failed to send RPC message on channel: ${channel}`, error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
function setupMessaging() {
|
|
configureWindowMessaging({
|
|
sendToFocused: (channel, data) =>
|
|
dispatchToWindow(getFocusedWindowId(), channel, data),
|
|
sendToWindow: (windowId, channel, data) =>
|
|
dispatchToWindow(windowId, channel, data),
|
|
sendToAll: (channel, data) => {
|
|
let sent = false
|
|
for (const id of windows.keys()) {
|
|
if (dispatchToWindow(id, channel, data)) {
|
|
sent = true
|
|
}
|
|
}
|
|
return sent
|
|
}
|
|
})
|
|
}
|
|
|
|
export function showMainWindow() {
|
|
const window = getMainWindow()
|
|
if (!window) return
|
|
|
|
if (window.isMinimized?.()) {
|
|
window.restore?.()
|
|
}
|
|
|
|
window.show?.()
|
|
window.activate?.()
|
|
}
|
|
|
|
export async function getMainViewUrl() {
|
|
const channel = await Updater.localInfo.channel()
|
|
if (channel === 'dev' || process.env.NODE_ENV === 'development') {
|
|
try {
|
|
await fetch(DEV_SERVER_URL, { method: 'HEAD' })
|
|
console.log(`Using Vite dev server at ${DEV_SERVER_URL}`)
|
|
return DEV_SERVER_URL
|
|
} catch {
|
|
console.warn(
|
|
'Vite dev server not running. Start it with `bun run dev:renderer`.'
|
|
)
|
|
}
|
|
}
|
|
|
|
return 'views://mainview/index.html'
|
|
}
|
|
|
|
function deliverNavigation(windowId, redirectPath) {
|
|
sendToWindow(windowId, 'navigate', redirectPath)
|
|
const window = getWindowById(windowId)
|
|
if (!window) return
|
|
|
|
if (window.isMinimized?.()) {
|
|
window.restore?.()
|
|
}
|
|
|
|
window.show?.()
|
|
window.activate?.()
|
|
}
|
|
|
|
function flushPendingNavigations(windowId) {
|
|
const entry = getEntry(windowId)
|
|
if (!entry?.domReady) return
|
|
|
|
while (entry.pendingNavigations.length > 0) {
|
|
const redirectPath = entry.pendingNavigations.shift()
|
|
setTimeout(() => deliverNavigation(windowId, redirectPath), 100)
|
|
}
|
|
}
|
|
|
|
function sendNavigateToRenderer(redirectPath) {
|
|
if (!redirectPath || typeof redirectPath !== 'string') {
|
|
return
|
|
}
|
|
|
|
const windowId = getFocusedWindowId()
|
|
if (!windowId) return
|
|
|
|
const entry = getEntry(windowId)
|
|
if (!entry?.domReady) {
|
|
entry?.pendingNavigations.push(redirectPath)
|
|
return
|
|
}
|
|
|
|
setTimeout(() => deliverNavigation(windowId, redirectPath), 100)
|
|
}
|
|
|
|
export function openInternalUrl(url) {
|
|
sendNavigateToRenderer(url)
|
|
return true
|
|
}
|
|
|
|
function parseDeepLinkPath(url) {
|
|
if (!url || typeof url !== 'string') {
|
|
return null
|
|
}
|
|
|
|
if (url.startsWith('/')) {
|
|
return url
|
|
}
|
|
|
|
if (!url.startsWith(`${PROTOCOL_PREFIX}app`)) {
|
|
return null
|
|
}
|
|
|
|
const redirectPath = url.slice(`${PROTOCOL_PREFIX}app`.length) || '/'
|
|
const normalizedPath = redirectPath.startsWith('/')
|
|
? redirectPath
|
|
: `/${redirectPath}`
|
|
|
|
try {
|
|
return decodeURI(normalizedPath)
|
|
} catch {
|
|
return normalizedPath
|
|
}
|
|
}
|
|
|
|
export function handleDeepLink(url) {
|
|
const path = parseDeepLinkPath(url)
|
|
if (!path) {
|
|
showMainWindow()
|
|
return false
|
|
}
|
|
|
|
sendNavigateToRenderer(path)
|
|
return true
|
|
}
|
|
|
|
export function handleDeepLinkFromArgv(launchUrl) {
|
|
if (process.platform === 'darwin') return
|
|
|
|
const url = launchUrl || findProtocolUrl(process.argv)
|
|
if (url) {
|
|
handleDeepLink(url)
|
|
}
|
|
}
|
|
|
|
function readWindowSize(window) {
|
|
const size = window?.getSize?.()
|
|
if (size && typeof size === 'object') {
|
|
return {
|
|
width: size.width ?? size[0],
|
|
height: size.height ?? size[1]
|
|
}
|
|
}
|
|
return { width: undefined, height: undefined }
|
|
}
|
|
|
|
function readWindowPosition(window) {
|
|
const position = window?.getPosition?.()
|
|
if (position && typeof position === 'object') {
|
|
return {
|
|
x: position.x ?? position[0],
|
|
y: position.y ?? position[1]
|
|
}
|
|
}
|
|
return { x: undefined, y: undefined }
|
|
}
|
|
|
|
function captureWindowLayout(windowId) {
|
|
const entry = getEntry(windowId)
|
|
if (!entry?.window) return
|
|
|
|
const { width, height } = readWindowSize(entry.window)
|
|
const { x, y } = readWindowPosition(entry.window)
|
|
if (width && height) {
|
|
entry.bounds = {
|
|
x: x ?? entry.bounds?.x ?? 100,
|
|
y: y ?? entry.bounds?.y ?? 100,
|
|
width,
|
|
height
|
|
}
|
|
}
|
|
entry.isFullScreen = entry.window.isFullScreen?.() ?? false
|
|
entry.isMaximized = isWindows
|
|
? isWindowWorkAreaMaximized(entry.window)
|
|
: (entry.window.isMaximized?.() ?? false)
|
|
}
|
|
|
|
function serializeWindows() {
|
|
return {
|
|
windows: [...windows.values()].map((entry) => ({
|
|
windowId: entry.id,
|
|
bounds: entry.bounds,
|
|
isMaximized: entry.isMaximized,
|
|
isFullScreen: entry.isFullScreen,
|
|
activeTabId: entry.activeTabId,
|
|
tabs: entry.tabs
|
|
}))
|
|
}
|
|
}
|
|
|
|
export function persistSessionNow() {
|
|
clearTimeout(persistTimer)
|
|
persistTimer = null
|
|
writeWindowSessionFileSync(serializeWindows())
|
|
}
|
|
|
|
function scheduleSessionPersist() {
|
|
clearTimeout(persistTimer)
|
|
persistTimer = setTimeout(() => {
|
|
void writeWindowSessionFile(serializeWindows())
|
|
}, 400)
|
|
}
|
|
|
|
export function syncWindowTabs(windowId, { activeTabId, tabs } = {}) {
|
|
const entry = getEntry(windowId) || getEntry(getFocusedWindowId())
|
|
if (!entry) return false
|
|
|
|
if (Array.isArray(tabs)) {
|
|
entry.tabs = tabs
|
|
}
|
|
if (activeTabId) {
|
|
entry.activeTabId = activeTabId
|
|
}
|
|
captureWindowLayout(entry.id)
|
|
scheduleSessionPersist()
|
|
return true
|
|
}
|
|
|
|
export function getWindowSession(windowId) {
|
|
const entry = getEntry(windowId) || getEntry(getFocusedWindowId())
|
|
if (!entry) {
|
|
return { windowId: null, tabs: [], activeTabId: null }
|
|
}
|
|
|
|
return {
|
|
windowId: entry.id,
|
|
tabs: entry.tabs,
|
|
activeTabId: entry.activeTabId,
|
|
bounds: entry.bounds,
|
|
isMaximized: entry.isMaximized,
|
|
isFullScreen: entry.isFullScreen
|
|
}
|
|
}
|
|
|
|
let pendingTabDrag = null
|
|
|
|
export function beginTabDrag({ windowId, tab } = {}) {
|
|
if (!tab?.id) return { ok: false }
|
|
pendingTabDrag = {
|
|
windowId: windowId || getFocusedWindowId(),
|
|
tab,
|
|
startedAt: Date.now()
|
|
}
|
|
return { ok: true }
|
|
}
|
|
|
|
export function completeTabDrop({
|
|
windowId,
|
|
beforeTabId,
|
|
insertBefore = false
|
|
} = {}) {
|
|
const pending = pendingTabDrag
|
|
pendingTabDrag = null
|
|
if (!pending?.tab) {
|
|
return { ok: false }
|
|
}
|
|
|
|
const targetWindowId = windowId || getFocusedWindowId()
|
|
if (pending.windowId && pending.windowId === targetWindowId) {
|
|
return { ok: true, sameWindow: true }
|
|
}
|
|
|
|
sendToWindow(pending.windowId, 'tabMovedAway', { tabId: pending.tab.id })
|
|
return {
|
|
ok: true,
|
|
sameWindow: false,
|
|
tab: pending.tab,
|
|
sourceWindowId: pending.windowId,
|
|
beforeTabId,
|
|
insertBefore
|
|
}
|
|
}
|
|
|
|
export function cancelTabDrag({ windowId } = {}) {
|
|
if (!pendingTabDrag) return { ok: true }
|
|
if (windowId && pendingTabDrag.windowId !== windowId) {
|
|
return { ok: true }
|
|
}
|
|
pendingTabDrag = null
|
|
return { ok: true }
|
|
}
|
|
|
|
function broadcastWindowState(windowId) {
|
|
const id = windowId || getFocusedWindowId()
|
|
if (!id) return
|
|
sendToWindow(id, 'windowState', getWindowState(id))
|
|
}
|
|
|
|
function applyStartupWindowState(window) {
|
|
if (!window) return
|
|
|
|
if (isMacOS) {
|
|
window.maximize?.()
|
|
}
|
|
|
|
setTimeout(() => broadcastWindowState(), 100)
|
|
}
|
|
|
|
function syncWindowsWebviewLayout(window) {
|
|
if (!window?.getSize || !window?.setSize) {
|
|
return
|
|
}
|
|
|
|
const { width, height } = window.getSize()
|
|
if (!width || !height) {
|
|
return
|
|
}
|
|
|
|
window.setSize(width, height)
|
|
}
|
|
|
|
function handleWindowsWindowChange(window, windowId) {
|
|
if (clampWindowToWorkArea(window)) {
|
|
syncWindowsWebviewLayout(window)
|
|
}
|
|
|
|
captureWindowLayout(windowId)
|
|
broadcastWindowState(windowId)
|
|
}
|
|
|
|
function applyWindowsStartupWindowState(window, windowId) {
|
|
if (!window) return
|
|
|
|
setTimeout(() => {
|
|
window.maximize?.()
|
|
setTimeout(() => handleWindowsWindowChange(window, windowId), 100)
|
|
}, WINDOWS_STARTUP_MAXIMIZE_DELAY_MS)
|
|
}
|
|
|
|
function setupWindowEvents(window, windowId) {
|
|
const onWindowChange = isWindows
|
|
? () => handleWindowsWindowChange(window, windowId)
|
|
: () => {
|
|
captureWindowLayout(windowId)
|
|
broadcastWindowState(windowId)
|
|
scheduleSessionPersist()
|
|
}
|
|
|
|
window.on?.('resize', onWindowChange)
|
|
window.on?.('move', onWindowChange)
|
|
window.on?.('focus', () => {
|
|
focusedWindowId = windowId
|
|
broadcastWindowState(windowId)
|
|
})
|
|
window.on?.('close', () => {
|
|
captureWindowLayout(windowId)
|
|
windows.delete(windowId)
|
|
if (focusedWindowId === windowId) {
|
|
focusedWindowId = getFocusedWindowId()
|
|
}
|
|
persistSessionNow()
|
|
})
|
|
}
|
|
|
|
function injectWindowId(window, windowId) {
|
|
try {
|
|
window?.webview?.executeJavascript?.(
|
|
`window.__farmcontrolWindowId = ${JSON.stringify(windowId)}`
|
|
)
|
|
} catch (error) {
|
|
console.warn('[window] Failed to inject window id.', error)
|
|
}
|
|
}
|
|
|
|
function createDefaultTab() {
|
|
const id = `tab-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
return {
|
|
id,
|
|
title: 'Overview - Production',
|
|
modelName: null,
|
|
history: [
|
|
{
|
|
pathname: DEFAULT_DASHBOARD_PATH,
|
|
search: '',
|
|
hash: ''
|
|
}
|
|
],
|
|
historyIndex: 0
|
|
}
|
|
}
|
|
|
|
function offsetBounds(bounds) {
|
|
if (!bounds) {
|
|
return { x: 120, y: 120, width: 1200, height: 800 }
|
|
}
|
|
return {
|
|
x: (bounds.x ?? 100) + 32,
|
|
y: (bounds.y ?? 100) + 32,
|
|
width: bounds.width || 1200,
|
|
height: bounds.height || 800
|
|
}
|
|
}
|
|
|
|
export async function createDesktopWindow({
|
|
rpc = sharedRpc,
|
|
windowId = createWindowId(),
|
|
bounds,
|
|
isMaximized = false,
|
|
isFullScreen = false,
|
|
tabs,
|
|
activeTabId,
|
|
applyDefaultMaximize = false
|
|
} = {}) {
|
|
if (!rpc) {
|
|
throw new Error('Cannot create a desktop window without RPC.')
|
|
}
|
|
|
|
sharedRpc = rpc
|
|
setupMessaging()
|
|
|
|
const initialTabs =
|
|
Array.isArray(tabs) && tabs.length > 0 ? tabs : [createDefaultTab()]
|
|
const initialActiveTabId = activeTabId || initialTabs[0].id
|
|
const frame = bounds || { width: 1200, height: 800, x: 100, y: 100 }
|
|
const url = await getMainViewUrl()
|
|
|
|
const window = new BrowserWindow({
|
|
title: 'Farm Control',
|
|
url,
|
|
rpc,
|
|
titleBarStyle: 'hiddenInset',
|
|
...(isMacOS
|
|
? { transparent: true, trafficLightOffset: MAC_TRAFFIC_LIGHT_OFFSET }
|
|
: {}),
|
|
frame: {
|
|
width: frame.width || 1200,
|
|
height: frame.height || 800,
|
|
x: frame.x ?? 100,
|
|
y: frame.y ?? 100
|
|
}
|
|
})
|
|
|
|
const entry = {
|
|
id: windowId,
|
|
window,
|
|
tabs: initialTabs,
|
|
activeTabId: initialActiveTabId,
|
|
bounds: frame,
|
|
isMaximized: Boolean(isMaximized),
|
|
isFullScreen: Boolean(isFullScreen),
|
|
domReady: false,
|
|
pendingNavigations: []
|
|
}
|
|
windows.set(windowId, entry)
|
|
focusedWindowId = windowId
|
|
injectWindowId(window, windowId)
|
|
|
|
if (isMacOS) {
|
|
applyMacOSWindowEffects(window)
|
|
}
|
|
|
|
setupAppListeners()
|
|
setupWindowEvents(window, windowId)
|
|
setupNavigationGestures(window, windowId)
|
|
|
|
if (applyDefaultMaximize) {
|
|
applyStartupWindowState(window)
|
|
} else if (isMaximized) {
|
|
window.maximize?.()
|
|
setTimeout(() => broadcastWindowState(windowId), 100)
|
|
} else if (isFullScreen) {
|
|
window.setFullScreen?.(true)
|
|
setTimeout(() => broadcastWindowState(windowId), 100)
|
|
} else {
|
|
setTimeout(() => broadcastWindowState(windowId), 100)
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
window.webview.on('dom-ready', () => {
|
|
entry.domReady = true
|
|
injectWindowId(window, windowId)
|
|
|
|
if (isWindows && applyDefaultMaximize) {
|
|
applyWindowsStartupWindowState(window, windowId)
|
|
}
|
|
|
|
flushPendingNavigations(windowId)
|
|
resolve(window)
|
|
})
|
|
})
|
|
}
|
|
|
|
export async function createAppWindow({
|
|
tabs,
|
|
activeTabId,
|
|
sourceWindowId
|
|
} = {}) {
|
|
const source = getEntry(sourceWindowId) || getEntry(getFocusedWindowId())
|
|
captureWindowLayout(source?.id)
|
|
return createDesktopWindow({
|
|
rpc: sharedRpc,
|
|
bounds: offsetBounds(source?.bounds),
|
|
tabs,
|
|
activeTabId,
|
|
applyDefaultMaximize: false
|
|
})
|
|
}
|
|
|
|
function setupAppListeners() {
|
|
if (appListenersBound) return
|
|
appListenersBound = true
|
|
|
|
if (isMacOS) {
|
|
applyApplicationMenu()
|
|
setupApplicationMenuEvents({
|
|
onNavigate: sendNavigateToRenderer,
|
|
onToggleDevTools: () => {
|
|
getMainWindow()?.webview?.toggleDevTools?.()
|
|
},
|
|
onCheckForUpdates: () => {
|
|
sendToRenderer('checkForUpdates')
|
|
},
|
|
onNewTab: () => {
|
|
sendToRenderer('newTab')
|
|
},
|
|
onNewWindow: () => {
|
|
sendToRenderer('newWindow')
|
|
}
|
|
})
|
|
}
|
|
|
|
Electrobun.events.on('open-url', (event) => {
|
|
const url = event?.data?.url
|
|
if (url) {
|
|
handleDeepLink(url)
|
|
}
|
|
})
|
|
}
|
|
|
|
export async function startDesktopWindows(rpc) {
|
|
sharedRpc = rpc
|
|
setupMessaging()
|
|
|
|
const settings = await getAppSettings()
|
|
const resumeLastSession = settings.appResumeLastSession !== false
|
|
const saved = resumeLastSession
|
|
? await getWindowSessionFile()
|
|
: { windows: [] }
|
|
const savedWindows = Array.isArray(saved.windows) ? saved.windows : []
|
|
|
|
if (savedWindows.length > 0) {
|
|
for (const savedWindow of savedWindows) {
|
|
await createDesktopWindow({
|
|
rpc,
|
|
windowId: savedWindow.windowId || createWindowId(),
|
|
bounds: savedWindow.bounds,
|
|
isMaximized: savedWindow.isMaximized,
|
|
isFullScreen: savedWindow.isFullScreen,
|
|
tabs: savedWindow.tabs,
|
|
activeTabId: savedWindow.activeTabId,
|
|
applyDefaultMaximize: false
|
|
})
|
|
}
|
|
return getMainWindow()
|
|
}
|
|
|
|
return createDesktopWindow({
|
|
rpc,
|
|
applyDefaultMaximize: true
|
|
})
|
|
}
|
|
|
|
export async function createMainWindow(rpc) {
|
|
return startDesktopWindows(rpc)
|
|
}
|
|
|
|
export async function setupDevAuthServer() {
|
|
const env = (process.env.NODE_ENV || 'development').trim()
|
|
if (env !== 'development') return
|
|
|
|
const express = (await import('express')).default
|
|
const app = express()
|
|
const port = 3500
|
|
|
|
app.use((req, res) => {
|
|
const redirectPath = req.originalUrl
|
|
res.send(`Open Farmcontrol to continue... (Redirect path: ${redirectPath})`)
|
|
sendNavigateToRenderer(redirectPath)
|
|
})
|
|
|
|
app.listen(port, () => {})
|
|
}
|
|
|
|
export function setupWindowsDeepLinkHandling() {
|
|
if (process.platform === 'darwin') {
|
|
return
|
|
}
|
|
|
|
setSingleInstanceHandlers({
|
|
onDeepLink: handleDeepLink,
|
|
onFocus: showMainWindow
|
|
})
|
|
}
|
|
|
|
export function getWindowState(windowId) {
|
|
const window = getWindowById(windowId || getFocusedWindowId())
|
|
if (!window) {
|
|
return { isFullScreen: false, isMaximized: false }
|
|
}
|
|
|
|
return {
|
|
isFullScreen: window.isFullScreen?.() ?? false,
|
|
isMaximized: isWindows
|
|
? isWindowWorkAreaMaximized(window)
|
|
: (window.isMaximized?.() ?? false)
|
|
}
|
|
}
|
|
|
|
export function handleWindowControl(action, windowId) {
|
|
const window = getWindowById(windowId || getFocusedWindowId())
|
|
if (!window && action !== 'quit') return
|
|
|
|
switch (action) {
|
|
case 'minimize':
|
|
window.minimize?.()
|
|
break
|
|
case 'maximize': {
|
|
const currentlyMaximized = isWindows
|
|
? isWindowWorkAreaMaximized(window)
|
|
: (window.isMaximized?.() ?? false)
|
|
|
|
if (currentlyMaximized) {
|
|
window.unmaximize?.()
|
|
} else {
|
|
window.maximize?.()
|
|
}
|
|
|
|
if (isWindows) {
|
|
setTimeout(
|
|
() =>
|
|
handleWindowsWindowChange(window, windowId || getFocusedWindowId()),
|
|
100
|
|
)
|
|
}
|
|
break
|
|
}
|
|
case 'fullscreen':
|
|
if (window.isFullScreen?.()) {
|
|
window.setFullScreen?.(false)
|
|
} else {
|
|
window.setFullScreen?.(true)
|
|
}
|
|
setTimeout(
|
|
() => broadcastWindowState(windowId || getFocusedWindowId()),
|
|
100
|
|
)
|
|
break
|
|
case 'close':
|
|
window.close?.()
|
|
break
|
|
case 'quit':
|
|
persistSessionNow()
|
|
Utils.quit()
|
|
break
|
|
case 'toggle-devtools':
|
|
window?.webview?.toggleDevTools?.()
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
export function sendNavigationGesture(direction, windowId) {
|
|
sendToWindow(windowId || getFocusedWindowId(), 'navigationGesture', direction)
|
|
}
|
|
|
|
export function setupNavigationGestures(window, windowId) {
|
|
if (!window) return
|
|
|
|
const id = windowId || getFocusedWindowId()
|
|
|
|
if (process.platform === 'darwin') {
|
|
window.on?.('swipe', (_event, direction) => {
|
|
if (direction === 'left') {
|
|
sendNavigationGesture('back', id)
|
|
} else if (direction === 'right') {
|
|
sendNavigationGesture('forward', id)
|
|
}
|
|
})
|
|
}
|
|
|
|
window.on?.('app-command', (_event, command) => {
|
|
if (command === 'browser-backward') {
|
|
sendNavigationGesture('back', id)
|
|
} else if (command === 'browser-forward') {
|
|
sendNavigationGesture('forward', id)
|
|
}
|
|
})
|
|
}
|
|
|
|
export function openExternalUrl(url) {
|
|
console.log('openExternalUrl', url)
|
|
Utils.openExternal(url)
|
|
}
|