diff --git a/FuelBoardWidgets/FuelPriceWidget.swift b/FuelBoardWidgets/FuelPriceWidget.swift index b471c9f..90eeb45 100644 --- a/FuelBoardWidgets/FuelPriceWidget.swift +++ b/FuelBoardWidgets/FuelPriceWidget.swift @@ -165,14 +165,15 @@ struct FuelPriceTimelineProvider [WidgetSort] { + identifiers.map { WidgetSort(id: $0) } + } + + func suggestedEntities() async throws -> [WidgetSort] { + var sorts = [WidgetSort.cheapest, WidgetSort.closest] + if !FuelStore.loadFavourites().isEmpty { + sorts.append(.favourites) + } + return sorts + } } // Per-widget distance filter. Modelled as an AppEntity (not an AppEnum) so @@ -68,42 +98,62 @@ struct WidgetDistanceQuery: EntityQuery { } } +// Fuel for the favourites face (small widget, Favourites sort). Modelled as an +// AppEntity so only fuels that ACTUALLY have favourites appear in the picker — +// mirrors the app's Favourites tab capsules. The chosen fuel dictates which +// favourites the Favourite picker offers (see WidgetFavouriteQuery). +struct FavouriteFuel: AppEntity, Identifiable, Hashable, Codable { + let fuel: FuelType + + var id: String { fuel.rawValue } + + var displayRepresentation: DisplayRepresentation { + DisplayRepresentation(stringLiteral: fuel.displayName) + } + + static var typeDisplayRepresentation: TypeDisplayRepresentation = "Fuel" + static var defaultQuery = FavouriteFuelQuery() +} + +struct FavouriteFuelQuery: EntityQuery { + func entities(for identifiers: [String]) async throws -> [FavouriteFuel] { + identifiers.compactMap { FuelType(rawValue: $0) }.map { FavouriteFuel(fuel: $0) } + } + + func suggestedEntities() async throws -> [FavouriteFuel] { + let fuels = Set(FuelStore.loadFavourites().map(\.fuel)) + return FuelType.allCases.filter { fuels.contains($0) }.map { FavouriteFuel(fuel: $0) } + } +} + // A pinned favourite station, selectable on SMALL widgets (which show a single // station). Reuses FavouriteEntry's id scheme ("fuel|stationID") so the // provider can resolve the choice straight back to a stored favourite. Only // surfaced when Sort = Favourites. // -// The display name is EMBEDDED in the entity (stationName + showsFuel): the -// config sheet can resolve entities in a process where keychain/app-group -// storage is unavailable, and a storage-backed lookup there made every picker -// row fall back to the "Favourite" placeholder. With the name carried on the -// value, rows render with no storage read at all. +// The display name is EMBEDDED in the entity (stationName): the config sheet +// can resolve entities in a process where keychain/app-group storage is +// unavailable, and a storage-backed lookup there made every picker row fall +// back to the "Favourite" placeholder. With the name carried on the value, +// rows render with no storage read at all. struct WidgetFavourite: AppEntity, Identifiable, Hashable, Codable { let fuel: FuelType let stationID: String /// Station display name, embedded so picker rows render storage-free. let stationName: String - /// True when favourites span more than one fuel type — embedded so the - /// fuel tag renders storage-free too. - let showsFuel: Bool var id: String { "\(fuel.rawValue)|\(stationID)" } var displayRepresentation: DisplayRepresentation { if !stationName.isEmpty { - return DisplayRepresentation( - stringLiteral: showsFuel ? "\(stationName) · \(fuel.displayName)" : stationName - ) + return DisplayRepresentation(stringLiteral: stationName) } // Id-only entity (e.g. a stored default): best-effort storage lookup. let favourites = FuelStore.loadFavourites() guard let entry = favourites.first(where: { $0.id == id }) else { return DisplayRepresentation(stringLiteral: "Favourite") } - let multiFuel = Set(favourites.map(\.fuel)).count > 1 - return DisplayRepresentation( - stringLiteral: multiFuel ? "\(entry.station.name) · \(entry.fuel.displayName)" : entry.station.name - ) + return DisplayRepresentation(stringLiteral: entry.station.name) } static var typeDisplayRepresentation: TypeDisplayRepresentation = "Favourite" @@ -111,9 +161,16 @@ struct WidgetFavourite: AppEntity, Identifiable, Hashable, Codable { } struct WidgetFavouriteQuery: EntityQuery { + /// Reads the small intent's Favourite Fuel parameter: the Favourite picker + /// only offers THAT fuel's favourites (requirement: the chosen fuel + /// dictates the list). Nil (medium widget / fresh sheet) → all favourites. + @IntentParameterDependency( + \.$favouriteFuel + ) + var smallIntent + func entities(for identifiers: [String]) async throws -> [WidgetFavourite] { let favourites = FuelStore.loadFavourites() - let multiFuel = Set(favourites.map(\.fuel)).count > 1 return identifiers.map { id in let parts = id.split(separator: "|", maxSplits: 1) let fuel = FuelType(rawValue: String(parts.first ?? "")) ?? .e10 @@ -122,21 +179,24 @@ struct WidgetFavouriteQuery: EntityQuery { return WidgetFavourite( fuel: fuel, stationID: stationID, - stationName: entry?.station.name ?? "", - showsFuel: multiFuel + stationName: entry?.station.name ?? "" ) } } func suggestedEntities() async throws -> [WidgetFavourite] { let favourites = FuelStore.loadFavourites() - let multiFuel = Set(favourites.map(\.fuel)).count > 1 - return favourites.map { entry in + let filtered: [FavouriteEntry] + if let fuel = smallIntent?.favouriteFuel.fuel { + filtered = favourites.filter { $0.fuel == fuel } + } else { + filtered = favourites + } + return filtered.map { entry in WidgetFavourite( fuel: entry.fuel, stationID: entry.station.id, - stationName: entry.station.name, - showsFuel: multiFuel + stationName: entry.station.name ) } } @@ -151,6 +211,10 @@ protocol WidgetConfigValues { /// The pinned favourite (small widget, Favourites sort) — nil when the /// widget lists favourites instead (medium). var favouriteChoice: WidgetFavourite? { get } + /// The favourites-face fuel (small widget, Favourites sort) — its options + /// are the fuels that have favourites, and it dictates which favourites + /// the Favourite picker offers. Nil on the medium widget. + var favouriteFuelChoice: FavouriteFuel? { get } } struct FuelBoardWidgetConfigurationIntent: WidgetConfigurationIntent, WidgetConfigValues { @@ -167,8 +231,9 @@ struct FuelBoardWidgetConfigurationIntent: WidgetConfigurationIntent, WidgetConf var distance: WidgetDistance // Medium/list widgets show ALL favourites for the chosen fuel — no - // pinned-favourite knob. + // pinned-favourite knob, no favourites-scoped fuel. var favouriteChoice: WidgetFavourite? { nil } + var favouriteFuelChoice: FavouriteFuel? { nil } /// Edit-Widget UI: the Distance picker only makes sense for Cheapest /// ordering — Closest is inherently "nearest within range" and Favourites @@ -198,9 +263,10 @@ struct FuelBoardWidgetConfigurationIntent: WidgetConfigurationIntent, WidgetConf // SMALL widget intent: same knobs as the list widget, PLUS a pinned-favourite // picker for Favourites sort (a small widget shows exactly ONE station, so the -// user chooses which favourite). Favourites mode keeps the Fuel row — it -// mirrors the app's Favourites tab: pick the fuel, then the favourite. The -// pinned favourite wins when set; the Fuel row picks the default favourite on +// user chooses which favourite). Favourites mode swaps the Fuel row for a +// favourites-scoped one — only fuels with favourites are offered, and the +// chosen fuel dictates which favourites the Favourite picker lists. The +// pinned favourite wins when set; the fuel row picks the default favourite on // a fresh widget. Distance stays hidden (a pinned station has no radius). struct FuelBoardSmallWidgetConfigurationIntent: WidgetConfigurationIntent, WidgetConfigValues { static var title: LocalizedStringResource = "Fuel & Sort" @@ -215,16 +281,20 @@ struct FuelBoardSmallWidgetConfigurationIntent: WidgetConfigurationIntent, Widge @Parameter(title: "Distance", default: WidgetDistance(id: 5)) var distance: WidgetDistance - @Parameter(title: "Favourite", default: WidgetFavourite(fuel: .e10, stationID: "", stationName: "", showsFuel: false)) + @Parameter(title: "Favourite Fuel", default: FavouriteFuel(fuel: .e10)) + var favouriteFuel: FavouriteFuel + + @Parameter(title: "Favourite", default: WidgetFavourite(fuel: .e10, stationID: "", stationName: "")) var favourite: WidgetFavourite var favouriteChoice: WidgetFavourite? { favourite } + var favouriteFuelChoice: FavouriteFuel? { favouriteFuel } static var parameterSummary: some ParameterSummary { When(\.$sort, .equalTo, WidgetSort.favourites) { - Summary("Show \(\.$fuel) favourite \(\.$favourite)") { + Summary("Show \(\.$favouriteFuel) favourite \(\.$favourite)") { \.$sort - \.$fuel + \.$favouriteFuel \.$favourite } } otherwise: {