This commit is contained in:
BiasF
2026-08-30 10:36:52 +02:00
commit fdcc644828
30 changed files with 4152 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
import SwiftUI
/// Kachel auf dem Dashboard: Hauptwert gross, darunter die wichtigsten
/// Nebenwerte und der Verbindungszustand.
struct DeviceCard: View {
let device: ConfiguredDevice
let snapshot: DeviceSnapshot?
let linkState: DeviceLinkState
var body: some View {
VStack(alignment: .leading, spacing: 12) {
header
if let snapshot, let primary = snapshot.primaryMetric {
HStack(alignment: .firstTextBaseline, spacing: 4) {
Text(primary.formatted)
.font(.system(size: 44, weight: .semibold, design: .rounded))
.contentTransition(.numericText())
Text(primary.unit)
.font(.title3.weight(.medium))
.foregroundStyle(.secondary)
}
.foregroundStyle(snapshot.isStale ? .secondary : .primary)
secondaryValues(for: snapshot)
} else {
Text(placeholderText)
.font(.callout)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 18)
}
footer
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(.secondarySystemGroupedBackground), in: .rect(cornerRadius: 16))
}
private var header: some View {
HStack(spacing: 8) {
Image(systemName: device.role.symbol)
.foregroundStyle(.tint)
VStack(alignment: .leading, spacing: 1) {
Text(device.name)
.font(.headline)
Text(device.role.title)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
StatusDot(linkState: linkState, isStale: snapshot?.isStale ?? true)
}
}
private func secondaryValues(for snapshot: DeviceSnapshot) -> some View {
let others = snapshot.metrics
.filter { $0.id != snapshot.primaryMetric?.id && $0.value != nil }
.prefix(3)
return HStack(spacing: 16) {
ForEach(Array(others)) { metric in
VStack(alignment: .leading, spacing: 1) {
Text(metric.formattedWithUnit)
.font(.subheadline.weight(.medium))
.monospacedDigit()
Text(metric.label)
.font(.caption2)
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
Spacer(minLength: 0)
}
}
@ViewBuilder
private var footer: some View {
if let fault = snapshot?.fault {
Label(fault, systemImage: "exclamationmark.triangle.fill")
.font(.caption)
.foregroundStyle(.red)
.lineLimit(2)
} else if let state = snapshot?.state {
// Bei "Aus" ist erst der Grund die eigentliche Information.
let reason = snapshot?.offReasons.first
Text(reason.map { "\(state) · \($0)" } ?? state)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
} else if case .failed(let message) = linkState {
Text(message)
.font(.caption)
.foregroundStyle(.orange)
.lineLimit(2)
}
}
private var placeholderText: String {
switch linkState {
case .needsKey: return "Verschlüsselungsschlüssel fehlt im Detail eintragen."
case .failed(let message): return message
default: return "Warte auf Daten…"
}
}
}
/// Kleiner Punkt, der Verbindungszustand und Aktualität zusammenfasst.
struct StatusDot: View {
let linkState: DeviceLinkState
let isStale: Bool
var body: some View {
HStack(spacing: 5) {
Circle()
.fill(color)
.frame(width: 8, height: 8)
Text(label)
.font(.caption2)
.foregroundStyle(.secondary)
}
}
private var color: Color {
switch linkState {
case .live: return isStale ? .orange : .green
case .needsKey: return .orange
case .failed: return .red
default: return .secondary
}
}
private var label: String {
if case .live = linkState, isStale { return "Veraltet" }
return linkState.label
}
}