Akku-Popover sagt, was gerade geschieht
„Am Netz" beantwortet die Frage nicht, die man hat, wenn der Ladestand stehen bleibt: lädt es gerade nicht, weil das Limit erreicht ist, oder stimmt etwas nicht? Jetzt steht es dort — lädt, angehalten wegen Ladelimit mit der Prozentzahl, am Netz ohne zu laden, oder Akkubetrieb. Der Vergleich mit dem Limit hat zwei Punkte Toleranz: der Ladestand pendelt um die Grenze, und auf das Prozent genau zu vergleichen hieße, den Grund die halbe Zeit zu verschweigen. Weit unter dem Limit wird dagegen kein Limit-Halt behauptet — dann steht das Laden aus einem anderen Grund, und einen falschen zu nennen wäre schlimmer als keinen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Was mit dem Akku gerade geschieht — in Worten statt in Symbolen.
|
||||||
|
///
|
||||||
|
/// „Am Netz" allein beantwortet die Frage nicht, die man hat, wenn der
|
||||||
|
/// Ladestand stehen bleibt: lädt es gerade nicht, weil das Limit erreicht ist,
|
||||||
|
/// oder stimmt etwas nicht?
|
||||||
|
public enum ChargeActivity: Equatable, Sendable {
|
||||||
|
case charging
|
||||||
|
/// Angehalten, weil das eingestellte Ladelimit erreicht ist.
|
||||||
|
case pausedAtLimit(Int)
|
||||||
|
/// Am Netz, lädt aber nicht — Grund unbekannt.
|
||||||
|
case pluggedNotCharging
|
||||||
|
case onBattery
|
||||||
|
|
||||||
|
/// Wie weit unter dem Limit noch als „am Limit" gilt.
|
||||||
|
///
|
||||||
|
/// Der Ladestand pendelt um die Grenze. Auf das Prozent genau zu
|
||||||
|
/// vergleichen hieße, den Grund die halbe Zeit zu verschweigen.
|
||||||
|
public static let limitTolerance = 2
|
||||||
|
|
||||||
|
public static func resolve(isCharging: Bool, isPluggedIn: Bool,
|
||||||
|
charge: Double, limit: Int?) -> ChargeActivity {
|
||||||
|
if isCharging { return .charging }
|
||||||
|
guard isPluggedIn else { return .onBattery }
|
||||||
|
|
||||||
|
let percent = Int((min(max(charge, 0), 1) * 100).rounded())
|
||||||
|
// Ein Limit von hundert ist keines.
|
||||||
|
if let limit, limit < 100, percent >= limit - limitTolerance {
|
||||||
|
return .pausedAtLimit(limit)
|
||||||
|
}
|
||||||
|
// Steht das Laden aus einem anderen Grund — etwa weil macOS den Akku
|
||||||
|
// schont —, ist das der ehrliche Befund. Einen falschen Grund zu
|
||||||
|
// nennen wäre schlimmer als keinen.
|
||||||
|
return .pluggedNotCharging
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -266,6 +266,10 @@ private struct BatteryDetail: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Was gerade geschieht, in Worten. „Am Netz" allein beantwortet die
|
||||||
|
// Frage nicht, die man hat, wenn der Ladestand stehen bleibt.
|
||||||
|
activityLine(battery)
|
||||||
|
|
||||||
// Das Ladelimit steht hier und nicht in den Einstellungen: es ist
|
// Das Ladelimit steht hier und nicht in den Einstellungen: es ist
|
||||||
// eine Akkusache, und man greift danach, während man auf den
|
// eine Akkusache, und man greift danach, während man auf den
|
||||||
// Ladestand schaut.
|
// Ladestand schaut.
|
||||||
@@ -316,6 +320,47 @@ private struct BatteryDetail: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func activityLine(_ battery: BatteryReading) -> some View {
|
||||||
|
let activity = ChargeActivity.resolve(isCharging: battery.isCharging,
|
||||||
|
isPluggedIn: battery.isPluggedIn,
|
||||||
|
charge: battery.charge,
|
||||||
|
limit: control?.chargeLimit)
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: symbol(for: activity))
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(tint(for: activity))
|
||||||
|
switch activity {
|
||||||
|
case .charging:
|
||||||
|
Text("popover.battery.charging", bundle: .module)
|
||||||
|
case .pausedAtLimit(let limit):
|
||||||
|
Text("popover.battery.pausedAtLimit \(limit)", bundle: .module)
|
||||||
|
case .pluggedNotCharging:
|
||||||
|
Text("popover.battery.notCharging", bundle: .module)
|
||||||
|
case .onBattery:
|
||||||
|
Text("popover.battery.onBattery", bundle: .module)
|
||||||
|
}
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
.font(.callout)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func symbol(for activity: ChargeActivity) -> String {
|
||||||
|
switch activity {
|
||||||
|
case .charging: "bolt.fill"
|
||||||
|
case .pausedAtLimit: "pause.fill"
|
||||||
|
case .pluggedNotCharging: "powerplug.fill"
|
||||||
|
case .onBattery: "battery.50percent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func tint(for activity: ChargeActivity) -> Color {
|
||||||
|
// Farbe nur, wo sie etwas meldet: Laden ist ein Zustand, den man
|
||||||
|
// sucht. Alles andere bleibt zurückhaltend.
|
||||||
|
activity == .charging ? .green : .secondary
|
||||||
|
}
|
||||||
|
|
||||||
private func duration(_ seconds: TimeInterval) -> String {
|
private func duration(_ seconds: TimeInterval) -> String {
|
||||||
let total = Int(seconds / 60)
|
let total = Int(seconds / 60)
|
||||||
return total >= 60 ? "\(total / 60) h \(total % 60) min" : "\(total) min"
|
return total >= 60 ? "\(total / 60) h \(total % 60) min" : "\(total) min"
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import Testing
|
||||||
|
@testable import MetricsProvider
|
||||||
|
|
||||||
|
// „Am Netz" allein beantwortet die Frage nicht, die man hat, wenn der
|
||||||
|
// Ladestand stehen bleibt: lädt es gerade nicht, weil das Limit erreicht ist,
|
||||||
|
// oder stimmt etwas nicht?
|
||||||
|
|
||||||
|
@Suite("Ladezustand in Worten")
|
||||||
|
struct ChargeActivityTests {
|
||||||
|
|
||||||
|
@Test("Lädt schlägt alles andere")
|
||||||
|
func chargingWins() {
|
||||||
|
#expect(ChargeActivity.resolve(isCharging: true, isPluggedIn: true,
|
||||||
|
charge: 0.5, limit: 85) == .charging)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Ohne Netzteil ist es Akkubetrieb")
|
||||||
|
func unpluggedIsOnBattery() {
|
||||||
|
#expect(ChargeActivity.resolve(isCharging: false, isPluggedIn: false,
|
||||||
|
charge: 0.5, limit: 85) == .onBattery)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Am Limit steht der Grund dabei")
|
||||||
|
func atLimitIsExplained() {
|
||||||
|
#expect(ChargeActivity.resolve(isCharging: false, isPluggedIn: true,
|
||||||
|
charge: 0.85, limit: 85) == .pausedAtLimit(85))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Knapp unter dem Limit zählt auch")
|
||||||
|
func justBelowTheLimitCounts() {
|
||||||
|
// Der Ladestand pendelt um die Grenze; auf das Prozent genau zu
|
||||||
|
// vergleichen hieße, den Grund die halbe Zeit zu verschweigen.
|
||||||
|
#expect(ChargeActivity.resolve(isCharging: false, isPluggedIn: true,
|
||||||
|
charge: 0.84, limit: 85) == .pausedAtLimit(85))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Weit unter dem Limit ist es kein Limit-Halt")
|
||||||
|
func wellBelowIsNotTheLimit() {
|
||||||
|
// Dann steht das Laden aus einem anderen Grund — etwa weil macOS den
|
||||||
|
// Akku schont. Einen falschen Grund zu nennen wäre schlimmer als
|
||||||
|
// keinen.
|
||||||
|
#expect(ChargeActivity.resolve(isCharging: false, isPluggedIn: true,
|
||||||
|
charge: 0.40, limit: 85) == .pluggedNotCharging)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Ohne bekanntes Limit bleibt es beim schlichten Befund")
|
||||||
|
func withoutLimitStaysPlain() {
|
||||||
|
#expect(ChargeActivity.resolve(isCharging: false, isPluggedIn: true,
|
||||||
|
charge: 0.99, limit: nil) == .pluggedNotCharging)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Ein Limit von hundert ist kein Limit")
|
||||||
|
func hundredIsNoLimit() {
|
||||||
|
#expect(ChargeActivity.resolve(isCharging: false, isPluggedIn: true,
|
||||||
|
charge: 1.0, limit: 100) == .pluggedNotCharging)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user