Settings Debug: device coordinates + in-range station indicator
- Debug section now shows the last device fix (lat/lng to 5dp) and its age. - In-range indicator uses the SAME criteria as live alerts: stations selling the monitored fuel within the alert radius of the fix. Green dot + count when in range, red when none, grey while waiting for a fix. - Shows the cheapest in-range station (name, distance, price). - ProximityMonitor recomputes the snapshot on every location/stations change and on Debug-section appear; ContentView passes the live status. - SettingsView extracted to its own property to keep TabView body within the compiler type-check budget. 35 tests pass.
This commit is contained in:
@@ -24,7 +24,11 @@ struct SettingsView: View {
|
||||
/// Drives the plain test (no criteria) — routed to the live monitor so it
|
||||
/// can carry a real station's name, price and coordinates.
|
||||
var onPlainTestAlert: () -> Void = {}
|
||||
/// Recomputes the debug location snapshot (coordinates + in-range count).
|
||||
var onRefreshDebugStatus: () -> Void = {}
|
||||
var onShowOnboarding: () -> Void = {}
|
||||
/// Live snapshot of the device fix + stations in range (from the monitor).
|
||||
var debugStatus: DebugLocationStatus?
|
||||
|
||||
@StateObject private var tipStore = TipStore()
|
||||
@State private var showTipAlert = false
|
||||
@@ -88,11 +92,13 @@ struct SettingsView: View {
|
||||
} label: {
|
||||
Label("Send plain test notification", systemImage: "bell.slash.fill")
|
||||
}
|
||||
debugLocationRows
|
||||
} header: {
|
||||
Text("Debug")
|
||||
} footer: {
|
||||
Text("The first button applies the real criteria — cheapest \(alertsFuel.displayName.lowercased()) station within \(distanceUnit.format(alertsRadiusKM)) of you — and sends the actual station's name, price and coordinates. The second fires with no criteria at all (nearest seller), still carrying a real station, just to verify a notification appears and tapping it opens directions.")
|
||||
}
|
||||
.onAppear { onRefreshDebugStatus() }
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -146,6 +152,100 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Debug location block: device coordinates + fix age, then the in-range
|
||||
/// indicator. "In range" mirrors the live alert criteria — stations selling
|
||||
/// the monitored fuel within the alert radius of the last fix.
|
||||
@ViewBuilder
|
||||
private var debugLocationRows: some View {
|
||||
Divider()
|
||||
if let status = debugStatus {
|
||||
HStack {
|
||||
Label("Device", systemImage: "location.fill")
|
||||
Spacer()
|
||||
if let coord = status.coordinate {
|
||||
Text(String(format: "%.5f, %.5f", coord.lat, coord.lng))
|
||||
.font(.footnote)
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Text("No fix")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
if let age = status.fixAge {
|
||||
HStack {
|
||||
Label("Fix age", systemImage: "clock.fill")
|
||||
Spacer()
|
||||
Text(ageText(age))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Label("In range", systemImage: "location.circle.fill")
|
||||
Spacer()
|
||||
HStack(spacing: 6) {
|
||||
if let coord = status.coordinate {
|
||||
Circle()
|
||||
.fill(inRangeColor(status.inRangeCount))
|
||||
.frame(width: 10, height: 10)
|
||||
} else {
|
||||
Circle()
|
||||
.fill(.gray)
|
||||
.frame(width: 10, height: 10)
|
||||
}
|
||||
Text(inRangeText(status))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
if let cheapest = status.cheapestInRange {
|
||||
HStack {
|
||||
Label("Cheapest in range", systemImage: "fuelpump.fill")
|
||||
Spacer()
|
||||
Text(cheapestText(cheapest, fuel: status.fuel, distance: status.cheapestDistanceKM))
|
||||
.font(.footnote)
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
HStack {
|
||||
Label("Device", systemImage: "location.fill")
|
||||
Spacer()
|
||||
Text("—")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func inRangeColor(_ count: Int) -> Color {
|
||||
count > 0 ? .green : .red
|
||||
}
|
||||
|
||||
private func inRangeText(_ status: DebugLocationStatus) -> String {
|
||||
guard status.coordinate != nil else { return "waiting for fix" }
|
||||
let fuelName = status.fuel.displayName.lowercased()
|
||||
if status.inRangeCount == 1 {
|
||||
return "1 \(fuelName) station within \(distanceUnit.format(status.radiusKM))"
|
||||
}
|
||||
return "\(status.inRangeCount) \(fuelName) stations within \(distanceUnit.format(status.radiusKM))"
|
||||
}
|
||||
|
||||
private func cheapestText(_ station: FuelStation, fuel: FuelType, distance: Double?) -> String {
|
||||
let price = station.prices[fuel].map { String(format: "%.1fp", $0) } ?? "—"
|
||||
if let distance {
|
||||
return "\(station.name) · \(distanceUnit.format(distance)) · \(price)"
|
||||
}
|
||||
return "\(station.name) · \(price)"
|
||||
}
|
||||
|
||||
private func ageText(_ age: TimeInterval) -> String {
|
||||
if age < 60 { return "\(Int(age))s ago" }
|
||||
if age < 3600 { return "\(Int(age / 60))m \(Int(age.truncatingRemainder(dividingBy: 60)))s ago" }
|
||||
return "\(Int(age / 3600))h \(Int((age.truncatingRemainder(dividingBy: 3600)) / 60))m ago"
|
||||
}
|
||||
|
||||
private var appVersion: String {
|
||||
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0"
|
||||
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "1"
|
||||
|
||||
Reference in New Issue
Block a user