diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c595ebe..a29bbfa 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -26,8 +26,8 @@ mod profile_importer; mod proxy_manager; mod settings_manager; // mod theme_detector; // removed: theme detection handled in webview via CSS prefers-color-scheme -mod version_updater; mod tag_manager; +mod version_updater; extern crate lazy_static; @@ -35,10 +35,10 @@ use browser_runner::{ check_browser_exists, check_browser_status, check_missing_binaries, check_missing_geoip_database, create_browser_profile_new, delete_profile, download_browser, ensure_all_binaries_exist, fetch_browser_versions_cached_first, fetch_browser_versions_with_count, - fetch_browser_versions_with_count_cached_first, get_downloaded_browser_versions, + fetch_browser_versions_with_count_cached_first, get_all_tags, get_downloaded_browser_versions, get_supported_browsers, is_browser_supported_on_platform, kill_browser_profile, launch_browser_profile, list_browser_profiles, rename_profile, update_camoufox_config, - update_profile_proxy, + update_profile_proxy, update_profile_tags, }; use settings_manager::{ @@ -63,13 +63,10 @@ use app_auto_updater::{ use profile_importer::{detect_existing_profiles, import_browser_profile}; -// use theme_detector::get_system_theme; - use group_manager::{ assign_profiles_to_group, create_profile_group, delete_profile_group, delete_selected_profiles, get_groups_with_profile_counts, get_profile_groups, update_profile_group, }; -use tag_manager::TAG_MANAGER; use geoip_downloader::GeoIPDownloader; diff --git a/src-tauri/src/settings_manager.rs b/src-tauri/src/settings_manager.rs index ebb8f2d..9ff755c 100644 --- a/src-tauri/src/settings_manager.rs +++ b/src-tauri/src/settings_manager.rs @@ -27,6 +27,8 @@ pub struct AppSettings { pub set_as_default_browser: bool, #[serde(default = "default_theme")] pub theme: String, // "light", "dark", or "system" + #[serde(default)] + pub custom_theme: Option>, // CSS var name -> value (e.g., "--background": "#1a1b26") } fn default_theme() -> String { @@ -38,6 +40,7 @@ impl Default for AppSettings { Self { set_as_default_browser: false, theme: "system".to_string(), + custom_theme: None, } } } @@ -321,6 +324,7 @@ mod tests { let test_settings = AppSettings { set_as_default_browser: true, theme: "dark".to_string(), + custom_theme: None, }; // Save settings diff --git a/src/components/settings-dialog.tsx b/src/components/settings-dialog.tsx index dbd00dd..6ffa439 100644 --- a/src/components/settings-dialog.tsx +++ b/src/components/settings-dialog.tsx @@ -1,12 +1,21 @@ "use client"; import { invoke } from "@tauri-apps/api/core"; +import Color from "color"; import { useTheme } from "next-themes"; import { useCallback, useEffect, useState } from "react"; import { BsCamera, BsMic } from "react-icons/bs"; import { LoadingButton } from "@/components/loading-button"; import { Badge } from "@/components/ui/badge"; - +import { + ColorPicker, + ColorPickerAlpha, + ColorPickerEyeDropper, + ColorPickerFormat, + ColorPickerHue, + ColorPickerOutput, + ColorPickerSelection, +} from "@/components/ui/color-picker"; import { Dialog, DialogContent, @@ -15,6 +24,11 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { Select, SelectContent, @@ -30,6 +44,7 @@ import { RippleButton } from "./ui/ripple"; interface AppSettings { set_as_default_browser: boolean; theme: string; + custom_theme?: Record; } interface PermissionInfo { @@ -49,10 +64,12 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { const [settings, setSettings] = useState({ set_as_default_browser: false, theme: "system", + custom_theme: undefined, }); const [originalSettings, setOriginalSettings] = useState({ set_as_default_browser: false, theme: "system", + custom_theme: undefined, }); const [isDefaultBrowser, setIsDefaultBrowser] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -109,12 +126,60 @@ 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"); - setSettings(appSettings); - setOriginalSettings(appSettings); + const merged: AppSettings = { + ...appSettings, + custom_theme: + appSettings.custom_theme && + Object.keys(appSettings.custom_theme).length > 0 + ? appSettings.custom_theme + : TOKYO_NIGHT_DEFAULTS, + }; + setSettings(merged); + setOriginalSettings(merged); } catch (error) { console.error("Failed to load settings:", error); } finally { @@ -215,7 +280,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { setIsSaving(true); try { await invoke("save_app_settings", { settings }); - setTheme(settings.theme); + setTheme(settings.theme === "custom" ? "dark" : settings.theme); setOriginalSettings(settings); onClose(); } catch (error) { @@ -226,8 +291,11 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { }, [onClose, setTheme, settings]); const updateSetting = useCallback( - (key: keyof AppSettings, value: boolean | string) => { - setSettings((prev) => ({ ...prev, [key]: value })); + ( + key: keyof AppSettings, + value: boolean | string | Record | undefined, + ) => { + setSettings((prev) => ({ ...prev, [key]: value as unknown as never })); }, [], ); @@ -285,7 +353,10 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { ]); // Check if settings have changed (excluding default browser setting) - const hasChanges = settings.theme !== originalSettings.theme; + const hasChanges = + settings.theme !== originalSettings.theme || + JSON.stringify(settings.custom_theme ?? {}) !== + JSON.stringify(originalSettings.custom_theme ?? {}); return ( @@ -307,6 +378,9 @@ 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); + } }} > @@ -316,6 +390,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { Light Dark System + Custom @@ -323,6 +398,77 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {

