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 } from '@ant-design/icons'; import { BrowserOpenURL, Environment, Quit, WindowFullscreen, WindowGetPosition, WindowGetSize, WindowIsFullscreen, WindowIsMaximised, WindowIsMinimised, WindowIsNormal, WindowMaximise, WindowMinimise, WindowSetPosition, WindowSetSize, WindowUnfullscreen, WindowUnmaximise } from '../wailsjs/runtime'; import Sidebar from './components/Sidebar'; import TabManager from './components/TabManager'; import ConnectionModal from './components/ConnectionModal'; import SnippetSettingsModal from './components/SnippetSettingsModal'; import ConnectionPackagePasswordModal from './components/ConnectionPackagePasswordModal'; import DataSyncModal from './components/DataSyncModal'; 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 AIChatPanel from './components/AIChatPanel'; import AIPanelErrorBoundary from './components/ai/AIPanelErrorBoundary'; import SecurityUpdateBanner from './components/SecurityUpdateBanner'; import SecurityUpdateIntroModal from './components/SecurityUpdateIntroModal'; import SecurityUpdateProgressModal from './components/SecurityUpdateProgressModal'; import SecurityUpdateSettingsModal from './components/SecurityUpdateSettingsModal'; import LanguageSettingsPanel from './components/LanguageSettingsPanel'; import { DEFAULT_APPEARANCE, useStore } from './store'; import { 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 { DENSITY_OPTIONS, sanitizeDataTableDensity, sanitizeDataTableFontSize, sanitizeSidebarTreeFontSize, } from './utils/dataGridDisplay'; import { TAB_DISPLAY_SECONDARY_DEFAULT_KEYS, TAB_DISPLAY_ELEMENT_META, applyTabDisplaySettingsPatch, resolveTabDisplayElementOrder, sanitizeTabDisplaySettings, switchTabDisplayLayout, type TabDisplayElementKey, type TabDisplayLayout, type TabDisplaySettings, } from './utils/tabDisplay'; import { getMacNativeTitlebarPaddingLeft, getMacNativeTitlebarPaddingRight, shouldHandleMacNativeFullscreenShortcut, shouldSuppressMacNativeEscapeExit } from './utils/macWindow'; import { shouldEnableMacWindowDiagnostics } from './utils/macWindowDiagnostics'; import { getConnectionWorkbenchState } from './utils/startupReadiness'; import { toSaveGlobalProxyInput } from './utils/globalProxyDraft'; import { detectConnectionImportKind, isConnectionPackagePasswordRequiredError, resolveConnectionPackageExportResult, normalizeConnectionPackagePassword, } from './utils/connectionExport'; import { bootstrapSecureConfig, finalizeSecurityUpdateStatus, mergeSecurityUpdateStatusWithLegacySource, prepareSecureConfigForExternalMCP, startSecurityUpdateFromBootstrap, } from './utils/secureConfigBootstrap'; import { bootstrapSavedQueries } from './utils/savedQueryPersistence'; import { LEGACY_PERSIST_KEY, hasLegacyMigratableSensitiveItems, stripLegacyPersistedConnectionById, } from './utils/legacyConnectionStorage'; import { getSecurityUpdateStatusMeta, resolveSecurityUpdateEntryVisibility, } from './utils/securityUpdatePresentation'; import { hasSecurityUpdateRecentResult, resolveSecurityUpdateRepairEntry, resolveSecurityUpdateSettingsFocusTarget, shouldRefreshSecurityUpdateDetailsFocus, shouldReopenSecurityUpdateDetails, shouldRetrySecurityUpdateAfterRepairSave, type SecurityUpdateRepairSource, type SecurityUpdateSettingsFocusTarget, } from './utils/securityUpdateRepairFlow'; import { getWindowsScaleFixNudgedWidth, hasWindowsViewportScaleDrift } from './utils/windowsScaleFix'; import { SHORTCUT_ACTION_META, SHORTCUT_ACTION_ORDER, ShortcutAction, canRecordShortcutForAction, eventToShortcut, findReservedConflicts, getShortcutDisplay, getShortcutDisplayLabel, getShortcutPlatform, installGlobalImeCompositionTracking, isEditableElement, isShortcutMatch, normalizeShortcutCombo, resolveShortcutBinding, splitConflictsByContext, type ConflictInfo, } from './utils/shortcuts'; import { resolveTitleBarToggleIconKey, resolveWindowsScaleCheckDelayMs, shouldApplyWindowsScaleFix, shouldResetWebViewZoomForScaleFix, shouldToggleMaximisedWindowForScaleFix, type WindowScaleFixReason, type WindowsScaleCheckTrigger } from './utils/windowStateUi'; import { resolveVisibleStartupWindowBounds } from './utils/windowRestoreBounds'; import { SIDEBAR_UTILITY_ITEM_KEYS, resolveAIEntryPlacement, resolveLegacyAIEdgeHandleAttachment, resolveLegacyAIEdgeHandleDockStyle, resolveLegacyAIEdgeHandleStyle, } from './utils/aiEntryLayout'; import { DEFAULT_AI_PANEL_WIDTH, resolveOverlayAIPanelWidth, shouldOverlayAIPanel } from './utils/aiPanelLayout'; import { safeWindowRuntimeCall } from './utils/wailsRuntime'; import { useAppUpdateManager } from './hooks/useAppUpdateManager'; import { useAppLogPanelResize } from './hooks/useAppLogPanelResize'; import { useAppSidebarResize } from './hooks/useAppSidebarResize'; import { useAppUtilityStyles } from './hooks/useAppUtilityStyles'; import { ApplyDataRootDirectory, GetDataRootDirectoryInfo, GetSavedConnections, ListInstalledFontFamilies, OpenDataRootDirectory, SelectDataRootDirectory, SetMacNativeWindowControls, SetWindowTranslucency } from '../wailsjs/go/app/App'; import { getAntdLocale } from './i18n/frameworkLocale'; import { useI18n } from './i18n/provider'; import './App.css'; import './v2-theme.css'; import './styles/v2-theme-workbench.css'; const { Sider, Content } = Layout; const MIN_UI_SCALE = 0.8; const MAX_UI_SCALE = 1.25; const MIN_FONT_SIZE = 12; const MAX_FONT_SIZE = 20; const DEFAULT_UI_SCALE = 1.0; const DEFAULT_FONT_SIZE = 14; const EMPTY_INSTALLED_FONT_FAMILIES: InstalledFontFamily[] = []; const createEmptySecurityUpdateStatus = (): SecurityUpdateStatus => ({ overallStatus: 'not_detected', summary: { total: 0, updated: 0, pending: 0, skipped: 0, failed: 0, }, issues: [], }); const detectNavigatorPlatform = (): string => { if (typeof navigator === 'undefined') { return ''; } const uaDataPlatform = (navigator as Navigator & { userAgentData?: { platform?: string }; }).userAgentData?.platform; if (uaDataPlatform) { return uaDataPlatform; } return navigator.userAgent || ''; }; const readCurrentVisibleViewport = () => ({ availWidth: window.screen?.availWidth || window.innerWidth || 0, availHeight: window.screen?.availHeight || window.innerHeight || 0, availLeft: (window.screen as Screen & { availLeft?: number })?.availLeft || 0, availTop: (window.screen as Screen & { availTop?: number })?.availTop || 0, }); const mergeSavedConnections = (current: SavedConnection[], imported: SavedConnection[]): SavedConnection[] => { const merged = new Map(); current.forEach((conn) => merged.set(conn.id, conn)); imported.forEach((conn) => merged.set(conn.id, conn)); return Array.from(merged.values()); }; type ConnectionPackageDialogMode = 'import' | 'export'; type ToolCenterGroupKey = 'config' | 'workflow' | 'workspace'; type ToolCenterPaneKey = | 'connection-package' | 'data-root' | 'security-update' | 'schema-compare' | 'data-compare' | 'sync' | 'drivers' | 'snippet-settings' | 'shortcut-settings'; type ToolCenterPaneState = { key: ToolCenterPaneKey; group: ToolCenterGroupKey; }; type ConnectionPackageDialogState = { open: boolean; mode: ConnectionPackageDialogMode; includeSecrets: boolean; useFilePassword: boolean; password: string; error: string; confirmLoading: boolean; }; const createClosedConnectionPackageDialogState = (): ConnectionPackageDialogState => ({ open: false, mode: 'export', includeSecrets: true, useFilePassword: false, password: '', error: '', confirmLoading: false, }); function App() { const { language, t } = useI18n(); const [isModalOpen, setIsModalOpen] = useState(false); const [isConnectionModalMounted, setIsConnectionModalMounted] = useState(false); const [isSyncModalOpen, setIsSyncModalOpen] = useState(false); const [syncModalEntryMode, setSyncModalEntryMode] = useState('sync'); const [isDriverModalOpen, setIsDriverModalOpen] = useState(false); const [editingConnection, setEditingConnection] = useState(null); const connectionModalWarmupDoneRef = useRef(false); const windowState = useStore(state => state.windowState); const themeMode = useStore(state => state.theme); const setTheme = useStore(state => state.setTheme); const appearance = useStore(state => state.appearance); const setAppearance = useStore(state => state.setAppearance); const uiScale = useStore(state => state.uiScale); const setUiScale = useStore(state => state.setUiScale); const fontSize = useStore(state => state.fontSize); const setFontSize = useStore(state => state.setFontSize); 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); const shortcutOptions = useStore(state => state.shortcutOptions); const updateShortcut = useStore(state => state.updateShortcut); const resetShortcutOptions = useStore(state => state.resetShortcutOptions); const darkMode = themeMode === 'dark'; const isV2Ui = appearance.uiVersion === 'v2'; const effectiveUiScale = Math.min(MAX_UI_SCALE, Math.max(MIN_UI_SCALE, Number(uiScale) || DEFAULT_UI_SCALE)); const effectiveFontSize = Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, Math.round(Number(fontSize) || DEFAULT_FONT_SIZE))); const tokenFontSize = Math.round(effectiveFontSize * effectiveUiScale); const titleBarToggleIconKey = resolveTitleBarToggleIconKey( windowState === 'fullscreen' ? 'fullscreen' : (windowState === 'maximized' ? 'maximized' : 'normal') ); const tokenFontSizeSM = Math.max(10, Math.round(tokenFontSize * 0.86)); const tokenFontSizeLG = Math.max(tokenFontSize + 1, Math.round(tokenFontSize * 1.14)); const tokenControlHeight = Math.max(24, Math.round(32 * effectiveUiScale)); const tokenControlHeightSM = Math.max(20, Math.round(24 * effectiveUiScale)); const tokenControlHeightLG = Math.max(30, Math.round(40 * effectiveUiScale)); const dataTableFontSizeFollowsGlobal = appearance.dataTableFontSizeFollowGlobal !== false; const sidebarTreeFontSizeFollowsGlobal = appearance.sidebarTreeFontSizeFollowGlobal !== false; const effectiveDataTableFontSize = dataTableFontSizeFollowsGlobal ? effectiveFontSize : (sanitizeDataTableFontSize(appearance.dataTableFontSize) ?? effectiveFontSize); const effectiveSidebarTreeFontSize = sidebarTreeFontSizeFollowsGlobal ? effectiveFontSize : (sanitizeSidebarTreeFontSize(appearance.sidebarTreeFontSize) ?? effectiveFontSize); const tableDoubleClickAction = appearance.tableDoubleClickAction === 'open-design' ? 'open-design' : 'open-data'; const tabDisplaySettings = useMemo( () => sanitizeTabDisplaySettings(appearance.tabDisplay), [appearance.tabDisplay], ); const tabDisplayElementOrder = useMemo( () => resolveTabDisplayElementOrder(tabDisplaySettings), [tabDisplaySettings], ); const visibleTabDisplayElementKeys = useMemo( () => new Set([ ...tabDisplaySettings.primaryElements, ...tabDisplaySettings.secondaryElements, ]), [tabDisplaySettings], ); const getTabDisplayElementLabel = useCallback( (key: TabDisplayElementKey) => t(TAB_DISPLAY_ELEMENT_META[key].labelKey), [t], ); const getTabDisplayElementDescription = useCallback( (key: TabDisplayElementKey) => t(TAB_DISPLAY_ELEMENT_META[key].descriptionKey), [t], ); const setTabDisplaySettings = useCallback((settings: Partial) => { setAppearance({ tabDisplay: applyTabDisplaySettingsPatch(tabDisplaySettings, settings), }); }, [setAppearance, tabDisplaySettings]); const setTabDisplayLayout = useCallback((layout: TabDisplayLayout) => { if (layout === tabDisplaySettings.layout) return; setAppearance({ tabDisplay: switchTabDisplayLayout(tabDisplaySettings, layout), }); }, [setAppearance, tabDisplaySettings]); const updateTabDisplayElementVisibility = useCallback((key: TabDisplayElementKey, checked: boolean) => { setFocusedTabDisplayElementKey(key); const removeKey = (keys: TabDisplayElementKey[]) => keys.filter((item) => item !== key); if (!checked) { setTabDisplaySettings({ layout: tabDisplaySettings.layout, primaryElements: removeKey(tabDisplaySettings.primaryElements), secondaryElements: removeKey(tabDisplaySettings.secondaryElements), }); return; } const primaryElements = removeKey(tabDisplaySettings.primaryElements); const secondaryElements = removeKey(tabDisplaySettings.secondaryElements); if (tabDisplaySettings.layout === 'double' && TAB_DISPLAY_SECONDARY_DEFAULT_KEYS.includes(key)) { secondaryElements.push(key); } else { primaryElements.push(key); } setTabDisplaySettings({ layout: tabDisplaySettings.layout, primaryElements, secondaryElements, }); }, [setTabDisplaySettings, tabDisplaySettings]); const moveTabDisplayElement = useCallback((key: TabDisplayElementKey, offset: -1 | 1) => { setFocusedTabDisplayElementKey(key); const moveWithin = (keys: TabDisplayElementKey[]) => { const index = keys.indexOf(key); if (index < 0) return keys; const nextIndex = index + offset; if (nextIndex < 0 || nextIndex >= keys.length) return keys; const next = [...keys]; [next[index], next[nextIndex]] = [next[nextIndex], next[index]]; return next; }; setTabDisplaySettings({ layout: tabDisplaySettings.layout, primaryElements: moveWithin(tabDisplaySettings.primaryElements), secondaryElements: moveWithin(tabDisplaySettings.secondaryElements), }); }, [setTabDisplaySettings, tabDisplaySettings]); const setTabDisplayElementRow = useCallback((key: TabDisplayElementKey, row: 'primary' | 'secondary') => { setFocusedTabDisplayElementKey(key); const primaryElements = tabDisplaySettings.primaryElements.filter((item) => item !== key); const secondaryElements = tabDisplaySettings.secondaryElements.filter((item) => item !== key); if (row === 'primary') { primaryElements.push(key); } else { secondaryElements.push(key); } setTabDisplaySettings({ layout: tabDisplaySettings.layout, primaryElements, secondaryElements, }); }, [setTabDisplaySettings, tabDisplaySettings]); const resolvedUiFontFamily = resolveUIFontFamily(appearance.customUIFontFamily); const resolvedMonoFontFamily = resolveMonoFontFamily(appearance.customMonoFontFamily); const appComponentSize: 'small' | 'middle' | 'large' = effectiveUiScale <= 0.92 ? 'small' : (effectiveUiScale >= 1.12 ? 'large' : 'middle'); const titleBarHeight = Math.max(28, Math.round(32 * effectiveUiScale)); const titleBarButtonWidth = Math.max(40, Math.round(46 * effectiveUiScale)); const floatingLogButtonHeight = Math.max(30, Math.round(34 * effectiveUiScale)); const resolvedAppearance = resolveAppearanceValues(appearance); const effectiveOpacity = normalizeOpacityForPlatform(resolvedAppearance.opacity); const effectiveBlur = normalizeBlurForPlatform(resolvedAppearance.blur); const blurFilter = blurToFilter(effectiveBlur); const [runtimePlatform, setRuntimePlatform] = useState(''); const [runtimeBuildType, setRuntimeBuildType] = useState(''); const [isLinuxRuntime, setIsLinuxRuntime] = useState(false); const [installedFontFamilies, setInstalledFontFamilies] = useState(EMPTY_INSTALLED_FONT_FAMILIES); const [isFontFamiliesLoading, setIsFontFamiliesLoading] = useState(false); const [fontFamiliesLoadError, setFontFamiliesLoadError] = useState(null); const hasLoadedInstalledFontsRef = useRef(false); const uiFontOptions = useMemo( () => buildFontFamilyOptions(runtimePlatform, 'ui', installedFontFamilies, t), [installedFontFamilies, runtimePlatform, t], ); const monoFontOptions = useMemo( () => buildFontFamilyOptions(runtimePlatform, 'mono', installedFontFamilies, t), [installedFontFamilies, runtimePlatform, t], ); const linuxCJKFontInstallHint = getLinuxCJKFontInstallHint(runtimePlatform, installedFontFamilies); const [isStoreHydrated, setIsStoreHydrated] = useState(() => useStore.persist.hasHydrated()); const [hasLoadedSecureConfig, setHasLoadedSecureConfig] = useState(false); const [viewportWidth, setViewportWidth] = useState(() => (typeof window === 'undefined' ? 1280 : window.innerWidth || 1280)); const [securityUpdateStatus, setSecurityUpdateStatus] = useState(() => createEmptySecurityUpdateStatus()); const [securityUpdateRawPayload, setSecurityUpdateRawPayload] = useState(null); const [securityUpdateHasLegacySensitiveItems, setSecurityUpdateHasLegacySensitiveItems] = useState(false); const [isSecurityUpdateIntroOpen, setIsSecurityUpdateIntroOpen] = useState(false); const [isSecurityUpdateBannerDismissed, setIsSecurityUpdateBannerDismissed] = useState(false); const [isSecurityUpdateSettingsOpen, setIsSecurityUpdateSettingsOpen] = useState(false); const [securityUpdateSettingsFocusTarget, setSecurityUpdateSettingsFocusTarget] = useState(null); const [securityUpdateSettingsFocusRequest, setSecurityUpdateSettingsFocusRequest] = useState(0); const [isSecurityUpdateProgressOpen, setIsSecurityUpdateProgressOpen] = useState(false); const [securityUpdateProgressStage, setSecurityUpdateProgressStage] = useState(() => t('app.security_update.stage.checking_saved_config')); const [securityUpdateRepairSource, setSecurityUpdateRepairSource] = useState(null); const [focusedTabDisplayElementKey, setFocusedTabDisplayElementKey] = useState(null); const [focusedAIProviderId, setFocusedAIProviderId] = useState(undefined); const [connectionPackageDialog, setConnectionPackageDialog] = useState(() => createClosedConnectionPackageDialogState()); const [pendingConnectionImportPayload, setPendingConnectionImportPayload] = useState(null); const [aiPanelRenderNonce, setAiPanelRenderNonce] = useState(0); const sidebarWidth = useStore(state => state.sidebarWidth); const setSidebarWidth = useStore(state => state.setSidebarWidth); 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); const connectionWorkbenchState = getConnectionWorkbenchState(isStoreHydrated, hasLoadedSecureConfig); const securityUpdateStatusMeta = useMemo( () => getSecurityUpdateStatusMeta(securityUpdateStatus, t), [securityUpdateStatus, t], ); const securityUpdateEntryVisibility = useMemo( () => resolveSecurityUpdateEntryVisibility(securityUpdateStatus), [securityUpdateStatus], ); const windowCornerRadius = 14; useEffect(() => { if (typeof window === 'undefined') { return; } const syncViewportWidth = () => { setViewportWidth(window.innerWidth || document.documentElement?.clientWidth || 1280); }; syncViewportWidth(); window.addEventListener('resize', syncViewportWidth); return () => window.removeEventListener('resize', syncViewportWidth); }, []); useEffect(()=>{ if (typeof document === 'undefined' || !document.body) { return; } switch(windowState){ case 'fullscreen': case 'maximized': document.body.style.setProperty('--gonavi-border-radius', '0px'); break; default: document.body.style.setProperty('--gonavi-border-radius', `${windowCornerRadius}px`); break; } }, [windowState]); // 同步 macOS 窗口透明度:opacity=1.0 且 blur=0 时关闭 NSVisualEffectView, // 避免 GPU 持续计算窗口背后的模糊合成 useEffect(() => { try { void SetWindowTranslucency(resolvedAppearance.opacity, resolvedAppearance.blur).catch(() => undefined); } catch(e) { /* ignore */ } }, [resolvedAppearance.blur, resolvedAppearance.opacity]); useEffect(() => { let cancelled = false; try { Environment() .then((env) => { if (cancelled) return; const platform = String(env?.platform || '').toLowerCase(); setRuntimePlatform(platform); setRuntimeBuildType(String(env?.buildType || '').toLowerCase()); setIsLinuxRuntime(platform === 'linux'); }) .catch(() => { if (cancelled) return; const platform = detectNavigatorPlatform(); const normalized = /linux/i.test(platform) ? 'linux' : (/mac/i.test(platform) ? 'darwin' : (/win/i.test(platform) ? 'windows' : '')); setRuntimePlatform(normalized); setIsLinuxRuntime(normalized === 'linux'); }); } catch(e) { if (cancelled) return; const platform = detectNavigatorPlatform(); const normalized = /linux/i.test(platform) ? 'linux' : (/mac/i.test(platform) ? 'darwin' : (/win/i.test(platform) ? 'windows' : '')); setRuntimePlatform(normalized); setIsLinuxRuntime(normalized === 'linux'); } return () => { cancelled = true; }; }, []); useEffect(() => { if (isStoreHydrated) { return; } const unsubscribe = useStore.persist.onFinishHydration(() => { setIsStoreHydrated(true); }); return () => { unsubscribe(); }; }, [isStoreHydrated]); useEffect(() => { if (!isStoreHydrated) { return; } let cancelled = false; const loadSavedQueries = async () => { try { await bootstrapSavedQueries({ backend: (window as any).go?.app?.App, replaceSavedQueries: (queries) => { if (!cancelled) { replaceSavedQueries(queries); } }, }); } catch (err) { console.warn('Failed to bootstrap saved queries', err); } }; void loadSavedQueries(); return () => { cancelled = true; }; }, [isStoreHydrated, replaceSavedQueries]); const normalizeSecurityUpdateStatus = useCallback((status?: Partial | null): SecurityUpdateStatus => { const fallback = createEmptySecurityUpdateStatus(); return { ...fallback, ...(status ?? {}), summary: { ...fallback.summary, ...(status?.summary ?? {}), }, issues: Array.isArray(status?.issues) ? status.issues : [], }; }, []); const applySecurityUpdateStatus = useCallback(( status?: Partial | null, options?: { openSettings?: boolean; refreshFocus?: boolean; resetBannerDismissed?: boolean; }, ) => { const nextStatus = normalizeSecurityUpdateStatus(status); const visibility = resolveSecurityUpdateEntryVisibility(nextStatus); setSecurityUpdateStatus(nextStatus); setIsSecurityUpdateIntroOpen(visibility.showIntro); if (options?.resetBannerDismissed !== false) { setIsSecurityUpdateBannerDismissed(false); } if (options?.openSettings) { if (options.refreshFocus !== false) { setSecurityUpdateSettingsFocusTarget(resolveSecurityUpdateSettingsFocusTarget(nextStatus)); setSecurityUpdateSettingsFocusRequest((current) => current + 1); } setIsSecurityUpdateSettingsOpen(true); } return nextStatus; }, [normalizeSecurityUpdateStatus]); useEffect(() => { if (!isStoreHydrated) { return; } let cancelled = false; const loadSecureConfig = async () => { try { const result = await bootstrapSecureConfig({ backend: (window as any).go?.app?.App, autoStartLegacySecurityUpdate: true, replaceConnections, replaceGlobalProxy, t, }); if (cancelled) { return; } setSecurityUpdateRawPayload(result.rawPayload); setSecurityUpdateHasLegacySensitiveItems(result.hasLegacySensitiveItems); applySecurityUpdateStatus(result.status); } catch (err) { console.warn('Failed to bootstrap secure config', err); } finally { if (!cancelled) { setHasLoadedSecureConfig(true); } } }; void loadSecureConfig(); return () => { cancelled = true; }; }, [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; const maxApplyAttempts = 6; const applyRetryDelayMs = 400; const settleDelayMs = 160; const useMaximiseForStartup = isWindowsPlatform(); const checkStartupPreferenceApplied = async (): Promise => { try { if (await WindowIsFullscreen()) { return true; } } catch (_) { // ignore } try { if (await WindowIsMaximised()) { return true; } } catch (_) { // ignore } return false; }; const applyStartupWindowPreference = (attempt: number) => { if (startupWindowTimer !== null) { window.clearTimeout(startupWindowTimer); } startupWindowTimer = window.setTimeout(() => { if (cancelled) { return; } if (!useStore.getState().startupFullscreen) { return; } void Promise.resolve() .then(async () => { if (await checkStartupPreferenceApplied()) { return; } // Windows 使用最大化,避免进入真正全屏后无法通过标题栏交互退出。 // 其他平台保持全屏优先、最大化兜底。 try { if (useMaximiseForStartup) { await WindowMaximise(); await new Promise((resolve) => window.setTimeout(resolve, settleDelayMs)); } else { await WindowFullscreen(); await new Promise((resolve) => window.setTimeout(resolve, settleDelayMs)); if (await checkStartupPreferenceApplied()) { return; } await WindowMaximise(); await new Promise((resolve) => window.setTimeout(resolve, settleDelayMs)); } } catch (e) { console.warn("Wails Window APIs unavailable", e); } if (await checkStartupPreferenceApplied()) { return; } if (attempt < maxApplyAttempts) { applyStartupWindowPreference(attempt + 1); } }); }, applyRetryDelayMs); }; const restoreWindowState = async () => { if (cancelled) return; const state = useStore.getState(); // startupFullscreen 设置优先 if (state.startupFullscreen) { applyStartupWindowPreference(1); return; } // 根据上次保存的窗口状态恢复 const savedState = state.windowState; if (savedState === 'fullscreen') { applyStartupWindowPreference(1); return; } if (savedState === 'maximized') { try { await WindowMaximise(); } catch (_) {} return; } // 普通窗口:恢复尺寸和位置 const bounds = state.windowBounds; if (!bounds || bounds.width < 400 || bounds.height < 300) return; try { const nextBounds = resolveVisibleStartupWindowBounds(bounds, readCurrentVisibleViewport()); if ( nextBounds.x !== bounds.x || nextBounds.y !== bounds.y || nextBounds.width !== bounds.width || nextBounds.height !== bounds.height ) { void emitWindowDiagnostic('adjust:startup-window-bounds', { from: bounds, to: nextBounds, }); state.setWindowBounds(nextBounds); } WindowSetSize(nextBounds.width, nextBounds.height); WindowSetPosition(nextBounds.x, nextBounds.y); } catch (e) { console.warn('Failed to restore window bounds', e); } }; if (useStore.persist.hasHydrated()) { void restoreWindowState(); } const unsubscribeHydration = useStore.persist.onFinishHydration(() => { if (cancelled) { return; } void restoreWindowState(); }); return () => { cancelled = true; if (startupWindowTimer !== null) { window.clearTimeout(startupWindowTimer); } unsubscribeHydration(); }; }, []); // 定时保存窗口状态、尺寸与位置 useEffect(() => { const SAVE_INTERVAL_MS = 2000; let cancelled = false; let hydrated = useStore.persist.hasHydrated(); let eventSaveTimer: number | null = null; let boundsRepairTimer: number | null = null; let lastSaved = ''; const saveWindowState = async () => { if (cancelled || !hydrated) { return; } try { const [isFs, isMax] = await Promise.all([ safeWindowRuntimeCall(() => WindowIsFullscreen(), false), safeWindowRuntimeCall(() => WindowIsMaximised(), false), ]); // 保存窗口状态 const store = useStore.getState(); const newState = isFs ? 'fullscreen' : (isMax ? 'maximized' : 'normal'); if (store.windowState !== newState) { void emitWindowDiagnostic('transition:windowState', { from: store.windowState, to: newState, }); store.setWindowState(newState); } // 只在普通窗口模式下保存尺寸和位置 if (isFs || isMax) return; const [size, pos] = await Promise.all([ safeWindowRuntimeCall(() => WindowGetSize(), null), safeWindowRuntimeCall(() => WindowGetPosition(), null), ]); if (!size || !pos) return; const w = Math.trunc(Number(size.w || 0)); const h = Math.trunc(Number(size.h || 0)); const x = Math.trunc(Number(pos.x || 0)); const y = Math.trunc(Number(pos.y || 0)); if (w < 400 || h < 300) return; const key = `${w},${h},${x},${y}`; if (key === lastSaved) return; lastSaved = key; if (Math.abs(x) > 5000 || Math.abs(y) > 5000) { void emitWindowDiagnostic('anomaly:windowBounds', { width: w, height: h, x, y }); } store.setWindowBounds({ width: w, height: h, x, y }); } catch (e) { // 静默忽略 } }; const scheduleWindowStateSave = (delayMs = 120) => { if (cancelled || !hydrated) { return; } if (eventSaveTimer !== null) { window.clearTimeout(eventSaveTimer); } eventSaveTimer = window.setTimeout(() => { eventSaveTimer = null; void saveWindowState(); }, delayMs); }; const repairRuntimeWindowBounds = async () => { if (cancelled || !hydrated) { return; } try { const [isFs, isMax] = await Promise.all([ safeWindowRuntimeCall(() => WindowIsFullscreen(), false), safeWindowRuntimeCall(() => WindowIsMaximised(), false), ]); if (isFs || isMax) { return; } const [size, pos] = await Promise.all([ safeWindowRuntimeCall(() => WindowGetSize(), null), safeWindowRuntimeCall(() => WindowGetPosition(), null), ]); if (!size || !pos) { return; } const currentBounds = { width: Math.trunc(Number(size.w || 0)), height: Math.trunc(Number(size.h || 0)), x: Math.trunc(Number(pos.x || 0)), y: Math.trunc(Number(pos.y || 0)), }; if (currentBounds.width <= 0 || currentBounds.height <= 0) { return; } const nextBounds = resolveVisibleStartupWindowBounds(currentBounds, readCurrentVisibleViewport()); if ( nextBounds.x === currentBounds.x && nextBounds.y === currentBounds.y && nextBounds.width === currentBounds.width && nextBounds.height === currentBounds.height ) { return; } void emitWindowDiagnostic('adjust:runtime-window-bounds', { from: currentBounds, to: nextBounds, }); WindowSetSize(nextBounds.width, nextBounds.height); WindowSetPosition(nextBounds.x, nextBounds.y); lastSaved = `${nextBounds.width},${nextBounds.height},${nextBounds.x},${nextBounds.y}`; useStore.getState().setWindowBounds(nextBounds); window.dispatchEvent(new Event('resize')); } catch { // Wails runtime window APIs are best-effort here. } }; const scheduleWindowBoundsRepair = (delayMs = 80) => { if (cancelled || !hydrated) { return; } if (boundsRepairTimer !== null) { window.clearTimeout(boundsRepairTimer); } boundsRepairTimer = window.setTimeout(() => { boundsRepairTimer = null; void repairRuntimeWindowBounds(); }, delayMs); }; const handleWindowRuntimeChange = () => { scheduleWindowBoundsRepair(); scheduleWindowStateSave(260); }; const handleVisibilityChange = () => { if (document.visibilityState === 'visible') { scheduleWindowBoundsRepair(); scheduleWindowStateSave(260); } }; const handleWindowLifecycleFlush = () => { void saveWindowState(); }; if (hydrated) { scheduleWindowBoundsRepair(360); scheduleWindowStateSave(320); } const unsubscribeHydration = useStore.persist.onFinishHydration(() => { if (cancelled || hydrated) { return; } hydrated = true; scheduleWindowBoundsRepair(360); scheduleWindowStateSave(320); }); const timer = window.setInterval(() => { void saveWindowState(); }, SAVE_INTERVAL_MS); window.addEventListener('resize', handleWindowRuntimeChange); window.addEventListener('focus', handleWindowRuntimeChange); window.addEventListener('pageshow', handleWindowRuntimeChange); window.addEventListener('pagehide', handleWindowLifecycleFlush, { capture: true }); window.addEventListener('beforeunload', handleWindowLifecycleFlush, { capture: true }); document.addEventListener('visibilitychange', handleVisibilityChange); return () => { cancelled = true; if (eventSaveTimer !== null) { window.clearTimeout(eventSaveTimer); } if (boundsRepairTimer !== null) { window.clearTimeout(boundsRepairTimer); } window.clearInterval(timer); window.removeEventListener('resize', handleWindowRuntimeChange); window.removeEventListener('focus', handleWindowRuntimeChange); window.removeEventListener('pageshow', handleWindowRuntimeChange); window.removeEventListener('pagehide', handleWindowLifecycleFlush, { capture: true }); window.removeEventListener('beforeunload', handleWindowLifecycleFlush, { capture: true }); document.removeEventListener('visibilitychange', handleVisibilityChange); unsubscribeHydration(); }; }, []); useEffect(() => { if (!isWindowsPlatform()) { return; } let cancelled = false; let inFlight = false; let lastRatio = Number(window.devicePixelRatio) || 1; let lastFixAt = 0; let activationTimer: number | null = null; let resizeTimer: number | null = null; let minimisedSeen = false; let hiddenSeen = document.visibilityState === 'hidden'; const wait = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms)); const fixWindowScaleIfNeeded = async (reason: WindowScaleFixReason) => { if (cancelled || inFlight) return; const now = Date.now(); if (now - lastFixAt < 700) return; inFlight = true; try { const [isFullscreen, isMaximised] = await Promise.all([ safeWindowRuntimeCall(() => WindowIsFullscreen(), false), safeWindowRuntimeCall(() => WindowIsMaximised(), false), ]); // 全屏状态下只广播 resize,避免破坏用户的全屏上下文。 if (isFullscreen) { window.dispatchEvent(new Event('resize')); lastFixAt = Date.now(); return; } const size = await safeWindowRuntimeCall(() => WindowGetSize(), null); const width = Math.trunc(Number(size?.w || 0)); const height = Math.trunc(Number(size?.h || 0)); const hasViewportScaleDrift = hasWindowsViewportScaleDrift({ windowWidth: width, innerWidth: window.innerWidth, devicePixelRatio: Number(window.devicePixelRatio) || 1, visualViewportScale: window.visualViewport?.scale, }); const shouldResetWebViewZoom = shouldResetWebViewZoomForScaleFix(reason, hasViewportScaleDrift); if (shouldResetWebViewZoom && !isMaximised) { try { const res = await (window as any).go?.app?.App?.ResetWebViewZoom?.(); if (!res?.success) { console.warn('ResetWebViewZoom unavailable in fixWindowScaleIfNeeded:', res?.message); } } catch (e) { console.warn('ResetWebViewZoom call failed in fixWindowScaleIfNeeded', e); } } if (isMaximised) { if (!shouldToggleMaximisedWindowForScaleFix(reason, hasViewportScaleDrift)) { // restore(任务栏点击恢复后字体异常变大/变糊)的零感知修复路径: // 调 backend App.ResetWebViewZoom 触发 WebView2 ICoreWebView2Controller::put_ZoomFactor(1.0), // 让 WebView2 重算 D2D/DirectWrite 字体度量。该异常不一定表现为 viewport ratio drift, // 所以 restore 场景不能依赖 hasViewportScaleDrift。完全不动窗口、零动画。 // backend 失败(wails 升级破坏反射 / 非 Windows)时回退到 dispatch resize 兜底; // 用户仍可按 Ctrl+Shift+0 手动 toggle 修复。 if (shouldResetWebViewZoom) { try { const res = await (window as any).go?.app?.App?.ResetWebViewZoom?.(); if (!res?.success) { console.warn('ResetWebViewZoom unavailable in fixWindowScaleIfNeeded:', res?.message); } } catch (e) { console.warn('ResetWebViewZoom call failed in fixWindowScaleIfNeeded', e); } } window.dispatchEvent(new Event('resize')); lastFixAt = Date.now(); return; } try { WindowUnmaximise(); await wait(96); WindowMaximise(); await wait(96); } catch (e) { console.warn("Wails Window maximise restore unavailable in fixWindowScaleIfNeeded", e); } window.dispatchEvent(new Event('resize')); lastFixAt = Date.now(); return; } if (width <= 0 || height <= 0) { window.dispatchEvent(new Event('resize')); lastFixAt = Date.now(); return; } if (!shouldApplyWindowsScaleFix(reason, hasViewportScaleDrift)) { window.dispatchEvent(new Event('resize')); lastFixAt = Date.now(); return; } const nudgedWidth = getWindowsScaleFixNudgedWidth(width); try { WindowSetSize(nudgedWidth, height); await wait(28); WindowSetSize(width, height); } catch(e) {} window.dispatchEvent(new Event('resize')); lastFixAt = Date.now(); } catch(e) { console.warn("Wails Window APIs unavailable in fixWindowScaleIfNeeded", e); } finally { inFlight = false; } }; const rememberMinimisedState = async (): Promise => { if (cancelled) return false; const isMinimised = await safeWindowRuntimeCall(() => WindowIsMinimised(), false); if (isMinimised) { minimisedSeen = true; } return isMinimised; }; const rememberMinimisedStateSoon = () => { window.setTimeout(() => { if (cancelled) return; void rememberMinimisedState(); }, 120); }; const checkDevicePixelRatio = () => { if (cancelled) return; const currentRatio = Number(window.devicePixelRatio) || 1; if (Math.abs(currentRatio - lastRatio) < 0.02) { return; } lastRatio = currentRatio; if (minimisedSeen || hiddenSeen) { scheduleActivationFix(); return; } void fixWindowScaleIfNeeded('ratio-change'); }; const scheduleDevicePixelRatioCheck = (trigger: WindowsScaleCheckTrigger) => { if (cancelled) return; const delayMs = resolveWindowsScaleCheckDelayMs(trigger); if (delayMs <= 0) { checkDevicePixelRatio(); return; } if (resizeTimer !== null) { window.clearTimeout(resizeTimer); } resizeTimer = window.setTimeout(() => { resizeTimer = null; if (cancelled) return; checkDevicePixelRatio(); }, delayMs); }; const scheduleActivationFix = () => { if (cancelled) return; if (activationTimer !== null) { window.clearTimeout(activationTimer); } const delayMs = (minimisedSeen || hiddenSeen) ? 260 : 80; activationTimer = window.setTimeout(async () => { activationTimer = null; if (cancelled) return; if (await rememberMinimisedState()) { return; } const reason: WindowScaleFixReason = (minimisedSeen || hiddenSeen) ? 'restore' : 'activation'; minimisedSeen = false; hiddenSeen = false; void fixWindowScaleIfNeeded(reason); }, delayMs); }; const handleWindowFocus = () => { if (cancelled) return; scheduleDevicePixelRatioCheck('focus'); scheduleActivationFix(); }; const handleWindowBlur = () => { if (cancelled) return; if (document.visibilityState === 'hidden') { hiddenSeen = true; } rememberMinimisedStateSoon(); }; const handleVisibilityChange = () => { if (cancelled) return; if (document.visibilityState !== 'visible') { hiddenSeen = true; rememberMinimisedStateSoon(); return; } scheduleDevicePixelRatioCheck('visibilitychange'); scheduleActivationFix(); }; const handlePageShow = () => { if (cancelled) return; scheduleDevicePixelRatioCheck('pageshow'); scheduleActivationFix(); }; const handleWindowResize = () => { rememberMinimisedStateSoon(); scheduleDevicePixelRatioCheck('resize'); }; const pollTimer = window.setInterval(() => { void rememberMinimisedState(); checkDevicePixelRatio(); }, 900); window.addEventListener('resize', handleWindowResize); window.addEventListener('focus', handleWindowFocus); window.addEventListener('blur', handleWindowBlur); window.addEventListener('pageshow', handlePageShow); document.addEventListener('visibilitychange', handleVisibilityChange); return () => { cancelled = true; if (activationTimer !== null) { window.clearTimeout(activationTimer); } if (resizeTimer !== null) { window.clearTimeout(resizeTimer); } window.clearInterval(pollTimer); window.removeEventListener('resize', handleWindowResize); window.removeEventListener('focus', handleWindowFocus); window.removeEventListener('blur', handleWindowBlur); window.removeEventListener('pageshow', handlePageShow); document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, []); const { bgContent, bgMain, floatingLogButtonBgColor, floatingLogButtonBorderColor, floatingLogButtonShadow, floatingLogButtonTextColor, isSidebarCompact, isSidebarNarrow, isSidebarUltraCompact, overlayTheme, renderUtilityModalTitle, sidebarCreateConnectionActionStyle, sidebarHorizontalPadding, sidebarQueryActionStyle, toolCenterContentPanelStyle, toolCenterDetailBodyStyle, toolCenterDetailPanelStyle, toolCenterModalContentStyle, toolCenterModalSplitStyle, toolCenterModalWorkspaceStyle, toolCenterNavPanelStyle, toolCenterNavScrollStyle, toolCenterRowDescriptionStyle, toolCenterRowStyle, toolCenterScrollableListStyle, utilityActionCardStyle, utilityActionHintStyle, utilityButtonStyle, utilityModalShellStyle, utilityMutedTextStyle, utilityPanelStyle, } = useAppUtilityStyles({ blurFilter, darkMode, effectiveOpacity, effectiveUiScale, resolvedAppearance, sidebarWidth, }); const addTab = useStore(state => state.addTab); const activeContext = useStore(state => state.activeContext); const connections = useStore(state => state.connections); const tabs = useStore(state => state.tabs); const activeTabId = useStore(state => state.activeTabId); const setActiveTab = useStore(state => state.setActiveTab); const openSecurityUpdateSettings = useCallback((focusTarget: SecurityUpdateSettingsFocusTarget | null = null) => { setIsSecurityUpdateIntroOpen(false); setSecurityUpdateSettingsFocusTarget(focusTarget); setSecurityUpdateSettingsFocusRequest((current) => current + 1); setIsSecurityUpdateSettingsOpen(true); }, []); const handleOpenSecurityUpdateSettings = useCallback((focusTarget: SecurityUpdateSettingsFocusTarget | null = null) => { openSecurityUpdateSettings(focusTarget); }, [openSecurityUpdateSettings]); const runSecurityUpdateRound = useCallback(async (mode: 'start' | 'retry' | 'restart') => { const backendApp = (window as any).go?.app?.App; const stageText = mode === 'start' ? t('app.security_update.stage.checking_saved_config') : (mode === 'retry' ? t('app.security_update.stage.verifying_result') : t('app.security_update.stage.updating_secure_storage')); const detailsWereOpen = isSecurityUpdateSettingsOpen; setSecurityUpdateProgressStage(stageText); setIsSecurityUpdateProgressOpen(true); setIsSecurityUpdateIntroOpen(false); setIsSecurityUpdateSettingsOpen(false); let nextStatus: SecurityUpdateStatus | null = null; let shouldOpenSettings = false; let refreshSettingsFocus = false; try { if (mode === 'start') { const result = await startSecurityUpdateFromBootstrap({ backend: backendApp, replaceConnections, replaceGlobalProxy, t, }); if (result.error) { throw result.error; } nextStatus = normalizeSecurityUpdateStatus(result.status); } else if (mode === 'retry') { if (typeof backendApp?.RetrySecurityUpdateCurrentRound !== 'function') { throw new Error(t('app.security_update.error.capability_unavailable')); } nextStatus = normalizeSecurityUpdateStatus(await backendApp.RetrySecurityUpdateCurrentRound({ migrationId: securityUpdateStatus.migrationId, })); } else { if (typeof backendApp?.RestartSecurityUpdate !== 'function') { throw new Error(t('app.security_update.error.capability_unavailable')); } nextStatus = normalizeSecurityUpdateStatus(await backendApp.RestartSecurityUpdate({ migrationId: securityUpdateStatus.migrationId, sourceType: 'current_app_saved_config', rawPayload: securityUpdateRawPayload ?? '', options: { allowPartial: true, writeBackup: true, }, })); } if (mode !== 'start') { nextStatus = await finalizeSecurityUpdateStatus({ backend: backendApp, replaceConnections, replaceGlobalProxy, t, }, nextStatus); } shouldOpenSettings = nextStatus.overallStatus === 'needs_attention' || nextStatus.overallStatus === 'rolled_back'; refreshSettingsFocus = shouldRefreshSecurityUpdateDetailsFocus({ requestedOpen: shouldOpenSettings, wasOpen: detailsWereOpen, }); } catch (err: any) { console.warn('Failed to execute security update round', err); setIsSecurityUpdateProgressOpen(false); if (detailsWereOpen) { setIsSecurityUpdateSettingsOpen(true); } void message.error(err?.message || t('app.security_update.message.not_finished_retry_later')); return; } if (!nextStatus) { setIsSecurityUpdateProgressOpen(false); return; } setIsSecurityUpdateProgressOpen(false); applySecurityUpdateStatus(nextStatus, { openSettings: shouldOpenSettings, refreshFocus: refreshSettingsFocus, }); if (nextStatus.overallStatus === 'completed') { setSecurityUpdateHasLegacySensitiveItems(false); setSecurityUpdateRawPayload(null); setIsSecurityUpdateSettingsOpen(false); void message.success(t('app.security_update.message.completed')); } else if (nextStatus.overallStatus === 'needs_attention') { void message.warning(t('app.security_update.message.needs_attention')); } else if (nextStatus.overallStatus === 'rolled_back') { void message.warning(t('app.security_update.message.rolled_back')); } }, [ applySecurityUpdateStatus, isSecurityUpdateSettingsOpen, normalizeSecurityUpdateStatus, replaceConnections, replaceGlobalProxy, securityUpdateRawPayload, securityUpdateStatus.migrationId, t, ]); const handleStartSecurityUpdate = useCallback(() => { void runSecurityUpdateRound('start'); }, [runSecurityUpdateRound]); const handlePrepareExternalMCPUse = useCallback(async () => { const backendApp = (window as any).go?.app?.App; const result = await prepareSecureConfigForExternalMCP({ backend: backendApp, replaceConnections, replaceGlobalProxy, t, }); if (result.error) { throw result.error; } if (!result.status) { return; } const nextStatus = normalizeSecurityUpdateStatus(result.status); const shouldOpenSettings = nextStatus.overallStatus === 'needs_attention' || nextStatus.overallStatus === 'rolled_back'; applySecurityUpdateStatus(nextStatus, { openSettings: shouldOpenSettings, refreshFocus: shouldOpenSettings, }); if (nextStatus.overallStatus === 'completed') { setSecurityUpdateHasLegacySensitiveItems(false); setSecurityUpdateRawPayload(null); setIsSecurityUpdateSettingsOpen(false); return; } const hasConnectionIssue = nextStatus.issues.some((issue) => issue.scope === 'connection' && issue.status !== 'updated', ); if (nextStatus.overallStatus === 'rolled_back' || hasConnectionIssue) { throw new Error(t('app.security_update.message.needs_attention')); } if (nextStatus.overallStatus === 'needs_attention') { void message.warning(t('app.security_update.message.needs_attention')); } }, [ applySecurityUpdateStatus, normalizeSecurityUpdateStatus, replaceConnections, replaceGlobalProxy, t, ]); const handleRetrySecurityUpdate = useCallback(() => { void runSecurityUpdateRound('retry'); }, [runSecurityUpdateRound]); const handleRestartSecurityUpdate = useCallback(() => { void runSecurityUpdateRound('restart'); }, [runSecurityUpdateRound]); const handlePostponeSecurityUpdate = useCallback(async () => { const backendApp = (window as any).go?.app?.App; setIsSecurityUpdateIntroOpen(false); try { if (typeof backendApp?.DismissSecurityUpdateReminder === 'function') { const nextStatus = mergeSecurityUpdateStatusWithLegacySource( await backendApp.DismissSecurityUpdateReminder(), securityUpdateRawPayload, { t }, ); applySecurityUpdateStatus(nextStatus); return; } applySecurityUpdateStatus({ overallStatus: 'postponed', canStart: true, canPostpone: true, summary: securityUpdateStatus.summary, issues: securityUpdateStatus.issues, }); } catch (err: any) { console.warn('Failed to dismiss security update reminder', err); void message.error(err?.message || t('app.security_update.message.postpone_failed')); } }, [ applySecurityUpdateStatus, securityUpdateRawPayload, securityUpdateStatus.issues, securityUpdateStatus.summary, t, ]); const handleSecurityUpdateIssueAction = useCallback((issue: SecurityUpdateIssue) => { const repairEntry = resolveSecurityUpdateRepairEntry(issue, connections, securityUpdateStatus, t); if (repairEntry.type === 'warning') { void message.warning(repairEntry.message); return; } if (repairEntry.type === 'connection') { setIsSecurityUpdateSettingsOpen(false); setSecurityUpdateRepairSource(repairEntry.repairSource); setEditingConnection(repairEntry.connection); setIsModalOpen(true); return; } if (repairEntry.type === 'proxy') { setIsSecurityUpdateSettingsOpen(false); setSecurityUpdateRepairSource(repairEntry.repairSource); setIsProxyModalOpen(true); return; } if (repairEntry.type === 'ai') { setIsSecurityUpdateSettingsOpen(false); setSecurityUpdateRepairSource(repairEntry.repairSource); setFocusedAIProviderId(repairEntry.providerId); setIsAISettingsOpen(true); return; } if (repairEntry.type === 'retry') { void runSecurityUpdateRound('retry'); return; } setSecurityUpdateRepairSource(null); openSecurityUpdateSettings(repairEntry.focusTarget); }, [connections, openSecurityUpdateSettings, runSecurityUpdateRound, securityUpdateStatus, t]); const isMacRuntime = runtimePlatform === 'darwin' || (runtimePlatform === '' && /mac/i.test(detectNavigatorPlatform())); const isWindowsRuntime = runtimePlatform === 'windows' || (runtimePlatform === '' && isWindowsPlatform()); const useNativeMacWindowControls = isMacRuntime && appearance.useNativeMacWindowControls === true; const activeShortcutPlatform = getShortcutPlatform(isMacRuntime); const macWindowDiagnosticsEnabled = shouldEnableMacWindowDiagnostics( isMacRuntime, import.meta.env.DEV, import.meta.env.VITE_GONAVI_ENABLE_MAC_WINDOW_DIAGNOSTICS, ); useEffect(() => { return installGlobalImeCompositionTracking(window, document); }, []); const { aboutDisplayVersion, aboutInfo, aboutLoading, aboutUpdateStatus, canShowProgressEntry, checkForUpdates, downloadUpdate, formatBytes, handleInstallFromProgress, hideUpdateDownloadProgress, isAboutOpen, isBackgroundProgressForLatestUpdate, isLatestUpdateDownloaded, lastUpdateInfo, markUpdateProgressDismissed, muteLatestUpdate, setIsAboutOpen, showUpdateDownloadProgress, updateDownloadProgress, } = useAppUpdateManager({ isMacRuntime, runtimeBuildType, t, }); const emitWindowDiagnostic = useCallback(async (stage: string, extra: Record = {}) => { if (!macWindowDiagnosticsEnabled) { return; } const backendApp = (window as any).go?.app?.App; if (typeof backendApp?.LogWindowDiagnostic !== 'function') { return; } try { const [isFullscreen, isMaximised, isMinimised, isNormal, size, position] = await Promise.all([ safeWindowRuntimeCall(() => WindowIsFullscreen(), false), safeWindowRuntimeCall(() => WindowIsMaximised(), false), safeWindowRuntimeCall(() => WindowIsMinimised(), false), safeWindowRuntimeCall(() => WindowIsNormal(), false), safeWindowRuntimeCall(() => WindowGetSize(), null), safeWindowRuntimeCall(() => WindowGetPosition(), null), ]); const payload = { seq: ++windowDiagSequenceRef.current, ts: new Date().toISOString(), stage, nativeControls: useNativeMacWindowControls, documentVisible: document.visibilityState, documentHasFocus: document.hasFocus(), devicePixelRatio: Number(window.devicePixelRatio) || 1, windowState: { isFullscreen, isMaximised, isMinimised, isNormal, }, size: size ? { w: Math.trunc(Number(size.w || 0)), h: Math.trunc(Number(size.h || 0)) } : null, position: position ? { x: Math.trunc(Number(position.x || 0)), y: Math.trunc(Number(position.y || 0)) } : null, extra, }; const signature = JSON.stringify({ stage, nativeControls: payload.nativeControls, visible: payload.documentVisible, focus: payload.documentHasFocus, state: payload.windowState, size: payload.size, position: payload.position, extra, }); const now = Date.now(); if (signature === windowDiagLastSignatureRef.current && now-windowDiagLastAtRef.current < 250) { return; } windowDiagLastSignatureRef.current = signature; windowDiagLastAtRef.current = now; await backendApp.LogWindowDiagnostic(stage, JSON.stringify(payload)); } catch (error) { console.warn('Failed to emit window diagnostic', error); } }, [macWindowDiagnosticsEnabled, useNativeMacWindowControls]); useEffect(() => { if (!isStoreHydrated || !isMacRuntime) { return; } const backendApp = (window as any).go?.app?.App; if (typeof backendApp?.SetMacNativeWindowControls !== 'function') { return; } void safeWindowRuntimeCall(() => SetMacNativeWindowControls(useNativeMacWindowControls), undefined); }, [isMacRuntime, isStoreHydrated, useNativeMacWindowControls]); useEffect(() => { if (!macWindowDiagnosticsEnabled) { return; } let cancelled = false; let pollTimer: number | null = null; let burstTimer: number | null = null; const stopBurst = () => { if (pollTimer !== null) { window.clearInterval(pollTimer); pollTimer = null; } if (burstTimer !== null) { window.clearTimeout(burstTimer); burstTimer = null; } }; const startBurst = (reason: string, extra: Record = {}) => { if (cancelled) { return; } void emitWindowDiagnostic(`burst:start:${reason}`, extra); if (pollTimer === null) { pollTimer = window.setInterval(() => { void emitWindowDiagnostic(`burst:tick:${reason}`); }, 250); } if (burstTimer !== null) { window.clearTimeout(burstTimer); } burstTimer = window.setTimeout(() => { stopBurst(); void emitWindowDiagnostic(`burst:stop:${reason}`); }, 6000); }; const handleFocus = () => { void emitWindowDiagnostic('event:focus'); }; const handleBlur = () => { void emitWindowDiagnostic('event:blur'); }; const handleResize = () => { void emitWindowDiagnostic('event:resize'); }; const handleVisibilityChange = () => { void emitWindowDiagnostic('event:visibilitychange', { visibility: document.visibilityState }); }; const handleEditableKeydown = (event: KeyboardEvent) => { if (!isEditableElement(event.target)) { return; } const key = String(event.key || ''); const maybeFullscreenKey = key === 'Escape' || key.toLowerCase() === 'f' || key === 'Process'; const hasModifier = event.ctrlKey || event.metaKey || event.altKey; startBurst('editable-keydown', { key, code: String(event.code || ''), ctrlKey: event.ctrlKey, metaKey: event.metaKey, altKey: event.altKey, shiftKey: event.shiftKey, maybeFullscreenKey, hasModifier, }); }; const handleCompositionStart = () => { startBurst('compositionstart'); }; const handleCompositionEnd = () => { startBurst('compositionend'); }; void emitWindowDiagnostic('session:start'); window.addEventListener('focus', handleFocus); window.addEventListener('blur', handleBlur); window.addEventListener('resize', handleResize); window.addEventListener('keydown', handleEditableKeydown, true); window.addEventListener('compositionstart', handleCompositionStart, true); window.addEventListener('compositionend', handleCompositionEnd, true); document.addEventListener('visibilitychange', handleVisibilityChange); return () => { cancelled = true; stopBurst(); window.removeEventListener('focus', handleFocus); window.removeEventListener('blur', handleBlur); window.removeEventListener('resize', handleResize); window.removeEventListener('keydown', handleEditableKeydown, true); window.removeEventListener('compositionstart', handleCompositionStart, true); window.removeEventListener('compositionend', handleCompositionEnd, true); document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, [emitWindowDiagnostic, macWindowDiagnosticsEnabled]); const handleNewQuery = useCallback(() => { let connId = ''; let db = ''; // Priority: Active Tab Context (if connection still valid) > Sidebar Selection (activeContext) if (activeTabId) { const currentTab = tabs.find(t => t.id === activeTabId); if (currentTab && currentTab.connectionId && connections.some(c => c.id === currentTab.connectionId)) { connId = currentTab.connectionId; db = currentTab.dbName || ''; } } // Fallback: Sidebar selection context (only if connection still valid) if (!connId && activeContext?.connectionId && connections.some(c => c.id === activeContext.connectionId)) { connId = activeContext.connectionId; db = activeContext.dbName || ''; } addTab({ id: `query-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, title: t('query.new'), type: 'query', connectionId: connId, dbName: db, query: '' }); }, [activeTabId, tabs, connections, activeContext, addTab, t]); const switchActiveTabByOffset = useCallback((offset: 1 | -1) => { if (tabs.length < 2) return; const activeIndex = tabs.findIndex(tab => tab.id === activeTabId); const baseIndex = activeIndex >= 0 ? activeIndex : 0; const nextIndex = (baseIndex + offset + tabs.length) % tabs.length; setActiveTab(tabs[nextIndex].id); }, [activeTabId, setActiveTab, tabs]); const closeConnectionPackageDialog = useCallback(() => { setConnectionPackageDialog(createClosedConnectionPackageDialogState()); setPendingConnectionImportPayload(null); setToolCenterBackGroupKey(null); setActiveToolCenterPane((current) => (current?.key === 'connection-package' ? null : current)); }, []); const refreshConnectionsAfterImport = useCallback(async (importedViews: SavedConnection[]) => { const backendApp = (window as any).go?.app?.App; if (typeof backendApp?.GetSavedConnections === 'function') { let latestConnections: unknown; try { latestConnections = await GetSavedConnections(); } catch (error) { const detail = error instanceof Error ? error.message : String(error ?? '').trim(); throw new Error( detail ? t('app.connection_package.message.import_failed_with_error', { error: detail }) : t('app.connection_package.message.import_failed'), ); } if (!Array.isArray(latestConnections)) { throw new Error(t('app.connection_package.error.refresh_failed_no_connections')); } replaceConnections(latestConnections as SavedConnection[]); return; } const latestConnections = useStore.getState().connections; replaceConnections(mergeSavedConnections(latestConnections, importedViews)); }, [replaceConnections]); const importConnectionsPayload = useCallback(async (raw: string, password: string) => { const backendApp = (window as any).go?.app?.App; if (typeof backendApp?.ImportConnectionsPayload !== 'function') { throw new Error(t('app.connection_package.error.import_capability_unavailable')); } let importedViews: unknown; try { importedViews = await backendApp.ImportConnectionsPayload(raw, password); } catch (error) { if (isConnectionPackagePasswordRequiredError(error)) { throw error; } const detail = error instanceof Error ? error.message : String(error ?? '').trim(); throw new Error( detail ? t('app.connection_package.message.import_failed_with_error', { error: detail }) : t('app.connection_package.message.import_failed'), ); } if (!Array.isArray(importedViews)) { throw new Error(t('app.connection_package.error.import_no_connections')); } await refreshConnectionsAfterImport(importedViews as SavedConnection[]); return importedViews as SavedConnection[]; }, [refreshConnectionsAfterImport, t]); const handleImportConnections = async (sourceGroup?: ToolCenterGroupKey) => { setToolCenterBackGroupKey(sourceGroup ?? null); const res = await (window as any).go.app.App.ImportConfigFile(); if (!res.success) { if (res.message !== "已取消") { void message.error(t('app.connection_package.message.import_failed_with_error', { error: res.message })); } return; } const raw = typeof res.data === 'string' ? res.data : String(res.data ?? ''); const importKind = detectConnectionImportKind(raw); if (importKind === 'invalid') { void message.error(t('app.connection_package.message.unsupported_file_format')); return; } try { setPendingConnectionImportPayload(null); const importedViews = await importConnectionsPayload(raw, ''); if ((importKind === 'mysql-workbench-xml' || importKind === 'navicat-ncx') && importedViews.some(v => !v.hasPrimaryPassword)) { void message.warning(t('app.connection_package.message.imported_with_missing_passwords', { count: importedViews.length })); } else { void message.success(t('app.connection_package.message.imported_connections', { count: importedViews.length })); } } catch (e: any) { if (isConnectionPackagePasswordRequiredError(e)) { if (sourceGroup) { setToolCenterBackGroupKey(sourceGroup); setActiveToolCenterPane({ key: 'connection-package', group: sourceGroup }); } setPendingConnectionImportPayload(raw); setConnectionPackageDialog({ open: true, mode: 'import', includeSecrets: true, useFilePassword: false, password: '', error: '', confirmLoading: false, }); return; } void message.error(e?.message || t('app.connection_package.message.import_failed')); } }; const handleExportConnections = async (sourceGroup?: ToolCenterGroupKey) => { if (connections.length === 0) { void message.warning(t('app.connection_package.message.no_connections_to_export')); return; } setToolCenterBackGroupKey(sourceGroup ?? null); if (sourceGroup) { setActiveToolCenterPane({ key: 'connection-package', group: sourceGroup }); } setConnectionPackageDialog({ open: true, mode: 'export', includeSecrets: true, useFilePassword: false, password: '', error: '', confirmLoading: false, }); }; const handleConfirmConnectionPackageDialog = async () => { const backendApp = (window as any).go?.app?.App; const password = normalizeConnectionPackagePassword(connectionPackageDialog.password); if (connectionPackageDialog.mode === 'import' && !password) { setConnectionPackageDialog((current) => ({ ...current, error: t('app.connection_package.error.restore_password_required'), })); return; } if ( connectionPackageDialog.mode === 'export' && connectionPackageDialog.includeSecrets && connectionPackageDialog.useFilePassword && !password ) { setConnectionPackageDialog((current) => ({ ...current, error: t('app.connection_package.error.file_password_required'), })); return; } setConnectionPackageDialog((current) => ({ ...current, password: ( current.mode === 'export' && (!current.includeSecrets || !current.useFilePassword) ) ? '' : password, error: '', confirmLoading: true, })); try { if (connectionPackageDialog.mode === 'export') { if (typeof backendApp?.ExportConnectionsPackage !== 'function') { throw new Error(t('app.connection_package.error.export_capability_unavailable')); } let res: unknown; try { res = await backendApp.ExportConnectionsPackage({ includeSecrets: connectionPackageDialog.includeSecrets, filePassword: ( connectionPackageDialog.includeSecrets && connectionPackageDialog.useFilePassword ) ? password : '', }); } catch (error) { const detail = error instanceof Error ? error.message : String(error ?? '').trim(); throw new Error( detail ? `${t('app.connection_package.message.export_failed')}: ${detail}` : t('app.connection_package.message.export_failed'), ); } const exportResult = resolveConnectionPackageExportResult(connectionPackageDialog, res); if (exportResult.kind === 'canceled') { setConnectionPackageDialog(exportResult.nextDialog); return; } if (exportResult.kind === 'failed') { throw new Error(exportResult.error); } closeConnectionPackageDialog(); void message.success(t('app.connection_package.message.export_succeeded')); return; } if (!pendingConnectionImportPayload) { throw new Error(t('app.connection_package.error.missing_import_payload')); } const importedViews = await importConnectionsPayload(pendingConnectionImportPayload, password); closeConnectionPackageDialog(); void message.success(t('app.connection_package.message.imported_connections', { count: importedViews.length })); } catch (e: any) { setConnectionPackageDialog((current) => ({ ...current, confirmLoading: false, error: e?.message || t( current.mode === 'export' ? 'app.connection_package.message.export_failed' : 'app.connection_package.message.import_failed', ), })); } }; const [isToolsModalOpen, setIsToolsModalOpen] = useState(false); const [activeToolCenterGroupKey, setActiveToolCenterGroupKey] = useState('config'); const [toolCenterBackGroupKey, setToolCenterBackGroupKey] = useState(null); const [activeToolCenterPane, setActiveToolCenterPane] = useState(null); const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); const [isLanguageModalOpen, setIsLanguageModalOpen] = useState(false); const [isThemeModalOpen, setIsThemeModalOpen] = useState(false); const [themeModalSection, setThemeModalSection] = useState<'theme' | 'appearance'>('theme'); const [isLinuxCJKFontBannerDismissed, setIsLinuxCJKFontBannerDismissed] = useState(false); const [isAppearanceModalOpen, setIsAppearanceModalOpen] = useState(false); const [isShortcutModalOpen, setIsShortcutModalOpen] = useState(false); const [isSnippetModalOpen, setIsSnippetModalOpen] = useState(false); const [capturingShortcutAction, setCapturingShortcutAction] = useState(null); const tabDisplaySettingsPanelRef = useRef(null); const [tabDisplaySettingsFocusRequest, setTabDisplaySettingsFocusRequest] = useState(0); useEffect(() => { const shouldLoadInstalledFonts = runtimePlatform === 'linux' || (isThemeModalOpen && themeModalSection === 'appearance'); if (!shouldLoadInstalledFonts) { return; } if (hasLoadedInstalledFontsRef.current || isFontFamiliesLoading) { return; } let cancelled = false; hasLoadedInstalledFontsRef.current = true; setIsFontFamiliesLoading(true); setFontFamiliesLoadError(null); ListInstalledFontFamilies() .then((result) => { if (cancelled) { return; } if (!result?.success) { throw new Error(String(result?.message || t('app.theme.font_family.load_failed'))); } const nextFonts = Array.isArray(result?.data) ? result.data .map((item) => ({ family: sanitizeFontFamilyInput((item as InstalledFontFamily | Record)?.family) || '', path: typeof (item as InstalledFontFamily | Record)?.path === 'string' ? String((item as InstalledFontFamily | Record).path) : undefined, })) .filter((item) => item.family) : EMPTY_INSTALLED_FONT_FAMILIES; setInstalledFontFamilies(nextFonts); }) .catch((error) => { if (cancelled) { return; } hasLoadedInstalledFontsRef.current = false; setFontFamiliesLoadError(String(error instanceof Error ? error.message : error || t('app.theme.font_family.load_failed'))); }) .finally(() => { if (!cancelled) { setIsFontFamiliesLoading(false); } }); return () => { cancelled = true; }; }, [isThemeModalOpen, runtimePlatform, t, themeModalSection]); useEffect(() => { if (!isThemeModalOpen || themeModalSection !== 'appearance' || tabDisplaySettingsFocusRequest === 0) { return; } const timer = window.setTimeout(() => { tabDisplaySettingsPanelRef.current?.scrollIntoView({ block: 'start', behavior: 'smooth' }); }, 80); return () => window.clearTimeout(timer); }, [isThemeModalOpen, themeModalSection, tabDisplaySettingsFocusRequest]); const shortcutConflictMap = useMemo(() => { const map: Partial> = {}; for (const action of SHORTCUT_ACTION_ORDER) { const binding = resolveShortcutBinding(shortcutOptions, action, activeShortcutPlatform); if (!binding?.enabled || !binding.combo) continue; const conflicts = findReservedConflicts(normalizeShortcutCombo(binding.combo), activeShortcutPlatform); if (conflicts.length > 0) { map[action] = conflicts; } } return map; }, [activeShortcutPlatform, language, shortcutOptions]); const [isProxyModalOpen, setIsProxyModalOpen] = useState(false); const [isDataRootModalOpen, setIsDataRootModalOpen] = useState(false); const [dataRootInfo, setDataRootInfo] = useState(null); const [selectedDataRootPath, setSelectedDataRootPath] = useState(''); const [dataRootLoading, setDataRootLoading] = useState(false); const [dataRootApplying, setDataRootApplying] = useState(false); const [isAISettingsOpen, setIsAISettingsOpen] = useState(false); const aiEntryPlacement = resolveAIEntryPlacement(); const legacyAiEdgeHandleAttachment = resolveLegacyAIEdgeHandleAttachment(aiPanelVisible); const aiPanelOverlayActive = aiPanelVisible && shouldOverlayAIPanel({ isV2Ui, viewportWidth, sidebarWidth, panelWidth: DEFAULT_AI_PANEL_WIDTH, }); const aiPanelRenderWidth = aiPanelOverlayActive ? resolveOverlayAIPanelWidth({ viewportWidth, sidebarWidth, panelWidth: DEFAULT_AI_PANEL_WIDTH, }) : DEFAULT_AI_PANEL_WIDTH; const legacyAiEdgeHandleDockStyle = useMemo( () => resolveLegacyAIEdgeHandleDockStyle(legacyAiEdgeHandleAttachment), [legacyAiEdgeHandleAttachment], ); const legacyAiEdgeHandleStyle = useMemo(() => ( resolveLegacyAIEdgeHandleStyle({ darkMode, aiPanelVisible, effectiveUiScale, }) ), [aiPanelVisible, darkMode, effectiveUiScale]); const handleOpenToolsModal = useCallback((group: ToolCenterGroupKey = 'config') => { setToolCenterBackGroupKey(null); setActiveToolCenterPane(null); setActiveToolCenterGroupKey(group); setIsToolsModalOpen(true); }, []); const handleOpenSettingsModal = useCallback(() => { setIsSettingsModalOpen(true); }, []); const handleOpenToolCenterPane = useCallback((group: ToolCenterGroupKey, key: ToolCenterPaneKey) => { setToolCenterBackGroupKey(group); setActiveToolCenterGroupKey(group); setActiveToolCenterPane({ key, group }); setIsToolsModalOpen(true); }, []); const handleReturnToToolCenter = useCallback((closeChild?: () => void) => { const returnGroup = toolCenterBackGroupKey ?? 'config'; closeChild?.(); setToolCenterBackGroupKey(null); setActiveToolCenterGroupKey(returnGroup); setActiveToolCenterPane(null); setIsToolsModalOpen(true); }, [toolCenterBackGroupKey]); const sidebarUtilityItems = useMemo(() => { const itemMap = { tools: { key: 'tools', title: t('app.sidebar.tools'), icon: , onClick: () => handleOpenToolsModal(), }, settings: { key: 'settings', title: t('app.sidebar.settings'), icon: , onClick: () => handleOpenSettingsModal(), }, } as const; return SIDEBAR_UTILITY_ITEM_KEYS.map((key) => itemMap[key]); }, [handleOpenSettingsModal, handleOpenToolsModal, t]); const handleFocusSidebarSearch = useCallback(() => { window.dispatchEvent(new CustomEvent('gonavi:focus-sidebar-search')); }, []); const renderLegacyAIEdgeHandle = () => ( ); const loadDataRootInfo = useCallback(async () => { setDataRootLoading(true); try { const res = await GetDataRootDirectoryInfo(); if (!res?.success) { throw new Error(res?.message || t('app.data_root.message.load_failed')); } const data = (res?.data || {}) as any; setDataRootInfo(data); setSelectedDataRootPath(String(data.path || '')); } catch (error) { const errMsg = error instanceof Error ? error.message : String(error || t('common.unknown')); void message.error(t('app.data_root.message.load_failed_with_error', { error: errMsg })); } finally { setDataRootLoading(false); } }, [t]); useEffect(() => { if (!isDataRootModalOpen && activeToolCenterPane?.key !== 'data-root') { return; } void loadDataRootInfo(); }, [activeToolCenterPane?.key, isDataRootModalOpen, loadDataRootInfo]); const handleSelectDataRoot = useCallback(async () => { try { const res = await SelectDataRootDirectory(selectedDataRootPath || dataRootInfo?.path || ''); if (!res?.success) { if (String(res?.message || '') !== '已取消') { throw new Error(res?.message || t('app.data_root.message.select_failed')); } return; } const data = (res?.data || {}) as any; setSelectedDataRootPath(String(data.path || '')); } catch (error) { const errMsg = error instanceof Error ? error.message : String(error || t('common.unknown')); void message.error(t('app.data_root.message.select_failed_with_error', { error: errMsg })); } }, [dataRootInfo?.path, selectedDataRootPath, t]); const handleApplyDataRoot = useCallback(async (migrate: boolean, useDefaultPath = false) => { const nextPath = useDefaultPath ? String(dataRootInfo?.defaultPath || '') : String(selectedDataRootPath || '').trim(); if (!nextPath) { void message.warning(t('app.data_root.message.select_valid_first')); return; } setDataRootApplying(true); try { const res = await ApplyDataRootDirectory(nextPath, migrate); if (!res?.success) { throw new Error(res?.message || t('app.data_root.message.apply_failed')); } const data = (res?.data || {}) as any; setDataRootInfo(data); setSelectedDataRootPath(String(data.path || nextPath)); void message.success(res?.message || t('app.data_root.message.updated')); } catch (error) { const errMsg = error instanceof Error ? error.message : String(error || t('common.unknown')); void message.error(t('app.data_root.message.apply_failed_with_error', { error: errMsg })); } finally { setDataRootApplying(false); } }, [dataRootInfo?.defaultPath, selectedDataRootPath, t]); const handleOpenDataRoot = useCallback(async () => { try { const res = await OpenDataRootDirectory(); if (!res?.success) { throw new Error(res?.message || t('app.data_root.message.open_failed')); } } catch (error) { const errMsg = error instanceof Error ? error.message : String(error || t('common.unknown')); void message.error(t('app.data_root.message.open_failed_with_error', { error: errMsg })); } }, [t]); const { handleCloseLogPanel: handleCloseAppLogPanel, handleLogResizeStart, handleToggleLogPanel: toggleAppLogPanel, isLogPanelOpen, logGhostRef, logPanelHeight, } = useAppLogPanelResize(); const handleToggleLogPanel = useCallback(() => { if (isV2Ui) { window.dispatchEvent(new CustomEvent('gonavi:show-sql-execution-log', { detail: { mode: 'open' } })); return; } toggleAppLogPanel(); }, [isV2Ui, toggleAppLogPanel]); const handleCloseLogPanel = useCallback(() => { handleCloseAppLogPanel(); }, [handleCloseAppLogPanel]); const handleCreateConnection = useCallback(() => { setSecurityUpdateRepairSource(null); setEditingConnection(null); setIsConnectionModalMounted(true); setIsModalOpen(true); }, []); const handleEditConnection = useCallback((conn: SavedConnection) => { setSecurityUpdateRepairSource(null); setIsConnectionModalMounted(true); void (async () => { const backendApp = (window as any).go?.app?.App; let nextConnection = conn; if (typeof backendApp?.GetEditableSavedConnection === 'function') { try { const editableConnection = await backendApp.GetEditableSavedConnection(conn.id); if (editableConnection) { nextConnection = editableConnection; } } catch (error: any) { const errorMessage = error?.message; const detail = ( typeof errorMessage === 'string' ? errorMessage : ( typeof errorMessage === 'number' || typeof errorMessage === 'boolean' ? String(errorMessage) : String(error ?? '') ) ).trim(); void message.warning( detail ? t('app.connection.message.editable_load_failed_with_detail', { detail }) : t('app.connection.message.editable_load_failed') ); } } setEditingConnection(nextConnection); setIsModalOpen(true); })(); }, [t]); useEffect(() => { if (connectionModalWarmupDoneRef.current) { return; } connectionModalWarmupDoneRef.current = true; const warmup = () => setIsConnectionModalMounted(true); if (typeof window === 'undefined') { warmup(); return; } if (typeof window.requestIdleCallback === 'function') { const idleId = window.requestIdleCallback(() => warmup(), { timeout: 1200 }); return () => window.cancelIdleCallback?.(idleId); } const timerId = window.setTimeout(warmup, 300); return () => window.clearTimeout(timerId); }, []); const handleConnectionSaved = useCallback(async (savedConnection: SavedConnection) => { if (!shouldRetrySecurityUpdateAfterRepairSave(securityUpdateRepairSource)) { return; } const backendApp = (window as any).go?.app?.App; if (securityUpdateStatus.migrationId) { if (typeof backendApp?.RetrySecurityUpdateCurrentRound !== 'function') { return; } const rawStatus = await backendApp.RetrySecurityUpdateCurrentRound({ migrationId: securityUpdateStatus.migrationId, }); const nextStatus = await finalizeSecurityUpdateStatus({ backend: backendApp, replaceConnections, replaceGlobalProxy, t, }, normalizeSecurityUpdateStatus(rawStatus)); applySecurityUpdateStatus(nextStatus, { openSettings: false, }); if (nextStatus.overallStatus === 'completed') { setSecurityUpdateHasLegacySensitiveItems(false); setSecurityUpdateRawPayload(null); } return; } if (!securityUpdateRawPayload || !savedConnection?.id) { return; } const nextRawPayload = stripLegacyPersistedConnectionById(securityUpdateRawPayload, savedConnection.id); if (!nextRawPayload || nextRawPayload === securityUpdateRawPayload) { return; } window.localStorage.setItem(LEGACY_PERSIST_KEY, nextRawPayload); const rawStatus = typeof backendApp?.GetSecurityUpdateStatus === 'function' ? await backendApp.GetSecurityUpdateStatus() : securityUpdateStatus; const nextStatus = mergeSecurityUpdateStatusWithLegacySource(rawStatus, nextRawPayload, { previousStatus: securityUpdateStatus, t, }); const nextHasLegacySensitiveItems = hasLegacyMigratableSensitiveItems(nextRawPayload); setSecurityUpdateRawPayload(nextRawPayload); setSecurityUpdateHasLegacySensitiveItems(nextHasLegacySensitiveItems); applySecurityUpdateStatus(nextStatus, { openSettings: false, }); }, [ applySecurityUpdateStatus, normalizeSecurityUpdateStatus, replaceConnections, replaceGlobalProxy, securityUpdateRawPayload, securityUpdateRepairSource, securityUpdateStatus, securityUpdateStatus.migrationId, t, ]); const handleCloseModal = () => { const reopenSecurityUpdateDetails = shouldReopenSecurityUpdateDetails(securityUpdateRepairSource); setIsModalOpen(false); setEditingConnection(null); setSecurityUpdateRepairSource(null); if (reopenSecurityUpdateDetails) { setIsSecurityUpdateSettingsOpen(true); } }; const handleOpenDriverManagerFromConnection = () => { setIsModalOpen(false); setEditingConnection(null); setToolCenterBackGroupKey(null); setIsDriverModalOpen(true); }; const handleCloseDriverManager = useCallback(() => { const reopenSecurityUpdateDetails = shouldReopenSecurityUpdateDetails(securityUpdateRepairSource); setIsDriverModalOpen(false); setToolCenterBackGroupKey(null); setSecurityUpdateRepairSource(null); if (reopenSecurityUpdateDetails) { setIsSecurityUpdateSettingsOpen(true); } }, [securityUpdateRepairSource]); const handleOpenGlobalProxySettings = useCallback(() => { setSecurityUpdateRepairSource(null); setIsProxyModalOpen(true); }, []); const handleCloseGlobalProxySettings = useCallback(() => { const reopenSecurityUpdateDetails = shouldReopenSecurityUpdateDetails(securityUpdateRepairSource); setIsProxyModalOpen(false); setSecurityUpdateRepairSource(null); if (reopenSecurityUpdateDetails) { setIsSecurityUpdateSettingsOpen(true); } }, [securityUpdateRepairSource]); const handleOpenAISettings = useCallback((providerId?: string) => { setSecurityUpdateRepairSource(null); setFocusedAIProviderId(providerId); setIsAISettingsOpen(true); }, []); const handleAIPanelRenderError = useCallback((error: Error, errorInfo: React.ErrorInfo) => { try { (window as any).__gonaviLastAIPanelRenderError = { message: error?.message || '', stack: error?.stack || '', componentStack: errorInfo?.componentStack || '', }; } catch { // ignore debug capture failures } console.error('AIChatPanel render error:', error, errorInfo); }, []); const handleRetryAIPanelRender = useCallback(() => { setAiPanelRenderNonce((current) => current + 1); }, []); const handleCloseAISettings = useCallback(() => { const reopenSecurityUpdateDetails = shouldReopenSecurityUpdateDetails(securityUpdateRepairSource); setIsAISettingsOpen(false); setFocusedAIProviderId(undefined); setSecurityUpdateRepairSource(null); if (reopenSecurityUpdateDetails) { setIsSecurityUpdateSettingsOpen(true); } }, [securityUpdateRepairSource]); const handleTitleBarWindowToggle = async (options?: { allowMacNativeFullscreen?: boolean }) => { const allowMacNativeFullscreen = options?.allowMacNativeFullscreen === true; const syncWindowStateFromRuntime = async () => { try { const [isFullscreen, isMaximised] = await Promise.all([ safeWindowRuntimeCall(() => WindowIsFullscreen(), false), safeWindowRuntimeCall(() => WindowIsMaximised(), false), ]); useStore.getState().setWindowState(isFullscreen ? 'fullscreen' : (isMaximised ? 'maximized' : 'normal')); } catch { // ignore } }; try { void emitWindowDiagnostic('action:titlebar-toggle:before'); if (await WindowIsFullscreen()) { await WindowUnfullscreen(); await syncWindowStateFromRuntime(); void emitWindowDiagnostic('action:titlebar-toggle:after-unfullscreen'); return; } if (allowMacNativeFullscreen && useNativeMacWindowControls && isMacRuntime) { await WindowFullscreen(); await syncWindowStateFromRuntime(); void emitWindowDiagnostic('action:titlebar-toggle:after-fullscreen'); return; } const isMaximised = await safeWindowRuntimeCall(() => WindowIsMaximised(), false); if (isMaximised) { WindowUnmaximise(); } else { WindowMaximise(); } await new Promise((resolve) => window.setTimeout(resolve, 96)); await syncWindowStateFromRuntime(); void emitWindowDiagnostic('action:titlebar-toggle:after-set-maximise-state'); } catch (_) { // ignore } }; const handleTitleBarDoubleClick = (e: React.MouseEvent) => { const target = e.target as HTMLElement | null; if (target?.closest('[data-no-titlebar-toggle="true"]')) { return; } void handleTitleBarWindowToggle({ allowMacNativeFullscreen: false }); }; // handleManualResetWindowZoom 由 resetWindowZoom 快捷键(默认 Ctrl+Shift+0)触发, // 作为自动路径失败时的兜底入口。 // // 优先调 backend App.ResetWebViewZoom 走 WebView2 zoom reset(零动画零感知); // 失败时回退到 Unmaximise→Maximise toggle —— 用户主动按了快捷键,预期看见动画。 const handleManualResetWindowZoom = React.useCallback(async () => { if (!isWindowsPlatform()) { message.info(t('app.window_zoom.message.windows_only')); return; } try { const res = await (window as any).go?.app?.App?.ResetWebViewZoom?.(); if (res?.success) { window.dispatchEvent(new Event('resize')); message.success(t('app.window_zoom.message.reset_success')); return; } console.warn('ResetWebViewZoom backend reported failure, falling back to maximise toggle:', res?.message); } catch (e) { console.warn('ResetWebViewZoom backend unavailable, falling back to maximise toggle', e); } try { const isFullscreen = await safeWindowRuntimeCall(() => WindowIsFullscreen(), false); if (isFullscreen) { message.info(t('app.window_zoom.message.fullscreen_exit_first')); return; } const isMaximised = await safeWindowRuntimeCall(() => WindowIsMaximised(), false); if (isMaximised) { WindowUnmaximise(); await new Promise((resolve) => window.setTimeout(resolve, 96)); WindowMaximise(); await new Promise((resolve) => window.setTimeout(resolve, 96)); } else { const size = await safeWindowRuntimeCall(() => WindowGetSize(), null); const width = Math.trunc(Number(size?.w) || 0); const height = Math.trunc(Number(size?.h) || 0); if (width > 0 && height > 0) { WindowSetSize(getWindowsScaleFixNudgedWidth(width), height); await new Promise((resolve) => window.setTimeout(resolve, 28)); WindowSetSize(width, height); } } window.dispatchEvent(new Event('resize')); message.success(t('app.window_zoom.message.reset_success_fallback')); } catch (e) { console.warn('Failed to reset window zoom', e); message.error(t('app.window_zoom.message.reset_failed')); } }, [t]); const { ghostRef, handleSidebarMouseDown, sidebarResizeHandleWidth, siderRef, } = useAppSidebarResize({ effectiveUiScale, setSidebarWidth, sidebarWidth, }); useEffect(() => { document.body.style.backgroundColor = 'transparent'; document.body.style.color = darkMode ? '#ffffff' : '#000000'; document.body.setAttribute('data-theme', darkMode ? 'dark' : 'light'); document.body.setAttribute('data-ui-version', appearance.uiVersion); document.body.setAttribute('data-platform', runtimePlatform || ''); document.body.style.fontSize = `${effectiveFontSize}px`; document.body.style.setProperty('--gn-font-sans', resolvedUiFontFamily); document.body.style.setProperty('--gn-font-mono', resolvedMonoFontFamily); document.documentElement.style.setProperty('--gonavi-font-size', `${effectiveFontSize}px`); document.documentElement.style.setProperty('--gn-font-sans', resolvedUiFontFamily); document.documentElement.style.setProperty('--gn-font-mono', resolvedMonoFontFamily); document.documentElement.style.setProperty('--gn-ui-scale', `${effectiveUiScale}`); document.documentElement.style.setProperty('--gn-font-size', `${effectiveFontSize}px`); document.documentElement.style.setProperty('--gn-font-size-sm', `${Math.max(10, Math.round(effectiveFontSize * 0.86))}px`); document.documentElement.style.setProperty('--gn-font-size-xs', `${Math.max(9, Math.round(effectiveFontSize * 0.76))}px`); document.documentElement.style.setProperty('--gn-font-size-mono', `${Math.max(10, Math.round(effectiveDataTableFontSize * 0.92))}px`); document.documentElement.style.setProperty('--gn-data-table-font-size', `${effectiveDataTableFontSize}px`); document.documentElement.style.setProperty('--gn-sidebar-tree-font-size', `${effectiveSidebarTreeFontSize}px`); document.documentElement.style.setProperty('--gn-control-height', `${tokenControlHeight}px`); document.documentElement.style.setProperty('--gn-control-height-sm', `${tokenControlHeightSM}px`); }, [ appearance.uiVersion, darkMode, effectiveDataTableFontSize, effectiveFontSize, resolvedMonoFontFamily, resolvedUiFontFamily, runtimePlatform, effectiveSidebarTreeFontSize, effectiveUiScale, tokenControlHeight, tokenControlHeightSM, ]); useEffect(() => { const handleOpenShortcutSettingsEvent = () => { setIsShortcutModalOpen(true); }; window.addEventListener('gonavi:open-shortcut-settings', handleOpenShortcutSettingsEvent as EventListener); return () => { window.removeEventListener('gonavi:open-shortcut-settings', handleOpenShortcutSettingsEvent as EventListener); }; }, []); useEffect(() => { const handleOpenSnippetSettingsEvent = () => { setIsSnippetModalOpen(false); handleOpenToolCenterPane('workspace', 'snippet-settings'); }; window.addEventListener('gonavi:open-snippet-settings', handleOpenSnippetSettingsEvent as EventListener); return () => { window.removeEventListener('gonavi:open-snippet-settings', handleOpenSnippetSettingsEvent as EventListener); }; }, [handleOpenToolCenterPane]); useEffect(() => { const handleOpenTabDisplaySettingsEvent = () => { setIsSettingsModalOpen(false); setThemeModalSection('appearance'); setIsThemeModalOpen(true); setTabDisplaySettingsFocusRequest((current) => current + 1); }; window.addEventListener('gonavi:open-tab-display-settings', handleOpenTabDisplaySettingsEvent as EventListener); return () => { window.removeEventListener('gonavi:open-tab-display-settings', handleOpenTabDisplaySettingsEvent as EventListener); }; }, []); useEffect(() => { const handleCreateQueryTabEvent = () => { handleNewQuery(); }; window.addEventListener('gonavi:create-query-tab', handleCreateQueryTabEvent as EventListener); return () => { window.removeEventListener('gonavi:create-query-tab', handleCreateQueryTabEvent as EventListener); }; }, [handleNewQuery]); useEffect(() => { if (!isMacRuntime || !useNativeMacWindowControls) { return; } const handleMacNativeEscapeCapture = (event: KeyboardEvent) => { if (!shouldSuppressMacNativeEscapeExit( isMacRuntime, useNativeMacWindowControls, useStore.getState().windowState === 'fullscreen', event, { isEditableTarget: isEditableElement(event.target) }, )) { return; } event.preventDefault(); event.stopPropagation(); }; window.addEventListener('keydown', handleMacNativeEscapeCapture, true); return () => { window.removeEventListener('keydown', handleMacNativeEscapeCapture, true); }; }, [isMacRuntime, useNativeMacWindowControls]); useEffect(() => { const handleGlobalShortcut = (event: KeyboardEvent) => { const matchedAction = SHORTCUT_ACTION_ORDER.find((action) => { const meta = SHORTCUT_ACTION_META[action]; if (meta.scope && meta.scope !== 'global') { return false; } const binding = resolveShortcutBinding(shortcutOptions, action, activeShortcutPlatform); if (!binding?.enabled) { return false; } if (isEditableElement(event.target) && !meta.allowInEditable) { return false; } return isShortcutMatch(event, binding.combo); }); if (!matchedAction) { return; } event.preventDefault(); event.stopPropagation(); switch (matchedAction) { case 'runQuery': window.dispatchEvent(new CustomEvent('gonavi:run-active-query')); break; case 'focusSidebarSearch': window.dispatchEvent(new CustomEvent('gonavi:focus-sidebar-search')); break; case 'newQueryTab': handleNewQuery(); break; case 'switchToNextTab': switchActiveTabByOffset(1); break; case 'switchToPreviousTab': switchActiveTabByOffset(-1); break; case 'newConnection': handleCreateConnection(); break; case 'toggleAIPanel': toggleAIPanel(); break; case 'toggleLogPanel': handleToggleLogPanel(); break; case 'toggleTheme': setTheme(themeMode === 'dark' ? 'light' : 'dark'); break; case 'openShortcutManager': setIsShortcutModalOpen(true); break; case 'toggleMacFullscreen': if (isMacRuntime && useNativeMacWindowControls) { void handleTitleBarWindowToggle({ allowMacNativeFullscreen: true }); } break; case 'resetWindowZoom': void handleManualResetWindowZoom(); break; } }; window.addEventListener('keydown', handleGlobalShortcut, true); return () => { window.removeEventListener('keydown', handleGlobalShortcut, true); }; }, [activeShortcutPlatform, handleCreateConnection, handleManualResetWindowZoom, handleNewQuery, handleTitleBarWindowToggle, handleToggleLogPanel, isMacRuntime, shortcutOptions, switchActiveTabByOffset, themeMode, setTheme, toggleAIPanel, useNativeMacWindowControls]); useEffect(() => { if (!capturingShortcutAction) { return; } const handleShortcutCapture = (event: KeyboardEvent) => { event.preventDefault(); event.stopPropagation(); if (event.key === 'Escape') { setCapturingShortcutAction(null); return; } const combo = eventToShortcut(event); if (!combo) { return; } const normalizedCombo = normalizeShortcutCombo(combo); if (!canRecordShortcutForAction(capturingShortcutAction, normalizedCombo)) { const meta = SHORTCUT_ACTION_META[capturingShortcutAction]; void message.warning(meta.scope === 'aiComposer' ? t('app.shortcuts.message.ai_send_limit') : t('app.shortcuts.message.modifier_required')); return; } const conflictAction = SHORTCUT_ACTION_ORDER.find((action) => { if (action === capturingShortcutAction) { return false; } const binding = resolveShortcutBinding(shortcutOptions, action, activeShortcutPlatform); if (!binding?.enabled) { return false; } return normalizeShortcutCombo(binding.combo) === normalizedCombo; }); if (conflictAction) { void message.warning(t('app.shortcuts.message.conflict', { action: SHORTCUT_ACTION_META[conflictAction].label })); return; } const reservedConflicts = findReservedConflicts(normalizedCombo, activeShortcutPlatform); if (reservedConflicts.length > 0) { const { hasMonaco, hasOther, monacoLabels, otherLabels, otherContexts } = splitConflictsByContext(reservedConflicts); if (hasMonaco) { void message.info(t('app.shortcuts.message.reserved_conflict_info', { labels: monacoLabels }), 4); } if (hasOther) { void message.warning(t('app.shortcuts.message.reserved_conflict_warning', { contexts: otherContexts, labels: otherLabels }), 4); } } updateShortcut(capturingShortcutAction, { combo: normalizedCombo, enabled: true }, activeShortcutPlatform); setCapturingShortcutAction(null); }; window.addEventListener('keydown', handleShortcutCapture, true); return () => { window.removeEventListener('keydown', handleShortcutCapture, true); }; }, [activeShortcutPlatform, capturingShortcutAction, shortcutOptions, t, updateShortcut]); const linuxResizeHandleStyleBase = { position: 'fixed', zIndex: 12000, background: 'transparent', WebkitAppRegion: 'drag', '--wails-draggable': 'drag', userSelect: 'none' } as any; const showLinuxResizeHandles = isLinuxRuntime; const resizeGuideColor = isV2Ui ? 'var(--gn-accent, #16a34a)' : (darkMode ? 'rgba(246, 196, 83, 0.55)' : 'rgba(24, 144, 255, 0.5)'); const antdTheme = useMemo(() => ({ algorithm: darkMode ? theme.darkAlgorithm : theme.defaultAlgorithm, token: { fontSize: tokenFontSize, fontSizeSM: tokenFontSizeSM, fontSizeLG: tokenFontSizeLG, fontFamily: resolvedUiFontFamily, fontFamilyCode: resolvedMonoFontFamily, controlHeight: tokenControlHeight, controlHeightSM: tokenControlHeightSM, controlHeightLG: tokenControlHeightLG, colorBgLayout: 'transparent', colorBgContainer: darkMode ? `rgba(29, 29, 29, ${effectiveOpacity})` : `rgba(255, 255, 255, ${effectiveOpacity})`, colorBgElevated: darkMode ? '#1f1f1f' : '#ffffff', colorFillAlter: darkMode ? `rgba(38, 38, 38, ${effectiveOpacity})` : `rgba(250, 250, 250, ${effectiveOpacity})`, colorPrimary: darkMode ? '#f6c453' : '#1677ff', colorPrimaryHover: darkMode ? '#ffd666' : '#4096ff', colorPrimaryActive: darkMode ? '#d8a93b' : '#0958d9', colorInfo: darkMode ? '#f6c453' : '#1677ff', colorLink: darkMode ? '#ffd666' : '#1677ff', colorLinkHover: darkMode ? '#ffe58f' : '#4096ff', colorLinkActive: darkMode ? '#d8a93b' : '#0958d9', colorPrimaryBg: darkMode ? 'rgba(246, 196, 83, 0.22)' : '#e6f4ff', colorPrimaryBgHover: darkMode ? 'rgba(246, 196, 83, 0.30)' : '#bae0ff', colorPrimaryBorder: darkMode ? 'rgba(246, 196, 83, 0.45)' : '#91caff', colorPrimaryBorderHover: darkMode ? 'rgba(246, 196, 83, 0.60)' : '#69b1ff', controlItemBgActive: darkMode ? 'rgba(246, 196, 83, 0.20)' : 'rgba(22, 119, 255, 0.12)', controlItemBgActiveHover: darkMode ? 'rgba(246, 196, 83, 0.28)' : 'rgba(22, 119, 255, 0.18)', controlOutline: darkMode ? 'rgba(246, 196, 83, 0.50)' : 'rgba(5, 145, 255, 0.24)', }, components: { Layout: { bodyBg: 'transparent', headerBg: 'transparent', siderBg: 'transparent', triggerBg: 'transparent' }, Table: { headerBg: 'transparent', rowHoverBg: darkMode ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.02)', }, Tabs: { cardBg: 'transparent', itemActiveColor: darkMode ? '#ffd666' : '#1890ff', itemHoverColor: darkMode ? '#ffe58f' : '#40a9ff', itemSelectedColor: darkMode ? '#ffd666' : '#1677ff', inkBarColor: darkMode ? '#ffd666' : '#1677ff', } } }), [ darkMode, effectiveOpacity, isV2Ui, tokenControlHeight, tokenControlHeightLG, tokenControlHeightSM, tokenFontSize, tokenFontSizeLG, tokenFontSizeSM, resolvedMonoFontFamily, resolvedUiFontFamily, ]); const filterFontOption = useCallback((input: string, option?: { value?: string; label?: React.ReactNode }) => ( matchFontFamilyOption(input, { value: String(option?.value || ''), label: String(option?.label || ''), }) ), []); const renderFontOptionLabel = useCallback((option: FontFamilyOption) => (
{option.label} {option.value}
), [darkMode]); const showLinuxCJKFontBanner = Boolean( linuxCJKFontInstallHint && hasLoadedInstalledFontsRef.current && !isFontFamiliesLoading && !fontFamiliesLoadError && !isLinuxCJKFontBannerDismissed, ); return ( {/* Custom Title Bar */}
{/* Logo can be added here if available */} GoNavi
{useNativeMacWindowControls ? (
) : (
e.stopPropagation()} style={{ display: 'flex', height: '100%', WebkitAppRegion: 'no-drag', '--wails-draggable': 'no-drag' } as any} >
)}
{showLinuxCJKFontBanner && ( { setThemeModalSection('appearance'); setIsThemeModalOpen(true); }} onDismiss={() => setIsLinuxCJKFontBannerDismissed(true)} /> )}
{!isV2Ui && ( <>
{sidebarUtilityItems.map((item) => (
)}
{!connectionWorkbenchState.ready && (
{connectionWorkbenchState.message}
)}
{ event.preventDefault(); event.stopPropagation(); }} role="separator" aria-orientation="vertical" title={t('app.sidebar.resize_width')} style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: sidebarResizeHandleWidth, cursor: 'col-resize', zIndex: 3, touchAction: 'none', userSelect: 'none', WebkitUserSelect: 'none', background: 'transparent', }} />
{/* Floating SQL Log Toggle */} {!isV2Ui && (
)}
{securityUpdateEntryVisibility.showBanner && !isSecurityUpdateBannerDismissed && ( handleOpenSecurityUpdateSettings( hasSecurityUpdateRecentResult(securityUpdateStatus) ? 'recent_result' : null, )} onDismiss={() => setIsSecurityUpdateBannerDismissed(true)} /> )}
{!isV2Ui && !aiPanelVisible && ( <> {aiEntryPlacement === 'content-edge' && legacyAiEdgeHandleAttachment === 'content-shell' && (
{renderLegacyAIEdgeHandle()}
)} )} {aiPanelVisible && (
{aiPanelOverlayActive && (
)} > setAIPanelVisible(false)} onOpenSettings={() => { handleOpenAISettings(); }} overlayTheme={overlayTheme} />
)} {!isV2Ui && isLogPanelOpen && ( )}
{isConnectionModalMounted && ( )} {isToolsModalOpen && (() => { const toolCenterGroups = [ { key: 'config', icon: , title: t('app.tools.group.config.title'), description: t('app.tools.group.config.description'), items: [ { key: 'import', icon: , title: t('app.tools.entry.import.title'), description: t('app.tools.entry.import.description'), onClick: () => { void handleImportConnections('config'); }, }, { key: 'export', icon: , title: t('app.tools.entry.export.title'), description: t('app.tools.entry.export.description'), onClick: () => { void handleExportConnections('config'); }, }, { key: 'data-root', icon: , title: t('app.tools.entry.data_root.title'), description: t('app.tools.entry.data_root.description'), onClick: () => { handleOpenToolCenterPane('config', 'data-root'); }, }, { key: 'security-update', icon: , title: t('app.tools.entry.security_update.title'), description: securityUpdateEntryVisibility.showDetailEntry || securityUpdateHasLegacySensitiveItems ? t('app.tools.entry.security_update.status_description', { status: securityUpdateStatusMeta.label }) : t('app.tools.entry.security_update.description'), onClick: () => { handleOpenToolCenterPane('config', 'security-update'); }, }, ], }, { key: 'workflow', icon: , title: t('app.tools.group.workflow.title'), description: t('app.tools.group.workflow.description'), items: [ { key: 'schema-compare', icon: , title: t('app.tools.entry.schema_compare.title'), description: t('app.tools.entry.schema_compare.description'), onClick: () => { setSyncModalEntryMode('schemaCompare'); handleOpenToolCenterPane('workflow', 'schema-compare'); }, }, { key: 'data-compare', icon: , title: t('app.tools.entry.data_compare.title'), description: t('app.tools.entry.data_compare.description'), onClick: () => { setSyncModalEntryMode('dataCompare'); handleOpenToolCenterPane('workflow', 'data-compare'); }, }, { key: 'sync', icon: , title: t('app.tools.entry.sync.title'), description: t('app.tools.entry.sync.description'), onClick: () => { setSyncModalEntryMode('sync'); handleOpenToolCenterPane('workflow', 'sync'); }, }, ], }, { key: 'workspace', icon: , title: t('app.tools.group.workspace.title'), description: t('app.tools.group.workspace.description'), items: [ { key: 'drivers', icon: , title: t('app.tools.entry.drivers.title'), description: t('app.tools.entry.drivers.description'), onClick: () => { handleOpenToolCenterPane('workspace', 'drivers'); }, }, { key: 'snippet-settings', icon: , title: t('app.tools.entry.snippets.title'), description: t('app.tools.entry.snippets.description'), onClick: () => { handleOpenToolCenterPane('workspace', 'snippet-settings'); }, }, { key: 'shortcut-settings', icon: , title: t('app.tools.entry.shortcuts.title'), description: t('app.tools.entry.shortcuts.description'), onClick: () => { handleOpenToolCenterPane('workspace', 'shortcut-settings'); }, }, ], }, ] as const; const activeToolCenterGroup = toolCenterGroups.find((group) => group.key === activeToolCenterGroupKey) ?? toolCenterGroups[0]; const activeToolCenterPaneItem = activeToolCenterPane ? toolCenterGroups .find((group) => group.key === activeToolCenterPane.group) ?.items.find((item) => item.key === activeToolCenterPane.key) : null; const closeToolCenterPane = () => { if (activeToolCenterPane?.key === 'connection-package') { closeConnectionPackageDialog(); return; } setToolCenterBackGroupKey(null); setActiveToolCenterPane(null); }; const renderToolCenterPane = () => { if (!activeToolCenterPane) { return null; } if (activeToolCenterPane.key === 'connection-package') { return ( { setConnectionPackageDialog((current) => ({ ...current, includeSecrets: value, useFilePassword: value ? current.useFilePassword : false, password: value ? current.password : '', error: '', })); }} onUseFilePasswordChange={(value) => { setConnectionPackageDialog((current) => ({ ...current, useFilePassword: value, password: value ? current.password : '', error: '', })); }} onPasswordChange={(value) => { setConnectionPackageDialog((current) => ({ ...current, password: value, error: '', })); }} onConfirm={() => { void handleConfirmConnectionPackageDialog(); }} onCancel={closeConnectionPackageDialog} /> ); } if (activeToolCenterPane.key === 'data-root') { return ( {t('common.close')} , , ]} styles={{ header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 }, }} > {dataRootLoading ? (
) : (
{t('app.data_root.current_directory')}
{t('app.data_root.default_directory')}
{dataRootInfo?.defaultPath || '-'}
{t('app.data_root.driver_directory')}
{dataRootInfo?.driverPath || '-'}
{t('app.data_root.switch_target')}
{t('app.data_root.apply_method')}
{t('app.data_root.restart_hint')}
)}
); } if (activeToolCenterPane.key === 'security-update') { return ( ); } if ( activeToolCenterPane.key === 'schema-compare' || activeToolCenterPane.key === 'data-compare' || activeToolCenterPane.key === 'sync' ) { return ( ); } if (activeToolCenterPane.key === 'drivers') { return ( ); } if (activeToolCenterPane.key === 'snippet-settings') { return ( ); } if (activeToolCenterPane.key === 'shortcut-settings') { return ( { setCapturingShortcutAction(null); closeToolCenterPane(); }} footer={[ , , , ]} styles={{ header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8, overflow: 'hidden', flex: 1, minHeight: 0 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 }, }} >
{t('app.shortcuts.capture_hint')}
{SHORTCUT_ACTION_ORDER.map((action) => { const meta = SHORTCUT_ACTION_META[action]; if (meta.platformOnly === 'mac' && !isMacRuntime) { return null; } const binding = resolveShortcutBinding(shortcutOptions, action, activeShortcutPlatform); const isCapturing = capturingShortcutAction === action; const conflicts = shortcutConflictMap[action]; const conflictInfo = conflicts?.length ? splitConflictsByContext(conflicts) : null; return (
{meta.label}
{meta.description}
{conflictInfo && (
{conflictInfo.hasMonaco && ( <>⚠ {t('app.shortcuts.message.reserved_conflict_info', { labels: conflictInfo.monacoLabels })} )} {conflictInfo.hasOther && ( <>⚠ {t('app.shortcuts.message.reserved_conflict_warning', { contexts: conflictInfo.otherContexts, labels: conflictInfo.otherLabels })} )}
)}
updateShortcut(action, { enabled: checked }, activeShortcutPlatform)} />
); })}
); } return null; }; return ( , t('app.tools.title'), t('app.tools.description'))} open={isToolsModalOpen} onCancel={() => { if (activeToolCenterPane?.key === 'connection-package') { closeConnectionPackageDialog(); } setActiveToolCenterPane(null); setToolCenterBackGroupKey(null); setIsToolsModalOpen(false); }} footer={null} centered width={1080} styles={{ content: toolCenterModalContentStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8, paddingBottom: 8, overflow: 'hidden', flex: 1, minHeight: 0 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 }, }} >
{toolCenterGroups.map((group) => { const active = group.key === activeToolCenterGroup.key; return ( ); })}
{activeToolCenterPane ? (
{activeToolCenterPaneItem?.title ?? activeToolCenterGroup.title}
{activeToolCenterPaneItem?.description ?? activeToolCenterGroup.description}
{renderToolCenterPane()}
) : ( <>
{activeToolCenterGroup.title}
{activeToolCenterGroup.description}
{activeToolCenterGroup.items.map((item, index) => ( ))}
)}
); })()} {isSettingsModalOpen && ( , t('app.settings.title'), t('app.settings.description'))} open={isSettingsModalOpen} onCancel={() => setIsSettingsModalOpen(false)} footer={null} width={560} styles={{ content: utilityModalShellStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 } }} >
{[ { key: 'language', icon: , title: t('settings.language.title'), description: t('settings.language.description'), onClick: () => { setIsSettingsModalOpen(false); setIsLanguageModalOpen(true); }, }, { key: 'theme', icon: , title: t('app.settings.entry.theme.title'), description: t('app.settings.entry.theme.description'), onClick: () => { setIsSettingsModalOpen(false); setThemeModalSection('theme'); setIsThemeModalOpen(true); }, }, { key: 'proxy', icon: , title: t('app.settings.entry.proxy.title'), description: t('app.settings.entry.proxy.description'), onClick: () => { setIsSettingsModalOpen(false); setSecurityUpdateRepairSource(null); setIsProxyModalOpen(true); }, }, { key: 'ai', icon: , title: t('app.settings.entry.ai.title'), description: t('app.settings.entry.ai.description'), onClick: () => { setIsSettingsModalOpen(false); handleOpenAISettings(); }, }, { key: 'about', icon: , title: t('app.settings.entry.about.title'), description: t('app.settings.entry.about.description'), onClick: () => { setIsSettingsModalOpen(false); setIsAboutOpen(true); }, }, ].map((item) => ( ))}
)} {isLanguageModalOpen && ( , t('settings.language.title'), t('settings.language.description'))} open={isLanguageModalOpen} onCancel={() => setIsLanguageModalOpen(false)} footer={null} width={520} styles={{ content: utilityModalShellStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 } }} > )} {isDataRootModalOpen && ( , t('app.data_root.title'), t('app.data_root.description'), )} open={isDataRootModalOpen} onCancel={() => { setIsDataRootModalOpen(false); setToolCenterBackGroupKey(null); }} footer={[ , toolCenterBackGroupKey === 'config' ? ( ) : null, ]} width={720} styles={{ content: utilityModalShellStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 } }} > {dataRootLoading ? (
) : (
{t('app.data_root.current_directory')}
{t('app.data_root.default_directory')}
{dataRootInfo?.defaultPath || '-'}
{t('app.data_root.driver_directory')}
{dataRootInfo?.driverPath || '-'}
{t('app.data_root.switch_target')}
{t('app.data_root.apply_method')}
{t('app.data_root.restart_hint')}
)}
)} {isSyncModalOpen && ( { setIsSyncModalOpen(false); setToolCenterBackGroupKey(null); }} onBack={toolCenterBackGroupKey === 'workflow' ? () => handleReturnToToolCenter(() => setIsSyncModalOpen(false)) : undefined} entryMode={syncModalEntryMode} /> )} {isDriverModalOpen && ( handleReturnToToolCenter(() => setIsDriverModalOpen(false)) : undefined} onOpenGlobalProxySettings={handleOpenGlobalProxySettings} /> )} handleOpenSecurityUpdateSettings()} /> { setIsSecurityUpdateSettingsOpen(false); setToolCenterBackGroupKey(null); }} onBack={toolCenterBackGroupKey === 'config' ? () => handleReturnToToolCenter(() => setIsSecurityUpdateSettingsOpen(false)) : undefined} onStart={handleStartSecurityUpdate} onRetry={handleRetrySecurityUpdate} onRestart={handleRestartSecurityUpdate} onIssueAction={handleSecurityUpdateIssueAction} /> {isAISettingsOpen && ( )} handleReturnToToolCenter(closeConnectionPackageDialog) : undefined} onIncludeSecretsChange={(value) => { setConnectionPackageDialog((current) => ({ ...current, includeSecrets: value, useFilePassword: value ? current.useFilePassword : false, password: value ? current.password : '', error: '', })); }} onUseFilePasswordChange={(value) => { setConnectionPackageDialog((current) => ({ ...current, useFilePassword: value, password: value ? current.password : '', error: '', })); }} onPasswordChange={(value) => { setConnectionPackageDialog((current) => ({ ...current, password: value, error: '', })); }} onConfirm={() => { void handleConfirmConnectionPackageDialog(); }} onCancel={closeConnectionPackageDialog} /> , t('app.about.title'), t('app.about.description'))} 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, , , lastUpdateInfo?.hasUpdate && !isLatestUpdateDownloaded && !isBackgroundProgressForLatestUpdate ? ( ) : 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')}
{aboutInfo?.communityUrl ? ( ) : null}
)}
{isThemeModalOpen && ( : , themeModalSection === 'theme' ? t('app.theme.theme_settings_title') : t('app.theme.appearance_settings_title'), themeModalSection === 'theme' ? t('app.theme.theme_settings_description') : t('app.theme.appearance_settings_description') )} open={isThemeModalOpen} onCancel={() => { setIsThemeModalOpen(false); setThemeModalSection('theme'); }} footer={null} width={820} styles={{ content: utilityModalShellStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8, height: 620, overflow: 'hidden' }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 } }} >
{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') }, ].map((item) => { const active = themeMode === 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
{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.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 ? (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 ? (darkMode ? '0 0 0 2px rgba(255,214,102,0.14)' : '0 0 0 2px rgba(24,144,255,0.12)') : 'none', background: isFocused ? (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')}
)}
)} {isShortcutModalOpen && ( , t('app.shortcuts.title'), t('app.shortcuts.description'), )} open={isShortcutModalOpen} onCancel={() => { setIsShortcutModalOpen(false); setCapturingShortcutAction(null); setToolCenterBackGroupKey(null); }} width={760} centered style={{ top: 0, maxHeight: 'calc(100vh - 80px)' }} styles={{ content: { ...utilityModalShellStyle, height: 'min(760px, calc(100vh - 80px))', display: 'flex', flexDirection: 'column', }, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8, overflow: 'hidden', flex: 1, minHeight: 0 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 } }} footer={[ , , toolCenterBackGroupKey === 'workspace' ? ( ) : null, ]} >
{t('app.shortcuts.capture_hint')}
{SHORTCUT_ACTION_ORDER.map((action) => { const meta = SHORTCUT_ACTION_META[action]; if (meta.platformOnly === 'mac' && !isMacRuntime) { return null; } const binding = resolveShortcutBinding(shortcutOptions, action, activeShortcutPlatform); const isCapturing = capturingShortcutAction === action; const conflicts = shortcutConflictMap[action]; const conflictInfo = conflicts?.length ? splitConflictsByContext(conflicts) : null; return (
{meta.label}
{meta.description}
{conflictInfo && (
{conflictInfo.hasMonaco && ( <>⚠ {t('app.shortcuts.message.reserved_conflict_info', { labels: conflictInfo.monacoLabels })} )} {conflictInfo.hasOther && ( <>⚠ {t('app.shortcuts.message.reserved_conflict_warning', { contexts: conflictInfo.otherContexts, labels: conflictInfo.otherLabels })} )}
)}
updateShortcut(action, { enabled: checked }, activeShortcutPlatform)} />
); })}
)} {isSnippetModalOpen && ( { setIsSnippetModalOpen(false); setToolCenterBackGroupKey(null); }} onBack={toolCenterBackGroupKey === 'workspace' ? () => handleReturnToToolCenter(() => setIsSnippetModalOpen(false)) : undefined} darkMode={darkMode} overlayTheme={overlayTheme} /> )} {isProxyModalOpen && ( , t('app.proxy.title'), t('app.proxy.description'))} open={isProxyModalOpen} onCancel={handleCloseGlobalProxySettings} footer={null} width={520} styles={{ content: utilityModalShellStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 } }} >
{t('app.proxy.section_title')}
{t('app.proxy.enable')} setGlobalProxy({ enabled: checked })} />
{t('app.proxy.type')}
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 })} />
{t('app.proxy.scope_hint')}
)} { markUpdateProgressDismissed(); hideUpdateDownloadProgress(); }} > {t('app.about.action.hide_to_background')} ] : (updateDownloadProgress.status === 'done' ? [ , ] : (updateDownloadProgress.status === 'error' ? [ ] : null))} >
{`${formatBytes(updateDownloadProgress.downloaded)} / ${formatBytes(updateDownloadProgress.total)}`}
{updateDownloadProgress.message ? (
{updateDownloadProgress.message}
) : null}
{showLinuxResizeHandles && ( <> {/* Linux Mint 下 frameless 仅局部可缩放:补四边四角命中层 */}
)} {/* Ghost Resize Line for Sidebar */}
{/* Ghost Resize Line for Log Panel */}
); } export default App;