diff --git a/Packages/XStatsKit/Sources/SMC/FanControl.swift b/Packages/XStatsKit/Sources/SMC/FanControl.swift index 00821b8..2d91651 100644 --- a/Packages/XStatsKit/Sources/SMC/FanControl.swift +++ b/Packages/XStatsKit/Sources/SMC/FanControl.swift @@ -36,6 +36,13 @@ public struct FanState: Sendable, Equatable, Identifiable { } } +extension Array where Element == FanState { + /// 汇总值是否显示“启动中”:仅当没有任何风扇在转、且至少一个处于起步窗口 + public var isStartingUp: Bool { + !contains { $0.current > 0 } && contains(where: \.isStarting) + } +} + /// 部分机型没有起步状态键,只能以手动目标转速推断。两种来源均最多提示 10 秒, /// 超时后保留真实的 0 RPM,直到风扇转动或起步条件消失才允许开始新的窗口。 struct FanStartupTracker { diff --git a/Packages/XStatsKit/Sources/XStatsUI/Panel/OverviewPage.swift b/Packages/XStatsKit/Sources/XStatsUI/Panel/OverviewPage.swift index f633043..67efd10 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Panel/OverviewPage.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Panel/OverviewPage.swift @@ -297,11 +297,11 @@ private struct FanTile: View { .foregroundStyle(DS.Palette.textSecondary) HStack(alignment: .firstTextBaseline, spacing: DS.Space.s1 / 2) { - Text(verbatim: fans.contains(where: \.isStarting) ? tr("启动中…") : fastest.map { Int($0.current).formatted() } ?? "—") + Text(verbatim: fans.isStartingUp ? tr("启动中…") : fastest.map { Int($0.current).formatted() } ?? "—") .dsFont(.xl, weight: .semibold) .monospacedDigit() .foregroundStyle(DS.Palette.textPrimary) - if !fans.contains(where: \.isStarting) { + if !fans.isStartingUp { Text(verbatim: "RPM").dsFont(.xs, weight: .medium).foregroundStyle(DS.Palette.textSecondary) } } diff --git a/Packages/XStatsKit/Sources/XStatsUI/Popover/CombinedPopover.swift b/Packages/XStatsKit/Sources/XStatsUI/Popover/CombinedPopover.swift index 0f629de..d25f279 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Popover/CombinedPopover.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Popover/CombinedPopover.swift @@ -295,8 +295,8 @@ struct OverviewReading { case .fan: let fans = store.sensors?.fans ?? [] if let fastest = store.fastestFan { - value = fans.contains(where: \.isStarting) ? tr("启动中…") : Int(fastest.current).formatted() - unit = fans.contains(where: \.isStarting) ? nil : "RPM" + value = fans.isStartingUp ? tr("启动中…") : Int(fastest.current).formatted() + unit = fans.isStartingUp ? nil : "RPM" let average = fans.map { $0.maximum > 0 ? $0.current / $0.maximum : 0 }.reduce(0, +) / Double(max(1, fans.count)) chart = .level(average, DS.Palette.primary) } diff --git a/Packages/XStatsKit/Sources/XStatsUI/Popover/MetricPopovers.swift b/Packages/XStatsKit/Sources/XStatsUI/Popover/MetricPopovers.swift index 9a49810..5bd38a3 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Popover/MetricPopovers.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Popover/MetricPopovers.swift @@ -720,8 +720,8 @@ struct ThermalPopover: View { if model.store.supportsFans { VStack(alignment: .leading, spacing: DS.Space.s1) { Text(tr("风扇")).dsFont(.xs).foregroundStyle(DS.Palette.textSecondary) - HeroValue(value: fans.contains(where: \.isStarting) ? tr("启动中…") : store.fastestFan.map { Int($0.current).formatted() } ?? "—", - unit: fans.isEmpty || fans.contains(where: \.isStarting) ? nil : "RPM") + HeroValue(value: fans.isStartingUp ? tr("启动中…") : store.fastestFan.map { Int($0.current).formatted() } ?? "—", + unit: fans.isEmpty || fans.isStartingUp ? nil : "RPM") } .frame(maxWidth: .infinity, alignment: .leading) } @@ -782,7 +782,7 @@ struct ThermalPopover: View { @MainActor func fanStatusText(fans: [FanState], mode: FanController.Mode) -> String { if fans.isEmpty { return tr("未检测到风扇") } - if fans.contains(where: \.isStarting) { return tr("正在启动风扇…") } + if fans.isStartingUp { return tr("正在启动风扇…") } guard fans.contains(where: \.isManual) else { return tr("由 macOS 调节") } return mode == .automatic ? tr("其他程序手动控制") : tr("XStats 控制中") } diff --git a/Packages/XStatsKit/Tests/MetricsTests/FanStartupTests.swift b/Packages/XStatsKit/Tests/MetricsTests/FanStartupTests.swift index 9beeb4a..915a5ce 100644 --- a/Packages/XStatsKit/Tests/MetricsTests/FanStartupTests.swift +++ b/Packages/XStatsKit/Tests/MetricsTests/FanStartupTests.swift @@ -71,3 +71,19 @@ struct FanStartupTests { #expect(!afterRepeatedFailure) } } + +struct FanStartupSummaryTests { + private func fan(_ id: Int, current: Double, starting: Bool = false) -> FanState { + FanState(id: id, current: current, minimum: 1000, maximum: 5000, target: 2000, isManual: true, isStarting: starting) + } + + @Test func spinningFanKeepsSummaryWhileAnotherStarts() { + #expect(![fan(0, current: 2400), fan(1, current: 0, starting: true)].isStartingUp) + } + + @Test func summaryStartsOnlyWhenNoFanSpins() { + #expect([fan(0, current: 0), fan(1, current: 0, starting: true)].isStartingUp) + #expect(![fan(0, current: 0), fan(1, current: 0)].isStartingUp) + #expect(![FanState]().isStartingUp) + } +}