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
+
+ + +
+ +
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); + }); +}