Files
OpenStats/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppSettings.swift
T
gentpan c9d722f20d 账号同步、磁盘工具、IP 归属地数据源与纯净度、弹窗与设置调整
- 账号与同步:GitHub / Google / Apple 登录后把菜单栏项目与风格等偏好同步到 getopenstats.com,换一台 Mac 登录即恢复(AccountSync 模块、SyncController、SettingsDocument、账号设置页,openstats:// 回调)
- 磁盘页成为工具页:已用 / 可清除 / 可用分段容量条、空间分析、文件系统检查、本地快照清理(辅助工具协议 v4)、其他磁盘推出
- 公网 IP:归属地数据源可选 CleanIP.io / ipapi.is / DB-IP / ipinfo.io / 本地 GeoLite2,每台 Mac 自己查、本地缓存 7 天;IPv4 / IPv6 双栈各自查询与切换;弹窗显示 ASN、运营方、原生 / 住宅等徽章与纯净度评分色带;接口区块可重置上传 / 下载统计
- 弹窗:右上角按钮改为该指标自己的图标 + 总设置;主窗口页面标题栏的开关点不动已修复;滚动条改为悬浮式,内容不再偏左;网络弹窗卡片被标题行撑宽的问题已修复
- 设置:⌘ 拖动排序说明;每个菜单栏项目单独选风格并带实时预览;网络与磁盘的进程榜单固定 5 行并平滑排序;CPU 持续高负载通知
- 设计系统:分段条、评分色带、标签徽章、胶囊切换、刷新按钮等组件与徽章配色 token
- 更新日志、架构说明、README 与译文同步
2026-09-15 03:37:24 +05:00

642 lines
26 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import Localization
import Metrics
import Observation
public enum MenuBarItem: String, CaseIterable, Identifiable, Sendable {
case cpu, memory, network, gpu, disk, temperature, fan
public var id: String { rawValue }
var title: String {
switch self {
case .cpu: "CPU"
case .memory: tr("内存")
case .network: tr("网络")
case .gpu: "GPU"
case .disk: tr("磁盘")
case .temperature: tr("CPU 温度")
case .fan: tr("风扇转速")
}
}
var subtitle: String {
switch self {
case .cpu: tr("总占用")
case .memory: tr("已用内存占比")
case .network: tr("上传与下载速度、IP 地址、DNS")
case .gpu: tr("GPU 占用")
case .disk: tr("启动磁盘已用占比")
case .temperature: tr("CPU 核心最高温度")
case .fan: tr("转速最高的风扇")
}
}
/// 菜单栏上方的小标签
var menuBarLabel: String {
switch self {
case .cpu: "CPU"
case .memory: "RAM"
case .network: "NET"
case .gpu: "GPU"
case .disk: "DISK"
case .temperature: "TEMP"
case .fan: "FAN"
}
}
/// 详情弹窗标题
var popoverTitle: String {
switch self {
case .cpu: "CPU"
case .memory: tr("内存")
case .network: tr("网络")
case .gpu: "GPU"
case .disk: tr("磁盘")
case .temperature: tr("温度")
case .fan: tr("风扇")
}
}
/// 点击该项弹出的详情里可以显示的内容,按显示顺序排列
var popoverSections: [PopoverSection] {
switch self {
case .cpu: [.cpuCores, .cpuHeatmap, .cpuClusters, .cpuLoadAverage, .cpuApps]
case .memory: [.memoryWaterline, .memoryCompression, .memoryApps]
case .network: [.networkHistory, .networkProbe, .networkInterface, .networkAddresses, .networkPurity, .networkDNS, .networkProcesses]
case .gpu: [.gpuHistory, .gpuDetails]
case .disk: [.diskActivity, .diskHealth, .diskProcesses]
case .temperature: [.thermalSensors, .thermalFans, .thermalPower]
case .fan: [.thermalFans, .thermalSensors, .thermalPower]
}
}
/// 可以单独指定风格的指标(网速有自己的样式)
var supportsStyleOverride: Bool { self != .network }
var symbol: String {
switch self {
case .cpu: "cpu"
case .memory: "memorychip"
case .network: "arrow.up.arrow.down"
case .gpu: "square.3.layers.3d"
case .disk: "internaldrive"
case .temperature: "thermometer.medium"
case .fan: "fan"
}
}
}
/// 详情弹窗里可以单独隐藏的区块
public enum PopoverSection: String, CaseIterable, Identifiable, Sendable {
case cpuHeatmap, cpuCores, cpuClusters, cpuLoadAverage, cpuApps
case memoryWaterline, memoryCompression, memoryApps
case networkHistory, networkProbe, networkInterface, networkAddresses, networkPurity, networkDNS, networkProcesses
case gpuHistory, gpuDetails
case diskActivity, diskHealth, diskProcesses
case thermalSensors, thermalFans, thermalPower
public var id: String { rawValue }
/// 弹窗默认不显示的区块(主窗口详情页始终显示):热力图已经能看出各核心此刻的占用,
/// 每核柱状图给想要一眼看清单个核心的人在设置里打开,避免弹窗默认太长
static let hiddenByDefault: Set<PopoverSection> = [.cpuCores]
var title: String {
switch self {
case .cpuHeatmap: tr("核心热力图")
case .cpuCores: tr("各核心占用")
case .cpuClusters: tr("核心分工")
case .cpuLoadAverage: tr("排队程度")
case .cpuApps, .memoryApps: tr("按应用汇总")
case .networkProcesses: tr("高占用进程")
case .memoryWaterline: tr("内存水位")
case .memoryCompression: tr("压缩与交换")
case .networkHistory: tr("流量历史")
case .networkProbe: tr("连接探测")
case .networkInterface: tr("接口")
case .networkAddresses: tr("IP 地址")
case .networkPurity: tr("IP 纯净度")
case .networkDNS: "DNS"
case .gpuHistory: tr("使用历史")
case .gpuDetails: tr("显卡信息")
case .diskActivity: tr("读写速度")
case .diskHealth: tr("SSD 健康")
case .diskProcesses: tr("读写最多的应用")
case .thermalSensors: tr("温度")
case .thermalFans: tr("风扇")
case .thermalPower: tr("功耗")
}
}
}
/// 菜单栏布局
public enum MenuBarLayout: String, CaseIterable, Identifiable, Sendable {
/// 每个指标一个图标,点击弹出该项详情
case separate
/// 所有指标合成一个图标,点击打开主窗口
case combined
public var id: String { rawValue }
var title: String {
switch self {
case .separate: tr("每项独立")
case .combined: tr("合并为一个")
}
}
}
/// 连接探测的目标
public enum ProbeTarget: String, CaseIterable, Identifiable, Sendable {
case cloudflare, google, aliyun, tencent, gateway
public var id: String { rawValue }
var title: String {
switch self {
case .cloudflare: tr("Cloudflare(1.1.1.1)")
case .google: tr("Google(8.8.8.8)")
case .aliyun: tr("阿里云(223.5.5.5)")
case .tencent: tr("腾讯(119.29.29.29)")
case .gateway: tr("路由器(网关)")
}
}
/// 网关地址随网络变化,由调用方传入
func address(router: String?) -> String? {
switch self {
case .cloudflare: "1.1.1.1"
case .google: "8.8.8.8"
case .aliyun: "223.5.5.5"
case .tencent: "119.29.29.29"
case .gateway: router
}
}
}
/// 公网 IP 归属地的数据源
public enum GeoSource: String, CaseIterable, Identifiable, Sendable {
case cleanIP, ipapi, dbip, ipinfo, localDatabase
public var id: String { rawValue }
var title: String {
switch self {
case .cleanIP: "CleanIP.io"
case .ipapi: "ipapi.is"
case .dbip: "DB-IP"
case .ipinfo: "ipinfo.io"
case .localDatabase: tr("本地 GeoLite2 数据库")
}
}
var detail: String {
switch self {
case .cleanIP: tr("归属地、ASN、网络类型、原生 / 广播、纯净度与风险评分,中文地名;把公网 IP 发给 cleanip.io")
case .ipapi: tr("归属地与 ASN;把公网 IP 发给 ipapi.is,匿名额度每天 30 次,同一 IP 的结果记住一天")
case .dbip: tr("只有国家、省 / 州与城市;把公网 IP 发给 db-ip.com")
case .ipinfo: tr("归属地与 ASN;把公网 IP 发给 ipinfo.io,匿名额度很小,容易被限流")
case .localDatabase: tr("在本机的 MaxMind 数据库里查国家、城市与 ASN,不向任何服务发送 IP;数据库未就绪时暂用 CleanIP.io")
}
}
/// 对应的在线数据源;本地数据库没有
var provider: GeoProvider? {
switch self {
case .cleanIP: .cleanIP
case .ipapi: .ipapi
case .dbip: .dbip
case .ipinfo: .ipinfo
case .localDatabase: nil
}
}
}
/// 菜单栏风格:整体选一种套用到所有指标,个别指标可以单独指定(AppSettings.styleOverrides);风格内部的排版保持一致
public enum MenuBarStyle: String, CaseIterable, Identifiable, Sendable {
case stacked, inline, icon, ring, pie, history, line, meter, dot
public var id: String { rawValue }
/// 温度、风扇没有百分比,图形类风格画不出来,只给文字类的三种
static func options(for item: MenuBarItem) -> [MenuBarStyle] {
switch item {
case .temperature, .fan: [.stacked, .inline, .icon]
default: allCases
}
}
var title: String {
switch self {
case .stacked: tr("双行文字")
case .inline: tr("单行文字")
case .icon: tr("图标")
case .ring: tr("圆环")
case .pie: tr("饼图")
case .history: tr("柱状历史")
case .line: tr("折线历史")
case .meter: tr("电量条")
case .dot: tr("状态圆点")
}
}
var detail: String {
switch self {
case .stacked: tr("小标签在上、数值在下,最紧凑")
case .inline: tr("标签与数值同一行,最易读")
case .icon: tr("用系统图标代替文字标签")
case .ring: tr("圆环表示当前占用比例")
case .pie: tr("饼图表示当前占用比例")
case .history: tr("10 根柱子是最近 10 次采样(约 20 秒)的变化")
case .line: tr("折线是最近 30 次采样(约 1 分钟)的走势")
case .meter: tr("竖向电量条表示当前占用比例")
case .dot: tr("绿色正常、橙色偏高(60% 以上)、红色很高(85% 以上)")
}
}
}
public enum NetworkMenuStyle: String, CaseIterable, Identifiable, Sendable {
case dots, arrows, inline
public var id: String { rawValue }
var title: String {
switch self {
case .dots: tr("双行圆点")
case .arrows: tr("双行箭头")
case .inline: tr("单行")
}
}
}
extension AppLanguage: Identifiable {
public var id: String { rawValue }
/// 语言名称用各自的文字写,不随界面语言翻译
var title: String {
switch self {
case .system: tr("跟随系统")
case .chinese: "简体中文"
case .english: "English"
}
}
}
public enum AppearanceMode: String, CaseIterable, Identifiable, Sendable {
case system, light, dark
public var id: String { rawValue }
var title: String {
switch self {
case .system: tr("跟随系统")
case .light: tr("浅色")
case .dark: tr("深色")
}
}
}
/// 主窗口侧边栏的页面
public enum PanelTab: String, CaseIterable, Identifiable, Sendable {
case overview, system, history, cpu, gpu, memory, disk, network, thermal, processes, keepAwake, cleaner, uninstaller, startupItems
case settingsGeneral, settingsMenuBar, settingsNotifications, settingsAccount, settingsHelper, settingsAbout
public var id: String { rawValue }
static let monitors: [PanelTab] = [.overview, .system, .history, .cpu, .gpu, .memory, .disk, .network, .thermal]
static let tools: [PanelTab] = [.processes, .startupItems, .keepAwake, .cleaner, .uninstaller]
static let settings: [PanelTab] = [.settingsGeneral, .settingsMenuBar, .settingsNotifications, .settingsAccount, .settingsHelper, .settingsAbout]
var isSettings: Bool { Self.settings.contains(self) }
/// 页面顶栏的标题:设置页带上分组名
var headerTitle: String { isSettings ? tr("设置 · \(title)") : title }
var title: String {
switch self {
case .overview: tr("仪表盘")
case .system: tr("本机信息")
case .history: tr("历史")
case .cpu: "CPU"
case .gpu: "GPU"
case .memory: tr("内存")
case .disk: tr("磁盘")
case .network: tr("网络")
case .thermal: tr("温度与风扇")
case .processes: tr("进程")
case .keepAwake: tr("防休眠")
case .cleaner: tr("清理")
case .uninstaller: tr("卸载应用")
case .startupItems: tr("启动项")
case .settingsGeneral: tr("通用")
case .settingsMenuBar: tr("菜单栏")
case .settingsNotifications: tr("通知")
case .settingsAccount: tr("账号与同步")
case .settingsHelper: tr("辅助工具")
case .settingsAbout: tr("关于")
}
}
var symbol: String {
switch self {
case .overview: "square.grid.2x2"
case .system: "laptopcomputer"
case .history: "clock.arrow.circlepath"
case .cpu: "cpu"
case .gpu: "square.3.layers.3d"
case .memory: "memorychip"
case .disk: "internaldrive"
case .network: "network"
case .thermal: "fan"
case .processes: "list.bullet.rectangle"
case .keepAwake: "cup.and.saucer"
case .cleaner: "sparkles"
case .uninstaller: "trash"
case .startupItems: "power"
case .settingsGeneral: "gearshape"
case .settingsMenuBar: "menubar.rectangle"
case .settingsNotifications: "bell.badge"
case .settingsAccount: "person.crop.circle"
case .settingsHelper: "lock.shield"
case .settingsAbout: "info.circle"
}
}
/// 页面对应的菜单栏项目,页面右上角可直接开关
var menuBarItem: MenuBarItem? {
switch self {
case .cpu: .cpu
case .gpu: .gpu
case .memory: .memory
case .network: .network
case .disk: .disk
case .thermal: .temperature
default: nil
}
}
/// 页面右上角可以直接开关的菜单栏项目:温度与风扇页有温度、风扇两项
var menuBarItems: [MenuBarItem] {
switch self {
case .thermal: [.temperature, .fan]
default: menuBarItem.map { [$0] } ?? []
}
}
init(item: MenuBarItem) {
switch item {
case .cpu: self = .cpu
case .gpu: self = .gpu
case .memory: self = .memory
case .network: self = .network
case .disk: self = .disk
case .temperature, .fan: self = .thermal
}
}
}
/// UserDefaults 持久化的偏好设置
@MainActor
@Observable
public final class AppSettings {
@ObservationIgnored private let defaults: UserDefaults
public var menuBarItems: Set<MenuBarItem> {
didSet { defaults.set(menuBarItems.map(\.rawValue).sorted(), forKey: Keys.menuBarItems) }
}
public var menuBarStyle: MenuBarStyle {
didSet { defaults.set(menuBarStyle.rawValue, forKey: Keys.menuBarStyle) }
}
public var networkStyle: NetworkMenuStyle {
didSet { defaults.set(networkStyle.rawValue, forKey: Keys.networkStyle) }
}
/// 个别指标单独指定的风格;未指定的跟随 menuBarStyle
public var styleOverrides: [MenuBarItem: MenuBarStyle] {
didSet {
defaults.set(Dictionary(uniqueKeysWithValues: styleOverrides.map { ($0.key.rawValue, $0.value.rawValue) }),
forKey: Keys.styleOverrides)
}
}
public var refreshSeconds: Int {
didSet { defaults.set(refreshSeconds, forKey: Keys.refreshSeconds) }
}
public var colorizeHighLoad: Bool {
didSet { defaults.set(colorizeHighLoad, forKey: Keys.colorizeHighLoad) }
}
public var useFahrenheit: Bool {
didSet { defaults.set(useFahrenheit, forKey: Keys.useFahrenheit) }
}
/// 电量低于该值时自动关闭“合盖运行”
public var lidModeBatteryFloor: Int {
didSet { defaults.set(lidModeBatteryFloor, forKey: Keys.lidModeBatteryFloor) }
}
/// 自定义风扇模式下 CPU 达到该温度时恢复系统控制
public var fanSafetyTemperature: Int {
didSet { defaults.set(fanSafetyTemperature, forKey: Keys.fanSafetyTemperature) }
}
public var panelTab: PanelTab {
didSet { defaults.set(panelTab.rawValue, forKey: Keys.panelTab) }
}
public var appearance: AppearanceMode {
didSet { defaults.set(appearance.rawValue, forKey: Keys.appearance) }
}
public var menuBarLayout: MenuBarLayout {
didSet { defaults.set(menuBarLayout.rawValue, forKey: Keys.menuBarLayout) }
}
/// 用户在详情弹窗里隐藏的区块
public var hiddenPopoverSections: Set<PopoverSection> {
didSet { defaults.set(hiddenPopoverSections.map(\.rawValue).sorted(), forKey: Keys.hiddenPopoverSections) }
}
public var probeEnabled: Bool {
didSet { defaults.set(probeEnabled, forKey: Keys.probeEnabled) }
}
/// 网络详情关闭时,菜单栏显示网络项期间继续低频探测
public var probeInBackground: Bool {
didSet { defaults.set(probeInBackground, forKey: Keys.probeInBackground) }
}
public var probeSeconds: Int {
didSet { defaults.set(probeSeconds, forKey: Keys.probeSeconds) }
}
public var probeTarget: ProbeTarget {
didSet { defaults.set(probeTarget.rawValue, forKey: Keys.probeTarget) }
}
/// 公网 IP 归属地的数据源
public var geoSource: GeoSource {
didSet { defaults.set(geoSource.rawValue, forKey: Keys.geoSource) }
}
/// 打开网络详情时查询公网 IP(会访问 Cloudflare / ipify)
public var publicIPLookup: Bool {
didSet { defaults.set(publicIPLookup, forKey: Keys.publicIPLookup) }
}
/// 本地归属地库包含城市数据(GeoLite2-City,体积约为国家库的 7 倍)
public var geoIncludeCity: Bool {
didSet { defaults.set(geoIncludeCity, forKey: Keys.geoIncludeCity) }
}
/// 定期从官网检查归属地数据库更新
public var geoAutoUpdate: Bool {
didSet { defaults.set(geoAutoUpdate, forKey: Keys.geoAutoUpdate) }
}
/// 打开了系统通知的状况
public var enabledAlerts: Set<AlertKind> {
didSet { defaults.set(enabledAlerts.map(\.rawValue).sorted(), forKey: Keys.enabledAlerts) }
}
/// CPU 过热提醒的温度(摄氏度)
public var alertCPUTemperature: Int {
didSet { defaults.set(alertCPUTemperature, forKey: Keys.alertCPUTemperature) }
}
/// CPU 持续高负载提醒的总占用阈值(百分比)
public var alertCPULoad: Int {
didSet { defaults.set(alertCPULoad, forKey: Keys.alertCPULoad) }
}
/// CPU 详情顶部走势图显示最近多少秒
public var cpuChartSeconds: Int {
didSet { defaults.set(cpuChartSeconds, forKey: Keys.cpuChartSeconds) }
}
/// 界面语言:界面文字立即切换;系统提供的名称(显示器、应用名)在下次启动时跟着切换
public var language: AppLanguage {
didSet {
defaults.set(language.rawValue, forKey: Keys.language)
language.applyToProcessLocale(defaults: defaults)
L10n.configure(language)
}
}
nonisolated public static let languageKey = "language"
/// 全局快捷键
public var hotKeys: [HotKeyAction: HotKey] {
didSet {
let stored = Dictionary(uniqueKeysWithValues: hotKeys.map { ($0.key.rawValue, $0.value) })
defaults.set(try? JSONEncoder().encode(stored), forKey: Keys.hotKeys)
}
}
/// 每分钟把主要指标写入本机历史库
public var historyEnabled: Bool {
didSet { defaults.set(historyEnabled, forKey: Keys.historyEnabled) }
}
/// 启动时与每天检查一次新版本
public var autoCheckUpdates: Bool {
didSet { defaults.set(autoCheckUpdates, forKey: Keys.autoCheckUpdates) }
}
/// 可再生的缓存也先移到废纸篓(可恢复,但不会立即释放空间)
public var cleanPrefersTrash: Bool {
didSet { defaults.set(cleanPrefersTrash, forKey: Keys.cleanPrefersTrash) }
}
public static let refreshOptions = [1, 2, 3, 5]
public static let probeOptions = [1, 2, 5]
public static let batteryFloorOptions = [10, 20, 30, 40]
public static let fanSafetyOptions = [85, 90, 95, 100]
public static let alertTemperatureOptions = [85, 90, 95, 100]
public static let alertLoadOptions = [70, 80, 90]
public static let cpuChartOptions = [60, 180, 300]
public init(defaults: UserDefaults = .standard) {
self.defaults = defaults
let items = defaults.stringArray(forKey: Keys.menuBarItems)?.compactMap(MenuBarItem.init(rawValue:))
menuBarItems = Set(items ?? [.cpu, .memory, .network])
menuBarStyle = defaults.string(forKey: Keys.menuBarStyle).flatMap(MenuBarStyle.init(rawValue:)) ?? .stacked
networkStyle = defaults.string(forKey: Keys.networkStyle).flatMap(NetworkMenuStyle.init(rawValue:)) ?? .dots
let storedOverrides = defaults.dictionary(forKey: Keys.styleOverrides) as? [String: String] ?? [:]
styleOverrides = Dictionary(uniqueKeysWithValues: storedOverrides.compactMap { key, value in
guard let item = MenuBarItem(rawValue: key), let style = MenuBarStyle(rawValue: value) else { return nil }
return (item, style)
})
refreshSeconds = Self.refreshOptions.contains(defaults.integer(forKey: Keys.refreshSeconds))
? defaults.integer(forKey: Keys.refreshSeconds) : 2
colorizeHighLoad = defaults.bool(forKey: Keys.colorizeHighLoad)
useFahrenheit = defaults.bool(forKey: Keys.useFahrenheit)
lidModeBatteryFloor = Self.batteryFloorOptions.contains(defaults.integer(forKey: Keys.lidModeBatteryFloor))
? defaults.integer(forKey: Keys.lidModeBatteryFloor) : 20
fanSafetyTemperature = Self.fanSafetyOptions.contains(defaults.integer(forKey: Keys.fanSafetyTemperature))
? defaults.integer(forKey: Keys.fanSafetyTemperature) : 95
panelTab = defaults.string(forKey: Keys.panelTab).flatMap(PanelTab.init(rawValue:)) ?? .overview
appearance = defaults.string(forKey: Keys.appearance).flatMap(AppearanceMode.init(rawValue:)) ?? .system
cleanPrefersTrash = defaults.bool(forKey: Keys.cleanPrefersTrash)
menuBarLayout = defaults.string(forKey: Keys.menuBarLayout).flatMap(MenuBarLayout.init(rawValue:)) ?? .separate
hiddenPopoverSections = defaults.stringArray(forKey: Keys.hiddenPopoverSections)
.map { Set($0.compactMap(PopoverSection.init(rawValue:))) } ?? PopoverSection.hiddenByDefault
probeEnabled = defaults.object(forKey: Keys.probeEnabled) as? Bool ?? true
probeInBackground = defaults.object(forKey: Keys.probeInBackground) as? Bool ?? true
probeSeconds = Self.probeOptions.contains(defaults.integer(forKey: Keys.probeSeconds))
? defaults.integer(forKey: Keys.probeSeconds) : 2
probeTarget = defaults.string(forKey: Keys.probeTarget).flatMap(ProbeTarget.init(rawValue:)) ?? .cloudflare
geoSource = defaults.string(forKey: Keys.geoSource).flatMap(GeoSource.init(rawValue:)) ?? .cleanIP
publicIPLookup = defaults.object(forKey: Keys.publicIPLookup) as? Bool ?? true
geoIncludeCity = defaults.bool(forKey: Keys.geoIncludeCity)
geoAutoUpdate = defaults.object(forKey: Keys.geoAutoUpdate) as? Bool ?? true
autoCheckUpdates = defaults.object(forKey: Keys.autoCheckUpdates) as? Bool ?? true
historyEnabled = defaults.object(forKey: Keys.historyEnabled) as? Bool ?? true
language = defaults.string(forKey: Keys.language).flatMap(AppLanguage.init(rawValue:)) ?? .system
let storedHotKeys = defaults.data(forKey: Keys.hotKeys).flatMap { try? JSONDecoder().decode([String: HotKey].self, from: $0) } ?? [:]
hotKeys = Dictionary(uniqueKeysWithValues: storedHotKeys.compactMap { key, value in
HotKeyAction(rawValue: key).map { ($0, value) }
})
enabledAlerts = Set(defaults.stringArray(forKey: Keys.enabledAlerts)?.compactMap(AlertKind.init(rawValue:)) ?? [])
alertCPUTemperature = Self.alertTemperatureOptions.contains(defaults.integer(forKey: Keys.alertCPUTemperature))
? defaults.integer(forKey: Keys.alertCPUTemperature) : 95
alertCPULoad = Self.alertLoadOptions.contains(defaults.integer(forKey: Keys.alertCPULoad))
? defaults.integer(forKey: Keys.alertCPULoad) : 80
cpuChartSeconds = Self.cpuChartOptions.contains(defaults.integer(forKey: Keys.cpuChartSeconds))
? defaults.integer(forKey: Keys.cpuChartSeconds) : 60
}
/// 按固定顺序返回已启用的菜单栏项目
var orderedMenuBarItems: [MenuBarItem] {
MenuBarItem.allCases.filter(menuBarItems.contains)
}
func isEnabled(_ item: MenuBarItem) -> Bool { menuBarItems.contains(item) }
func style(for item: MenuBarItem) -> MenuBarStyle {
styleOverrides[item] ?? menuBarStyle
}
/// nil 表示跟随整体风格
func setStyleOverride(_ style: MenuBarStyle?, for item: MenuBarItem) {
styleOverrides[item] = style
}
func isVisible(_ section: PopoverSection) -> Bool { !hiddenPopoverSections.contains(section) }
func setVisible(_ section: PopoverSection, _ visible: Bool) {
if visible { hiddenPopoverSections.remove(section) } else { hiddenPopoverSections.insert(section) }
}
func setEnabled(_ item: MenuBarItem, _ enabled: Bool) {
if enabled { menuBarItems.insert(item) } else { menuBarItems.remove(item) }
}
private enum Keys {
static let menuBarItems = "menuBarItems"
static let menuBarStyle = "menuBarStyle"
static let networkStyle = "networkStyle"
static let styleOverrides = "styleOverrides"
static let refreshSeconds = "refreshSeconds"
static let colorizeHighLoad = "colorizeHighLoad"
static let useFahrenheit = "useFahrenheit"
static let lidModeBatteryFloor = "lidModeBatteryFloor"
static let fanSafetyTemperature = "fanSafetyTemperature"
static let panelTab = "panelTab"
static let appearance = "appearance"
static let cleanPrefersTrash = "cleanPrefersTrash"
static let menuBarLayout = "menuBarLayout"
static let hiddenPopoverSections = "hiddenPopoverSections"
static let probeEnabled = "probeEnabled"
static let probeSeconds = "probeSeconds"
static let probeInBackground = "probeInBackground"
static let probeTarget = "probeTarget"
static let geoSource = "geoSource"
static let publicIPLookup = "publicIPLookup"
static let geoIncludeCity = "geoIncludeCity"
static let geoAutoUpdate = "geoAutoUpdate"
static let autoCheckUpdates = "autoCheckUpdates"
static let historyEnabled = "historyEnabled"
static let hotKeys = "hotKeys"
static let language = "language"
static let enabledAlerts = "enabledAlerts"
static let alertCPUTemperature = "alertCPUTemperature"
static let alertCPULoad = "alertCPULoad"
static let cpuChartSeconds = "cpuChartSeconds"
}
}