Favourites gains the same info button the Stations list has, last in the trailing toolbar group so it sits to the right of Edit. It stays visible when the list is empty, because the legend explains the prices and labels before anything is starred — Trends and Edit still hide with no favourites. The legend itself is now one view, KeySheetView, lifted out of StationsView's private computed property so both tabs present the same sheet and cannot drift apart. It lives in StationsView.swift because the key's row types are file-private there, which keeps the Xcode project untouched. Stations passes its selected fuel's tint and Favourites its active tab's, so the station-label pump badge matches the rows on screen; everything else — detents, the two nested sheets, and the -showKeySheet / -showBestValueInfo / -showImportantInfo QA launch hooks — behaves exactly as before.
289 lines
14 KiB
Swift
289 lines
14 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. The list shows the
|
|
/// USER'S manual order (drag to reorder — Edit button in the toolbar): the
|
|
/// FIRST station in each fuel's list is the one used as "the favourite" in
|
|
/// single widgets. 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
|
|
let priceDisplayStyle: PriceDisplayStyle
|
|
let stationTapAction: StationTapAction
|
|
/// Fuel Savings config. When it is configured this tab shows the SAME
|
|
/// fill-up control and per-row estimate as the Stations list: one session
|
|
/// amount is shared, so adjusting it in either list updates both, and it
|
|
/// still never touches the Settings default.
|
|
let fuelSavingsSettings: FuelSavingsSettings
|
|
@Binding var fuelSavingsAmount: FuelSavingsFillAmount
|
|
/// Estimated saving for a favourite, against the closest station by
|
|
/// journey distance — the same baseline and definition the Stations list uses.
|
|
var savingsForStation: (FuelStation) -> FuelSavingsAvailability? = { _ in nil }
|
|
var journeyDiagnosticsForStation: (FuelStation) -> FuelSavingsJourneyDiagnostics? = { _ in nil }
|
|
var onToggleFavourite: (FuelStation, FuelType) -> Void = { _, _ in }
|
|
/// Persists a reordered favourites array (after drag-and-drop).
|
|
var onReorder: ([FavouriteEntry]) -> Void = { _ in }
|
|
/// Propagated from ContentView — Trends' history failure with no data
|
|
/// raises the global connection banner; recovery clears it.
|
|
var onHistoryUnavailable: (() -> Void)? = nil
|
|
var onHistoryRecovered: (() -> Void)? = nil
|
|
|
|
/// 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
|
|
|
|
/// Trends sheet (price history chart) presentation state.
|
|
@State private var showTrends = false
|
|
|
|
/// The Key (legend) sheet — the same sheet the Stations tab presents, opened
|
|
/// from the info button beside Edit.
|
|
@State private var showKey = false
|
|
/// The Key sheet's two nested sheets, owned here because its rows set them
|
|
/// (same arrangement as StationsView, so the QA hooks behave identically).
|
|
@State private var showBestValueInfo = false
|
|
@State private var showImportantInformation = false
|
|
|
|
/// Station picked for the More-info sheet (tap action = showDetails).
|
|
@State private var detailStation: FuelStation?
|
|
|
|
private var activeFuel: FuelType {
|
|
availableFuels.contains(fuel) ? fuel : (availableFuels.first ?? .e10)
|
|
}
|
|
|
|
/// Stations favourited for the active fuel, in the USER'S stored order —
|
|
/// NOT price-sorted. The first row is the favourite used by single
|
|
/// widgets (see footer note).
|
|
private var ordered: [FuelStation] {
|
|
favourites.filter { $0.fuel == activeFuel }.map(\.station)
|
|
}
|
|
|
|
/// Cheapest favourited station for the active fuel — the RAG baseline and
|
|
/// the "cheapest favourite" callout, independent of the manual order.
|
|
private var cheapest: FuelStation? {
|
|
ordered.min { ($0.prices[activeFuel] ?? .infinity) < ($1.prices[activeFuel] ?? .infinity) }
|
|
}
|
|
private var cheapestPrice: Double? { cheapest?.prices[activeFuel] }
|
|
|
|
/// The baseline note is useful only when at least one visible favourite has
|
|
/// an actual savings comparison. An unavailable result (for example, while
|
|
/// location is missing) must not leave an orphaned explanation below the list.
|
|
private var showsSavingsComparisonExplanation: Bool {
|
|
fuelSavingsSettings.isConfigured && ordered.contains { station in
|
|
savingsForStation(station)?.result != nil
|
|
}
|
|
}
|
|
|
|
/// 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))
|
|
}
|
|
|
|
init(favourites: [FavouriteEntry],
|
|
selectedFuel: FuelType,
|
|
location: Coordinate?,
|
|
distanceUnit: DistanceUnit,
|
|
priceDisplayStyle: PriceDisplayStyle,
|
|
stationTapAction: StationTapAction,
|
|
fuelSavingsSettings: FuelSavingsSettings = .defaults,
|
|
fuelSavingsAmount: Binding<FuelSavingsFillAmount>,
|
|
savingsForStation: @escaping (FuelStation) -> FuelSavingsAvailability? = { _ in nil },
|
|
journeyDiagnosticsForStation: @escaping (FuelStation) -> FuelSavingsJourneyDiagnostics? = { _ in nil },
|
|
onToggleFavourite: @escaping (FuelStation, FuelType) -> Void = { _, _ in },
|
|
onReorder: @escaping ([FavouriteEntry]) -> Void = { _ in },
|
|
onHistoryUnavailable: (() -> Void)? = nil,
|
|
onHistoryRecovered: (() -> Void)? = nil) {
|
|
self.favourites = favourites
|
|
self.selectedFuel = selectedFuel
|
|
self.location = location
|
|
self.distanceUnit = distanceUnit
|
|
self.priceDisplayStyle = priceDisplayStyle
|
|
self.stationTapAction = stationTapAction
|
|
self.fuelSavingsSettings = fuelSavingsSettings
|
|
_fuelSavingsAmount = fuelSavingsAmount
|
|
self.savingsForStation = savingsForStation
|
|
self.journeyDiagnosticsForStation = journeyDiagnosticsForStation
|
|
self.onToggleFavourite = onToggleFavourite
|
|
self.onReorder = onReorder
|
|
self.onHistoryUnavailable = onHistoryUnavailable
|
|
self.onHistoryRecovered = onHistoryRecovered
|
|
_fuel = State(initialValue: selectedFuel)
|
|
}
|
|
|
|
/// Drag-and-drop reorder (List edit mode): the active fuel's entries are
|
|
/// reordered within the global array; the fuel's block keeps its original
|
|
/// position and other fuels keep their relative order (FuelStore helper).
|
|
private func moveFavourite(fromOffsets source: IndexSet, toOffset destination: Int) {
|
|
onReorder(FuelStore.reorderedFavourites(
|
|
favourites, fuel: activeFuel,
|
|
fromOffsets: source, toOffset: destination
|
|
))
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
List {
|
|
if favourites.isEmpty {
|
|
Section {
|
|
VStack(spacing: 10) {
|
|
Image(systemName: "star")
|
|
.font(.system(size: 40))
|
|
.foregroundStyle(.secondary)
|
|
Text(String(localized: "No favourites yet", comment: "Favourites empty-state title"))
|
|
.font(.headline)
|
|
Text(String(localized: "Tap the star on any station in the Stations tab to pin it here. The first station in each list becomes the favourite used in single widgets — drag to put your pick first.", comment: "Favourites empty-state explanation"))
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 24)
|
|
}
|
|
} else {
|
|
Section(String(localized: "Fuel type", comment: "Fuel type section title")) {
|
|
FuelTypeSegmentedPicker(
|
|
selection: $fuel,
|
|
fuels: availableFuels
|
|
)
|
|
}
|
|
|
|
if fuelSavingsSettings.isConfigured {
|
|
Section {
|
|
FuelSavingsAdjustmentView(amount: $fuelSavingsAmount)
|
|
}
|
|
}
|
|
|
|
if showsSavingsComparisonExplanation {
|
|
Section {
|
|
Text(String(localized: "Price colours show pump price competitiveness. Savings compare with your closest station, including journey costs.", comment: "Station listing price and savings explanation"))
|
|
.font(.footnote)
|
|
.foregroundStyle(.secondary)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
}
|
|
|
|
Section {
|
|
ForEach(Array(ordered.enumerated()), id: \.element.id) { index, station in
|
|
StationRow(
|
|
station: station,
|
|
fuel: activeFuel,
|
|
location: location,
|
|
distanceUnit: distanceUnit,
|
|
priceDisplayStyle: priceDisplayStyle,
|
|
baselinePrice: cheapestPrice,
|
|
isTopResult: station.id == cheapest?.id,
|
|
isFavourite: activeFuelFavouriteIDs.contains(station.id),
|
|
tapAction: stationTapAction,
|
|
fuelSavings: savingsForStation(station),
|
|
onToggleFavourite: { onToggleFavourite(station, activeFuel) },
|
|
onShowDetails: { detailStation = $0 }
|
|
)
|
|
}
|
|
.onMove(perform: moveFavourite)
|
|
} header: {
|
|
Text(String(format: String(localized: "%@ favourites", comment: "Favourites section title for fuel type"), activeFuel.displayName))
|
|
} footer: {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text(String(localized: "The first station is the favourite used in single widgets. Tap Edit, then drag to reorder.", comment: "Favourites reorder explanation"))
|
|
}
|
|
.font(.footnote)
|
|
.foregroundStyle(.secondary)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
if let cheapest = cheapest {
|
|
Section {
|
|
Text(String(format: String(localized: "%@ is your cheapest %@ 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.", comment: "Cheapest favourite explanation"), cheapest.name, activeFuel.displayName))
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle(String(localized: "Favourites", comment: "Favourites tab title"))
|
|
.toolbar {
|
|
ToolbarItemGroup(placement: .topBarTrailing) {
|
|
if !favourites.isEmpty {
|
|
Button {
|
|
showTrends = true
|
|
} label: {
|
|
Image(systemName: "chart.xyaxis.line")
|
|
.accessibilityLabel(String(localized: "Trends", comment: "Trends button accessibility label"))
|
|
}
|
|
EditButton()
|
|
}
|
|
// Same button and same slot as the Stations tab: last in the
|
|
// group, so it sits to the right of Edit. Shown even with no
|
|
// favourites — the legend explains the prices and labels, which
|
|
// matter before anything is starred.
|
|
Button {
|
|
showKey = true
|
|
} label: {
|
|
Image(systemName: "info.circle")
|
|
}
|
|
.accessibilityLabel(String(localized: "Key", comment: "Key button accessibility label"))
|
|
}
|
|
}
|
|
.sheet(isPresented: $showKey) {
|
|
KeySheetView(
|
|
fuelTint: activeFuel.tintColor,
|
|
showBestValueInfo: $showBestValueInfo,
|
|
showImportantInformation: $showImportantInformation
|
|
)
|
|
}
|
|
.sheet(isPresented: $showTrends) {
|
|
TrendsView(
|
|
favourites: favourites,
|
|
selectedFuel: activeFuel,
|
|
priceDisplayStyle: priceDisplayStyle,
|
|
onHistoryUnavailable: onHistoryUnavailable,
|
|
onHistoryRecovered: onHistoryRecovered
|
|
)
|
|
}
|
|
.sheet(item: $detailStation) { station in
|
|
StationDetailView(
|
|
station: station,
|
|
location: location,
|
|
distanceUnit: distanceUnit,
|
|
priceStyle: priceDisplayStyle,
|
|
selectedFuel: activeFuel,
|
|
fuelSavings: savingsForStation(station),
|
|
fuelSavingsAmount: fuelSavingsAmount,
|
|
journeyDiagnostics: journeyDiagnosticsForStation(station),
|
|
// Every station listed here IS a favourite for the tab's
|
|
// fuel, so the star starts filled; tapping it unstars.
|
|
isFavourite: true,
|
|
onToggleFavourite: { onToggleFavourite(station, activeFuel) }
|
|
)
|
|
}
|
|
.onReceive(NotificationCenter.default.publisher(for: .fuelBoardOpenBestValueSettings)) { _ in
|
|
// "Setup Best Value" from the explanation sheet, opened inside
|
|
// this view's More info sheet. That sheet closes itself; this one
|
|
// is presented from HERE and would otherwise stay over Settings.
|
|
detailStation = nil
|
|
}
|
|
.onAppear {
|
|
#if DEBUG
|
|
// QA hook: launch with `-showTrends` to open the sheet
|
|
// without a tap (same pattern as -showKeySheet).
|
|
if ProcessInfo.processInfo.arguments.contains("-showTrends") {
|
|
showTrends = true
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|