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",
|
"dotenv": "^17.2.1",
|
||||||
"gcode-preview": "^2.18.0",
|
"gcode-preview": "^2.18.0",
|
||||||
"keycloak-js": "^26.2.0",
|
"keycloak-js": "^26.2.0",
|
||||||
"keytar": "^7.9.0",
|
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
"loglevel": "^1.9.2",
|
"loglevel": "^1.9.2",
|
||||||
"online-3d-viewer": "^0.16.0",
|
"online-3d-viewer": "^0.16.0",
|
||||||
@ -63,15 +62,15 @@
|
|||||||
"tsparticles": "^3.9.1",
|
"tsparticles": "^3.9.1",
|
||||||
"web-vitals": "^5.1.0"
|
"web-vitals": "^5.1.0"
|
||||||
},
|
},
|
||||||
"main": "build/electron.js",
|
"main": "src/bun/index.ts",
|
||||||
"description": "3D Printer ERP and Control Software.",
|
"description": "3D Printer ERP and Control Software.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env NODE_ENV=development vite",
|
"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",
|
"start": "serve -s build",
|
||||||
"build": "vite 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\"",
|
"dev:electrobun": "concurrently \"cross-env NODE_ENV=development vite --no-open\" \"cross-env ELECTROBUN_START_URL=http://localhost:5173 electrobun dev\"",
|
||||||
"build:electron": "vite build && electron-builder",
|
"build:electrobun": "vite build && electrobun build",
|
||||||
"build:cloudflare": "cross-env VITE_DEPLOY_TARGET=cloudflare vite build",
|
"build:cloudflare": "cross-env VITE_DEPLOY_TARGET=cloudflare vite build",
|
||||||
"deploy": "npm run build:cloudflare && wrangler pages deploy --branch main"
|
"deploy": "npm run build:cloudflare && wrangler pages deploy --branch main"
|
||||||
},
|
},
|
||||||
@ -97,9 +96,8 @@
|
|||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"electron": "^38.7.1",
|
"electrobun": "^1.13.1",
|
||||||
"electron-builder": "^26.0.12",
|
"typescript": "^5.0.0",
|
||||||
"electron-packager": "^17.1.2",
|
|
||||||
"eslint": "^9.34.0",
|
"eslint": "^9.34.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.5.4",
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import {
|
|||||||
import AppError from './components/App/AppError'
|
import AppError from './components/App/AppError'
|
||||||
import { ApiServerProvider } from './components/Dashboard/context/ApiServerContext.jsx'
|
import { ApiServerProvider } from './components/Dashboard/context/ApiServerContext.jsx'
|
||||||
import { NotificationProvider } from './components/Dashboard/context/NotificationContext.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 { MessageProvider } from './components/Dashboard/context/MessageContext.jsx'
|
||||||
import AuthCallback from './components/App/AuthCallback.jsx'
|
import AuthCallback from './components/App/AuthCallback.jsx'
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ const AppContent = () => {
|
|||||||
<ConfigProvider theme={themeConfig}>
|
<ConfigProvider theme={themeConfig}>
|
||||||
<App>
|
<App>
|
||||||
<Router>
|
<Router>
|
||||||
<ElectronProvider>
|
<ElectrobunProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<PrintServerProvider>
|
<PrintServerProvider>
|
||||||
<ApiServerProvider>
|
<ApiServerProvider>
|
||||||
@ -122,7 +122,7 @@ const AppContent = () => {
|
|||||||
</ApiServerProvider>
|
</ApiServerProvider>
|
||||||
</PrintServerProvider>
|
</PrintServerProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</ElectronProvider>
|
</ElectrobunProvider>
|
||||||
</Router>
|
</Router>
|
||||||
</App>
|
</App>
|
||||||
</ConfigProvider>
|
</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 SearchIcon from '../../Icons/SearchIcon'
|
||||||
import SettingsIcon from '../../Icons/SettingsIcon'
|
import SettingsIcon from '../../Icons/SettingsIcon'
|
||||||
import DeveloperIcon from '../../Icons/DeveloperIcon'
|
import DeveloperIcon from '../../Icons/DeveloperIcon'
|
||||||
import { ElectronContext } from '../context/ElectronContext'
|
import { ElectrobunContext } from '../context/ElectrobunContext'
|
||||||
import DashboardWindowButtons from './DashboardWindowButtons'
|
import DashboardWindowButtons from './DashboardWindowButtons'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
@ -60,7 +60,7 @@ const DashboardNavigation = () => {
|
|||||||
icon: <ProductionIcon />
|
icon: <ProductionIcon />
|
||||||
})
|
})
|
||||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||||
const { platform, isElectron, isFullScreen } = useContext(ElectronContext)
|
const { platform, isElectron, isFullScreen } = useContext(ElectrobunContext)
|
||||||
|
|
||||||
const mainMenuItems = useMemo(
|
const mainMenuItems = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import ExpandSidebarIcon from '../../Icons/ExpandSidebarIcon'
|
|||||||
import { useMediaQuery } from 'react-responsive'
|
import { useMediaQuery } from 'react-responsive'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { ElectronContext } from '../context/ElectronContext'
|
import { ElectrobunContext } from '../context/ElectrobunContext'
|
||||||
const { Sider } = Layout
|
const { Sider } = Layout
|
||||||
|
|
||||||
const DashboardSidebar = ({
|
const DashboardSidebar = ({
|
||||||
@ -24,7 +24,7 @@ const DashboardSidebar = ({
|
|||||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const { isElectron } = useContext(ElectronContext)
|
const { isElectron } = useContext(ElectrobunContext)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof collapsedProp === 'boolean') {
|
if (typeof collapsedProp === 'boolean') {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useContext } from 'react'
|
import { useContext } from 'react'
|
||||||
import { Flex, Button } from 'antd'
|
import { Flex, Button } from 'antd'
|
||||||
import { ElectronContext } from '../context/ElectronContext'
|
import { ElectrobunContext } from '../context/ElectrobunContext'
|
||||||
import XMarkIcon from '../../Icons/XMarkIcon'
|
import XMarkIcon from '../../Icons/XMarkIcon'
|
||||||
import MinusIcon from '../../Icons/MinusIcon'
|
import MinusIcon from '../../Icons/MinusIcon'
|
||||||
import ContractIcon from '../../Icons/ContractIcon'
|
import ContractIcon from '../../Icons/ContractIcon'
|
||||||
@ -8,7 +8,7 @@ import ExpandIcon from '../../Icons/ExpandIcon'
|
|||||||
|
|
||||||
const DashboardWindowButtons = () => {
|
const DashboardWindowButtons = () => {
|
||||||
const { isMaximized, handleWindowControl, platform, isFullScreen } =
|
const { isMaximized, handleWindowControl, platform, isFullScreen } =
|
||||||
useContext(ElectronContext)
|
useContext(ElectrobunContext)
|
||||||
|
|
||||||
const closeButton = (
|
const closeButton = (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -40,7 +40,7 @@ import CheckIcon from '../../Icons/CheckIcon'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
|
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
|
||||||
import { AuthContext } from '../context/AuthContext'
|
import { AuthContext } from '../context/AuthContext'
|
||||||
import { ElectronContext } from '../context/ElectronContext'
|
import { ElectrobunContext } from '../context/ElectrobunContext'
|
||||||
import ActionsIcon from '../../Icons/ActionsIcon'
|
import ActionsIcon from '../../Icons/ActionsIcon'
|
||||||
|
|
||||||
const logger = loglevel.getLogger('DasboardTable')
|
const logger = loglevel.getLogger('DasboardTable')
|
||||||
@ -103,7 +103,7 @@ const ObjectTable = forwardRef(
|
|||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const { token } = useContext(AuthContext)
|
const { token } = useContext(AuthContext)
|
||||||
const { isElectron } = useContext(ElectronContext)
|
const { isElectron } = useContext(ElectrobunContext)
|
||||||
const onStateChangeRef = useRef(onStateChange)
|
const onStateChangeRef = useRef(onStateChange)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import ExclamationOctogonIcon from '../../Icons/ExclamationOctagonIcon'
|
|||||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
||||||
import config from '../../../config'
|
import config from '../../../config'
|
||||||
import loglevel from 'loglevel'
|
import loglevel from 'loglevel'
|
||||||
import { ElectronContext } from './ElectronContext'
|
import { ElectrobunContext } from './ElectrobunContext'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
getAuthCookies,
|
getAuthCookies,
|
||||||
@ -63,7 +63,7 @@ const AuthProvider = ({ children }) => {
|
|||||||
getAuthSession,
|
getAuthSession,
|
||||||
setAuthSession,
|
setAuthSession,
|
||||||
clearAuthSession
|
clearAuthSession
|
||||||
} = useContext(ElectronContext)
|
} = useContext(ElectrobunContext)
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
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 InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
||||||
import { ApiServerContext } from './ApiServerContext'
|
import { ApiServerContext } from './ApiServerContext'
|
||||||
import { AuthContext } from './AuthContext'
|
import { AuthContext } from './AuthContext'
|
||||||
import { ElectronContext } from './ElectronContext'
|
import { ElectrobunContext } from './ElectrobunContext'
|
||||||
|
|
||||||
const SpotlightContext = createContext()
|
const SpotlightContext = createContext()
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ const SpotlightContent = ({
|
|||||||
const [inputPrefix, setInputPrefix] = useState(null)
|
const [inputPrefix, setInputPrefix] = useState(null)
|
||||||
const { fetchSpotlightData } = useContext(ApiServerContext)
|
const { fetchSpotlightData } = useContext(ApiServerContext)
|
||||||
const { token } = useContext(AuthContext)
|
const { token } = useContext(AuthContext)
|
||||||
const { openInternalUrl } = useContext(ElectronContext)
|
const { openInternalUrl } = useContext(ElectrobunContext)
|
||||||
|
|
||||||
// Refs for throttling/debouncing
|
// Refs for throttling/debouncing
|
||||||
const lastFetchTime = useRef(0)
|
const lastFetchTime = useRef(0)
|
||||||
@ -606,7 +606,7 @@ SpotlightProvider.propTypes = {
|
|||||||
const ElectronSpotlightContentPage = () => {
|
const ElectronSpotlightContentPage = () => {
|
||||||
const cardRef = useRef(null)
|
const cardRef = useRef(null)
|
||||||
const resizeTimeoutRef = useRef(null)
|
const resizeTimeoutRef = useRef(null)
|
||||||
const { resizeSpotlightWindow, isElectron } = useContext(ElectronContext)
|
const { resizeSpotlightWindow, isElectron } = useContext(ElectrobunContext)
|
||||||
|
|
||||||
// Function to measure and resize window
|
// Function to measure and resize window
|
||||||
const updateWindowHeight = useCallback(() => {
|
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