Files
OpenStats/Packages/OpenStatsKit/Sources/OpenStatsUI/State/SettingsDocument.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

133 lines
6.7 KiB
Swift

import Foundation
import Localization
/// 同步到云端的设置文档:只含偏好,不含本机状态(当前页签、辅助工具安装状态、历史库、归属地库)。
/// 枚举以原始值保存,字段全部可选:旧版本读到新字段会忽略,新版本读到旧文档不会解码失败;
/// 应用时逐项校验,不认识的值跳过
public struct SettingsDocument: Codable, Equatable, Sendable {
public var schema: Int? = 1
public var menuBarItems: [String]?
public var menuBarStyle: String?
public var networkStyle: String?
public var styleOverrides: [String: String]?
public var refreshSeconds: Int?
public var colorizeHighLoad: Bool?
public var useFahrenheit: Bool?
public var lidModeBatteryFloor: Int?
public var fanSafetyTemperature: Int?
public var appearance: String?
public var menuBarLayout: String?
public var hiddenPopoverSections: [String]?
public var probeEnabled: Bool?
public var probeInBackground: Bool?
public var probeSeconds: Int?
public var probeTarget: String?
public var geoSource: String?
public var publicIPLookup: Bool?
public var geoIncludeCity: Bool?
public var geoAutoUpdate: Bool?
public var enabledAlerts: [String]?
public var alertCPUTemperature: Int?
public var alertCPULoad: Int?
public var language: String?
public var hotKeys: [String: HotKey]?
public var historyEnabled: Bool?
public var autoCheckUpdates: Bool?
public var cleanPrefersTrash: Bool?
public init() {}
}
extension AppSettings {
/// 当前设置导出成文档。读取这里列出的每一项也就登记了观察,SyncController 借此得知设置变化
public func exportDocument() -> SettingsDocument {
var doc = SettingsDocument()
doc.menuBarItems = menuBarItems.map(\.rawValue).sorted()
doc.menuBarStyle = menuBarStyle.rawValue
doc.networkStyle = networkStyle.rawValue
doc.styleOverrides = Dictionary(uniqueKeysWithValues: styleOverrides.map { ($0.key.rawValue, $0.value.rawValue) })
doc.refreshSeconds = refreshSeconds
doc.colorizeHighLoad = colorizeHighLoad
doc.useFahrenheit = useFahrenheit
doc.lidModeBatteryFloor = lidModeBatteryFloor
doc.fanSafetyTemperature = fanSafetyTemperature
doc.appearance = appearance.rawValue
doc.menuBarLayout = menuBarLayout.rawValue
doc.hiddenPopoverSections = hiddenPopoverSections.map(\.rawValue).sorted()
doc.probeEnabled = probeEnabled
doc.probeInBackground = probeInBackground
doc.probeSeconds = probeSeconds
doc.probeTarget = probeTarget.rawValue
doc.geoSource = geoSource.rawValue
doc.publicIPLookup = publicIPLookup
doc.geoIncludeCity = geoIncludeCity
doc.geoAutoUpdate = geoAutoUpdate
doc.enabledAlerts = enabledAlerts.map(\.rawValue).sorted()
doc.alertCPUTemperature = alertCPUTemperature
doc.alertCPULoad = alertCPULoad
doc.language = language.rawValue
doc.hotKeys = Dictionary(uniqueKeysWithValues: hotKeys.map { ($0.key.rawValue, $0.value) })
doc.historyEnabled = historyEnabled
doc.autoCheckUpdates = autoCheckUpdates
doc.cleanPrefersTrash = cleanPrefersTrash
return doc
}
/// 把文档套用到当前设置。只改有变化的项,避免无谓地触发持久化与界面重建
public func apply(_ doc: SettingsDocument) {
func assign<T: Equatable>(_ keyPath: ReferenceWritableKeyPath<AppSettings, T>, _ value: T?) {
guard let value, self[keyPath: keyPath] != value else { return }
self[keyPath: keyPath] = value
}
func option(_ value: Int?, in options: [Int]) -> Int? {
value.flatMap { options.contains($0) ? $0 : nil }
}
assign(\.menuBarItems, doc.menuBarItems.map { Set($0.compactMap(MenuBarItem.init(rawValue:))) })
assign(\.menuBarStyle, doc.menuBarStyle.flatMap(MenuBarStyle.init(rawValue:)))
assign(\.networkStyle, doc.networkStyle.flatMap(NetworkMenuStyle.init(rawValue:)))
assign(\.styleOverrides, doc.styleOverrides.map { overrides in
Dictionary(uniqueKeysWithValues: overrides.compactMap { key, value in
guard let item = MenuBarItem(rawValue: key), let style = MenuBarStyle(rawValue: value) else { return nil }
return (item, style)
})
})
assign(\.refreshSeconds, option(doc.refreshSeconds, in: Self.refreshOptions))
assign(\.colorizeHighLoad, doc.colorizeHighLoad)
assign(\.useFahrenheit, doc.useFahrenheit)
assign(\.lidModeBatteryFloor, option(doc.lidModeBatteryFloor, in: Self.batteryFloorOptions))
assign(\.fanSafetyTemperature, option(doc.fanSafetyTemperature, in: Self.fanSafetyOptions))
assign(\.appearance, doc.appearance.flatMap(AppearanceMode.init(rawValue:)))
assign(\.menuBarLayout, doc.menuBarLayout.flatMap(MenuBarLayout.init(rawValue:)))
assign(\.hiddenPopoverSections, doc.hiddenPopoverSections.map { Set($0.compactMap(PopoverSection.init(rawValue:))) })
assign(\.probeEnabled, doc.probeEnabled)
assign(\.probeInBackground, doc.probeInBackground)
assign(\.probeSeconds, option(doc.probeSeconds, in: Self.probeOptions))
assign(\.probeTarget, doc.probeTarget.flatMap(ProbeTarget.init(rawValue:)))
assign(\.geoSource, doc.geoSource.flatMap(GeoSource.init(rawValue:)))
assign(\.publicIPLookup, doc.publicIPLookup)
assign(\.geoIncludeCity, doc.geoIncludeCity)
assign(\.geoAutoUpdate, doc.geoAutoUpdate)
assign(\.enabledAlerts, doc.enabledAlerts.map { Set($0.compactMap(AlertKind.init(rawValue:))) })
assign(\.alertCPUTemperature, option(doc.alertCPUTemperature, in: Self.alertTemperatureOptions))
assign(\.alertCPULoad, option(doc.alertCPULoad, in: Self.alertLoadOptions))
assign(\.language, doc.language.flatMap(AppLanguage.init(rawValue:)))
assign(\.hotKeys, doc.hotKeys.map { stored in
Dictionary(uniqueKeysWithValues: stored.compactMap { key, value in
HotKeyAction(rawValue: key).map { ($0, value) }
})
})
assign(\.historyEnabled, doc.historyEnabled)
assign(\.autoCheckUpdates, doc.autoCheckUpdates)
assign(\.cleanPrefersTrash, doc.cleanPrefersTrash)
}
/// 出厂设置对应的文档:用一个空的 defaults 域构造一次即可,不会写盘
public static func defaultDocument() -> SettingsDocument {
let name = "com.openstats.defaults-probe"
let defaults = UserDefaults(suiteName: name) ?? .standard
defaults.removePersistentDomain(forName: name)
return AppSettings(defaults: defaults).exportDocument()
}
}