From aaeeec75ae297873244f9e3e623b5485a53a231c Mon Sep 17 00:00:00 2001 From: InstaZDLL Date: Sun, 17 May 2026 11:28:53 +0200 Subject: [PATCH] fix(theme): persist preference before view transition Save the next theme value to localStorage before triggering startViewTransition so the choice survives a webview crash on Linux WebKitGTK with NVIDIA + Wayland (issue #34). Also restore the stored value on mount so dark mode now persists across launches everywhere. Closes #34 --- src/contexts/ThemeContext.tsx | 50 ++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx index 24a8c14..95a248d 100644 --- a/src/contexts/ThemeContext.tsx +++ b/src/contexts/ThemeContext.tsx @@ -1,22 +1,64 @@ import { + useCallback, useState, type MouseEvent as ReactMouseEvent, type ReactNode, } from "react"; import { ThemeContext } from "../hooks/useTheme"; +const THEME_STORAGE_KEY = "waveflow.theme.is_dark"; + +// Read the persisted preference synchronously so the very first render already +// matches the user's last choice. Survives crashes during the View Transitions +// animation (issue #34): even if the webview dies mid-toggle, the new value +// has already been written to localStorage before startViewTransition runs. +const readStoredTheme = (): boolean => { + if (typeof window === "undefined") return false; + try { + return window.localStorage.getItem(THEME_STORAGE_KEY) === "true"; + } catch { + return false; + } +}; + +const writeStoredTheme = (isDark: boolean) => { + if (typeof window === "undefined") return; + try { + window.localStorage.setItem(THEME_STORAGE_KEY, isDark ? "true" : "false"); + } catch { + // localStorage unavailable (private mode, quota) — preference simply + // won't survive the next launch. Not worth surfacing to the user. + } +}; + export function ThemeProvider({ children }: { children: ReactNode }) { - const [isDark, setIsDark] = useState(false); + const [isDark, setIsDark] = useState(readStoredTheme); - const toggleTheme = (event?: ReactMouseEvent) => { - const flipTheme = () => setIsDark((prev) => !prev); + const toggleTheme = useCallback((event?: ReactMouseEvent) => { + let nextValue = false; + const flipTheme = () => + setIsDark((prev) => { + nextValue = !prev; + return nextValue; + }); + + // Persist BEFORE triggering any animation. Some Linux WebKitGTK builds + // crash the webview during startViewTransition on certain GPU/Wayland + // stacks (issue #34) — writing first guarantees the next launch picks + // up the new theme even if this transition kills the process. + const persistNext = (value: boolean) => writeStoredTheme(value); // Fallback: no View Transitions API support → instant swap if (typeof document === "undefined" || !document.startViewTransition) { flipTheme(); + persistNext(nextValue); return; } + // Compute & persist the future value before the animation starts so the + // write lands even if the compositor crashes mid-transition. + persistNext(!isDark); + const transition = document.startViewTransition(flipTheme); // Radial reveal from the click point @@ -48,7 +90,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) { // Animation failed — the theme has still toggled via flipTheme() }); } - }; + }, [isDark]); return (