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

161 lines
6.7 KiB
Swift

import Foundation
import Metrics
import Observation
import ServiceManagement
import SMC
import Updates
/// 应用状态总入口,注入到所有 SwiftUI 视图
@MainActor
@Observable
public final class AppModel {
public let settings: AppSettings
public let store: MetricsStore
public let helper: HelperClient
public let fans: FanController
public let keepAwake: KeepAwakeController
public let cleaner: CleanerController
public let maintenance: MaintenanceController
public let network: NetworkController
public let geo: GeoDatabaseController
public let explainer = ProcessExplainer()
public let updates: UpdateController
let diagnostics = DiagnosticsExporter()
public let alerts: AlertController
public let uninstaller = UninstallerController()
let startupItems = StartupItemsController()
public let history: HistoryRecorder
public let sync: SyncController
let diskTools: DiskToolsController
@ObservationIgnored public let hub = MetricsHub()
public var isMainWindowVisible = false
/// 被其他应用占用、没能注册的快捷键
public var hotKeyConflicts: Set<HotKeyAction> = []
/// 当前打开的菜单栏详情弹窗
public var openPopover: MenuBarItem?
public private(set) var launchAtLoginEnabled = SMAppService.mainApp.status == .enabled
public private(set) var launchAtLoginError: String?
@ObservationIgnored var openSettings: () -> Void = {}
@ObservationIgnored var openMainWindow: (PanelTab?) -> Void = { _ in }
@ObservationIgnored var quit: () -> Void = {}
public init(settings: AppSettings = AppSettings(), historyURL: URL? = HistoryDatabase.defaultURL) {
let store = MetricsStore()
let helper = HelperClient()
self.settings = settings
self.store = store
self.helper = helper
fans = FanController(helper: helper, store: store, settings: settings)
keepAwake = KeepAwakeController(helper: helper, settings: settings)
cleaner = CleanerController(settings: settings)
maintenance = MaintenanceController(helper: helper)
geo = GeoDatabaseController(settings: settings)
network = NetworkController(settings: settings, geo: geo)
updates = UpdateController(settings: settings)
alerts = AlertController(settings: settings)
history = HistoryRecorder(settings: settings, databaseURL: historyURL)
sync = SyncController(settings: settings)
diskTools = DiskToolsController(helper: helper)
}
/// 根据当前可见内容决定采集范围:主窗口看标签页,详情弹窗看是哪一项
var demand: MetricsDemand {
var demand = MetricsDemand()
let tab = settings.panelTab
let window = isMainWindowVisible
let popover = openPopover
let menu = settings.menuBarItems
let thermalPopover = popover == .temperature || popover == .fan
// 进程页要读全系统进程(启动 ps),每 2 秒刷新一次足够,也更省电
demand.interval = window && tab == .processes && popover == nil ? .seconds(2)
: window || popover != nil ? .seconds(1) : .seconds(settings.refreshSeconds)
demand.memory = true
demand.network = true
let showing = { (page: PanelTab, item: MenuBarItem) in (window && tab == page) || popover == item }
demand.gpu = (window && [.overview, .system].contains(tab)) || menu.contains(.gpu) || showing(.gpu, .gpu)
demand.disk = (window && [.overview, .system, .cleaner, .disk].contains(tab)) || menu.contains(.disk) || popover == .disk
demand.diskDetail = showing(.disk, .disk)
demand.battery = (window && [.overview, .system, .keepAwake].contains(tab)) || keepAwake.lidClosedActive
demand.processes = (window && [.processes, .overview, .disk].contains(tab)) || showing(.cpu, .cpu) || showing(.memory, .memory)
|| popover == .disk
demand.systemProcesses = window && tab == .processes
var groups = Set<TemperatureGroup>()
if window && tab == .overview { groups.formUnion([.cpu, .gpu]) }
if (window && tab == .thermal) || thermalPopover { groups.formUnion(TemperatureGroup.allCases) }
if menu.contains(.temperature) || fans.mode != .automatic || showing(.cpu, .cpu) { groups.insert(.cpu) }
if showing(.gpu, .gpu) { groups.insert(.gpu) }
if settings.enabledAlerts.contains(.cpuTemperature) { groups.insert(.cpu) }
demand.temperatures = groups
demand.power = (window && tab == .thermal) || thermalPopover
demand.cpuFrequency = showing(.cpu, .cpu)
demand.fans = (window && (tab == .thermal || tab == .overview)) || menu.contains(.fan)
|| fans.mode != .automatic || thermalPopover
return demand
}
/// 网络详情(接口、公网 IP、进程流量)正在显示
var isNetworkDetailVisible: Bool {
openPopover == .network || (isMainWindowVisible && settings.panelTab == .network)
}
func refreshLaunchAtLogin() {
launchAtLoginEnabled = SMAppService.mainApp.status == .enabled
}
func setLaunchAtLogin(_ enabled: Bool) {
launchAtLoginError = nil
do {
if enabled {
try SMAppService.mainApp.register()
} else {
try SMAppService.mainApp.unregister()
}
} catch {
launchAtLoginError = error.localizedDescription
}
refreshLaunchAtLogin()
}
/// 快捷切换风扇:未安装辅助工具时打开散热页引导安装
func requestFanMode(_ mode: FanController.Mode) {
guard helper.isReady else {
settings.panelTab = .thermal
if !isMainWindowVisible { openMainWindow(.thermal) }
return
}
Task { await fans.select(mode) }
}
/// 概览页快捷开关合盖运行:同时开启防休眠
func requestLidMode(_ enabled: Bool) {
guard helper.isReady else {
settings.panelTab = .keepAwake
if !isMainWindowVisible { openMainWindow(.keepAwake) }
return
}
Task {
await keepAwake.setLidClosed(enabled)
if enabled, !keepAwake.isActive { await keepAwake.start() }
}
}
/// 用 Apple 智能解释进程:结果显示在主窗口的进程页
func explainProcess(_ subject: ProcessExplainer.Subject) {
settings.panelTab = .processes
if !isMainWindowVisible { openMainWindow(.processes) }
explainer.explain(subject)
}
func handle(_ snapshot: MetricsSnapshot) {
store.apply(snapshot)
fans.evaluateSafety()
keepAwake.evaluateBattery(store.battery)
alerts.evaluate(store)
history.record(snapshot)
}
}