farmcontrol-ui/src/electrobun-bridge.js
Tom Butcher 805361516e Enhance macOS window effects and improve Electrobun integration
- Adjusted CSS for macOS vibrancy, refining background opacity for light and dark modes.
- Introduced new styles for selected menu items in dark mode to enhance UI consistency.
- Added traffic light positioning and window chrome observer in native macOS window effects for better window management.
- Refactored Electron context to utilize the Electrobun bridge for improved API interactions and error handling.
- Streamlined the initialization process for the Electrobun bridge in the main application entry point.
2026-08-02 12:18:30 +01:00

192 lines
4.7 KiB
JavaScript

const listeners = new Map()
const pendingMessages = new Map()
let rpc = null
let initPromise = null
let initialized = false
export function isElectrobunDesktop() {
return Boolean(
typeof window !== 'undefined' &&
window.__electrobunWebviewId &&
window.__electrobunRpcSocketPort
)
}
function dispatchMessage(channel, data) {
const channelListeners = listeners.get(channel)
if (!channelListeners?.size) {
if (!pendingMessages.has(channel)) {
pendingMessages.set(channel, [])
}
pendingMessages.get(channel).push(data)
return
}
for (const callback of channelListeners) {
callback(data)
}
}
// Bun delivers push messages via executeJavascript for immediate handling.
// WebSocket RPC messages can be deferred by WKWebView during native window
// transitions (e.g. fullscreen) until the next user interaction.
if (typeof window !== 'undefined') {
window.__farmcontrolDispatchRpcMessage = dispatchMessage
}
async function setupRpc() {
// electrobun/view captures webview globals at import time — import only after
// Electrobun preload has set them (Vite dev can evaluate modules earlier).
const { default: Electrobun, Electroview } = await import('electrobun/view')
rpc = Electroview.defineRPC({
maxRequestTime: 30000,
handlers: {
requests: {},
messages: {
'*': (channel, data) => {
dispatchMessage(channel, data)
}
}
}
})
new Electrobun.Electroview({ rpc })
}
function shouldWaitForElectrobun() {
if (isElectrobunDesktop()) {
return true
}
if (typeof window === 'undefined') {
return false
}
if (window.__electrobunWebviewId) {
return true
}
const { protocol, hostname, port } = window.location
if (protocol === 'views:') {
return true
}
// Vite dev server used by electrobun dev:app
if (hostname === 'localhost' && port === '5780') {
return true
}
return Boolean(
window.__electrobun ||
window.__electrobunEventBridge ||
window.__electrobunInternalBridge
)
}
export async function initElectrobunBridge() {
if (initialized) {
return
}
if (!initPromise) {
initPromise = (async () => {
if (!shouldWaitForElectrobun()) {
return
}
const deadline = Date.now() + 3000
while (Date.now() < deadline) {
if (isElectrobunDesktop()) {
await setupRpc()
window.electronAPI = electronAPI
initialized = true
return
}
await new Promise((resolve) => setTimeout(resolve, 50))
}
console.warn(
'Electrobun bridge: webview globals not found; desktop RPC unavailable.'
)
})()
}
await initPromise
}
function onMessage(channel, callback) {
void initElectrobunBridge()
if (!listeners.has(channel)) {
listeners.set(channel, new Set())
}
listeners.get(channel).add(callback)
const queued = pendingMessages.get(channel)
if (queued?.length) {
pendingMessages.delete(channel)
for (const payload of queued) {
callback(payload)
}
}
return () => {
listeners.get(channel)?.delete(callback)
}
}
function removeAllListeners(channel) {
listeners.delete(channel)
pendingMessages.delete(channel)
}
async function invokeRequest(method, params = {}) {
await initElectrobunBridge()
if (!rpc?.request) {
console.warn(`Electrobun RPC unavailable for request: ${method}`)
return null
}
try {
return await rpc.request[method](params)
} catch (error) {
console.warn(`Electrobun RPC request failed: ${method}`, error)
return null
}
}
const electronAPI = {
get isDesktop() {
return isElectrobunDesktop()
},
onMessage,
removeAllListeners,
getOsInfo: () => invokeRequest('getOsInfo'),
getWindowState: () => invokeRequest('getWindowState'),
windowControl: (action) => invokeRequest('windowControl', { action }),
openExternalUrl: (url) => invokeRequest('openExternalUrl', { url }),
openInternalUrl: (url) => invokeRequest('openInternalUrl', { url }),
getAuthSession: () => invokeRequest('getAuthSession'),
setAuthSession: (session) => invokeRequest('setAuthSession', { session }),
clearAuthSession: () => invokeRequest('clearAuthSession'),
getAppSettings: () => invokeRequest('getAppSettings'),
setAppSettings: (settings) => invokeRequest('setAppSettings', { settings }),
startAppUpdate: (update) => invokeRequest('startAppUpdate', { update }),
resizeSpotlightWindow: (height) =>
invokeRequest('resizeSpotlightWindow', { height }),
setSidebarViewMenu: (sections) =>
invokeRequest('setSidebarViewMenu', { sections }),
getAppVersion: () => invokeRequest('getAppVersion')
}
if (typeof window !== 'undefined') {
window.electronAPI = electronAPI
}
export default electronAPI