1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
import { ButtonItem, Field, PanelSection, PanelSectionRow } from "@decky/ui";
import { useCallback, useEffect, useState } from "react";
import {
getFlatpakApps,
prepareFlatpakApp,
removeFlatpakApp,
type FlatpakApp,
} from "../api/lsfgApi";
import { showErrorToast } from "../utils/toastUtils";
interface Props {
enabled: boolean;
}
export function FlatpakSetupSection({ enabled }: Props) {
const [apps, setApps] = useState<FlatpakApp[]>([]);
const [loading, setLoading] = useState(false);
const [busyApp, setBusyApp] = useState("");
const [error, setError] = useState<string | null>(null);
const load = useCallback(async () => {
if (!enabled) {
setApps([]);
setError(null);
return;
}
setLoading(true);
try {
const result = await getFlatpakApps();
if (!result.success) throw new Error(result.error || "Could not list Flatpak applications");
setApps(result.apps || []);
setError(null);
} catch (loadError) {
const message = loadError instanceof Error ? loadError.message : String(loadError);
setError(message);
} finally {
setLoading(false);
}
}, [enabled]);
useEffect(() => {
void load();
}, [load]);
const toggle = async (app: FlatpakApp) => {
if (busyApp || (app.prepared && !app.owned)) return;
setBusyApp(app.app_id);
try {
const result = app.prepared
? await removeFlatpakApp(app.app_id)
: await prepareFlatpakApp(app.app_id);
if (!result.success) throw new Error(result.error || "Flatpak setup failed");
await load();
} catch (operationError) {
const message = operationError instanceof Error ? operationError.message : String(operationError);
showErrorToast("Flatpak setup failed", message);
} finally {
setBusyApp("");
}
};
if (!enabled) return null;
return (
<PanelSection title="Flatpak Setup">
<PanelSectionRow>
<Field
label="Flatpak applications"
description="Prepare an emulator or launcher once. Steam game profiles remain separate."
/>
</PanelSectionRow>
{error && (
<PanelSectionRow>
<Field label="Flatpak unavailable" description={error} />
</PanelSectionRow>
)}
{apps.map((app) => {
const busy = busyApp === app.app_id;
const description = [
app.app_id,
app.runtime_branch ? `runtime ${app.runtime_branch}` : null,
app.error,
].filter(Boolean).join(" · ");
const label = busy
? "Working..."
: app.prepared
? app.owned ? "Remove" : "Prepared externally"
: app.error ? "Unavailable" : "Prepare";
return (
<PanelSectionRow key={app.app_id}>
<Field label={app.app_name} description={description}>
<ButtonItem
layout="below"
disabled={busy || Boolean(app.error) || (app.prepared && !app.owned)}
onClick={() => void toggle(app)}
>
{label}
</ButtonItem>
</Field>
</PanelSectionRow>
);
})}
<PanelSectionRow>
<ButtonItem layout="below" disabled={loading || Boolean(busyApp)} onClick={() => void load()}>
{loading ? "Refreshing..." : "Refresh Flatpaks"}
</ButtonItem>
</PanelSectionRow>
</PanelSection>
);
}
|