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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
edb327cabe
commit
f289d02c37
@@ -0,0 +1,90 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user