Small widget: Favourites gains a picker to pin which favourite to show (separate FuelPriceWidgetSmall kind + AppEntity picker; medium keeps the list, no picker). Backlog: dual-unit widget picker labels
This commit is contained in:
@@ -3,8 +3,12 @@ import SwiftUI
|
||||
import AppIntents
|
||||
|
||||
// FuelBoard widget — petrol stations near you.
|
||||
// systemMedium: top 3-4 stations with price + distance, each row opens Maps
|
||||
// systemSmall: single station, whole widget opens Maps
|
||||
// FuelPriceWidget (medium): top 3 stations with price + distance, each row
|
||||
// opens Maps. Per-widget fuel + sort (+ distance for Cheapest).
|
||||
// FuelPriceSmallWidget (small): single station, whole widget opens Maps.
|
||||
// Same knobs, plus a Favourite picker when Sort = Favourites (a small face
|
||||
// shows ONE station, so you choose which favourite to pin; the medium face
|
||||
// lists all favourites and has no picker).
|
||||
// Taps deep-link to Apple Maps directions (maps://?daddr=). On the Home
|
||||
// Screen the system either opens Maps directly or delivers the URL to
|
||||
// FuelBoard, whose onOpenURL forwards it (and also still handles legacy
|
||||
@@ -39,15 +43,10 @@ struct FuelPriceWidget: Widget {
|
||||
// widget taps there can never launch Maps (Apple only allows a widget
|
||||
// to launch its own app in CarPlay, and only CarPlay-enabled apps).
|
||||
// Disfavored = read-only in the car, no dead interaction.
|
||||
baseConfiguration()
|
||||
.disfavoredLocations([.carPlay], for: [.systemSmall])
|
||||
}
|
||||
|
||||
private func baseConfiguration() -> some WidgetConfiguration {
|
||||
AppIntentConfiguration(
|
||||
kind: kind,
|
||||
intent: FuelBoardWidgetConfigurationIntent.self,
|
||||
provider: FuelPriceTimelineProvider()
|
||||
provider: FuelPriceTimelineProvider<FuelBoardWidgetConfigurationIntent>()
|
||||
) { entry in
|
||||
FuelPriceWidgetView(entry: entry)
|
||||
.containerBackground(for: .widget) {
|
||||
@@ -56,7 +55,33 @@ struct FuelPriceWidget: Widget {
|
||||
}
|
||||
.configurationDisplayName("FuelBoard Prices")
|
||||
.description("Fuel prices near you. Configure fuel + sort per widget.")
|
||||
.supportedFamilies([.systemSmall, .systemMedium])
|
||||
.supportedFamilies([.systemMedium])
|
||||
.disfavoredLocations([.carPlay], for: [.systemMedium])
|
||||
}
|
||||
}
|
||||
|
||||
// Small widget — exactly ONE station. Favourites sort gains a picker to choose
|
||||
// WHICH favourite to pin, because a small face can show only one (the medium
|
||||
// face lists them all, so the picker exists only here). Separate kind keeps
|
||||
// the extra parameter off the list widget's Edit-Widget sheet.
|
||||
struct FuelPriceSmallWidget: Widget {
|
||||
let kind = "FuelPriceWidgetSmall"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
AppIntentConfiguration(
|
||||
kind: kind,
|
||||
intent: FuelBoardSmallWidgetConfigurationIntent.self,
|
||||
provider: FuelPriceTimelineProvider<FuelBoardSmallWidgetConfigurationIntent>()
|
||||
) { entry in
|
||||
FuelPriceWidgetView(entry: entry)
|
||||
.containerBackground(for: .widget) {
|
||||
Color(.systemBackground)
|
||||
}
|
||||
}
|
||||
.configurationDisplayName("FuelBoard Favourite")
|
||||
.description("One pinned favourite station with its price.")
|
||||
.supportedFamilies([.systemSmall])
|
||||
.disfavoredLocations([.carPlay], for: [.systemSmall])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +96,9 @@ struct FuelPriceEntry: TimelineEntry {
|
||||
let unit: DistanceUnit // user's display unit for distances
|
||||
}
|
||||
|
||||
struct FuelPriceTimelineProvider: AppIntentTimelineProvider {
|
||||
struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & WidgetConfigValues>: AppIntentTimelineProvider {
|
||||
typealias Intent = Configuration
|
||||
|
||||
func placeholder(in context: Context) -> FuelPriceEntry {
|
||||
let unit = FuelStore.loadDistanceUnit()
|
||||
let sample = SampleFuelProvider.sampleStations
|
||||
@@ -84,12 +111,12 @@ struct FuelPriceTimelineProvider: AppIntentTimelineProvider {
|
||||
)
|
||||
}
|
||||
|
||||
func snapshot(for configuration: FuelBoardWidgetConfigurationIntent, in context: Context) async -> FuelPriceEntry {
|
||||
func snapshot(for configuration: Configuration, in context: Context) async -> FuelPriceEntry {
|
||||
await makeEntry(configuration: configuration)
|
||||
}
|
||||
|
||||
func timeline(
|
||||
for configuration: FuelBoardWidgetConfigurationIntent,
|
||||
for configuration: Configuration,
|
||||
in context: Context
|
||||
) async -> Timeline<FuelPriceEntry> {
|
||||
let entry = await makeEntry(configuration: configuration)
|
||||
@@ -99,7 +126,7 @@ struct FuelPriceTimelineProvider: AppIntentTimelineProvider {
|
||||
return Timeline(entries: [entry], policy: .after(nextRefresh))
|
||||
}
|
||||
|
||||
private func makeEntry(configuration: FuelBoardWidgetConfigurationIntent) async -> FuelPriceEntry {
|
||||
private func makeEntry(configuration: Configuration) async -> FuelPriceEntry {
|
||||
// Per-widget config: fuel + sort + distance come from THIS widget instance.
|
||||
let fuel = FuelType(rawValue: configuration.fuel.rawValue) ?? .e10
|
||||
let isFavourites = configuration.sort == .favourites
|
||||
@@ -131,20 +158,40 @@ struct FuelPriceTimelineProvider: AppIntentTimelineProvider {
|
||||
// 3) Load + order stations per widget mode.
|
||||
let ordered: [FuelStation]
|
||||
if isFavourites {
|
||||
// Favourites: pinned stations for THIS fuel, cheapest-first. Not
|
||||
// radius-bound — a favourite in Edinburgh shows on a widget in
|
||||
let favourites = FuelStore.loadFavourites()
|
||||
.filter { $0.station.prices[$0.fuel] != nil }
|
||||
|
||||
// Small widget, pinned favourite: exactly ONE station, its own
|
||||
// fuel, no radius. Freshen its price when a focused fetch covers it.
|
||||
if let chosen = configuration.favouriteChoice,
|
||||
let entry = favourites.first(where: { $0.id == chosen.id }) {
|
||||
var station = entry.station
|
||||
if let location,
|
||||
let fetched = await Self.fetchFocused(near: location, fuel: entry.fuel, radiusKM: radiusKM),
|
||||
let fresh = fetched.first(where: { $0.id == entry.station.id }) {
|
||||
station = fresh
|
||||
}
|
||||
return FuelPriceEntry(
|
||||
date: Date(), stations: [station],
|
||||
fuel: entry.fuel, sort: .cheapest, isFavourites: true,
|
||||
location: location, locationSource: source, unit: unit
|
||||
)
|
||||
}
|
||||
|
||||
// List mode (medium): pinned stations for THIS fuel, cheapest-first.
|
||||
// Not radius-bound — a favourite in Edinburgh shows on a widget in
|
||||
// London. Prices come from the keychain snapshot (the app
|
||||
// refreshes favourite prices into keychain after every fetch), or
|
||||
// fresher from a focused relay fetch when a favourite happens to
|
||||
// be in range.
|
||||
let favourites = FuelStore.loadFavourites()
|
||||
let fuelFavourites = favourites
|
||||
.filter { $0.fuel == fuel && $0.station.prices[fuel] != nil }
|
||||
.map(\.station)
|
||||
var refreshed: [FuelStation] = favourites
|
||||
var refreshed: [FuelStation] = fuelFavourites
|
||||
if let location,
|
||||
let fetched = await Self.fetchFocused(near: location, fuel: fuel, radiusKM: radiusKM) {
|
||||
let freshByID = Dictionary(uniqueKeysWithValues: fetched.map { ($0.id, $0) })
|
||||
refreshed = favourites.map { freshByID[$0.id] ?? $0 }
|
||||
refreshed = fuelFavourites.map { freshByID[$0.id] ?? $0 }
|
||||
}
|
||||
ordered = refreshed.sorted { lhs, rhs in
|
||||
let lPrice = lhs.prices[fuel]!
|
||||
|
||||
Reference in New Issue
Block a user