From 49895b34e42e4d12407fb588b1a36bf6a6fcdbd8 Mon Sep 17 00:00:00 2001 From: ysicing Date: Wed, 23 Sep 2026 11:14:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(settings):=20=E7=BB=9F=E4=B8=80=E5=8F=AF?= =?UTF-8?q?=E9=80=89=E6=A8=A1=E5=9D=97=E5=BC=80=E5=85=B3=E5=B9=B6=E9=9A=90?= =?UTF-8?q?=E8=97=8F=E5=81=9C=E7=94=A8=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Localization/Resources/ar.tsv | 1 + .../Sources/Localization/Resources/de.tsv | 1 + .../Sources/Localization/Resources/es.tsv | 1 + .../Sources/Localization/Resources/fr.tsv | 1 + .../Sources/Localization/Resources/ja.tsv | 1 + .../Sources/Localization/Resources/ko.tsv | 1 + .../Localization/Resources/zh-Hant.tsv | 1 + .../Sources/Localization/Translations.swift | 1 + .../XStatsUI/AppKit/AppController.swift | 3 +- .../XStatsUI/Calendar/CalendarSettings.swift | 7 ++- .../XStatsUI/Panel/MainWindowView.swift | 6 ++- .../XStatsUI/Settings/SettingsPages.swift | 35 ++++++++++++--- .../Sources/XStatsUI/State/AppSettings.swift | 19 +++++--- .../AIUsageControllerTests.swift | 43 +++++++++++++++---- 14 files changed, 97 insertions(+), 24 deletions(-) diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv index 0167265..9726161 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv @@ -1155,6 +1155,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 ذاكرة حزم P 选择显示项目 اختيار العناصر 选择语言 اختيار اللغة 通用 عام +可选功能 ميزات اختيارية 通电时间 ساعات التشغيل 通电次数 دورات التشغيل 通知 الإشعارات diff --git a/Packages/XStatsKit/Sources/Localization/Resources/de.tsv b/Packages/XStatsKit/Sources/Localization/Resources/de.tsv index 2e2b236..f2f04f6 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/de.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/de.tsv @@ -1155,6 +1155,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Von uv geladener Py 选择显示项目 Elemente auswählen 选择语言 Sprache auswählen 通用 Allgemein +可选功能 Optionale Funktionen 通电时间 Betriebsstunden 通电次数 Einschaltvorgänge 通知 Mitteilungen diff --git a/Packages/XStatsKit/Sources/Localization/Resources/es.tsv b/Packages/XStatsKit/Sources/Localization/Resources/es.tsv index b084083..b856611 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/es.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/es.tsv @@ -1155,6 +1155,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Cachés de paquetes 选择显示项目 Elegir elementos 选择语言 Elegir idioma 通用 General +可选功能 Funciones opcionales 通电时间 Horas encendido 通电次数 Ciclos de encendido 通知 Notificaciones diff --git a/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv b/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv index cda7275..f027014 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv @@ -1155,6 +1155,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Caches de paquets P 选择显示项目 Choisir les éléments 选择语言 Choisir une langue 通用 Général +可选功能 Fonctionnalités facultatives 通电时间 Heures sous tension 通电次数 Cycles d’alimentation 通知 Notifications diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv index 61825d2..3e891f9 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv @@ -1155,6 +1155,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 uvのPythonパッ 选择显示项目 表示項目を選択 选择语言 言語を選択 通用 一般 +可选功能 オプション機能 通电时间 通電時間 通电次数 電源投入回数 通知 通知 diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv index 931c483..1a763b3 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv @@ -1155,6 +1155,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 uv의 Python 패키 选择显示项目 표시 항목 선택 选择语言 언어 선택 通用 일반 +可选功能 선택 기능 通电时间 전원 켜짐 시간 通电次数 전원 켜짐 횟수 通知 알림 diff --git a/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv b/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv index 104c31f..2d4455f 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv @@ -1156,6 +1156,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 uv 下載的 Python 选择显示项目 選擇顯示項目 选择语言 選擇語言 通用 通用 +可选功能 可選功能 通电时间 通電時間 通电次数 通電次數 通知 通知 diff --git a/Packages/XStatsKit/Sources/Localization/Translations.swift b/Packages/XStatsKit/Sources/Localization/Translations.swift index bca1171..ab97809 100644 --- a/Packages/XStatsKit/Sources/Localization/Translations.swift +++ b/Packages/XStatsKit/Sources/Localization/Translations.swift @@ -1184,6 +1184,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Python package and 适中 Moderate 选择显示项目 Choose Items 通用 General +可选功能 Optional Features 通电时间 Power-on hours 通电次数 Power cycles 通知 Notifications diff --git a/Packages/XStatsKit/Sources/XStatsUI/AppKit/AppController.swift b/Packages/XStatsKit/Sources/XStatsUI/AppKit/AppController.swift index 7d6f840..ba96470 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/AppKit/AppController.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/AppKit/AppController.swift @@ -295,7 +295,8 @@ public final class AppController: NSObject, NSApplicationDelegate { Task { @MainActor in guard let self else { return } self.model.aiUsage.start() - self.menuBar.refreshImages() + // 模块关闭时菜单栏保留展示偏好但隐藏实际项目,切换主开关需要重算布局。 + self.menuBar.update() self.menuBar.refreshPopoverHeight() self.observeAIUsageSchedule() } diff --git a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarSettings.swift b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarSettings.swift index c7ff15c..b7d9e4a 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarSettings.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarSettings.swift @@ -12,7 +12,12 @@ struct CalendarSettings: View { SettingsGroup(caption: tr("日历")) { GroupRow(showsDivider: false) { SettingRow(title: tr("菜单栏日历"), subtitle: tr("独立显示日期,点击打开月历;不受指标合并布局影响"), icon: "calendar") { - DSToggle(isOn: $settings.calendarEnabled, label: tr("菜单栏日历")) + if settings.calendarEnabled { + StatusBadge(text: tr("已启用"), tone: .success) + } else { + Button(tr("设置")) { settings.panelTab = .settingsGeneral } + .buttonStyle(DSButtonStyle(kind: .secondary)) + } } } if settings.calendarEnabled { diff --git a/Packages/XStatsKit/Sources/XStatsUI/Panel/MainWindowView.swift b/Packages/XStatsKit/Sources/XStatsUI/Panel/MainWindowView.swift index 2c092f6..a21450d 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Panel/MainWindowView.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Panel/MainWindowView.swift @@ -168,7 +168,7 @@ private struct MainSidebar: View { private var navigation: some View { VStack(alignment: .leading, spacing: DS.Space.s1) { - group(tr("监控"), PanelTab.monitors) + group(tr("监控"), PanelTab.monitors.filter { $0 != .aiUsage || model.settings.aiUsageEnabled }) group(tr("工具"), PanelTab.tools) group(tr("设置"), PanelTab.settings) } @@ -261,12 +261,14 @@ private struct PageHeader: View { HStack(spacing: DS.Space.s3) { Text(tr("在菜单栏显示")).dsFont(.xs).foregroundStyle(DS.Palette.textSecondary) ForEach(items) { item in + let moduleEnabled = item != .aiUsage || settings.aiUsageEnabled if items.count > 1 { Text(item.popoverTitle).dsFont(.xs).foregroundStyle(DS.Palette.textTertiary) } DSToggle(isOn: Binding(get: { settings.isEnabled(item) }, set: { settings.setEnabled(item, $0) }), label: tr("在菜单栏显示\(item.title)")) - .help(tr("开启后图标出现在菜单栏;按住 ⌘ 键拖动图标可以调整位置")) + .disabled(!moduleEnabled) + .help(moduleEnabled ? tr("开启后图标出现在菜单栏;按住 ⌘ 键拖动图标可以调整位置") : tr("尚未启用")) } } .modifier(HeaderControlGroup()) diff --git a/Packages/XStatsKit/Sources/XStatsUI/Settings/SettingsPages.swift b/Packages/XStatsKit/Sources/XStatsUI/Settings/SettingsPages.swift index b9b7808..17b572e 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Settings/SettingsPages.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Settings/SettingsPages.swift @@ -104,6 +104,23 @@ struct GeneralSettings: View { } } + SettingsGroup(caption: tr("可选功能")) { + GroupRow(showsDivider: false) { + SettingRow(title: tr("AI 使用统计"), + subtitle: tr("只读本机 Codex / Claude Code 会话日志,按模型统计 Token;不读取登录凭据,不查询订阅额度。"), + icon: "sparkles") { + DSToggle(isOn: $settings.aiUsageEnabled, label: tr("AI 使用统计")) + } + } + GroupRow { + SettingRow(title: tr("菜单栏日历"), + subtitle: tr("独立显示日期,点击打开月历;不受指标合并布局影响"), + icon: "calendar") { + DSToggle(isOn: $settings.calendarEnabled, label: tr("菜单栏日历")) + } + } + } + HotKeySettings() } } @@ -325,14 +342,22 @@ struct MenuBarSettings: View { text: tr("菜单栏里的图标可以调整顺序:按住 ⌘ 键拖动任意一个,松开后位置会一直保留。新开启的项目由系统安排位置,可能离其他图标较远,拖一下就能挪到一起。")) } ForEach(Array(MenuBarItem.allCases.enumerated()), id: \.element) { index, item in + let moduleEnabled = item != .aiUsage || settings.aiUsageEnabled GroupRow { VStack(alignment: .leading, spacing: DS.Space.s3) { - SettingRow(title: item.title, subtitle: itemSubtitle(item), icon: item.symbol) { - DSToggle(isOn: Binding(get: { settings.isEnabled(item) }, - set: { settings.setEnabled(item, $0) }), - label: item.title) + SettingRow(title: item.title, + subtitle: moduleEnabled ? itemSubtitle(item) : tr("尚未启用"), + icon: item.symbol) { + if moduleEnabled { + DSToggle(isOn: Binding(get: { settings.isEnabled(item) }, + set: { settings.setEnabled(item, $0) }), + label: item.title) + } else { + Button(tr("设置")) { settings.panelTab = .settingsGeneral } + .buttonStyle(DSButtonStyle(kind: .secondary)) + } } - if settings.isEnabled(item) { + if moduleEnabled, settings.isEnabled(item) { ItemStyleRow(item: item) if !item.popoverSections.isEmpty { PopoverSectionPicker(item: item) } } diff --git a/Packages/XStatsKit/Sources/XStatsUI/State/AppSettings.swift b/Packages/XStatsKit/Sources/XStatsUI/State/AppSettings.swift index 94baab1..dd768ec 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/State/AppSettings.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/State/AppSettings.swift @@ -427,7 +427,10 @@ public final class AppSettings { } /// 本机 AI 日志统计默认不启用,独立于系统指标的秒级采样。 public var aiUsageEnabled: Bool { - didSet { defaults.set(aiUsageEnabled, forKey: Keys.aiUsageEnabled) } + didSet { + defaults.set(aiUsageEnabled, forKey: Keys.aiUsageEnabled) + if !aiUsageEnabled && panelTab == .aiUsage { panelTab = .settingsGeneral } + } } /// 来源开关只保存在本机;关闭后不扫描,也不显示其历史缓存。 public var aiUsageSources: Set { @@ -573,7 +576,8 @@ public final class AppSettings { }) refreshSeconds = Self.refreshOptions.contains(defaults.integer(forKey: Keys.refreshSeconds)) ? defaults.integer(forKey: Keys.refreshSeconds) : 2 - aiUsageEnabled = defaults.bool(forKey: Keys.aiUsageEnabled) + let isAIUsageEnabled = defaults.bool(forKey: Keys.aiUsageEnabled) + aiUsageEnabled = isAIUsageEnabled aiUsageSources = defaults.stringArray(forKey: Keys.aiUsageSources) .map { Set($0.compactMap(AIProviderID.init(rawValue:))) } ?? Set(AIProviderID.allCases) aiUsageRefreshMinutes = Self.aiUsageRefreshOptions.contains(defaults.integer(forKey: Keys.aiUsageRefreshMinutes)) @@ -586,7 +590,8 @@ public final class AppSettings { fanSafetyTemperature = Self.fanSafetyOptions.contains(defaults.integer(forKey: Keys.fanSafetyTemperature)) ? defaults.integer(forKey: Keys.fanSafetyTemperature) : 95 let savedPanelTab = defaults.string(forKey: Keys.panelTab).flatMap(PanelTab.init(rawValue:)) ?? .overview - panelTab = savedPanelTab == .settingsAccount ? .settingsGeneral : savedPanelTab + panelTab = savedPanelTab == .settingsAccount || (savedPanelTab == .aiUsage && !isAIUsageEnabled) + ? .settingsGeneral : savedPanelTab appearance = defaults.string(forKey: Keys.appearance).flatMap(AppearanceMode.init(rawValue:)) ?? .system showDockIcon = defaults.bool(forKey: Keys.showDockIcon) speedTestBudget = defaults.string(forKey: Keys.speedTestBudget).flatMap(SpeedTestBudget.init(rawValue:)) ?? .full @@ -616,9 +621,11 @@ public final class AppSettings { ? defaults.integer(forKey: Keys.cpuChartSeconds) : 60 } - /// 按固定顺序返回已启用的菜单栏项目 + /// 按固定顺序返回当前实际显示的菜单栏项目;可选模块关闭时保留展示偏好但不渲染。 var orderedMenuBarItems: [MenuBarItem] { - MenuBarItem.allCases.filter(menuBarItems.contains) + MenuBarItem.allCases.filter { item in + menuBarItems.contains(item) && (item != .aiUsage || aiUsageEnabled) + } } func isEnabled(_ item: MenuBarItem) -> Bool { menuBarItems.contains(item) } @@ -647,8 +654,6 @@ public final class AppSettings { func setEnabled(_ item: MenuBarItem, _ enabled: Bool) { if enabled { menuBarItems.insert(item) - // 菜单栏项是本机用量统计最自然的发现入口;扫描没开的话只会显示一个永久的占位符。 - if item == .aiUsage { aiUsageEnabled = true } } else { menuBarItems.remove(item) } diff --git a/Packages/XStatsKit/Tests/XStatsUITests/AIUsageControllerTests.swift b/Packages/XStatsKit/Tests/XStatsUITests/AIUsageControllerTests.swift index a00b74a..691ba2c 100644 --- a/Packages/XStatsKit/Tests/XStatsUITests/AIUsageControllerTests.swift +++ b/Packages/XStatsKit/Tests/XStatsUITests/AIUsageControllerTests.swift @@ -126,20 +126,47 @@ private actor GatedUsageProvider: AIUsageProvider { #expect(settings.aiUsageRefreshMinutes == 30) } - /// 菜单栏项是这个统计最自然的发现入口。开了却不扫描,用户只会看到一个 - /// 永远不解释自己的 “AI —”,所以打开菜单栏项要一并打开扫描。 - @Test func enablingTheMenuBarItemAlsoTurnsScanningOn() { + @Test func disablingAIUsageLeavesItsHiddenPageAndRestoresNavigationOnEnable() { + let defaults = defaultsForAIUsage() + let settings = AppSettings(defaults: defaults) + + settings.aiUsageEnabled = true + settings.panelTab = .aiUsage + settings.aiUsageEnabled = false + + #expect(settings.panelTab == .settingsGeneral) + #expect(AppSettings(defaults: defaults).panelTab == .settingsGeneral) + + settings.aiUsageEnabled = true + settings.panelTab = .aiUsage + #expect(settings.panelTab == .aiUsage) + } + + @Test func disabledAIUsageDoesNotRestoreItsSavedPage() { + let defaults = defaultsForAIUsage() + defaults.set("aiUsage", forKey: "panelTab") + + #expect(AppSettings(defaults: defaults).panelTab == .settingsGeneral) + } + + /// 模块开关控制是否读取日志,菜单栏开关只保存展示偏好;模块关闭时不应显示占位图标, + /// 重新启用后应恢复用户原来的菜单栏选择。 + @Test func menuBarPreferenceDoesNotEnableScanningAndIsRestoredWithTheModule() { let settings = AppSettings(defaults: defaultsForAIUsage()) #expect(!settings.aiUsageEnabled) settings.setEnabled(.aiUsage, true) - #expect(settings.aiUsageEnabled) - #expect(settings.isEnabled(.aiUsage)) - // 其他指标不应该有这种副作用 - settings.aiUsageEnabled = false - settings.setEnabled(.cpu, true) #expect(!settings.aiUsageEnabled) + #expect(settings.isEnabled(.aiUsage)) + #expect(!settings.orderedMenuBarItems.contains(.aiUsage)) + + settings.aiUsageEnabled = true + #expect(settings.orderedMenuBarItems.contains(.aiUsage)) + + settings.aiUsageEnabled = false + #expect(settings.isEnabled(.aiUsage)) + #expect(!settings.orderedMenuBarItems.contains(.aiUsage)) } @Test func transientFailurePreservesLastGoodSnapshotAsStale() async {