diff --git a/frontend/src/App.settings-center.test.ts b/frontend/src/App.settings-center.test.ts index f7fac223..743a4bc7 100644 --- a/frontend/src/App.settings-center.test.ts +++ b/frontend/src/App.settings-center.test.ts @@ -7,10 +7,15 @@ const appSource = readFileSync( 'utf8', ); +const aiSettingsModalSource = readFileSync( + fileURLToPath(new globalThis.URL('./components/AISettingsModal.tsx', import.meta.url)), + 'utf8', +); + describe('settings center layout', () => { it('uses the same split navigation shell as the tool center', () => { expect(appSource).toContain("type SettingsCenterGroupKey = 'preferences' | 'services' | 'about';"); - expect(appSource).toContain("type SettingsCenterPaneKey = 'language' | 'sidebar-metadata' | 'proxy' | 'web-auth';"); + expect(appSource).toContain("type SettingsCenterPaneKey = 'language' | 'theme' | 'sidebar-metadata' | 'proxy' | 'web-auth' | 'ai' | 'about-go-navi';"); expect(appSource).toContain("const [activeSettingsCenterGroupKey, setActiveSettingsCenterGroupKey] = useState('preferences');"); expect(appSource).toContain("const [activeSettingsCenterPane, setActiveSettingsCenterPane] = useState(null);"); expect(appSource).toContain('style={toolCenterModalWorkspaceStyle}'); @@ -49,4 +54,64 @@ describe('settings center layout', () => { expect(appSource).toContain("handleOpenSettingsCenterPane('services', 'web-auth')"); expect(appSource).toContain(" { + expect(appSource).toContain("const DEFAULT_GLOBAL_PROXY_TEST_URL = 'https://api.github.com/';"); + expect(appSource).toContain('const [proxyTestUrl, setProxyTestUrl]'); + expect(appSource).toContain('const [proxyTesting, setProxyTesting]'); + expect(appSource).toContain('const [proxyTestResult, setProxyTestResult]'); + expect(appSource).toContain('handleTestGlobalProxyDraft'); + expect(appSource).toContain('TestGlobalProxyConnection'); + expect(appSource).toContain('https://github.com/Syngnat/GoNavi/releases/latest'); + expect(appSource).toContain("t('app.proxy.test.action')"); + expect(appSource).toContain("t('app.proxy.test.target_placeholder')"); + }); + + it('adds cancel and back actions to settings center detail panes', () => { + expect(appSource).toContain('const handleBackFromSettingsCenterPane = useCallback(() => {'); + expect(appSource).toContain('const handleCancelSettingsCenterPane = useCallback(() => {'); + expect(appSource).toContain('onClick={handleCancelSettingsCenterPane}'); + expect(appSource).toContain("t('common.cancel')"); + expect(appSource).toContain('onClick={handleBackFromSettingsCenterPane}'); + expect(appSource).toContain("t('common.back_to_previous')"); + }); + + it('opens theme, AI, and about entries inside settings center detail panes', () => { + expect(appSource).toContain("handleOpenSettingsCenterPane('preferences', 'theme')"); + expect(appSource).toContain("handleOpenSettingsCenterPane('services', 'ai')"); + expect(appSource).toContain("handleOpenSettingsCenterPane('about', 'about-go-navi')"); + expect(appSource).toContain("if (activeSettingsCenterPane.key === 'theme')"); + expect(appSource).toContain("if (activeSettingsCenterPane.key === 'ai')"); + expect(appSource).toContain(' { + expect(appSource).toContain('const renderSettingsCenterAboutPane = () => {'); + expect(appSource).toContain('const renderSettingsCenterAboutProjectEntry = ({'); + expect(appSource).toContain("padding: '18px 22px'"); + expect(appSource).toContain('width: 64'); + expect(appSource).toContain('height: 64'); + expect(appSource).toContain('minWidth: 260'); + expect(appSource).toContain("t('app.about.version_update.title')"); + expect(appSource).toContain("t('app.about.project.github.title')"); + expect(appSource).toContain("t('app.about.project.issues.title')"); + expect(appSource).toContain("t('app.about.project.releases.title')"); + expect(appSource).toContain('const renderSettingsCenterAboutFooter = () => ('); + expect(appSource).toContain("t('app.about.last_checked_at', { time: aboutLastCheckedAt })"); + expect(appSource).toContain('renderAboutUpdateActions()'); + }); + + it('keeps embedded split-pane settings stable at scroll boundaries', () => { + expect(appSource).toContain('const isSettingsCenterContainedScrollPane ='); + expect(appSource).toContain("activeSettingsCenterPane?.key === 'theme' || activeSettingsCenterPane?.key === 'ai'"); + expect(appSource).toContain('const settingsCenterDetailBodyStyle: React.CSSProperties = isSettingsCenterContainedScrollPane'); + expect(appSource).toContain("overflowY: 'hidden'"); + expect(appSource).toContain('style={settingsCenterDetailBodyStyle}'); + expect(appSource).toContain("boxSizing: 'border-box'"); + expect(appSource).toContain("overscrollBehavior: 'contain'"); + expect(aiSettingsModalSource).toContain("boxSizing: 'border-box'"); + expect(aiSettingsModalSource).toContain("overscrollBehavior: 'contain'"); + }); }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d31c3d50..f7266126 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,7 @@ import Modal from './components/common/ResizableDraggableModal'; import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; -import { Layout, Button, ConfigProvider, theme, message, Spin, Slider, Progress, Switch, Input, InputNumber, Select, Segmented, Tooltip } from 'antd'; -import { PlusOutlined, ConsoleSqlOutlined, UploadOutlined, DownloadOutlined, CloudDownloadOutlined, BugOutlined, ToolOutlined, GlobalOutlined, InfoCircleOutlined, GithubOutlined, SkinOutlined, CheckOutlined, MinusOutlined, BorderOutlined, CloseOutlined, SettingOutlined, LinkOutlined, BgColorsOutlined, AppstoreOutlined, RobotOutlined, FolderOpenOutlined, HddOutlined, SafetyCertificateOutlined, SwitcherOutlined, CodeOutlined, RightOutlined, TableOutlined, MenuOutlined, PoweroffOutlined } from '@ant-design/icons'; +import { Layout, Button, ConfigProvider, theme, message, Spin, Slider, Progress, Switch, Input, InputNumber, Select, Segmented, Tooltip, Alert } from 'antd'; +import { PlusOutlined, ConsoleSqlOutlined, UploadOutlined, DownloadOutlined, CloudDownloadOutlined, BugOutlined, ToolOutlined, GlobalOutlined, InfoCircleOutlined, GithubOutlined, SkinOutlined, CheckOutlined, MinusOutlined, BorderOutlined, CloseOutlined, SettingOutlined, LinkOutlined, BgColorsOutlined, AppstoreOutlined, RobotOutlined, FolderOpenOutlined, HddOutlined, SafetyCertificateOutlined, SwitcherOutlined, CodeOutlined, RightOutlined, TableOutlined, MenuOutlined, PoweroffOutlined, TagOutlined, UserOutlined, UpCircleOutlined, MessageOutlined, FileTextOutlined, SyncOutlined, SendOutlined } from '@ant-design/icons'; import { DndContext, PointerSensor, closestCenter, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core'; import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; @@ -16,7 +16,7 @@ import { type DataSyncEntryMode } from './components/dataSyncEntryMode'; import DriverManagerModal from './components/DriverManagerModal'; import LinuxCJKFontBanner from './components/LinuxCJKFontBanner'; import LogPanel from './components/LogPanel'; -import AISettingsModal from './components/AISettingsModal'; +import AISettingsModal, { AISettingsContent } from './components/AISettingsModal'; import AIChatPanel from './components/AIChatPanel'; import AIPanelErrorBoundary from './components/ai/AIPanelErrorBoundary'; import SecurityUpdateBanner from './components/SecurityUpdateBanner'; @@ -32,7 +32,7 @@ import { sanitizeV2SidebarRailScale, useStore, } from './store'; -import { SavedConnection, SecurityUpdateIssue, SecurityUpdateStatus } from './types'; +import { GlobalProxyConfig, SavedConnection, SecurityUpdateIssue, SecurityUpdateStatus } from './types'; import { blurToFilter, normalizeBlurForPlatform, normalizeOpacityForPlatform, isWindowsPlatform, resolveAppearanceValues } from './utils/appearance'; import { buildFontFamilyOptions, DEFAULT_MONO_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, getLinuxCJKFontInstallHint, matchFontFamilyOption, resolveMonoFontFamily, resolveUIFontFamily, sanitizeFontFamilyInput, type FontFamilyOption, type InstalledFontFamily } from './utils/fontFamilies'; import { @@ -55,7 +55,7 @@ import { import { getMacNativeTitlebarPaddingLeft, getMacNativeTitlebarPaddingRight, shouldHandleMacNativeFullscreenShortcut, shouldSuppressMacNativeEscapeExit } from './utils/macWindow'; import { shouldEnableMacWindowDiagnostics } from './utils/macWindowDiagnostics'; import { getConnectionWorkbenchState } from './utils/startupReadiness'; -import { toSaveGlobalProxyInput } from './utils/globalProxyDraft'; +import { createGlobalProxyDraft, toSaveGlobalProxyInput } from './utils/globalProxyDraft'; import { detectConnectionImportKind, isConnectionPackagePasswordRequiredError, @@ -220,12 +220,57 @@ type ToolCenterPaneState = { }; type SettingsCenterGroupKey = 'preferences' | 'services' | 'about'; -type SettingsCenterPaneKey = 'language' | 'sidebar-metadata' | 'proxy' | 'web-auth'; +type SettingsCenterPaneKey = 'language' | 'theme' | 'sidebar-metadata' | 'proxy' | 'web-auth' | 'ai' | 'about-go-navi'; type SettingsCenterPaneState = { key: SettingsCenterPaneKey; group: SettingsCenterGroupKey; }; +const DEFAULT_GLOBAL_PROXY_TEST_URL = 'https://api.github.com/'; + +type GlobalProxyTestResultState = { + success: boolean; + message: string; + url?: string; + finalUrl?: string; + statusCode?: number; + durationMs?: number; + viaProxy?: boolean; +}; + +const getGlobalProxyDefaultPort = (type: GlobalProxyConfig['type']): number => ( + type === 'http' ? 8080 : 1080 +); + +const createGlobalProxyComparableDraft = ( + value: Partial = {}, +): GlobalProxyConfig => ({ + ...createGlobalProxyDraft(value), + password: typeof value.password === 'string' ? value.password : '', +}); + +const areGlobalProxyDraftsEqual = ( + left: Partial, + right: Partial, +): boolean => { + const normalizedLeft = createGlobalProxyComparableDraft(left); + const normalizedRight = createGlobalProxyComparableDraft(right); + return ( + normalizedLeft.enabled === normalizedRight.enabled && + normalizedLeft.type === normalizedRight.type && + normalizedLeft.host === normalizedRight.host && + normalizedLeft.port === normalizedRight.port && + normalizedLeft.user === normalizedRight.user && + normalizedLeft.password === normalizedRight.password && + normalizedLeft.hasPassword === normalizedRight.hasPassword + ); +}; + +const formatAboutCheckedAt = (value: Date): string => { + const pad = (input: number) => String(input).padStart(2, '0'); + return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())} ${pad(value.getHours())}:${pad(value.getMinutes())}`; +}; + type ConnectionPackageDialogState = { open: boolean; mode: ConnectionPackageDialogMode; @@ -348,7 +393,6 @@ function App() { const startupFullscreen = useStore(state => state.startupFullscreen); const setStartupFullscreen = useStore(state => state.setStartupFullscreen); const globalProxy = useStore(state => state.globalProxy); - const setGlobalProxy = useStore(state => state.setGlobalProxy); const replaceConnections = useStore(state => state.replaceConnections); const replaceGlobalProxy = useStore(state => state.replaceGlobalProxy); const replaceSavedQueries = useStore(state => state.replaceSavedQueries); @@ -580,7 +624,6 @@ function App() { const aiPanelVisible = useStore(state => state.aiPanelVisible); const toggleAIPanel = useStore(state => state.toggleAIPanel); const setAIPanelVisible = useStore(state => state.setAIPanelVisible); - const globalProxyInvalidHintShownRef = React.useRef(false); const windowDiagSequenceRef = React.useRef(0); const windowDiagLastSignatureRef = React.useRef(''); const windowDiagLastAtRef = React.useRef(0); @@ -777,71 +820,6 @@ function App() { }; }, [applySecurityUpdateStatus, isStoreHydrated, replaceConnections, replaceGlobalProxy, t]); - useEffect(() => { - if (!isStoreHydrated || !hasLoadedSecureConfig) { - return; - } - - const host = String(globalProxy.host || '').trim(); - const port = Number(globalProxy.port); - const portValid = Number.isFinite(port) && port > 0 && port <= 65535; - const invalidWhenEnabled = globalProxy.enabled && (!host || !portValid); - - if (invalidWhenEnabled) { - if (!globalProxyInvalidHintShownRef.current) { - void message.warning({ - content: t('app.proxy.message.invalid_enabled'), - key: 'global-proxy-invalid', - }); - globalProxyInvalidHintShownRef.current = true; - } - return; - } - - globalProxyInvalidHintShownRef.current = false; - void message.destroy('global-proxy-invalid'); - - const backendApp = (window as any).go?.app?.App; - if (typeof backendApp?.SaveGlobalProxy !== 'function') { - return; - } - - let cancelled = false; - Promise.resolve( - backendApp.SaveGlobalProxy( - toSaveGlobalProxyInput({ - ...globalProxy, - host, - port: portValid ? port : (globalProxy.type === 'http' ? 8080 : 1080), - }) - ) - ) - .catch((err) => { - if (cancelled) { - return; - } - const errMsg = err instanceof Error ? err.message : String(err || t('common.unknown')); - void message.error({ - content: t('app.proxy.message.save_failed', { error: errMsg }), - key: 'global-proxy-sync-error', - }); - }); - - return () => { - cancelled = true; - }; - }, [ - isStoreHydrated, - hasLoadedSecureConfig, - globalProxy.enabled, - globalProxy.type, - globalProxy.host, - globalProxy.port, - globalProxy.user, - globalProxy.password, - t, - ]); - useEffect(() => { let cancelled = false; let startupWindowTimer: number | null = null; @@ -902,7 +880,7 @@ function App() { } catch (e) { console.warn("Wails Window APIs unavailable", e); } - + if (await checkStartupPreferenceApplied()) { return; } @@ -1433,7 +1411,7 @@ function App() { resolvedAppearance, sidebarWidth, }); - + const addTab = useStore(state => state.addTab); const activeContext = useStore(state => state.activeContext); const connections = useStore(state => state.connections); @@ -1718,6 +1696,18 @@ function App() { runtimeBuildType, t, }); + const [aboutLastCheckedAt, setAboutLastCheckedAt] = useState(''); + useEffect(() => { + if (!lastUpdateInfo) { + return; + } + setAboutLastCheckedAt(formatAboutCheckedAt(new Date())); + }, [ + lastUpdateInfo?.channel, + lastUpdateInfo?.currentVersion, + lastUpdateInfo?.hasUpdate, + lastUpdateInfo?.latestVersion, + ]); const emitWindowDiagnostic = useCallback(async (stage: string, extra: Record = {}) => { if (!macWindowDiagnosticsEnabled) { @@ -2262,9 +2252,10 @@ function App() { const [capturingShortcutAction, setCapturingShortcutAction] = useState(null); const tabDisplaySettingsPanelRef = useRef(null); const [tabDisplaySettingsFocusRequest, setTabDisplaySettingsFocusRequest] = useState(0); + const isThemeSettingsPaneOpen = activeSettingsCenterPane?.key === 'theme'; useEffect(() => { const shouldLoadInstalledFonts = - runtimePlatform === 'linux' || (isThemeModalOpen && themeModalSection === 'appearance'); + runtimePlatform === 'linux' || ((isThemeModalOpen || isThemeSettingsPaneOpen) && themeModalSection === 'appearance'); if (!shouldLoadInstalledFonts) { return; } @@ -2313,17 +2304,17 @@ function App() { return () => { cancelled = true; }; - }, [isThemeModalOpen, runtimePlatform, t, themeModalSection]); + }, [isThemeModalOpen, isThemeSettingsPaneOpen, runtimePlatform, t, themeModalSection]); useEffect(() => { - if (!isThemeModalOpen || themeModalSection !== 'appearance' || tabDisplaySettingsFocusRequest === 0) { + if ((!isThemeModalOpen && !isThemeSettingsPaneOpen) || themeModalSection !== 'appearance' || tabDisplaySettingsFocusRequest === 0) { return; } const timer = window.setTimeout(() => { tabDisplaySettingsPanelRef.current?.scrollIntoView({ block: 'start', behavior: 'smooth' }); }, 80); return () => window.clearTimeout(timer); - }, [isThemeModalOpen, themeModalSection, tabDisplaySettingsFocusRequest]); + }, [isThemeModalOpen, isThemeSettingsPaneOpen, themeModalSection, tabDisplaySettingsFocusRequest]); const shortcutConflictMap = useMemo(() => { const map: Partial> = {}; @@ -2342,6 +2333,12 @@ function App() { return map; }, [activeShortcutPlatform, language, shortcutOptions]); const [isProxyModalOpen, setIsProxyModalOpen] = useState(false); + const [proxyDraft, setProxyDraft] = useState(() => createGlobalProxyComparableDraft(globalProxy)); + const [proxyDraftClearPassword, setProxyDraftClearPassword] = useState(false); + const [proxyApplying, setProxyApplying] = useState(false); + const [proxyTestUrl, setProxyTestUrl] = useState(DEFAULT_GLOBAL_PROXY_TEST_URL); + const [proxyTesting, setProxyTesting] = useState(false); + const [proxyTestResult, setProxyTestResult] = useState(null); const [isDataRootModalOpen, setIsDataRootModalOpen] = useState(false); const [dataRootInfo, setDataRootInfo] = useState(null); const [selectedDataRootPath, setSelectedDataRootPath] = useState(''); @@ -2363,6 +2360,219 @@ function App() { panelWidth: DEFAULT_AI_PANEL_WIDTH, }) : DEFAULT_AI_PANEL_WIDTH; + const appliedGlobalProxyDraft = useMemo(() => ( + createGlobalProxyComparableDraft(globalProxy) + ), [ + globalProxy.enabled, + globalProxy.type, + globalProxy.host, + globalProxy.port, + globalProxy.user, + globalProxy.password, + globalProxy.hasPassword, + ]); + const proxyDraftHost = String(proxyDraft.host || '').trim(); + const proxyDraftUser = String(proxyDraft.user || '').trim(); + const proxyDraftPort = Number(proxyDraft.port); + const proxyDraftPortValid = Number.isFinite(proxyDraftPort) && proxyDraftPort > 0 && proxyDraftPort <= 65535; + const proxyDraftValid = !proxyDraft.enabled || (proxyDraftHost !== '' && proxyDraftPortValid); + const proxyDraftDirty = proxyDraftClearPassword || !areGlobalProxyDraftsEqual(proxyDraft, appliedGlobalProxyDraft); + const proxyPanelOpen = isProxyModalOpen || activeSettingsCenterPane?.key === 'proxy'; + const proxyPanelWasOpenRef = useRef(false); + const proxyStatusTone = proxyDraft.enabled + ? (proxyDraftValid ? 'success' : 'warning') + : 'info'; + const proxyStatusTitle = proxyDraft.enabled + ? (proxyDraftValid ? t('app.proxy.status.enabled') : t('app.proxy.status.incomplete')) + : t('app.proxy.status.disabled'); + const proxyStatusDescription = proxyDraft.enabled && proxyDraftValid + ? t('app.proxy.status.enabled_description', { + type: proxyDraft.type.toUpperCase(), + endpoint: `${proxyDraftHost}:${proxyDraftPort}`, + }) + : (proxyDraft.enabled + ? t('app.proxy.status.incomplete_description') + : t('app.proxy.status.disabled_description')); + const proxyPresetItems = useMemo(() => ([ + { key: 'clash-mixed', label: t('app.proxy.preset.clash_mixed'), type: 'socks5' as const, host: '127.0.0.1', port: 7890 }, + { key: 'socks5-local', label: t('app.proxy.preset.socks5_local'), type: 'socks5' as const, host: '127.0.0.1', port: 1080 }, + { key: 'http-local', label: t('app.proxy.preset.http_local'), type: 'http' as const, host: '127.0.0.1', port: 8080 }, + ]), [t]); + const proxyTestPresetItems = useMemo(() => ([ + { key: 'github-api', label: t('app.proxy.test.preset.github_api'), url: 'https://api.github.com/' }, + { key: 'github-release', label: t('app.proxy.test.preset.github_release'), url: 'https://github.com/Syngnat/GoNavi/releases/latest' }, + { key: 'go-module-proxy', label: t('app.proxy.test.preset.go_module_proxy'), url: 'https://proxy.golang.org/' }, + { key: 'baidu', label: t('app.proxy.test.preset.baidu'), url: 'https://www.baidu.com/' }, + ]), [t]); + const proxyTestUrlTrimmed = String(proxyTestUrl || '').trim(); + const proxyCanTest = proxyDraft.enabled && proxyDraftValid && proxyTestUrlTrimmed !== '' && !proxyTesting; + useEffect(() => { + if (!proxyPanelOpen) { + proxyPanelWasOpenRef.current = false; + return; + } + if (proxyPanelWasOpenRef.current) { + return; + } + proxyPanelWasOpenRef.current = true; + setProxyDraft(appliedGlobalProxyDraft); + setProxyDraftClearPassword(false); + }, [appliedGlobalProxyDraft, proxyPanelOpen]); + useEffect(() => { + setProxyTestResult(null); + }, [ + proxyDraft.enabled, + proxyDraft.type, + proxyDraft.host, + proxyDraft.port, + proxyDraft.user, + proxyDraft.password, + proxyDraftClearPassword, + proxyTestUrlTrimmed, + ]); + const resetProxyDraftToCurrent = useCallback(() => { + setProxyDraft(appliedGlobalProxyDraft); + setProxyDraftClearPassword(false); + }, [appliedGlobalProxyDraft]); + const updateProxyDraftType = useCallback((type: GlobalProxyConfig['type']) => { + setProxyDraft((current) => { + const currentPort = Number(current.port); + const previousDefault = getGlobalProxyDefaultPort(current.type); + const shouldSwitchPort = !Number.isFinite(currentPort) || currentPort === previousDefault; + return { + ...current, + type, + port: shouldSwitchPort ? getGlobalProxyDefaultPort(type) : current.port, + }; + }); + }, []); + const applyProxyPreset = useCallback((preset: { type: GlobalProxyConfig['type']; host: string; port: number }) => { + setProxyDraft((current) => ({ + ...current, + enabled: true, + type: preset.type, + host: preset.host, + port: preset.port, + })); + }, []); + const handleTestGlobalProxyDraft = useCallback(async () => { + if (!proxyDraft.enabled) { + void message.warning(t('app.proxy.test.message.enable_first')); + return; + } + if (!proxyDraftValid) { + void message.warning(t('app.proxy.message.invalid_enabled')); + return; + } + if (proxyTestUrlTrimmed === '') { + void message.warning(t('app.proxy.test.message.url_required')); + return; + } + const backendApp = (window as any).go?.app?.App; + if (typeof backendApp?.TestGlobalProxyConnection !== 'function') { + void message.error(t('app.proxy.test.message.unavailable')); + return; + } + setProxyTesting(true); + try { + const res = await backendApp.TestGlobalProxyConnection({ + proxy: toSaveGlobalProxyInput({ + ...proxyDraft, + host: proxyDraftHost, + user: proxyDraftUser, + port: proxyDraftPortValid ? proxyDraftPort : getGlobalProxyDefaultPort(proxyDraft.type), + clearPassword: proxyDraftClearPassword, + }), + url: proxyTestUrlTrimmed, + timeoutSeconds: 8, + }); + const data = (res?.data || {}) as Partial; + const statusCode = Number(data.statusCode); + setProxyTestResult({ + success: res?.success === true, + message: res?.message || t('common.unknown'), + url: data.url || proxyTestUrlTrimmed, + finalUrl: data.finalUrl, + statusCode: Number.isFinite(statusCode) ? statusCode : undefined, + durationMs: typeof data.durationMs === 'number' ? data.durationMs : undefined, + viaProxy: data.viaProxy === true, + }); + } catch (err) { + const errMsg = err instanceof Error ? err.message : String(err || t('common.unknown')); + setProxyTestResult({ + success: false, + message: errMsg, + url: proxyTestUrlTrimmed, + }); + } finally { + setProxyTesting(false); + } + }, [ + proxyDraft, + proxyDraftClearPassword, + proxyDraftHost, + proxyDraftPort, + proxyDraftPortValid, + proxyDraftUser, + proxyDraftValid, + proxyTestUrlTrimmed, + t, + ]); + const handleApplyGlobalProxyDraft = useCallback(async () => { + if (!proxyDraftValid) { + void message.warning({ + content: t('app.proxy.message.invalid_enabled'), + key: 'global-proxy-invalid', + }); + return; + } + void message.destroy('global-proxy-invalid'); + const backendApp = (window as any).go?.app?.App; + if (typeof backendApp?.SaveGlobalProxy !== 'function') { + void message.error({ + content: t('app.proxy.message.save_failed', { error: t('common.unknown') }), + key: 'global-proxy-sync-error', + }); + return; + } + const saveInput = toSaveGlobalProxyInput({ + ...proxyDraft, + host: proxyDraftHost, + user: proxyDraftUser, + port: proxyDraftPortValid ? proxyDraftPort : getGlobalProxyDefaultPort(proxyDraft.type), + clearPassword: proxyDraftClearPassword, + }); + setProxyApplying(true); + try { + const saved = await backendApp.SaveGlobalProxy(saveInput); + const nextDraft = createGlobalProxyComparableDraft(saved || saveInput); + replaceGlobalProxy(nextDraft); + setProxyDraft(nextDraft); + setProxyDraftClearPassword(false); + void message.success({ + content: t('app.proxy.message.config_applied'), + key: 'global-proxy-applied', + }); + } catch (err) { + const errMsg = err instanceof Error ? err.message : String(err || t('common.unknown')); + void message.error({ + content: t('app.proxy.message.save_failed', { error: errMsg }), + key: 'global-proxy-sync-error', + }); + } finally { + setProxyApplying(false); + } + }, [ + proxyDraft, + proxyDraftClearPassword, + proxyDraftHost, + proxyDraftPort, + proxyDraftPortValid, + proxyDraftUser, + proxyDraftValid, + replaceGlobalProxy, + t, + ]); const legacyAiEdgeHandleDockStyle = useMemo( () => resolveLegacyAIEdgeHandleDockStyle(legacyAiEdgeHandleAttachment), [legacyAiEdgeHandleAttachment], @@ -2390,6 +2600,15 @@ function App() { setActiveSettingsCenterPane({ key, group }); setIsSettingsModalOpen(true); }, []); + const handleBackFromSettingsCenterPane = useCallback(() => { + const returnGroup = activeSettingsCenterPane?.group ?? activeSettingsCenterGroupKey; + setActiveSettingsCenterGroupKey(returnGroup); + setActiveSettingsCenterPane(null); + }, [activeSettingsCenterGroupKey, activeSettingsCenterPane?.group]); + const handleCancelSettingsCenterPane = useCallback(() => { + setActiveSettingsCenterPane(null); + setIsSettingsModalOpen(false); + }, []); const handleOpenToolCenterPane = useCallback((group: ToolCenterGroupKey, key: ToolCenterPaneKey) => { setToolCenterBackGroupKey(group); setActiveToolCenterGroupKey(group); @@ -2536,7 +2755,7 @@ function App() { const handleCloseLogPanel = useCallback(() => { handleCloseAppLogPanel(); }, [handleCloseAppLogPanel]); - + const handleCreateConnection = useCallback(() => { setSecurityUpdateRepairSource(null); setEditingConnection(null); @@ -2853,7 +3072,7 @@ function App() { message.error(t('app.window_zoom.message.reset_failed')); } }, [t]); - + const { ghostRef, handleSidebarMouseDown, @@ -3276,74 +3495,261 @@ function App() { ), }); }, [setQueryOptions, sidebarMetadataFieldItems, sidebarTableMetadataFields]); - const renderProxySettingsContent = useCallback(() => ( -
-
-
{t('app.proxy.section_title')}
-
- {t('app.proxy.enable')} - setGlobalProxy({ enabled: checked })} /> + const renderProxySettingsContent = useCallback(() => { + const fieldLabelStyle: React.CSSProperties = { + marginBottom: 6, + fontSize: 12, + color: darkMode ? 'rgba(255,255,255,0.55)' : 'rgba(16,24,40,0.58)', + }; + const proxyGridColumns = viewportWidth < 760 ? '1fr' : '1fr 1fr'; + const proxyCardAccent = proxyDraft.enabled + ? (darkMode ? 'rgba(34,197,94,0.12)' : 'rgba(16,185,129,0.10)') + : (darkMode ? 'rgba(148,163,184,0.12)' : 'rgba(71,85,105,0.08)'); + const proxyTestAlertType = proxyTestResult + ? (!proxyTestResult.success ? 'error' : ((proxyTestResult.statusCode || 0) >= 400 ? 'warning' : 'success')) + : 'info'; + + return ( +
+
+
+
+
{t('app.proxy.section_title')}
+
{t('app.proxy.description')}
+
+ setProxyDraft((current) => ({ ...current, enabled: checked }))} + /> +
+ +
+ {proxyPresetItems.map((preset) => ( + + ))} +
+ {proxyDraftDirty && ( +
+ {t('app.proxy.unsaved_hint')} +
+ )}
-
-
-
{t('app.proxy.type')}
- setProxyDraft((current) => ({ ...current, host: e.target.value }))} + /> +
-
-
{t('app.proxy.port')}
- setGlobalProxy({ - port: typeof value === 'number' ? value : (globalProxy.type === 'http' ? 8080 : 1080), - })} - /> -
-
-
{t('app.proxy.host')}
- setGlobalProxy({ host: e.target.value })} - /> -
-
-
{t('app.proxy.username_optional')}
- setGlobalProxy({ user: e.target.value })} - /> -
-
-
{t('app.proxy.password_optional')}
- setGlobalProxy({ password: e.target.value })} - /> +
+ {proxyDraft.enabled ? t('app.proxy.enabled_edit_hint') : t('app.proxy.disabled_hint')}
-
- {t('app.proxy.scope_hint')} + +
+
{t('app.proxy.auth_title')}
+
+
+
{t('app.proxy.username_optional')}
+ setProxyDraft((current) => ({ ...current, user: e.target.value }))} + /> +
+
+
{t('app.proxy.password_optional')}
+ { + const nextPassword = e.target.value; + setProxyDraft((current) => ({ + ...current, + password: nextPassword, + hasPassword: nextPassword !== '' ? true : current.hasPassword, + })); + setProxyDraftClearPassword(false); + }} + /> +
+
+ {proxyDraftClearPassword ? ( + + ) : proxyDraft.hasPassword && proxyDraft.password === '' ? ( +
+ {t('app.proxy.password_saved_hint')} + +
+ ) : ( +
{t('app.proxy.no_auth_hint')}
+ )} +
+ +
+
+
+
{t('app.proxy.test.title')}
+
{t('app.proxy.test.description')}
+
+ +
+
+
+
{t('app.proxy.test.target_label')}
+ setProxyTestUrl(event.target.value)} + onPressEnter={() => { + if (proxyCanTest) { + void handleTestGlobalProxyDraft(); + } + }} + /> + {!proxyDraft.enabled && ( +
{t('app.proxy.test.disabled_hint')}
+ )} + {proxyTestResult && ( + + )} +
+
+ +
+
{t('app.proxy.scope_hint')}
+
+ + +
-
- ), [darkMode, globalProxy.enabled, globalProxy.host, globalProxy.password, globalProxy.port, globalProxy.type, globalProxy.user, setGlobalProxy, t, utilityPanelStyle]); + ); + }, [ + applyProxyPreset, + darkMode, + handleApplyGlobalProxyDraft, + proxyApplying, + proxyCanTest, + proxyDraft.enabled, + proxyDraft.hasPassword, + proxyDraft.host, + proxyDraft.password, + proxyDraft.port, + proxyDraft.type, + proxyDraft.user, + proxyDraftClearPassword, + proxyDraftDirty, + proxyDraftHost, + proxyDraftPortValid, + proxyTestPresetItems, + proxyTestResult, + proxyTesting, + proxyTestUrl, + proxyTestUrlTrimmed, + proxyPresetItems, + proxyStatusDescription, + proxyStatusTitle, + proxyStatusTone, + resetProxyDraftToCurrent, + t, + handleTestGlobalProxyDraft, + updateProxyDraftType, + utilityMutedTextStyle, + utilityPanelStyle, + viewportWidth, + ]); const renderSidebarMetadataSettingsPane = useCallback(() => (
@@ -3406,6 +3812,1116 @@ function App() { utilityMutedTextStyle, utilityPanelStyle, ]); + const renderAboutUpdateActions = (closeAction?: React.ReactNode) => [ + isBackgroundProgressForLatestUpdate && !isLatestUpdateDownloaded ? ( + + ) : null, + lastUpdateInfo?.hasUpdate && !isLatestUpdateDownloaded && !isBackgroundProgressForLatestUpdate ? ( + + ) : null, + , + closeAction ?? null, + lastUpdateInfo?.hasUpdate && !isLatestUpdateDownloaded && !isBackgroundProgressForLatestUpdate ? ( + + ) : null, + isLatestUpdateDownloaded ? ( + + ) : null, + isLatestUpdateDownloaded ? ( + + ) : null, + ].filter(Boolean); + + const renderAboutSettingsContent = () => ( + aboutLoading ? ( +
+ +
+ ) : ( +
+
+
+
+
{t('app.about.field.version')}
+
{aboutDisplayVersion}
+
+
+
{t('app.about.field.author')}
+
{aboutInfo?.author || t('common.unknown')}
+
+
+
{t('app.about.field.update_status')}
+
{aboutUpdateStatus || t('app.about.update_status.not_checked')}
+
+
+
{t('app.about.field.update_channel')}
+ { + void changeUpdateChannel(String(value)); + }} + loading={isUpdateChannelLoading} + disabled={ + isUpdateChannelLoading + || isUpdateChannelSaving + || updateDownloadProgress.status === 'start' + || updateDownloadProgress.status === 'downloading' + } + style={{ width: '100%' }} + /> +
+
+
+ {versionRows.map(([label, value]) => ( +
+
{label}
+
+ {value} +
+
+ ))} +
+
+
+ + {hasUpdate ? t('app.about.version_update.available') : t('app.about.version_update.up_to_date')} +
+
+ +
+
+ {t('app.about.project_links')} +
+
+ {renderSettingsCenterAboutProjectEntry({ + icon: , + title: t('app.about.project.github.title'), + description: t('app.about.project.github.description'), + url: aboutInfo?.repoUrl, + })} + {renderSettingsCenterAboutProjectEntry({ + icon: , + title: t('app.about.project.issues.title'), + description: t('app.about.project.issues.description'), + url: aboutInfo?.issueUrl, + })} + {renderSettingsCenterAboutProjectEntry({ + icon: , + title: t('app.about.project.releases.title'), + description: t('app.about.project.releases.description'), + url: lastUpdateInfo?.releaseNotesUrl || aboutInfo?.releaseUrl, + })} +
+
+
+
+ ); + }; + + const renderSettingsCenterAboutFooter = () => ( + <> +
+ + + {aboutLastCheckedAt + ? t('app.about.last_checked_at', { time: aboutLastCheckedAt }) + : t('app.about.last_checked_never')} + +
+
+ {renderAboutUpdateActions()} +
+ + ); + + const renderThemeSettingsContent = () => ( +
+
+
{t('app.theme.navigation_title')}
+
+ {[ + { key: 'theme', title: t('app.theme.nav.theme.title'), description: t('app.theme.nav.theme.description'), icon: }, + { key: 'appearance', title: t('app.theme.nav.appearance.title'), description: t('app.theme.nav.appearance.description'), icon: }, + ].map((item) => { + const active = themeModalSection === item.key; + return ( + + ); + })} +
+
+
+ {themeModalSection === 'theme' ? ( +
+
+
+ {t('app.theme.ui_version.title')} + + {t('app.theme.ui_version.badge.new')} + +
+
+ {t('app.theme.ui_version.description')} +
+
+ {[ + { key: 'legacy', label: t('app.theme.ui_version.legacy.label'), description: t('app.theme.ui_version.legacy.description'), badge: t('app.theme.ui_version.legacy.badge') }, + { key: 'v2', label: t('app.theme.ui_version.v2.label'), description: t('app.theme.ui_version.v2.description'), badge: t('app.theme.ui_version.v2.badge') }, + ].map((item) => { + const active = (appearance.uiVersion ?? 'legacy') === item.key; + return ( + + ); + })} +
+
+ {t('app.theme.ui_version.platform_hint')} +
+ {appearance.uiVersion === 'v2' && ( +
+ {t('app.theme.ui_version.beta_warning')} +
+ )} + {appearance.uiVersion === 'v2' && ( +
+
{t('app.theme.ui_version.sidebar_search.title')}
+ setAppearance({ v2SidebarSearchMode: value as 'command' | 'filter' })} + /> +
+ {t('app.theme.ui_version.sidebar_search.hint')} +
+
+ )} +
+
+
{t('app.theme.mode_title')}
+
+ {[ + { key: 'light', label: t('app.theme.mode.light.label'), description: t('app.theme.mode.light.description') }, + { key: 'dark', label: t('app.theme.mode.dark.label'), description: t('app.theme.mode.dark.description') }, + { key: 'system', label: t('app.theme.mode.system.label'), description: t('app.theme.mode.system.description') }, + ].map((item) => { + const active = themePreference === item.key; + return ( + + ); + })} +
+
+
+ ) : ( +
+
+
{t('app.theme.appearance.ui_scale_title')}
+
+ setUiScale(Number(v))} + style={{ flex: 1 }} + /> + {Math.round(effectiveUiScale * 100)}% +
+
+ {t('app.theme.appearance.ui_scale_hint')} +
+
+
+
{t('app.theme.appearance.font_size_title')}
+
+ setFontSize(Number(v))} + style={{ flex: 1 }} + /> + {effectiveFontSize}px +
+
+ {appearance.uiVersion === 'v2' && ( +
+
{t('app.theme.appearance.sidebar_rail_scale_title')}
+
+ setAppearance({ + v2SidebarRailScale: sanitizeV2SidebarRailScale(value), + })} + style={{ flex: 1 }} + /> + {Math.round(effectiveSidebarRailScale * 100)}% +
+
+ {t('app.theme.appearance.sidebar_rail_scale_hint')} +
+
+ )} +
+
{t('app.theme.font_family.title')}
+
+
+
{t('app.theme.font_family.ui_title')}
+ setAppearance({ + customMonoFontFamily: sanitizeFontFamilyInput(value), + })} + onClear={() => setAppearance({ customMonoFontFamily: null })} + options={monoFontOptions.map((option) => ({ + value: option.value, + label: option.label, + }))} + filterOption={filterFontOption} + popupMatchSelectWidth + style={{ width: '100%' }} + optionRender={(option) => renderFontOptionLabel({ + value: String(option.data.value), + label: String(option.data.label), + })} + /> +
+ {fontFamiliesLoadError + ? t('app.theme.font_family.mono_fallback_hint') + : t('app.theme.font_family.mono_hint')} +
+
+
+
+
+
{t('app.theme.query_template.title')}
+
+
+ {t('app.theme.query_template.description')} +
+ setAppearance({ newQuerySqlTemplate: event.target.value })} + style={{ fontFamily: 'var(--gn-font-mono)' }} + /> +
+
+ {t('app.theme.query_template.hint')} +
+ +
+
+
+
+
+
+
{t('app.theme.tab_display.title')}
+
+ {t('app.theme.tab_display.description')} +
+
+ setTabDisplayLayout(value as TabDisplayLayout)} + /> +
+
+ {tabDisplayElementOrder.map((key) => { + const checked = visibleTabDisplayElementKeys.has(key); + const row = tabDisplaySettings.secondaryElements.includes(key) ? 'secondary' : 'primary'; + const currentRowElements = row === 'secondary' + ? tabDisplaySettings.secondaryElements + : tabDisplaySettings.primaryElements; + const indexInRow = currentRowElements.indexOf(key); + const canMoveUp = checked && indexInRow > 0; + const canMoveDown = checked && indexInRow >= 0 && indexInRow < currentRowElements.length - 1; + const isFocused = focusedTabDisplayElementKey === key; + return ( +
setFocusedTabDisplayElementKey(key)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + setFocusedTabDisplayElementKey(key); + } + }} + style={{ + display: 'grid', + gridTemplateColumns: 'minmax(0, 1fr) auto', + gap: 10, + alignItems: 'center', + padding: '9px 10px', + borderRadius: 10, + border: `1px solid ${isFocused + ? (isV2Ui + ? v2AntPrimaryBorderHoverColor + : (darkMode ? 'rgba(255,214,102,0.54)' : 'rgba(24,144,255,0.54)')) + : (darkMode ? 'rgba(255,255,255,0.08)' : 'rgba(16,24,40,0.08)')}`, + boxShadow: isFocused + ? (isV2Ui + ? `0 0 0 2px ${v2AntControlActiveBg}` + : (darkMode ? '0 0 0 2px rgba(255,214,102,0.14)' : '0 0 0 2px rgba(24,144,255,0.12)')) + : 'none', + background: isFocused + ? (isV2Ui + ? `linear-gradient(90deg, ${v2AntPrimaryBgHoverColor} 0%, rgba(255,255,255,0.045) 100%)` + : (darkMode ? 'linear-gradient(90deg, rgba(255,214,102,0.12) 0%, rgba(255,255,255,0.045) 100%)' : 'linear-gradient(90deg, rgba(24,144,255,0.10) 0%, rgba(255,255,255,0.78) 100%)')) + : checked + ? (darkMode ? 'rgba(255,255,255,0.045)' : 'rgba(255,255,255,0.62)') + : (darkMode ? 'rgba(255,255,255,0.02)' : 'rgba(16,24,40,0.025)'), + cursor: 'pointer', + transition: 'border-color 140ms ease, box-shadow 140ms ease, background 140ms ease', + }} + > +
+ + {checked && indexInRow >= 0 ? indexInRow + 1 : '-'} + + event.stopPropagation()} + onChange={(nextChecked) => updateTabDisplayElementVisibility(key, nextChecked)} + /> +
+
+ {getTabDisplayElementLabel(key)} + {isFocused ? ( + + {t('app.theme.tab_display.badge.current')} + + ) : null} + {checked && tabDisplaySettings.layout === 'double' ? ( + + {row === 'secondary' + ? t('app.theme.tab_display.row.secondary') + : t('app.theme.tab_display.row.primary')} + + ) : null} +
+
{getTabDisplayElementDescription(key)}
+
+
+
+ {tabDisplaySettings.layout === 'double' && checked ? ( + setTabDisplayElementRow(key, value as 'primary' | 'secondary')} + onClick={(event) => event.stopPropagation()} + /> + ) : null} + + +
+
+ ); + })} +
+
+ {t('app.theme.tab_display.preview.prefix')} + {tabDisplaySettings.layout === 'double' ? `${t('app.theme.tab_display.row.primary')} ` : ''} + {tabDisplaySettings.primaryElements.map(getTabDisplayElementLabel).join(' / ') || t('app.theme.tab_display.preview.default_label')} + {tabDisplaySettings.layout === 'double' && tabDisplaySettings.secondaryElements.length > 0 + ? t('app.theme.tab_display.preview.secondary', { + labels: tabDisplaySettings.secondaryElements.map(getTabDisplayElementLabel).join(' / '), + }) + : ''} + {focusedTabDisplayElementKey + ? t('app.theme.tab_display.preview.focused', { + label: getTabDisplayElementLabel(focusedTabDisplayElementKey), + }) + : ''} +
+
+
+
{t('app.theme.appearance.transparency_blur_title')}
+
+
+
{t('app.theme.appearance.enable_transparency_blur')}
+
{t('app.theme.appearance.enable_transparency_blur_hint')}
+
+ setAppearance({ enabled: checked })} /> +
+
+
+
{t('app.theme.appearance.opacity_title')}
+
+ setAppearance({ opacity: v })} + style={{ flex: 1 }} + /> + {Math.round((appearance.opacity ?? 1.0) * 100)}% +
+
+
+
{t('app.theme.appearance.blur_title')}
+ {isWindowsPlatform() ? ( +
+ {t('app.theme.appearance.windows_acrylic_hint')} +
+ ) : ( + <> +
+ setAppearance({ blur: v })} + style={{ flex: 1 }} + /> + {appearance.blur}px +
+
+ {t('app.theme.appearance.blur_hint')} +
+ + )} +
+
+
+
+
{t('app.theme.data_table.title')}
+
+
+
+
{t('app.theme.data_table.vertical_borders')}
+
{t('app.theme.data_table.vertical_borders_hint')}
+
+ setAppearance({ showDataTableVerticalBorders: checked })} + /> +
+
+
{t('app.theme.data_table.table_double_click_action')}
+ setAppearance({ tableDoubleClickAction: value as 'open-data' | 'open-design' })} + /> +
+ {t('app.theme.data_table.table_double_click_action_hint')} +
+
+
+
{t('app.theme.data_table.density')}
+ ({ + ...option, + label: t(`app.theme.data_table.density.${option.value}`), + }))} + value={appearance.dataTableDensity} + onChange={(value) => setAppearance({ dataTableDensity: sanitizeDataTableDensity(value) })} + /> +
+ {t('app.theme.data_table.density_hint')} +
+
+
+
+
{t('app.theme.data_table.font_size')}
+ +
+
+ setAppearance({ + dataTableFontSize: sanitizeDataTableFontSize(value), + dataTableFontSizeFollowGlobal: false, + })} + style={{ flex: 1 }} + /> + {effectiveDataTableFontSize}px +
+
+
+
+
{t('app.theme.data_table.sidebar_tree_font_size')}
+ +
+
+ setAppearance({ + sidebarTreeFontSize: sanitizeSidebarTreeFontSize(value), + sidebarTreeFontSizeFollowGlobal: false, + })} + style={{ flex: 1 }} + /> + {effectiveSidebarTreeFontSize}px +
+
+
+
+ {isMacRuntime ? ( +
+
{t('app.theme.mac_window.title')}
+
+
+
{t('app.theme.mac_window.use_native_controls')}
+
{t('app.theme.mac_window.use_native_controls_hint')}
+
+ setAppearance({ useNativeMacWindowControls: checked })} + /> +
+
+ {t('app.theme.mac_window.restart_hint')} +
+
+ ) : null} +
+
{t('app.theme.startup_window.title')}
+
+ {isWindowsRuntime ? t('app.theme.startup_window.fullscreen_windows') : t('app.theme.startup_window.fullscreen')} + setStartupFullscreen(checked)} /> +
+
+ {isWindowsRuntime ? t('app.theme.startup_window.windows_hint') : t('app.theme.startup_window.hint')} +
+
+
+ +
+
+ )} +
+
+ ); + const settingsCenterGroups = [ { key: 'preferences' as const, @@ -3426,9 +4942,8 @@ function App() { title: t('app.settings.entry.theme.title'), description: t('app.settings.entry.theme.description'), onClick: () => { - setIsSettingsModalOpen(false); setThemeModalSection('theme'); - setIsThemeModalOpen(true); + handleOpenSettingsCenterPane('preferences', 'theme'); }, }, { @@ -3466,8 +4981,9 @@ function App() { title: t('app.settings.entry.ai.title'), description: t('app.settings.entry.ai.description'), onClick: () => { - setIsSettingsModalOpen(false); - handleOpenAISettings(); + setSecurityUpdateRepairSource(null); + setFocusedAIProviderId(undefined); + handleOpenSettingsCenterPane('services', 'ai'); }, }, ], @@ -3483,10 +4999,7 @@ function App() { icon: , title: t('app.settings.entry.about.title'), description: t('app.settings.entry.about.description'), - onClick: () => { - setIsSettingsModalOpen(false); - setIsAboutOpen(true); - }, + onClick: () => handleOpenSettingsCenterPane('about', 'about-go-navi'), }, ], }, @@ -3497,6 +5010,16 @@ function App() { .find((group) => group.key === activeSettingsCenterPane.group) ?.items.find((item) => item.key === activeSettingsCenterPane.key) : null; + const isSettingsCenterContainedScrollPane = + activeSettingsCenterPane?.key === 'theme' || activeSettingsCenterPane?.key === 'ai'; + const settingsCenterDetailBodyStyle: React.CSSProperties = isSettingsCenterContainedScrollPane + ? { + ...toolCenterDetailBodyStyle, + overflowY: 'hidden', + overflowX: 'hidden', + paddingRight: 0, + } + : toolCenterDetailBodyStyle; const renderSettingsCenterPane = () => { if (!activeSettingsCenterPane) { return null; @@ -3510,6 +5033,13 @@ function App() {
); } + if (activeSettingsCenterPane.key === 'theme') { + return ( +
+ {renderThemeSettingsContent()} +
+ ); + } if (activeSettingsCenterPane.key === 'sidebar-metadata') { return renderSidebarMetadataSettingsPane(); } @@ -3526,6 +5056,24 @@ function App() { /> ); } + if (activeSettingsCenterPane.key === 'ai') { + return ( +
+ +
+ ); + } + if (activeSettingsCenterPane.key === 'about-go-navi') { + return ( + renderSettingsCenterAboutPane() + ); + } return null; }; @@ -3535,10 +5083,10 @@ function App() { componentSize={appComponentSize} theme={antdTheme} > - e.stopPropagation()} style={{ display: 'flex', height: '100%', WebkitAppRegion: 'no-drag', '--wails-draggable': 'no-drag' } as any} > -
@@ -3628,11 +5176,11 @@ function App() { )} - )} - +
)} {isConnectionModalMounted && (
-
+
{renderSettingsCenterPane()}
+
+ {activeSettingsCenterPane.key === 'about-go-navi' ? ( + renderSettingsCenterAboutFooter() + ) : ( + <> + + + + )} +
) : ( <> @@ -4963,104 +6536,11 @@ function App() { open={isAboutOpen} onCancel={() => setIsAboutOpen(false)} styles={{ content: utilityModalShellStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10, display: 'flex', flexWrap: 'wrap', gap: 10, justifyContent: 'flex-end' } }} - footer={[ - isBackgroundProgressForLatestUpdate && !isLatestUpdateDownloaded ? ( - - ) : null, - lastUpdateInfo?.hasUpdate && !isLatestUpdateDownloaded && !isBackgroundProgressForLatestUpdate ? ( - - ) : null, - , + footer={renderAboutUpdateActions( , - lastUpdateInfo?.hasUpdate && !isLatestUpdateDownloaded && !isBackgroundProgressForLatestUpdate ? ( - - ) : null, - isLatestUpdateDownloaded ? ( - - ) : null, - isLatestUpdateDownloaded ? ( - - ) : null, - ].filter(Boolean)} - > - {aboutLoading ? ( -
- -
- ) : ( -
-
-
-
-
{t('app.about.field.version')}
-
{aboutDisplayVersion}
-
-
-
{t('app.about.field.author')}
-
{aboutInfo?.author || t('common.unknown')}
-
-
-
{t('app.about.field.update_status')}
-
{aboutUpdateStatus || t('app.about.update_status.not_checked')}
-
-
-
{t('app.about.field.update_channel')}
- setAppearance({ - customUIFontFamily: sanitizeFontFamilyInput(value), - })} - onClear={() => setAppearance({ customUIFontFamily: null })} - options={uiFontOptions.map((option) => ({ - value: option.value, - label: option.label, - }))} - filterOption={filterFontOption} - popupMatchSelectWidth - style={{ width: '100%' }} - optionRender={(option) => renderFontOptionLabel({ - value: String(option.data.value), - label: String(option.data.label), - })} - /> -
- {fontFamiliesLoadError - ? t('app.theme.font_family.load_failed_fallback', { error: fontFamiliesLoadError }) - : (installedFontFamilies.length > 0 - ? t('app.theme.font_family.loaded_ui_hint', { count: installedFontFamilies.length }) - : t('app.theme.font_family.loading_ui_hint'))} -
- {linuxCJKFontInstallHint && hasLoadedInstalledFontsRef.current && !isFontFamiliesLoading && !fontFamiliesLoadError && ( -
- {t('app.theme.font_family.linux_cjk_install_prefix')} - {linuxCJKFontInstallHint} - {t('app.theme.font_family.linux_cjk_install_suffix')} -
- )} -
-
-
{t('app.theme.font_family.mono_title')}
-