diff --git a/FuelBoard/ContentView.swift b/FuelBoard/ContentView.swift index e9be66e..26c12a3 100644 --- a/FuelBoard/ContentView.swift +++ b/FuelBoard/ContentView.swift @@ -235,6 +235,18 @@ struct ContentView: View { .sheet(item: $monitor.pendingStationMap) { request in StationMapView(request: request) } + .sheet(item: $monitor.pendingDetailStation) { station in + StationDetailView( + station: station, + location: location, + distanceUnit: distanceUnit + ) + } + .onReceive(NotificationCenter.default.publisher(for: .fuelBoardShowStationDetail)) { note in + guard let id = note.userInfo?["stationID"] as? String, + let station = stations.first(where: { $0.id == id }) else { return } + monitor.pendingDetailStation = station + } ) } diff --git a/FuelBoard/FuelBoardApp.swift b/FuelBoard/FuelBoardApp.swift index c8ed83d..67a4e25 100644 --- a/FuelBoard/FuelBoardApp.swift +++ b/FuelBoard/FuelBoardApp.swift @@ -69,9 +69,25 @@ struct FuelBoardApp: App { /// `http://maps.apple.com` form is also handled, from very old timelines. /// In CarPlay the widget uses the same `maps://` URL, which routes to /// Apple Maps without ever launching this app. + /// A `fuelboard://station?stationID=…` link (from a Live Activity tap when + /// Settings → Tap station = More info) posts a notification that + /// ContentView observes to present the detail sheet. private func handleOpenURL(_ url: URL) { - guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), - let items = components.queryItems + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) + else { return } + + if components.scheme == "fuelboard", + components.host == "station", + let id = components.queryItems?.first(where: { $0.name == "stationID" })?.value { + NotificationCenter.default.post( + name: .fuelBoardShowStationDetail, + object: nil, + userInfo: ["stationID": id] + ) + return + } + + guard let items = components.queryItems else { return } // Preferred: maps://?daddr=lat,lng&t=d (also covers http://maps.apple.com). diff --git a/FuelBoard/ProximityMonitor.swift b/FuelBoard/ProximityMonitor.swift index 39fa330..529dd95 100644 --- a/FuelBoard/ProximityMonitor.swift +++ b/FuelBoard/ProximityMonitor.swift @@ -84,6 +84,9 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca @Published var lastAlert: String? @Published private(set) var lastTestResult: String? @Published var pendingStationMap: StationMapRequest? + /// Station tapped via notification when Settings → Tap station = More info. + /// ContentView presents this as a StationDetailView sheet. + @Published var pendingDetailStation: FuelStation? /// Debug-only snapshot for the Settings → Debug section. Recomputed on /// every location/stations change via `refreshDebugStatus()`. @Published private(set) var debugStatus: DebugLocationStatus? @@ -791,9 +794,11 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca } } - /// Tapping a notification opens Apple Maps directions to the station from - /// the user's current location. If Apple Maps can't be opened (e.g. inside - /// LiveContainer), an in-app map sheet with directions is shown instead. + /// Tapping a notification honours Settings → Tap station: Open map opens + /// Apple Maps directions to the station from the user's current location; + /// More info opens the station's detail sheet in the app instead. If Apple + /// Maps can't be opened (e.g. inside LiveContainer), an in-app map sheet + /// with directions is shown instead. /// /// When the notification carried tie-choice action buttons, the tapped /// button's identifier ("tie_0", "tie_1", …) selects the corresponding @@ -818,9 +823,9 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca let tieName = ties[index]["name"] as? String, let tieLat = ties[index]["lat"] as? Double, let tieLng = ties[index]["lng"] as? Double { - openDirections(to: tieName, latitude: tieLat, longitude: tieLng) + routeNotificationTap(name: tieName, latitude: tieLat, longitude: tieLng) } else if !isTie, let name, let lat, let lng { - openDirections(to: name, latitude: lat, longitude: lng) + routeNotificationTap(name: name, latitude: lat, longitude: lng) } // Tie body tap: intentionally no directions — the user must pick // an option button to choose a station. @@ -828,6 +833,25 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca } } + /// Routes a resolved notification tap through the user's Tap station + /// preference: Open map → Apple Maps; More info → in-app detail sheet + /// (falling back to the map sheet when the station isn't in cache). + @MainActor + private func routeNotificationTap(name: String, latitude: Double, longitude: Double) { + guard FuelStore.loadStationTapAction() == .showDetails else { + openDirections(to: name, latitude: latitude, longitude: longitude) + return + } + if let station = stations.first(where: { $0.lat == latitude && $0.lng == longitude }) + ?? stations.first(where: { $0.name == name }) { + pendingDetailStation = station + } else { + // Station not in cache (e.g. data refreshed since the alert fired) + // — fall back to the map sheet so the tap still shows something. + pendingStationMap = StationMapRequest(name: name, latitude: latitude, longitude: longitude) + } + } + /// Opens Apple Maps with driving directions to the station; falls back to /// the in-app map sheet when the hand-off fails. @MainActor diff --git a/FuelBoard/StationDetailView.swift b/FuelBoard/StationDetailView.swift index 0747866..cd788e7 100644 --- a/FuelBoard/StationDetailView.swift +++ b/FuelBoard/StationDetailView.swift @@ -1,19 +1,37 @@ import SwiftUI +import MapKit 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. +/// Shows a small map preview, 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 + private var stationCoordinate: CLLocationCoordinate2D { + CLLocationCoordinate2D(latitude: station.lat, longitude: station.lng) + } + var body: some View { NavigationStack { List { + Section { + Map(position: .constant(.region(MKCoordinateRegion( + center: stationCoordinate, + span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02) + )))) { + Marker(station.name, coordinate: stationCoordinate) + .tint(.red) + } + .frame(height: 180) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .allowsHitTesting(false) + .listRowInsets(EdgeInsets()) + } Section { LabeledContent("Name", value: station.name) LabeledContent("Address", value: "\(station.address), \(station.postcode)") diff --git a/FuelBoard/WatchSyncManager.swift b/FuelBoard/WatchSyncManager.swift index 5053eb0..99b65bb 100644 --- a/FuelBoard/WatchSyncManager.swift +++ b/FuelBoard/WatchSyncManager.swift @@ -3,6 +3,7 @@ import WatchConnectivity extension Notification.Name { static let fuelBoardWatchRefreshRequested = Notification.Name("FuelBoardWatchRefreshRequested") + static let fuelBoardShowStationDetail = Notification.Name("FuelBoardShowStationDetail") } @MainActor diff --git a/FuelBoardWidgets/FuelBoardLiveActivityView.swift b/FuelBoardWidgets/FuelBoardLiveActivityView.swift index 6f98ae9..b13ca78 100644 --- a/FuelBoardWidgets/FuelBoardLiveActivityView.swift +++ b/FuelBoardWidgets/FuelBoardLiveActivityView.swift @@ -84,7 +84,7 @@ private struct FuelBoardLiveActivityView: View { @State private var slotWidth: CGFloat = 400 var body: some View { - Link(destination: context.state.mapsURL) { + Link(destination: tapDestination) { Group { // Branch on the ACTUAL proposed width. iPhone/iPad offer the // full Lock Screen width (>= threshold) → rich card, no matter @@ -210,9 +210,26 @@ private struct FuelBoardLiveActivityStationView: View { let context: ActivityViewContext var body: some View { - Text("\(context.state.stationName) · \(context.state.distanceText)") - .font(.caption) - .lineLimit(1) + Link(destination: FuelBoardLiveActivityTap.tapDestination(for: context.state)) { + Text("\(context.state.stationName) · \(context.state.distanceText)") + .font(.caption) + .lineLimit(1) + } + } +} + +/// Tap routing shared by the Lock Screen card and the island: honours +/// Settings → Tap station (Open map → Maps, More info → in-app detail). +enum FuelBoardLiveActivityTap { + static func tapDestination(for state: FuelBoardLiveActivityAttributes.ContentState) -> URL { + FuelStore.loadStationTapAction() == .showDetails ? state.detailURL : state.mapsURL + } +} + +private extension FuelBoardLiveActivityView { + /// Lock Screen tap honours Settings → Tap station. + var tapDestination: URL { + FuelBoardLiveActivityTap.tapDestination(for: context.state) } } @@ -235,4 +252,10 @@ extension FuelBoardLiveActivityAttributes.ContentState { var mapsURL: URL { URL(string: "maps://?daddr=\(lat),\(lng)&t=d")! } + + /// In-app deep link to the station's detail sheet, used when + /// Settings → Tap station = More info. + var detailURL: URL { + URL(string: "fuelboard://station?stationID=\(stationID)")! + } } \ No newline at end of file