Tom Butcher 35ce3c39af
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
Refactor ElectronContext for improved Electron detection and streamlined URL handling
- Simplified the `isElectron` function to directly return the result of `isElectrobunBridgeReady` or `isElectrobunDesktop`.
- Removed redundant checks and consolidated URL opening logic to enhance clarity and reduce code duplication.
- Updated event handling for Electron-specific features to utilize the desktopBridge API, improving maintainability and performance.
2026-08-09 12:09:45 +01:00

331 lines
9.1 KiB
JavaScript

import { createContext, useCallback, useEffect, useRef, useState } from 'react'
import PropTypes from 'prop-types'
import { useNavigate } from 'react-router-dom'
import desktopBridge, {
isElectrobunBridgeReady,
isElectrobunDesktop
} from '../../../electrobun-bridge.js'
// eslint-disable-next-line react-refresh/only-export-components
export function isElectron() {
return isElectrobunBridgeReady() || isElectrobunDesktop()
}
const ElectronContext = createContext()
function isInHorizontalScrollContainer(element) {
let node = element
while (node && node !== document.body) {
const { overflowX } = window.getComputedStyle(node)
if (
(overflowX === 'auto' || overflowX === 'scroll') &&
node.scrollWidth > node.clientWidth
) {
return true
}
node = node.parentElement
}
return false
}
const ElectronProvider = ({ children }) => {
const [platform, setPlatform] = useState('unknown')
const [isMaximized, setIsMaximized] = useState(false)
const [isFullScreen, setIsFullScreen] = useState(false)
const [electronAvailable] = useState(isElectron())
const navigate = useNavigate()
const lastNavigationAtRef = useRef(0)
const navigateHistory = useCallback(
(direction) => {
const now = Date.now()
if (now - lastNavigationAtRef.current < 300) return
lastNavigationAtRef.current = now
if (direction === 'back') {
navigate(-1)
} else if (direction === 'forward') {
navigate(1)
}
},
[navigate]
)
const applyWindowState = useCallback((state) => {
if (state && typeof state.isMaximized === 'boolean') {
setIsMaximized(state.isMaximized)
}
if (state && typeof state.isFullScreen === 'boolean') {
setIsFullScreen(state.isFullScreen)
}
}, [])
const openExternalUrl = (url) => {
if (!electronAvailable) return false
void desktopBridge.openExternalUrl(url).catch((error) => {
console.warn('[ElectronContext] Failed to open external url:', error)
})
return true
}
const openInternalUrl = (url) => {
if (!electronAvailable) return false
void desktopBridge.openInternalUrl(url).catch((error) => {
console.warn('[ElectronContext] Failed to open internal url:', error)
})
return true
}
useEffect(() => {
if (!electronAvailable) return
document.body.classList.add('electron-body')
return () => {
document.body.classList.remove('electron-body')
}
}, [electronAvailable])
useEffect(() => {
if (!electronAvailable) return
desktopBridge.getOsInfo().then((info) => {
if (info?.platform) {
setPlatform(info.platform)
if (info.platform === 'darwin') {
document.documentElement.classList.add('macos-vibrancy')
}
}
})
desktopBridge.getWindowState().then(applyWindowState)
const unsubWindowState = desktopBridge.onMessage(
'windowState',
applyWindowState
)
const unsubNavigate = desktopBridge.onMessage('navigate', (url) => {
if (url.toLowerCase() == '/favicon.ico') {
return
}
console.log('[ElectronContext] Navigating to:', url)
navigate(url)
})
const unsubNavigationGesture = desktopBridge.onMessage(
'navigationGesture',
navigateHistory
)
return () => {
unsubWindowState()
unsubNavigate()
unsubNavigationGesture()
}
}, [applyWindowState, electronAvailable, navigate, navigateHistory])
useEffect(() => {
if (!electronAvailable || platform !== 'darwin') return
let accumulatedDeltaX = 0
let resetTimer
const handleWheel = (event) => {
if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey)
return
if (Math.abs(event.deltaX) < Math.abs(event.deltaY)) return
if (Math.abs(event.deltaX) < 2) return
if (isInHorizontalScrollContainer(event.target)) return
accumulatedDeltaX += event.deltaX
clearTimeout(resetTimer)
resetTimer = setTimeout(() => {
if (Math.abs(accumulatedDeltaX) > 60) {
navigateHistory(accumulatedDeltaX > 0 ? 'forward' : 'back')
}
accumulatedDeltaX = 0
}, 80)
}
window.addEventListener('wheel', handleWheel, { passive: true })
return () => {
window.removeEventListener('wheel', handleWheel)
clearTimeout(resetTimer)
}
}, [electronAvailable, navigateHistory, platform])
const handleWindowControl = (action) => {
if (!electronAvailable) return
void desktopBridge.windowControl(action)
}
const getAuthSession = async () => {
if (!electronAvailable) return null
return await desktopBridge.getAuthSession()
}
const setAuthSession = async (session) => {
if (!electronAvailable) return false
const result = await desktopBridge.setAuthSession(session)
return result?.ok ?? false
}
const clearAuthSession = async () => {
if (!electronAvailable) return false
const result = await desktopBridge.clearAuthSession()
return result?.ok ?? false
}
const getAppSettings = useCallback(async () => {
if (!electronAvailable) return {}
return await desktopBridge.getAppSettings()
}, [electronAvailable])
const setAppSettings = useCallback(
async (settings) => {
if (!electronAvailable) return false
const result = await desktopBridge.setAppSettings(settings)
return result?.ok ?? false
},
[electronAvailable]
)
const startAppUpdate = useCallback(
async (update) => {
if (!electronAvailable) return false
const result = await desktopBridge.startAppUpdate(update)
return result?.ok ?? false
},
[electronAvailable]
)
const checkAppUpdateResult = useCallback(async () => {
if (!electronAvailable) return null
return await desktopBridge.checkAppUpdateResult()
}, [electronAvailable])
const checkDuplicateInstallations = useCallback(async () => {
if (!electronAvailable) return null
return await desktopBridge.checkDuplicateInstallations()
}, [electronAvailable])
const removeDuplicateInstallations = useCallback(async () => {
if (!electronAvailable) return false
const result = await desktopBridge.removeDuplicateInstallations()
return result?.ok ?? false
}, [electronAvailable])
const onDuplicateInstallationsRemoved = useCallback(
(handler) => {
if (!electronAvailable || typeof handler !== 'function') {
return () => {}
}
return desktopBridge.onMessage('duplicateInstallationsRemoved', handler)
},
[electronAvailable]
)
const onAppUpdateProgress = useCallback(
(handler) => {
if (!electronAvailable || typeof handler !== 'function') {
return () => {}
}
return desktopBridge.onMessage('appUpdateProgress', handler)
},
[electronAvailable]
)
const onCheckForUpdatesRequest = useCallback(
(handler) => {
if (!electronAvailable || typeof handler !== 'function') {
return () => {}
}
return desktopBridge.onMessage('checkForUpdates', handler)
},
[electronAvailable]
)
const getToken = async () => {
const session = await getAuthSession()
return session?.token || null
}
const setToken = async (token) => {
const session = (await getAuthSession()) || {}
return await setAuthSession({ ...session, token })
}
const resizeSpotlightWindow = async (height) => {
if (!electronAvailable) return false
try {
const result = await desktopBridge.resizeSpotlightWindow(height)
return result?.ok ?? false
} catch (error) {
console.warn(
'[ElectronContext] Failed to resize spotlight window:',
error
)
return false
}
}
const setSidebarViewMenu = useCallback(
async (sections) => {
if (!electronAvailable) return false
const result = await desktopBridge.setSidebarViewMenu(sections)
return result?.ok ?? false
},
[electronAvailable]
)
const getElectronVersion = useCallback(async () => {
if (!electronAvailable) return null
return await desktopBridge.getAppVersion()
}, [electronAvailable])
const getAppEngine = useCallback(async () => {
if (!electronAvailable) return 'native'
const engine = await desktopBridge.getAppEngine()
return engine === 'chromium' ? 'chromium' : 'native'
}, [electronAvailable])
return (
<ElectronContext.Provider
value={{
platform,
isMaximized,
isFullScreen,
isElectron: electronAvailable,
handleWindowControl,
openExternalUrl,
openInternalUrl,
getAuthSession,
setAuthSession,
clearAuthSession,
getAppSettings,
setAppSettings,
startAppUpdate,
checkAppUpdateResult,
checkDuplicateInstallations,
removeDuplicateInstallations,
onDuplicateInstallationsRemoved,
onAppUpdateProgress,
onCheckForUpdatesRequest,
getToken,
setToken,
resizeSpotlightWindow,
setSidebarViewMenu,
getElectronVersion,
getAppEngine
}}
>
{children}
</ElectronContext.Provider>
)
}
ElectronProvider.propTypes = {
children: PropTypes.node.isRequired
}
export { ElectronContext, ElectronProvider }