Files
onyx/Packages/OnyxKit/Sources/MetricsProvider/BatteryHistoryView.swift
T
ScarriffleandClaude Opus 5 f289d02c37 Der Akku bekommt ein Gedächtnis
Verlauf über 24 Stunden, sieben Tage und dreißig Tage, aufgezeichnet alle fünf
Minuten. Als Säulen und nicht als Linie: eine Linie verbindet auch dort, wo
nichts gemessen wurde, und ein zugeklappter Rechner hatte keinen Ladestand —
eine Linie darüber behauptet einen.

Dazu Segelmodus und Hitzeschutz als reine Regeln im Helfer. Beide haben zwei
Schwellen, eine zum Anhalten und eine zum Weitermachen. Ohne diesen Abstand
hinge der Akku am Limit und würde bei jedem verlorenen Prozent nachgeladen —
hunderte winzige Ladevorgänge am Tag, und genau die altern ihn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 21:06:06 +02:00

91 lines
3.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
import OnyxDesign
/// Der Ladeverlauf als Säulen.
///
/// **Säulen und keine Linie.** Eine Linie verbindet auch dort, wo nichts
/// gemessen wurde — ein zugeklappter Rechner hat keinen Ladestand gehabt, und
/// eine Linie darüber behauptet einen. Säulen können einfach fehlen.
public struct BatteryHistoryView: View {
private let store: BatteryHistoryStore
@State private var window: HistoryWindow = .day
public init(store: BatteryHistoryStore) { self.store = store }
public var body: some View {
VStack(alignment: .leading, spacing: Onyx.Space.sm) {
header
chart
}
}
private var header: some View {
HStack(spacing: Onyx.Space.sm) {
if let summary = store.summary(window) {
Text(verbatim: "\(percent(summary.low)) – \(percent(summary.high)) %")
.font(Onyx.Font.metricSmall)
.foregroundStyle(Onyx.Color.textSecondary)
} else {
Text("battery.history.empty", bundle: .module)
.font(Onyx.Font.caption)
.foregroundStyle(Onyx.Color.textTertiary)
}
Spacer(minLength: Onyx.Space.xs)
// Drei kurze Wörter statt eines Menüs: die Auswahl hat drei
// Möglichkeiten, und ein Menü dafür verlangt zwei Klicks für das,
// was ein Klick kann.
ForEach(HistoryWindow.allCases, id: \.self) { candidate in
Button { window = candidate } label: {
Text(candidate.localizedName, bundle: .module)
.font(Onyx.Font.micro)
.foregroundStyle(candidate == window
? Onyx.Color.accent : Onyx.Color.textTertiary)
}
.buttonStyle(.plain)
}
}
}
private var chart: some View {
GeometryReader { geometry in
let buckets = store.buckets(window)
let gap: CGFloat = 1
let width = max((geometry.size.width - gap * CGFloat(buckets.count - 1))
/ CGFloat(buckets.count), 1)
HStack(alignment: .bottom, spacing: gap) {
ForEach(Array(buckets.enumerated()), id: \.offset) { _, value in
if let value {
RoundedRectangle(cornerRadius: Onyx.Radius.bar, style: .continuous)
.fill(Onyx.Color.accent)
.frame(width: width,
height: max(geometry.size.height * value, 1))
} else {
// Die Lücke bleibt eine Lücke.
Color.clear.frame(width: width, height: 1)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
}
.frame(height: 44)
}
private func percent(_ value: Double) -> Int {
Int((min(max(value, 0), 1) * 100).rounded())
}
}
extension HistoryWindow {
var localizedName: LocalizedStringKey {
switch self {
case .day: "battery.history.day"
case .week: "battery.history.week"
case .month: "battery.history.month"
}
}
}