Choose your preferred theme or follow your system settings.

+ + {settings.theme === "custom" && ( +
+
Custom theme
+
+ {THEME_VARIABLES.map(({ key, label }) => { + const colorValue = + settings.custom_theme?.[key] ?? + TOKYO_NIGHT_DEFAULTS[key] ?? + "#000000"; + return ( +
+ + +
+ ); + })} +
+
+ )} {/* Default Browser Section */} diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx index 42def71..3501b7f 100644 --- a/src/components/theme-provider.tsx +++ b/src/components/theme-provider.tsx @@ -6,6 +6,7 @@ import { useEffect, useState } from "react"; interface AppSettings { set_as_default_browser: boolean; theme: string; + custom_theme?: Record; } interface CustomThemeProviderProps { @@ -27,12 +28,22 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) { // Lazy import to avoid pulling Tauri API on SSR const { invoke } = await import("@tauri-apps/api/core"); const settings = await invoke("get_app_settings"); - if ( - settings?.theme === "light" || - settings?.theme === "dark" || - settings?.theme === "system" + const themeValue = settings?.theme ?? "system"; + if (themeValue === "custom") { + setDefaultTheme("light"); + const vars = settings.custom_theme ?? {}; + try { + const root = document.documentElement; + Object.entries(vars).forEach(([k, v]) => { + root.style.setProperty(k, v); + }); + } catch {} + } else if ( + themeValue === "light" || + themeValue === "dark" || + themeValue === "system" ) { - setDefaultTheme(settings.theme); + setDefaultTheme(themeValue); } else { setDefaultTheme("system"); } diff --git a/src/components/ui/color-picker.tsx b/src/components/ui/color-picker.tsx index 3952f15..914b4ff 100644 --- a/src/components/ui/color-picker.tsx +++ b/src/components/ui/color-picker.tsx @@ -53,17 +53,21 @@ export const useColorPicker = () => { return context; }; -export type ColorPickerProps = HTMLAttributes & { +export type ColorPickerProps = Omit< + HTMLAttributes, + "onChange" +> & { value?: Parameters[0]; defaultValue?: Parameters[0]; - onChange?: (value: Parameters[0]) => void; + onColorChange?: (value: [number, number, number, number]) => void; }; export const ColorPicker = ({ value, defaultValue = "#000000", - onChange, + onColorChange, className, + children, ...props }: ColorPickerProps) => { const selectedColor = Color(value); @@ -97,13 +101,13 @@ export const ColorPicker = ({ // Notify parent of changes useEffect(() => { - if (onChange) { + if (onColorChange) { const color = Color.hsl(hue, saturation, lightness).alpha(alpha / 100); const rgba = color.rgb().array(); - onChange([rgba[0], rgba[1], rgba[2], alpha / 100]); + onColorChange([rgba[0], rgba[1], rgba[2], alpha / 100]); } - }, [hue, saturation, lightness, alpha, onChange]); + }, [hue, saturation, lightness, alpha, onColorChange]); return ( + > + {children} + ); };