import React, { useEffect, useRef, useState } from "react" import { Menu, Card, Form, Select, Switch, message, Layout, Button, Space } from "antd" import { SettingOutlined, SafetyOutlined, UserOutlined, DatabaseOutlined, FileTextOutlined, LinkOutlined, InfoCircleOutlined, ApiOutlined, CloseOutlined, MinusOutlined, FullscreenOutlined, FullscreenExitOutlined, } from "@ant-design/icons" import { ThemeQuickSettings } from "./ThemeQuickSettings" import { useTranslation } from "react-i18next" import { pinyin } from "pinyin-pro" import { changeLanguage, getCurrentLanguage, languageOptions, AppLanguage } from "../i18n" import { useResponsive } from "../hooks/useResponsive" import { buildSystemFontFamily, buildSystemFontValue, SYSTEM_FONT_STACK, } from "../shared/fontFamily" import { useConfig } from "../hooks/useConfig" const { Content } = Layout type permissionLevel = "admin" | "points" | "view" interface FontOption { value: string label: string fontFamily: string searchText: string } const CHINESE_FONT_CHAR_PATTERN = /[\u3400-\u9fff]/ const CHINESE_FONT_SEGMENT_PATTERN = /[\u3400-\u9fff]+/g const toPascalCasePinyin = (value: string): string => pinyin(value, { toneType: "none" }) .split(/\s+/) .filter(Boolean) .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1).toLowerCase()) .join("") const formatFontDisplayName = (name: string): string => { if (!CHINESE_FONT_CHAR_PATTERN.test(name)) return name return name.replace(CHINESE_FONT_SEGMENT_PATTERN, (segment) => toPascalCasePinyin(segment)) } const buildFontSearchText = (name: string, label: string): string => `${name} ${label}`.toLowerCase() const defaultFontOptions: FontOption[] = [ { value: "system", label: "系统默认", fontFamily: SYSTEM_FONT_STACK, searchText: "系统默认 system default", }, ] const mergeFontOptions = (options: FontOption[]): FontOption[] => { const map = new Map() for (const option of options) { if (!map.has(option.value)) { map.set(option.value, option) } } return Array.from(map.values()) } const findFontOption = (options: FontOption[], value?: string): FontOption | undefined => { if (!value) return options.find((item) => item.value === "system") || options[0] return ( options.find((item) => item.value === value) || options.find((item) => item.value === "system") ) } const applyFontFamily = (fontFamily: string) => { document.documentElement.style.setProperty("--ss-font-family", fontFamily) document.body.style.fontFamily = fontFamily } export const SettingsWindow: React.FC<{ permission: permissionLevel }> = ({ permission }) => { const { t } = useTranslation() const breakpoint = useResponsive() const isMobile = breakpoint === "xs" || breakpoint === "sm" const [activeTab, setActiveTab] = useState("appearance") const [currentLanguage, setCurrentLanguage] = useState(getCurrentLanguage()) const [fontFamily, setFontFamily, fontLoading] = useConfig("font_family", "system") const [windowZoom, setWindowZoom] = useConfig("window_zoom", 1.0) const [searchKeyboardLayout, setSearchKeyboardLayout] = useConfig( "search_keyboard_layout", "qwerty26" as any ) const [disableSearchKeyboard, setDisableSearchKeyboard] = useConfig( "disable_search_keyboard", false ) const [fontOptions, setFontOptions] = useState(defaultFontOptions) const [isLoadingFonts, setIsLoadingFonts] = useState(false) const fontOptionsRef = useRef(defaultFontOptions) const canAdmin = permission === "admin" const [messageApi, contextHolder] = message.useMessage() // 窗口控制状态 const [isMaximized, setIsMaximized] = useState(false) // 窗口控制函数 const handleMinimize = async () => { const api = (window as any).api if (api?.windowMinimize) { await api.windowMinimize() } } const handleMaximize = async () => { const api = (window as any).api if (api?.windowMaximize) { const result = await api.windowMaximize() setIsMaximized(result) } } const handleClose = async () => { const api = (window as any).api if (api?.closeSettingsWindow) { await api.closeSettingsWindow() } } // 监听最大化状态变化 useEffect(() => { const api = (window as any).api if (api?.onWindowMaximizedChanged) { api.onWindowMaximizedChanged((maximized: boolean) => { setIsMaximized(maximized) }) } }, []) const loadSystemFonts = async (selectedFontValue?: string) => { setIsLoadingFonts(true) const applySelectedFont = (options: FontOption[]) => { const current = findFontOption(options, selectedFontValue || fontFamily) if (current) applyFontFamily(current.fontFamily) } const api = (window as any).api if (!api?.getSystemFonts) { setFontOptions(defaultFontOptions) applySelectedFont(defaultFontOptions) setIsLoadingFonts(false) return } try { const res = await api.getSystemFonts() if (res.success && Array.isArray(res.data)) { const remoteOptions = res.data .map((name: string) => String(name || "").trim()) .filter((name: string) => Boolean(name)) .map((name: string) => { const label = formatFontDisplayName(name) return { value: buildSystemFontValue(name), label, fontFamily: buildSystemFontFamily(name), searchText: buildFontSearchText(name, label), } satisfies FontOption }) .sort((a: FontOption, b: FontOption) => a.label.localeCompare(b.label, "zh-Hans-CN-u-co-pinyin") ) const mergedOptions = mergeFontOptions([...defaultFontOptions, ...remoteOptions]) setFontOptions(mergedOptions) applySelectedFont(mergedOptions) } else { setFontOptions(defaultFontOptions) applySelectedFont(defaultFontOptions) } } catch (error) { console.error("Failed to load system fonts:", error) setFontOptions(defaultFontOptions) applySelectedFont(defaultFontOptions) } finally { setIsLoadingFonts(false) } } useEffect(() => { fontOptionsRef.current = fontOptions }, [fontOptions]) useEffect(() => { loadSystemFonts(fontFamily) }, []) const menuItems = [ { key: "appearance", icon: , label: t("settings.tabs.appearance"), }, { key: "security", icon: , label: t("settings.tabs.security"), }, { key: "account", icon: , label: t("settings.tabs.account"), }, { key: "database", icon: , label: t("settings.database.title"), }, { key: "data", icon: , label: t("settings.data.title"), }, { key: "url", icon: , label: t("settings.url.title"), }, { key: "about", icon: , label: t("settings.about.title"), }, { key: "api", icon: , label: t("settings.mcp.title"), }, ] const renderContent = () => { switch (activeTab) { case "appearance": return (
{ const fontOpt = findFontOption(fontOptions, String(v)) if (!fontOpt) return applyFontFamily(fontOpt.fontFamily) await setFontFamily(String(v)) messageApi.success(t("settings.general.saved")) }} style={{ width: "320px" }} loading={isLoadingFonts || fontLoading} options={fontOptions.map((opt) => ({ value: opt.value, label: opt.label, searchText: opt.searchText, }))} showSearch filterOption={(input, option) => String(option?.searchText ?? option?.label ?? "") .toLowerCase() .includes(input.toLowerCase()) } /> {!isMobile && ( <> { await setWindowZoom(Number(v)) messageApi.success(t("settings.general.saved")) }} style={{ width: "320px" }} disabled={!canAdmin} options={[ { value: "0.7", label: t("settings.zoomOptions.small70") }, { value: "0.8", label: "80%" }, { value: "0.9", label: "90%" }, { value: "1.0", label: t("settings.zoomOptions.default100") }, { value: "1.1", label: "110%" }, { value: "1.2", label: "120%" }, { value: "1.3", label: "130%" }, { value: "1.5", label: t("settings.zoomOptions.large150") }, ]} />
) default: return (
{t("common.comingSoon")}
) } } return ( {contextHolder} {!isMobile && (
{t("settings.title")}
setActiveTab(key)} style={{ borderRight: 0 }} /> )} {/* 窗口标题栏 */}
{t("settings.title")}
{renderContent()}
{/* Mobile bottom navigation */} {isMobile && (
{menuItems.map((item) => (
setActiveTab(item.key)} style={{ textAlign: "center", color: activeTab === item.key ? "var(--ant-color-primary)" : "var(--ss-text-secondary)", cursor: "pointer", }} > {item.icon}
{item.label}
))}
)} ) }