mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-19 17:37:16 +02:00
refactor: better theme initialization
This commit is contained in:
@@ -187,13 +187,14 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Apply or clear custom theme live without restart
|
const applyCustomTheme = useCallback((vars: Record<string, string>) => {
|
||||||
// Defer application until Save
|
|
||||||
const _applyCustomTheme = useCallback((vars: Record<string, string>) => {
|
|
||||||
const root = document.documentElement;
|
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;
|
const root = document.documentElement;
|
||||||
THEME_VARIABLES.forEach(({ key }) => root.style.removeProperty(key));
|
THEME_VARIABLES.forEach(({ key }) => root.style.removeProperty(key));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -302,7 +303,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
root.style.removeProperty(key),
|
root.style.removeProperty(key),
|
||||||
);
|
);
|
||||||
Object.entries(settings.custom_theme).forEach(([k, v]) =>
|
Object.entries(settings.custom_theme).forEach(([k, v]) =>
|
||||||
root.style.setProperty(k, v),
|
root.style.setProperty(k, v, "important"),
|
||||||
);
|
);
|
||||||
} catch {}
|
} 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(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
loadSettings().catch(console.error);
|
loadSettings().catch(console.error);
|
||||||
@@ -390,7 +421,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
JSON.stringify(originalSettings.custom_theme ?? {});
|
JSON.stringify(originalSettings.custom_theme ?? {});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||||
<DialogContent className="max-w-md max-h-[80vh] my-8 flex flex-col">
|
<DialogContent className="max-w-md max-h-[80vh] my-8 flex flex-col">
|
||||||
<DialogHeader className="flex-shrink-0">
|
<DialogHeader className="flex-shrink-0">
|
||||||
<DialogTitle>Settings</DialogTitle>
|
<DialogTitle>Settings</DialogTitle>
|
||||||
@@ -613,7 +644,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="flex-shrink-0">
|
<DialogFooter className="flex-shrink-0">
|
||||||
<RippleButton variant="outline" onClick={onClose}>
|
<RippleButton variant="outline" onClick={handleClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</RippleButton>
|
</RippleButton>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
|
|||||||
@@ -29,12 +29,29 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
|
|||||||
const { invoke } = await import("@tauri-apps/api/core");
|
const { invoke } = await import("@tauri-apps/api/core");
|
||||||
const settings = await invoke<AppSettings>("get_app_settings");
|
const settings = await invoke<AppSettings>("get_app_settings");
|
||||||
const themeValue = settings?.theme ?? "system";
|
const themeValue = settings?.theme ?? "system";
|
||||||
|
|
||||||
if (
|
if (
|
||||||
themeValue === "light" ||
|
themeValue === "light" ||
|
||||||
themeValue === "dark" ||
|
themeValue === "dark" ||
|
||||||
themeValue === "system"
|
themeValue === "system"
|
||||||
) {
|
) {
|
||||||
setDefaultTheme(themeValue);
|
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 {
|
} else {
|
||||||
setDefaultTheme("system");
|
setDefaultTheme("system");
|
||||||
}
|
}
|
||||||
@@ -53,6 +70,30 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
|
|||||||
void loadTheme();
|
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<AppSettings>("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) {
|
if (isLoading) {
|
||||||
// Keep UI simple during initial settings load to avoid flicker
|
// Keep UI simple during initial settings load to avoid flicker
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -112,20 +112,13 @@ export const ColorPicker = ({
|
|||||||
(Number.isFinite(c.alpha()) ? c.alpha() : 1) * 100,
|
(Number.isFinite(c.alpha()) ? c.alpha() : 1) * 100,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Only update internal state if it actually changed
|
// Update internal state unconditionally when value prop changes
|
||||||
if (
|
setHue(nextHue);
|
||||||
Math.round(nextHue) !== Math.round(hue) ||
|
setSaturation(nextSat);
|
||||||
Math.round(nextSat) !== Math.round(saturation) ||
|
setLightness(nextLight);
|
||||||
Math.round(nextLight) !== Math.round(lightness) ||
|
setAlpha(nextAlpha);
|
||||||
Math.round(nextAlpha) !== Math.round(alpha)
|
|
||||||
) {
|
|
||||||
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
|
// Notify parent of changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -175,7 +168,8 @@ export const ColorPickerSelection = memo(
|
|||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [positionX, setPositionX] = useState(0);
|
const [positionX, setPositionX] = useState(0);
|
||||||
const [positionY, setPositionY] = useState(0);
|
const [positionY, setPositionY] = useState(0);
|
||||||
const { hue, setSaturation, setLightness } = useColorPicker();
|
const { hue, saturation, lightness, setSaturation, setLightness } =
|
||||||
|
useColorPicker();
|
||||||
|
|
||||||
const backgroundGradient = useMemo(() => {
|
const backgroundGradient = useMemo(() => {
|
||||||
return `linear-gradient(0deg, rgba(0,0,0,1), rgba(0,0,0,0)),
|
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%)`;
|
hsl(${hue}, 100%, 50%)`;
|
||||||
}, [hue]);
|
}, [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(
|
const handlePointerMove = useCallback(
|
||||||
(event: PointerEvent) => {
|
(event: PointerEvent) => {
|
||||||
if (!(isDragging && containerRef.current)) {
|
if (!(isDragging && containerRef.current)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user