From 5be7b2956b38faafe5e6984fa7075044113d665e Mon Sep 17 00:00:00 2001 From: Kang Date: Tue, 24 Feb 2026 15:32:33 +0800 Subject: [PATCH] refactor: update global UI, layout visibility, and theme configuration - Dynamically map visibility states via AppContext - Improve SVG dynamic background thematic integration --- src-tauri/src/pty.rs | 44 ++- src-tauri/src/ssh.rs | 51 ++- src/App.tsx | 424 ++++++++++++--------- src/components/dialog/AboutDialog.tsx | 10 + src/components/dialog/NewSessionDialog.tsx | 11 + src/components/layout/Header.tsx | 20 +- src/components/panel/SavedConnections.tsx | 4 +- src/context/AppContext.tsx | 176 ++++++++- src/context/ThemeContext.tsx | 19 +- src/main.tsx | 2 + 10 files changed, 535 insertions(+), 226 deletions(-) diff --git a/src-tauri/src/pty.rs b/src-tauri/src/pty.rs index e4ab7ef7..909924a2 100644 --- a/src-tauri/src/pty.rs +++ b/src-tauri/src/pty.rs @@ -15,15 +15,41 @@ struct OutputBuffer { buffer: Vec, } -fn get_default_shell() -> CommandBuilder { - #[cfg(target_os = "windows")] - { - CommandBuilder::new("powershell.exe") +fn get_default_shell(app: Option<&AppHandle>) -> CommandBuilder { + let mut shell_cmd = String::new(); + + if let Some(app) = app { + if let Ok(settings) = crate::config::load_app_settings(app) { + let user_shell = settings.general.default_local_shell; + if !user_shell.trim().is_empty() { + shell_cmd = user_shell; + } + } } - #[cfg(not(target_os = "windows"))] - { - let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string()); - CommandBuilder::new(shell) + + if shell_cmd.is_empty() { + #[cfg(target_os = "windows")] + { + shell_cmd = "powershell.exe".to_string(); + } + #[cfg(not(target_os = "windows"))] + { + shell_cmd = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string()); + } + } + + let parts: Vec<&str> = shell_cmd.split_whitespace().collect(); + if parts.is_empty() { + #[cfg(target_os = "windows")] + return CommandBuilder::new("powershell.exe"); + #[cfg(not(target_os = "windows"))] + return CommandBuilder::new("/bin/bash"); + } else { + let mut builder = CommandBuilder::new(parts[0]); + if parts.len() > 1 { + builder.args(&parts[1..]); + } + builder } } @@ -86,7 +112,7 @@ fn pty_session_thread( } }; - let cmd = get_default_shell(); + let cmd = get_default_shell(Some(&app)); let mut _child = match pair.slave.spawn_command(cmd) { Ok(c) => c, Err(e) => { diff --git a/src-tauri/src/ssh.rs b/src-tauri/src/ssh.rs index 63088a1b..5343ccfa 100644 --- a/src-tauri/src/ssh.rs +++ b/src-tauri/src/ssh.rs @@ -127,6 +127,44 @@ impl client::Handler for SshHandler { } } +pub async fn connect_with_proxy( + app: &AppHandle, + config: &SshConfig, + ssh_config: Arc, + handler: SshHandler, +) -> AppResult> { + let mut proxy_settings = None; + if let Ok(app_settings) = crate::config::load_app_settings(app) { + if app_settings.proxy.enabled { + proxy_settings = Some(app_settings.proxy); + } + } + + let target = (config.host.as_str(), config.port); + let handle = if let Some(proxy) = proxy_settings { + let proxy_addr = format!("{}:{}", proxy.host, proxy.port); + match proxy.protocol.as_str() { + "socks5" => { + let stream = tokio_socks::tcp::Socks5Stream::connect(proxy_addr.as_str(), target).await + .map_err(|e| AppError::Auth(format!("SOCKS5 proxy connection failed: {}", e)))?; + client::connect_stream(ssh_config, stream.into_inner(), handler).await + } + "http" => { + let mut stream = tokio::net::TcpStream::connect(&proxy_addr).await + .map_err(|e| AppError::Auth(format!("HTTP proxy connection failed: {}", e)))?; + async_http_proxy::http_connect_tokio(&mut stream, &config.host, config.port).await + .map_err(|e| AppError::Auth(format!("HTTP proxy tunnel failed: {}", e)))?; + client::connect_stream(ssh_config, stream, handler).await + } + _ => client::connect(ssh_config, target, handler).await, + } + } else { + client::connect(ssh_config, target, handler).await + }.map_err(|e| AppError::Auth(format!("SSH connection failed: {}", e)))?; + + Ok(handle) +} + /// Connects via SSH, opens a PTY shell, and spawns the I/O loop. pub async fn create_ssh_session( app: AppHandle, @@ -137,12 +175,17 @@ pub async fn create_ssh_session( let session_id = uuid::Uuid::new_v4().to_string(); let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::(); - let ssh_config = Arc::new(client::Config::default()); + let mut ssh_config_obj = client::Config::default(); + if let Ok(app_settings) = crate::config::load_app_settings(&app) { + let interval = app_settings.terminal.keep_alive_interval; + if interval > 0 { + ssh_config_obj.keepalive_interval = Some(std::time::Duration::from_secs(interval as u64)); + } + } + let ssh_config = Arc::new(ssh_config_obj); let handler = SshHandler::new(app.clone(), config.host.clone(), config.port); - let mut handle = client::connect(ssh_config, (config.host.as_str(), config.port), handler) - .await - .map_err(|e| AppError::Auth(format!("SSH connection failed: {}", e)))?; + let mut handle = connect_with_proxy(&app, &config, ssh_config, handler).await?; match &config.auth { SshAuth::Password { password } => { diff --git a/src/App.tsx b/src/App.tsx index a69b68cb..38d02917 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,9 @@ import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import AboutDialog from "./components/dialog/AboutDialog"; +import GlobalContextMenu from "./components/dialog/GlobalContextMenu"; import NewSessionDialog from "./components/dialog/NewSessionDialog"; +import SettingsDialog from "./components/dialog/SettingsDialog"; import Header from "./components/layout/Header"; import ResizeHandle from "./components/layout/ResizeHandle"; import StatusBar from "./components/layout/StatusBar"; @@ -9,11 +11,13 @@ import ActiveSessions from "./components/panel/ActiveSessions"; import CommandHistory from "./components/panel/CommandHistory"; import SavedConnections from "./components/panel/SavedConnections"; import FileExplorer from "./components/sidebar/FileExplorer"; +import FileTransfer from "./components/sidebar/FileTransfer"; import QuickCommands from "./components/terminal/QuickCommands"; import TabBar from "./components/terminal/TabBar"; import XTerminal from "./components/terminal/XTerminal"; import ToastContainer from "./components/toast/ToastContainer"; import { ToastProvider } from "./components/toast/ToastContext"; +import { TransferProvider } from "./context/TransferContext"; import { useApp } from "./context/AppContext"; import type { SavedConnection } from "./types"; @@ -69,8 +73,8 @@ function App() { // Handle connection success from Dialog or SavedConnections const handleSessionConnected = useCallback( - (sessionId: string, name: string, type_: "SSH" | "Local") => { - addTab(sessionId, name, type_); + (sessionId: string, name: string, type_: "SSH" | "Local", connectionId?: string) => { + addTab(sessionId, name, type_, connectionId); setShowNewSession(false); setEditingConnection(undefined); }, @@ -145,219 +149,263 @@ function App() { [updateUiConfig, uiConfig.quick_cmd_height], ); + const handleFileTransferResize = useCallback( + (delta: number) => { + updateUiConfig({ + file_transfer_height: Math.max(80, Math.min(500, (uiConfig.file_transfer_height || 240) - delta)), + }); + }, + [updateUiConfig, uiConfig.file_transfer_height], + ); + return ( -
- {/* Header */} -
setMobileLeftOpen(!mobileLeftOpen)} - onToggleRight={() => setMobileRightOpen(!mobileRightOpen)} - onAbout={() => setShowAbout(true)} - /> + +
+ {/* Header */} +
setMobileLeftOpen(!mobileLeftOpen)} + onToggleRight={() => setMobileRightOpen(!mobileRightOpen)} + onAbout={() => setShowAbout(true)} + /> - {/* Main Content */} -
- {/* Backdrop for mobile */} - {(mobileLeftOpen || mobileRightOpen) && ( -
{ - setMobileLeftOpen(false); - setMobileRightOpen(false); - }} - /> - )} - - {/* Left Sidebar - File Explorer */} - {uiConfig.show_file_explorer && ( - <> + {/* Main Content */} +
+ {/* Backdrop for mobile */} + {(mobileLeftOpen || mobileRightOpen) && (
{ + setMobileLeftOpen(false); + setMobileRightOpen(false); + }} + /> + )} + + {/* Left Sidebar - File Explorer & File Transfer */} + {(uiConfig.show_file_explorer || uiConfig.show_file_transfer) && ( + <> +
-
- -
-
- {/* Only show resize handle on desktop */} - - - )} - - {/* Center - Terminal Area */} -
- {/* Tab Bar */} - - - {/* Terminal Instances */} -
- {tabs.length === 0 ? ( -
-
- terminal -

{t("app.noActiveSessions")}

- -
-
- ) : ( - tabs.map((tab) => ( - - )) - )} -
- - {/* Quick Commands Bar */} - {uiConfig.show_quick_commands && ( - <> - -
- -
- - )} -
- - {/* Right Sidebar: three independent panels */} - {(uiConfig.show_saved_connections || - uiConfig.show_active_sessions || - uiConfig.show_command_history) && ( - <> - {/* Only show resize handle on desktop */} - - + {uiConfig.show_file_transfer && ( + <> + {uiConfig.show_file_explorer && ( + + )} +
+ +
+ + )} +
+
+ {/* Only show resize handle on desktop */} + )} -
- {/* Status Bar */} - + {/* Center - Terminal Area */} +
+ {/* Tab Bar */} + - {/* New Session Dialog */} - { - setShowNewSession(false); - setEditingConnection(undefined); - }} - onConnect={handleSessionConnected} - onSaved={() => { - setShowNewSession(false); - setEditingConnection(undefined); - refreshConnections(); - }} - initialData={editingConnection} - /> + {/* Terminal Instances */} +
+ {tabs.length === 0 ? ( +
+
+ terminal +

{t("app.noActiveSessions")}

+ +
+
+ ) : ( + tabs.map((tab) => ( + + )) + )} +
- setShowAbout(false)} /> + {/* Quick Commands Bar */} + {uiConfig.show_quick_commands && ( + <> + +
+ +
+ + )} +
- -
+ {/* Right Sidebar: three independent panels */} + {(uiConfig.show_saved_connections || + uiConfig.show_active_sessions || + uiConfig.show_command_history) && ( + <> + {/* Only show resize handle on desktop */} + + + + )} + + + {/* Status Bar */} + + + {/* New Session Dialog */} + { + setShowNewSession(false); + setEditingConnection(undefined); + }} + onConnect={handleSessionConnected} + onSaved={() => { + setShowNewSession(false); + setEditingConnection(undefined); + refreshConnections(); + }} + initialData={editingConnection} + /> + + setShowAbout(false)} /> + + + + +
+
); } diff --git a/src/components/dialog/AboutDialog.tsx b/src/components/dialog/AboutDialog.tsx index f2845fcf..b58a9873 100644 --- a/src/components/dialog/AboutDialog.tsx +++ b/src/components/dialog/AboutDialog.tsx @@ -22,6 +22,16 @@ export default function AboutDialog({ open, onClose }: AboutDialogProps) { } }, [open]); + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape" && open) { + onClose(); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [open, onClose]); + if (!open) return null; return ( diff --git a/src/components/dialog/NewSessionDialog.tsx b/src/components/dialog/NewSessionDialog.tsx index 69a4d2a6..7fde92d3 100644 --- a/src/components/dialog/NewSessionDialog.tsx +++ b/src/components/dialog/NewSessionDialog.tsx @@ -108,6 +108,17 @@ export default function NewSessionDialog({ } }, [open, initialData, resetForm]); + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape" && open && !connecting && !showGroupDropdown) { + resetForm(); + onClose(); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [open, connecting, showGroupDropdown, resetForm, onClose]); + const handleClose = () => { resetForm(); onClose(); diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 33ab0ac3..9dac1aa8 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -5,6 +5,7 @@ import { appLogDir } from "@tauri-apps/api/path"; import packageJson from "../../../package.json"; import { useApp } from "../../context/AppContext"; import { useTheme } from "../../context/ThemeContext"; +import { AVAILABLE_LANGUAGES } from "../../i18n"; interface HeaderProps { onNewSession: () => void; @@ -29,7 +30,7 @@ export default function Header({ const [activeMenu, setActiveMenu] = useState(null); const menuRef = useRef(null); const { themeName, setTheme, themeNames } = useTheme(); - const { uiConfig, updateUiConfig } = useApp(); + const { uiConfig, updateUiConfig, setShowSettingsDialog } = useApp(); const { t, i18n } = useTranslation(); const changeLanguage = (lng: string) => { @@ -117,10 +118,11 @@ export default function Header({ { label: t("menu.language"), icon: "translate", - submenu: [ - { label: "English", checked: i18n.language === "en", action: () => changeLanguage("en") }, - { label: "中文", checked: i18n.language === "zh-CN", action: () => changeLanguage("zh-CN") }, - ], + submenu: AVAILABLE_LANGUAGES.map((l) => ({ + label: l.name, + checked: i18n.language === l.id, + action: () => changeLanguage(l.id) + })), }, { label: "separator", separator: true }, { label: t("menu.zoomIn"), action: () => handleZoom(0.1), icon: "zoom_in" }, @@ -227,10 +229,10 @@ export default function Header({ view_sidebar - - search - - + setShowSettingsDialog(true)} + > settings diff --git a/src/components/panel/SavedConnections.tsx b/src/components/panel/SavedConnections.tsx index dc325871..7e08acfe 100644 --- a/src/components/panel/SavedConnections.tsx +++ b/src/components/panel/SavedConnections.tsx @@ -8,7 +8,7 @@ import { useToast } from "../toast/ToastContext"; interface SavedConnectionsProps { onEditConnection: (connection: SavedConnection) => void; - onSessionCreated: (sessionId: string, name: string, type: SessionType) => void; + onSessionCreated: (sessionId: string, name: string, type: SessionType, connectionId?: string) => void; } interface HoverInfo { @@ -69,7 +69,7 @@ export default function SavedConnections({ setConnectingId(conn.id); try { const sessionId = await invoke("create_ssh_session", { connectionId: conn.id }); - onSessionCreated(sessionId, conn.name, "SSH"); + onSessionCreated(sessionId, conn.name, "SSH", conn.id); } catch (e) { logger.error(`SSH connection failed for "${conn.name}"`, e); toast.error(t("savedConnections.connectionFailed", { error: e })); diff --git a/src/context/AppContext.tsx b/src/context/AppContext.tsx index bb861630..86800b45 100644 --- a/src/context/AppContext.tsx +++ b/src/context/AppContext.tsx @@ -9,20 +9,38 @@ import { } from "react"; import { invoke } from "../lib/invoke"; import { logger } from "../lib/logger"; -import type { Group, SavedConnection, SessionType, Tab, UiConfig } from "../types"; +import type { AppSettings, Group, SavedConnection, SessionType, Tab, UiConfig } from "../types"; + +export interface ContextMenuItem { + icon?: string; + label: string; + color?: string; + onClick: () => void; + divider?: boolean; +} + +export interface ContextMenuState { + x: number; + y: number; + items: ContextMenuItem[]; +} interface AppContextType { // Tabs tabs: Tab[]; activeTabId: string | null; setActiveTabId: (id: string | null) => void; - addTab: (sessionId: string, name: string, type: SessionType) => void; + addTab: (sessionId: string, name: string, type: SessionType, connectionId?: string) => void; closeTab: (tabId: string) => void; // UI Config uiConfig: UiConfig; updateUiConfig: (updates: Partial) => void; + // App Settings + appSettings: AppSettings; + updateAppSettings: (updates: Partial) => void; + // Data savedConnections: SavedConnection[]; savedGroups: Group[]; @@ -33,6 +51,13 @@ interface AppContextType { setShowNewSession: (show: boolean) => void; editingConnection: SavedConnection | undefined; setEditingConnection: (conn: SavedConnection | undefined) => void; + showSettingsDialog: boolean; + setShowSettingsDialog: (show: boolean) => void; + + // Global Context Menu + contextMenu: ContextMenuState | null; + showContextMenu: (state: ContextMenuState) => void; + hideContextMenu: () => void; } /** @@ -42,21 +67,76 @@ interface AppContextType { const AppContext = createContext(null); const DEFAULT_UI_CONFIG: UiConfig = { + open_tabs: [], left_width: 256, right_width: 288, saved_conn_height: 240, history_height: 200, quick_cmd_height: 36, + file_transfer_height: 240, show_file_explorer: true, + show_file_transfer: true, show_saved_connections: true, show_active_sessions: true, show_command_history: true, show_quick_commands: true, zoom_level: 1.0, - theme: "github-dark", language: "en", }; +const DEFAULT_APP_SETTINGS: AppSettings = { + general: { + startup_restore: true, + default_local_shell: navigator.userAgent.includes("Win") ? "powershell.exe" : "bash", + minimize_to_tray: false, + boss_key: null, + }, + appearance: { + theme: "github-dark", + font_family: "JetBrains Mono, Fira Code, Consolas, monospace", + font_size: 14, + ligatures: false, + background_opacity: 1.0, + cursor_style: "block", + cursor_blink: true, + }, + proxy: { + enabled: false, + protocol: "socks5", + host: "127.0.0.1", + port: 1080, + }, + search: { + default_engine: "google", + custom_engines: [ + { name: "Google", url_template: "https://google.com/search?q=%s" }, + { name: "Bing", url_template: "https://bing.com/search?q=%s" }, + { name: "GitHub", url_template: "https://github.com/search?q=%s" }, + ], + }, + translation: { + provider: "deepl", + api_key: "", + }, + security: { + use_os_keyring: true, + require_master_password: false, + idle_lock_minutes: 0, + host_key_policy: "prompt", + }, + terminal: { + scrollback_lines: 10000, + keep_alive_interval: 60, + hardware_acceleration: false, + }, + interaction: { + copy_on_select: true, + right_click_paste: true, + word_separators: " ()[]{}\"'", + default_encoding: "UTF-8", + }, +}; + /** Provides tabs, uiConfig, savedConnections, and dialog state to the app. */ export function AppProvider({ children }: { children: ReactNode }) { // Tabs State @@ -68,6 +148,11 @@ export function AppProvider({ children }: { children: ReactNode }) { const uiConfigLoaded = useRef(false); const saveTimerRef = useRef | null>(null); + // App Settings State + const [appSettings, setAppSettings] = useState(DEFAULT_APP_SETTINGS); + const appSettingsLoaded = useRef(false); + const appSettingsSaveTimerRef = useRef | null>(null); + // Data State const [savedConnections, setSavedConnections] = useState([]); const [savedGroups, setSavedGroups] = useState([]); @@ -77,6 +162,18 @@ export function AppProvider({ children }: { children: ReactNode }) { const [editingConnection, setEditingConnection] = useState( undefined, ); + const [showSettingsDialog, setShowSettingsDialog] = useState(false); + + // Global Context Menu State + const [contextMenu, setContextMenu] = useState(null); + + const showContextMenu = useCallback((state: ContextMenuState) => { + setContextMenu(state); + }, []); + + const hideContextMenu = useCallback(() => { + setContextMenu(null); + }, []); // 1. Load UI Config useEffect(() => { @@ -90,6 +187,18 @@ export function AppProvider({ children }: { children: ReactNode }) { }); }, []); + // 1.5. Load App Settings + useEffect(() => { + invoke("get_app_settings") + .then((cfg) => { + setAppSettings(cfg); + appSettingsLoaded.current = true; + }) + .catch(() => { + appSettingsLoaded.current = true; + }); + }, []); + // 2. Save UI Config Debounced const updateUiConfig = useCallback((updates: Partial) => { setUiConfig((prev) => { @@ -107,6 +216,22 @@ export function AppProvider({ children }: { children: ReactNode }) { }); }, []); + // 2.5 Save App Settings Debounced + const updateAppSettings = useCallback((updates: Partial) => { + setAppSettings((prev) => { + const next = { ...prev, ...updates }; + if (appSettingsLoaded.current) { + if (appSettingsSaveTimerRef.current) clearTimeout(appSettingsSaveTimerRef.current); + appSettingsSaveTimerRef.current = setTimeout(() => { + invoke("save_app_settings", { settings: next }).catch((e) => + logger.error("Failed to save app settings", e), + ); + }, 500); + } + return next; + }); + }, []); + // 3. Load Connections const refreshConnections = useCallback(async () => { try { @@ -128,9 +253,9 @@ export function AppProvider({ children }: { children: ReactNode }) { }, [refreshConnections]); // 4. Tab Logic - const addTab = useCallback((sessionId: string, name: string, type: SessionType) => { + const addTab = useCallback((sessionId: string, name: string, type: SessionType, connectionId?: string) => { const tabId = `tab-${Date.now()}`; - const newTab: Tab = { id: tabId, sessionId, name, type }; + const newTab: Tab = { id: tabId, sessionId, name, type, connectionId }; setTabs((prev) => [...prev, newTab]); setActiveTabId(tabId); @@ -155,6 +280,40 @@ export function AppProvider({ children }: { children: ReactNode }) { }, [activeTabId], ); + // 5. Startup Restore Logic + const hasRestored = useRef(false); + + useEffect(() => { + if (!hasRestored.current && uiConfigLoaded.current && appSettingsLoaded.current) { + hasRestored.current = true; + if (appSettings.general.startup_restore && uiConfig.open_tabs && uiConfig.open_tabs.length > 0) { + uiConfig.open_tabs.forEach(tab => { + if (tab.session_type === "SSH" && tab.connection_id) { + invoke("create_ssh_session", { connectionId: tab.connection_id }).then(sessionId => { + addTab(sessionId, tab.title, "SSH", tab.connection_id); + }).catch(e => logger.error(`Restore SSH failed for ${tab.title}`, e)); + } else if (tab.session_type === "Local" || tab.session_type === "local") { + invoke("create_local_session").then(sessionId => { + addTab(sessionId, tab.title, "Local"); + }).catch(e => logger.error(`Restore Local failed`, e)); + } + }); + } + } + }, [uiConfig, appSettings, addTab]); + + // 6. Sync opened tabs + useEffect(() => { + if (hasRestored.current && appSettings.general.startup_restore) { + updateUiConfig({ + open_tabs: tabs.map(t => ({ + title: t.name, + session_type: t.type, + connection_id: t.connectionId + })) + }); + } + }, [tabs, appSettings.general.startup_restore, updateUiConfig]); return ( {children} diff --git a/src/context/ThemeContext.tsx b/src/context/ThemeContext.tsx index cf03349e..72a999f7 100644 --- a/src/context/ThemeContext.tsx +++ b/src/context/ThemeContext.tsx @@ -34,10 +34,10 @@ function applyThemeToDOM(colors: ThemeColors) { root.setProperty("--df-accent", colors.accent); } -/** Provides theme, themeName, setTheme. Syncs with uiConfig.theme from backend. */ +/** Provides theme, themeName, setTheme. Syncs with appSettings.appearance.theme from backend. */ export function ThemeProvider({ children }: { children: ReactNode }) { - const { uiConfig, updateUiConfig } = useApp(); - const initialId = uiConfig.theme || DEFAULT_THEME_ID; + const { appSettings, updateAppSettings } = useApp(); + const initialId = appSettings.appearance.theme || DEFAULT_THEME_ID; const [themeName, setThemeName] = useState(initialId); const current = themes[themeName] || themes[DEFAULT_THEME_ID]; @@ -47,21 +47,22 @@ export function ThemeProvider({ children }: { children: ReactNode }) { applyThemeToDOM(current.colors); }, [current]); - // Sync from backend when uiConfig.theme changes (e.g. on load) + // Sync from backend when appSettings.appearance.theme changes (e.g. on load or from Settings dialog) useEffect(() => { - if (uiConfig.theme && uiConfig.theme !== themeName && themes[uiConfig.theme]) { - setThemeName(uiConfig.theme); + const configTheme = appSettings.appearance.theme; + if (configTheme && configTheme !== themeName && themes[configTheme]) { + setThemeName(configTheme); } - }, [uiConfig.theme, themeName]); + }, [appSettings.appearance.theme, themeName]); const setTheme = useCallback( (id: string) => { if (themes[id]) { setThemeName(id); - updateUiConfig({ theme: id }); + updateAppSettings({ appearance: { ...appSettings.appearance, theme: id } }); } }, - [updateUiConfig], + [appSettings.appearance, updateAppSettings], ); return ( diff --git a/src/main.tsx b/src/main.tsx index c2856eae..90d54b15 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -14,6 +14,8 @@ import "./index.css"; import { AppProvider } from "./context/AppContext"; import { ThemeProvider } from "./context/ThemeContext"; +document.addEventListener("contextmenu", (e) => e.preventDefault()); + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(