"use client" import { useCallback, useEffect, useMemo, useState } from "react" import { ArrowUpCircle, CheckCircle2, Languages, Loader2, MonitorCog, RefreshCw, Wifi, } from "lucide-react" import { Github } from "@lobehub/icons" // eslint-disable-next-line @typescript-eslint/no-explicit-any type Update = any import { useLocale, useTranslations } from "next-intl" import ReactMarkdown from "react-markdown" import remarkGfm from "remark-gfm" import { toast } from "sonner" import { useAppI18n } from "@/components/i18n-provider" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { ScrollArea } from "@/components/ui/scroll-area" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { getSystemProxySettings, getSystemRenderingSettings, updateSystemLanguageSettings, updateSystemProxySettings, updateSystemRenderingSettings, } from "@/lib/api" import { isDesktop, openUrl } from "@/lib/platform" import type { AppLocale } from "@/lib/types" import { usePlatform } from "@/hooks/use-platform" import { checkAppUpdate, closeAppUpdate, getCurrentAppVersion, installAppUpdate, normalizeAppUpdateError, relaunchApp, } from "@/lib/updater" import type { DownloadEvent } from "@/lib/updater" import { APP_LOCALES } from "@/lib/i18n" function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` return `${(bytes / (1024 * 1024)).toFixed(1)} MB` } const PROXY_EXAMPLE = "http://127.0.0.1:7890" const APP_LANGUAGE_VALUES = APP_LOCALES type LanguageSelectValue = "system" | AppLocale function isAppLocale(value: string): value is AppLocale { return APP_LANGUAGE_VALUES.includes(value as AppLocale) } type UpdateAction = "check" | "install" // Captured the first time settings page loads: represents the value that the // running webview process was launched with. Survives settings-shell remounts // so the "Restart now" banner doesn't vanish if the user navigates away and // back without restarting. let processStartDisableHwAccel: boolean | null = null export function SystemNetworkSettings() { const t = useTranslations("SystemSettings") const tLanguage = useTranslations("Language") const locale = useLocale() const { languageSettings, languageSettingsLoaded, setLanguageSettings } = useAppI18n() const { isWindows } = usePlatform() const renderingSettingsLoadable = isDesktop() const renderingSectionVisible = renderingSettingsLoadable && isWindows const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [savingLanguage, setSavingLanguage] = useState(false) const [enabled, setEnabled] = useState(false) const [proxyUrl, setProxyUrl] = useState("") const [loadError, setLoadError] = useState(null) const [disableHwAccel, setDisableHwAccel] = useState(false) const [savingRendering, setSavingRendering] = useState(false) const [persistedDisableHwAccel, setPersistedDisableHwAccel] = useState(false) const [processStartLoaded, setProcessStartLoaded] = useState( processStartDisableHwAccel !== null ) const renderingDirty = processStartLoaded && persistedDisableHwAccel !== processStartDisableHwAccel const [currentVersion, setCurrentVersion] = useState("") const [availableUpdate, setAvailableUpdate] = useState(null) const [checkingUpdate, setCheckingUpdate] = useState(false) const [installingUpdate, setInstallingUpdate] = useState(false) const [updateError, setUpdateError] = useState(null) const [lastCheckedAt, setLastCheckedAt] = useState(null) const [downloadProgress, setDownloadProgress] = useState<{ downloaded: number total: number | null phase: "downloading" | "installing" } | null>(null) const [appLanguage, setAppLanguage] = useState( languageSettings.mode === "system" ? "system" : languageSettings.language ) useEffect(() => { setAppLanguage( languageSettings.mode === "system" ? "system" : languageSettings.language ) }, [languageSettings]) const languageLabels = useMemo( () => ({ en: tLanguage("english"), zh_cn: tLanguage("simplifiedChinese"), zh_tw: tLanguage("traditionalChinese"), ja: tLanguage("japanese"), ko: tLanguage("korean"), es: tLanguage("spanish"), de: tLanguage("german"), fr: tLanguage("french"), pt: tLanguage("portuguese"), ar: tLanguage("arabic"), }), [tLanguage] ) const formattedLastCheckedAt = useMemo(() => { if (!lastCheckedAt) return null return new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short", }).format(lastCheckedAt) }, [lastCheckedAt, locale]) const formattedUpdateDate = useMemo(() => { if (!availableUpdate?.date) return null const parsed = new Date(availableUpdate.date) if (Number.isNaN(parsed.getTime())) return availableUpdate.date return new Intl.DateTimeFormat(locale, { dateStyle: "medium", }).format(parsed) }, [availableUpdate?.date, locale]) const updateNotes = useMemo( () => availableUpdate?.body?.trim() ?? "", [availableUpdate?.body] ) const updateStatusMessage = useMemo(() => { if (checkingUpdate) return t("checking") if (installingUpdate) return t("updating") if (availableUpdate) return null if (lastCheckedAt) return t("alreadyLatest") return null }, [availableUpdate, checkingUpdate, installingUpdate, lastCheckedAt, t]) const loadSettings = useCallback(async () => { setLoading(true) setLoadError(null) try { const [proxySettings, version, renderingSettings] = await Promise.all([ getSystemProxySettings(), getCurrentAppVersion(), renderingSettingsLoadable ? getSystemRenderingSettings() : Promise.resolve(null), ]) setEnabled(proxySettings.enabled) setProxyUrl(proxySettings.proxy_url ?? "") setCurrentVersion(version) if (renderingSettings) { const value = renderingSettings.disable_hardware_acceleration setDisableHwAccel(value) setPersistedDisableHwAccel(value) if (processStartDisableHwAccel === null) { processStartDisableHwAccel = value setProcessStartLoaded(true) } } } catch (err) { const message = err instanceof Error ? err.message : String(err) setLoadError(message) console.error("[Settings] load system settings failed:", err) } finally { setLoading(false) } }, [renderingSettingsLoadable]) useEffect(() => { loadSettings().catch((err) => { console.error("[Settings] load system settings failed:", err) }) checkForUpdates().catch((err) => { console.error("[Settings] auto check update failed:", err) }) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { return () => { if (!availableUpdate) return closeAppUpdate(availableUpdate).catch((err) => { console.error("[Settings] release updater resource failed:", err) }) } }, [availableUpdate]) const saveProxySettings = useCallback( async (nextEnabled: boolean, nextProxyUrl: string) => { if (nextEnabled && !nextProxyUrl.trim()) return setSaving(true) try { const next = await updateSystemProxySettings({ enabled: nextEnabled, proxy_url: nextProxyUrl.trim() || null, }) setEnabled(next.enabled) setProxyUrl(next.proxy_url ?? "") } catch (err) { const message = err instanceof Error ? err.message : String(err) toast.error(t("saveFailed", { message })) } finally { setSaving(false) } }, [t] ) const saveRenderingSettings = useCallback( async (next: boolean, prev: boolean) => { setSavingRendering(true) try { const result = await updateSystemRenderingSettings({ disable_hardware_acceleration: next, }) setDisableHwAccel(result.disable_hardware_acceleration) setPersistedDisableHwAccel(result.disable_hardware_acceleration) } catch (err) { setDisableHwAccel(prev) const message = err instanceof Error ? err.message : String(err) toast.error(t("renderingSaveFailed", { message })) } finally { setSavingRendering(false) } }, [t] ) const restartNow = useCallback(async () => { try { await relaunchApp() } catch (err) { const message = err instanceof Error ? err.message : String(err) toast.error(t("restartFailed", { message })) } }, [t]) const saveLanguage = useCallback( async (lang: LanguageSelectValue) => { setSavingLanguage(true) try { const next = await updateSystemLanguageSettings({ mode: lang === "system" ? "system" : "manual", language: lang === "system" ? languageSettings.language : lang, }) setLanguageSettings(next) } catch (err) { const message = err instanceof Error ? err.message : String(err) toast.error(t("languageSaveFailed", { message })) } finally { setSavingLanguage(false) } }, [languageSettings.language, setLanguageSettings, t] ) const formatUpdateError = useCallback( (error: unknown, action: UpdateAction): string => { const { kind, rawMessage } = normalizeAppUpdateError(error) switch (kind) { case "source_unreachable": return t("updateErrors.sourceUnavailable") case "network": return t("updateErrors.network") case "download_failed": return t("updateErrors.downloadFailed") case "install_failed": return t("updateErrors.installFailed") case "unknown": default: if (action === "install") { return t("updateErrors.installFailed") } console.error("[Settings] updater unknown error:", rawMessage) return t("updateErrors.unknown") } }, [t] ) const checkForUpdates = useCallback(async () => { setCheckingUpdate(true) setUpdateError(null) try { const previousUpdate = availableUpdate const result = await checkAppUpdate() setCurrentVersion(result.currentVersion) setLastCheckedAt(new Date()) if (result.update) { setAvailableUpdate(result.update) } else { setAvailableUpdate(null) } if (previousUpdate && previousUpdate !== result.update) { await closeAppUpdate(previousUpdate) } } catch (err) { const message = formatUpdateError(err, "check") setUpdateError(message) toast.error(t("checkUpdateFailed", { message })) console.error("[Settings] check app update failed:", err) } finally { setCheckingUpdate(false) } }, [availableUpdate, formatUpdateError, t]) const installUpdate = useCallback(async () => { if (!availableUpdate) return setInstallingUpdate(true) setUpdateError(null) setDownloadProgress(null) let downloaded = 0 try { await installAppUpdate(availableUpdate, (event: DownloadEvent) => { switch (event.event) { case "Started": setDownloadProgress({ downloaded: 0, total: event.data.contentLength ?? null, phase: "downloading", }) break case "Progress": downloaded += event.data.chunkLength setDownloadProgress((prev) => ({ downloaded, total: prev?.total ?? null, phase: "downloading", })) break case "Finished": setDownloadProgress((prev) => ({ downloaded: prev?.downloaded ?? downloaded, total: prev?.total ?? null, phase: "installing", })) break } }) toast.success(t("installSuccess")) await relaunchApp() } catch (err) { const message = formatUpdateError(err, "install") setUpdateError(message) toast.error(t("installFailed", { message })) console.error("[Settings] install app update failed:", err) } finally { setInstallingUpdate(false) setDownloadProgress(null) } }, [availableUpdate, formatUpdateError, t]) if (loading) { return (
{t("loading")}
) } return (

{t("sectionTitle")}

{t("sectionDescription")}

{checkingUpdate ? ( ) : availableUpdate ? ( ) : lastCheckedAt ? ( ) : ( )}

{t("versionTitle")}

{t("updateDescription")}

{t("currentVersion")}: {currentVersion ? `v${currentVersion}` : "-"}

{checkingUpdate ? ( ) : availableUpdate ? ( ) : ( )}
{!availableUpdate && formattedLastCheckedAt && (

{t("lastChecked", { time: formattedLastCheckedAt })}

)} {updateStatusMessage && !downloadProgress && (

{updateStatusMessage}

)} {downloadProgress && (
{downloadProgress.phase === "downloading" ? t("downloading") : t("updating")} {formatBytes(downloadProgress.downloaded)} {downloadProgress.total ? ` / ${formatBytes(downloadProgress.total)}` : ""}
0 ? `${Math.min(100, (downloadProgress.downloaded / downloadProgress.total) * 100)}%` : "30%", }} />
)} {availableUpdate && (
{t("upgradableVersion")}:v{availableUpdate.version} {formattedUpdateDate && ( {formattedUpdateDate} )}
{updateNotes ? ( {updateNotes} ) : ( t("none") )}
)}
{updateError && (
{t("updateError", { message: updateError })}
)}

{t("proxyTitle")}

{t("proxyDescription")}

{loadError && (
{t("loadFailed", { message: loadError })}
)}
setProxyUrl(event.target.value)} onBlur={() => saveProxySettings(enabled, proxyUrl)} placeholder={PROXY_EXAMPLE} disabled={saving} />

{t("proxyHint", { example: PROXY_EXAMPLE })}

{renderingSectionVisible && (

{t("renderingTitle")}

{t("renderingDescription")}

{renderingDirty && (
{t("restartRequired")}
)}
)}

{t("languageTitle")}

{t("languageDescription")}

) }