Files
musicmouse/web/src/components/ParentPanel.tsx
2026-08-27 12:32:20 +02:00

168 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 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>
);
}