The explanation sheet now carries a full-width button that closes it and the sheet it was opened from, then lands on Settings — whose Best Value section is the first thing in its list, so the controls are on screen on arrival. It uses .controlSize(.large), the standard iOS button height. Both the More info sheet and the Key sheet are presented by StationsView, and FavouritesView presents its own More info sheet — not ContentView's monitor as assumed — so each host closes whichever sheet it owns on the notification. Before this, one of them stayed up over the Settings tab. The button now announces itself half a second after its own dismissal, so the child and parent dismissals cannot race and drop one of the two.
243 lines
12 KiB
Swift
243 lines
12 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
|
|
|
|
/// 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] }
|
|
|
|
/// 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)
|
|
} footer: {
|
|
Text(String(localized: "Savings below are estimated against the closest station by journey distance — the same baseline as the Stations list.", comment: "Favourites fuel-savings explanation"))
|
|
}
|
|
}
|
|
|
|
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: {
|
|
Text(String(localized: "The first station is the favourite used in single widgets. Tap Edit, then drag to reorder.", comment: "Favourites reorder explanation"))
|
|
}
|
|
|
|
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 {
|
|
if !favourites.isEmpty {
|
|
ToolbarItemGroup(placement: .topBarTrailing) {
|
|
Button {
|
|
showTrends = true
|
|
} label: {
|
|
Image(systemName: "chart.xyaxis.line")
|
|
.accessibilityLabel(String(localized: "Trends", comment: "Trends button accessibility label"))
|
|
}
|
|
EditButton()
|
|
}
|
|
}
|
|
}
|
|
.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
|
|
}
|
|
}
|
|
}
|
|
}
|