import React, { useEffect, useState } from "react"; import { ArrowUpCircle, Check, Loader2, RefreshCcw } from "lucide-react"; import AppLogo from "./AppLogo"; import AppWordmark from "./AppWordmark"; import { Button } from "./ui/button"; import { useApplicationBackend } from "../application/state/useApplicationBackend"; import type { UpdateState, UseUpdateCheckResult } from "../application/state/useUpdateCheck"; import { useI18n } from "../application/i18n/I18nProvider"; import { SettingsAnchor, SettingsTabContent } from "./settings/settings-ui"; import { toast } from "./ui/toast"; import { useStoredString } from "../application/state/useStoredString"; import { STORAGE_KEY_MONITORING_DASHBOARD_TITLE, STORAGE_KEY_REPORT_PRODUCT_NAME } from "../infrastructure/config/storageKeys"; type AppInfo = { name: string; version: string; platform?: string; }; interface SettingsApplicationTabProps { updateState: UpdateState; checkNow: UseUpdateCheckResult['checkNow']; openReleasePage: UseUpdateCheckResult['openReleasePage']; installUpdate: UseUpdateCheckResult['installUpdate']; startDownload: UseUpdateCheckResult['startDownload']; isUpdateDemoMode: boolean; productName: string; setProductName: (value: string) => void; } export default function SettingsApplicationTab({ updateState, checkNow, openReleasePage, installUpdate, startDownload, isUpdateDemoMode, productName, setProductName }: SettingsApplicationTabProps) { const { t } = useI18n(); const { openExternal, getApplicationInfo } = useApplicationBackend(); const [appInfo, setAppInfo] = useState({ name: "Netcatty", version: "" }); const [lastCheckResult, setLastCheckResult] = useState<'none' | 'available' | 'upToDate'>('none'); const [monitoringTitle, setMonitoringTitle] = useStoredString(STORAGE_KEY_MONITORING_DASHBOARD_TITLE, '云启Net智能巡检监测系统'); const [reportProductName, setReportProductName] = useStoredString(STORAGE_KEY_REPORT_PRODUCT_NAME, ''); useEffect(() => { let cancelled = false; const load = async () => { try { const info = await getApplicationInfo(); if (!cancelled && info?.name && typeof info?.version === "string") { setAppInfo(info); } } catch { // Ignore: running in browser/dev without Electron bridge } }; void load(); return () => { cancelled = true; }; }, [getApplicationInfo]); const handleOpenExternal = async (url: string) => { try { await openExternal(url); } catch (err) { console.warn("[SettingsApplicationTab] openExternal failed:", err); toast.error( t("settings.application.openExternal.failedBody"), t("settings.application.openExternal.failedTitle"), ); } }; const handleCheckForUpdates = async () => { // In demo mode, allow checking even for dev builds if (!isUpdateDemoMode && (!appInfo.version || appInfo.version === '0.0.0')) { // Dev build - just open releases page openReleasePage(); return; } setLastCheckResult('none'); const result = await checkNow(); if (result?.hasUpdate && result.latestRelease) { setLastCheckResult('available'); toast.info( t('update.available.message', { version: result.latestRelease.version }), t('update.available.title') ); // Don't auto-open the release page here — checkNow() already triggers // electron-updater on supported platforms, and the Settings > System tab // shows a "Manual Download" link on unsupported platforms. } else if (result) { setLastCheckResult('upToDate'); toast.success( t('update.upToDate.message', { version: appInfo.version }), t('update.upToDate.title') ); } // Reset the result after 3 seconds setTimeout(() => setLastCheckResult('none'), 3000); }; return (
{appInfo.version ? appInfo.version : " "} {/* Update badge - reflects auto-download state */} {updateState.latestRelease && (updateState.hasUpdate || updateState.autoDownloadStatus === 'downloading' || updateState.autoDownloadStatus === 'ready') && ( )}
{/* 检查更新按钮已隐藏 */} {/* 产品名称配置 —— 本地 state 缓冲输入,blur/Enter 才提交,避免每次按键都触发全局持久化 + IPC 导致闪动 */} {/* 大屏名称配置 —— 同样用本地 state 缓冲,避免每次按键都触发 localStorage 写入和事件广播 */} { setMonitoringTitle(v); try { window.dispatchEvent(new CustomEvent('monitoringTitleChanged')); } catch {} }} /> {/* 报告产品名配置 —— 专门用于 PDF 巡检报告标题 */} setReportProductName(v)} />
); } /** * 带本地 state 缓冲的设置输入框。输入过程只更新本地 draft, * 回车或失焦才调用 onCommit,避免每次按键都触发全局持久化 + IPC 导致闪动。 */ function SettingsInputBuffered({ value, onCommit, placeholder, label, hint, trim = false, }: { value: string; onCommit: (v: string) => void; placeholder?: string; label: string; hint?: string; trim?: boolean; }) { const [draft, setDraft] = useState(value); useEffect(() => { setDraft(value); }, [value]); const commit = () => { const next = trim ? draft.trim() : draft; if (next !== value) onCommit(next); }; return (
setDraft(e.target.value)} onBlur={commit} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); commit(); (e.target as HTMLInputElement).blur(); } else if (e.key === "Escape") { setDraft(value); (e.target as HTMLInputElement).blur(); } }} placeholder={placeholder} className="w-full px-3 py-2 rounded-lg border border-input bg-background text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring" /> {hint &&

{hint}

}
); } /** * 产品名称输入框 —— SettingsInputBuffered + trim 空白。 */ function ProductNameInput({ value, onChange }: { value: string; onChange: (v: string) => void }) { return ( ); }