diff options
Diffstat (limited to 'src/components/FlatpaksTab.tsx')
| -rw-r--r-- | src/components/FlatpaksTab.tsx | 195 |
1 files changed, 195 insertions, 0 deletions
diff --git a/src/components/FlatpaksTab.tsx b/src/components/FlatpaksTab.tsx new file mode 100644 index 0000000..dc73858 --- /dev/null +++ b/src/components/FlatpaksTab.tsx @@ -0,0 +1,195 @@ +import { useEffect, useState } from "react"; +import { + ButtonItem, + ConfirmModal, + Field, + PanelSection, + PanelSectionRow, + Spinner, + Toggle, + showModal, +} from "@decky/ui"; +import { FaCheck, FaCog, FaDownload, FaTimes, FaTrash } from "react-icons/fa"; +import { + checkFlatpakExtensionStatus, + FlatpakApp, + FlatpakAppInfo, + FlatpakExtensionStatus, + getFlatpakApps, + installFlatpakExtension, + removeFlatpakAppOverride, + setFlatpakAppOverride, + uninstallFlatpakExtension, +} from "../api/lsfgApi"; +import { showErrorToast } from "../utils/toastUtils"; +import t from "../i18n/i18n"; + +const runtimeVersions = [ + { version: "23.08", key: "installed_23_08" }, + { version: "24.08", key: "installed_24_08" }, + { version: "25.08", key: "installed_25_08" }, +] as const; + +interface RuntimeRowProps { + version: string; + installed: boolean; + busy: boolean; + onAction: () => void; +} + +function RuntimeRow({ version, installed, busy, onAction }: RuntimeRowProps) { + return ( + <PanelSectionRow> + <Field + label={`Runtime ${version}`} + description={installed ? t("FLATPAK_INSTALLED", "Installed") : t("FLATPAK_NOT_INSTALLED", "Not installed")} + icon={installed ? <FaCheck style={{ color: "green" }} /> : <FaTimes style={{ color: "red" }} />} + > + <ButtonItem layout="below" onClick={onAction} disabled={busy}> + {busy ? <Spinner /> : installed ? <><FaTrash /> {t("FLATPAK_UNINSTALL_BTN", "Uninstall")}</> : <><FaDownload /> {t("FLATPAK_INSTALL_BTN", "Install")}</>} + </ButtonItem> + </Field> + </PanelSectionRow> + ); +} + +interface AppRowProps { + app: FlatpakApp; + busy: boolean; + onToggle: () => void; +} + +function AppRow({ app, busy, onToggle }: AppRowProps) { + const configured = app.has_filesystem_override && app.has_env_override; + const partial = app.has_filesystem_override || app.has_env_override; + const status = configured + ? t("FLATPAK_STATUS_CONFIGURED", "Configured") + : partial + ? t("FLATPAK_STATUS_PARTIAL", "Partial") + : t("FLATPAK_STATUS_NO_OVERRIDES", "No overrides"); + + return ( + <PanelSectionRow> + <Field + label={app.app_name || app.app_id} + description={`${app.app_id} - ${status}`} + icon={<FaCog style={{ color: configured ? "green" : partial ? "orange" : "red" }} />} + > + <Toggle value={configured} onChange={onToggle} disabled={busy} /> + </Field> + </PanelSectionRow> + ); +} + +export function FlatpaksTab() { + const [extensionStatus, setExtensionStatus] = useState<FlatpakExtensionStatus | null>(null); + const [apps, setApps] = useState<FlatpakAppInfo | null>(null); + const [loading, setLoading] = useState(true); + const [operation, setOperation] = useState<string | null>(null); + const [error, setError] = useState<string | null>(null); + + const load = async () => { + setLoading(true); + try { + const [nextStatus, nextApps] = await Promise.all([ + checkFlatpakExtensionStatus(), + getFlatpakApps(), + ]); + setExtensionStatus(nextStatus); + setApps(nextApps); + } catch (loadError) { + setError(String(loadError)); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + void load(); + }, []); + + const runExtensionOperation = async (version: string, installed: boolean) => { + const action = installed ? "uninstall" : "install"; + setOperation(`${action}-${version}`); + setError(null); + try { + const result = installed + ? await uninstallFlatpakExtension(version) + : await installFlatpakExtension(version); + if (!result.success) throw new Error(result.error || result.message); + setExtensionStatus(await checkFlatpakExtensionStatus()); + } catch (operationError) { + const message = String(operationError); + setError(message); + showErrorToast("Flatpak operation failed", message); + } finally { + setOperation(null); + } + }; + + const confirmExtensionOperation = (version: string, installed: boolean) => { + if (!installed) { + void runExtensionOperation(version, false); + return; + } + showModal( + <ConfirmModal + strTitle={t("FLATPAK_UNINSTALL_TITLE", "Uninstall Runtime Extension")} + strDescription={`${t("FLATPAK_UNINSTALL_CONFIRM_PREFIX", "Are you sure you want to uninstall the")} ${version} ${t("FLATPAK_UNINSTALL_CONFIRM_SUFFIX", "runtime extension?")}`} + onOK={() => void runExtensionOperation(version, true)} + onCancel={() => {}} + />, + ); + }; + + const toggleApp = async (app: FlatpakApp) => { + const configured = app.has_filesystem_override && app.has_env_override; + setOperation(`app-${app.app_id}`); + setError(null); + try { + const result = configured + ? await removeFlatpakAppOverride(app.app_id) + : await setFlatpakAppOverride(app.app_id); + if (!result.success) throw new Error(result.error || result.message); + setApps(await getFlatpakApps()); + } catch (operationError) { + const message = String(operationError); + setError(message); + showErrorToast("Flatpak override failed", message); + } finally { + setOperation(null); + } + }; + + if (loading) { + return <PanelSection title={t("FLATPAK_MODAL_TITLE", "Flatpak Extensions")}><PanelSectionRow><Spinner /></PanelSectionRow></PanelSection>; + } + + return ( + <> + <PanelSection title={t("FLATPAK_RUNTIME_INSTALLER", "Runtime Extension Installer")}> + {error && <PanelSectionRow><Field label={t("FLATPAK_OPERATION_ERROR", "Operation failed")} description={error} icon={<FaTimes style={{ color: "red" }} />} /></PanelSectionRow>} + {extensionStatus?.success ? runtimeVersions.map(({ version, key }) => ( + <RuntimeRow + key={version} + version={version} + installed={extensionStatus[key]} + busy={operation === `${extensionStatus[key] ? "uninstall" : "install"}-${version}`} + onAction={() => confirmExtensionOperation(version, extensionStatus[key])} + /> + )) : <PanelSectionRow><Field label={t("FLATPAK_ERROR", "Error")} description={extensionStatus?.error || error || t("FLATPAK_ERROR_STATUS", "Failed to check extension status")} /></PanelSectionRow>} + </PanelSection> + + <PanelSection title={t("FLATPAK_APPS_TITLE", "Flatpak Applications")}> + {apps?.success ? apps.apps.length ? apps.apps.map((app) => ( + <AppRow + key={app.app_id} + app={app} + busy={operation === `app-${app.app_id}`} + onToggle={() => void toggleApp(app)} + /> + )) : <PanelSectionRow><Field label={t("FLATPAK_NO_APPS", "No Flatpak Apps Found")} description={t("FLATPAK_NO_APPS_DESC", "No Flatpak applications are currently installed")} /></PanelSectionRow> : <PanelSectionRow><Field label={t("FLATPAK_ERROR", "Error")} description={apps?.error || error || t("FLATPAK_ERROR_APPS", "Failed to load Flatpak applications")} /></PanelSectionRow>} + </PanelSection> + </> + ); +} |
