Files
OpenStats/Packages/OpenStatsKit/Sources/OpenStatsUI/State/BluetoothController.swift
T
gentpan 51b954983b 菜单栏新增“电池”项目:电量、24 小时曲线、功耗、健康度与蓝牙设备电量
- 电池按九种风格画在菜单栏,充电中带闪电,电量低于 20% 按“高负载时着色”变红;点击弹出电池详情,主窗口同名页面复用同一视图,历史页多了电池电量曲线
- 历史库加 battery 列(旧库首次打开时 ALTER TABLE 迁移),HistoryRecorder 单独查最近 24 小时电量供弹窗使用
- BluetoothController 按需轮询蓝牙设备电量:弹窗 / 页面开着每分钟,菜单栏需要时每 5 分钟;本机信息页的蓝牙卡片改用它
- 没有电池的 Mac 上电池项目改为显示电量最低的蓝牙设备;「设置 · 菜单栏」新增“蓝牙设备电量低时提示”开关(同步文档一并带上)
- README、架构说明、更新日志与译文同步
2026-09-15 03:58:30 +05:00

86 lines
2.7 KiB
Swift

import Foundation
import Metrics
import Observation
/// 已连接蓝牙设备的电量。读取要起 system_profiler、一次一两秒,所以只在有人看或菜单栏需要时轮询
@MainActor
@Observable
public final class BluetoothController {
public enum Demand: Equatable, Sendable {
/// 不轮询
case off
/// 菜单栏需要(电量低提示、没有电池的 Mac 显示设备电量):每 5 分钟
case background
/// 电池弹窗 / 页面、本机信息页打开着:每分钟
case foreground
var interval: Duration? {
switch self {
case .off: nil
case .background: .seconds(300)
case .foreground: .seconds(60)
}
}
}
/// nil 表示还没读过
public private(set) var devices: [BluetoothDevice]?
public private(set) var updatedAt: Date?
public private(set) var isReading = false
@ObservationIgnored private var demand: Demand = .off
@ObservationIgnored private var task: Task<Void, Never>?
/// 电量最低的一块电池(耳机的左右耳、充电盒分别算)
public var lowest: (device: BluetoothDevice, label: String, percent: Int)? {
var best: (device: BluetoothDevice, label: String, percent: Int)?
for device in devices ?? [] {
for battery in device.batteries {
if let current = best, battery.percent >= current.percent { continue }
best = (device, battery.label, battery.percent)
}
}
return best
}
func setDemand(_ demand: Demand) {
guard demand != self.demand else { return }
self.demand = demand
task?.cancel()
task = nil
guard let interval = demand.interval else { return }
task = Task { [weak self] in
while !Task.isCancelled {
await self?.read()
try? await Task.sleep(for: interval)
}
}
}
/// 弹窗里的刷新按钮
func refresh() {
Task { await read() }
}
private func read() async {
guard !isReading else { return }
isReading = true
devices = await Task.detached(priority: .utility) { BluetoothBatteryReader.read() }.value
updatedAt = Date()
isReading = false
}
}
extension BluetoothDevice.Kind {
/// 列表与菜单栏里代表设备类型的图标
var symbol: String {
switch self {
case .keyboard: "keyboard"
case .mouse: "computermouse"
case .trackpad: "rectangle.and.hand.point.up.left"
case .headphones: "headphones"
case .phone: "iphone"
case .other: "dot.radiowaves.left.and.right"
}
}
}