feat(settings): 统一可选模块开关并隐藏停用入口

This commit is contained in:
ysicing committed 2026-09-23 11:14:49 +08:00
1 parent f34ba88db9
commit 49895b34e4
14 files changed
+97 -24

No files matched your search

@@ -1155,6 +1155,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 ذاكرة حزم P
选择显示项目 اختيار العناصر
选择语言 اختيار اللغة
通用 عام
可选功能 ميزات اختيارية
通电时间 ساعات التشغيل
通电次数 دورات التشغيل
通知 الإشعارات
1 # Copyright (C) 2026 ysicing
1155 选择显示项目
1156 选择语言
1157 通用
1158 可选功能
1159 通电时间
1160 通电次数
1161 通知
@@ -1155,6 +1155,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Von uv geladener Py
选择显示项目 Elemente auswählen
选择语言 Sprache auswählen
通用 Allgemein
可选功能 Optionale Funktionen
通电时间 Betriebsstunden
通电次数 Einschaltvorgänge
通知 Mitteilungen
1 # Copyright (C) 2026 ysicing
1155 选择显示项目
1156 选择语言
1157 通用
1158 可选功能
1159 通电时间
1160 通电次数
1161 通知
@@ -1155,6 +1155,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Cachés de paquetes
选择显示项目 Elegir elementos
选择语言 Elegir idioma
通用 General
可选功能 Funciones opcionales
通电时间 Horas encendido
通电次数 Ciclos de encendido
通知 Notificaciones
1 # Copyright (C) 2026 ysicing
1155 选择显示项目
1156 选择语言
1157 通用
1158 可选功能
1159 通电时间
1160 通电次数
1161 通知
@@ -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
1 # Copyright (C) 2026 ysicing
1155 选择显示项目
1156 选择语言
1157 通用
1158 可选功能
1159 通电时间
1160 通电次数
1161 通知
@@ -1155,6 +1155,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 uvのPythonパッ
选择显示项目 表示項目を選択
选择语言 言語を選択
通用 一般
可选功能 オプション機能
通电时间 通電時間
通电次数 電源投入回数
通知 通知
1 # Copyright (C) 2026 ysicing
1155 选择显示项目
1156 选择语言
1157 通用
1158 可选功能
1159 通电时间
1160 通电次数
1161 通知
@@ -1155,6 +1155,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 uv의 Python 패키
选择显示项目 표시 항목 선택
选择语言 언어 선택
通用 일반
可选功能 선택 기능
通电时间 전원 켜짐 시간
通电次数 전원 켜짐 횟수
通知 알림
1 # Copyright (C) 2026 ysicing
1155 选择显示项目
1156 选择语言
1157 通用
1158 可选功能
1159 通电时间
1160 通电次数
1161 通知
@@ -1156,6 +1156,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 uv 下載的 Python
选择显示项目 選擇顯示項目
选择语言 選擇語言
通用 通用
可选功能 可選功能
通电时间 通電時間
通电次数 通電次數
通知 通知
1 # Copyright (c) 2026 GiantAccel, LLC
1156 选择显示项目
1157 选择语言
1158 通用
1159 可选功能
1160 通电时间
1161 通电次数
1162 通知
@@ -1184,6 +1184,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Python package and
适中 Moderate
选择显示项目 Choose Items
通用 General
可选功能 Optional Features
通电时间 Power-on hours
通电次数 Power cycles
通知 Notifications
@@ -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()
}
@@ -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 {
@@ -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())
@@ -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) }
}
@@ -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<AIProviderID> {
@@ -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)
}
@@ -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 {