Compare commits
1 Commits
main
...
electrobun
| Author | SHA1 | Date | |
|---|---|---|---|
| 550f0eca06 |
4805
dist-test/index.js
Normal file
4805
dist-test/index.js
Normal file
File diff suppressed because it is too large
Load Diff
31
electrobun.config.ts
Normal file
31
electrobun.config.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import type { ElectrobunConfig } from 'electrobun'
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
const packageJson = JSON.parse(readFileSync('./package.json', 'utf8'))
|
||||
|
||||
export default {
|
||||
app: {
|
||||
name: 'Farm Control',
|
||||
identifier: 'com.tombutcher.farmcontrol',
|
||||
version: packageJson.version,
|
||||
urlSchemes: ['farmcontrol']
|
||||
},
|
||||
runtime: {
|
||||
exitOnLastWindowClosed: true
|
||||
},
|
||||
build: {
|
||||
bun: {
|
||||
entrypoint: 'src/bun/index.ts'
|
||||
},
|
||||
views: {
|
||||
preload: {
|
||||
entrypoint: 'src/preload/index.ts'
|
||||
}
|
||||
},
|
||||
copy: {
|
||||
'build/index.html': 'views/main/index.html',
|
||||
'build/assets': 'views/main/assets'
|
||||
},
|
||||
watch: ['build']
|
||||
}
|
||||
} satisfies ElectrobunConfig
|
||||
14
package.json
14
package.json
@ -41,7 +41,6 @@
|
||||
"dotenv": "^17.2.1",
|
||||
"gcode-preview": "^2.18.0",
|
||||
"keycloak-js": "^26.2.0",
|
||||
"keytar": "^7.9.0",
|
||||
"lodash": "^4.17.23",
|
||||
"loglevel": "^1.9.2",
|
||||
"online-3d-viewer": "^0.16.0",
|
||||
@ -63,15 +62,15 @@
|
||||
"tsparticles": "^3.9.1",
|
||||
"web-vitals": "^5.1.0"
|
||||
},
|
||||
"main": "build/electron.js",
|
||||
"main": "src/bun/index.ts",
|
||||
"description": "3D Printer ERP and Control Software.",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development vite",
|
||||
"electron": "cross-env ELECTRON_START_URL=http://0.0.0.0:5780 && cross-env NODE_ENV=development && electron .",
|
||||
"electrobun": "cross-env ELECTROBUN_START_URL=http://localhost:5173 electrobun dev",
|
||||
"start": "serve -s build",
|
||||
"build": "vite build",
|
||||
"dev:electron": "concurrently \"cross-env NODE_ENV=development vite --no-open\" \"cross-env ELECTRON_START_URL=http://localhost:5780 cross-env NODE_ENV=development electron public/electron.js\"",
|
||||
"build:electron": "vite build && electron-builder",
|
||||
"dev:electrobun": "concurrently \"cross-env NODE_ENV=development vite --no-open\" \"cross-env ELECTROBUN_START_URL=http://localhost:5173 electrobun dev\"",
|
||||
"build:electrobun": "vite build && electrobun build",
|
||||
"build:cloudflare": "cross-env VITE_DEPLOY_TARGET=cloudflare vite build",
|
||||
"deploy": "npm run build:cloudflare && wrangler pages deploy --branch main"
|
||||
},
|
||||
@ -97,9 +96,8 @@
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"concurrently": "^9.2.1",
|
||||
"electron": "^38.7.1",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-packager": "^17.1.2",
|
||||
"electrobun": "^1.13.1",
|
||||
"typescript": "^5.0.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
|
||||
@ -25,7 +25,7 @@ import {
|
||||
import AppError from './components/App/AppError'
|
||||
import { ApiServerProvider } from './components/Dashboard/context/ApiServerContext.jsx'
|
||||
import { NotificationProvider } from './components/Dashboard/context/NotificationContext.jsx'
|
||||
import { ElectronProvider } from './components/Dashboard/context/ElectronContext.jsx'
|
||||
import { ElectrobunProvider } from './components/Dashboard/context/ElectrobunContext.jsx'
|
||||
import { MessageProvider } from './components/Dashboard/context/MessageContext.jsx'
|
||||
import AuthCallback from './components/App/AuthCallback.jsx'
|
||||
|
||||
@ -56,7 +56,7 @@ const AppContent = () => {
|
||||
<ConfigProvider theme={themeConfig}>
|
||||
<App>
|
||||
<Router>
|
||||
<ElectronProvider>
|
||||
<ElectrobunProvider>
|
||||
<AuthProvider>
|
||||
<PrintServerProvider>
|
||||
<ApiServerProvider>
|
||||
@ -122,7 +122,7 @@ const AppContent = () => {
|
||||
</ApiServerProvider>
|
||||
</PrintServerProvider>
|
||||
</AuthProvider>
|
||||
</ElectronProvider>
|
||||
</ElectrobunProvider>
|
||||
</Router>
|
||||
</App>
|
||||
</ConfigProvider>
|
||||
|
||||
157
src/bun/index.ts
Normal file
157
src/bun/index.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import Electrobun, {
|
||||
BrowserWindow,
|
||||
BrowserView,
|
||||
Utils,
|
||||
GlobalShortcut,
|
||||
ApplicationMenu
|
||||
} from 'electrobun/bun'
|
||||
import { join } from 'path'
|
||||
import { type FarmControlRPCType } from '../shared/rpcTypes'
|
||||
import { createMainWindow, getMainWindow } from './mainWindow'
|
||||
import {
|
||||
openSpotlightContentWindow,
|
||||
registerGlobalShortcuts,
|
||||
setupSpotlightRPC
|
||||
} from './spotlightWindow'
|
||||
|
||||
// Auth session storage (file-based, keytar alternative for Bun)
|
||||
const AUTH_FILE = join(Utils.paths.userData, 'auth-session.json')
|
||||
|
||||
async function readAuthSession(): Promise<object | null> {
|
||||
try {
|
||||
const f = Bun.file(AUTH_FILE)
|
||||
if (!(await f.exists())) return null
|
||||
const raw = await f.text()
|
||||
return JSON.parse(raw) as object
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function writeAuthSession(session: object): Promise<boolean> {
|
||||
try {
|
||||
await Bun.write(AUTH_FILE, JSON.stringify(session))
|
||||
return true
|
||||
} catch (e) {
|
||||
console.warn('[auth] Failed to write session:', e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function clearAuthSession(): Promise<boolean> {
|
||||
try {
|
||||
const f = Bun.file(AUTH_FILE)
|
||||
if (await f.exists()) {
|
||||
await Bun.write(AUTH_FILE, '{}')
|
||||
}
|
||||
return true
|
||||
} catch (e) {
|
||||
console.warn('[auth] Failed to clear session:', e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const farmControlRPC = BrowserView.defineRPC<FarmControlRPCType>({
|
||||
maxRequestTime: 5000,
|
||||
handlers: {
|
||||
requests: {
|
||||
osInfo: () => ({ platform: process.platform }),
|
||||
windowState: () => {
|
||||
const win = getMainWindow()
|
||||
if (!win || win.isDestroyed?.()) {
|
||||
return { isFullScreen: false, isMaximized: false }
|
||||
}
|
||||
return {
|
||||
isFullScreen: win.isFullScreen?.(),
|
||||
isMaximized: win.isMaximized?.()
|
||||
}
|
||||
},
|
||||
openExternalUrl: ({ url }) => {
|
||||
Utils.openExternal(url)
|
||||
},
|
||||
openInternalUrl: ({ url }) => {
|
||||
const win = getMainWindow()
|
||||
if (!win || win.isDestroyed?.()) {
|
||||
createMainWindow(farmControlRPC)
|
||||
const newWin = getMainWindow()
|
||||
if (newWin?.webview) {
|
||||
newWin.webview.on?.('dom-ready', () => {
|
||||
;(newWin.webview as any).rpc?.navigate?.({ url })
|
||||
newWin.show?.()
|
||||
newWin.focus?.()
|
||||
})
|
||||
}
|
||||
} else if (win.webview) {
|
||||
;(win.webview as any).rpc?.navigate?.({ url })
|
||||
win.show?.()
|
||||
win.focus?.()
|
||||
}
|
||||
return true
|
||||
},
|
||||
windowControl: ({ action }) => {
|
||||
const win = getMainWindow()
|
||||
if (!win) return
|
||||
switch (action) {
|
||||
case 'minimize':
|
||||
win.minimize?.()
|
||||
break
|
||||
case 'maximize':
|
||||
if (win.isMaximized?.()) {
|
||||
win.unmaximize?.()
|
||||
} else {
|
||||
win.maximize?.()
|
||||
}
|
||||
break
|
||||
case 'close':
|
||||
win.close?.()
|
||||
break
|
||||
}
|
||||
},
|
||||
authSessionGet: () => readAuthSession(),
|
||||
authSessionSet: ({ session }) => writeAuthSession(session),
|
||||
authSessionClear: () => clearAuthSession(),
|
||||
spotlightWindowResize: ({ height }) => setupSpotlightRPC(height)
|
||||
},
|
||||
messages: {}
|
||||
}
|
||||
})
|
||||
|
||||
createMainWindow(farmControlRPC)
|
||||
registerGlobalShortcuts()
|
||||
|
||||
// Application menu
|
||||
const env = (process.env.NODE_ENV || 'development').trim()
|
||||
if (env === 'development') {
|
||||
ApplicationMenu.setApplicationMenu([
|
||||
{
|
||||
label: 'Developer',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Toggle Developer Tools',
|
||||
accelerator:
|
||||
process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
|
||||
action: 'toggle-devtools'
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
} else {
|
||||
ApplicationMenu.setApplicationMenu([])
|
||||
}
|
||||
|
||||
// open-url for farmcontrol:// scheme
|
||||
Electrobun.events.on('open-url', (e: { data: { url: string } }) => {
|
||||
const url = e.data.url
|
||||
if (url.startsWith('farmcontrol://app')) {
|
||||
const redirectPath = url.replace('farmcontrol://app', '') || '/'
|
||||
const win = getMainWindow()
|
||||
if (win?.webview) {
|
||||
;(win.webview as any).rpc?.navigate?.({ url: redirectPath })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Unregister shortcuts on quit
|
||||
Electrobun.events.on('before-quit', () => {
|
||||
GlobalShortcut.unregisterAll()
|
||||
})
|
||||
90
src/bun/mainWindow.ts
Normal file
90
src/bun/mainWindow.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import Electrobun, { BrowserWindow, Utils } from 'electrobun/bun'
|
||||
import { join } from 'path'
|
||||
import type { FarmControlRPCType } from '../shared/rpcTypes'
|
||||
|
||||
let win: InstanceType<typeof BrowserWindow> | null = null
|
||||
|
||||
function getAppUrl(): string {
|
||||
if (process.env.ELECTROBUN_START_URL || process.env.ELECTRON_START_URL) {
|
||||
return (
|
||||
process.env.ELECTROBUN_START_URL ||
|
||||
process.env.ELECTRON_START_URL ||
|
||||
'http://localhost:5173'
|
||||
)
|
||||
}
|
||||
return 'views://main/index.html'
|
||||
}
|
||||
|
||||
function setupWindowEvents(
|
||||
browserWindow: InstanceType<typeof BrowserWindow>,
|
||||
rpc: any
|
||||
): void {
|
||||
if (!browserWindow) return
|
||||
|
||||
browserWindow.on?.('resize', () => {
|
||||
const webview = browserWindow.webview
|
||||
if (webview?.rpc) {
|
||||
try {
|
||||
;(webview.rpc as any).windowState?.({
|
||||
isMaximized: browserWindow.isMaximized?.(),
|
||||
isFullScreen: browserWindow.isFullScreen?.()
|
||||
})
|
||||
} catch (_) {}
|
||||
}
|
||||
})
|
||||
|
||||
// Electrobun may use different event names - check docs
|
||||
const sendWindowState = (state: { isMaximized?: boolean; isFullScreen?: boolean }) => {
|
||||
const webview = browserWindow.webview
|
||||
if (webview?.rpc) {
|
||||
try {
|
||||
;(webview.rpc as any).windowState?.(state)
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for maximize/unmaximize/fullscreen - Electrobun BrowserWindow events
|
||||
browserWindow.on?.('maximize', () => sendWindowState({ isMaximized: true }))
|
||||
browserWindow.on?.('unmaximize', () => sendWindowState({ isMaximized: false }))
|
||||
browserWindow.on?.('enter-full-screen', () => sendWindowState({ isFullScreen: true }))
|
||||
browserWindow.on?.('leave-full-screen', () => sendWindowState({ isFullScreen: false }))
|
||||
}
|
||||
|
||||
export function createMainWindow(rpc: any): void {
|
||||
const url = getAppUrl()
|
||||
|
||||
win = new BrowserWindow({
|
||||
title: 'Farm Control',
|
||||
url,
|
||||
frame: {
|
||||
width: 1200,
|
||||
height: 800
|
||||
},
|
||||
titleBarStyle: 'hiddenInset',
|
||||
preload: 'views://preload/index.js',
|
||||
rpc,
|
||||
styleMask: {
|
||||
Titled: true,
|
||||
Closable: true,
|
||||
Miniaturizable: true,
|
||||
Resizable: true,
|
||||
FullSizeContentView: true
|
||||
}
|
||||
})
|
||||
|
||||
setupWindowEvents(win, rpc)
|
||||
|
||||
// Handle window-all-closed
|
||||
Electrobun.events.on('close', () => {
|
||||
const wins = (BrowserWindow as any).getAll?.() ?? []
|
||||
if (wins.length <= 1) {
|
||||
if (process.platform !== 'darwin') {
|
||||
Utils.quit?.()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function getMainWindow(): InstanceType<typeof BrowserWindow> | null {
|
||||
return win
|
||||
}
|
||||
80
src/bun/spotlightWindow.ts
Normal file
80
src/bun/spotlightWindow.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { BrowserWindow, GlobalShortcut } from 'electrobun/bun'
|
||||
import { join } from 'path'
|
||||
|
||||
let spotlightWin: InstanceType<typeof BrowserWindow> | null = null
|
||||
|
||||
function getSpotlightRouteUrl(): string {
|
||||
const routePath = '/dashboard/electron/spotlightcontent'
|
||||
|
||||
if (process.env.ELECTROBUN_START_URL || process.env.ELECTRON_START_URL) {
|
||||
const base = String(
|
||||
process.env.ELECTROBUN_START_URL || process.env.ELECTRON_START_URL
|
||||
).replace(/\/$/, '')
|
||||
return `${base}${routePath}`
|
||||
}
|
||||
|
||||
return `views://main/index.html#${routePath}`
|
||||
}
|
||||
|
||||
export function openSpotlightContentWindow(): void {
|
||||
if (spotlightWin && !spotlightWin.isDestroyed?.()) {
|
||||
spotlightWin.show?.()
|
||||
spotlightWin.focus?.()
|
||||
return
|
||||
}
|
||||
|
||||
const target = getSpotlightRouteUrl()
|
||||
|
||||
spotlightWin = new BrowserWindow({
|
||||
title: 'Spotlight',
|
||||
url: target,
|
||||
frame: {
|
||||
width: 700,
|
||||
height: 40
|
||||
},
|
||||
titleBarStyle: 'hidden',
|
||||
transparent: true,
|
||||
resizable: false
|
||||
})
|
||||
|
||||
spotlightWin.on?.('close', (e: any) => {
|
||||
if (e?.preventDefault) e.preventDefault()
|
||||
if (spotlightWin && !spotlightWin.isDestroyed?.()) {
|
||||
spotlightWin.hide?.()
|
||||
}
|
||||
})
|
||||
|
||||
spotlightWin.on?.('blur', () => {
|
||||
if (spotlightWin && !spotlightWin.isDestroyed?.()) {
|
||||
spotlightWin.hide?.()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function registerGlobalShortcuts(): void {
|
||||
try {
|
||||
const success = GlobalShortcut.register('Alt+Shift+Q', () => {
|
||||
openSpotlightContentWindow()
|
||||
})
|
||||
if (!success) {
|
||||
console.warn('[GlobalShortcut] Failed to register Alt+Shift+Q')
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[GlobalShortcut] Error:', (e as Error)?.message)
|
||||
}
|
||||
}
|
||||
|
||||
export function setupSpotlightRPC(height: number): boolean {
|
||||
if (!spotlightWin || spotlightWin.isDestroyed?.()) return false
|
||||
try {
|
||||
const frame = spotlightWin.getFrame?.()
|
||||
if (frame) {
|
||||
spotlightWin.setSize?.(frame.width, height)
|
||||
spotlightWin.center?.()
|
||||
}
|
||||
return true
|
||||
} catch (e) {
|
||||
console.warn('[spotlight] Failed to resize:', (e as Error)?.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -39,7 +39,7 @@ import BellIcon from '../../Icons/BellIcon'
|
||||
import SearchIcon from '../../Icons/SearchIcon'
|
||||
import SettingsIcon from '../../Icons/SettingsIcon'
|
||||
import DeveloperIcon from '../../Icons/DeveloperIcon'
|
||||
import { ElectronContext } from '../context/ElectronContext'
|
||||
import { ElectrobunContext } from '../context/ElectrobunContext'
|
||||
import DashboardWindowButtons from './DashboardWindowButtons'
|
||||
|
||||
const { Text } = Typography
|
||||
@ -60,7 +60,7 @@ const DashboardNavigation = () => {
|
||||
icon: <ProductionIcon />
|
||||
})
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const { platform, isElectron, isFullScreen } = useContext(ElectronContext)
|
||||
const { platform, isElectron, isFullScreen } = useContext(ElectrobunContext)
|
||||
|
||||
const mainMenuItems = useMemo(
|
||||
() => [
|
||||
|
||||
@ -6,7 +6,7 @@ import ExpandSidebarIcon from '../../Icons/ExpandSidebarIcon'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ElectronContext } from '../context/ElectronContext'
|
||||
import { ElectrobunContext } from '../context/ElectrobunContext'
|
||||
const { Sider } = Layout
|
||||
|
||||
const DashboardSidebar = ({
|
||||
@ -24,7 +24,7 @@ const DashboardSidebar = ({
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { isElectron } = useContext(ElectronContext)
|
||||
const { isElectron } = useContext(ElectrobunContext)
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof collapsedProp === 'boolean') {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useContext } from 'react'
|
||||
import { Flex, Button } from 'antd'
|
||||
import { ElectronContext } from '../context/ElectronContext'
|
||||
import { ElectrobunContext } from '../context/ElectrobunContext'
|
||||
import XMarkIcon from '../../Icons/XMarkIcon'
|
||||
import MinusIcon from '../../Icons/MinusIcon'
|
||||
import ContractIcon from '../../Icons/ContractIcon'
|
||||
@ -8,7 +8,7 @@ import ExpandIcon from '../../Icons/ExpandIcon'
|
||||
|
||||
const DashboardWindowButtons = () => {
|
||||
const { isMaximized, handleWindowControl, platform, isFullScreen } =
|
||||
useContext(ElectronContext)
|
||||
useContext(ElectrobunContext)
|
||||
|
||||
const closeButton = (
|
||||
<Button
|
||||
|
||||
@ -40,7 +40,7 @@ import CheckIcon from '../../Icons/CheckIcon'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import { ElectronContext } from '../context/ElectronContext'
|
||||
import { ElectrobunContext } from '../context/ElectrobunContext'
|
||||
import ActionsIcon from '../../Icons/ActionsIcon'
|
||||
|
||||
const logger = loglevel.getLogger('DasboardTable')
|
||||
@ -103,7 +103,7 @@ const ObjectTable = forwardRef(
|
||||
ref
|
||||
) => {
|
||||
const { token } = useContext(AuthContext)
|
||||
const { isElectron } = useContext(ElectronContext)
|
||||
const { isElectron } = useContext(ElectrobunContext)
|
||||
const onStateChangeRef = useRef(onStateChange)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -22,7 +22,7 @@ import ExclamationOctogonIcon from '../../Icons/ExclamationOctagonIcon'
|
||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
||||
import config from '../../../config'
|
||||
import loglevel from 'loglevel'
|
||||
import { ElectronContext } from './ElectronContext'
|
||||
import { ElectrobunContext } from './ElectrobunContext'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
getAuthCookies,
|
||||
@ -63,7 +63,7 @@ const AuthProvider = ({ children }) => {
|
||||
getAuthSession,
|
||||
setAuthSession,
|
||||
clearAuthSession
|
||||
} = useContext(ElectronContext)
|
||||
} = useContext(ElectrobunContext)
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
|
||||
166
src/components/Dashboard/context/ElectrobunContext.jsx
Normal file
166
src/components/Dashboard/context/ElectrobunContext.jsx
Normal file
@ -0,0 +1,166 @@
|
||||
import { createContext, useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
// Electrobun RPC - only available when running in Electrobun
|
||||
const getRpc = () =>
|
||||
typeof window !== 'undefined' && window.__ELECTROBUN_RPC__ ? window.__ELECTROBUN_RPC__ : null
|
||||
|
||||
// Utility to check if running in Electrobun
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function isElectrobun() {
|
||||
if (typeof window === 'undefined') return false
|
||||
if (getRpc()) return true
|
||||
if (
|
||||
typeof navigator === 'object' &&
|
||||
typeof navigator.userAgent === 'string' &&
|
||||
navigator.userAgent.toLowerCase().includes('electrobun')
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const ElectrobunContext = createContext()
|
||||
|
||||
const ElectrobunProvider = ({ children }) => {
|
||||
const [platform, setPlatform] = useState('unknown')
|
||||
const [isMaximized, setIsMaximized] = useState(false)
|
||||
const [isFullScreen, setIsFullScreen] = useState(false)
|
||||
const [electrobunAvailable] = useState(isElectrobun())
|
||||
const navigate = useNavigate()
|
||||
|
||||
const rpc = getRpc()
|
||||
|
||||
// Function to open external URL via Electrobun
|
||||
const openExternalUrl = (url) => {
|
||||
if (electrobunAvailable && rpc?.request?.openExternalUrl) {
|
||||
rpc.request.openExternalUrl({ url })
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Function to open internal URL via Electrobun
|
||||
const openInternalUrl = (url) => {
|
||||
if (electrobunAvailable && rpc?.request?.openInternalUrl) {
|
||||
return rpc.request.openInternalUrl({ url })
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!rpc) return
|
||||
|
||||
// Get initial platform
|
||||
rpc.request?.osInfo?.()?.then?.((info) => {
|
||||
if (info?.platform) setPlatform(info.platform)
|
||||
})
|
||||
|
||||
// Get initial window state
|
||||
rpc.request?.windowState?.()?.then?.((state) => {
|
||||
if (state && typeof state.isMaximized === 'boolean') {
|
||||
setIsMaximized(state.isMaximized)
|
||||
}
|
||||
if (state && typeof state.isFullScreen === 'boolean') {
|
||||
setIsFullScreen(state.isFullScreen)
|
||||
}
|
||||
})
|
||||
|
||||
// Register for window state updates from bun
|
||||
const unregister =
|
||||
window.__ELECTROBUN_REGISTER__?.('windowState', (state) => {
|
||||
if (state && typeof state.isMaximized === 'boolean') {
|
||||
setIsMaximized(state.isMaximized)
|
||||
}
|
||||
if (state && typeof state.isFullScreen === 'boolean') {
|
||||
setIsFullScreen(state.isFullScreen)
|
||||
}
|
||||
})
|
||||
|
||||
// Register for navigate from bun
|
||||
const unregisterNav =
|
||||
window.__ELECTROBUN_REGISTER__?.('navigate', (url) => {
|
||||
navigate(url)
|
||||
})
|
||||
|
||||
return () => {
|
||||
unregister?.()
|
||||
unregisterNav?.()
|
||||
}
|
||||
}, [navigate, rpc])
|
||||
|
||||
// Window control handler
|
||||
const handleWindowControl = (action) => {
|
||||
if (electrobunAvailable && rpc?.request?.windowControl) {
|
||||
rpc.request.windowControl({ action })
|
||||
}
|
||||
}
|
||||
|
||||
const getAuthSession = async () => {
|
||||
if (!electrobunAvailable || !rpc?.request?.authSessionGet) return null
|
||||
return await rpc.request.authSessionGet()
|
||||
}
|
||||
|
||||
const setAuthSession = async (session) => {
|
||||
if (!electrobunAvailable || !rpc?.request?.authSessionSet) return false
|
||||
return await rpc.request.authSessionSet({ session })
|
||||
}
|
||||
|
||||
const clearAuthSession = async () => {
|
||||
if (!electrobunAvailable || !rpc?.request?.authSessionClear) return false
|
||||
return await rpc.request.authSessionClear()
|
||||
}
|
||||
|
||||
// Backwards-compatible helpers
|
||||
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 (!electrobunAvailable || !rpc?.request?.spotlightWindowResize) return false
|
||||
try {
|
||||
return await rpc.request.spotlightWindowResize({ height })
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'[ElectrobunContext] Failed to resize spotlight window:',
|
||||
error
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ElectrobunContext.Provider
|
||||
value={{
|
||||
platform,
|
||||
isMaximized,
|
||||
isFullScreen,
|
||||
isElectron: electrobunAvailable,
|
||||
handleWindowControl,
|
||||
openExternalUrl,
|
||||
openInternalUrl,
|
||||
getAuthSession,
|
||||
setAuthSession,
|
||||
clearAuthSession,
|
||||
getToken,
|
||||
setToken,
|
||||
resizeSpotlightWindow
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ElectrobunContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
ElectrobunProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired
|
||||
}
|
||||
|
||||
export { ElectrobunContext, ElectrobunProvider }
|
||||
@ -1,174 +0,0 @@
|
||||
import { createContext, useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
// Only available in Electron renderer
|
||||
const electron = window.require ? window.require('electron') : null
|
||||
const ipcRenderer = electron ? electron.ipcRenderer : null
|
||||
|
||||
// Utility to check if running in Electron
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function isElectron() {
|
||||
// Renderer process
|
||||
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
window.process &&
|
||||
window.process.type === 'renderer'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// User agent
|
||||
if (
|
||||
typeof navigator === 'object' &&
|
||||
typeof navigator.userAgent === 'string' &&
|
||||
navigator.userAgent.indexOf('Electron') >= 0
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const ElectronContext = createContext()
|
||||
|
||||
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()
|
||||
|
||||
// Function to open external URL via Electron
|
||||
const openExternalUrl = (url) => {
|
||||
if (electronAvailable && ipcRenderer) {
|
||||
ipcRenderer.invoke('open-external-url', url)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Function to open internal URL via Electron
|
||||
const openInternalUrl = (url) => {
|
||||
if (electronAvailable && ipcRenderer) {
|
||||
ipcRenderer.invoke('open-internal-url', url)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!ipcRenderer) return
|
||||
|
||||
// Get initial platform
|
||||
ipcRenderer.invoke('os-info').then((info) => {
|
||||
if (info && info.platform) setPlatform(info.platform)
|
||||
})
|
||||
|
||||
// Get initial window state
|
||||
ipcRenderer.invoke('window-state').then((state) => {
|
||||
if (state && typeof state.isMaximized === 'boolean') {
|
||||
setIsMaximized(state.isMaximized)
|
||||
}
|
||||
if (state && typeof state.isFullScreen === 'boolean') {
|
||||
setIsFullScreen(state.isFullScreen)
|
||||
}
|
||||
})
|
||||
|
||||
// Listen for window state changes
|
||||
const windowStateHandler = (event, state) => {
|
||||
if (state && typeof state.isMaximized === 'boolean') {
|
||||
setIsMaximized(state.isMaximized)
|
||||
}
|
||||
if (state && typeof state.isFullScreen === 'boolean') {
|
||||
setIsFullScreen(state.isFullScreen)
|
||||
}
|
||||
}
|
||||
ipcRenderer.on('window-state', windowStateHandler)
|
||||
|
||||
// Listen for navigate
|
||||
const navigateHandler = (event, url) => {
|
||||
navigate(url)
|
||||
}
|
||||
ipcRenderer.on('navigate', navigateHandler)
|
||||
|
||||
return () => {
|
||||
ipcRenderer.removeListener('navigate', navigateHandler)
|
||||
ipcRenderer.removeListener('window-state', windowStateHandler)
|
||||
}
|
||||
}, [navigate])
|
||||
|
||||
// Window control handler
|
||||
const handleWindowControl = (action) => {
|
||||
if (electronAvailable && ipcRenderer) {
|
||||
ipcRenderer.send('window-control', action)
|
||||
}
|
||||
}
|
||||
|
||||
const getAuthSession = async () => {
|
||||
if (!electronAvailable || !ipcRenderer) return null
|
||||
return await ipcRenderer.invoke('auth-session-get')
|
||||
}
|
||||
|
||||
const setAuthSession = async (session) => {
|
||||
if (!electronAvailable || !ipcRenderer) return false
|
||||
return await ipcRenderer.invoke('auth-session-set', session)
|
||||
}
|
||||
|
||||
const clearAuthSession = async () => {
|
||||
if (!electronAvailable || !ipcRenderer) return false
|
||||
return await ipcRenderer.invoke('auth-session-clear')
|
||||
}
|
||||
|
||||
// Backwards-compatible helpers
|
||||
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 || !ipcRenderer) return false
|
||||
try {
|
||||
return await ipcRenderer.invoke('spotlight-window-resize', height)
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'[ElectronContext] Failed to resize spotlight window:',
|
||||
error
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ElectronContext.Provider
|
||||
value={{
|
||||
platform,
|
||||
isMaximized,
|
||||
isFullScreen,
|
||||
isElectron: electronAvailable,
|
||||
handleWindowControl,
|
||||
openExternalUrl,
|
||||
openInternalUrl,
|
||||
getAuthSession,
|
||||
setAuthSession,
|
||||
clearAuthSession,
|
||||
getToken,
|
||||
setToken,
|
||||
resizeSpotlightWindow
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ElectronContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
ElectronProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired
|
||||
}
|
||||
|
||||
export { ElectronContext, ElectronProvider }
|
||||
@ -35,7 +35,7 @@ import {
|
||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
||||
import { ApiServerContext } from './ApiServerContext'
|
||||
import { AuthContext } from './AuthContext'
|
||||
import { ElectronContext } from './ElectronContext'
|
||||
import { ElectrobunContext } from './ElectrobunContext'
|
||||
|
||||
const SpotlightContext = createContext()
|
||||
|
||||
@ -55,7 +55,7 @@ const SpotlightContent = ({
|
||||
const [inputPrefix, setInputPrefix] = useState(null)
|
||||
const { fetchSpotlightData } = useContext(ApiServerContext)
|
||||
const { token } = useContext(AuthContext)
|
||||
const { openInternalUrl } = useContext(ElectronContext)
|
||||
const { openInternalUrl } = useContext(ElectrobunContext)
|
||||
|
||||
// Refs for throttling/debouncing
|
||||
const lastFetchTime = useRef(0)
|
||||
@ -606,7 +606,7 @@ SpotlightProvider.propTypes = {
|
||||
const ElectronSpotlightContentPage = () => {
|
||||
const cardRef = useRef(null)
|
||||
const resizeTimeoutRef = useRef(null)
|
||||
const { resizeSpotlightWindow, isElectron } = useContext(ElectronContext)
|
||||
const { resizeSpotlightWindow, isElectron } = useContext(ElectrobunContext)
|
||||
|
||||
// Function to measure and resize window
|
||||
const updateWindowHeight = useCallback(() => {
|
||||
|
||||
26
src/electrobun.d.ts
vendored
Normal file
26
src/electrobun.d.ts
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Electrobun global declarations for the browser context.
|
||||
*/
|
||||
declare global {
|
||||
interface Window {
|
||||
__ELECTROBUN_RPC__?: {
|
||||
request?: {
|
||||
osInfo?: () => Promise<{ platform: string }>
|
||||
windowState?: () => Promise<{ isMaximized?: boolean; isFullScreen?: boolean }>
|
||||
openExternalUrl?: (params: { url: string }) => void
|
||||
openInternalUrl?: (params: { url: string }) => Promise<boolean>
|
||||
windowControl?: (params: { action: string }) => void
|
||||
authSessionGet?: () => Promise<object | null>
|
||||
authSessionSet?: (params: { session: object }) => Promise<boolean>
|
||||
authSessionClear?: () => Promise<boolean>
|
||||
spotlightWindowResize?: (params: { height: number }) => Promise<boolean>
|
||||
}
|
||||
}
|
||||
__ELECTROBUN_REGISTER__?: (
|
||||
type: 'navigate' | 'windowState',
|
||||
cb: (...args: unknown[]) => void
|
||||
) => () => void
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
52
src/preload/index.ts
Normal file
52
src/preload/index.ts
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Electrobun preload script - runs before the page, sets up RPC bridge.
|
||||
*/
|
||||
import { Electroview } from 'electrobun/view'
|
||||
import type { FarmControlRPCType } from '../shared/rpcTypes'
|
||||
|
||||
const callbacks: {
|
||||
navigate: Array<(url: string) => void>
|
||||
windowState: Array<(state: { isMaximized?: boolean; isFullScreen?: boolean }) => void>
|
||||
} = {
|
||||
navigate: [],
|
||||
windowState: []
|
||||
}
|
||||
|
||||
const rpc = Electroview.defineRPC<FarmControlRPCType>({
|
||||
handlers: {
|
||||
requests: {},
|
||||
messages: {
|
||||
navigate: ({ url }) => {
|
||||
callbacks.navigate.forEach((cb) => cb(url))
|
||||
},
|
||||
windowState: (state) => {
|
||||
callbacks.windowState.forEach((cb) => cb(state))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const electroview = new Electroview({ rpc })
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__ELECTROBUN_RPC__: typeof electroview.rpc
|
||||
__ELECTROBUN_REGISTER__: (
|
||||
type: 'navigate' | 'windowState',
|
||||
cb: (...args: any[]) => void
|
||||
) => () => void
|
||||
}
|
||||
}
|
||||
|
||||
window.__ELECTROBUN_RPC__ = electroview.rpc
|
||||
|
||||
window.__ELECTROBUN_REGISTER__ = (type, cb) => {
|
||||
if (type === 'navigate' || type === 'windowState') {
|
||||
callbacks[type].push(cb)
|
||||
return () => {
|
||||
const i = callbacks[type].indexOf(cb)
|
||||
if (i >= 0) callbacks[type].splice(i, 1)
|
||||
}
|
||||
}
|
||||
return () => {}
|
||||
}
|
||||
29
src/shared/rpcTypes.ts
Normal file
29
src/shared/rpcTypes.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* RPC types for Electrobun main <-> browser communication.
|
||||
* Bun handlers respond to requests from the browser.
|
||||
* Browser handlers respond to messages from bun.
|
||||
*/
|
||||
|
||||
export type FarmControlRPCType = {
|
||||
bun: {
|
||||
requests: {
|
||||
osInfo: { params: void; response: { platform: string } }
|
||||
windowState: { params: void; response: { isMaximized?: boolean; isFullScreen?: boolean } }
|
||||
openExternalUrl: { params: { url: string }; response: void }
|
||||
openInternalUrl: { params: { url: string }; response: boolean }
|
||||
windowControl: { params: { action: 'minimize' | 'maximize' | 'close' }; response: void }
|
||||
authSessionGet: { params: void; response: object | null }
|
||||
authSessionSet: { params: { session: object }; response: boolean }
|
||||
authSessionClear: { params: void; response: boolean }
|
||||
spotlightWindowResize: { params: { height: number }; response: boolean }
|
||||
}
|
||||
messages: Record<string, never>
|
||||
}
|
||||
webview: {
|
||||
requests: Record<string, never>
|
||||
messages: {
|
||||
navigate: { url: string }
|
||||
windowState: { isMaximized?: boolean; isFullScreen?: boolean }
|
||||
}
|
||||
}
|
||||
}
|
||||
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"types": ["bun-types"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "electrobun.config.ts"]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user