TypeScript · 8270 bytes Raw Blame History
1 import { useState, useEffect } from "react";
2 import { getConfig, updateConfig, getProtonVersions, runDoctor } from "../hooks/useApi";
3 import type { ConfigDto, ProtonInfo, DoctorReport } from "../types";
4
5 export default function SettingsView() {
6 const [config, setConfig] = useState<ConfigDto | null>(null);
7 const [protonVersions, setProtonVersions] = useState<ProtonInfo[]>([]);
8 const [doctor, setDoctor] = useState<DoctorReport | null>(null);
9 const [loading, setLoading] = useState(true);
10 const [saving, setSaving] = useState(false);
11 const [runningDoctor, setRunningDoctor] = useState(false);
12 const [error, setError] = useState<string | null>(null);
13 const [success, setSuccess] = useState<string | null>(null);
14
15 useEffect(() => {
16 loadData();
17 }, []);
18
19 async function loadData() {
20 try {
21 setLoading(true);
22 const [cfg, proton] = await Promise.all([getConfig(), getProtonVersions()]);
23 setConfig(cfg);
24 setProtonVersions(proton);
25 } catch (err) {
26 setError(String(err));
27 } finally {
28 setLoading(false);
29 }
30 }
31
32 async function handleSave() {
33 if (!config) return;
34
35 setSaving(true);
36 setError(null);
37 setSuccess(null);
38
39 try {
40 await updateConfig(config);
41 setSuccess("Settings saved successfully!");
42 setTimeout(() => setSuccess(null), 3000);
43 } catch (err) {
44 setError(String(err));
45 } finally {
46 setSaving(false);
47 }
48 }
49
50 async function handleRunDoctor() {
51 setRunningDoctor(true);
52 try {
53 const report = await runDoctor();
54 setDoctor(report);
55 } catch (err) {
56 setError(String(err));
57 } finally {
58 setRunningDoctor(false);
59 }
60 }
61
62 if (loading || !config) {
63 return (
64 <div>
65 <div className="page-header">
66 <h1 className="page-title">Settings</h1>
67 </div>
68 <div style={{ textAlign: "center", padding: "50px" }}>
69 <div className="spinner" style={{ margin: "0 auto" }} />
70 </div>
71 </div>
72 );
73 }
74
75 return (
76 <div>
77 <div className="page-header">
78 <h1 className="page-title">Settings</h1>
79 </div>
80
81 {error && (
82 <div
83 className="card"
84 style={{ backgroundColor: "rgba(248, 113, 113, 0.1)", marginBottom: "20px" }}
85 >
86 <p style={{ color: "var(--error)" }}>{error}</p>
87 </div>
88 )}
89
90 {success && (
91 <div
92 className="card"
93 style={{ backgroundColor: "rgba(74, 222, 128, 0.1)", marginBottom: "20px" }}
94 >
95 <p style={{ color: "var(--success)" }}>{success}</p>
96 </div>
97 )}
98
99 {/* Steam Settings */}
100 <div className="card">
101 <h2 className="card-title" style={{ marginBottom: "20px" }}>
102 Steam
103 </h2>
104
105 <div className="form-group">
106 <label className="form-label">Steam Installation Path (leave empty for auto-detect)</label>
107 <input
108 type="text"
109 className="form-input"
110 value={config.steam_path || ""}
111 onChange={(e) => setConfig({ ...config, steam_path: e.target.value || null })}
112 placeholder="Auto-detect"
113 />
114 </div>
115
116 <div className="form-group">
117 <label className="form-checkbox">
118 <input
119 type="checkbox"
120 checked={config.scan_flatpak}
121 onChange={(e) => setConfig({ ...config, scan_flatpak: e.target.checked })}
122 />
123 <span>Scan Flatpak Steam installation</span>
124 </label>
125 </div>
126 </div>
127
128 {/* Proton Settings */}
129 <div className="card">
130 <h2 className="card-title" style={{ marginBottom: "20px" }}>
131 Proton
132 </h2>
133
134 <div className="form-group">
135 <label className="form-label">Preferred Proton Version</label>
136 <select
137 className="form-input"
138 value={config.preferred_proton || ""}
139 onChange={(e) => setConfig({ ...config, preferred_proton: e.target.value || null })}
140 >
141 <option value="">Auto (Recommended)</option>
142 {protonVersions.map((v) => (
143 <option key={v.name} value={v.name}>
144 {v.name}{" "}
145 {v.compatibility === "recommended"
146 ? "(Recommended)"
147 : v.compatibility === "unsupported"
148 ? "(Unsupported)"
149 : ""}
150 </option>
151 ))}
152 </select>
153 </div>
154
155 {protonVersions.length > 0 && (
156 <div style={{ marginTop: "15px" }}>
157 <p style={{ fontSize: "0.8rem", color: "var(--text-secondary)", marginBottom: "10px" }}>
158 Available Proton versions:
159 </p>
160 <div style={{ display: "flex", flexWrap: "wrap", gap: "8px" }}>
161 {protonVersions.map((v) => (
162 <span
163 key={v.name}
164 className={`badge ${
165 v.compatibility === "recommended"
166 ? "badge-success"
167 : v.compatibility === "supported"
168 ? "badge-success"
169 : v.compatibility === "experimental"
170 ? "badge-warning"
171 : "badge-error"
172 }`}
173 >
174 {v.name}
175 </span>
176 ))}
177 </div>
178 </div>
179 )}
180 </div>
181
182 {/* WeMod Settings */}
183 <div className="card">
184 <h2 className="card-title" style={{ marginBottom: "20px" }}>
185 WeMod
186 </h2>
187
188 <div className="form-group">
189 <label className="form-checkbox">
190 <input
191 type="checkbox"
192 checked={config.auto_update_wemod}
193 onChange={(e) => setConfig({ ...config, auto_update_wemod: e.target.checked })}
194 />
195 <span>Automatically update WeMod</span>
196 </label>
197 </div>
198 </div>
199
200 <button className="btn btn-primary" onClick={handleSave} disabled={saving}>
201 {saving ? "Saving..." : "Save Settings"}
202 </button>
203
204 {/* Diagnostics */}
205 <div className="card" style={{ marginTop: "30px" }}>
206 <div className="card-header">
207 <h2 className="card-title">Diagnostics</h2>
208 <button
209 className="btn btn-secondary btn-small"
210 onClick={handleRunDoctor}
211 disabled={runningDoctor}
212 >
213 {runningDoctor ? "Running..." : "Run Doctor"}
214 </button>
215 </div>
216
217 {doctor && (
218 <div className="doctor-section">
219 <div className="doctor-item">
220 <span className={`doctor-status ${doctor.steam_ok ? "ok" : "error"}`} />
221 <span>
222 Steam: {doctor.steam_ok ? `Found at ${doctor.steam_path}` : "Not found"}
223 </span>
224 </div>
225 <div className="doctor-item">
226 <span className={`doctor-status ${doctor.proton_ok ? "ok" : "error"}`} />
227 <span>
228 Proton: {doctor.proton_ok ? `${doctor.proton_count} versions found` : "Not found"}
229 </span>
230 </div>
231 <div className="doctor-item">
232 <span className={`doctor-status ${doctor.prefix_ok ? "ok" : "error"}`} />
233 <span>Prefix: {doctor.prefix_ok ? "OK" : "Not initialized"}</span>
234 </div>
235 <div className="doctor-item">
236 <span className={`doctor-status ${doctor.wemod_ok ? "ok" : "error"}`} />
237 <span>WeMod: {doctor.wemod_ok ? "Installed" : "Not installed"}</span>
238 </div>
239
240 {doctor.issues.length > 0 && (
241 <div style={{ marginTop: "15px" }}>
242 <p style={{ color: "var(--warning)", marginBottom: "10px" }}>Issues:</p>
243 <ul style={{ paddingLeft: "20px" }}>
244 {doctor.issues.map((issue, i) => (
245 <li key={i} style={{ color: "var(--text-secondary)", fontSize: "0.9rem" }}>
246 {issue}
247 </li>
248 ))}
249 </ul>
250 </div>
251 )}
252 </div>
253 )}
254 </div>
255 </div>
256 );
257 }