- 电池按九种风格画在菜单栏,充电中带闪电,电量低于 20% 按“高负载时着色”变红;点击弹出电池详情,主窗口同名页面复用同一视图,历史页多了电池电量曲线 - 历史库加 battery 列(旧库首次打开时 ALTER TABLE 迁移),HistoryRecorder 单独查最近 24 小时电量供弹窗使用 - BluetoothController 按需轮询蓝牙设备电量:弹窗 / 页面开着每分钟,菜单栏需要时每 5 分钟;本机信息页的蓝牙卡片改用它 - 没有电池的 Mac 上电池项目改为显示电量最低的蓝牙设备;「设置 · 菜单栏」新增“蓝牙设备电量低时提示”开关(同步文档一并带上) - README、架构说明、更新日志与译文同步
86 lines
2.7 KiB
Swift
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"
|
|
}
|
|
}
|
|
}
|