Compare commits

..

3 Commits

Author SHA1 Message Date
3534a797fc Refactor About component to use getAppEngine for Electron context
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Replaced getElectronVersion with getAppEngine in the About component to retrieve the app engine type.
- Updated state management to reflect the app engine instead of the Electron version.
- Adjusted UI elements to display the app engine type, enhancing clarity for users regarding the application environment.
2026-08-08 19:40:30 +01:00
a5cad3b746 Integrate Electron context into NotificationProvider for responsive styling
- Added `ElectronContext` to the `NotificationProvider` to access the `isElectron` state.
- Updated the notification center's styling to conditionally apply a top margin based on whether the app is running in an Electron environment, enhancing the UI for better responsiveness.
2026-08-08 19:38:00 +01:00
998084160f Add app update engine handling and UI enhancements
- Introduced `getAppEngine` method in the Electron context to retrieve the current app update engine.
- Updated the Settings component to manage and display the app update engine, allowing users to select between 'Native' and 'Chromium'.
- Enhanced the app update logic to persist the selected engine and ensure proper handling during updates.
- Improved UI in the NewAppUpdate component to show the engine type associated with updates.
- Refactored app update settings management to include engine normalization and validation.
2026-08-08 19:37:51 +01:00
9 changed files with 290 additions and 47 deletions

View File

