diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index eee15b6..c189de5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { Layout, Button, ConfigProvider, theme, Dropdown, MenuProps, message, Modal, Spin, Slider, Progress, Switch, Input, InputNumber, Select } from 'antd'; import zhCN from 'antd/locale/zh_CN'; -import { PlusOutlined, BulbOutlined, BulbFilled, ConsoleSqlOutlined, UploadOutlined, DownloadOutlined, CloudDownloadOutlined, BugOutlined, ToolOutlined, GlobalOutlined, InfoCircleOutlined, GithubOutlined, SkinOutlined, CheckOutlined, MinusOutlined, BorderOutlined, CloseOutlined, SettingOutlined, LinkOutlined } from '@ant-design/icons'; +import { PlusOutlined, BulbOutlined, BulbFilled, ConsoleSqlOutlined, UploadOutlined, DownloadOutlined, CloudDownloadOutlined, BugOutlined, ToolOutlined, GlobalOutlined, InfoCircleOutlined, GithubOutlined, SkinOutlined, CheckOutlined, MinusOutlined, BorderOutlined, CloseOutlined, SettingOutlined } from '@ant-design/icons'; import { Environment, EventsOn, WindowFullscreen, WindowIsFullscreen, WindowIsMaximised, WindowMaximise } from '../wailsjs/runtime/runtime'; import Sidebar from './components/Sidebar'; import TabManager from './components/TabManager'; @@ -18,1256 +18,1253 @@ import './App.css'; const { Sider, Content } = Layout; function App() { - const [isModalOpen, setIsModalOpen] = useState(false); - const [isSyncModalOpen, setIsSyncModalOpen] = useState(false); - const [isDriverModalOpen, setIsDriverModalOpen] = useState(false); - const [editingConnection, setEditingConnection] = useState(null); - 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 startupFullscreen = useStore(state => state.startupFullscreen); - const setStartupFullscreen = useStore(state => state.setStartupFullscreen); - const globalProxy = useStore(state => state.globalProxy); - const setGlobalProxy = useStore(state => state.setGlobalProxy); - const darkMode = themeMode === 'dark'; - const effectiveOpacity = normalizeOpacityForPlatform(appearance.opacity); - const effectiveBlur = normalizeBlurForPlatform(appearance.blur); - const blurFilter = blurToFilter(effectiveBlur); - const windowCornerRadius = 14; - const [isLinuxRuntime, setIsLinuxRuntime] = useState(false); - const [isStoreHydrated, setIsStoreHydrated] = useState(() => useStore.persist.hasHydrated()); - const globalProxyInvalidHintShownRef = React.useRef(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isSyncModalOpen, setIsSyncModalOpen] = useState(false); + const [isDriverModalOpen, setIsDriverModalOpen] = useState(false); + const [editingConnection, setEditingConnection] = useState(null); + 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 startupFullscreen = useStore(state => state.startupFullscreen); + const setStartupFullscreen = useStore(state => state.setStartupFullscreen); + const globalProxy = useStore(state => state.globalProxy); + const setGlobalProxy = useStore(state => state.setGlobalProxy); + const darkMode = themeMode === 'dark'; + const effectiveOpacity = normalizeOpacityForPlatform(appearance.opacity); + const effectiveBlur = normalizeBlurForPlatform(appearance.blur); + const blurFilter = blurToFilter(effectiveBlur); + const windowCornerRadius = 14; + const [isLinuxRuntime, setIsLinuxRuntime] = useState(false); + const [isStoreHydrated, setIsStoreHydrated] = useState(() => useStore.persist.hasHydrated()); + const globalProxyInvalidHintShownRef = React.useRef(false); - // 同步 macOS 窗口透明度:opacity=1.0 且 blur=0 时关闭 NSVisualEffectView, - // 避免 GPU 持续计算窗口背后的模糊合成 - useEffect(() => { - SetWindowTranslucency(appearance.opacity, appearance.blur).catch(() => { }); - }, [appearance.opacity, appearance.blur]); + // 同步 macOS 窗口透明度:opacity=1.0 且 blur=0 时关闭 NSVisualEffectView, + // 避免 GPU 持续计算窗口背后的模糊合成 + useEffect(() => { + SetWindowTranslucency(appearance.opacity, appearance.blur).catch(() => {}); + }, [appearance.opacity, appearance.blur]); - useEffect(() => { - let cancelled = false; - Environment() - .then((env) => { - if (cancelled) return; - setIsLinuxRuntime((env?.platform || '').toLowerCase() === 'linux'); - }) - .catch(() => { - if (cancelled) return; - const platform = typeof navigator !== 'undefined' ? navigator.platform : ''; - setIsLinuxRuntime(/linux/i.test(platform)); - }); - return () => { - cancelled = true; - }; - }, []); + useEffect(() => { + let cancelled = false; + Environment() + .then((env) => { + if (cancelled) return; + setIsLinuxRuntime((env?.platform || '').toLowerCase() === 'linux'); + }) + .catch(() => { + if (cancelled) return; + const platform = typeof navigator !== 'undefined' ? navigator.platform : ''; + setIsLinuxRuntime(/linux/i.test(platform)); + }); + return () => { + cancelled = true; + }; + }, []); - useEffect(() => { - if (isStoreHydrated) { - return; - } - const unsubscribe = useStore.persist.onFinishHydration(() => { - setIsStoreHydrated(true); - }); - return () => { - unsubscribe(); - }; - }, [isStoreHydrated]); + useEffect(() => { + if (isStoreHydrated) { + return; + } + const unsubscribe = useStore.persist.onFinishHydration(() => { + setIsStoreHydrated(true); + }); + return () => { + unsubscribe(); + }; + }, [isStoreHydrated]); - useEffect(() => { - if (!isStoreHydrated) { - return; - } + useEffect(() => { + if (!isStoreHydrated) { + 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); + 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) { - message.warning({ - content: '全局代理已开启,但地址或端口无效,当前按未启用处理', - key: 'global-proxy-invalid', - }); - globalProxyInvalidHintShownRef.current = true; - } - } else { - globalProxyInvalidHintShownRef.current = false; - message.destroy('global-proxy-invalid'); - } + if (invalidWhenEnabled) { + if (!globalProxyInvalidHintShownRef.current) { + message.warning({ + content: '全局代理已开启,但地址或端口无效,当前按未启用处理', + key: 'global-proxy-invalid', + }); + globalProxyInvalidHintShownRef.current = true; + } + } else { + globalProxyInvalidHintShownRef.current = false; + message.destroy('global-proxy-invalid'); + } - const enabledForBackend = globalProxy.enabled && !invalidWhenEnabled; - let cancelled = false; - ConfigureGlobalProxy(enabledForBackend, { - type: globalProxy.type, - host, - port: portValid ? port : (globalProxy.type === 'http' ? 8080 : 1080), - user: String(globalProxy.user || '').trim(), - password: globalProxy.password || '', - }) - .then((res) => { - if (cancelled || res?.success) { - return; - } - message.error({ - content: '全局代理配置失败: ' + (res?.message || '未知错误'), - key: 'global-proxy-sync-error', - }); - }) - .catch((err) => { - if (cancelled) { - return; - } - const errMsg = err instanceof Error ? err.message : String(err || '未知错误'); - message.error({ - content: '全局代理配置失败: ' + errMsg, - key: 'global-proxy-sync-error', - }); - }); + const enabledForBackend = globalProxy.enabled && !invalidWhenEnabled; + let cancelled = false; + ConfigureGlobalProxy(enabledForBackend, { + type: globalProxy.type, + host, + port: portValid ? port : (globalProxy.type === 'http' ? 8080 : 1080), + user: String(globalProxy.user || '').trim(), + password: globalProxy.password || '', + }) + .then((res) => { + if (cancelled || res?.success) { + return; + } + message.error({ + content: '全局代理配置失败: ' + (res?.message || '未知错误'), + key: 'global-proxy-sync-error', + }); + }) + .catch((err) => { + if (cancelled) { + return; + } + const errMsg = err instanceof Error ? err.message : String(err || '未知错误'); + message.error({ + content: '全局代理配置失败: ' + errMsg, + key: 'global-proxy-sync-error', + }); + }); - return () => { - cancelled = true; - }; - }, [ - isStoreHydrated, - globalProxy.enabled, - globalProxy.type, - globalProxy.host, - globalProxy.port, - globalProxy.user, - globalProxy.password, - ]); + return () => { + cancelled = true; + }; + }, [ + isStoreHydrated, + globalProxy.enabled, + globalProxy.type, + globalProxy.host, + globalProxy.port, + globalProxy.user, + globalProxy.password, + ]); - useEffect(() => { - let cancelled = false; - let startupWindowTimer: number | null = null; - const maxApplyAttempts = 6; - const applyRetryDelayMs = 400; - const settleDelayMs = 160; + useEffect(() => { + let cancelled = false; + let startupWindowTimer: number | null = null; + const maxApplyAttempts = 6; + const applyRetryDelayMs = 400; + const settleDelayMs = 160; - const checkStartupPreferenceApplied = async (): Promise => { - try { - if (await WindowIsFullscreen()) { - return true; - } - } catch (_) { - // ignore - } - try { - if (await WindowIsMaximised()) { - return true; - } - } catch (_) { - // ignore - } - return false; - }; + 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; - } - Promise.resolve() - .then(async () => { - if (await checkStartupPreferenceApplied()) { - return; - } - // 优先尝试全屏,若当前平台/时机不生效,后续走最大化兜底。 - WindowFullscreen(); - await new Promise((resolve) => window.setTimeout(resolve, settleDelayMs)); - if (await checkStartupPreferenceApplied()) { - return; - } - WindowMaximise(); - await new Promise((resolve) => window.setTimeout(resolve, settleDelayMs)); - if (await checkStartupPreferenceApplied()) { - return; - } - if (attempt < maxApplyAttempts) { - applyStartupWindowPreference(attempt + 1); - } - }); - }, 300); - }; + const applyStartupWindowPreference = (attempt: number) => { + if (startupWindowTimer !== null) { + window.clearTimeout(startupWindowTimer); + } + startupWindowTimer = window.setTimeout(() => { + if (cancelled) { + return; + } + if (!useStore.getState().startupFullscreen) { + return; + } + Promise.resolve() + .then(async () => { + if (await checkStartupPreferenceApplied()) { + return; + } + // 优先尝试全屏,若当前平台/时机不生效,后续走最大化兜底。 + WindowFullscreen(); + await new Promise((resolve) => window.setTimeout(resolve, settleDelayMs)); + if (await checkStartupPreferenceApplied()) { + return; + } + WindowMaximise(); + await new Promise((resolve) => window.setTimeout(resolve, settleDelayMs)); + if (await checkStartupPreferenceApplied()) { + return; + } + if (attempt < maxApplyAttempts) { + applyStartupWindowPreference(attempt + 1); + } + }); + }, 300); + }; - if (useStore.persist.hasHydrated()) { - applyStartupWindowPreference(1); - } - const unsubscribeHydration = useStore.persist.onFinishHydration(() => { - if (cancelled) { - return; - } - applyStartupWindowPreference(1); - }); + if (useStore.persist.hasHydrated()) { + applyStartupWindowPreference(1); + } + const unsubscribeHydration = useStore.persist.onFinishHydration(() => { + if (cancelled) { + return; + } + applyStartupWindowPreference(1); + }); - return () => { - cancelled = true; - if (startupWindowTimer !== null) { - window.clearTimeout(startupWindowTimer); - } - unsubscribeHydration(); - }; - }, []); + return () => { + cancelled = true; + if (startupWindowTimer !== null) { + window.clearTimeout(startupWindowTimer); + } + unsubscribeHydration(); + }; + }, []); - // Background Helper - const getBg = (darkHex: string, lightHex: string) => { - if (!darkMode) return `rgba(255, 255, 255, ${effectiveOpacity})`; // Light mode usually white + // Background Helper + const getBg = (darkHex: string, lightHex: string) => { + if (!darkMode) return `rgba(255, 255, 255, ${effectiveOpacity})`; // Light mode usually white + + // Parse hex to rgb + const hex = darkHex.replace('#', ''); + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + return `rgba(${r}, ${g}, ${b}, ${effectiveOpacity})`; + }; + // Specific colors + const bgMain = getBg('#141414', '#ffffff'); + const bgContent = getBg('#1d1d1d', '#ffffff'); + + const addTab = useStore(state => state.addTab); + const activeContext = useStore(state => state.activeContext); + const connections = useStore(state => state.connections); + const addConnection = useStore(state => state.addConnection); + const tabs = useStore(state => state.tabs); + const activeTabId = useStore(state => state.activeTabId); + const updateCheckInFlightRef = React.useRef(false); + const updateDownloadInFlightRef = React.useRef(false); + const updateDownloadedVersionRef = React.useRef(null); + const updateDownloadMetaRef = React.useRef(null); + const updateDeferredVersionRef = React.useRef(null); + const updateNotifiedVersionRef = React.useRef(null); + const updateMutedVersionRef = React.useRef(null); + const [isAboutOpen, setIsAboutOpen] = useState(false); + const [aboutLoading, setAboutLoading] = useState(false); + const [aboutInfo, setAboutInfo] = useState<{ version: string; author: string; buildTime?: string; repoUrl?: string; issueUrl?: string; releaseUrl?: string } | null>(null); + const [aboutUpdateStatus, setAboutUpdateStatus] = useState(''); + const [lastUpdateInfo, setLastUpdateInfo] = useState(null); + const [updateDownloadProgress, setUpdateDownloadProgress] = useState<{ + open: boolean; + version: string; + status: 'idle' | 'start' | 'downloading' | 'done' | 'error'; + percent: number; + downloaded: number; + total: number; + message: string; + }>({ + open: false, + version: '', + status: 'idle', + percent: 0, + downloaded: 0, + total: 0, + message: '' + }); - // Parse hex to rgb - const hex = darkHex.replace('#', ''); - const r = parseInt(hex.substring(0, 2), 16); - const g = parseInt(hex.substring(2, 4), 16); - const b = parseInt(hex.substring(4, 6), 16); - return `rgba(${r}, ${g}, ${b}, ${effectiveOpacity})`; - }; - // Specific colors - const bgMain = getBg('#141414', '#ffffff'); - const bgContent = getBg('#1d1d1d', '#ffffff'); + type UpdateInfo = { + hasUpdate: boolean; + currentVersion: string; + latestVersion: string; + releaseName?: string; + releaseNotesUrl?: string; + assetName?: string; + assetUrl?: string; + assetSize?: number; + sha256?: string; + downloaded?: boolean; + downloadPath?: string; + }; - const addTab = useStore(state => state.addTab); - const activeContext = useStore(state => state.activeContext); - const connections = useStore(state => state.connections); - const addConnection = useStore(state => state.addConnection); - const tabs = useStore(state => state.tabs); - const activeTabId = useStore(state => state.activeTabId); - const updateCheckInFlightRef = React.useRef(false); - const updateDownloadInFlightRef = React.useRef(false); - const updateDownloadedVersionRef = React.useRef(null); - const updateDownloadMetaRef = React.useRef(null); - const updateDeferredVersionRef = React.useRef(null); - const updateNotifiedVersionRef = React.useRef(null); - const updateMutedVersionRef = React.useRef(null); - const [isAboutOpen, setIsAboutOpen] = useState(false); - const [aboutLoading, setAboutLoading] = useState(false); - const [aboutInfo, setAboutInfo] = useState<{ version: string; author: string; buildTime?: string; repoUrl?: string; issueUrl?: string; releaseUrl?: string; communityUrl?: string } | null>(null); - const [aboutUpdateStatus, setAboutUpdateStatus] = useState(''); - const [lastUpdateInfo, setLastUpdateInfo] = useState(null); - const [updateDownloadProgress, setUpdateDownloadProgress] = useState<{ - open: boolean; - version: string; - status: 'idle' | 'start' | 'downloading' | 'done' | 'error'; - percent: number; - downloaded: number; - total: number; - message: string; - }>({ - open: false, - version: '', - status: 'idle', - percent: 0, - downloaded: 0, - total: 0, - message: '' - }); + type UpdateDownloadProgressEvent = { + status?: 'start' | 'downloading' | 'done' | 'error'; + percent?: number; + downloaded?: number; + total?: number; + message?: string; + }; - type UpdateInfo = { - hasUpdate: boolean; - currentVersion: string; - latestVersion: string; - releaseName?: string; - releaseNotesUrl?: string; - assetName?: string; - assetUrl?: string; - assetSize?: number; - sha256?: string; - downloaded?: boolean; - downloadPath?: string; - }; + type UpdateDownloadResultData = { + info?: UpdateInfo; + downloadPath?: string; + installLogPath?: string; + installTarget?: string; + platform?: string; + autoRelaunch?: boolean; + }; - type UpdateDownloadProgressEvent = { - status?: 'start' | 'downloading' | 'done' | 'error'; - percent?: number; - downloaded?: number; - total?: number; - message?: string; - }; + const formatBytes = (bytes?: number) => { + if (!bytes || bytes <= 0) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let value = bytes; + let idx = 0; + while (value >= 1024 && idx < units.length - 1) { + value /= 1024; + idx++; + } + return `${value.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`; + }; - type UpdateDownloadResultData = { - info?: UpdateInfo; - downloadPath?: string; - installLogPath?: string; - installTarget?: string; - platform?: string; - autoRelaunch?: boolean; - }; + const promptRestartForUpdate = (info: UpdateInfo, resultData?: UpdateDownloadResultData) => { + const downloadPathHint = resultData?.downloadPath + ? `更新包路径:${resultData.downloadPath}` + : ''; + const installLogHint = resultData?.installLogPath + ? `安装日志:${resultData.installLogPath}` + : ''; + Modal.confirm({ + title: '更新已下载', + content: ( +
+
{`版本 ${info.latestVersion} 已下载完成,是否现在重启完成更新?`}
+ {downloadPathHint ?
{downloadPathHint}
: null} + {installLogHint ?
{installLogHint}
: null} +
+ ), + okText: '立即重启', + cancelText: '稍后', + onOk: async () => { + updateDeferredVersionRef.current = null; + const res = await (window as any).go.app.App.InstallUpdateAndRestart(); + if (!res?.success) { + message.error('更新安装失败: ' + (res?.message || '未知错误')); + } + }, + onCancel: () => { + updateDeferredVersionRef.current = info.latestVersion; + } + }); + }; - const formatBytes = (bytes?: number) => { - if (!bytes || bytes <= 0) return '0 B'; - const units = ['B', 'KB', 'MB', 'GB', 'TB']; - let value = bytes; - let idx = 0; - while (value >= 1024 && idx < units.length - 1) { - value /= 1024; - idx++; - } - return `${value.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`; - }; + const downloadUpdate = React.useCallback(async (info: UpdateInfo, silent: boolean) => { + if (updateDownloadInFlightRef.current) return; + if (updateDownloadedVersionRef.current === info.latestVersion) { + if (!silent) { + const cachedDownloadPath = updateDownloadMetaRef.current?.downloadPath; + message.info(cachedDownloadPath ? `更新包已就绪(${info.latestVersion}),路径:${cachedDownloadPath}` : `更新包已就绪(${info.latestVersion})`); + } + if (!silent || updateDeferredVersionRef.current !== info.latestVersion) { + promptRestartForUpdate(info, updateDownloadMetaRef.current || undefined); + } + return; + } + updateDownloadInFlightRef.current = true; + updateDownloadMetaRef.current = null; + const key = 'update-download'; + setUpdateDownloadProgress({ + open: true, + version: info.latestVersion, + status: 'start', + percent: 0, + downloaded: 0, + total: info.assetSize || 0, + message: '' + }); + message.loading({ content: `正在下载更新 ${info.latestVersion}...`, key, duration: 0 }); + const res = await (window as any).go.app.App.DownloadUpdate(); + updateDownloadInFlightRef.current = false; + if (res?.success) { + const resultData = (res?.data || {}) as UpdateDownloadResultData; + updateDownloadMetaRef.current = resultData; + updateDownloadedVersionRef.current = info.latestVersion; + setUpdateDownloadProgress(prev => ({ ...prev, status: 'done', percent: 100, open: false })); + if (resultData?.downloadPath) { + message.success({ content: `更新下载完成,更新包路径:${resultData.downloadPath}`, key, duration: 5 }); + } else { + message.success({ content: '更新下载完成', key, duration: 2 }); + } + setAboutUpdateStatus(`发现新版本 ${info.latestVersion}(已下载,待重启安装)`); + if (!silent || updateDeferredVersionRef.current !== info.latestVersion) { + promptRestartForUpdate(info, resultData); + } + } else { + setUpdateDownloadProgress(prev => ({ + ...prev, + status: 'error', + message: res?.message || '未知错误' + })); + message.error({ content: '更新下载失败: ' + (res?.message || '未知错误'), key, duration: 4 }); + } + }, []); - const promptRestartForUpdate = (info: UpdateInfo, resultData?: UpdateDownloadResultData) => { - const downloadPathHint = resultData?.downloadPath - ? `更新包路径:${resultData.downloadPath}` - : ''; - const installLogHint = resultData?.installLogPath - ? `安装日志:${resultData.installLogPath}` - : ''; - Modal.confirm({ - title: '更新已下载', - content: ( -
-
{`版本 ${info.latestVersion} 已下载完成,是否现在重启完成更新?`}
- {downloadPathHint ?
{downloadPathHint}
: null} - {installLogHint ?
{installLogHint}
: null} -
- ), - okText: '立即重启', - cancelText: '稍后', - onOk: async () => { - updateDeferredVersionRef.current = null; - const res = await (window as any).go.app.App.InstallUpdateAndRestart(); - if (!res?.success) { - message.error('更新安装失败: ' + (res?.message || '未知错误')); - } + const checkForUpdates = React.useCallback(async (silent: boolean) => { + if (updateCheckInFlightRef.current) return; + updateCheckInFlightRef.current = true; + if (!silent) { + setAboutUpdateStatus('正在检查更新...'); + } + const res = await (window as any).go.app.App.CheckForUpdates(); + updateCheckInFlightRef.current = false; + if (!res?.success) { + if (!silent) { + message.error('检查更新失败: ' + (res?.message || '未知错误')); + setAboutUpdateStatus('检查更新失败: ' + (res?.message || '未知错误')); + } + return; + } + const info: UpdateInfo = res.data; + if (!info) return; + setLastUpdateInfo(info); + if (info.hasUpdate) { + const localDownloaded = updateDownloadedVersionRef.current === info.latestVersion; + const hasDownloaded = Boolean(info.downloaded) || localDownloaded; + if (hasDownloaded) { + const downloadPath = info.downloadPath || updateDownloadMetaRef.current?.downloadPath || ''; + updateDownloadedVersionRef.current = info.latestVersion; + updateDownloadMetaRef.current = { + ...(updateDownloadMetaRef.current || {}), + info, + downloadPath: downloadPath || undefined, + }; + } else { + if (updateDownloadedVersionRef.current !== info.latestVersion) { + updateDownloadMetaRef.current = null; + } + } + const statusText = hasDownloaded + ? `发现新版本 ${info.latestVersion}(已下载,待重启安装)` + : `发现新版本 ${info.latestVersion}(未下载)`; + if (!silent) { + message.info(`发现新版本 ${info.latestVersion}`); + setAboutUpdateStatus(statusText); + } + if (silent && isAboutOpen) { + setAboutUpdateStatus(statusText); + } + if (silent && !isAboutOpen && updateMutedVersionRef.current !== info.latestVersion && updateNotifiedVersionRef.current !== info.latestVersion) { + updateNotifiedVersionRef.current = info.latestVersion; + setIsAboutOpen(true); + } + } else if (!silent) { + const text = `当前已是最新版本(${info.currentVersion || '未知'})`; + message.success(text); + setAboutUpdateStatus(text); + } else if (silent && isAboutOpen) { + const text = `当前已是最新版本(${info.currentVersion || '未知'})`; + setAboutUpdateStatus(text); + } + }, [downloadUpdate]); + + const loadAboutInfo = React.useCallback(async () => { + setAboutLoading(true); + const res = await (window as any).go.app.App.GetAppInfo(); + if (res?.success) { + setAboutInfo(res.data); + } else { + message.error('获取应用信息失败: ' + (res?.message || '未知错误')); + } + setAboutLoading(false); + }, []); + + const handleNewQuery = () => { + let connId = activeContext?.connectionId || ''; + let db = activeContext?.dbName || ''; + + // Priority: Active Tab Context > Sidebar Selection + if (activeTabId) { + const currentTab = tabs.find(t => t.id === activeTabId); + if (currentTab && currentTab.connectionId) { + connId = currentTab.connectionId; + db = currentTab.dbName || ''; + } + } + + addTab({ + id: `query-${Date.now()}`, + title: '新建查询', + type: 'query', + connectionId: connId, + dbName: db, + query: '' + }); + }; + + const handleImportConnections = async () => { + const res = await (window as any).go.app.App.ImportConfigFile(); + if (res.success) { + try { + const imported = JSON.parse(res.data); + if (Array.isArray(imported)) { + let count = 0; + imported.forEach((conn: any) => { + if (!connections.some(c => c.id === conn.id)) { + addConnection(conn); + count++; + } + }); + message.success(`成功导入 ${count} 个连接`); + } else { + message.error("文件格式错误:需要 JSON 数组"); + } + } catch (e) { + message.error("解析 JSON 失败"); + } + } else if (res.message !== "Cancelled") { + message.error("导入失败: " + res.message); + } + }; + + const handleExportConnections = async () => { + if (connections.length === 0) { + message.warning("没有连接可导出"); + return; + } + const res = await (window as any).go.app.App.ExportData(connections, [], "connections", "json"); + if (res.success) { + message.success("导出成功"); + } else if (res.message !== "Cancelled") { + message.error("导出失败: " + res.message); + } + }; + + const toolsMenu: MenuProps['items'] = [ + { + key: 'import', + label: '导入连接配置', + icon: , + onClick: handleImportConnections + }, + { + key: 'export', + label: '导出连接配置', + icon: , + onClick: handleExportConnections + }, + { + key: 'sync', + label: '数据同步', + icon: , + onClick: () => setIsSyncModalOpen(true) + }, + { + key: 'drivers', + label: '驱动管理', + icon: , + onClick: () => setIsDriverModalOpen(true) + } + ]; + + const themeMenu: MenuProps['items'] = [ + { + key: 'light', + label: '亮色主题', + icon: themeMode === 'light' ? : undefined, + onClick: () => setTheme('light') + }, + { + key: 'dark', + label: '暗色主题', + icon: themeMode === 'dark' ? : undefined, + onClick: () => setTheme('dark') + }, + { type: 'divider' }, + { + key: 'settings', + label: '外观设置...', + icon: , + onClick: () => setIsAppearanceModalOpen(true) + } + ]; + + const [isAppearanceModalOpen, setIsAppearanceModalOpen] = useState(false); + const [isProxyModalOpen, setIsProxyModalOpen] = useState(false); + + + // Log Panel: 最小高度按“工具栏 + 1 条日志行(微增)”限制 + const LOG_PANEL_TOOLBAR_HEIGHT = 32; + const LOG_PANEL_SINGLE_ROW_HEIGHT = 39; + const LOG_PANEL_MIN_VISIBLE_ROWS = 1; + const LOG_PANEL_MIN_HEIGHT = LOG_PANEL_TOOLBAR_HEIGHT + (LOG_PANEL_SINGLE_ROW_HEIGHT * LOG_PANEL_MIN_VISIBLE_ROWS); + const LOG_PANEL_MAX_HEIGHT = 800; + const [logPanelHeight, setLogPanelHeight] = useState(Math.max(200, LOG_PANEL_MIN_HEIGHT)); + const [isLogPanelOpen, setIsLogPanelOpen] = useState(false); + const logResizeRef = React.useRef<{ startY: number, startHeight: number } | null>(null); + const logGhostRef = React.useRef(null); + + const handleLogResizeStart = (e: React.MouseEvent) => { + e.preventDefault(); + logResizeRef.current = { startY: e.clientY, startHeight: logPanelHeight }; + + if (logGhostRef.current) { + logGhostRef.current.style.top = `${e.clientY}px`; + logGhostRef.current.style.display = 'block'; + } + + document.addEventListener('mousemove', handleLogResizeMove); + document.addEventListener('mouseup', handleLogResizeUp); + }; + + const handleLogResizeMove = (e: MouseEvent) => { + if (!logResizeRef.current) return; + // Just update ghost line, no state update + if (logGhostRef.current) { + logGhostRef.current.style.top = `${e.clientY}px`; + } + }; + + const handleLogResizeUp = (e: MouseEvent) => { + if (logResizeRef.current) { + const delta = logResizeRef.current.startY - e.clientY; + const newHeight = Math.max( + LOG_PANEL_MIN_HEIGHT, + Math.min(LOG_PANEL_MAX_HEIGHT, logResizeRef.current.startHeight + delta) + ); + setLogPanelHeight(newHeight); + } + + if (logGhostRef.current) { + logGhostRef.current.style.display = 'none'; + } + + logResizeRef.current = null; + document.removeEventListener('mousemove', handleLogResizeMove); + document.removeEventListener('mouseup', handleLogResizeUp); + }; + + const handleEditConnection = (conn: SavedConnection) => { + setEditingConnection(conn); + setIsModalOpen(true); + }; + + const handleCloseModal = () => { + setIsModalOpen(false); + setEditingConnection(null); + }; + + const handleOpenDriverManagerFromConnection = () => { + setIsModalOpen(false); + setEditingConnection(null); + setIsDriverModalOpen(true); + }; + + const handleTitleBarDoubleClick = (e: React.MouseEvent) => { + const target = e.target as HTMLElement | null; + if (target?.closest('[data-no-titlebar-toggle="true"]')) { + return; + } + (window as any).runtime.WindowToggleMaximise(); + }; + + // Sidebar Resizing + const [sidebarWidth, setSidebarWidth] = useState(300); + const sidebarDragRef = React.useRef<{ startX: number, startWidth: number } | null>(null); + const rafRef = React.useRef(null); + const ghostRef = React.useRef(null); + const latestMouseX = React.useRef(0); // Store latest mouse position + + const handleSidebarMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + + if (ghostRef.current) { + ghostRef.current.style.left = `${sidebarWidth}px`; + ghostRef.current.style.display = 'block'; + } + + sidebarDragRef.current = { startX: e.clientX, startWidth: sidebarWidth }; + latestMouseX.current = e.clientX; // Init + document.addEventListener('mousemove', handleSidebarMouseMove); + document.addEventListener('mouseup', handleSidebarMouseUp); + }; + + const handleSidebarMouseMove = (e: MouseEvent) => { + if (!sidebarDragRef.current) return; + + latestMouseX.current = e.clientX; // Always update latest pos + + if (rafRef.current) return; // Schedule once per frame + + rafRef.current = requestAnimationFrame(() => { + if (!sidebarDragRef.current || !ghostRef.current) return; + // Use latestMouseX.current instead of stale closure 'e.clientX' + const delta = latestMouseX.current - sidebarDragRef.current.startX; + const newWidth = Math.max(200, Math.min(600, sidebarDragRef.current.startWidth + delta)); + ghostRef.current.style.left = `${newWidth}px`; + rafRef.current = null; + }); + }; + + const handleSidebarMouseUp = (e: MouseEvent) => { + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + rafRef.current = null; + } + + if (sidebarDragRef.current) { + // Use latest position for final commit too + const delta = e.clientX - sidebarDragRef.current.startX; + const newWidth = Math.max(200, Math.min(600, sidebarDragRef.current.startWidth + delta)); + setSidebarWidth(newWidth); + } + + if (ghostRef.current) { + ghostRef.current.style.display = 'none'; + } + + sidebarDragRef.current = null; + document.removeEventListener('mousemove', handleSidebarMouseMove); + document.removeEventListener('mouseup', handleSidebarMouseUp); + }; + + useEffect(() => { + document.body.style.backgroundColor = 'transparent'; + document.body.style.color = darkMode ? '#ffffff' : '#000000'; + document.body.setAttribute('data-theme', darkMode ? 'dark' : 'light'); + }, [darkMode]); + + useEffect(() => { + if (isAboutOpen) { + if (lastUpdateInfo?.hasUpdate) { + setAboutUpdateStatus(`发现新版本 ${lastUpdateInfo.latestVersion}(未下载)`); + } else if (lastUpdateInfo) { + setAboutUpdateStatus(`当前已是最新版本(${lastUpdateInfo.currentVersion || '未知'})`); + } else { + setAboutUpdateStatus('未检查'); + } + loadAboutInfo(); + } + }, [isAboutOpen, lastUpdateInfo, loadAboutInfo]); + + useEffect(() => { + const startupTimer = window.setTimeout(() => { + checkForUpdates(true); + }, 2000); + const interval = window.setInterval(() => { + checkForUpdates(true); + }, 30 * 60 * 1000); + return () => { + window.clearTimeout(startupTimer); + window.clearInterval(interval); + }; + }, [checkForUpdates]); + + useEffect(() => { + const offDownloadProgress = EventsOn('update:download-progress', (event: UpdateDownloadProgressEvent) => { + if (!event) return; + const status = event.status || 'downloading'; + const nextStatus: 'idle' | 'start' | 'downloading' | 'done' | 'error' = + status === 'start' || status === 'downloading' || status === 'done' || status === 'error' + ? status + : 'downloading'; + const downloaded = typeof event.downloaded === 'number' ? event.downloaded : 0; + const total = typeof event.total === 'number' ? event.total : 0; + const percentRaw = typeof event.percent === 'number' + ? event.percent + : (total > 0 ? (downloaded / total) * 100 : 0); + const percent = Math.max(0, Math.min(100, percentRaw)); + setUpdateDownloadProgress(prev => ({ + open: nextStatus === 'start' || nextStatus === 'downloading' || nextStatus === 'error', + version: prev.version, + status: nextStatus, + percent, + downloaded, + total, + message: String(event.message || '') + })); + }); + return () => { + offDownloadProgress(); + }; + }, []); + + const linuxResizeHandleStyleBase = { + position: 'fixed', + zIndex: 12000, + background: 'transparent', + WebkitAppRegion: 'drag', + '--wails-draggable': 'drag', + userSelect: 'none' + } as any; + + const showLinuxResizeHandles = isLinuxRuntime; + + return ( + { - updateDeferredVersionRef.current = info.latestVersion; - } - }); - }; - - const downloadUpdate = React.useCallback(async (info: UpdateInfo, silent: boolean) => { - if (updateDownloadInFlightRef.current) return; - if (updateDownloadedVersionRef.current === info.latestVersion) { - if (!silent) { - const cachedDownloadPath = updateDownloadMetaRef.current?.downloadPath; - message.info(cachedDownloadPath ? `更新包已就绪(${info.latestVersion}),路径:${cachedDownloadPath}` : `更新包已就绪(${info.latestVersion})`); - } - if (!silent || updateDeferredVersionRef.current !== info.latestVersion) { - promptRestartForUpdate(info, updateDownloadMetaRef.current || undefined); - } - return; - } - updateDownloadInFlightRef.current = true; - updateDownloadMetaRef.current = null; - const key = 'update-download'; - setUpdateDownloadProgress({ - open: true, - version: info.latestVersion, - status: 'start', - percent: 0, - downloaded: 0, - total: info.assetSize || 0, - message: '' - }); - message.loading({ content: `正在下载更新 ${info.latestVersion}...`, key, duration: 0 }); - const res = await (window as any).go.app.App.DownloadUpdate(); - updateDownloadInFlightRef.current = false; - if (res?.success) { - const resultData = (res?.data || {}) as UpdateDownloadResultData; - updateDownloadMetaRef.current = resultData; - updateDownloadedVersionRef.current = info.latestVersion; - setUpdateDownloadProgress(prev => ({ ...prev, status: 'done', percent: 100, open: false })); - if (resultData?.downloadPath) { - message.success({ content: `更新下载完成,更新包路径:${resultData.downloadPath}`, key, duration: 5 }); - } else { - message.success({ content: '更新下载完成', key, duration: 2 }); - } - setAboutUpdateStatus(`发现新版本 ${info.latestVersion}(已下载,待重启安装)`); - if (!silent || updateDeferredVersionRef.current !== info.latestVersion) { - promptRestartForUpdate(info, resultData); - } - } else { - setUpdateDownloadProgress(prev => ({ - ...prev, - status: 'error', - message: res?.message || '未知错误' - })); - message.error({ content: '更新下载失败: ' + (res?.message || '未知错误'), key, duration: 4 }); - } - }, []); - - const checkForUpdates = React.useCallback(async (silent: boolean) => { - if (updateCheckInFlightRef.current) return; - updateCheckInFlightRef.current = true; - if (!silent) { - setAboutUpdateStatus('正在检查更新...'); - } - const res = await (window as any).go.app.App.CheckForUpdates(); - updateCheckInFlightRef.current = false; - if (!res?.success) { - if (!silent) { - message.error('检查更新失败: ' + (res?.message || '未知错误')); - setAboutUpdateStatus('检查更新失败: ' + (res?.message || '未知错误')); - } - return; - } - const info: UpdateInfo = res.data; - if (!info) return; - setLastUpdateInfo(info); - if (info.hasUpdate) { - const localDownloaded = updateDownloadedVersionRef.current === info.latestVersion; - const hasDownloaded = Boolean(info.downloaded) || localDownloaded; - if (hasDownloaded) { - const downloadPath = info.downloadPath || updateDownloadMetaRef.current?.downloadPath || ''; - updateDownloadedVersionRef.current = info.latestVersion; - updateDownloadMetaRef.current = { - ...(updateDownloadMetaRef.current || {}), - info, - downloadPath: downloadPath || undefined, - }; - } else { - if (updateDownloadedVersionRef.current !== info.latestVersion) { - updateDownloadMetaRef.current = null; - } - } - const statusText = hasDownloaded - ? `发现新版本 ${info.latestVersion}(已下载,待重启安装)` - : `发现新版本 ${info.latestVersion}(未下载)`; - if (!silent) { - message.info(`发现新版本 ${info.latestVersion}`); - setAboutUpdateStatus(statusText); - } - if (silent && isAboutOpen) { - setAboutUpdateStatus(statusText); - } - if (silent && !isAboutOpen && updateMutedVersionRef.current !== info.latestVersion && updateNotifiedVersionRef.current !== info.latestVersion) { - updateNotifiedVersionRef.current = info.latestVersion; - setIsAboutOpen(true); - } - } else if (!silent) { - const text = `当前已是最新版本(${info.currentVersion || '未知'})`; - message.success(text); - setAboutUpdateStatus(text); - } else if (silent && isAboutOpen) { - const text = `当前已是最新版本(${info.currentVersion || '未知'})`; - setAboutUpdateStatus(text); - } - }, [downloadUpdate]); - - const loadAboutInfo = React.useCallback(async () => { - setAboutLoading(true); - const res = await (window as any).go.app.App.GetAppInfo(); - if (res?.success) { - setAboutInfo(res.data); - } else { - message.error('获取应用信息失败: ' + (res?.message || '未知错误')); - } - setAboutLoading(false); - }, []); - - const handleNewQuery = () => { - let connId = activeContext?.connectionId || ''; - let db = activeContext?.dbName || ''; - - // Priority: Active Tab Context > Sidebar Selection - if (activeTabId) { - const currentTab = tabs.find(t => t.id === activeTabId); - if (currentTab && currentTab.connectionId) { - connId = currentTab.connectionId; - db = currentTab.dbName || ''; - } - } - - addTab({ - id: `query-${Date.now()}`, - title: '新建查询', - type: 'query', - connectionId: connId, - dbName: db, - query: '' - }); - }; - - const handleImportConnections = async () => { - const res = await (window as any).go.app.App.ImportConfigFile(); - if (res.success) { - try { - const imported = JSON.parse(res.data); - if (Array.isArray(imported)) { - let count = 0; - imported.forEach((conn: any) => { - if (!connections.some(c => c.id === conn.id)) { - addConnection(conn); - count++; - } - }); - message.success(`成功导入 ${count} 个连接`); - } else { - message.error("文件格式错误:需要 JSON 数组"); - } - } catch (e) { - message.error("解析 JSON 失败"); - } - } else if (res.message !== "Cancelled") { - message.error("导入失败: " + res.message); - } - }; - - const handleExportConnections = async () => { - if (connections.length === 0) { - message.warning("没有连接可导出"); - return; - } - const res = await (window as any).go.app.App.ExportData(connections, [], "connections", "json"); - if (res.success) { - message.success("导出成功"); - } else if (res.message !== "Cancelled") { - message.error("导出失败: " + res.message); - } - }; - - const toolsMenu: MenuProps['items'] = [ - { - key: 'import', - label: '导入连接配置', - icon: , - onClick: handleImportConnections - }, - { - key: 'export', - label: '导出连接配置', - icon: , - onClick: handleExportConnections - }, - { - key: 'sync', - label: '数据同步', - icon: , - onClick: () => setIsSyncModalOpen(true) - }, - { - key: 'drivers', - label: '驱动管理', - icon: , - onClick: () => setIsDriverModalOpen(true) - } - ]; - - const themeMenu: MenuProps['items'] = [ - { - key: 'light', - label: '亮色主题', - icon: themeMode === 'light' ? : undefined, - onClick: () => setTheme('light') - }, - { - key: 'dark', - label: '暗色主题', - icon: themeMode === 'dark' ? : undefined, - onClick: () => setTheme('dark') - }, - { type: 'divider' }, - { - key: 'settings', - label: '外观设置...', - icon: , - onClick: () => setIsAppearanceModalOpen(true) - } - ]; - - const [isAppearanceModalOpen, setIsAppearanceModalOpen] = useState(false); - const [isProxyModalOpen, setIsProxyModalOpen] = useState(false); - - - // Log Panel: 最小高度按“工具栏 + 1 条日志行(微增)”限制 - const LOG_PANEL_TOOLBAR_HEIGHT = 32; - const LOG_PANEL_SINGLE_ROW_HEIGHT = 39; - const LOG_PANEL_MIN_VISIBLE_ROWS = 1; - const LOG_PANEL_MIN_HEIGHT = LOG_PANEL_TOOLBAR_HEIGHT + (LOG_PANEL_SINGLE_ROW_HEIGHT * LOG_PANEL_MIN_VISIBLE_ROWS); - const LOG_PANEL_MAX_HEIGHT = 800; - const [logPanelHeight, setLogPanelHeight] = useState(Math.max(200, LOG_PANEL_MIN_HEIGHT)); - const [isLogPanelOpen, setIsLogPanelOpen] = useState(false); - const logResizeRef = React.useRef<{ startY: number, startHeight: number } | null>(null); - const logGhostRef = React.useRef(null); - - const handleLogResizeStart = (e: React.MouseEvent) => { - e.preventDefault(); - logResizeRef.current = { startY: e.clientY, startHeight: logPanelHeight }; - - if (logGhostRef.current) { - logGhostRef.current.style.top = `${e.clientY}px`; - logGhostRef.current.style.display = 'block'; - } - - document.addEventListener('mousemove', handleLogResizeMove); - document.addEventListener('mouseup', handleLogResizeUp); - }; - - const handleLogResizeMove = (e: MouseEvent) => { - if (!logResizeRef.current) return; - // Just update ghost line, no state update - if (logGhostRef.current) { - logGhostRef.current.style.top = `${e.clientY}px`; - } - }; - - const handleLogResizeUp = (e: MouseEvent) => { - if (logResizeRef.current) { - const delta = logResizeRef.current.startY - e.clientY; - const newHeight = Math.max( - LOG_PANEL_MIN_HEIGHT, - Math.min(LOG_PANEL_MAX_HEIGHT, logResizeRef.current.startHeight + delta) - ); - setLogPanelHeight(newHeight); - } - - if (logGhostRef.current) { - logGhostRef.current.style.display = 'none'; - } - - logResizeRef.current = null; - document.removeEventListener('mousemove', handleLogResizeMove); - document.removeEventListener('mouseup', handleLogResizeUp); - }; - - const handleEditConnection = (conn: SavedConnection) => { - setEditingConnection(conn); - setIsModalOpen(true); - }; - - const handleCloseModal = () => { - setIsModalOpen(false); - setEditingConnection(null); - }; - - const handleOpenDriverManagerFromConnection = () => { - setIsModalOpen(false); - setEditingConnection(null); - setIsDriverModalOpen(true); - }; - - const handleTitleBarDoubleClick = (e: React.MouseEvent) => { - const target = e.target as HTMLElement | null; - if (target?.closest('[data-no-titlebar-toggle="true"]')) { - return; - } - (window as any).runtime.WindowToggleMaximise(); - }; - - // Sidebar Resizing - const [sidebarWidth, setSidebarWidth] = useState(300); - const sidebarDragRef = React.useRef<{ startX: number, startWidth: number } | null>(null); - const rafRef = React.useRef(null); - const ghostRef = React.useRef(null); - const latestMouseX = React.useRef(0); // Store latest mouse position - - const handleSidebarMouseDown = (e: React.MouseEvent) => { - e.preventDefault(); - - if (ghostRef.current) { - ghostRef.current.style.left = `${sidebarWidth}px`; - ghostRef.current.style.display = 'block'; - } - - sidebarDragRef.current = { startX: e.clientX, startWidth: sidebarWidth }; - latestMouseX.current = e.clientX; // Init - document.addEventListener('mousemove', handleSidebarMouseMove); - document.addEventListener('mouseup', handleSidebarMouseUp); - }; - - const handleSidebarMouseMove = (e: MouseEvent) => { - if (!sidebarDragRef.current) return; - - latestMouseX.current = e.clientX; // Always update latest pos - - if (rafRef.current) return; // Schedule once per frame - - rafRef.current = requestAnimationFrame(() => { - if (!sidebarDragRef.current || !ghostRef.current) return; - // Use latestMouseX.current instead of stale closure 'e.clientX' - const delta = latestMouseX.current - sidebarDragRef.current.startX; - const newWidth = Math.max(200, Math.min(600, sidebarDragRef.current.startWidth + delta)); - ghostRef.current.style.left = `${newWidth}px`; - rafRef.current = null; - }); - }; - - const handleSidebarMouseUp = (e: MouseEvent) => { - if (rafRef.current) { - cancelAnimationFrame(rafRef.current); - rafRef.current = null; - } - - if (sidebarDragRef.current) { - // Use latest position for final commit too - const delta = e.clientX - sidebarDragRef.current.startX; - const newWidth = Math.max(200, Math.min(600, sidebarDragRef.current.startWidth + delta)); - setSidebarWidth(newWidth); - } - - if (ghostRef.current) { - ghostRef.current.style.display = 'none'; - } - - sidebarDragRef.current = null; - document.removeEventListener('mousemove', handleSidebarMouseMove); - document.removeEventListener('mouseup', handleSidebarMouseUp); - }; - - useEffect(() => { - document.body.style.backgroundColor = 'transparent'; - document.body.style.color = darkMode ? '#ffffff' : '#000000'; - document.body.setAttribute('data-theme', darkMode ? 'dark' : 'light'); - }, [darkMode]); - - useEffect(() => { - if (isAboutOpen) { - if (lastUpdateInfo?.hasUpdate) { - setAboutUpdateStatus(`发现新版本 ${lastUpdateInfo.latestVersion}(未下载)`); - } else if (lastUpdateInfo) { - setAboutUpdateStatus(`当前已是最新版本(${lastUpdateInfo.currentVersion || '未知'})`); - } else { - setAboutUpdateStatus('未检查'); - } - loadAboutInfo(); - } - }, [isAboutOpen, lastUpdateInfo, loadAboutInfo]); - - useEffect(() => { - const startupTimer = window.setTimeout(() => { - checkForUpdates(true); - }, 2000); - const interval = window.setInterval(() => { - checkForUpdates(true); - }, 30 * 60 * 1000); - return () => { - window.clearTimeout(startupTimer); - window.clearInterval(interval); - }; - }, [checkForUpdates]); - - useEffect(() => { - const offDownloadProgress = EventsOn('update:download-progress', (event: UpdateDownloadProgressEvent) => { - if (!event) return; - const status = event.status || 'downloading'; - const nextStatus: 'idle' | 'start' | 'downloading' | 'done' | 'error' = - status === 'start' || status === 'downloading' || status === 'done' || status === 'error' - ? status - : 'downloading'; - const downloaded = typeof event.downloaded === 'number' ? event.downloaded : 0; - const total = typeof event.total === 'number' ? event.total : 0; - const percentRaw = typeof event.percent === 'number' - ? event.percent - : (total > 0 ? (downloaded / total) * 100 : 0); - const percent = Math.max(0, Math.min(100, percentRaw)); - setUpdateDownloadProgress(prev => ({ - open: nextStatus === 'start' || nextStatus === 'downloading' || nextStatus === 'error', - version: prev.version, - status: nextStatus, - percent, - downloaded, - total, - message: String(event.message || '') - })); - }); - return () => { - offDownloadProgress(); - }; - }, []); - - const linuxResizeHandleStyleBase = { - position: 'fixed', - zIndex: 12000, - background: 'transparent', - WebkitAppRegion: 'drag', - '--wails-draggable': 'drag', - userSelect: 'none' - } as any; - - const showLinuxResizeHandles = isLinuxRuntime; - - return ( - - + + {/* Custom Title Bar */} +
- {/* Custom Title Bar */} -
-
- {/* Logo can be added here if available */} - GoNavi -
-
e.stopPropagation()} - style={{ display: 'flex', height: '100%', WebkitAppRegion: 'no-drag', '--wails-draggable': 'no-drag' } as any} - > -
+ alignItems: 'center', + justifyContent: 'space-between', + background: bgMain, + borderBottom: 'none', + userSelect: 'none', + WebkitAppRegion: 'drag', // Wails drag region + '--wails-draggable': 'drag', + paddingLeft: 16 + } as any} + > +
+ {/* Logo can be added here if available */} + GoNavi +
+
e.stopPropagation()} + style={{ display: 'flex', height: '100%', WebkitAppRegion: 'no-drag', '--wails-draggable': 'no-drag' } as any} + > +
+
+ +
+ + + + + + + + +
+ + +
+
+ +
+
+
+ +
+
-
- - - - - - - - -
- - +
-
- -
- -
- - {/* Sidebar Footer for Log Toggle */} -
- -
- - - {/* Sidebar Resize Handle */} -
- - -
- -
- {isLogPanelOpen && ( - setIsLogPanelOpen(false)} - onResizeStart={handleLogResizeStart} - /> - )} -
- - +
+ + + {/* Sidebar Resize Handle */} +
+ + +
+ +
+ {isLogPanelOpen && ( + setIsLogPanelOpen(false)} + onResizeStart={handleLogResizeStart} /> - setIsSyncModalOpen(false)} - /> - setIsDriverModalOpen(false)} - /> - setIsAboutOpen(false)} - footer={[ - lastUpdateInfo?.hasUpdate ? ( - - ) : null, - lastUpdateInfo?.hasUpdate ? ( - - ) : null, - , - - ].filter(Boolean)} - > - {aboutLoading ? ( -
- -
- ) : ( -
-
版本:{aboutInfo?.version || '未知'}
-
作者:{aboutInfo?.author || '未知'}
- {aboutInfo?.communityUrl ? ( - - ) : null} -
更新状态:{aboutUpdateStatus || '未检查'}
- - - -
- )} -
+ )} +
+ + + setIsSyncModalOpen(false)} + /> + setIsDriverModalOpen(false)} + /> + setIsAboutOpen(false)} + footer={[ + lastUpdateInfo?.hasUpdate ? ( + + ) : null, + lastUpdateInfo?.hasUpdate ? ( + + ) : null, + , + + ].filter(Boolean)} + > + {aboutLoading ? ( +
+ +
+ ) : ( +
+
版本:{aboutInfo?.version || '未知'}
+
作者:{aboutInfo?.author || '未知'}
+
更新状态:{aboutUpdateStatus || '未检查'}
+ + + +
+ )} +
- setIsAppearanceModalOpen(false)} - footer={null} - width={460} - > -
-
-
背景不透明度 (Opacity)
-
- setAppearance({ opacity: v })} + setIsAppearanceModalOpen(false)} + footer={null} + width={460} + > +
+
+
背景不透明度 (Opacity)
+
+ setAppearance({ opacity: v })} + style={{ flex: 1 }} + /> + {Math.round((appearance.opacity ?? 1.0) * 100)}% +
+
+
+
高斯模糊 (Blur)
+ {isWindowsPlatform() ? ( +
+ Windows 使用系统 Acrylic 效果,模糊程度由系统控制 +
+ ) : ( + <> +
+ setAppearance({ blur: v })} style={{ flex: 1 }} - /> - {Math.round((appearance.opacity ?? 1.0) * 100)}% -
-
-
-
高斯模糊 (Blur)
- {isWindowsPlatform() ? ( -
- Windows 使用系统 Acrylic 效果,模糊程度由系统控制 -
- ) : ( - <> -
- setAppearance({ blur: v })} - style={{ flex: 1 }} - /> - {appearance.blur}px -
-
- * 仅控制应用内覆盖层的模糊效果 -
- - )} -
-
-
启动窗口
-
- 启动时全屏 - setStartupFullscreen(checked)} /> -
-
- * 修改后下次启动生效 -
-
-
-
+ /> + {appearance.blur}px +
+
+ * 仅控制应用内覆盖层的模糊效果 +
+ + )} +
+
+
启动窗口
+
+ 启动时全屏 + setStartupFullscreen(checked)} /> +
+
+ * 修改后下次启动生效 +
+
+
+
- setIsProxyModalOpen(false)} - footer={null} - width={460} - > -
-
-
全局代理
-
- 启用全局代理 - setGlobalProxy({ enabled: checked })} /> -
-
-
-
代理类型
- setGlobalProxy({ host: e.target.value })} - /> -
-
-
用户名(可选)
- setGlobalProxy({ user: e.target.value })} - /> -
-
-
密码(可选)
- setGlobalProxy({ password: e.target.value })} - /> -
-
-
- * 作用于更新检查、驱动管理网络请求,以及未单独配置代理的数据库连接 -
-
-
-
+ setIsProxyModalOpen(false)} + footer={null} + width={460} + > +
+
+
全局代理
+
+ 启用全局代理 + setGlobalProxy({ enabled: checked })} /> +
+
+
+
代理类型
+ setGlobalProxy({ host: e.target.value })} + /> +
+
+
用户名(可选)
+ setGlobalProxy({ user: e.target.value })} + /> +
+
+
密码(可选)
+ setGlobalProxy({ password: e.target.value })} + /> +
+
+
+ * 作用于更新检查、驱动管理网络请求,以及未单独配置代理的数据库连接 +
+
+
+
- { - if (updateDownloadProgress.status === 'error') { - setUpdateDownloadProgress({ - open: false, - version: '', - status: 'idle', - percent: 0, - downloaded: 0, - total: 0, - message: '' - }); - } - }} - footer={updateDownloadProgress.status === 'error' ? [ - - ] : null} - > -
- -
- {`${formatBytes(updateDownloadProgress.downloaded)} / ${formatBytes(updateDownloadProgress.total)}`} -
- {updateDownloadProgress.message ? ( -
{updateDownloadProgress.message}
- ) : null} -
-
+ { + if (updateDownloadProgress.status === 'error') { + setUpdateDownloadProgress({ + open: false, + version: '', + status: 'idle', + percent: 0, + downloaded: 0, + total: 0, + message: '' + }); + } + }} + footer={updateDownloadProgress.status === 'error' ? [ + + ] : null} + > +
+ +
+ {`${formatBytes(updateDownloadProgress.downloaded)} / ${formatBytes(updateDownloadProgress.total)}`} +
+ {updateDownloadProgress.message ? ( +
{updateDownloadProgress.message}
+ ) : null} +
+
- {showLinuxResizeHandles && ( - <> - {/* Linux Mint 下 frameless 仅局部可缩放:补四边四角命中层 */} -
-
-
-
+ {showLinuxResizeHandles && ( + <> + {/* Linux Mint 下 frameless 仅局部可缩放:补四边四角命中层 */} +
+
+
+
-
-
-
-
- - )} - - {/* Ghost Resize Line for Sidebar */} -
- - {/* Ghost Resize Line for Log Panel */} -
- - - ); +
+
+
+
+ + )} + + {/* Ghost Resize Line for Sidebar */} +
+ + {/* Ghost Resize Line for Log Panel */} +
+ + + ); } export default App; diff --git a/internal/app/methods_update.go b/internal/app/methods_update.go index 6c992a3..5707b6e 100644 --- a/internal/app/methods_update.go +++ b/internal/app/methods_update.go @@ -51,13 +51,12 @@ type UpdateInfo struct { } type AppInfo struct { - Version string `json:"version"` - Author string `json:"author"` - RepoURL string `json:"repoUrl,omitempty"` - IssueURL string `json:"issueUrl,omitempty"` - ReleaseURL string `json:"releaseUrl,omitempty"` - CommunityURL string `json:"communityUrl,omitempty"` - BuildTime string `json:"buildTime,omitempty"` + Version string `json:"version"` + Author string `json:"author"` + RepoURL string `json:"repoUrl,omitempty"` + IssueURL string `json:"issueUrl,omitempty"` + ReleaseURL string `json:"releaseUrl,omitempty"` + BuildTime string `json:"buildTime,omitempty"` } type updateDownloadResult struct { @@ -138,13 +137,12 @@ func (a *App) CheckForUpdates() connection.QueryResult { func (a *App) GetAppInfo() connection.QueryResult { info := AppInfo{ - Version: getCurrentVersion(), - Author: getCurrentAuthor(), - RepoURL: "https://github.com/" + updateRepo, - IssueURL: "https://github.com/" + updateRepo + "/issues", - ReleaseURL: "https://github.com/" + updateRepo + "/releases", - CommunityURL: "https://aibook.ren", - BuildTime: strings.TrimSpace(AppBuildTime), + Version: getCurrentVersion(), + Author: getCurrentAuthor(), + RepoURL: "https://github.com/" + updateRepo, + IssueURL: "https://github.com/" + updateRepo + "/issues", + ReleaseURL: "https://github.com/" + updateRepo + "/releases", + BuildTime: strings.TrimSpace(AppBuildTime), } return connection.QueryResult{Success: true, Message: "OK", Data: info} }