Enhance Electron integration by adding ElectronContext for managing window state and external URL handling. Update AuthContext to support Electron-specific login flows and improve session management. Add new icons and adjust App.css for better UI consistency.
1
.gitignore
vendored
@ -10,6 +10,7 @@
|
||||
|
||||
# production
|
||||
/build
|
||||
/app_dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
4128
package-lock.json
generated
48
package.json
@ -1,7 +1,12 @@
|
||||
{
|
||||
"name": "farmcontrol-ui",
|
||||
"author": {
|
||||
"name": "Tom Butcher",
|
||||
"email": "tom@tombutcher.work"
|
||||
},
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"homepage": "./",
|
||||
"dependencies": {
|
||||
"@ant-design/charts": "^2.3.0",
|
||||
"@ant-design/pro-components": "^2.8.7",
|
||||
@ -49,13 +54,20 @@
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-cli": "^6.0.1"
|
||||
},
|
||||
|
||||
"main": "build/electron.js",
|
||||
"description": "3D Printer ERP and Control Software.",
|
||||
"scripts": {
|
||||
"dev": "react-scripts start",
|
||||
"electron": "ELECTRON_START_URL=http://192.168.68.53:3000 electron .",
|
||||
"start": "serve -s build",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"minify-svgs": "node scripts/minify-svgs.js"
|
||||
"minify-svgs": "node scripts/minify-svgs.js",
|
||||
|
||||
"dev:electron": "concurrently \"react-scripts start\" \"ELECTRON_START_URL=http://192.168.68.53:3000 electron src/electron/main.js\"",
|
||||
"build:electron": "npm run build && electron-builder"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
@ -77,6 +89,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"concurrently": "^8.2.2",
|
||||
"electron": "^29.0.0",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-packager": "^17.1.2",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-prettier": "^5.4.1",
|
||||
@ -90,5 +106,35 @@
|
||||
"svgo-loader": "^4.0.0",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-cli": "^6.0.1"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.tombutcher.farmcontrol",
|
||||
"icon": "src/assets/logos/farmcontrolicon.png",
|
||||
"directories": {
|
||||
"buildResources": "assets",
|
||||
"output": "app_dist"
|
||||
},
|
||||
"files": [
|
||||
"build/**/*",
|
||||
"node_modules/**/*"
|
||||
],
|
||||
"mac": {
|
||||
"target": "dmg",
|
||||
"extendInfo": {
|
||||
"CFBundleURLTypes": [
|
||||
{
|
||||
"CFBundleURLName": "com.tombutcher.farmcontrol",
|
||||
"CFBundleURLSchemes": ["farmcontrol"]
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
},
|
||||
"win": {
|
||||
"target": "nsis"
|
||||
},
|
||||
"linux": {
|
||||
"target": "AppImage"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
107
public/electron.js
Normal file
@ -0,0 +1,107 @@
|
||||
const { app, BrowserWindow, ipcMain, shell } = require('electron')
|
||||
const path = require('path')
|
||||
|
||||
let win
|
||||
|
||||
function createWindow() {
|
||||
win = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
frame: false,
|
||||
backgroundColor: '#141414',
|
||||
icon: path.join(__dirname, './logo512.png'),
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
}
|
||||
})
|
||||
|
||||
// For development, load from localhost; for production, load the built index.html
|
||||
if (process.env.ELECTRON_START_URL) {
|
||||
win.loadURL(process.env.ELECTRON_START_URL)
|
||||
} else {
|
||||
win.loadFile(path.join(__dirname, '../build/index.html'))
|
||||
}
|
||||
|
||||
setupWindowEvents()
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow)
|
||||
|
||||
// IPC handler to get OS
|
||||
ipcMain.handle('os-info', () => {
|
||||
return {
|
||||
platform: process.platform
|
||||
}
|
||||
})
|
||||
|
||||
// IPC handler to get window state
|
||||
ipcMain.handle('window-state', () => {
|
||||
return {
|
||||
isMaximized: win ? win.isMaximized() : false
|
||||
}
|
||||
})
|
||||
|
||||
// Emit events to renderer when window is maximized/unmaximized
|
||||
function setupWindowEvents() {
|
||||
if (!win) return
|
||||
win.on('maximize', () => {
|
||||
win.webContents.send('window-state', {
|
||||
isMaximized: true
|
||||
})
|
||||
})
|
||||
win.on('unmaximize', () => {
|
||||
win.webContents.send('window-state', {
|
||||
isMaximized: false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// IPC handlers for window controls
|
||||
ipcMain.on('window-control', (event, action) => {
|
||||
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
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
// Add this after other ipcMain handlers
|
||||
ipcMain.handle('open-external-url', (event, url) => {
|
||||
console.log('Opening external url...')
|
||||
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
app.on('open-url', (event, url) => {
|
||||
event.preventDefault()
|
||||
console.log('App opened with URL:', url)
|
||||
if (url.startsWith('farmcontrol://app')) {
|
||||
// Extract the path/query after 'farmcontrol://app'
|
||||
const redirectPath = url.replace('farmcontrol://app', '') || '/'
|
||||
if (win && win.webContents) {
|
||||
win.webContents.send('navigate', redirectPath)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit()
|
||||
})
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
|
Before Width: | Height: | Size: 4.0 MiB |
18
src/App.css
@ -51,6 +51,24 @@
|
||||
line-height: 0.7;
|
||||
}
|
||||
|
||||
.electron-navigation-wrapper {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.electron-navigation-wrapper li, .electron-navigation-wrapper button, .electron-navigation-wrapper .ant-tag{
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.electron-navigation {
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.electron-sidebar .ant-menu-item {
|
||||
height: 32.5px;
|
||||
line-height: 32.5px;
|
||||
}
|
||||
|
||||
|
||||
:root {
|
||||
--unit-100vh: 100vh;
|
||||
}
|
||||
|
||||
333
src/App.jsx
@ -66,6 +66,9 @@ import { ApiServerProvider } from './components/Dashboard/context/ApiServerConte
|
||||
import Users from './components/Dashboard/Management/Users.jsx'
|
||||
import UserInfo from './components/Dashboard/Management/Users/UserInfo.jsx'
|
||||
import SubJobs from './components/Dashboard/Production/SubJobs.jsx'
|
||||
import Hosts from './components/Dashboard/Management/Hosts.jsx'
|
||||
import { ElectronProvider } from './components/Dashboard/context/ElectronContext.js'
|
||||
import AuthCallback from './components/App/AuthCallback.jsx'
|
||||
|
||||
const AppContent = () => {
|
||||
const { themeConfig } = useThemeContext()
|
||||
@ -73,167 +76,179 @@ const AppContent = () => {
|
||||
return (
|
||||
<ConfigProvider theme={themeConfig}>
|
||||
<App>
|
||||
<AuthProvider>
|
||||
<Router>
|
||||
<PrintServerProvider>
|
||||
<ApiServerProvider>
|
||||
<SpotlightProvider>
|
||||
<Routes>
|
||||
<Route
|
||||
path='/'
|
||||
element={
|
||||
<PrivateRoute
|
||||
component={() => (
|
||||
<Navigate
|
||||
to='/dashboard/production/overview'
|
||||
replace
|
||||
/>
|
||||
)}
|
||||
<Router>
|
||||
<ElectronProvider>
|
||||
<AuthProvider>
|
||||
<PrintServerProvider>
|
||||
<ApiServerProvider>
|
||||
<SpotlightProvider>
|
||||
<Routes>
|
||||
<Route
|
||||
path='/'
|
||||
element={
|
||||
<PrivateRoute
|
||||
component={() => (
|
||||
<Navigate
|
||||
to='/dashboard/production/overview'
|
||||
replace
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path='/auth/callback' element={<AuthCallback />} />
|
||||
<Route
|
||||
path='/dashboard'
|
||||
element={
|
||||
<PrivateRoute component={() => <Dashboard />} />
|
||||
}
|
||||
>
|
||||
{/* Production Routes */}
|
||||
<Route
|
||||
path='production/overview'
|
||||
element={<ProductionOverview />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/dashboard'
|
||||
element={<PrivateRoute component={() => <Dashboard />} />}
|
||||
>
|
||||
{/* Production Routes */}
|
||||
<Route
|
||||
path='production/overview'
|
||||
element={<ProductionOverview />}
|
||||
/>
|
||||
<Route
|
||||
path='production/printers'
|
||||
element={<Printers />}
|
||||
/>
|
||||
<Route
|
||||
path='production/printers/control'
|
||||
element={<ControlPrinter />}
|
||||
/>
|
||||
<Route
|
||||
path='production/printers/info'
|
||||
element={<PrinterInfo />}
|
||||
/>
|
||||
<Route path='production/jobs' element={<Jobs />} />
|
||||
<Route path='production/subjobs' element={<SubJobs />} />
|
||||
<Route
|
||||
path='production/jobs/info'
|
||||
element={<JobInfo />}
|
||||
/>
|
||||
<Route
|
||||
path='production/gcodefiles'
|
||||
element={<GCodeFiles />}
|
||||
/>
|
||||
<Route
|
||||
path='production/gcodefiles/info'
|
||||
element={<GCodeFileInfo />}
|
||||
/>
|
||||
|
||||
{/* Inventory Routes */}
|
||||
<Route
|
||||
path='inventory/filamentstocks'
|
||||
element={<FilamentStocks />}
|
||||
/>
|
||||
<Route
|
||||
path='inventory/filamentstocks/info'
|
||||
element={<FilamentStockInfo />}
|
||||
/>
|
||||
<Route
|
||||
path='inventory/partstocks'
|
||||
element={<PartStocks />}
|
||||
/>
|
||||
<Route
|
||||
path='inventory/stockevents'
|
||||
element={<StockEvents />}
|
||||
/>
|
||||
<Route
|
||||
path='inventory/stockaudits'
|
||||
element={<StockAudits />}
|
||||
/>
|
||||
<Route
|
||||
path='inventory/stockaudits/info'
|
||||
element={<StockAuditInfo />}
|
||||
/>
|
||||
|
||||
{/* Management Routes */}
|
||||
<Route
|
||||
path='management/filaments'
|
||||
element={<Filaments />}
|
||||
/>
|
||||
<Route
|
||||
path='management/filaments/info'
|
||||
element={<FilamentInfo />}
|
||||
/>
|
||||
<Route path='management/parts' element={<Parts />} />
|
||||
<Route
|
||||
path='management/parts/info'
|
||||
element={<PartInfo />}
|
||||
/>
|
||||
<Route
|
||||
path='management/products'
|
||||
element={<Products />}
|
||||
/>
|
||||
<Route
|
||||
path='management/products/info'
|
||||
element={<ProductInfo />}
|
||||
/>
|
||||
<Route path='management/vendors' element={<Vendors />} />
|
||||
<Route
|
||||
path='management/users/info'
|
||||
element={<UserInfo />}
|
||||
/>
|
||||
<Route
|
||||
path='management/vendors/info'
|
||||
element={<VendorInfo />}
|
||||
/>
|
||||
<Route
|
||||
path='management/materials'
|
||||
element={<Materials />}
|
||||
/>
|
||||
<Route
|
||||
path='management/notetypes'
|
||||
element={<NoteTypes />}
|
||||
/>
|
||||
<Route path='management/users' element={<Users />} />
|
||||
<Route
|
||||
path='management/notetypes/info'
|
||||
element={<NoteTypeInfo />}
|
||||
/>
|
||||
<Route
|
||||
path='management/settings'
|
||||
element={<Settings />}
|
||||
/>
|
||||
<Route
|
||||
path='management/auditlogs'
|
||||
element={<AuditLogs />}
|
||||
/>
|
||||
<Route
|
||||
path='developer/sessionstorage'
|
||||
element={<SessionStorage />}
|
||||
/>
|
||||
<Route
|
||||
path='developer/authcontextdebug'
|
||||
element={<AuthContextDebug />}
|
||||
/>
|
||||
<Route
|
||||
path='developer/printservercontextdebug'
|
||||
element={<PrintServerContextDebug />}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path='*'
|
||||
element={
|
||||
<AppError
|
||||
message='Error 404. Page not found.'
|
||||
showRefresh={false}
|
||||
<Route
|
||||
path='production/printers'
|
||||
element={<Printers />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</SpotlightProvider>
|
||||
</ApiServerProvider>
|
||||
</PrintServerProvider>
|
||||
</Router>
|
||||
</AuthProvider>
|
||||
<Route
|
||||
path='production/printers/control'
|
||||
element={<ControlPrinter />}
|
||||
/>
|
||||
<Route
|
||||
path='production/printers/info'
|
||||
element={<PrinterInfo />}
|
||||
/>
|
||||
<Route path='production/jobs' element={<Jobs />} />
|
||||
<Route
|
||||
path='production/subjobs'
|
||||
element={<SubJobs />}
|
||||
/>
|
||||
<Route
|
||||
path='production/jobs/info'
|
||||
element={<JobInfo />}
|
||||
/>
|
||||
<Route
|
||||
path='production/gcodefiles'
|
||||
element={<GCodeFiles />}
|
||||
/>
|
||||
<Route
|
||||
path='production/gcodefiles/info'
|
||||
element={<GCodeFileInfo />}
|
||||
/>
|
||||
|
||||
{/* Inventory Routes */}
|
||||
<Route
|
||||
path='inventory/filamentstocks'
|
||||
element={<FilamentStocks />}
|
||||
/>
|
||||
<Route
|
||||
path='inventory/filamentstocks/info'
|
||||
element={<FilamentStockInfo />}
|
||||
/>
|
||||
<Route
|
||||
path='inventory/partstocks'
|
||||
element={<PartStocks />}
|
||||
/>
|
||||
<Route
|
||||
path='inventory/stockevents'
|
||||
element={<StockEvents />}
|
||||
/>
|
||||
<Route
|
||||
path='inventory/stockaudits'
|
||||
element={<StockAudits />}
|
||||
/>
|
||||
<Route
|
||||
path='inventory/stockaudits/info'
|
||||
element={<StockAuditInfo />}
|
||||
/>
|
||||
|
||||
{/* Management Routes */}
|
||||
<Route
|
||||
path='management/filaments'
|
||||
element={<Filaments />}
|
||||
/>
|
||||
<Route
|
||||
path='management/filaments/info'
|
||||
element={<FilamentInfo />}
|
||||
/>
|
||||
<Route path='management/parts' element={<Parts />} />
|
||||
<Route
|
||||
path='management/parts/info'
|
||||
element={<PartInfo />}
|
||||
/>
|
||||
<Route
|
||||
path='management/products'
|
||||
element={<Products />}
|
||||
/>
|
||||
<Route
|
||||
path='management/products/info'
|
||||
element={<ProductInfo />}
|
||||
/>
|
||||
<Route
|
||||
path='management/vendors'
|
||||
element={<Vendors />}
|
||||
/>
|
||||
<Route path='management/hosts' element={<Hosts />} />
|
||||
<Route
|
||||
path='management/users/info'
|
||||
element={<UserInfo />}
|
||||
/>
|
||||
<Route
|
||||
path='management/vendors/info'
|
||||
element={<VendorInfo />}
|
||||
/>
|
||||
<Route
|
||||
path='management/materials'
|
||||
element={<Materials />}
|
||||
/>
|
||||
<Route
|
||||
path='management/notetypes'
|
||||
element={<NoteTypes />}
|
||||
/>
|
||||
<Route path='management/users' element={<Users />} />
|
||||
<Route
|
||||
path='management/notetypes/info'
|
||||
element={<NoteTypeInfo />}
|
||||
/>
|
||||
<Route
|
||||
path='management/settings'
|
||||
element={<Settings />}
|
||||
/>
|
||||
<Route
|
||||
path='management/auditlogs'
|
||||
element={<AuditLogs />}
|
||||
/>
|
||||
<Route
|
||||
path='developer/sessionstorage'
|
||||
element={<SessionStorage />}
|
||||
/>
|
||||
<Route
|
||||
path='developer/authcontextdebug'
|
||||
element={<AuthContextDebug />}
|
||||
/>
|
||||
<Route
|
||||
path='developer/printservercontextdebug'
|
||||
element={<PrintServerContextDebug />}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path='*'
|
||||
element={
|
||||
<AppError
|
||||
message='Error 404. Page not found.'
|
||||
showRefresh={false}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</SpotlightProvider>
|
||||
</ApiServerProvider>
|
||||
</PrintServerProvider>
|
||||
</AuthProvider>
|
||||
</ElectronProvider>
|
||||
</Router>
|
||||
</App>
|
||||
</ConfigProvider>
|
||||
)
|
||||
|
||||
BIN
src/assets/icons/contracticon.afdesign
Normal file
1
src/assets/icons/contracticon.min.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 64 64"><path d="M5.49 28.706h17.316c3.564 0 5.633-2.094 5.633-5.658V5.954c0-1.928-1.461-3.39-3.41-3.39-1.954 0-3.383 1.436-3.383 3.39v2.243l.673 9.826-7.322-7.694-8.992-9.08A3.3 3.3 0 0 0 3.611.253C1.509.253 0 1.694 0 3.776c0 .936.38 1.813 1.032 2.471l9.035 9.023 7.694 7.291-9.86-.648H5.49c-1.954 0-3.416 1.404-3.416 3.384 0 1.96 1.436 3.409 3.416 3.409m30.784 31.607c1.955 0 3.384-1.41 3.384-3.39v-2.519l-.673-9.807 7.322 7.701 9.171 9.228c.633.663 1.466.995 2.394.995 2.076 0 3.586-1.44 3.586-3.517 0-.941-.355-1.818-1.013-2.471l-9.208-9.208-7.726-7.291 9.892.654h2.693c1.954 0 3.415-1.404 3.415-3.359 0-1.985-1.435-3.409-3.415-3.409H38.498c-3.57 0-5.633 2.063-5.633 5.628v17.375c0 1.954 1.429 3.39 3.409 3.39" style="fill-rule:nonzero" transform="matrix(.94087 0 0 .92768 3.088 2.883)"/></svg>
|
||||
|
After Width: | Height: | Size: 957 B |
7
src/assets/icons/contracticon.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(0.940871,0,0,0.927684,3.08821,2.88259)">
|
||||
<path d="M5.49,28.706L22.806,28.706C26.37,28.706 28.439,26.612 28.439,23.048L28.439,5.954C28.439,4.026 26.978,2.564 25.029,2.564C23.075,2.564 21.646,4 21.646,5.954L21.646,8.197L22.319,18.023L14.997,10.329L6.005,1.249C5.367,0.591 4.507,0.253 3.611,0.253C1.509,0.253 0,1.694 0,3.776C0,4.712 0.38,5.589 1.032,6.247L10.067,15.27L17.761,22.561L7.901,21.913L5.49,21.913C3.536,21.913 2.074,23.317 2.074,25.297C2.074,27.257 3.51,28.706 5.49,28.706ZM36.274,60.313C38.229,60.313 39.658,58.903 39.658,56.923L39.658,54.404L38.985,44.597L46.307,52.298L55.478,61.526C56.111,62.189 56.944,62.521 57.872,62.521C59.948,62.521 61.458,61.081 61.458,59.004C61.458,58.063 61.103,57.186 60.445,56.533L51.237,47.325L43.511,40.034L53.403,40.688L56.096,40.688C58.05,40.688 59.511,39.284 59.511,37.329C59.511,35.344 58.076,33.92 56.096,33.92L38.498,33.92C34.928,33.92 32.865,35.983 32.865,39.548L32.865,56.923C32.865,58.877 34.294,60.313 36.274,60.313Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/icons/expandicon.afdesign
Normal file
1
src/assets/icons/expandicon.min.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 64 64"><path d="M3.409 26.139c1.955 0 3.384-1.436 3.384-3.39v-2.237l-.647-9.832 7.296 7.694 9.017 9.08c.633.664 1.467.97 2.369.97 2.101 0 3.611-1.415 3.611-3.497a3.42 3.42 0 0 0-1.013-2.471l-9.054-9.023-7.7-7.291 9.891.648h2.386c1.954 0 3.415-1.404 3.415-3.378 0-1.986-1.435-3.41-3.415-3.41H5.633C2.063.002 0 2.066 0 5.63v17.119c0 1.928 1.455 3.39 3.409 3.39m33.775 34.074h17.322c3.564 0 5.653-2.064 5.653-5.634V37.461c0-1.923-1.455-3.385-3.435-3.385-1.929 0-3.384 1.436-3.384 3.385v2.242l.673 9.826-7.322-7.694-8.992-9.074c-.632-.664-1.491-.97-2.393-.97-2.077 0-3.612 1.415-3.612 3.491 0 .962.38 1.819 1.039 2.471l9.028 9.023 7.726 7.291-9.891-.648h-2.412c-1.948 0-3.415 1.404-3.415 3.384 0 1.986 1.467 3.41 3.415 3.41" style="fill-rule:nonzero" transform="matrix(.96112 0 0 .96325 3 3)"/></svg>
|
||||
|
After Width: | Height: | Size: 956 B |
7
src/assets/icons/expandicon.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(0.961121,0,0,0.963255,3,3)">
|
||||
<path d="M3.409,26.139C5.364,26.139 6.793,24.703 6.793,22.749L6.793,20.512L6.146,10.68L13.442,18.374L22.459,27.454C23.092,28.118 23.926,28.424 24.828,28.424C26.929,28.424 28.439,27.009 28.439,24.927C28.439,23.966 28.084,23.114 27.426,22.456L18.372,13.433L10.672,6.142L20.563,6.79L22.949,6.79C24.903,6.79 26.364,5.386 26.364,3.412C26.364,1.426 24.929,0.002 22.949,0.002L5.633,0.002C2.063,0.002 0,2.066 0,5.63L0,22.749C0,24.677 1.455,26.139 3.409,26.139ZM37.184,60.213L54.506,60.213C58.07,60.213 60.159,58.149 60.159,54.579L60.159,37.461C60.159,35.538 58.704,34.076 56.724,34.076C54.795,34.076 53.34,35.512 53.34,37.461L53.34,39.703L54.013,49.529L46.691,41.835L37.699,32.761C37.067,32.097 36.208,31.791 35.306,31.791C33.229,31.791 31.694,33.206 31.694,35.282C31.694,36.244 32.074,37.101 32.733,37.753L41.761,46.776L49.487,54.067L39.596,53.419L37.184,53.419C35.236,53.419 33.769,54.823 33.769,56.803C33.769,58.789 35.236,60.213 37.184,60.213Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/icons/hosticon.afdesign
Normal file
1
src/assets/icons/hosticon.min.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 64 64"><path d="M10.251 46.567h54.035c6.66 0 10.251-3.539 10.251-10.148V25.612c0-4.444-1.135-7.628-3.757-10.275L60.737 5.006C57.346 1.558 54.946.02 50.458.02H24.084c-4.462 0-6.887 1.538-10.258 4.986L3.757 15.337C1.121 18.035 0 21.089 0 25.612v10.807c0 6.609 3.591 10.148 10.251 10.148m.277-5.993c-2.867 0-4.484-1.566-4.484-4.535v-6.391c0-1.904.897-2.827 2.775-2.827h56.899c1.878 0 2.775.923 2.775 2.827v6.391c0 2.969-1.617 4.535-4.484 4.535zM8.292 21.549c-1.071 0-1.373-.983-.733-1.628L19.028 7.799c1.751-1.835 3.025-2.508 5.391-2.508h25.705c2.36 0 3.634.673 5.391 2.508l11.489 12.122c.614.645.338 1.628-.759 1.628zm4.555 15.102c1.656 0 3.051-1.375 3.051-3.052 0-1.656-1.395-3.025-3.051-3.025-1.676 0-3.046 1.369-3.046 3.025a3.06 3.06 0 0 0 3.046 3.052m8.362.971c1.037 0 1.925-.812 1.925-1.828V31.61c0-1.031-.888-1.843-1.925-1.843a1.823 1.823 0 0 0-1.828 1.843v4.184a1.82 1.82 0 0 0 1.828 1.828m6.062 0c1.037 0 1.925-.812 1.925-1.828V31.61c0-1.031-.888-1.843-1.925-1.843-1.01 0-1.822.812-1.822 1.843v4.184c0 1.016.812 1.828 1.822 1.828m6.535-2.105a1.9 1.9 0 0 0 1.892-1.918 1.89 1.89 0 0 0-1.892-1.891 1.89 1.89 0 0 0-1.897 1.891c0 1.068.849 1.918 1.897 1.918m6.555 2.105a1.817 1.817 0 0 0 1.817-1.828V31.61a1.82 1.82 0 0 0-1.817-1.843c-1.042 0-1.93.812-1.93 1.843v4.184c0 1.016.888 1.828 1.93 1.828m6.062 0a1.803 1.803 0 0 0 1.823-1.828V31.61c0-1.031-.787-1.843-1.823-1.843s-1.905.812-1.905 1.843v4.184c0 1.016.869 1.828 1.905 1.828m7.228-2.041h9.447c1.048 0 1.848-.877 1.848-1.925 0-1.027-.8-1.822-1.848-1.822h-9.447a1.8 1.8 0 0 0-1.828 1.822c0 1.048.801 1.925 1.828 1.925" style="fill-rule:nonzero" transform="translate(2 13.25)scale(.80497)"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
7
src/assets/icons/hosticon.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(0.804972,0,0,0.804972,2,13.2492)">
|
||||
<path d="M10.251,46.567L64.286,46.567C70.946,46.567 74.537,43.028 74.537,36.419L74.537,25.612C74.537,21.168 73.402,17.984 70.78,15.337L60.737,5.006C57.346,1.558 54.946,0.02 50.458,0.02L24.084,0.02C19.622,0.02 17.197,1.558 13.826,5.006L3.757,15.337C1.121,18.035 0,21.089 0,25.612L0,36.419C0,43.028 3.591,46.567 10.251,46.567ZM10.528,40.574C7.661,40.574 6.044,39.008 6.044,36.039L6.044,29.648C6.044,27.744 6.941,26.821 8.819,26.821L65.718,26.821C67.596,26.821 68.493,27.744 68.493,29.648L68.493,36.039C68.493,39.008 66.876,40.574 64.009,40.574L10.528,40.574ZM8.292,21.549C7.221,21.549 6.919,20.566 7.559,19.921L19.028,7.799C20.779,5.964 22.053,5.291 24.419,5.291L50.124,5.291C52.484,5.291 53.758,5.964 55.515,7.799L67.004,19.921C67.618,20.566 67.342,21.549 66.245,21.549L8.292,21.549ZM12.847,36.651C14.503,36.651 15.898,35.276 15.898,33.599C15.898,31.943 14.503,30.574 12.847,30.574C11.171,30.574 9.801,31.943 9.801,33.599C9.801,35.276 11.171,36.651 12.847,36.651ZM21.209,37.622C22.246,37.622 23.134,36.81 23.134,35.794L23.134,31.61C23.134,30.579 22.246,29.767 21.209,29.767C20.193,29.767 19.381,30.579 19.381,31.61L19.381,35.794C19.381,36.81 20.193,37.622 21.209,37.622ZM27.271,37.622C28.308,37.622 29.196,36.81 29.196,35.794L29.196,31.61C29.196,30.579 28.308,29.767 27.271,29.767C26.261,29.767 25.449,30.579 25.449,31.61L25.449,35.794C25.449,36.81 26.261,37.622 27.271,37.622ZM33.806,35.517C34.849,35.517 35.698,34.667 35.698,33.599C35.698,32.551 34.849,31.708 33.806,31.708C32.758,31.708 31.909,32.551 31.909,33.599C31.909,34.667 32.758,35.517 33.806,35.517ZM40.361,37.622C41.366,37.622 42.178,36.81 42.178,35.794L42.178,31.61C42.178,30.579 41.366,29.767 40.361,29.767C39.319,29.767 38.431,30.579 38.431,31.61L38.431,35.794C38.431,36.81 39.319,37.622 40.361,37.622ZM46.423,37.622C47.459,37.622 48.246,36.81 48.246,35.794L48.246,31.61C48.246,30.579 47.459,29.767 46.423,29.767C45.387,29.767 44.518,30.579 44.518,31.61L44.518,35.794C44.518,36.81 45.387,37.622 46.423,37.622ZM53.651,35.581L63.098,35.581C64.146,35.581 64.946,34.704 64.946,33.656C64.946,32.629 64.146,31.834 63.098,31.834L53.651,31.834C52.624,31.834 51.823,32.629 51.823,33.656C51.823,34.704 52.624,35.581 53.651,35.581Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src/assets/icons/minusicon.afdesign
Normal file
1
src/assets/icons/minusicon.min.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 64 64"><path d="M7.327 43.04c1.358 1.327 3.648 1.31 4.941.017l30.717-30.73c1.316-1.315 1.336-3.578-.023-4.91-1.358-1.359-3.62-1.364-4.936-.023L7.31 38.129c-1.293 1.288-1.336 3.578.017 4.911" style="fill-rule:nonzero" transform="rotate(45 26.952 34.187)scale(1.15242)"/></svg>
|
||||
|
After Width: | Height: | Size: 435 B |
7
src/assets/icons/minusicon.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(0.814885,0.814885,-0.814885,0.814885,32.0681,-9.04462)">
|
||||
<path d="M7.327,43.04C8.685,44.367 10.975,44.35 12.268,43.057L42.985,12.327C44.301,11.012 44.321,8.749 42.962,7.417C41.604,6.058 39.342,6.053 38.026,7.394L7.31,38.129C6.017,39.417 5.974,41.707 7.327,43.04Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 785 B |
@ -373,9 +373,9 @@ const ApiServerProvider = ({ children }) => {
|
||||
try {
|
||||
const response = await axios.get(fetchUrl, {
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
withCredentials: true
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
return response.data
|
||||
} catch (err) {
|
||||
@ -417,9 +417,9 @@ const ApiServerProvider = ({ children }) => {
|
||||
order: sorter.order
|
||||
},
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
withCredentials: true
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ -465,9 +465,9 @@ const ApiServerProvider = ({ children }) => {
|
||||
properties: properties.join(',') // Convert array to comma-separated string
|
||||
},
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
withCredentials: true
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ -489,9 +489,9 @@ const ApiServerProvider = ({ children }) => {
|
||||
try {
|
||||
const response = await axios.put(updateUrl, value, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
withCredentials: true
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
logger.debug('Object updated successfully')
|
||||
if (socketRef.current && socketRef.current.connected == true) {
|
||||
@ -517,9 +517,9 @@ const ApiServerProvider = ({ children }) => {
|
||||
try {
|
||||
const response = await axios.delete(deleteUrl, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
withCredentials: true
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
logger.debug('Object deleted successfully')
|
||||
if (socketRef.current && socketRef.current.connected == true) {
|
||||
@ -545,9 +545,9 @@ const ApiServerProvider = ({ children }) => {
|
||||
try {
|
||||
const response = await axios.post(createUrl, value, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
withCredentials: true
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
return response.data
|
||||
} catch (err) {
|
||||
@ -568,9 +568,9 @@ const ApiServerProvider = ({ children }) => {
|
||||
`${config.backendUrl}/${type.toLowerCase()}s/${id}/content`,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
withCredentials: true
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ -599,9 +599,9 @@ const ApiServerProvider = ({ children }) => {
|
||||
order: 'ascend'
|
||||
},
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
withCredentials: true
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
const notesData = response.data
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
// src/contexts/AuthContext.js
|
||||
import React, { createContext, useState, useCallback, useEffect } from 'react'
|
||||
import React, {
|
||||
createContext,
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useContext
|
||||
} from 'react'
|
||||
import axios from 'axios'
|
||||
import { message, Modal, notification, Progress, Button, Space } from 'antd'
|
||||
import PropTypes from 'prop-types'
|
||||
@ -8,6 +14,8 @@ import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
||||
import config from '../../../config'
|
||||
import AppError from '../../App/AppError'
|
||||
import loglevel from 'loglevel'
|
||||
import { ElectronContext } from './ElectronContext'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
const logger = loglevel.getLogger('ApiServerContext')
|
||||
logger.setLevel(config.logLevel)
|
||||
|
||||
@ -18,6 +26,7 @@ const AuthProvider = ({ children }) => {
|
||||
const [notificationApi, notificationContextHolder] =
|
||||
notification.useNotification()
|
||||
const [authenticated, setAuthenticated] = useState(false)
|
||||
const [initialized, setInitialized] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [token, setToken] = useState(null)
|
||||
const [expiresAt, setExpiresAt] = useState(null)
|
||||
@ -25,12 +34,27 @@ const AuthProvider = ({ children }) => {
|
||||
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false)
|
||||
const [showUnauthorizedModal, setShowUnauthorizedModal] = useState(false)
|
||||
const [authError, setAuthError] = useState(null)
|
||||
const { openExternalUrl, isElectron } = useContext(ElectronContext)
|
||||
const location = useLocation()
|
||||
|
||||
// Read token from session storage if present
|
||||
useEffect(() => {
|
||||
const storedToken = sessionStorage.getItem('authToken')
|
||||
const storedExpiresAt = sessionStorage.getItem('authExpiresAt')
|
||||
if (storedToken && storedExpiresAt) {
|
||||
setToken(storedToken)
|
||||
setExpiresAt(storedExpiresAt)
|
||||
setAuthenticated(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const logout = useCallback((redirectUri = '/login') => {
|
||||
setAuthenticated(false)
|
||||
setToken(null)
|
||||
setExpiresAt(null)
|
||||
setUserProfile(null)
|
||||
sessionStorage.removeItem('authToken')
|
||||
sessionStorage.removeItem('authExpiresAt')
|
||||
window.location.href = `${config.backendUrl}/auth/logout?redirect_uri=${encodeURIComponent(redirectUri)}`
|
||||
}, [])
|
||||
|
||||
@ -38,9 +62,54 @@ const AuthProvider = ({ children }) => {
|
||||
const loginWithSSO = useCallback(
|
||||
(redirectUri = window.location.pathname + window.location.search) => {
|
||||
messageApi.info('Logging in with tombutcher.work')
|
||||
window.location.href = `${config.backendUrl}/auth/login?redirect_uri=${encodeURIComponent(redirectUri)}`
|
||||
const loginUrl = `${config.backendUrl}/auth/${isElectron ? 'app/' : ''}login?redirect_uri=${encodeURIComponent(redirectUri)}`
|
||||
if (isElectron) {
|
||||
console.log('Opening external url...')
|
||||
openExternalUrl(loginUrl)
|
||||
} else {
|
||||
console.log('Redirecting...')
|
||||
window.location.href = loginUrl
|
||||
}
|
||||
},
|
||||
[messageApi]
|
||||
[messageApi, openExternalUrl, isElectron]
|
||||
)
|
||||
|
||||
const getLoginToken = useCallback(
|
||||
async (code) => {
|
||||
setLoading(true)
|
||||
setShowUnauthorizedModal(false)
|
||||
setShowSessionExpiredModal(false)
|
||||
setAuthError(null)
|
||||
try {
|
||||
// Make a call to your backend to check auth status
|
||||
const response = await axios.get(
|
||||
`${config.backendUrl}/auth/${isElectron ? 'app/' : ''}token?code=${code}`
|
||||
)
|
||||
|
||||
if (response.status === 200 && response.data) {
|
||||
logger.debug('Got auth token!')
|
||||
setToken(response.data.access_token)
|
||||
setExpiresAt(response.data.expires_at)
|
||||
setUserProfile(response.data)
|
||||
sessionStorage.setItem('authToken', response.data.access_token)
|
||||
sessionStorage.setItem('authExpiresAt', response.data.expires_at)
|
||||
} else {
|
||||
setAuthenticated(false)
|
||||
setAuthError('Failed to authenticate user.')
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug('Auth check failed', error)
|
||||
if (error.response?.status === 401) {
|
||||
setShowUnauthorizedModal(true)
|
||||
} else {
|
||||
setAuthError('Error connecting to authentication service.')
|
||||
}
|
||||
setAuthenticated(false)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[isElectron]
|
||||
)
|
||||
// Function to check if the user is logged in
|
||||
const checkAuthStatus = useCallback(async () => {
|
||||
@ -49,7 +118,9 @@ const AuthProvider = ({ children }) => {
|
||||
try {
|
||||
// Make a call to your backend to check auth status
|
||||
const response = await axios.get(`${config.backendUrl}/auth/user`, {
|
||||
withCredentials: true // Important for including cookies
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (response.status === 200 && response.data) {
|
||||
@ -57,6 +128,8 @@ const AuthProvider = ({ children }) => {
|
||||
setToken(response.data.access_token)
|
||||
setExpiresAt(response.data.expires_at)
|
||||
setUserProfile(response.data)
|
||||
sessionStorage.setItem('authToken', response.data.access_token)
|
||||
sessionStorage.setItem('authExpiresAt', response.data.expires_at)
|
||||
} else {
|
||||
setAuthenticated(false)
|
||||
setAuthError('Failed to authenticate user.')
|
||||
@ -72,7 +145,7 @@ const AuthProvider = ({ children }) => {
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
}, [token])
|
||||
|
||||
const refreshToken = useCallback(async () => {
|
||||
try {
|
||||
@ -82,6 +155,8 @@ const AuthProvider = ({ children }) => {
|
||||
if (response.status === 200 && response.data) {
|
||||
setToken(response.data.access_token)
|
||||
setExpiresAt(response.data.expires_at)
|
||||
sessionStorage.setItem('authToken', response.data.access_token)
|
||||
sessionStorage.setItem('authExpiresAt', response.data.expires_at)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token refresh failed', error)
|
||||
@ -180,8 +255,27 @@ const AuthProvider = ({ children }) => {
|
||||
}, [expiresAt, authenticated, notificationApi, refreshToken])
|
||||
|
||||
useEffect(() => {
|
||||
checkAuthStatus()
|
||||
}, [checkAuthStatus])
|
||||
if (initialized == false) {
|
||||
const authCode =
|
||||
new URLSearchParams(location.search).get('authCode') || null
|
||||
if (authCode != null) {
|
||||
getLoginToken(authCode)
|
||||
if (window && window.history && window.location) {
|
||||
const url = new URL(window.location.href)
|
||||
if (url.searchParams.has('authCode')) {
|
||||
url.searchParams.delete('authCode')
|
||||
window.history.replaceState(
|
||||
{},
|
||||
document.title,
|
||||
url.pathname + url.search
|
||||
)
|
||||
}
|
||||
}
|
||||
setInitialized(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
}, [checkAuthStatus, location.search, getLoginToken, initialized])
|
||||
|
||||
if (authError) {
|
||||
return <AppError message={authError} showBack={false} />
|
||||
@ -195,6 +289,7 @@ const AuthProvider = ({ children }) => {
|
||||
value={{
|
||||
authenticated,
|
||||
loginWithSSO,
|
||||
getLoginToken,
|
||||
token,
|
||||
loading,
|
||||
userProfile,
|
||||
@ -261,6 +356,19 @@ const AuthProvider = ({ children }) => {
|
||||
You need to be logged in to access FarmControl. Please log in with
|
||||
tombutcher.work to continue.
|
||||
</Modal>
|
||||
<Modal
|
||||
title={
|
||||
<Space size={'middle'}>
|
||||
<ExclamationOctogonIcon />
|
||||
Loading...
|
||||
</Space>
|
||||
}
|
||||
open={loading}
|
||||
style={{ maxWidth: 200, top: '50%', transform: 'translateY(-50%)' }}
|
||||
closable={false}
|
||||
maskClosable={false}
|
||||
footer={false}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
117
src/components/Dashboard/context/ElectronContext.js
Normal file
@ -0,0 +1,117 @@
|
||||
import React, { 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
|
||||
export function isElectron() {
|
||||
// Renderer process
|
||||
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
window.process &&
|
||||
window.process.type === 'renderer'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
// Main process
|
||||
if (
|
||||
typeof process !== 'undefined' &&
|
||||
process.versions &&
|
||||
!!process.versions.electron
|
||||
) {
|
||||
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 [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
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
// Listen for window state changes
|
||||
const windowStateHandler = (event, state) => {
|
||||
if (state && typeof state.isMaximized === 'boolean')
|
||||
setIsMaximized(state.isMaximized)
|
||||
}
|
||||
ipcRenderer.on('window-state', windowStateHandler)
|
||||
|
||||
// Listen for navigate
|
||||
const navigateHandler = (event, url) => {
|
||||
console.log('Navigating to:', url)
|
||||
navigate(url)
|
||||
}
|
||||
ipcRenderer.on('navigate', navigateHandler)
|
||||
|
||||
return () => {
|
||||
ipcRenderer.removeListener('navigate', navigateHandler)
|
||||
ipcRenderer.removeListener('window-state', windowStateHandler)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Window control handler
|
||||
const handleWindowControl = (action) => {
|
||||
if (electronAvailable && ipcRenderer) {
|
||||
ipcRenderer.send('window-control', action)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ElectronContext.Provider
|
||||
value={{
|
||||
platform,
|
||||
isMaximized,
|
||||
isElectron: electronAvailable,
|
||||
handleWindowControl,
|
||||
openExternalUrl
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ElectronContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
ElectronProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired
|
||||
}
|
||||
|
||||
export { ElectronContext, ElectronProvider }
|
||||