Tom Butcher d0345fd7f3
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
Enhance window management for macOS and Windows
- Added functionality to maximize the window on macOS and set it to fullscreen on Windows during startup.
- Introduced applyStartupWindowState function to manage initial window state based on the operating system.
- Updated createMainWindow to call applyStartupWindowState for improved user experience.
2026-08-02 16:20:59 +01:00

292 lines
7.0 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 { sendToRenderer, setMessageSender } from './notify.js'
const isMacOS = process.platform === 'darwin'
const DEV_SERVER_PORT = 5780
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`
const PROTOCOL_PREFIX = 'farmcontrol://'
let mainWindow = null
let webviewDomReady = false
const pendingNavigations = []
export function getMainWindow() {
return mainWindow
}
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(redirectPath) {
sendToRenderer('navigate', redirectPath)
mainWindow?.show?.()
mainWindow?.activate?.()
}
function flushPendingNavigations() {
if (!mainWindow || !webviewDomReady) {
return
}
while (pendingNavigations.length > 0) {
const redirectPath = pendingNavigations.shift()
setTimeout(() => deliverNavigation(redirectPath), 100)
}
}
function sendNavigateToRenderer(redirectPath) {
if (!redirectPath || typeof redirectPath !== 'string') {
return
}
if (!mainWindow || !webviewDomReady) {
pendingNavigations.push(redirectPath)
return
}
setTimeout(() => deliverNavigation(redirectPath), 100)
}
export function handleDeepLink(url) {
if (!url?.startsWith(`${PROTOCOL_PREFIX}app`)) return
const redirectPath = url.replace(`${PROTOCOL_PREFIX}app`, '') || '/'
sendNavigateToRenderer(redirectPath)
}
function findProtocolUrl(args) {
return args.find(
(arg) => typeof arg === 'string' && arg.startsWith(PROTOCOL_PREFIX)
)
}
export function handleDeepLinkFromArgv() {
if (process.platform === 'darwin') return
const url = findProtocolUrl(process.argv)
if (url) handleDeepLink(url)
}
function broadcastWindowState() {
sendToRenderer('windowState', getWindowState())
}
function applyStartupWindowState(window) {
if (!window) return
if (isMacOS) {
window.maximize?.()
} else if (process.platform === 'win32') {
window.setFullScreen?.(true)
}
setTimeout(broadcastWindowState, 100)
}
function setupWindowEvents(window) {
// Electrobun emits resize/focus, not Electron's maximize/fullscreen events.
window.on?.('resize', broadcastWindowState)
window.on?.('focus', broadcastWindowState)
window.on?.('move', broadcastWindowState)
}
export function setupMainWindowMessaging(window = mainWindow) {
if (!window) {
return
}
setMessageSender((channel, data) => {
try {
const webview = window.webview
const channelLiteral = JSON.stringify(channel)
const payloadLiteral = JSON.stringify(data ?? null)
// Prefer direct JS dispatch — WebSocket RPC pushes can be deferred by
// WKWebView until the next interaction during native window transitions.
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
}
})
}
export async function createMainWindow(rpc) {
const url = await getMainViewUrl()
mainWindow = new BrowserWindow({
title: 'Farm Control',
url,
rpc,
titleBarStyle: 'hiddenInset',
...(isMacOS
? { transparent: true, trafficLightOffset: MAC_TRAFFIC_LIGHT_OFFSET }
: {}),
frame: {
width: 1200,
height: 800,
x: 100,
y: 100
}
})
if (isMacOS) {
applyMacOSWindowEffects(mainWindow)
}
setupMainWindowMessaging(mainWindow)
applyApplicationMenu()
setupApplicationMenuEvents({
onNavigate: sendNavigateToRenderer,
onToggleDevTools: () => {
mainWindow?.webview?.toggleDevTools?.()
}
})
setupWindowEvents(mainWindow)
applyStartupWindowState(mainWindow)
Electrobun.events.on('open-url', (event) => {
const url = event?.data?.url
if (url) {
handleDeepLink(url)
}
})
return new Promise((resolve) => {
mainWindow.webview.on('dom-ready', () => {
webviewDomReady = true
flushPendingNavigations()
resolve(mainWindow)
})
})
}
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 openInternalUrl(url) {
sendNavigateToRenderer(url)
return true
}
export function getWindowState() {
if (!mainWindow) {
return { isFullScreen: false, isMaximized: false }
}
return {
isFullScreen: mainWindow.isFullScreen?.() ?? false,
isMaximized: mainWindow.isMaximized?.() ?? false
}
}
export function handleWindowControl(action) {
if (!mainWindow) return
switch (action) {
case 'minimize':
mainWindow.minimize?.()
break
case 'maximize':
if (mainWindow.isMaximized?.()) {
mainWindow.unmaximize?.()
} else {
mainWindow.maximize?.()
}
break
case 'fullscreen':
if (mainWindow.isFullScreen?.()) {
mainWindow.setFullScreen?.(false)
} else {
mainWindow.setFullScreen?.(true)
}
setTimeout(broadcastWindowState, 100)
break
case 'close':
mainWindow.close?.()
break
default:
break
}
}
export function sendNavigationGesture(direction) {
sendToRenderer('navigationGesture', direction)
}
export function setupNavigationGestures(window) {
if (!window) return
if (process.platform === 'darwin') {
window.on?.('swipe', (_event, direction) => {
if (direction === 'left') {
sendNavigationGesture('back')
} else if (direction === 'right') {
sendNavigationGesture('forward')
}
})
}
window.on?.('app-command', (_event, command) => {
if (command === 'browser-backward') {
sendNavigationGesture('back')
} else if (command === 'browser-forward') {
sendNavigationGesture('forward')
}
})
}
export function openExternalUrl(url) {
console.log('openExternalUrl', url)
Utils.openExternal(url)
}