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' const electron = window.require ? window.require('electron') : null const ipcRenderer = electron ? electron.ipcRenderer : null // eslint-disable-next-line react-refresh/only-export-components export function isElectron() { if (isElectrobunDesktop()) { return true } if ( typeof window !== 'undefined' && window.process && window.process.type === 'renderer' ) { return true } if ( typeof navigator === 'object' && typeof navigator.userAgent === 'string' && navigator.userAgent.indexOf('Electron') >= 0 ) { return true } return false } 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 useElectrobun = isElectrobunBridgeReady() || isElectrobunDesktop() 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 (useElectrobun) { void desktopBridge.openExternalUrl(url).catch((error) => { console.warn('[ElectronContext] Failed to open external url:', error) }) return true } if (electronAvailable && ipcRenderer) { ipcRenderer.invoke('open-external-url', url) return true } return false } const openInternalUrl = (url) => { if (useElectrobun) { void desktopBridge.openInternalUrl(url).catch((error) => { console.warn('[ElectronContext] Failed to open internal url:', error) }) return true } if (electronAvailable && ipcRenderer) { ipcRenderer.invoke('open-internal-url', url) return true } return false } useEffect(() => { if (!electronAvailable) return if (useElectrobun) { 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() } } if (!ipcRenderer) return ipcRenderer.invoke('os-info').then((info) => { if (info?.platform) { setPlatform(info.platform) if (info.platform === 'darwin') { document.documentElement.classList.add('macos-vibrancy') } } }) ipcRenderer.invoke('window-state').then(applyWindowState) const windowStateHandler = (_event, state) => { applyWindowState(state) } ipcRenderer.on('window-state', windowStateHandler) const navigateHandler = (_event, url) => { if (url.toLowerCase() == '/favicon.ico') { return } console.log('[ElectronContext] Navigating to:', url) navigate(url) } ipcRenderer.on('navigate', navigateHandler) const navigationGestureHandler = (_event, direction) => { navigateHistory(direction) } ipcRenderer.on('navigation-gesture', navigationGestureHandler) return () => { ipcRenderer.removeListener('navigate', navigateHandler) ipcRenderer.removeListener('navigation-gesture', navigationGestureHandler) ipcRenderer.removeListener('window-state', windowStateHandler) } }, [ applyWindowState, electronAvailable, navigate, navigateHistory, useElectrobun ]) 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 (useElectrobun) { void desktopBridge.windowControl(action) return } if (electronAvailable && ipcRenderer) { ipcRenderer.send('window-control', action) } } const getAuthSession = async () => { if (!electronAvailable) return null if (useElectrobun) return await desktopBridge.getAuthSession() if (!ipcRenderer) return null return await ipcRenderer.invoke('auth-session-get') } const setAuthSession = async (session) => { if (!electronAvailable) return false if (useElectrobun) { const result = await desktopBridge.setAuthSession(session) return result?.ok ?? false } if (!ipcRenderer) return false return await ipcRenderer.invoke('auth-session-set', session) } const clearAuthSession = async () => { if (!electronAvailable) return false if (useElectrobun) { const result = await desktopBridge.clearAuthSession() return result?.ok ?? false } if (!ipcRenderer) return false return await ipcRenderer.invoke('auth-session-clear') } const getAppSettings = useCallback(async () => { if (!electronAvailable) return {} if (useElectrobun) return await desktopBridge.getAppSettings() if (!ipcRenderer) return {} return await ipcRenderer.invoke('app-settings-get') }, [electronAvailable, useElectrobun]) const setAppSettings = useCallback( async (settings) => { if (!electronAvailable) return false if (useElectrobun) { const result = await desktopBridge.setAppSettings(settings) return result?.ok ?? false } if (!ipcRenderer) return false return await ipcRenderer.invoke('app-settings-set', settings) }, [electronAvailable, useElectrobun] ) const startAppUpdate = useCallback( async (update) => { if (!electronAvailable) return false if (useElectrobun) { const result = await desktopBridge.startAppUpdate(update) return result?.ok ?? false } if (!ipcRenderer) return false return await ipcRenderer.invoke('app-update-start', update) }, [electronAvailable, useElectrobun] ) const checkAppUpdateResult = useCallback(async () => { if (!electronAvailable || !useElectrobun) return null return await desktopBridge.checkAppUpdateResult() }, [electronAvailable, useElectrobun]) const checkDuplicateInstallations = useCallback(async () => { if (!electronAvailable || !useElectrobun) return null return await desktopBridge.checkDuplicateInstallations() }, [electronAvailable, useElectrobun]) const removeDuplicateInstallations = useCallback(async () => { if (!electronAvailable || !useElectrobun) return false const result = await desktopBridge.removeDuplicateInstallations() return result?.ok ?? false }, [electronAvailable, useElectrobun]) const onDuplicateInstallationsRemoved = useCallback( (handler) => { if ( !electronAvailable || !useElectrobun || typeof handler !== 'function' ) { return () => {} } return desktopBridge.onMessage('duplicateInstallationsRemoved', handler) }, [electronAvailable, useElectrobun] ) const onAppUpdateProgress = useCallback( (handler) => { if (!electronAvailable || typeof handler !== 'function') { return () => {} } if (useElectrobun) { return desktopBridge.onMessage('appUpdateProgress', handler) } if (!ipcRenderer) return () => {} const progressHandler = (_event, progress) => { handler(progress) } ipcRenderer.on('app-update-progress', progressHandler) return () => { ipcRenderer.removeListener('app-update-progress', progressHandler) } }, [electronAvailable, useElectrobun] ) const onCheckForUpdatesRequest = useCallback( (handler) => { if (!electronAvailable || typeof handler !== 'function') { return () => {} } if (useElectrobun) { return desktopBridge.onMessage('checkForUpdates', handler) } if (!ipcRenderer) return () => {} const checkHandler = () => { handler() } ipcRenderer.on('check-for-updates', checkHandler) return () => { ipcRenderer.removeListener('check-for-updates', checkHandler) } }, [electronAvailable, useElectrobun] ) 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 { if (useElectrobun) { const result = await desktopBridge.resizeSpotlightWindow(height) return result?.ok ?? false } if (!ipcRenderer) return false return await ipcRenderer.invoke('spotlight-window-resize', height) } catch (error) { console.warn( '[ElectronContext] Failed to resize spotlight window:', error ) return false } } const setSidebarViewMenu = useCallback( async (sections) => { if (!electronAvailable) return false if (useElectrobun) { const result = await desktopBridge.setSidebarViewMenu(sections) return result?.ok ?? false } if (!ipcRenderer) return false return await ipcRenderer.invoke('set-sidebar-view-menu', sections) }, [electronAvailable, useElectrobun] ) const getElectronVersion = useCallback(async () => { if (!electronAvailable) return null if (useElectrobun) return await desktopBridge.getAppVersion() if (!ipcRenderer) return null return await ipcRenderer.invoke('electron-version') }, [electronAvailable, useElectrobun]) const getAppEngine = useCallback(async () => { if (!electronAvailable) return 'native' if (useElectrobun) { const engine = await desktopBridge.getAppEngine() return engine === 'chromium' ? 'chromium' : 'native' } return 'native' }, [electronAvailable, useElectrobun]) return ( {children} ) } ElectronProvider.propTypes = { children: PropTypes.node.isRequired } export { ElectronContext, ElectronProvider }