From d7a787586ded2602c97af79b6b69d94710cb3f19 Mon Sep 17 00:00:00 2001
From: zhom <2717306+zhom@users.noreply.github.com>
Date: Fri, 15 Aug 2025 18:08:33 +0400
Subject: [PATCH] refactor: custom theme cleanup
---
.vscode/settings.json | 1 +
src/app/page.tsx | 8 +-
src/components/settings-dialog.tsx | 216 +++++++++++++++++++----------
src/components/theme-provider.tsx | 16 +--
src/components/ui/dialog.tsx | 2 +-
src/lib/themes.ts | 215 ++++++++++++++++++++++++++++
6 files changed, 371 insertions(+), 87 deletions(-)
create mode 100644 src/lib/themes.ts
diff --git a/.vscode/settings.json b/.vscode/settings.json
index bb98f99..b54297c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -83,6 +83,7 @@
"localtime",
"lxml",
"lzma",
+ "Matchalk",
"mmdb",
"mountpoint",
"msiexec",
diff --git a/src/app/page.tsx b/src/app/page.tsx
index e0038e1..6406f80 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -765,7 +765,7 @@ export default function Home() {
}, [isInitialized, checkAllPermissions]);
return (
-
+
{isInitializing && (
-
-
+
+
Initializing
Please don't close the app
-
+
)}
diff --git a/src/components/settings-dialog.tsx b/src/components/settings-dialog.tsx
index f481049..c4befe5 100644
--- a/src/components/settings-dialog.tsx
+++ b/src/components/settings-dialog.tsx
@@ -38,6 +38,12 @@ import {
} from "@/components/ui/select";
import type { PermissionType } from "@/hooks/use-permissions";
import { usePermissions } from "@/hooks/use-permissions";
+import {
+ getThemeByColors,
+ getThemeById,
+ THEME_VARIABLES,
+ THEMES,
+} from "@/lib/themes";
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
import { RippleButton } from "./ui/ripple";
@@ -47,6 +53,11 @@ interface AppSettings {
custom_theme?: Record
;
}
+interface CustomThemeState {
+ selectedThemeId: string | null;
+ colors: Record;
+}
+
interface PermissionInfo {
permission_type: PermissionType;
isGranted: boolean;
@@ -71,6 +82,10 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
theme: "system",
custom_theme: undefined,
});
+ const [customThemeState, setCustomThemeState] = useState({
+ selectedThemeId: null,
+ colors: {},
+ });
const [isDefaultBrowser, setIsDefaultBrowser] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
@@ -126,60 +141,40 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
return "Access to camera for browser applications";
}
}, []);
- const TOKYO_NIGHT_DEFAULTS: Record = {
- "--background": "#1a1b26",
- "--foreground": "#c0caf5",
- "--card": "#24283b",
- "--card-foreground": "#c0caf5",
- "--popover": "#24283b",
- "--popover-foreground": "#c0caf5",
- "--primary": "#7aa2f7",
- "--primary-foreground": "#1a1b26",
- "--secondary": "#2ac3de",
- "--secondary-foreground": "#1a1b26",
- "--muted": "#3b4261",
- "--muted-foreground": "#a9b1d6",
- "--accent": "#bb9af7",
- "--accent-foreground": "#1a1b26",
- "--destructive": "#f7768e",
- "--destructive-foreground": "#1a1b26",
- "--border": "#3b4261",
- };
-
- const THEME_VARIABLES: Array<{ key: string; label: string }> = [
- { key: "--background", label: "Background" },
- { key: "--foreground", label: "Foreground" },
- { key: "--card", label: "Card" },
- { key: "--card-foreground", label: "Card FG" },
- { key: "--popover", label: "Popover" },
- { key: "--popover-foreground", label: "Popover FG" },
- { key: "--primary", label: "Primary" },
- { key: "--primary-foreground", label: "Primary FG" },
- { key: "--secondary", label: "Secondary" },
- { key: "--secondary-foreground", label: "Secondary FG" },
- { key: "--muted", label: "Muted" },
- { key: "--muted-foreground", label: "Muted FG" },
- { key: "--accent", label: "Accent" },
- { key: "--accent-foreground", label: "Accent FG" },
- { key: "--destructive", label: "Destructive" },
- { key: "--destructive-foreground", label: "Destructive FG" },
- { key: "--border", label: "Border" },
- ];
const loadSettings = useCallback(async () => {
setIsLoading(true);
try {
const appSettings = await invoke("get_app_settings");
+ const tokyoNightTheme = getThemeById("tokyo-night");
+ if (!tokyoNightTheme) {
+ throw new Error("Tokyo Night theme not found");
+ }
const merged: AppSettings = {
...appSettings,
custom_theme:
appSettings.custom_theme &&
Object.keys(appSettings.custom_theme).length > 0
? appSettings.custom_theme
- : TOKYO_NIGHT_DEFAULTS,
+ : tokyoNightTheme.colors,
};
setSettings(merged);
setOriginalSettings(merged);
+
+ // Initialize custom theme state
+ if (merged.theme === "custom" && merged.custom_theme) {
+ const matchingTheme = getThemeByColors(merged.custom_theme);
+ setCustomThemeState({
+ selectedThemeId: matchingTheme?.id || null,
+ colors: merged.custom_theme,
+ });
+ } else if (merged.theme === "custom") {
+ // Initialize with Tokyo Night if no custom theme exists
+ setCustomThemeState({
+ selectedThemeId: "tokyo-night",
+ colors: tokyoNightTheme.colors,
+ });
+ }
} catch (error) {
console.error("Failed to load settings:", error);
} finally {
@@ -196,7 +191,9 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
const clearCustomTheme = useCallback(() => {
const root = document.documentElement;
- THEME_VARIABLES.forEach(({ key }) => root.style.removeProperty(key));
+ THEME_VARIABLES.forEach(({ key }) =>
+ root.style.removeProperty(key as string),
+ );
}, []);
const loadPermissions = useCallback(async () => {
@@ -291,18 +288,31 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
const handleSave = useCallback(async () => {
setIsSaving(true);
try {
- await invoke("save_app_settings", { settings });
+ // Update settings with current custom theme state
+ const settingsToSave = {
+ ...settings,
+ custom_theme:
+ settings.theme === "custom"
+ ? customThemeState.colors
+ : settings.custom_theme,
+ };
+
+ await invoke("save_app_settings", { settings: settingsToSave });
setTheme(settings.theme === "custom" ? "dark" : settings.theme);
+
// Apply or clear custom variables only on Save
if (settings.theme === "custom") {
- if (settings.custom_theme) {
+ if (
+ customThemeState.colors &&
+ Object.keys(customThemeState.colors).length > 0
+ ) {
try {
const root = document.documentElement;
// Clear any previous custom vars first
THEME_VARIABLES.forEach(({ key }) =>
- root.style.removeProperty(key),
+ root.style.removeProperty(key as string),
);
- Object.entries(settings.custom_theme).forEach(([k, v]) =>
+ Object.entries(customThemeState.colors).forEach(([k, v]) =>
root.style.setProperty(k, v, "important"),
);
} catch {}
@@ -310,17 +320,20 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
} else {
try {
const root = document.documentElement;
- THEME_VARIABLES.forEach(({ key }) => root.style.removeProperty(key));
+ THEME_VARIABLES.forEach(({ key }) =>
+ root.style.removeProperty(key as string),
+ );
} catch {}
}
- setOriginalSettings(settings);
+
+ setOriginalSettings(settingsToSave);
onClose();
} catch (error) {
console.error("Failed to save settings:", error);
} finally {
setIsSaving(false);
}
- }, [onClose, setTheme, settings]);
+ }, [onClose, setTheme, settings, customThemeState]);
const updateSetting = useCallback(
(
@@ -339,6 +352,16 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
} else {
clearCustomTheme();
}
+
+ // Reset custom theme state to original
+ if (originalSettings.theme === "custom" && originalSettings.custom_theme) {
+ const matchingTheme = getThemeByColors(originalSettings.custom_theme);
+ setCustomThemeState({
+ selectedThemeId: matchingTheme?.id || null,
+ colors: originalSettings.custom_theme,
+ });
+ }
+
onClose();
}, [
originalSettings.theme,
@@ -348,19 +371,12 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
onClose,
]);
- // Apply custom theme live when editing
+ // Only clear custom theme when switching away from custom, don't apply live changes
useEffect(() => {
- if (settings.theme === "custom" && settings.custom_theme) {
- applyCustomTheme(settings.custom_theme);
- } else if (settings.theme !== "custom") {
+ if (settings.theme !== "custom") {
clearCustomTheme();
}
- }, [
- settings.theme,
- settings.custom_theme,
- applyCustomTheme,
- clearCustomTheme,
- ]);
+ }, [settings.theme, clearCustomTheme]);
useEffect(() => {
if (isOpen) {
@@ -417,8 +433,12 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
// Check if settings have changed (excluding default browser setting)
const hasChanges =
settings.theme !== originalSettings.theme ||
- JSON.stringify(settings.custom_theme ?? {}) !==
- JSON.stringify(originalSettings.custom_theme ?? {});
+ (settings.theme === "custom" &&
+ JSON.stringify(customThemeState.colors) !==
+ JSON.stringify(originalSettings.custom_theme ?? {})) ||
+ (settings.theme !== "custom" &&
+ JSON.stringify(settings.custom_theme ?? {}) !==
+ JSON.stringify(originalSettings.custom_theme ?? {}));
return (
@@ -440,8 +460,14 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
value={settings.theme}
onValueChange={(value) => {
updateSetting("theme", value);
- if (value === "custom" && !settings.custom_theme) {
- updateSetting("custom_theme", TOKYO_NIGHT_DEFAULTS);
+ if (value === "custom") {
+ const tokyoNightTheme = getThemeById("tokyo-night");
+ if (tokyoNightTheme) {
+ setCustomThemeState({
+ selectedThemeId: "tokyo-night",
+ colors: tokyoNightTheme.colors,
+ });
+ }
}
}}
>
@@ -458,18 +484,57 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
- Choose your preferred theme or follow your system settings.
+ Choose your preferred theme or follow your system settings. Custom
+ theme changes are applied only when you save.
{settings.theme === "custom" && (
-
Custom theme
+
+
+ Theme Preset
+
+ {
+ if (value === "custom") {
+ setCustomThemeState((prev) => ({
+ ...prev,
+ selectedThemeId: null,
+ }));
+ } else {
+ const theme = getThemeById(value);
+ if (theme) {
+ setCustomThemeState({
+ selectedThemeId: value,
+ colors: theme.colors,
+ });
+ }
+ }
+ }}
+ >
+
+
+
+
+ {THEMES.map((theme) => (
+
+ {theme.name}
+
+ ))}
+ Your Own
+
+
+
+
+
Custom Colors
{THEME_VARIABLES.map(({ key, label }) => {
const colorValue =
- settings.custom_theme?.[key] ??
- TOKYO_NIGHT_DEFAULTS[key] ??
- "#000000";
+ customThemeState.colors[key] || "#000000";
return (
{
const next = Color({ r, g, b }).alpha(a);
const nextStr = next.hexa();
- const nextTheme = {
- ...(settings.custom_theme ?? {}),
+ const newColors = {
+ ...customThemeState.colors,
[key]: nextStr,
- } as Record
;
- updateSetting("custom_theme", nextTheme);
- // No live preview; applied on Save
+ };
+
+ // Check if colors match any preset theme
+ const matchingTheme =
+ getThemeByColors(newColors);
+
+ setCustomThemeState({
+ selectedThemeId: matchingTheme?.id || null,
+ colors: newColors,
+ });
}}
>
diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx
index 45f8b49..ad2f6d3 100644
--- a/src/components/theme-provider.tsx
+++ b/src/components/theme-provider.tsx
@@ -2,6 +2,7 @@
import { ThemeProvider } from "next-themes";
import { useEffect, useState } from "react";
+import { applyThemeColors, clearThemeColors } from "@/lib/themes";
interface AppSettings {
set_as_default_browser: boolean;
@@ -37,17 +38,13 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
) {
setDefaultTheme(themeValue);
} else if (themeValue === "custom") {
- setDefaultTheme("light");
+ setDefaultTheme("dark");
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");
- });
+ applyThemeColors(settings.custom_theme);
} catch (error) {
console.warn("Failed to apply custom theme variables:", error);
}
@@ -79,10 +76,9 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
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");
- });
+ applyThemeColors(settings.custom_theme);
+ } else {
+ clearThemeColors();
}
} catch (error) {
console.warn("Failed to reapply custom theme:", error);
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
index f3b27c0..d1e8394 100644
--- a/src/components/ui/dialog.tsx
+++ b/src/components/ui/dialog.tsx
@@ -39,7 +39,7 @@ function DialogOverlay({
{
+ "--background": string;
+ "--foreground": string;
+ "--card": string;
+ "--card-foreground": string;
+ "--popover": string;
+ "--popover-foreground": string;
+ "--primary": string;
+ "--primary-foreground": string;
+ "--secondary": string;
+ "--secondary-foreground": string;
+ "--muted": string;
+ "--muted-foreground": string;
+ "--accent": string;
+ "--accent-foreground": string;
+ "--destructive": string;
+ "--destructive-foreground": string;
+ "--border": string;
+}
+
+export interface Theme {
+ id: string;
+ name: string;
+ colors: ThemeColors;
+}
+
+export const THEMES: Theme[] = [
+ {
+ id: "tokyo-night",
+ name: "Tokyo Night",
+ colors: {
+ "--background": "#1a1b26",
+ "--foreground": "#c0caf5",
+ "--card": "#24283b",
+ "--card-foreground": "#c0caf5",
+ "--popover": "#24283b",
+ "--popover-foreground": "#c0caf5",
+ "--primary": "#7aa2f7",
+ "--primary-foreground": "#1a1b26",
+ "--secondary": "#2ac3de",
+ "--secondary-foreground": "#1a1b26",
+ "--muted": "#3b4261",
+ "--muted-foreground": "#a9b1d6",
+ "--accent": "#bb9af7",
+ "--accent-foreground": "#1a1b26",
+ "--destructive": "#f7768e",
+ "--destructive-foreground": "#1a1b26",
+ "--border": "#3b4261",
+ },
+ },
+ {
+ id: "dracula",
+ name: "Dracula",
+ colors: {
+ "--background": "#282a36",
+ "--foreground": "#f8f8f2",
+ "--card": "#44475a",
+ "--card-foreground": "#f8f8f2",
+ "--popover": "#44475a",
+ "--popover-foreground": "#f8f8f2",
+ "--primary": "#bd93f9",
+ "--primary-foreground": "#282a36",
+ "--secondary": "#8be9fd",
+ "--secondary-foreground": "#282a36",
+ "--muted": "#6272a4",
+ "--muted-foreground": "#f8f8f2",
+ "--accent": "#ff79c6",
+ "--accent-foreground": "#282a36",
+ "--destructive": "#ff5555",
+ "--destructive-foreground": "#f8f8f2",
+ "--border": "#6272a4",
+ },
+ },
+ {
+ id: "matchalk",
+ name: "Matchalk",
+ colors: {
+ "--background": "#273136",
+ "--foreground": "#D1DED3",
+ "--card": "#1c2427",
+ "--card-foreground": "#D1DED3",
+ "--popover": "#323e45",
+ "--popover-foreground": "#D1DED3",
+ "--primary": "#7eb08a",
+ "--primary-foreground": "#273136",
+ "--secondary": "#d2b48c",
+ "--secondary-foreground": "#273136",
+ "--muted": "#323e45",
+ "--muted-foreground": "#7ea4b0",
+ "--accent": "#d2b48c",
+ "--accent-foreground": "#273136",
+ "--destructive": "#ff819f",
+ "--destructive-foreground": "#273136",
+ "--border": "#304e37",
+ },
+ },
+ {
+ id: "houston",
+ name: "Houston",
+ colors: {
+ "--background": "#17191e",
+ "--foreground": "#f7f7f8",
+ "--card": "#21252e",
+ "--card-foreground": "#f7f7f8",
+ "--popover": "#21252e",
+ "--popover-foreground": "#f7f7f8",
+ "--primary": "#5755d9",
+ "--primary-foreground": "#f7f7f8",
+ "--secondary": "#f25f4c",
+ "--secondary-foreground": "#f7f7f8",
+ "--muted": "#2a2e39",
+ "--muted-foreground": "#9ca3af",
+ "--accent": "#0ea5e9",
+ "--accent-foreground": "#f7f7f8",
+ "--destructive": "#ef4444",
+ "--destructive-foreground": "#f7f7f8",
+ "--border": "#2a2e39",
+ },
+ },
+ {
+ id: "ayu-dark",
+ name: "Ayu Dark",
+ colors: {
+ "--background": "#0a0e14",
+ "--foreground": "#b3b1ad",
+ "--card": "#11151c",
+ "--card-foreground": "#b3b1ad",
+ "--popover": "#11151c",
+ "--popover-foreground": "#b3b1ad",
+ "--primary": "#39bae6",
+ "--primary-foreground": "#0a0e14",
+ "--secondary": "#ffb454",
+ "--secondary-foreground": "#0a0e14",
+ "--muted": "#1f2430",
+ "--muted-foreground": "#5c6773",
+ "--accent": "#d2a6ff",
+ "--accent-foreground": "#0a0e14",
+ "--destructive": "#f07178",
+ "--destructive-foreground": "#b3b1ad",
+ "--border": "#1f2430",
+ },
+ },
+ {
+ id: "ayu-light",
+ name: "Ayu Light",
+ colors: {
+ "--background": "#fafafa",
+ "--foreground": "#5c6773",
+ "--card": "#ffffff",
+ "--card-foreground": "#5c6773",
+ "--popover": "#ffffff",
+ "--popover-foreground": "#5c6773",
+ "--primary": "#399ee6",
+ "--primary-foreground": "#fafafa",
+ "--secondary": "#fa8d3e",
+ "--secondary-foreground": "#fafafa",
+ "--muted": "#f0f0f0",
+ "--muted-foreground": "#828c99",
+ "--accent": "#a37acc",
+ "--accent-foreground": "#fafafa",
+ "--destructive": "#f07178",
+ "--destructive-foreground": "#fafafa",
+ "--border": "#e7eaed",
+ },
+ },
+];
+
+export const THEME_VARIABLES: Array<{ key: keyof ThemeColors; label: string }> =
+ [
+ { key: "--background", label: "Background" },
+ { key: "--foreground", label: "Foreground" },
+ { key: "--card", label: "Card" },
+ { key: "--card-foreground", label: "Card FG" },
+ { key: "--popover", label: "Popover" },
+ { key: "--popover-foreground", label: "Popover FG" },
+ { key: "--primary", label: "Primary" },
+ { key: "--primary-foreground", label: "Primary FG" },
+ { key: "--secondary", label: "Secondary" },
+ { key: "--secondary-foreground", label: "Secondary FG" },
+ { key: "--muted", label: "Muted" },
+ { key: "--muted-foreground", label: "Muted FG" },
+ { key: "--accent", label: "Accent" },
+ { key: "--accent-foreground", label: "Accent FG" },
+ { key: "--destructive", label: "Destructive" },
+ { key: "--destructive-foreground", label: "Destructive FG" },
+ { key: "--border", label: "Border" },
+ ];
+
+export function getThemeById(id: string): Theme | undefined {
+ return THEMES.find((theme) => theme.id === id);
+}
+
+export function getThemeByColors(
+ colors: Record,
+): Theme | undefined {
+ return THEMES.find((theme) => {
+ return THEME_VARIABLES.every(({ key }) => {
+ return theme.colors[key] === colors[key];
+ });
+ });
+}
+
+export function applyThemeColors(colors: Record): void {
+ const root = document.documentElement;
+ Object.entries(colors).forEach(([key, value]) => {
+ root.style.setProperty(key, value, "important");
+ });
+}
+
+export function clearThemeColors(): void {
+ const root = document.documentElement;
+ THEME_VARIABLES.forEach(({ key }) => {
+ root.style.removeProperty(key as string);
+ });
+}