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 { 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, 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 } } } }