Alerts choose their own fuel, independent of the Stations tab
The proximity monitor was tied to the Stations-tab fuel selection, so switching the list to another fuel silently re-targeted geofences. Now the Alerts tab has its own 'Fuel to monitor' picker (persisted as fuelboard.alertsFuel): - Monitor reads loadAlertsFuel() on init/background relaunch - Changing the alerts fuel re-registers geofences around stations selling that fuel only - Favourites priority slots stay scoped to the monitored fuel - Tests: alerts fuel round-trip + default (33/33 passing)
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import SwiftUI
|
||||
|
||||
/// Alerts tab — enables the cheapest-station proximity alerts and shows what's
|
||||
/// being monitored.
|
||||
/// being monitored. The fuel being monitored is chosen HERE, independently of
|
||||
/// the Stations-tab selection: users can browse any fuel without re-targeting
|
||||
/// their alerts (and vice versa).
|
||||
struct AlertsView: View {
|
||||
@Binding var enabled: Bool
|
||||
@Binding var radius: Double // stored in km (monitor + storage)
|
||||
@Binding var fuel: FuelType // the fuel alerts monitor for
|
||||
let distanceUnit: DistanceUnit
|
||||
let monitoredCount: Int
|
||||
let lastAlert: String?
|
||||
@@ -23,6 +26,19 @@ struct AlertsView: View {
|
||||
}
|
||||
|
||||
if enabled {
|
||||
Section {
|
||||
Picker("Fuel", selection: $fuel) {
|
||||
ForEach(FuelType.allCases) { fuel in
|
||||
Text(fuel.shortName).tag(fuel)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
} header: {
|
||||
Text("Fuel to monitor")
|
||||
} footer: {
|
||||
Text("Alerts watch for the cheapest \(fuel.displayName.lowercased()) station within the radius. Browsing a different fuel in the Stations tab does not change this.")
|
||||
}
|
||||
|
||||
Section("Trigger radius") {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
@@ -47,10 +63,10 @@ struct AlertsView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
LabeledContent("Geofenced stations", value: "\(monitoredCount)")
|
||||
Text("Your favourites get priority, then the closest stations fill the rest (18 max, iOS region limit).")
|
||||
Text("Your favourites for \(fuel.displayName.lowercased()) get priority, then the closest stations selling that fuel fill the rest (18 max, iOS region limit).")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Alerts are checked against the cheapest station within the trigger radius for the selected fuel. Each station alerts at most once per hour.")
|
||||
Text("Alerts are checked against the cheapest station within the trigger radius for the fuel chosen above. Each station alerts at most once per hour.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ struct ContentView: View {
|
||||
@State private var favourites: [FavouriteEntry] = FuelStore.loadFavourites()
|
||||
@State private var alertsEnabled: Bool = FuelStore.loadAlertsEnabled()
|
||||
@State private var alertsRadius: Double = FuelStore.loadAlertsRadius()
|
||||
@State private var alertsFuel: FuelType = FuelStore.loadAlertsFuel()
|
||||
@State private var location: Coordinate? = {
|
||||
if let loc = FuelStore.loadLocation() { return Coordinate(lat: loc.lat, lng: loc.lng) }
|
||||
return nil
|
||||
@@ -136,6 +137,7 @@ struct ContentView: View {
|
||||
AlertsView(
|
||||
enabled: $alertsEnabled,
|
||||
radius: $alertsRadius,
|
||||
fuel: $alertsFuel,
|
||||
distanceUnit: distanceUnit,
|
||||
monitoredCount: monitor.monitoredStationIDs.count,
|
||||
lastAlert: monitor.lastAlert
|
||||
@@ -162,7 +164,7 @@ struct ContentView: View {
|
||||
showOnboarding = true
|
||||
}
|
||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||
fuel: selectedFuel, radiusKM: alertsRadius)
|
||||
fuel: alertsFuel, radiusKM: alertsRadius)
|
||||
monitor.setEnabled(alertsEnabled)
|
||||
// Refresh only when the cache is stale (twice-a-day policy).
|
||||
Task { await refresh() }
|
||||
@@ -178,7 +180,7 @@ struct ContentView: View {
|
||||
if newPhase == .active {
|
||||
locationManager.startForegroundTracking()
|
||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||
fuel: selectedFuel, radiusKM: alertsRadius)
|
||||
fuel: alertsFuel, radiusKM: alertsRadius)
|
||||
// No network fetch on foreground — pull-to-refresh is the override.
|
||||
} else {
|
||||
locationManager.stopForegroundTracking()
|
||||
@@ -192,7 +194,7 @@ struct ContentView: View {
|
||||
// Geofences follow the user's position, but the station list is
|
||||
// NOT re-fetched on every movement (cached, twice-a-day policy).
|
||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||
fuel: selectedFuel, radiusKM: alertsRadius)
|
||||
fuel: alertsFuel, radiusKM: alertsRadius)
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedFuel) { _, _ in
|
||||
@@ -211,7 +213,7 @@ struct ContentView: View {
|
||||
FuelStore.saveAlertsEnabled(newValue)
|
||||
monitor.setEnabled(newValue)
|
||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||
fuel: selectedFuel, radiusKM: alertsRadius)
|
||||
fuel: alertsFuel, radiusKM: alertsRadius)
|
||||
if newValue {
|
||||
locationManager.startBackgroundTracking()
|
||||
}
|
||||
@@ -219,7 +221,14 @@ struct ContentView: View {
|
||||
.onChange(of: alertsRadius) { _, newValue in
|
||||
FuelStore.saveAlertsRadius(newValue)
|
||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||
fuel: selectedFuel, radiusKM: alertsRadius)
|
||||
fuel: alertsFuel, radiusKM: alertsRadius)
|
||||
}
|
||||
.onChange(of: alertsFuel) { _, newValue in
|
||||
// Alerts fuel is independent of the Stations-tab selection —
|
||||
// changing it re-targets geofences to stations selling that fuel.
|
||||
FuelStore.saveAlertsFuel(newValue)
|
||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||
fuel: newValue, radiusKM: alertsRadius)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +242,7 @@ struct ContentView: View {
|
||||
FuelStore.saveFavourites(favourites)
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||
fuel: selectedFuel, radiusKM: alertsRadius)
|
||||
fuel: alertsFuel, radiusKM: alertsRadius)
|
||||
}
|
||||
|
||||
/// Fetches fresh prices, but only when the cache is stale — unless
|
||||
@@ -261,7 +270,7 @@ struct ContentView: View {
|
||||
}
|
||||
// Keep monitor geofences in sync with the freshest data.
|
||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||
fuel: selectedFuel, radiusKM: alertsRadius)
|
||||
fuel: alertsFuel, radiusKM: alertsRadius)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
// Restore persisted state on background relaunch so region events work
|
||||
// even before the view fully appears.
|
||||
enabled = FuelStore.loadAlertsEnabled()
|
||||
fuel = FuelStore.loadSelectedFuel()
|
||||
fuel = FuelStore.loadAlertsFuel()
|
||||
radiusKM = FuelStore.loadAlertsRadius()
|
||||
stations = FuelStore.loadStations()
|
||||
favourites = FuelStore.loadFavourites().filter { $0.fuel == fuel }.map(\.station)
|
||||
|
||||
Reference in New Issue
Block a user