/** * Ansichtseinstellungen: Tag/Nacht, Fahrbahn unter dem Fahrzeug, Beschriftung * der Menüleiste. Übernommen aus dem alten Panel (A6 des Design-Audits): * gespeicherte Wahl schlägt Systemeinstellung, Standard für das Thema ist * „nacht". * * WARUM HIER EIN GETEILTER SPEICHER STEHT UND NICHT DREIMAL useState * ------------------------------------------------------------------ * Bis 2026-08-31 legte jeder dieser Haken sein eigenes `useState` an. Wer ihn * zweimal aufrief, bekam zwei voneinander unabhängige Kopien desselben Werts: * * useTheme() → App.tsx (setzt data-theme), Einstellungen.tsx (der Schalter), * Typenschild.tsx (zweimal, für die Farbe des Schilds) * useTabBeschriftung() → Einstellungen.tsx (der Schalter), App.tsx (die Leiste) * * Der Schalter in den Einstellungen änderte damit nur seine eigene Kopie und * den Speicher der Ablage — die Kopie in App.tsx blieb stehen, `data-theme` * wechselte nie, und die Umschaltung wirkte erst nach einem Neustart der App. * Genau so gemeldet: „Tag/Nacht Umschaltung wirkungslos". Die übrigen * Schalter hatten denselben Fehler, nur ist er dort weniger auffällig. * * `useSyncExternalStore` (in React eingebaut) löst das an der Wurzel: es gibt * je Einstellung genau einen Wert, jeder Aufrufer sieht denselben, und ein * Setzen benachrichtigt alle. Das ist der React-eigene Weg für einen Zustand, * der außerhalb des Komponentenbaums lebt — kein Kontext-Anbieter nötig, und * die Aufrufstellen bleiben unverändert. * * Alle Zugriffe auf die Ablage sind abgesichert: im Privatmodus mancher * Browser wirft schon das Lesen. */ import { useCallback, useEffect, useSyncExternalStore } from "react" export type Theme = "nacht" | "tag" function ablageLesen(schluessel: string): string | null { try { return localStorage.getItem(schluessel) } catch { return null } } function ablageSchreiben(schluessel: string, wert: string): void { try { localStorage.setItem(schluessel, wert) } catch { // Privatmodus — die Wahl gilt dann nur für diese Sitzung. } } /** * Ein Wert, den sich alle Aufrufer teilen. * * `wert` wird beim ersten Zugriff aus der Ablage gelesen und danach im * Speicher gehalten — `useSyncExternalStore` verlangt, dass zwei Aufrufe von * `stand()` ohne zwischenzeitliche Änderung dasselbe liefern. Bei jedem Mal * neu aus der Ablage zu lesen genügt dem zwar auch, aber nur zufällig; ein * gehaltener Wert macht die Zusage explizit. */ function einstellung( schluessel: string, ausAblage: (roh: string | null) => T, inAblage: (wert: T) => string, ) { let wert = ausAblage(ablageLesen(schluessel)) const horcher = new Set<() => void>() return { stand: () => wert, /** Ob überhaupt etwas in der Ablage steht - also eine bewusste Wahl. */ gewaehlt: () => ablageLesen(schluessel) !== null, /* Setzt den gehaltenen Wert, OHNE ihn zu speichern. Für Werte, die dem System folgen, solange niemand selbst gewählt hat: eine Systemumstellung ist keine Entscheidung des Nutzers und darf deshalb keine ablegen. */ ausSystem: (neu: T) => { if (neu === wert) return wert = neu for (const bei of horcher) bei() }, horchen: (bei: () => void) => { horcher.add(bei) return () => horcher.delete(bei) }, setzen: (neu: T) => { if (neu === wert) return wert = neu ablageSchreiben(schluessel, inAblage(neu)) for (const bei of horcher) bei() }, } } const THEMA = einstellung( "dm360.theme", (roh) => { if (roh === "nacht" || roh === "tag") return roh // Ohne gespeicherte Wahl entscheidet das System; ohne beides: nacht. const hell = typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: light)").matches return hell ? "tag" : "nacht" }, (wert) => wert, ) /* Systemumstellung im laufenden Betrieb nachziehen. Der Startwert oben liest `prefers-color-scheme` genau einmal, beim Laden des Moduls. Stellt das Gerät danach um - etwa durch die automatische Umschaltung bei Sonnenuntergang -, folgte das Stylesheet sofort (es hängt selbst an der Medienabfrage), der React-Zustand aber nie. Alles, was sein Bild aus `useTheme()` wählt statt aus CSS, blieb damit auf der alten Fassung stehen: die Vier Ringe und das Typenschild sind `` mit eingebettetem SVG und je einer hellen und einer dunklen Fassung (siehe VierRinge in Typenschild.tsx). Auf dunklem Grund stand dann die dunkle Fassung - schwarz auf schwarz, vom Eigentümer am 05.09.2026 gemeldet. Ein Neuladen räumte es auf, weshalb es sprunghaft wirkte. Nur solange niemand selbst gewählt hat: eine gespeicherte Wahl in "Einstellungen" bleibt unangetastet. */ if (typeof window !== "undefined" && typeof window.matchMedia === "function") { const dunkel = window.matchMedia("(prefers-color-scheme: dark)") const nachziehen = () => { if (THEMA.gewaehlt()) return THEMA.ausSystem(dunkel.matches ? "nacht" : "tag") } if (typeof dunkel.addEventListener === "function") dunkel.addEventListener("change", nachziehen) else if (typeof dunkel.addListener === "function") dunkel.addListener(nachziehen) } const TABBESCHRIFTUNG = einstellung( "dm360.tabbeschriftung", (roh) => roh !== "aus", (an) => (an ? "an" : "aus"), ) /** Nur für Tests: setzt den gehaltenen Wert auf den Stand der Ablage zurück. */ export function themeStartwert(): Theme { return THEMA.stand() } export function useTheme(): [Theme, (theme: Theme) => void] { const theme = useSyncExternalStore(THEMA.horchen, THEMA.stand, THEMA.stand) const setzen = useCallback((neu: Theme) => THEMA.setzen(neu), []) // Farbe der Systemleisten mitziehen, damit auf dem Handy kein heller // Balken über einer dunklen App steht. Werte des Panels (--canvas in // audi-dashboard-ios.css); hier standen einmal #161b23/#ffffff aus dem // Basis-Stylesheet, das im Panel nie greift — die Systemleiste hatte damit // einen anderen Ton als die App darunter. useEffect(() => { const marke = document.querySelector('meta[name="theme-color"]') if (marke) marke.setAttribute("content", theme === "nacht" ? "#0C1014" : "#F2F2F7") }, [theme]) return [theme, setzen] } /* useBoden() ist am 2026-08-31 entfallen: der Bodenschatten unter dem Fahrzeug ist weg (Ansage des Eigentuemers), damit hatte der Schalter nichts mehr zu schalten. Der abgelegte Wert unter "dm360.boden" bleibt liegen und stoert nicht - er wird schlicht nicht mehr gelesen. */ /** Sichtbarkeit der Tab-Beschriftungen, ebenfalls dauerhaft gespeichert. */ export function useTabBeschriftung(): [boolean, (an: boolean) => void] { const an = useSyncExternalStore( TABBESCHRIFTUNG.horchen, TABBESCHRIFTUNG.stand, TABBESCHRIFTUNG.stand, ) const setzen = useCallback((neu: boolean) => TABBESCHRIFTUNG.setzen(neu), []) return [an, setzen] }