Files
fuelboard/FuelBoard/FavouritesView.swift
T
FuelBoard Contributor c2a9943d4c Favourites tab: fuel-type selector gets the same coloured pump icons
- FuelTypeSegmentedPicker promoted to shared (was private in StationsView)
  and parameterised by fuel list so Favourites passes only fuels with favourites
- Favourites tab swaps native .segmented picker for the capsule control with
  green/purple/grey fuelpump icons, matching Stations
2026-08-12 23:03:35 +01:00

121 lines
5.3 KiB
Swift

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: [FavouriteEntry]
/// The app's currently selected fuel — used only as the initial tab.
let selectedFuel: FuelType
let location: Coordinate?
let distanceUnit: DistanceUnit
var onToggleFavourite: (FuelStation, FuelType) -> Void = { _, _ in }
/// 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.fuel == fuel }
}
}
/// The active tab. Falls back if the chosen fuel ever loses all its
/// favourites (e.g. the last one is un-starred while viewing it).
@State private var fuel: FuelType = .e10
private var activeFuel: FuelType {
availableFuels.contains(fuel) ? fuel : (availableFuels.first ?? .e10)
}
/// Stations favourited for the active fuel, cheapest first (distance tiebreak).
private var ranked: [FuelStation] {
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]!
if lPrice != rPrice { return lPrice < rPrice }
guard let location else { return false }
return lhs.distanceKM(to: location.lat, lng2: location.lng) <
rhs.distanceKM(to: location.lat, lng2: location.lng)
}
}
/// 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: [FavouriteEntry],
selectedFuel: FuelType,
location: Coordinate?,
distanceUnit: DistanceUnit,
onToggleFavourite: @escaping (FuelStation, FuelType) -> Void = { _, _ in }) {
self.favourites = favourites
self.selectedFuel = selectedFuel
self.location = location
self.distanceUnit = distanceUnit
self.onToggleFavourite = onToggleFavourite
_fuel = State(initialValue: selectedFuel)
}
var body: some View {
NavigationStack {
List {
if favourites.isEmpty {
Section {
VStack(spacing: 10) {
Image(systemName: "star")
.font(.system(size: 40))
.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 for that fuel.")
.font(.caption)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 24)
}
} else {
Section("Fuel type") {
FuelTypeSegmentedPicker(
selection: $fuel,
fuels: availableFuels
)
}
Section("\(activeFuel.displayName) favourites — cheapest first") {
ForEach(Array(ranked.enumerated()), id: \.element.id) { index, station in
StationRow(
station: station,
fuel: activeFuel,
location: location,
distanceUnit: distanceUnit,
baselinePrice: cheapestPrice,
isTopResult: index == 0,
isFavourite: activeFuelFavouriteIDs.contains(station.id),
onToggleFavourite: { onToggleFavourite(station, activeFuel) }
)
}
}
if let cheapest = cheapest {
Section {
Text("\(cheapest.name) is your cheapest \(activeFuel.displayName) favourite. Favourites are monitored automatically — switch to the Alerts tab and enable alerts to get pinged when you approach one that's the cheapest within the radius.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
.navigationTitle("Favourites")
}
}
}