import SwiftUI import CoreLocation import WidgetKit struct ContentView: View { @Environment(\.scenePhase) private var scenePhase @State private var stations: [FuelStation] = [] @State private var selectedFuel: FuelType = FuelStore.loadSelectedFuel() @State private var location: Coordinate? = { if let loc = FuelStore.loadLocation() { return Coordinate(lat: loc.lat, lng: loc.lng) } return nil }() @State private var isLoading = false @State private var statusMessage = "" @State private var locationManager = LocationManager() private var sortedStations: [FuelStation] { stations .filter { $0.prices[selectedFuel] != nil } .sorted { lhs, rhs in // Cheapest first; distance only breaks ties. let lPrice = lhs.prices[selectedFuel]! let rPrice = rhs.prices[selectedFuel]! if lPrice != rPrice { return lPrice < rPrice } guard let location else { return false } return lhs.distanceKM(to: location.lat, lng2: location.lng) < rhs.distanceKM(to: location.lat, lng2: location.lng) } } var body: some View { NavigationStack { List { Section { Text("Cheapest \(selectedFuel.displayName) near you. Tap a station for directions.") .font(.footnote) .foregroundStyle(.secondary) } Section("Fuel type") { Picker("Fuel type", selection: $selectedFuel) { ForEach(FuelType.allCases) { fuel in Text(fuel.displayName).tag(fuel) } } .pickerStyle(.segmented) .onChange(of: selectedFuel) { _, newValue in FuelStore.saveSelectedFuel(newValue) WidgetCenter.shared.reloadAllTimelines() } } Section("Stations") { if isLoading { HStack(spacing: 10) { ProgressView() Text("Fetching prices…") } } else if sortedStations.isEmpty { Text("No \(selectedFuel.displayName) stations found.") .foregroundStyle(.secondary) } else { ForEach(sortedStations) { station in StationRow(station: station, fuel: selectedFuel, location: location) } } } Section("Location") { if let location { Text("Using location \(String(format: "%.4f, %.4f", location.lat, location.lng))") .font(.caption) .foregroundStyle(.secondary) } else { Text("No location yet — allow location access to sort by distance.") .font(.caption) .foregroundStyle(.secondary) } Button("Update my location") { locationManager.requestUpdate() } .font(.footnote) } if !statusMessage.isEmpty { Section("Status") { Text(statusMessage) .font(.caption2) .monospaced() .textSelection(.enabled) } } } .navigationTitle("FuelBoard") .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { Task { await refresh() } } label: { Image(systemName: "arrow.clockwise") } .disabled(isLoading) } } .onAppear { // Auto-request location on first launch so the widget gets a // fresh fix without the user hunting for the button. if FuelStore.loadLocation() == nil { locationManager.requestUpdate() } Task { await refresh() } } .onChange(of: scenePhase) { _, newPhase in if newPhase == .active { Task { await refresh() } } } .onChange(of: locationManager.current) { _, newLocation in if let newLocation { location = newLocation FuelStore.saveLocation(lat: newLocation.lat, lng: newLocation.lng) WidgetCenter.shared.reloadAllTimelines() Task { await refresh() } } } } } private func refresh() async { isLoading = true defer { isLoading = false } do { let fetched = try await FuelPriceProvider.active.fetchStations(near: location?.lat, lng: location?.lng, fuel: selectedFuel) stations = fetched FuelStore.saveStations(fetched) WidgetCenter.shared.reloadAllTimelines() statusMessage = "Loaded \(fetched.count) stations · \(Date().formatted(date: .omitted, time: .shortened))" } catch { statusMessage = "Live fetch failed: \(error.localizedDescription). Showing cached/sample data." stations = FuelStore.loadStations().isEmpty ? SampleFuelProvider.sampleStations : FuelStore.loadStations() } } } struct StationRow: View { let station: FuelStation let fuel: FuelType let location: Coordinate? var body: some View { HStack(spacing: 12) { VStack(alignment: .leading, spacing: 2) { Text(station.name) .font(.headline) Text("\(station.address), \(station.postcode)") .font(.caption) .foregroundStyle(.secondary) if let location { Text(String(format: "%.1f km away", station.distanceKM(to: location.lat, lng2: location.lng))) .font(.caption2) .foregroundStyle(.secondary) } } Spacer() if let price = station.prices[fuel] { Text(String(format: "%.1fp", price)) .font(.title3.bold()) .foregroundStyle(.green) } Image(systemName: "arrow.triangle.turn.up.right.circle") .foregroundStyle(.secondary) } .contentShape(Rectangle()) .onTapGesture { if let url = station.mapsDirectionsURL { UIApplication.shared.open(url) } } } } // MARK: - Location manager @MainActor final class LocationManager: NSObject, ObservableObject, @preconcurrency CLLocationManagerDelegate { @Published var current: Coordinate? private let manager = CLLocationManager() override init() { super.init() manager.delegate = self manager.desiredAccuracy = kCLLocationAccuracyHundredMeters } func requestUpdate() { switch manager.authorizationStatus { case .notDetermined: manager.requestWhenInUseAuthorization() manager.requestLocation() case .authorizedWhenInUse, .authorizedAlways: manager.requestLocation() default: break } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let loc = locations.last else { return } current = Coordinate(lat: loc.coordinate.latitude, lng: loc.coordinate.longitude) } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { // Ignore — the app still works price-sorted without location. } }