Favourites are fuel-scoped: starring pins the station for one fuel only
Fixing the bug where favouriting an Unleaded entry also created a Diesel favourite. Favourites are now [FavouriteEntry] = (station, fuel) pairs: - Starring a row pins it only for the fuel being viewed - Favourites tabs show only fuels that actually have favourites - Geofence monitor gives priority slots only to favourites matching the monitored fuel - Tests updated + new fuel-scoping test (31/31 passing)
This commit is contained in:
@@ -10,7 +10,7 @@ struct ContentView: View {
|
||||
@State private var sortMode: SortMode = FuelStore.loadSortMode()
|
||||
@State private var stationLimit: Int = FuelStore.loadStationLimit()
|
||||
@State private var distanceUnit: DistanceUnit = FuelStore.loadDistanceUnit()
|
||||
@State private var favourites: [FuelStation] = FuelStore.loadFavourites()
|
||||
@State private var favourites: [FavouriteEntry] = FuelStore.loadFavourites()
|
||||
@State private var alertsEnabled: Bool = FuelStore.loadAlertsEnabled()
|
||||
@State private var alertsRadius: Double = FuelStore.loadAlertsRadius()
|
||||
@State private var location: Coordinate? = {
|
||||
@@ -95,11 +95,13 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Station IDs favourited for the CURRENTLY selected fuel — the star on a
|
||||
/// Stations-tab row reflects the fuel being viewed (fuel-scoped favourites).
|
||||
private var favouriteIDs: Set<String> {
|
||||
Set(favourites.map(\.id))
|
||||
Set(favourites.filter { $0.fuel == selectedFuel }.map(\.station.id))
|
||||
}
|
||||
|
||||
private var refreshedFavourites: [FuelStation] {
|
||||
private var refreshedFavourites: [FavouriteEntry] {
|
||||
FuelStore.refreshedFavourites(favourites, from: stations)
|
||||
}
|
||||
|
||||
@@ -127,7 +129,6 @@ struct ContentView: View {
|
||||
selectedFuel: selectedFuel,
|
||||
location: location,
|
||||
distanceUnit: distanceUnit,
|
||||
favouriteIDs: favouriteIDs,
|
||||
onToggleFavourite: toggleFavourite
|
||||
)
|
||||
.tabItem { Label("Favourites", systemImage: "star.fill") }
|
||||
@@ -222,11 +223,12 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleFavourite(_ station: FuelStation) {
|
||||
if favouriteIDs.contains(station.id) {
|
||||
favourites.removeAll { $0.id == station.id }
|
||||
private func toggleFavourite(_ station: FuelStation, fuel: FuelType) {
|
||||
let key = FavouriteEntry(station: station, fuel: fuel)
|
||||
if favourites.contains(where: { $0.id == key.id }) {
|
||||
favourites.removeAll { $0.id == key.id }
|
||||
} else {
|
||||
favourites.append(station)
|
||||
favourites.append(key)
|
||||
}
|
||||
FuelStore.saveFavourites(favourites)
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
|
||||
@@ -3,20 +3,21 @@ import SwiftUI
|
||||
/// Favourites tab — starred stations, split by fuel type. Each fuel type
|
||||
/// with at least one favourite appears as its own tab; that tab's favourites
|
||||
/// are ranked cheapest-first for that fuel, with the cheapest called out.
|
||||
/// Favourites are fuel-scoped entries — starring a station only pins it for
|
||||
/// the fuel you were viewing, so an Unleaded favourite never shows as Diesel.
|
||||
struct FavouritesView: View {
|
||||
let favourites: [FuelStation]
|
||||
let favourites: [FavouriteEntry]
|
||||
/// The app's currently selected fuel — used only as the initial tab.
|
||||
let selectedFuel: FuelType
|
||||
let location: Coordinate?
|
||||
let distanceUnit: DistanceUnit
|
||||
let favouriteIDs: Set<String>
|
||||
var onToggleFavourite: (FuelStation) -> Void = { _ in }
|
||||
var onToggleFavourite: (FuelStation, FuelType) -> Void = { _, _ in }
|
||||
|
||||
/// Fuel types that currently have at least one favourite selling them —
|
||||
/// these are the only tabs shown.
|
||||
/// Fuel types that currently have at least one favourite — these are the
|
||||
/// only tabs shown (a fuel with no favourites gets no tab).
|
||||
private var availableFuels: [FuelType] {
|
||||
FuelType.allCases.filter { fuel in
|
||||
favourites.contains { $0.prices[fuel] != nil }
|
||||
favourites.contains { $0.fuel == fuel }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,9 +29,9 @@ struct FavouritesView: View {
|
||||
availableFuels.contains(fuel) ? fuel : (availableFuels.first ?? .e10)
|
||||
}
|
||||
|
||||
/// Favourites that sell the active fuel, cheapest first (distance tiebreak).
|
||||
/// Stations favourited for the active fuel, cheapest first (distance tiebreak).
|
||||
private var ranked: [FuelStation] {
|
||||
let available = favourites.filter { $0.prices[activeFuel] != nil }
|
||||
let available = favourites.filter { $0.fuel == activeFuel }.map(\.station)
|
||||
return available.sorted { lhs, rhs in
|
||||
let lPrice = lhs.prices[activeFuel]!
|
||||
let rPrice = rhs.prices[activeFuel]!
|
||||
@@ -41,20 +42,23 @@ struct FavouritesView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// IDs favourited for the active fuel — the star state on each row.
|
||||
private var activeFuelFavouriteIDs: Set<String> {
|
||||
Set(favourites.filter { $0.fuel == activeFuel }.map(\.station.id))
|
||||
}
|
||||
|
||||
private var cheapest: FuelStation? { ranked.first }
|
||||
private var cheapestPrice: Double? { cheapest?.prices[activeFuel] }
|
||||
|
||||
init(favourites: [FuelStation],
|
||||
init(favourites: [FavouriteEntry],
|
||||
selectedFuel: FuelType,
|
||||
location: Coordinate?,
|
||||
distanceUnit: DistanceUnit,
|
||||
favouriteIDs: Set<String>,
|
||||
onToggleFavourite: @escaping (FuelStation) -> Void = { _ in }) {
|
||||
onToggleFavourite: @escaping (FuelStation, FuelType) -> Void = { _, _ in }) {
|
||||
self.favourites = favourites
|
||||
self.selectedFuel = selectedFuel
|
||||
self.location = location
|
||||
self.distanceUnit = distanceUnit
|
||||
self.favouriteIDs = favouriteIDs
|
||||
self.onToggleFavourite = onToggleFavourite
|
||||
_fuel = State(initialValue: selectedFuel)
|
||||
}
|
||||
@@ -70,7 +74,7 @@ struct FavouritesView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
Text("No favourites yet")
|
||||
.font(.headline)
|
||||
Text("Tap the star on any station in the Stations tab to pin it here, ranked by which is cheapest.")
|
||||
Text("Tap the star on any station in the Stations tab to pin it here, ranked by which is cheapest for that fuel.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
@@ -108,8 +112,8 @@ struct FavouritesView: View {
|
||||
distanceUnit: distanceUnit,
|
||||
baselinePrice: cheapestPrice,
|
||||
isTopResult: index == 0,
|
||||
isFavourite: favouriteIDs.contains(station.id),
|
||||
onToggleFavourite: { onToggleFavourite(station) }
|
||||
isFavourite: activeFuelFavouriteIDs.contains(station.id),
|
||||
onToggleFavourite: { onToggleFavourite(station, activeFuel) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,15 +31,20 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
fuel = FuelStore.loadSelectedFuel()
|
||||
radiusKM = FuelStore.loadAlertsRadius()
|
||||
stations = FuelStore.loadStations()
|
||||
favourites = FuelStore.loadFavourites()
|
||||
favourites = FuelStore.loadFavourites().filter { $0.fuel == fuel }.map(\.station)
|
||||
}
|
||||
|
||||
/// Re-registers geofences. Call whenever stations/favourites/settings change.
|
||||
func update(stations: [FuelStation], favourites: [FuelStation], fuel: FuelType, radiusKM: Double) {
|
||||
/// Favourites are fuel-scoped entries; only entries for the monitored fuel
|
||||
/// get priority slots (a Diesel favourite must not grab a slot while
|
||||
/// monitoring Unleaded).
|
||||
func update(stations: [FuelStation], favourites: [FavouriteEntry], fuel: FuelType, radiusKM: Double) {
|
||||
// Fall back to the shared cache when called before the first fetch
|
||||
// completes (launch, background region-event wake).
|
||||
self.stations = stations.isEmpty ? FuelStore.loadStations() : stations
|
||||
self.favourites = favourites.isEmpty ? FuelStore.loadFavourites() : favourites
|
||||
self.favourites = favourites.isEmpty
|
||||
? FuelStore.loadFavourites().filter { $0.fuel == fuel }.map(\.station)
|
||||
: favourites.filter { $0.fuel == fuel }.map(\.station)
|
||||
self.fuel = fuel
|
||||
self.radiusKM = radiusKM
|
||||
|
||||
@@ -51,8 +56,9 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
guard enabled else { return }
|
||||
|
||||
// Favourites first (guaranteed slots), then closest stations, max 18.
|
||||
var candidates: [FuelStation] = favourites
|
||||
let favIDs = Set(favourites.map(\.id))
|
||||
// `self.favourites` is already filtered to the monitored fuel.
|
||||
var candidates: [FuelStation] = self.favourites
|
||||
let favIDs = Set(self.favourites.map(\.id))
|
||||
let location = FuelStore.loadLocation()
|
||||
let others = stations
|
||||
.filter { !favIDs.contains($0.id) }
|
||||
@@ -88,7 +94,7 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
} else {
|
||||
requestPermissions()
|
||||
// Re-register geofences from restored cache immediately.
|
||||
update(stations: stations, favourites: favourites, fuel: fuel, radiusKM: radiusKM)
|
||||
update(stations: stations, favourites: FuelStore.loadFavourites(), fuel: fuel, radiusKM: radiusKM)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ struct StationsView: View {
|
||||
let topStationID: String?
|
||||
let location: Coordinate?
|
||||
let favouriteIDs: Set<String>
|
||||
var onToggleFavourite: (FuelStation) -> Void = { _ in }
|
||||
var onToggleFavourite: (FuelStation, FuelType) -> Void = { _, _ in }
|
||||
var onRefresh: () async -> Void = {}
|
||||
|
||||
/// Pagination: one page = 10 rows, reset whenever the underlying list
|
||||
@@ -107,7 +107,7 @@ struct StationsView: View {
|
||||
baselinePrice: baselinePrice,
|
||||
isTopResult: station.id == topStationID,
|
||||
isFavourite: favouriteIDs.contains(station.id),
|
||||
onToggleFavourite: { onToggleFavourite(station) }
|
||||
onToggleFavourite: { onToggleFavourite(station, selectedFuel) }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user