import WidgetKit import SwiftUI // FuelBoard widget — cheapest petrol stations near you. // systemMedium: top 4 stations with price + distance, each row opens Maps // systemSmall: single cheapest station, whole widget opens Maps // Taps deep-link to Apple Maps directions (http://maps.apple.com/?daddr=). // Note: on the home screen Link opens Maps directly. Inside CarPlay the widget // renders but can't launch Maps (widgets can only launch their own CarPlay app). struct FuelPriceWidget: Widget { let kind = "FuelPriceWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: FuelPriceProvider2()) { entry in FuelPriceWidgetView(entry: entry) .containerBackground(for: .widget) { Color(.systemBackground) } } .configurationDisplayName("FuelBoard Prices") .description("The cheapest fuel near you. Tap a station for directions.") .supportedFamilies([.systemSmall, .systemMedium]) } } struct FuelPriceEntry: TimelineEntry { let date: Date let stations: [FuelStation] // already filtered + sorted for the selected fuel let fuel: FuelType let location: (lat: Double, lng: Double)? } struct FuelPriceProvider2: TimelineProvider { func placeholder(in context: Context) -> FuelPriceEntry { let fuel = FuelStore.loadSelectedFuel() let sample = SampleFuelProvider.sampleStations .filter { $0.prices[fuel] != nil } .sorted { $0.prices[fuel]! < $1.prices[fuel]! } return FuelPriceEntry(date: Date(), stations: Array(sample.prefix(4)), fuel: fuel, location: nil) } func getSnapshot(in context: Context, completion: @escaping (FuelPriceEntry) -> Void) { completion(makeEntry()) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { let entry = makeEntry() let nextRefresh = Calendar.current.date(byAdding: .minute, value: 15, to: Date())! completion(Timeline(entries: [entry], policy: .after(nextRefresh))) } private func makeEntry() -> FuelPriceEntry { let fuel = FuelStore.loadSelectedFuel() let location = FuelStore.loadLocation().map { (lat: $0.lat, lng: $0.lng) } var stations = FuelStore.loadStations() if stations.isEmpty { stations = SampleFuelProvider.sampleStations } let filtered = stations.filter { $0.prices[fuel] != nil } let sorted: [FuelStation] if let location { sorted = filtered.sorted { lhs, rhs in let lScore = lhs.distanceKM(to: location.lat, lng2: location.lng) * 0.4 + lhs.prices[fuel]! * 0.001 let rScore = rhs.distanceKM(to: location.lat, lng2: location.lng) * 0.4 + rhs.prices[fuel]! * 0.001 return lScore < rScore } } else { sorted = filtered.sorted { $0.prices[fuel]! < $1.prices[fuel]! } } return FuelPriceEntry(date: Date(), stations: Array(sorted.prefix(4)), fuel: fuel, location: location) } } struct FuelPriceWidgetView: View { let entry: FuelPriceEntry var body: some View { Group { if entry.stations.isEmpty { VStack(spacing: 6) { Image(systemName: "fuelpump") .font(.title2) .foregroundStyle(.secondary) Text("No \(entry.fuel.displayName) stations") .font(.caption2) .foregroundStyle(.secondary) } } else if entry.stations.count == 1 || UIDevice.current.userInterfaceIdiom == .pad { singleCheapest } else { stationList } } } private var singleCheapest: some View { let station = entry.stations[0] return VStack(alignment: .leading, spacing: 6) { HStack { Image(systemName: "fuelpump.fill") .foregroundStyle(.green) Text("Cheapest \(entry.fuel.displayName)") .font(.caption2) .foregroundStyle(.secondary) } Text(station.brand) .font(.headline) .lineLimit(1) if let price = station.prices[entry.fuel] { Text(String(format: "%.1fp", price)) .font(.system(size: 28, weight: .bold)) .foregroundStyle(.green) } if let location = entry.location { Text(String(format: "%.1f km away", station.distanceKM(to: location.lat, lng2: location.lng))) .font(.caption2) .foregroundStyle(.secondary) } Text("Tap for directions") .font(.caption2) .foregroundStyle(.secondary) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .widgetURL(station.mapsDirectionsURL) } private var stationList: some View { VStack(alignment: .leading, spacing: 6) { HStack { Image(systemName: "fuelpump.fill") .foregroundStyle(.green) Text("Cheapest \(entry.fuel.displayName)") .font(.caption2) .foregroundStyle(.secondary) Spacer() Text("Tap → Maps") .font(.caption2) .foregroundStyle(.secondary) } ForEach(entry.stations.prefix(3)) { station in Link(destination: station.mapsDirectionsURL ?? URL(string: "http://maps.apple.com")!) { HStack(spacing: 8) { Text(station.brand) .font(.caption.weight(.semibold)) .lineLimit(1) if let location = entry.location { Text(String(format: "%.1f km", station.distanceKM(to: location.lat, lng2: location.lng))) .font(.caption2) .foregroundStyle(.secondary) } Spacer() if let price = station.prices[entry.fuel] { Text(String(format: "%.1fp", price)) .font(.caption.weight(.bold)) .foregroundStyle(.green) } } } .buttonStyle(.plain) } Spacer(minLength: 0) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } }