168 lines
5.7 KiB
TypeScript
168 lines
5.7 KiB
TypeScript
/** Parent mode: reachable only via `?parentMode=1`, never from the child-facing UI.
|
||
*
|
||
* This hides the settings, it does not protect them - there is no login, and the
|
||
* endpoints behind it are as open as the rest of the API. That matches the device's
|
||
* threat model (a box on a home network) and is stated in the README rather than
|
||
* implied.
|
||
*/
|
||
|
||
import { useEffect, useState } from "react";
|
||
|
||
import { api } from "../api/client";
|
||
import type { Settings } from "../api/types";
|
||
|
||
const FIELDS: Array<[keyof Settings, string, string, number, number, number]> = [
|
||
["min_volume", "Minimale Lautstärke", "Untergrenze am Gerät (0-200)", 0, 200, 1],
|
||
["max_volume", "Maximale Lautstärke", "Was 100 % in der Kinder-Ansicht bedeutet", 0, 200, 1],
|
||
["initial_volume", "Start-Lautstärke", "Beim Einschalten", 0, 200, 1],
|
||
["volume_increment", "Schritt am Drehknopf", "Pro Rasterung", 1, 100, 1],
|
||
["button_leds_brightness", "Tastenbeleuchtung", "0 bis 1", 0, 1, 0.05],
|
||
];
|
||
|
||
export function ParentPanel({ onClose }: { onClose: () => void }) {
|
||
const [settings, setSettings] = useState<Settings | null>(null);
|
||
const [status, setStatus] = useState<string | null>(null);
|
||
const [busy, setBusy] = useState(false);
|
||
|
||
useEffect(() => {
|
||
api
|
||
.settings()
|
||
.then(setSettings)
|
||
.catch((cause: unknown) => setStatus(String(cause)));
|
||
}, []);
|
||
|
||
const save = async () => {
|
||
if (!settings) return;
|
||
setBusy(true);
|
||
setStatus(null);
|
||
try {
|
||
setSettings(await api.saveSettings(settings));
|
||
setStatus("Gespeichert.");
|
||
} catch {
|
||
// The backend rejects an inverted range or an out-of-band start volume.
|
||
setStatus("Nicht gespeichert — bitte die Werte prüfen (min ≤ Start ≤ max).");
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
};
|
||
|
||
const rescan = async () => {
|
||
setStatus("Bibliothek wird eingelesen …");
|
||
await api.refreshLibrary();
|
||
setStatus("Einlesen gestartet. Neue Titel erscheinen gleich von selbst.");
|
||
};
|
||
|
||
return (
|
||
<div className="overlay" style={{ zIndex: 6, background: "oklch(15% 0.03 210 / .75)" }}>
|
||
<div className="sheet" style={{ padding: 28, width: 460, maxHeight: "100%", overflow: "auto" }}>
|
||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||
<div style={{ fontSize: 20, fontWeight: 900, color: "var(--ink)", flex: 1 }}>
|
||
🔧 Eltern-Einstellungen
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
aria-label="Schließen"
|
||
style={{
|
||
border: "none",
|
||
cursor: "pointer",
|
||
background: "oklch(88% 0.02 210)",
|
||
color: "var(--ink)",
|
||
fontSize: 18,
|
||
fontWeight: 900,
|
||
width: 36,
|
||
height: 36,
|
||
borderRadius: 999,
|
||
}}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
{settings === null ? (
|
||
<div style={{ marginTop: 18, color: "oklch(45% 0.03 210)" }}>Wird geladen …</div>
|
||
) : (
|
||
<div style={{ display: "flex", flexDirection: "column", gap: 14, marginTop: 18 }}>
|
||
{FIELDS.map(([key, label, hint, min, max, step]) => (
|
||
<label key={key} style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||
<span style={{ fontSize: 14, fontWeight: 800, color: "var(--ink)" }}>{label}</span>
|
||
<span style={{ fontSize: 12, fontWeight: 600, color: "oklch(50% 0.03 210)" }}>
|
||
{hint}
|
||
</span>
|
||
<input
|
||
type="number"
|
||
min={min}
|
||
max={max}
|
||
step={step}
|
||
value={settings[key]}
|
||
onChange={(event) =>
|
||
setSettings({ ...settings, [key]: Number(event.target.value) })
|
||
}
|
||
style={{
|
||
padding: "8px 12px",
|
||
borderRadius: 10,
|
||
border: "2px solid oklch(85% 0.02 210)",
|
||
fontSize: 16,
|
||
fontWeight: 700,
|
||
}}
|
||
/>
|
||
</label>
|
||
))}
|
||
|
||
<div style={{ display: "flex", gap: 10, marginTop: 6 }}>
|
||
<button
|
||
onClick={() => void save()}
|
||
disabled={busy}
|
||
style={{
|
||
border: "none",
|
||
cursor: "pointer",
|
||
background: "var(--accent)",
|
||
color: "#fff",
|
||
fontSize: 15,
|
||
fontWeight: 800,
|
||
padding: "11px 20px",
|
||
borderRadius: 999,
|
||
}}
|
||
>
|
||
Speichern
|
||
</button>
|
||
<button
|
||
onClick={() => void rescan()}
|
||
style={{
|
||
border: "none",
|
||
cursor: "pointer",
|
||
background: "oklch(88% 0.02 210)",
|
||
color: "var(--ink)",
|
||
fontSize: 15,
|
||
fontWeight: 800,
|
||
padding: "11px 20px",
|
||
borderRadius: 999,
|
||
}}
|
||
>
|
||
Bibliothek neu einlesen
|
||
</button>
|
||
</div>
|
||
|
||
<div
|
||
style={{
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
color: "oklch(50% 0.03 210)",
|
||
lineHeight: 1.5,
|
||
}}
|
||
>
|
||
Speichern schreibt in die <code>config.yml</code> auf dem Gerät. Die neue
|
||
Obergrenze gilt sofort, ohne Neustart.
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{status && (
|
||
<div style={{ marginTop: 14, fontSize: 14, fontWeight: 700, color: "var(--ink)" }}>
|
||
{status}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|