Files
fuelboard/FuelBoard/FavouritesView.swift
T
FuelBoard Contributor 0dbf88ee7d Favourites: split list by fuel type with segmented tabs
Only fuel types that currently have at least one favourite appear as
tabs; each tab ranks that fuel's favourites cheapest-first and the
narrative (crown header, section title, cheapest-favourite explainer)
names the active fuel.
2026-08-12 08:17:10 +01:00

130 lines
5.6 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.
struct FavouritesView: View {
let favourites: [FuelStation]
/// 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 }
/// Fuel types that currently have at least one favourite selling them —
/// these are the only tabs shown.
private var availableFuels: [FuelType] {
FuelType.allCases.filter { fuel in
favourites.contains { $0.prices[fuel] != nil }
}
}
/// 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)
}
/// Favourites that sell the active fuel, cheapest first (distance tiebreak).
private var ranked: [FuelStation] {
let available = favourites.filter { $0.prices[activeFuel] != nil }
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)
}
}
private var cheapest: FuelStation? { ranked.first }
private var cheapestPrice: Double? { cheapest?.prices[activeFuel] }
init(favourites: [FuelStation],
selectedFuel: FuelType,
location: Coordinate?,
distanceUnit: DistanceUnit,
favouriteIDs: Set<String>,
onToggleFavourite: @escaping (FuelStation) -> Void = { _ in }) {
self.favourites = favourites
self.selectedFuel = selectedFuel
self.location = location
self.distanceUnit = distanceUnit
self.favouriteIDs = favouriteIDs
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.")
.font(.caption)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 24)
}
} else {
Section("Fuel type") {
Picker("Fuel type", selection: $fuel) {
ForEach(availableFuels) { fuel in
Text(fuel.shortName).tag(fuel)
}
}
.pickerStyle(.segmented)
}
Section {
if let cheapest, let price = cheapestPrice {
HStack(spacing: 10) {
Image(systemName: "crown.fill")
.foregroundStyle(.yellow)
Text("Cheapest \(activeFuel.displayName) favourite: \(cheapest.name)\(String(format: "%.1fp", price))")
.font(.footnote)
}
}
}
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: favouriteIDs.contains(station.id),
onToggleFavourite: { onToggleFavourite(station) }
)
}
}
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")
}
}
}