@@ -104,7 +104,7 @@ final class AppData : ObservableObject {
|
||
| 104 | 104 |
|
| 105 | 105 |
return MeterSummary( |
| 106 | 106 |
macAddress: macAddress, |
| 107 |
- displayName: liveMeter?.name ?? record?.customName ?? macAddress, |
|
| 107 |
+ displayName: Self.friendlyDisplayName(liveMeter: liveMeter, record: record), |
|
| 108 | 108 |
modelSummary: liveMeter?.deviceModelSummary ?? record?.modelName ?? "Meter", |
| 109 | 109 |
advertisedName: liveMeter?.modelString ?? record?.advertisedName, |
| 110 | 110 |
lastSeen: liveMeter?.lastSeen ?? record?.lastSeen, |
@@ -113,6 +113,12 @@ final class AppData : ObservableObject {
|
||
| 113 | 113 |
) |
| 114 | 114 |
} |
| 115 | 115 |
.sorted { lhs, rhs in
|
| 116 |
+ if lhs.meter != nil && rhs.meter == nil {
|
|
| 117 |
+ return true |
|
| 118 |
+ } |
|
| 119 |
+ if lhs.meter == nil && rhs.meter != nil {
|
|
| 120 |
+ return false |
|
| 121 |
+ } |
|
| 116 | 122 |
let byName = lhs.displayName.localizedCaseInsensitiveCompare(rhs.displayName) |
| 117 | 123 |
if byName != .orderedSame {
|
| 118 | 124 |
return byName == .orderedAscending |
@@ -167,3 +173,24 @@ extension AppData.MeterSummary {
|
||
| 167 | 173 |
} |
| 168 | 174 |
} |
| 169 | 175 |
} |
| 176 |
+ |
|
| 177 |
+private extension AppData {
|
|
| 178 |
+ static func friendlyDisplayName(liveMeter: Meter?, record: MeterNameStore.Record?) -> String {
|
|
| 179 |
+ if let liveName = liveMeter?.name.trimmingCharacters(in: .whitespacesAndNewlines), !liveName.isEmpty {
|
|
| 180 |
+ return liveName |
|
| 181 |
+ } |
|
| 182 |
+ if let customName = record?.customName {
|
|
| 183 |
+ return customName |
|
| 184 |
+ } |
|
| 185 |
+ if let advertisedName = record?.advertisedName {
|
|
| 186 |
+ return advertisedName |
|
| 187 |
+ } |
|
| 188 |
+ if let recordModel = record?.modelName {
|
|
| 189 |
+ return recordModel |
|
| 190 |
+ } |
|
| 191 |
+ if let liveModel = liveMeter?.deviceModelSummary {
|
|
| 192 |
+ return liveModel |
|
| 193 |
+ } |
|
| 194 |
+ return "Meter" |
|
| 195 |
+ } |
|
| 196 |
+} |
|
@@ -18,31 +18,14 @@ struct ConnectionHomeInfoPreviewView: View {
|
||
| 18 | 18 |
MeterInfoRow(label: "Name", value: meter.name) |
| 19 | 19 |
MeterInfoRow(label: "Device Model", value: meter.deviceModelName) |
| 20 | 20 |
MeterInfoRow(label: "Advertised Model", value: meter.modelString) |
| 21 |
- MeterInfoRow(label: "Working Voltage", value: meter.documentedWorkingVoltage) |
|
| 22 |
- MeterInfoRow(label: "Temperature Unit", value: meter.temperatureUnitDescription) |
|
| 23 |
- MeterInfoRow(label: "Last Seen", value: meterHistoryText(for: meter.lastSeen)) |
|
| 24 |
- MeterInfoRow(label: "Last Connected", value: meterHistoryText(for: meter.lastConnectedAt)) |
|
| 25 |
- } |
|
| 26 |
- |
|
| 27 |
- MeterInfoCard(title: "Identifiers", tint: .blue) {
|
|
| 28 | 21 |
MeterInfoRow(label: "MAC", value: meter.btSerial.macAddress.description) |
| 29 | 22 |
if meter.modelNumber != 0 {
|
| 30 | 23 |
MeterInfoRow(label: "Model Identifier", value: "\(meter.modelNumber)") |
| 31 | 24 |
} |
| 32 |
- } |
|
| 33 |
- |
|
| 34 |
- MeterInfoCard(title: "Screen Reporting", tint: .orange) {
|
|
| 35 |
- if meter.reportsCurrentScreenIndex {
|
|
| 36 |
- MeterInfoRow(label: "Current Screen", value: meter.currentScreenDescription) |
|
| 37 |
- Text("The active screen index is reported by the meter and mapped by the app to a known label.")
|
|
| 38 |
- .font(.footnote) |
|
| 39 |
- .foregroundColor(.secondary) |
|
| 40 |
- } else {
|
|
| 41 |
- MeterInfoRow(label: "Current Screen", value: "Not Reported") |
|
| 42 |
- Text("The current screen is not reported by the device payload, or we have not yet identified where and how the protocol announces it.")
|
|
| 43 |
- .font(.footnote) |
|
| 44 |
- .foregroundColor(.secondary) |
|
| 45 |
- } |
|
| 25 |
+ MeterInfoRow(label: "Working Voltage", value: meter.documentedWorkingVoltage) |
|
| 26 |
+ MeterInfoRow(label: "Temperature Unit", value: meter.temperatureUnitDescription) |
|
| 27 |
+ MeterInfoRow(label: "Last Seen", value: meterHistoryText(for: meter.lastSeen)) |
|
| 28 |
+ MeterInfoRow(label: "Last Connected", value: meterHistoryText(for: meter.lastConnectedAt)) |
|
| 46 | 29 |
} |
| 47 | 30 |
|
| 48 | 31 |
MeterInfoCard(title: "Live Device Details", tint: .indigo) {
|
@@ -76,10 +76,6 @@ struct MeterConnectionTabView: View {
|
||
| 76 | 76 |
statusBadge |
| 77 | 77 |
} |
| 78 | 78 |
|
| 79 |
- if compact {
|
|
| 80 |
- Spacer(minLength: 0) |
|
| 81 |
- } |
|
| 82 |
- |
|
| 83 | 79 |
connectionActionArea(compact: compact) |
| 84 | 80 |
|
| 85 | 81 |
if showsActions {
|
@@ -95,15 +91,8 @@ struct MeterConnectionTabView: View {
|
||
| 95 | 91 |
.padding(compact ? 16 : 20) |
| 96 | 92 |
.meterCard(tint: meter.color, fillOpacity: 0.22, strokeOpacity: 0.24) |
| 97 | 93 |
|
| 98 |
- return Group {
|
|
| 99 |
- if compact {
|
|
| 100 |
- cardContent |
|
| 101 |
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) |
|
| 102 |
- } else {
|
|
| 103 |
- cardContent |
|
| 104 |
- .frame(maxWidth: .infinity, alignment: .topLeading) |
|
| 105 |
- } |
|
| 106 |
- } |
|
| 94 |
+ return cardContent |
|
| 95 |
+ .frame(maxWidth: .infinity, alignment: .topLeading) |
|
| 107 | 96 |
} |
| 108 | 97 |
|
| 109 | 98 |
private func meterIdentity(compact: Bool) -> some View {
|
@@ -50,6 +50,15 @@ struct MeterSettingsTabView: View {
|
||
| 50 | 50 |
} |
| 51 | 51 |
} |
| 52 | 52 |
|
| 53 |
+ if meter.operationalState == .dataIsAvailable && meter.model == .TC66C {
|
|
| 54 |
+ settingsCard(title: "Screen Reporting", tint: .orange) {
|
|
| 55 |
+ MeterInfoRow(label: "Current Screen", value: "Not Reported") |
|
| 56 |
+ Text("TC66 is the exception: it does not report the current screen in the payload, so the app keeps this note here instead of showing it on the home screen.")
|
|
| 57 |
+ .font(.footnote) |
|
| 58 |
+ .foregroundColor(.secondary) |
|
| 59 |
+ } |
|
| 60 |
+ } |
|
| 61 |
+ |
|
| 53 | 62 |
if meter.operationalState == .dataIsAvailable {
|
| 54 | 63 |
settingsCard( |
| 55 | 64 |
title: meter.reportsCurrentScreenIndex ? "Screen Controls" : "Page Controls", |
@@ -1,36 +1,36 @@ |
||
| 1 | 1 |
import SwiftUI |
| 2 | 2 |
|
| 3 | 3 |
struct MeterDetailView: View {
|
| 4 |
- let knownMeter: AppData.KnownMeterSummary |
|
| 4 |
+ let meterSummary: AppData.MeterSummary |
|
| 5 | 5 |
|
| 6 | 6 |
var body: some View {
|
| 7 | 7 |
ScrollView {
|
| 8 | 8 |
VStack(spacing: 18) {
|
| 9 | 9 |
headerCard |
| 10 | 10 |
statusCard |
| 11 |
- historyCard |
|
| 11 |
+ identifiersCard |
|
| 12 | 12 |
} |
| 13 | 13 |
.padding() |
| 14 | 14 |
} |
| 15 | 15 |
.background( |
| 16 | 16 |
LinearGradient( |
| 17 |
- colors: [knownMeter.tint.opacity(0.18), Color.clear], |
|
| 17 |
+ colors: [meterSummary.tint.opacity(0.18), Color.clear], |
|
| 18 | 18 |
startPoint: .topLeading, |
| 19 | 19 |
endPoint: .bottomTrailing |
| 20 | 20 |
) |
| 21 | 21 |
.ignoresSafeArea() |
| 22 | 22 |
) |
| 23 |
- .navigationTitle(knownMeter.displayName) |
|
| 23 |
+ .navigationTitle(meterSummary.displayName) |
|
| 24 | 24 |
} |
| 25 | 25 |
|
| 26 | 26 |
private var headerCard: some View {
|
| 27 | 27 |
VStack(alignment: .leading, spacing: 8) {
|
| 28 |
- Text(knownMeter.displayName) |
|
| 28 |
+ Text(meterSummary.displayName) |
|
| 29 | 29 |
.font(.title2.weight(.semibold)) |
| 30 |
- Text(knownMeter.modelSummary) |
|
| 30 |
+ Text(meterSummary.modelSummary) |
|
| 31 | 31 |
.font(.subheadline) |
| 32 | 32 |
.foregroundColor(.secondary) |
| 33 |
- if let advertisedName = knownMeter.advertisedName {
|
|
| 33 |
+ if let advertisedName = meterSummary.advertisedName {
|
|
| 34 | 34 |
Text("Advertised as " + advertisedName)
|
| 35 | 35 |
.font(.caption2) |
| 36 | 36 |
.foregroundColor(.secondary) |
@@ -38,7 +38,7 @@ struct MeterDetailView: View {
|
||
| 38 | 38 |
} |
| 39 | 39 |
.frame(maxWidth: .infinity, alignment: .leading) |
| 40 | 40 |
.padding(18) |
| 41 |
- .meterCard(tint: knownMeter.tint, fillOpacity: 0.22, strokeOpacity: 0.28, cornerRadius: 20) |
|
| 41 |
+ .meterCard(tint: meterSummary.tint, fillOpacity: 0.22, strokeOpacity: 0.28, cornerRadius: 20) |
|
| 42 | 42 |
} |
| 43 | 43 |
|
| 44 | 44 |
private var statusCard: some View {
|
@@ -47,7 +47,7 @@ struct MeterDetailView: View {
|
||
| 47 | 47 |
.font(.headline) |
| 48 | 48 |
HStack(spacing: 8) {
|
| 49 | 49 |
Circle() |
| 50 |
- .fill(knownMeter.tint) |
|
| 50 |
+ .fill(meterSummary.tint) |
|
| 51 | 51 |
.frame(width: 10, height: 10) |
| 52 | 52 |
Text("Offline")
|
| 53 | 53 |
.font(.caption.weight(.semibold)) |
@@ -59,16 +59,17 @@ struct MeterDetailView: View {
|
||
| 59 | 59 |
} |
| 60 | 60 |
.frame(maxWidth: .infinity, alignment: .leading) |
| 61 | 61 |
.padding(18) |
| 62 |
- .meterCard(tint: knownMeter.tint, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 18) |
|
| 62 |
+ .meterCard(tint: meterSummary.tint, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 18) |
|
| 63 | 63 |
} |
| 64 | 64 |
|
| 65 |
- private var historyCard: some View {
|
|
| 65 |
+ private var identifiersCard: some View {
|
|
| 66 | 66 |
VStack(alignment: .leading, spacing: 10) {
|
| 67 |
- Text("History")
|
|
| 67 |
+ Text("Identifiers")
|
|
| 68 | 68 |
.font(.headline) |
| 69 |
- infoRow(label: "Last Seen", value: lastSeenText) |
|
| 70 |
- Divider() |
|
| 71 |
- infoRow(label: "Last Connected", value: lastConnectedText) |
|
| 69 |
+ infoRow(label: "MAC Address", value: meterSummary.macAddress) |
|
| 70 |
+ if let advertisedName = meterSummary.advertisedName {
|
|
| 71 |
+ infoRow(label: "Advertised as", value: advertisedName) |
|
| 72 |
+ } |
|
| 72 | 73 |
} |
| 73 | 74 |
.frame(maxWidth: .infinity, alignment: .leading) |
| 74 | 75 |
.padding(18) |
@@ -84,22 +85,12 @@ struct MeterDetailView: View {
|
||
| 84 | 85 |
.font(.caption) |
| 85 | 86 |
} |
| 86 | 87 |
} |
| 87 |
- |
|
| 88 |
- private var lastSeenText: String {
|
|
| 89 |
- guard let date = knownMeter.lastSeen else { return "—" }
|
|
| 90 |
- return date.format(as: "yyyy-MM-dd HH:mm") |
|
| 91 |
- } |
|
| 92 |
- |
|
| 93 |
- private var lastConnectedText: String {
|
|
| 94 |
- guard let date = knownMeter.lastConnected else { return "—" }
|
|
| 95 |
- return date.format(as: "yyyy-MM-dd HH:mm") |
|
| 96 |
- } |
|
| 97 | 88 |
} |
| 98 | 89 |
|
| 99 | 90 |
struct MeterDetailView_Previews: PreviewProvider {
|
| 100 | 91 |
static var previews: some View {
|
| 101 | 92 |
MeterDetailView( |
| 102 |
- knownMeter: AppData.KnownMeterSummary( |
|
| 93 |
+ meterSummary: AppData.MeterSummary( |
|
| 103 | 94 |
macAddress: "AA:BB:CC:DD:EE:FF", |
| 104 | 95 |
displayName: "Desk Meter", |
| 105 | 96 |
modelSummary: "UM25C", |
@@ -7,7 +7,7 @@ import SwiftUI |
||
| 7 | 7 |
|
| 8 | 8 |
struct SidebarLinkCardView: View {
|
| 9 | 9 |
let title: String |
| 10 |
- let subtitle: String |
|
| 10 |
+ let subtitle: String? |
|
| 11 | 11 |
let symbol: String |
| 12 | 12 |
let tint: Color |
| 13 | 13 |
|
@@ -19,12 +19,14 @@ struct SidebarLinkCardView: View {
|
||
| 19 | 19 |
.frame(width: 42, height: 42) |
| 20 | 20 |
.background(Circle().fill(tint.opacity(0.18))) |
| 21 | 21 |
|
| 22 |
- VStack(alignment: .leading, spacing: 4) {
|
|
| 22 |
+ VStack(alignment: .leading, spacing: subtitle == nil ? 0 : 4) {
|
|
| 23 | 23 |
Text(title) |
| 24 | 24 |
.font(.headline) |
| 25 |
- Text(subtitle) |
|
| 26 |
- .font(.caption) |
|
| 27 |
- .foregroundColor(.secondary) |
|
| 25 |
+ if let subtitle {
|
|
| 26 |
+ Text(subtitle) |
|
| 27 |
+ .font(.caption) |
|
| 28 |
+ .foregroundColor(.secondary) |
|
| 29 |
+ } |
|
| 28 | 30 |
} |
| 29 | 31 |
|
| 30 | 32 |
Spacer() |
@@ -36,4 +38,4 @@ struct SidebarLinkCardView: View {
|
||
| 36 | 38 |
.padding(14) |
| 37 | 39 |
.meterCard(tint: tint, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 18) |
| 38 | 40 |
} |
| 39 |
-} |
|
| 41 |
+} |
|
@@ -97,33 +97,33 @@ struct ContentSidebarHelpSectionView<BluetoothHelpDestination: View, DeviceHelpD |
||
| 97 | 97 |
|
| 98 | 98 |
if activeReason == .cloudSyncUnavailable {
|
| 99 | 99 |
Button(action: onOpenSettings) {
|
| 100 |
- SidebarLinkCardView( |
|
| 101 |
- title: "Open Settings", |
|
| 102 |
- subtitle: "Check Apple ID, iCloud Drive, and any restrictions affecting Cloud sync.", |
|
| 103 |
- symbol: "gearshape.fill", |
|
| 104 |
- tint: .indigo |
|
| 105 |
- ) |
|
| 100 |
+ SidebarLinkCardView( |
|
| 101 |
+ title: "Open Settings", |
|
| 102 |
+ subtitle: nil, |
|
| 103 |
+ symbol: "gearshape.fill", |
|
| 104 |
+ tint: .indigo |
|
| 105 |
+ ) |
|
| 106 | 106 |
} |
| 107 | 107 |
.buttonStyle(.plain) |
| 108 | 108 |
} |
| 109 | 109 |
|
| 110 | 110 |
NavigationLink(destination: bluetoothHelpDestination) {
|
| 111 |
- SidebarLinkCardView( |
|
| 112 |
- title: "Bluetooth", |
|
| 113 |
- subtitle: "Permissions, adapter state, and connection tips.", |
|
| 114 |
- symbol: "bolt.horizontal.circle.fill", |
|
| 115 |
- tint: bluetoothStatusTint |
|
| 116 |
- ) |
|
| 111 |
+ SidebarLinkCardView( |
|
| 112 |
+ title: "Bluetooth", |
|
| 113 |
+ subtitle: nil, |
|
| 114 |
+ symbol: "bolt.horizontal.circle.fill", |
|
| 115 |
+ tint: bluetoothStatusTint |
|
| 116 |
+ ) |
|
| 117 | 117 |
} |
| 118 | 118 |
.buttonStyle(.plain) |
| 119 | 119 |
|
| 120 | 120 |
NavigationLink(destination: deviceHelpDestination) {
|
| 121 |
- SidebarLinkCardView( |
|
| 122 |
- title: "Device", |
|
| 123 |
- subtitle: "Quick checks when a meter is not responding as expected.", |
|
| 124 |
- symbol: "questionmark.circle.fill", |
|
| 125 |
- tint: .orange |
|
| 126 |
- ) |
|
| 121 |
+ SidebarLinkCardView( |
|
| 122 |
+ title: "Device", |
|
| 123 |
+ subtitle: nil, |
|
| 124 |
+ symbol: "questionmark.circle.fill", |
|
| 125 |
+ tint: .orange |
|
| 126 |
+ ) |
|
| 127 | 127 |
} |
| 128 | 128 |
.buttonStyle(.plain) |
| 129 | 129 |
} |
@@ -138,4 +138,4 @@ struct ContentSidebarHelpSectionView<BluetoothHelpDestination: View, DeviceHelpD |
||
| 138 | 138 |
private var sectionSymbol: String {
|
| 139 | 139 |
activeReason?.symbol ?? "questionmark.circle.fill" |
| 140 | 140 |
} |
| 141 |
-} |
|
| 141 |
+} |
|
@@ -97,12 +97,12 @@ struct SidebarHelpSectionView<BluetoothHelpDestination: View, DeviceHelpDestinat |
||
| 97 | 97 |
|
| 98 | 98 |
if activeReason == .cloudSyncUnavailable {
|
| 99 | 99 |
Button(action: onOpenSettings) {
|
| 100 |
- SidebarLinkCardView( |
|
| 101 |
- title: "Open Settings", |
|
| 102 |
- subtitle: "Check Apple ID, iCloud Drive, and any restrictions affecting Cloud sync.", |
|
| 103 |
- symbol: "gearshape.fill", |
|
| 104 |
- tint: .indigo |
|
| 105 |
- ) |
|
| 100 |
+ SidebarLinkCardView( |
|
| 101 |
+ title: "Open Settings", |
|
| 102 |
+ subtitle: nil, |
|
| 103 |
+ symbol: "gearshape.fill", |
|
| 104 |
+ tint: .indigo |
|
| 105 |
+ ) |
|
| 106 | 106 |
} |
| 107 | 107 |
.buttonStyle(.plain) |
| 108 | 108 |
} |
@@ -110,7 +110,7 @@ struct SidebarHelpSectionView<BluetoothHelpDestination: View, DeviceHelpDestinat |
||
| 110 | 110 |
NavigationLink(destination: bluetoothHelpDestination) {
|
| 111 | 111 |
SidebarLinkCardView( |
| 112 | 112 |
title: "Bluetooth", |
| 113 |
- subtitle: "Permissions, adapter state, and connection tips.", |
|
| 113 |
+ subtitle: nil, |
|
| 114 | 114 |
symbol: "bolt.horizontal.circle.fill", |
| 115 | 115 |
tint: bluetoothStatusTint |
| 116 | 116 |
) |
@@ -120,7 +120,7 @@ struct SidebarHelpSectionView<BluetoothHelpDestination: View, DeviceHelpDestinat |
||
| 120 | 120 |
NavigationLink(destination: deviceHelpDestination) {
|
| 121 | 121 |
SidebarLinkCardView( |
| 122 | 122 |
title: "Device", |
| 123 |
- subtitle: "Quick checks when a meter is not responding as expected.", |
|
| 123 |
+ subtitle: nil, |
|
| 124 | 124 |
symbol: "questionmark.circle.fill", |
| 125 | 125 |
tint: .orange |
| 126 | 126 |
) |
@@ -138,4 +138,4 @@ struct SidebarHelpSectionView<BluetoothHelpDestination: View, DeviceHelpDestinat |
||
| 138 | 138 |
private var sectionSymbol: String {
|
| 139 | 139 |
activeReason?.symbol ?? "questionmark.circle.fill" |
| 140 | 140 |
} |
| 141 |
-} |
|
| 141 |
+} |
|
@@ -67,9 +67,17 @@ struct SidebarUSBMetersSectionView: View {
|
||
| 67 | 67 |
} |
| 68 | 68 |
|
| 69 | 69 |
private var usbSectionHeader: some View {
|
| 70 |
- HStack {
|
|
| 71 |
- Text("USB Meters")
|
|
| 72 |
- .font(.headline) |
|
| 70 |
+ HStack(alignment: .firstTextBaseline) {
|
|
| 71 |
+ VStack(alignment: .leading, spacing: 2) {
|
|
| 72 |
+ Text("USB & Known Meters")
|
|
| 73 |
+ .font(.headline) |
|
| 74 |
+ if meters.isEmpty == false {
|
|
| 75 |
+ Text(sectionSubtitleText) |
|
| 76 |
+ .font(.caption) |
|
| 77 |
+ .foregroundColor(.secondary) |
|
| 78 |
+ .lineLimit(1) |
|
| 79 |
+ } |
|
| 80 |
+ } |
|
| 73 | 81 |
Spacer() |
| 74 | 82 |
Text("\(meters.count)")
|
| 75 | 83 |
.font(.caption.weight(.bold)) |
@@ -78,4 +86,25 @@ struct SidebarUSBMetersSectionView: View {
|
||
| 78 | 86 |
.meterCard(tint: .blue, fillOpacity: 0.18, strokeOpacity: 0.24, cornerRadius: 999) |
| 79 | 87 |
} |
| 80 | 88 |
} |
| 89 |
+ |
|
| 90 |
+ private var sectionSubtitleText: String {
|
|
| 91 |
+ switch (liveMeterCount, offlineMeterCount) {
|
|
| 92 |
+ case let (live, offline) where live > 0 && offline > 0: |
|
| 93 |
+ return "\(live) live • \(offline) stored" |
|
| 94 |
+ case let (live, _) where live > 0: |
|
| 95 |
+ return "\(live) live meter\(live == 1 ? "" : "s")" |
|
| 96 |
+ case let (_, offline) where offline > 0: |
|
| 97 |
+ return "\(offline) known meter\(offline == 1 ? "" : "s")" |
|
| 98 |
+ default: |
|
| 99 |
+ return "" |
|
| 100 |
+ } |
|
| 101 |
+ } |
|
| 102 |
+ |
|
| 103 |
+ private var liveMeterCount: Int {
|
|
| 104 |
+ meters.filter { $0.meter != nil }.count
|
|
| 105 |
+ } |
|
| 106 |
+ |
|
| 107 |
+ private var offlineMeterCount: Int {
|
|
| 108 |
+ max(0, meters.count - liveMeterCount) |
|
| 109 |
+ } |
|
| 81 | 110 |
} |