@ -30,7 +30,7 @@ const About = () => {
const { token } = useContext(AuthContext)
const { fetchApiServerVersion, fetchWsServerVersion } =
useContext(ApiServerContext)
const { isElectron, getElectronVersion } = useContext(ElectronContext)
const { isElectron, getAppEngine } = useContext(ElectronContext)
const { checkForUpdates } = useContext(AppUpdateContext)
const isMobile = useMediaQuery({ maxWidth: 768 })
@ -74,18 +74,18 @@ const About = () => {
useEffect(() => {
if (!isElectron) return
getElectronVersion()
.then((version) => {
setElectronVersion(version || 'unknown')
getAppEngine()
.then((engine) => {
setAppEngine(engine === 'chromium' ? 'Chromium' : 'Native')
})
.catch(() => {
setElectronVersion('unknown')
setAppEngine('Unknown')
})
}, [getElectronVersion, isElectron])
}, [getAppEngine, isElectron])
const [apiServerVersion, setApiServerVersion] = useState(null)
const [wsServerVersion, setWsServerVersion] = useState(null)
const [electronVersion, setElectronVersion] = useState(null)
const [appEngine, setAppEngine] = useState(null)
const apiServerVersionText = apiServerVersion ? (
<Text>
@ -99,8 +99,8 @@ const About = () => {
) : (
<Skeleton.Input active size='small' className='text-skeleton' />
)
const electronVersionText = electronVersion ? (
<Text>{`v${electronVersion}`}</Text>
const appEngineText = appEngine ? (
<Text>{appEngine}</Text>
) : (
<Skeleton.Input active size='small' className='text-skeleton' />
)
@ -153,9 +153,7 @@ const About = () => {
</Text>
</Text>
{isElectron && (
<Text type='secondary'>
Electron: {electronVersionText}
</Text>
<Text type='secondary'>Engine: {appEngineText}</Text>
)}
<Text type='secondary'>REST API: {apiServerVersionText}</Text>

View File

@ -88,6 +88,16 @@ const NewAppUpdate = ({ update, onCancel, onUpdate }) => {
<Text style={{ margin: 0 }} type='secondary'>
Branch: <Text>{update?.branch || 'Unknown'}</Text>
</Text>
<Text style={{ margin: 0 }} type='secondary'>
Engine:{' '}
<Text>
{update?.engine === 'chromium'
? 'Chromium'
: update?.engine === 'native'
? 'Native'
: 'Unknown'}
</Text>
</Text>
</Flex>
<Dropdown menu={actionsMenu}>
<Button size='small' type='text'>

View File

@ -5,6 +5,10 @@ import { useThemeContext } from '../context/ThemeContext'
import { ApiServerContext } from '../context/ApiServerContext'
import { ElectronContext } from '../context/ElectronContext'
import { AuthContext } from '../context/AuthContext'
import {
normalizeAppUpdateEngine,
useAppUpdateContext
} from '../context/AppUpdateContext'
import { useMessageContext } from '../context/MessageContext'
import useCollapseState from '../hooks/useCollapseState'
import InfoCollapse from '../common/InfoCollapse'
@ -14,13 +18,22 @@ import EditButtons from '../common/EditButtons'
const { Text } = Typography
const { Option } = Select
const DEFAULT_UPDATE_BRANCH = 'main'
const DEFAULT_UPDATE_ENGINE = 'native'
const engineLabel = (engine) => {
const normalized = normalizeAppUpdateEngine(engine)
if (normalized === 'chromium') return 'Chromium'
if (normalized === 'native') return 'Native'
return 'Not configured'
}
const Settings = () => {
const { isDarkMode, isCompact, isSystem, setThemeMode, setDensityMode } =
useThemeContext()
const { fetchAppUpdateBranches } = useContext(ApiServerContext)
const { isElectron, getAppSettings, setAppSettings } =
const { isElectron, getAppSettings, setAppSettings, getAppEngine } =
useContext(ElectronContext)
const { recheckForUpdates } = useAppUpdateContext()
const { userProfile, setUserProfile } = useContext(AuthContext)
const { showSuccess, showError } = useMessageContext()
const [collapseState, updateCollapseState] = useCollapseState('Settings', {
@ -34,20 +47,31 @@ const Settings = () => {
const [draftSettings, setDraftSettings] = useState({})
const [branches, setBranches] = useState([])
const [branchLoading, setBranchLoading] = useState(false)
const [runningEngine, setRunningEngine] = useState(DEFAULT_UPDATE_ENGINE)
useEffect(() => {
const loadSettings = async () => {
setSettingsLoading(true)
const storedSettings = isElectron
? await getAppSettings()
: userProfile?.settings || {}
setAppSettingsState(storedSettings || {})
setDraftSettings(storedSettings || {})
const [storedSettings, detectedEngine] = await Promise.all([
isElectron ? getAppSettings() : Promise.resolve(userProfile?.settings || {}),
isElectron ? getAppEngine() : Promise.resolve(DEFAULT_UPDATE_ENGINE)
])
const nextEngine =
normalizeAppUpdateEngine(detectedEngine) || DEFAULT_UPDATE_ENGINE
setRunningEngine(nextEngine)
const nextSettings = { ...(storedSettings || {}) }
if (isElectron && !normalizeAppUpdateEngine(nextSettings.appUpdateEngine)) {
nextSettings.appUpdateEngine = nextEngine
}
setAppSettingsState(nextSettings)
setDraftSettings(nextSettings)
setSettingsLoading(false)
}
loadSettings()
}, [getAppSettings, isElectron, userProfile?.settings])
}, [getAppEngine, getAppSettings, isElectron, userProfile?.settings])
useEffect(() => {
if (settingsLoading || isEditing) return
@ -118,12 +142,17 @@ const Settings = () => {
(branches.includes(DEFAULT_UPDATE_BRANCH) ? DEFAULT_UPDATE_BRANCH : null) ||
branches[0] ||
'Not configured'
const currentEngine =
normalizeAppUpdateEngine(appSettings.appUpdateEngine) ||
normalizeAppUpdateEngine(runningEngine) ||
DEFAULT_UPDATE_ENGINE
const startEditing = () => {
setDraftSettings({
...appSettings,
appUpdateBranch:
currentBranch === 'Not configured' ? undefined : currentBranch,
appUpdateEngine: currentEngine,
theme: currentThemeValue,
density: currentDensityValue
})
@ -139,12 +168,18 @@ const Settings = () => {
setSaving(true)
try {
const nextEngine =
normalizeAppUpdateEngine(draftSettings.appUpdateEngine) ||
currentEngine
const nextSettings = {
...appSettings,
theme: draftSettings.theme,
density: draftSettings.density,
...(isElectron
? { appUpdateBranch: draftSettings.appUpdateBranch }
? {
appUpdateBranch: draftSettings.appUpdateBranch,
appUpdateEngine: nextEngine
}
: {})
}
const saved = isElectron
@ -173,6 +208,16 @@ const Settings = () => {
setDraftSettings(nextSettings)
setIsEditing(false)
showSuccess('Settings saved.')
const appUpdateSettingsChanged =
isElectron &&
(nextSettings.appUpdateBranch !== appSettings.appUpdateBranch ||
nextSettings.appUpdateEngine !==
normalizeAppUpdateEngine(appSettings.appUpdateEngine))
if (appUpdateSettingsChanged) {
void recheckForUpdates()
}
} finally {
setSaving(false)
}
@ -196,7 +241,9 @@ const Settings = () => {
startEditing={startEditing}
formValid={
Boolean(draftSettings.theme && draftSettings.density) &&
(!isElectron || Boolean(draftSettings.appUpdateBranch))
(!isElectron ||
(Boolean(draftSettings.appUpdateBranch) &&
Boolean(normalizeAppUpdateEngine(draftSettings.appUpdateEngine))))
}
disabled={settingsLoading || (!isElectron && !userProfile)}
loading={saving}
@ -297,6 +344,29 @@ const Settings = () => {
<Text>{currentBranch}</Text>
)}
</Descriptions.Item>
<Descriptions.Item label='Engine'>
{isEditing ? (
<Select
value={
normalizeAppUpdateEngine(draftSettings.appUpdateEngine) ||
currentEngine
}
onChange={(value) =>
setDraftSettings((previous) => ({
...previous,
appUpdateEngine: value
}))
}
style={{ width: '100%' }}
placeholder='Select an engine'
>
<Option value='native'>Native</Option>
<Option value='chromium'>Chromium</Option>
</Select>
) : (
<Text>{engineLabel(currentEngine)}</Text>
)}
</Descriptions.Item>
</Descriptions>
</InfoCollapse>
)}

View File

@ -20,10 +20,22 @@ import SoftwareUpdateIcon from '../../Icons/SoftwareUpdateIcon'
const { Text } = Typography
const UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1000
const DEFAULT_MODEL_WIDTH = 710
const DEFAULT_UPDATE_BRANCH = 'main'
const DEFAULT_UPDATE_ENGINE = 'native'
const CURRENT_BUILD_NUMBER = import.meta.env.VITE_BUILD_NUMBER
const APP_UPDATE_DISMISSED_KEY = 'appUpdateDismissed'
export const normalizeAppUpdateEngine = (engine) => {
const value = String(engine || '')
.trim()
.toLowerCase()
if (value === 'chromium' || value === 'cef') return 'chromium'
if (value === 'native') return 'native'
return null
}
const getDismissedUpdate = () => {
try {
const stored = sessionStorage.getItem(APP_UPDATE_DISMISSED_KEY)
@ -42,7 +54,9 @@ const isUpdateDismissed = (update) => {
return (
dismissed.version === update.version &&
dismissed.buildNumber === update.buildNumber &&
dismissed.branch === update.branch
dismissed.branch === update.branch &&
normalizeAppUpdateEngine(dismissed.engine) ===
normalizeAppUpdateEngine(update.engine)
)
}
@ -54,7 +68,8 @@ const saveDismissedUpdate = (update) => {
JSON.stringify({
version: update.version,
buildNumber: update.buildNumber,
branch: update.branch
branch: update.branch,
engine: normalizeAppUpdateEngine(update.engine) || DEFAULT_UPDATE_ENGINE
})
)
}
@ -109,8 +124,14 @@ export const AppUpdateProvider = ({ children }) => {
const { fetchAppUpdateBranches, fetchAppUpdateCurrent } =
useContext(ApiServerContext)
const { token } = useContext(AuthContext)
const { isElectron, getAppSettings, startAppUpdate, onAppUpdateProgress } =
useContext(ElectronContext)
const {
isElectron,
getAppSettings,
setAppSettings,
getAppEngine,
startAppUpdate,
onAppUpdateProgress
} = useContext(ElectronContext)
const [checking, setChecking] = useState(false)
const [noUpdateOpen, setNoUpdateOpen] = useState(false)
const [availableUpdate, setAvailableUpdate] = useState(null)
@ -120,12 +141,14 @@ export const AppUpdateProvider = ({ children }) => {
const runningCheckRef = useRef(null)
const updateCheckDependenciesRef = useRef({})
const [modelWidth, setModelWidth] = useState(650)
const [modelWidth, setModelWidth] = useState(DEFAULT_MODEL_WIDTH)
updateCheckDependenciesRef.current = {
fetchAppUpdateBranches,
fetchAppUpdateCurrent,
getAppSettings,
setAppSettings,
getAppEngine,
isElectron,
token
}
@ -135,6 +158,8 @@ export const AppUpdateProvider = ({ children }) => {
fetchAppUpdateBranches,
fetchAppUpdateCurrent,
getAppSettings,
setAppSettings,
getAppEngine,
isElectron,
token
} = updateCheckDependenciesRef.current
@ -143,9 +168,10 @@ export const AppUpdateProvider = ({ children }) => {
if (runningCheckRef.current) return runningCheckRef.current
const checkPromise = (async () => {
const [branches, appSettings] = await Promise.all([
const [branches, appSettings, runningEngine] = await Promise.all([
fetchAppUpdateBranches(),
getAppSettings()
getAppSettings(),
getAppEngine()
])
const configuredBranch = appSettings?.appUpdateBranch
const defaultBranch = branches.includes(DEFAULT_UPDATE_BRANCH)
@ -157,11 +183,52 @@ export const AppUpdateProvider = ({ children }) => {
if (!selectedBranch) return null
const update = await fetchAppUpdateCurrent(selectedBranch)
const selectedEngine =
normalizeAppUpdateEngine(appSettings?.appUpdateEngine) ||
normalizeAppUpdateEngine(runningEngine) ||
DEFAULT_UPDATE_ENGINE
const currentRunningEngine =
normalizeAppUpdateEngine(runningEngine) || DEFAULT_UPDATE_ENGINE
return isAppUpdateAvailable(update, appVersion, CURRENT_BUILD_NUMBER)
? update
: null
const settingsUpdates = {}
if (!appSettings?.appUpdateRunningBranch) {
settingsUpdates.appUpdateRunningBranch = selectedBranch
}
if (!normalizeAppUpdateEngine(appSettings?.appUpdateEngine)) {
settingsUpdates.appUpdateEngine = selectedEngine
}
if (Object.keys(settingsUpdates).length > 0) {
await setAppSettings({
...appSettings,
...settingsUpdates
})
}
const runningBranch =
appSettings?.appUpdateRunningBranch ||
settingsUpdates.appUpdateRunningBranch ||
selectedBranch
const update = await fetchAppUpdateCurrent(selectedBranch)
if (!update) return null
const newerVersionAvailable = isAppUpdateAvailable(
update,
appVersion,
CURRENT_BUILD_NUMBER
)
const engineMismatch = selectedEngine !== currentRunningEngine
const branchMismatch = selectedBranch !== runningBranch
if (!newerVersionAvailable && !engineMismatch && !branchMismatch) {
return null
}
return {
...update,
branch: update.branch || selectedBranch,
engine: selectedEngine
}
})()
runningCheckRef.current = checkPromise
@ -181,6 +248,7 @@ export const AppUpdateProvider = ({ children }) => {
setNoUpdateOpen(false)
setAvailableUpdate(update)
if (forcePrompt || !isUpdateDismissed(update)) {
setModelWidth(DEFAULT_MODEL_WIDTH)
setUpdatePromptOpen(true)
}
}
@ -205,6 +273,11 @@ export const AppUpdateProvider = ({ children }) => {
}
}, [isElectron, showUpdateIfAvailable])
const recheckForUpdates = useCallback(async () => {
if (!isElectron) return null
return showUpdateIfAvailable({ forcePrompt: true })
}, [isElectron, showUpdateIfAvailable])
useEffect(() => {
if (!isElectron) return undefined
@ -252,7 +325,15 @@ export const AppUpdateProvider = ({ children }) => {
})
try {
const result = await startAppUpdate(update)
const appSettings = await getAppSettings()
const engine =
normalizeAppUpdateEngine(update?.engine) ||
normalizeAppUpdateEngine(appSettings?.appUpdateEngine) ||
DEFAULT_UPDATE_ENGINE
const result = await startAppUpdate({
...update,
engine
})
if (!result) {
throw new Error(
@ -273,7 +354,9 @@ export const AppUpdateProvider = ({ children }) => {
Boolean(installingUpdate) && updateProgress?.phase !== 'error'
return (
<AppUpdateContext.Provider value={{ availableUpdate, checkForUpdates }}>
<AppUpdateContext.Provider
value={{ availableUpdate, checkForUpdates, recheckForUpdates }}
>
{children}
<Modal
open={checking}

View File

@ -369,6 +369,15 @@ const ElectronProvider = ({ children }) => {
return await ipcRenderer.invoke('electron-version')
}, [electronAvailable, useElectrobun])
const getAppEngine = useCallback(async () => {
if (!electronAvailable) return 'native'
if (useElectrobun) {
const engine = await desktopBridge.getAppEngine()
return engine === 'chromium' ? 'chromium' : 'native'
}
return 'native'
}, [electronAvailable, useElectrobun])
return (
<ElectronContext.Provider
value={{
@ -390,7 +399,8 @@ const ElectronProvider = ({ children }) => {
setToken,
resizeSpotlightWindow,
setSidebarViewMenu,
getElectronVersion
getElectronVersion,
getAppEngine
}}
>
{children}

View File

@ -13,6 +13,7 @@ import { ApiServerContext } from './ApiServerContext'
import NotificationCenter from '../common/NotificationCenter'
import Notification from '../common/Notification'
import { useMediaQuery } from 'react-responsive'
import { ElectronContext } from './ElectronContext'
const NotificationContext = createContext()
@ -35,6 +36,8 @@ const NotificationProvider = ({ children }) => {
const [notifications, setNotifications] = useState([])
const [notificationsLoading, setNotificationsLoading] = useState(false)
const { isElectron } = useContext(ElectronContext)
const isMobile = useMediaQuery({ maxWidth: 768 })
const fetchNotifications = useCallback(async () => {
@ -180,6 +183,7 @@ const NotificationProvider = ({ children }) => {
title='Notifications'
placement='right'
width={isMobile ? '100%' : 460}
style={{ marginTop: isElectron ? '40px' : '0px' }}
onClose={() => setNotificationCenterVisible(false)}
open={notificationCenterVisible}
>

View File

@ -8,6 +8,7 @@ import { Utils } from "electrobun/bun";
import { launchMacInstaller } from "./macappupdate.js";
import { launchWindowsInstaller } from "./winappupdate.js";
import { scheduleAppRestart } from "./updater-runner.js";
import { getAppSettings, setAppSettings } from "./store.js";
const SUPPORTED_TARGETS = {
darwin: {
@ -20,6 +21,8 @@ const SUPPORTED_TARGETS = {
},
};
const DEFAULT_UPDATE_ENGINE = "native";
let runningUpdate = null;
const getArtifactName = (artifact) =>
@ -31,6 +34,33 @@ const normalizeArch = (arch) => {
return arch;
};
const normalizeEngine = (engine) => {
const value = String(engine || "")
.trim()
.toLowerCase();
if (value === "chromium" || value === "cef") return "chromium";
if (value === "native") return "native";
return null;
};
const artifactIsChromium = (artifact) => {
const explicit = String(
artifact?.engine || artifact?.renderer || "",
).toLowerCase();
if (explicit === "cef" || explicit === "chromium") return true;
if (explicit === "native") return false;
const name = getArtifactName(artifact).toLowerCase();
return /[-_.]cef(?:[-_.]|$)/.test(name);
};
const artifactMatchesEngine = (artifact, engine) => {
const wantsChromium = normalizeEngine(engine) === "chromium";
return artifactIsChromium(artifact) === wantsChromium;
};
const artifactMatchesPlatform = (artifact, target, platform, arch) => {
const name = getArtifactName(artifact).toLowerCase();
const normalizedArch = normalizeArch(arch);
@ -69,22 +99,23 @@ const selectUpdateArtifact = (
throw new Error(`App updates are not supported on ${platform}.`);
}
const engine =
normalizeEngine(update?.engine) || DEFAULT_UPDATE_ENGINE;
const artifacts = Array.isArray(update?.artifacts) ? update.artifacts : [];
const matchingArtifact = artifacts.find((artifact) =>
artifactMatchesPlatform(artifact, target, platform, arch),
const matchingArtifact = artifacts.find(
(artifact) =>
artifactMatchesPlatform(artifact, target, platform, arch) &&
artifactMatchesEngine(artifact, engine),
);
const fallbackArtifact = artifacts.find((artifact) => {
const name = getArtifactName(artifact).toLowerCase();
return artifact?.url && name.endsWith(target.extension);
});
if (!matchingArtifact && !fallbackArtifact) {
if (!matchingArtifact) {
const engineLabel = engine === "chromium" ? "Chromium (cef)" : "Native";
throw new Error(
`No ${target.extension} update artifact found for ${platform}/${arch}.`,
`No ${target.extension} ${engineLabel} update artifact found for ${platform}/${arch}.`,
);
}
return matchingArtifact || fallbackArtifact;
return matchingArtifact;
};
const getInstallErrorMessage = (error, output = "") => {
@ -181,10 +212,34 @@ const downloadArtifact = async (artifact, destinationPath, sendProgress) => {
});
};
const persistInstalledUpdateSettings = async (update) => {
try {
const settings = await getAppSettings();
const engine =
normalizeEngine(update?.engine) ||
normalizeEngine(settings?.appUpdateEngine) ||
DEFAULT_UPDATE_ENGINE;
await setAppSettings({
...settings,
appUpdateEngine: engine,
...(update?.branch
? { appUpdateRunningBranch: update.branch }
: {}),
});
} catch (error) {
console.warn(
"[app-update] Failed to persist installed update settings.",
error,
);
}
};
const launchInstallerAndRestart = async (
mainWindow,
installerPath,
sendProgress,
update,
) => {
const installerHelpers = { sendProgress, getInstallErrorMessage };
@ -206,6 +261,8 @@ const launchInstallerAndRestart = async (
throw new Error(`App updates are not supported on ${process.platform}.`);
}
await persistInstalledUpdateSettings(update);
if (process.platform === "darwin") {
scheduleAppRestart();
}
@ -241,7 +298,12 @@ const runAppUpdate = async (mainWindow, update, sendProgress) => {
message: "Update downloaded",
});
await launchInstallerAndRestart(mainWindow, installerPath, sendProgress);
await launchInstallerAndRestart(
mainWindow,
installerPath,
sendProgress,
update,
);
};
export function startAppUpdate(mainWindow, update, sendProgress) {

View File

@ -72,7 +72,12 @@ export function createAppRpc() {
setSidebarViewMenu: async ({ sections }) => ({
ok: setSidebarViewMenu(sections)
}),
getAppVersion: async () => process.env.ELECTROBUN_VERSION || 'desktop'
getAppVersion: async () => process.env.ELECTROBUN_VERSION || 'desktop',
getAppEngine: async () => {
const mainWindow = getMainWindow()
const renderer = mainWindow?.renderer || 'native'
return renderer === 'cef' ? 'chromium' : 'native'
}
},
messages: {}
}

View File

@ -185,7 +185,8 @@ const electronAPI = {
invokeRequest('resizeSpotlightWindow', { height }),
setSidebarViewMenu: (sections) =>
invokeRequest('setSidebarViewMenu', { sections }),
getAppVersion: () => invokeRequest('getAppVersion')
getAppVersion: () => invokeRequest('getAppVersion'),
getAppEngine: () => invokeRequest('getAppEngine')
}
if (typeof window !== 'undefined') {