From 95ee807f3bb4f2c32f81ee7be201b94b633144f0 Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Fri, 15 Aug 2025 10:08:15 +0400 Subject: [PATCH] refactor: better theme initialization --- src/components/settings-dialog.tsx | 47 +++++++++++++++++++++++++----- src/components/theme-provider.tsx | 41 ++++++++++++++++++++++++++ src/components/ui/color-picker.tsx | 33 ++++++++++++--------- 3 files changed, 99 insertions(+), 22 deletions(-) diff --git a/src/components/settings-dialog.tsx b/src/components/settings-dialog.tsx index 201bf4d..f481049 100644 --- a/src/components/settings-dialog.tsx +++ b/src/components/settings-dialog.tsx @@ -187,13 +187,14 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { } }, []); - // Apply or clear custom theme live without restart - // Defer application until Save - const _applyCustomTheme = useCallback((vars: Record) => { + const applyCustomTheme = useCallback((vars: Record) => { const root = document.documentElement; - Object.entries(vars).forEach(([k, v]) => root.style.setProperty(k, v)); + Object.entries(vars).forEach(([k, v]) => + root.style.setProperty(k, v, "important"), + ); }, []); - const _clearCustomTheme = useCallback(() => { + + const clearCustomTheme = useCallback(() => { const root = document.documentElement; THEME_VARIABLES.forEach(({ key }) => root.style.removeProperty(key)); }, []); @@ -302,7 +303,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { root.style.removeProperty(key), ); Object.entries(settings.custom_theme).forEach(([k, v]) => - root.style.setProperty(k, v), + root.style.setProperty(k, v, "important"), ); } catch {} } @@ -331,6 +332,36 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { [], ); + const handleClose = useCallback(() => { + // Restore original theme when closing without saving + if (originalSettings.theme === "custom" && originalSettings.custom_theme) { + applyCustomTheme(originalSettings.custom_theme); + } else { + clearCustomTheme(); + } + onClose(); + }, [ + originalSettings.theme, + originalSettings.custom_theme, + applyCustomTheme, + clearCustomTheme, + onClose, + ]); + + // Apply custom theme live when editing + useEffect(() => { + if (settings.theme === "custom" && settings.custom_theme) { + applyCustomTheme(settings.custom_theme); + } else if (settings.theme !== "custom") { + clearCustomTheme(); + } + }, [ + settings.theme, + settings.custom_theme, + applyCustomTheme, + clearCustomTheme, + ]); + useEffect(() => { if (isOpen) { loadSettings().catch(console.error); @@ -390,7 +421,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { JSON.stringify(originalSettings.custom_theme ?? {}); return ( - + Settings @@ -613,7 +644,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { - + Cancel ("get_app_settings"); const themeValue = settings?.theme ?? "system"; + if ( themeValue === "light" || themeValue === "dark" || themeValue === "system" ) { setDefaultTheme(themeValue); + } else if (themeValue === "custom") { + setDefaultTheme("light"); + if ( + settings.custom_theme && + Object.keys(settings.custom_theme).length > 0 + ) { + try { + const root = document.documentElement; + // Apply with !important to override CSS defaults + Object.entries(settings.custom_theme).forEach(([k, v]) => { + root.style.setProperty(k, v, "important"); + }); + } catch (error) { + console.warn("Failed to apply custom theme variables:", error); + } + } } else { setDefaultTheme("system"); } @@ -53,6 +70,30 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) { void loadTheme(); }, []); + // Additional effect to ensure custom theme is applied after mount + useEffect(() => { + if (!isLoading && _mounted) { + const reapplyCustomTheme = async () => { + try { + const { invoke } = await import("@tauri-apps/api/core"); + const settings = await invoke("get_app_settings"); + + if (settings?.theme === "custom" && settings.custom_theme) { + const root = document.documentElement; + Object.entries(settings.custom_theme).forEach(([k, v]) => { + root.style.setProperty(k, v, "important"); + }); + } + } catch (error) { + console.warn("Failed to reapply custom theme:", error); + } + }; + + // Apply after a short delay to ensure CSS has loaded + setTimeout(reapplyCustomTheme, 100); + } + }, [isLoading, _mounted]); + if (isLoading) { // Keep UI simple during initial settings load to avoid flicker return null; diff --git a/src/components/ui/color-picker.tsx b/src/components/ui/color-picker.tsx index 188370e..c75c1ef 100644 --- a/src/components/ui/color-picker.tsx +++ b/src/components/ui/color-picker.tsx @@ -112,20 +112,13 @@ export const ColorPicker = ({ (Number.isFinite(c.alpha()) ? c.alpha() : 1) * 100, ); - // Only update internal state if it actually changed - if ( - Math.round(nextHue) !== Math.round(hue) || - Math.round(nextSat) !== Math.round(saturation) || - Math.round(nextLight) !== Math.round(lightness) || - Math.round(nextAlpha) !== Math.round(alpha) - ) { - setHue(nextHue); - setSaturation(nextSat); - setLightness(nextLight); - setAlpha(nextAlpha); - } + // Update internal state unconditionally when value prop changes + setHue(nextHue); + setSaturation(nextSat); + setLightness(nextLight); + setAlpha(nextAlpha); } - }, [value, alpha, hue, lightness, saturation]); + }, [value]); // Remove state values from dependency array to prevent infinite loop // Notify parent of changes useEffect(() => { @@ -175,7 +168,8 @@ export const ColorPickerSelection = memo( const [isDragging, setIsDragging] = useState(false); const [positionX, setPositionX] = useState(0); const [positionY, setPositionY] = useState(0); - const { hue, setSaturation, setLightness } = useColorPicker(); + const { hue, saturation, lightness, setSaturation, setLightness } = + useColorPicker(); const backgroundGradient = useMemo(() => { return `linear-gradient(0deg, rgba(0,0,0,1), rgba(0,0,0,0)), @@ -183,6 +177,17 @@ export const ColorPickerSelection = memo( hsl(${hue}, 100%, 50%)`; }, [hue]); + // Update position indicators when saturation/lightness change externally + useEffect(() => { + if (!isDragging) { + const x = saturation / 100; + const topLightness = x < 0.01 ? 100 : 50 + 50 * (1 - x); + const y = topLightness > 0 ? 1 - lightness / topLightness : 0; + setPositionX(x); + setPositionY(Math.max(0, Math.min(1, y))); + } + }, [saturation, lightness, isDragging]); + const handlePointerMove = useCallback( (event: PointerEvent) => { if (!(isDragging && containerRef.current)) {