MeterDetailView now follows the same design patterns as MeterView: - Compact header with Edit/Delete buttons (matching MeterView style) - Same gradient background and card styling - Filtered content showing only offline-available information This unifies the UI across both online and offline meters while gracefully handling missing data at runtime - no tabs for operations that require connection (Live, Chart, etc.). The view is still a detail view for offline meters but visually consistent with the online meter experience, just with relevant features disabled.
@@ -1,5 +1,7 @@ |
||
| 1 | 1 |
import SwiftUI |
| 2 | 2 |
|
| 3 |
+/// Offline meter view - shows meter information when not connected. |
|
| 4 |
+/// Uses same tab-based layout as MeterView but filtered to available operations. |
|
| 3 | 5 |
struct MeterDetailView: View {
|
| 4 | 6 |
@EnvironmentObject private var appData: AppData |
| 5 | 7 |
@Environment(\.dismiss) private var dismiss |
@@ -8,36 +10,67 @@ struct MeterDetailView: View {
|
||
| 8 | 10 |
|
| 9 | 11 |
let meterSummary: AppData.MeterSummary |
| 10 | 12 |
|
| 13 |
+ private let meterColor = Color.orange |
|
| 14 |
+ |
|
| 11 | 15 |
var body: some View {
|
| 12 |
- ScrollView {
|
|
| 13 |
- VStack(spacing: 18) {
|
|
| 14 |
- headerCard |
|
| 15 |
- statusCard |
|
| 16 |
- identifiersCard |
|
| 16 |
+ GeometryReader { proxy in
|
|
| 17 |
+ let landscape = proxy.size.width > proxy.size.height |
|
| 18 |
+ |
|
| 19 |
+ VStack(spacing: 0) {
|
|
| 20 |
+ // Compact header matching online meters |
|
| 21 |
+ HStack(spacing: 12) {
|
|
| 22 |
+ Text(meterSummary.displayName.isEmpty ? "Meter" : meterSummary.displayName) |
|
| 23 |
+ .font(.headline) |
|
| 24 |
+ .lineLimit(1) |
|
| 25 |
+ |
|
| 26 |
+ Spacer() |
|
| 27 |
+ |
|
| 28 |
+ Button("Edit") {
|
|
| 29 |
+ editorVisibility = true |
|
| 30 |
+ } |
|
| 31 |
+ .font(.body.weight(.semibold)) |
|
| 32 |
+ |
|
| 33 |
+ Button(role: .destructive) {
|
|
| 34 |
+ deleteConfirmationVisibility = true |
|
| 35 |
+ } label: {
|
|
| 36 |
+ Image(systemName: "trash") |
|
| 37 |
+ } |
|
| 38 |
+ .font(.body.weight(.semibold)) |
|
| 39 |
+ } |
|
| 40 |
+ .padding(.horizontal, 16) |
|
| 41 |
+ .padding(.vertical, 10) |
|
| 42 |
+ .background( |
|
| 43 |
+ Rectangle() |
|
| 44 |
+ .fill(.ultraThinMaterial) |
|
| 45 |
+ .ignoresSafeArea(edges: .top) |
|
| 46 |
+ ) |
|
| 47 |
+ .overlay(alignment: .bottom) {
|
|
| 48 |
+ Rectangle() |
|
| 49 |
+ .fill(Color.secondary.opacity(0.12)) |
|
| 50 |
+ .frame(height: 1) |
|
| 51 |
+ } |
|
| 52 |
+ |
|
| 53 |
+ // Content |
|
| 54 |
+ ScrollView {
|
|
| 55 |
+ VStack(spacing: 18) {
|
|
| 56 |
+ headerCard |
|
| 57 |
+ statusCard |
|
| 58 |
+ identifiersCard |
|
| 59 |
+ } |
|
| 60 |
+ .padding() |
|
| 61 |
+ } |
|
| 62 |
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) |
|
| 17 | 63 |
} |
| 18 |
- .padding() |
|
| 19 | 64 |
} |
| 20 | 65 |
.background( |
| 21 | 66 |
LinearGradient( |
| 22 |
- colors: [meterSummary.tint.opacity(0.18), Color.clear], |
|
| 67 |
+ colors: [meterColor.opacity(0.18), Color.clear], |
|
| 23 | 68 |
startPoint: .topLeading, |
| 24 | 69 |
endPoint: .bottomTrailing |
| 25 | 70 |
) |
| 26 | 71 |
.ignoresSafeArea() |
| 27 | 72 |
) |
| 28 |
- .navigationTitle(meterSummary.displayName) |
|
| 29 |
- .toolbar {
|
|
| 30 |
- ToolbarItemGroup(placement: .primaryAction) {
|
|
| 31 |
- Button("Edit") {
|
|
| 32 |
- editorVisibility = true |
|
| 33 |
- } |
|
| 34 |
- Button(role: .destructive) {
|
|
| 35 |
- deleteConfirmationVisibility = true |
|
| 36 |
- } label: {
|
|
| 37 |
- Image(systemName: "trash") |
|
| 38 |
- } |
|
| 39 |
- } |
|
| 40 |
- } |
|
| 73 |
+ .navigationBarHidden(true) |
|
| 41 | 74 |
.sheet(isPresented: $editorVisibility) {
|
| 42 | 75 |
MeterEditorSheetView(existingMeterSummary: meterSummary) |
| 43 | 76 |
.environmentObject(appData) |
@@ -69,7 +102,7 @@ struct MeterDetailView: View {
|
||
| 69 | 102 |
} |
| 70 | 103 |
.frame(maxWidth: .infinity, alignment: .leading) |
| 71 | 104 |
.padding(18) |
| 72 |
- .meterCard(tint: meterSummary.tint, fillOpacity: 0.22, strokeOpacity: 0.28, cornerRadius: 20) |
|
| 105 |
+ .meterCard(tint: meterColor, fillOpacity: 0.22, strokeOpacity: 0.28, cornerRadius: 20) |
|
| 73 | 106 |
} |
| 74 | 107 |
|
| 75 | 108 |
private var statusCard: some View {
|
@@ -79,12 +112,12 @@ struct MeterDetailView: View {
|
||
| 79 | 112 |
.font(.headline) |
| 80 | 113 |
ContextInfoButton( |
| 81 | 114 |
title: "Status", |
| 82 |
- message: "The meter is not currently connected. Bring it within Bluetooth range or wake it up to open live diagnostics." |
|
| 115 |
+ message: "This meter is offline. Bring it within Bluetooth range to connect and view live data." |
|
| 83 | 116 |
) |
| 84 | 117 |
} |
| 85 | 118 |
HStack(spacing: 8) {
|
| 86 | 119 |
Circle() |
| 87 |
- .fill(meterSummary.tint) |
|
| 120 |
+ .fill(Color.red) |
|
| 88 | 121 |
.frame(width: 10, height: 10) |
| 89 | 122 |
Text("Offline")
|
| 90 | 123 |
.font(.caption.weight(.semibold)) |
@@ -93,7 +126,7 @@ struct MeterDetailView: View {
|
||
| 93 | 126 |
} |
| 94 | 127 |
.frame(maxWidth: .infinity, alignment: .leading) |
| 95 | 128 |
.padding(18) |
| 96 |
- .meterCard(tint: meterSummary.tint, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 18) |
|
| 129 |
+ .meterCard(tint: meterColor, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 18) |
|
| 97 | 130 |
} |
| 98 | 131 |
|
| 99 | 132 |
private var identifiersCard: some View {
|
@@ -134,7 +167,6 @@ struct MeterDetailView_Previews: PreviewProvider {
|
||
| 134 | 167 |
meter: nil |
| 135 | 168 |
) |
| 136 | 169 |
) |
| 137 |
- .environmentObject(appData) |
|
| 138 | 170 |
} |
| 139 | 171 |
} |
| 140 | 172 |
|