From cc948dac3a616345e62729f42e467b84bc086c60 Mon Sep 17 00:00:00 2001 From: FuelBoard Contributor Date: Mon, 14 Sep 2026 22:03:20 +0100 Subject: [PATCH] feat: station tap action toggle (open map vs more info) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings → Units gains a Tap station picker: Open map (default, historic behaviour — tap opens Apple Maps directions) or More info (tap shows a detail sheet with name, address, distance from current location, and a Directions button). Applies to Stations and Favourites tabs; persisted keychain-first as fuelboard.stationTapAction. Stations footer hint follows the setting. --- FuelBoard/ContentView.swift | 15 ++++++-- FuelBoard/FavouritesView.swift | 17 +++++++++- FuelBoard/SettingsView.swift | 12 ++++++- FuelBoard/StationDetailView.swift | 47 ++++++++++++++++++++++++++ FuelBoard/StationsView.swift | 22 ++++++++++-- FuelBoard/en.lproj/Localizable.strings | 7 ++++ Shared/FuelStore.swift | 35 +++++++++++++++++++ 7 files changed, 148 insertions(+), 7 deletions(-) create mode 100644 FuelBoard/StationDetailView.swift diff --git a/FuelBoard/ContentView.swift b/FuelBoard/ContentView.swift index dc9b915..e9be66e 100644 --- a/FuelBoard/ContentView.swift +++ b/FuelBoard/ContentView.swift @@ -21,6 +21,7 @@ struct ContentView: View { @State private var stationLimit: Int = FuelStore.loadStationLimit() @State private var distanceUnit: DistanceUnit = FuelStore.loadDistanceUnit() @State private var priceDisplayStyle: PriceDisplayStyle = FuelStore.loadPriceDisplayStyle() + @State private var stationTapAction: StationTapAction = FuelStore.loadStationTapAction() @State private var favourites: [FavouriteEntry] = FuelStore.loadFavourites() @State private var alertsEnabled: Bool = FuelStore.loadAlertsEnabled() @State private var alertsRadius: Double = FuelStore.loadAlertsRadius() @@ -742,6 +743,7 @@ struct ContentView: View { topStationID: topStationID, location: location, favouriteIDs: favouriteIDs, + stationTapAction: stationTapAction, onToggleFavourite: toggleFavourite, onRefresh: { await refresh(force: true) } ) @@ -756,6 +758,7 @@ struct ContentView: View { location: location, distanceUnit: distanceUnit, priceDisplayStyle: priceDisplayStyle, + stationTapAction: stationTapAction, onToggleFavourite: toggleFavourite, onReorder: reorderFavourites, onHistoryUnavailable: { if dataStatus == .live { dataStatus = .connectionProblem } }, @@ -794,6 +797,7 @@ struct ContentView: View { tipStore: tipStore, distanceUnit: $distanceUnit, priceDisplayStyle: $priceDisplayStyle, + stationTapAction: $stationTapAction, dataRefreshMode: $dataRefreshMode, alertsFuel: alertsFuel, alertsRadiusKM: alertsRadius, @@ -931,7 +935,9 @@ struct StationRow: View { let baselinePrice: Double? let isTopResult: Bool let isFavourite: Bool + var tapAction: StationTapAction = .openMap var onToggleFavourite: () -> Void = {} + var onShowDetails: (FuelStation) -> Void = { _ in } private var ragColor: Color { guard let price = station.prices[fuel], let baselinePrice else { return .gray } @@ -1030,8 +1036,13 @@ struct StationRow: View { } .contentShape(Rectangle()) .onTapGesture { - if let url = station.mapsDirectionsURL { - UIApplication.shared.open(url) + switch tapAction { + case .openMap: + if let url = station.mapsDirectionsURL { + UIApplication.shared.open(url) + } + case .showDetails: + onShowDetails(station) } } } diff --git a/FuelBoard/FavouritesView.swift b/FuelBoard/FavouritesView.swift index 13faf17..7d539f2 100644 --- a/FuelBoard/FavouritesView.swift +++ b/FuelBoard/FavouritesView.swift @@ -14,6 +14,7 @@ struct FavouritesView: View { let location: Coordinate? let distanceUnit: DistanceUnit let priceDisplayStyle: PriceDisplayStyle + let stationTapAction: StationTapAction var onToggleFavourite: (FuelStation, FuelType) -> Void = { _, _ in } /// Persists a reordered favourites array (after drag-and-drop). var onReorder: ([FavouriteEntry]) -> Void = { _ in } @@ -37,6 +38,9 @@ struct FavouritesView: View { /// 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) } @@ -65,6 +69,7 @@ struct FavouritesView: View { location: Coordinate?, distanceUnit: DistanceUnit, priceDisplayStyle: PriceDisplayStyle, + stationTapAction: StationTapAction, onToggleFavourite: @escaping (FuelStation, FuelType) -> Void = { _, _ in }, onReorder: @escaping ([FavouriteEntry]) -> Void = { _ in }, onHistoryUnavailable: (() -> Void)? = nil, @@ -74,6 +79,7 @@ struct FavouritesView: View { self.location = location self.distanceUnit = distanceUnit self.priceDisplayStyle = priceDisplayStyle + self.stationTapAction = stationTapAction self.onToggleFavourite = onToggleFavourite self.onReorder = onReorder self.onHistoryUnavailable = onHistoryUnavailable @@ -129,7 +135,9 @@ struct FavouritesView: View { baselinePrice: cheapestPrice, isTopResult: index == 0, isFavourite: activeFuelFavouriteIDs.contains(station.id), - onToggleFavourite: { onToggleFavourite(station, activeFuel) } + tapAction: stationTapAction, + onToggleFavourite: { onToggleFavourite(station, activeFuel) }, + onShowDetails: { detailStation = $0 } ) } .onMove(perform: moveFavourite) @@ -171,6 +179,13 @@ struct FavouritesView: View { onHistoryRecovered: onHistoryRecovered ) } + .sheet(item: $detailStation) { station in + StationDetailView( + station: station, + location: location, + distanceUnit: distanceUnit + ) + } .onAppear { #if DEBUG // QA hook: launch with `-showTrends` to open the sheet diff --git a/FuelBoard/SettingsView.swift b/FuelBoard/SettingsView.swift index 7fe3080..40bf899 100644 --- a/FuelBoard/SettingsView.swift +++ b/FuelBoard/SettingsView.swift @@ -18,6 +18,7 @@ struct SettingsView: View { @ObservedObject var tipStore: TipStore @Binding var distanceUnit: DistanceUnit @Binding var priceDisplayStyle: PriceDisplayStyle + @Binding var stationTapAction: StationTapAction @Binding var dataRefreshMode: DataRefreshMode /// The fuel + radius currently configured for alerts (mirrors the Alerts /// tab) so the test notification matches what real alerts will say. @@ -117,10 +118,19 @@ struct SettingsView: View { WidgetCenter.shared.reloadAllTimelines() WatchSyncManager.shared.pushSnapshot() } + Picker("Tap station", selection: $stationTapAction) { + ForEach(StationTapAction.allCases) { action in + Text(action.displayName).tag(action) + } + } + .pickerStyle(.segmented) + .onChange(of: stationTapAction) { _, newValue in + FuelStore.saveStationTapAction(newValue) + } } header: { Text("Units") } footer: { - Text("Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (129.9) or in pounds and pence (£1.29⁹/L).") + Text("Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (129.9) or in pounds and pence (£1.29⁹/L). Tapping a station either opens directions straight away or shows its details first.") } Section { diff --git a/FuelBoard/StationDetailView.swift b/FuelBoard/StationDetailView.swift new file mode 100644 index 0000000..0747866 --- /dev/null +++ b/FuelBoard/StationDetailView.swift @@ -0,0 +1,47 @@ +import SwiftUI +import UIKit + +/// Detail sheet for a tapped station (when Settings → Tap station = More info). +/// Shows the station name, address, distance from the current location, and a +/// Directions button that opens Apple Maps — the same destination a direct tap +/// would have opened. +struct StationDetailView: View { + let station: FuelStation + let location: Coordinate? + let distanceUnit: DistanceUnit + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationStack { + List { + Section { + LabeledContent("Name", value: station.name) + LabeledContent("Address", value: "\(station.address), \(station.postcode)") + if let location { + let km = FuelStore.displayDistanceKM( + station: station, userLat: location.lat, userLng: location.lng + ) + LabeledContent("Distance", value: distanceUnit.format(km)) + .monospacedDigit() + } + } + Section { + Button { + if let url = station.mapsDirectionsURL { + UIApplication.shared.open(url) + } + } label: { + Label("Directions", systemImage: "arrow.triangle.turn.up.right.diamond.fill") + } + } + } + .navigationTitle(station.name) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Done") { dismiss() } + } + } + } + } +} diff --git a/FuelBoard/StationsView.swift b/FuelBoard/StationsView.swift index 54ec67e..d51bc74 100644 --- a/FuelBoard/StationsView.swift +++ b/FuelBoard/StationsView.swift @@ -15,6 +15,7 @@ struct StationsView: View { let topStationID: String? let location: Coordinate? let favouriteIDs: Set + let stationTapAction: StationTapAction var onToggleFavourite: (FuelStation, FuelType) -> Void = { _, _ in } var onRefresh: () async -> Void = {} @@ -29,6 +30,9 @@ struct StationsView: View { /// info button in the header — keeps the list focused on stations. @State private var showKey = false + /// Station picked for the More-info sheet (tap action = showDetails). + @State private var detailStation: FuelStation? + /// Bottom-of-tab explainer (moved from the top 2026-08-16): mode/radius/ /// directions context + "N/TOTAL stations updated". The numerator is the /// current list pool; the denominator is the full UK station total from @@ -38,6 +42,9 @@ struct StationsView: View { let unit = distanceUnit.label(for: Double(miles)) let fuel = selectedFuel.displayName let mode = sortMode == .closest ? "Closest" : "Cheapest" + let tapHint = stationTapAction == .openMap + ? "tap a station for directions." + : "tap a station for details." let ratio: String if let total = FuelStore.loadStationCount() { ratio = "\(totalCount)/\(total)" @@ -48,9 +55,9 @@ struct StationsView: View { if sortMode == .closest { return "\(mode) \(fuel) stations — nearest first, best value within \(miles) \(unit) · \(ratio) stations updated" } - return "\(mode) \(fuel) within \(miles) \(unit) — \(ratio) stations updated · tap a station for directions." + return "\(mode) \(fuel) within \(miles) \(unit) — \(ratio) stations updated · \(tapHint)" } - return "\(mode) \(fuel) — \(ratio) stations updated · tap a station for directions." + return "\(mode) \(fuel) — \(ratio) stations updated · \(tapHint)" } var body: some View { @@ -126,7 +133,9 @@ struct StationsView: View { baselinePrice: baselinePrice, isTopResult: station.id == topStationID, isFavourite: favouriteIDs.contains(station.id), - onToggleFavourite: { onToggleFavourite(station, selectedFuel) } + tapAction: stationTapAction, + onToggleFavourite: { onToggleFavourite(station, selectedFuel) }, + onShowDetails: { detailStation = $0 } ) } @@ -260,6 +269,13 @@ struct StationsView: View { .presentationDetents([.fraction(0.6)]) .presentationBackground(Color(UIColor.systemGroupedBackground)) } + .sheet(item: $detailStation) { station in + StationDetailView( + station: station, + location: location, + distanceUnit: distanceUnit + ) + } } } diff --git a/FuelBoard/en.lproj/Localizable.strings b/FuelBoard/en.lproj/Localizable.strings index 0750d2b..1a321ce 100644 --- a/FuelBoard/en.lproj/Localizable.strings +++ b/FuelBoard/en.lproj/Localizable.strings @@ -56,6 +56,13 @@ "Units" = "Units"; "Price display" = "Price display"; "Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (129.9) or in pounds and pence (£1.29⁹/L)." = "Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (129.9) or in pounds and pence (£1.29⁹/L)."; +"Tap station" = "Tap station"; +"Open map" = "Open map"; +"More info" = "More info"; +"Name" = "Name"; +"Address" = "Address"; +"Distance" = "Distance"; +"Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (129.9) or in pounds and pence (£1.29⁹/L). Tapping a station either opens directions straight away or shows its details first." = "Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (129.9) or in pounds and pence (£1.29⁹/L). Tapping a station either opens directions straight away or shows its details first."; "Show introduction" = "Replay onboarding"; "Replay the welcome screen, including the location and notification permission prompts." = "Replay the welcome screen, including the location and notification permission prompts."; "Test alert notification (real data)" = "Test alert notification (real data)"; diff --git a/Shared/FuelStore.swift b/Shared/FuelStore.swift index 51ff958..c81ed27 100644 --- a/Shared/FuelStore.swift +++ b/Shared/FuelStore.swift @@ -208,6 +208,25 @@ enum PriceDisplayStyle: String, Codable, CaseIterable, Identifiable { } } +// MARK: - Station tap action + +/// What happens when a station row is tapped in the Stations/Favourites tabs: +/// open Apple Maps directions immediately (historic behaviour, default), or +/// show a detail sheet (name, address, distance, Directions button). +enum StationTapAction: String, Codable, CaseIterable, Identifiable { + case openMap // tap → Apple Maps directions straight away + case showDetails // tap → detail sheet, Directions button inside + + var id: String { rawValue } + + var displayName: String { + switch self { + case .openMap: return "Open map" + case .showDetails: return "More info" + } + } +} + // MARK: - Station model struct FuelStation: Identifiable, Codable, Equatable { @@ -542,6 +561,22 @@ struct FuelStore { saveString(style.rawValue, service: priceDisplayStyleKey) } + // MARK: Station tap action — open map vs detail sheet. Stored raw value; + // default open map preserves the historic tap behaviour for existing installs. + + static let stationTapActionKey = "fuelboard.stationTapAction" + + static func loadStationTapAction() -> StationTapAction { + if let raw = loadString(service: stationTapActionKey), let action = StationTapAction(rawValue: raw) { + return action + } + return .openMap + } + + static func saveStationTapAction(_ action: StationTapAction) { + saveString(action.rawValue, service: stationTapActionKey) + } + /// Superscript digit glyphs for the pounds & pence format's small raised /// third digit (the forecourt style: "£1.29⁹"). private static let superscriptDigits: [Character] = ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"]