feat(settings): 统一可选模块开关并隐藏停用入口
This commit is contained in:
1 parent
f34ba88db9
commit
49895b34e4
14 files changed
+97
-24
No files matched your search
@@ -1155,6 +1155,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 ذاكرة حزم P
|
||||
选择显示项目 اختيار العناصر
|
||||
选择语言 اختيار اللغة
|
||||
通用 عام
|
||||
可选功能 ميزات اختيارية
|
||||
通电时间 ساعات التشغيل
|
||||
通电次数 دورات التشغيل
|
||||
通知 الإشعارات
|
||||
|
||||
|
@@ -1155,6 +1155,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Von uv geladener Py
|
||||
选择显示项目 Elemente auswählen
|
||||
选择语言 Sprache auswählen
|
||||
通用 Allgemein
|
||||
可选功能 Optionale Funktionen
|
||||
通电时间 Betriebsstunden
|
||||
通电次数 Einschaltvorgänge
|
||||
通知 Mitteilungen
|
||||
|
||||
|
@@ -1155,6 +1155,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Cachés de paquetes
|
||||
选择显示项目 Elegir elementos
|
||||
选择语言 Elegir idioma
|
||||
通用 General
|
||||
可选功能 Funciones opcionales
|
||||
通电时间 Horas encendido
|
||||
通电次数 Ciclos de encendido
|
||||
通知 Notificaciones
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -1155,6 +1155,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 uvのPythonパッ
|
||||
选择显示项目 表示項目を選択
|
||||
选择语言 言語を選択
|
||||
通用 一般
|
||||
可选功能 オプション機能
|
||||
通电时间 通電時間
|
||||
通电次数 電源投入回数
|
||||
通知 通知
|
||||
|
||||
|
@@ -1155,6 +1155,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 uv의 Python 패키
|
||||
选择显示项目 표시 항목 선택
|
||||
选择语言 언어 선택
|
||||
通用 일반
|
||||
可选功能 선택 기능
|
||||
通电时间 전원 켜짐 시간
|
||||
通电次数 전원 켜짐 횟수
|
||||
通知 알림
|
||||
|
||||
|
@@ -1156,6 +1156,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 uv 下載的 Python
|
||||
选择显示项目 選擇顯示項目
|
||||
选择语言 選擇語言
|
||||
通用 通用
|
||||
可选功能 可選功能
|
||||
通电时间 通電時間
|
||||
通电次数 通電次數
|
||||
通知 通知
|
||||
|
||||
|
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user