All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Introduced a Windows pipe server to facilitate communication between instances, improving single instance management. - Updated `ensureSingleInstanceLock` to handle payload forwarding and signal writing more effectively. - Refactored deep link handling to utilize the new protocol detection logic, enhancing URL parsing and navigation. - Improved error handling and connection management for the Windows-specific implementation.
376 lines
8.5 KiB
JavaScript
376 lines
8.5 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}`
|
|
import { findProtocolUrl, setSingleInstanceHandlers } from './single-instance.js'
|
|
|
|
let mainWindow = null
|
|
let webviewDomReady = false
|
|
const pendingNavigations = []
|
|
|
|
export function getMainWindow() {
|
|
return mainWindow
|
|
}
|
|
|
|
export function showMainWindow() {
|
|
if (!mainWindow) return
|
|
|
|
if (mainWindow.isMinimized?.()) {
|
|
mainWindow.restore?.()
|
|
}
|
|
|
|
mainWindow.show?.()
|
|
mainWindow.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(redirectPath) {
|
|
sendToRenderer('navigate', redirectPath)
|
|
|
|
if (!mainWindow) return
|
|
|
|
if (mainWindow.isMinimized?.()) {
|
|
mainWindow.restore?.()
|
|
}
|
|
|
|
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 openInternalUrl(url) {
|
|
sendNavigateToRenderer(url)
|
|
return true
|
|
}
|
|
|
|
const PROTOCOL_PREFIX = 'farmcontrol://'
|
|
|
|
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 broadcastWindowState() {
|
|
sendToRenderer('windowState', getWindowState())
|
|
}
|
|
|
|
function applyStartupWindowState(window) {
|
|
if (!window) return
|
|
|
|
if (isMacOS || process.platform === 'win32') {
|
|
window.maximize?.()
|
|
}
|
|
|
|
setTimeout(broadcastWindowState, 100)
|
|
}
|
|
|
|
function syncWindowsWebviewLayout(window) {
|
|
if (!window?.getSize || !window?.setSize) {
|
|
return
|
|
}
|
|
|
|
const { width, height } = window.getSize()
|
|
if (!width || !height) {
|
|
return
|
|
}
|
|
|
|
// Re-apply the current size so the native webview relayouts to the window.
|
|
window.setSize(width, height)
|
|
}
|
|
|
|
function applyWindowsStartupWindowState(window) {
|
|
if (!window) return
|
|
|
|
syncWindowsWebviewLayout(window)
|
|
setTimeout(() => syncWindowsWebviewLayout(window), 0)
|
|
setTimeout(() => syncWindowsWebviewLayout(window), 100)
|
|
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
|
|
|
|
if (process.platform === 'win32') {
|
|
applyWindowsStartupWindowState(mainWindow)
|
|
}
|
|
|
|
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 setupWindowsDeepLinkHandling() {
|
|
if (process.platform === 'darwin') {
|
|
return
|
|
}
|
|
|
|
setSingleInstanceHandlers({
|
|
onDeepLink: handleDeepLink,
|
|
onFocus: showMainWindow
|
|
})
|
|
}
|
|
|
|
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)
|
|
}
|