Files
XStats/Packages/XStatsKit/Sources/XStatsUI/Panel/ThermalPage.swift
T

238 lines
9.1 KiB
Swift

import Localization
import Metrics
import SMC
import SwiftUI
struct ThermalPage: View {
@Environment(AppModel.self) private var model
var body: some View {
PageScroll {
HStack(alignment: .top, spacing: DS.Space.s3) {
TemperatureCard().frame(maxWidth: .infinity)
if model.store.supportsFans { FanCard().frame(maxWidth: .infinity) }
}
.fixedSize(horizontal: false, vertical: true)
PowerCard()
if model.store.supportsFans { FanSafetySettings() }
}
}
}
private struct TemperatureCard: View {
@Environment(AppModel.self) private var model
var body: some View {
let summaries = model.store.sensors?.temperatures ?? []
let sensorCount = summaries.reduce(0) { $0 + $1.sensorCount }
Card {
CardHeader(icon: "thermometer.medium", title: tr("温度"),
detail: summaries.isEmpty ? tr("读取中") : tr("\(sensorCount) 个传感器"))
if summaries.isEmpty {
PlaceholderLine()
} else {
ForEach(summaries) { summary in
TemperatureRow(summary: summary, fahrenheit: model.settings.useFahrenheit)
}
}
}
}
}
private struct TemperatureRow: View {
let summary: TemperatureSummary
let fahrenheit: Bool
var body: some View {
let tone = Tone.forTemperature(summary.maximum)
VStack(alignment: .leading, spacing: DS.Space.s1) {
HStack(spacing: DS.Space.s3) {
Text(summary.group.title)
.dsFont(.sm, weight: .medium)
.foregroundStyle(DS.Palette.textPrimary)
.frame(width: DS.Size.labelColumn, alignment: .leading)
ProgressTrack(fraction: summary.maximum / DS.Thermal.scaleMax, color: tone.color)
Text(Format.temperature(summary.maximum, fahrenheit: fahrenheit))
.dsFont(.sm, weight: .semibold)
.monospacedDigit()
.foregroundStyle(DS.Palette.textPrimary)
.frame(width: DS.Size.valueColumn, alignment: .trailing)
}
if summary.sensorCount > 1 {
Text(tr("平均 \(Format.temperature(summary.average, fahrenheit: fahrenheit)) · 最高 \(Format.temperature(summary.maximum, fahrenheit: fahrenheit))"))
.dsFont(.xs)
.foregroundStyle(DS.Palette.textTertiary)
.padding(.leading, DS.Size.labelColumn + DS.Space.s3)
}
}
}
}
private struct PowerCard: View {
@Environment(AppModel.self) private var model
var body: some View {
let power = model.store.power
Card {
CardHeader(icon: "bolt", title: tr("功耗"), detail: power?.system.map { tr("整机 \(Format.watts($0))") } ?? tr("读取中"))
PowerRows(power: power, history: model.store.powerHistory.elements)
}
}
}
private struct FanCard: View {
@Environment(AppModel.self) private var model
var body: some View {
let fans = model.store.sensors?.fans ?? []
let controller = model.fans
Card {
CardHeader(icon: "fan", title: tr("风扇")) {
if !fans.isEmpty {
StatusBadge(text: fans.contains(where: \.isManual)
? (controller.mode == .automatic ? tr("其他程序控制") : tr("XStats 控制"))
: tr("系统自动"),
tone: fans.contains(where: \.isManual) ? .primary : .neutral)
}
}
if fans.isEmpty {
Text(model.store.sensors == nil ? tr("正在读取风扇…") : tr("此设备没有可调节的风扇"))
.dsFont(.sm)
.foregroundStyle(DS.Palette.textSecondary)
} else {
ForEach(fans) { fan in
FanRow(fan: fan, name: name(for: fan, count: fans.count))
}
HairlineDivider()
Text(tr("调速模式"))
.dsFont(.sm, weight: .semibold)
.foregroundStyle(DS.Palette.textPrimary)
SegmentedControl(selection: modeBinding, options: FanController.Mode.allCases.map { ($0, $0.title) })
.disabled(!model.helper.isReady || controller.isApplying)
if controller.mode == .custom {
CustomLevelControl(fans: fans)
}
if model.helper.needsAttention {
HelperRequiredBanner(text: tr("调节风扇需要安装辅助工具,仅需管理员授权一次。"))
}
if let notice = controller.notice {
InfoBanner(icon: controller.noticeIsError ? "exclamationmark.triangle.fill" : "checkmark.circle.fill",
text: notice,
tone: controller.noticeIsError ? .error : .success)
}
Text(verbatim: tr("自定义模式下 CPU 达到 \(model.settings.fanSafetyTemperature)°C 会自动交还系统控制;退出应用时风扇恢复自动。"))
.dsFont(.xs)
.foregroundStyle(DS.Palette.textTertiary)
.fixedSize(horizontal: false, vertical: true)
}
}
}
private var modeBinding: Binding<FanController.Mode> {
Binding(get: { model.fans.mode },
set: { mode in Task { await model.fans.select(mode) } })
}
private func name(for fan: FanState, count: Int) -> String {
guard count == 2 else { return tr("风扇 \(fan.id + 1)") }
return fan.id == 0 ? tr("左侧风扇") : tr("右侧风扇")
}
}
private struct FanRow: View {
let fan: FanState
let name: String
var body: some View {
VStack(alignment: .leading, spacing: DS.Space.s1) {
HStack {
Text(name).dsFont(.sm, weight: .medium).foregroundStyle(DS.Palette.textPrimary)
Spacer()
Text(fan.isStarting ? tr("启动中…") : Format.rpm(fan.current))
.dsFont(.sm, weight: .semibold)
.monospacedDigit()
.foregroundStyle(DS.Palette.textPrimary)
}
ProgressTrack(fraction: fan.fraction, color: DS.Palette.secondary)
Text(verbatim: tr("最低 \(Int(fan.minimum)) · 最高 \(Int(fan.maximum)) · 目标 \(Int(fan.target)) RPM"))
.dsFont(.xs)
.monospacedDigit()
.foregroundStyle(DS.Palette.textTertiary)
}
}
}
private struct CustomLevelControl: View {
@Environment(AppModel.self) private var model
let fans: [FanState]
var body: some View {
@Bindable var controller = model.fans
let reference = fans.first
let rpm = reference.map { model.fans.targetRPM(for: $0) } ?? 0
VStack(alignment: .leading, spacing: DS.Space.s2) {
HStack {
Text(tr("目标转速")).dsFont(.xs).foregroundStyle(DS.Palette.textSecondary)
Spacer()
Text(tr("\(Format.percent(controller.customLevel)) · 约 \(Format.rpm(rpm))"))
.dsFont(.xs, weight: .semibold)
.monospacedDigit()
.foregroundStyle(DS.Palette.textPrimary)
}
DSSlider(value: $controller.customLevel) {
Task { await model.fans.commitCustomLevel() }
}
HStack {
Text(tr("安静")).dsFont(.xs).foregroundStyle(DS.Palette.textTertiary)
Spacer()
Text(tr("最强")).dsFont(.xs).foregroundStyle(DS.Palette.textTertiary)
}
}
}
}
struct HelperRequiredBanner: View {
@Environment(AppModel.self) private var model
let text: String
var body: some View {
InfoBanner(icon: "lock.shield", text: bannerText, tone: .warning) {
switch model.helper.status {
case .notInstalled:
Button(tr("安装")) { model.helper.install() }
.buttonStyle(DSButtonStyle(kind: .primary))
.disabled(model.helper.isWorking)
case .requiresApproval:
Button(tr("去批准")) { model.helper.openLoginItemsSettings() }
.buttonStyle(DSButtonStyle(kind: .primary))
case .enabled where model.helper.isOutdated:
Button(tr("重新安装")) { Task { await model.helper.reinstall() } }
.buttonStyle(DSButtonStyle(kind: .primary))
.disabled(model.helper.isWorking)
case .enabled, .unavailable:
EmptyView()
}
}
}
private var bannerText: String {
switch model.helper.status {
case .requiresApproval: tr("请在“系统设置 › 通用 › 登录项”中允许 XStats 的后台项目。")
case .unavailable(let reason): reason
case .enabled where model.helper.isOutdated: HelperClient.outdatedMessage
default: text
}
}
}