Small widget favourites config: fuel dictates the favourite list (@IntentParameterDependency filters picker options by the chosen fuel), fuel picker shows only fuels with favourites (FavouriteFuel entity), Favourites sort hidden when no favourites exist (WidgetSort → dynamic AppEntity), fuel tags removed from favourite rows
This commit is contained in:
@@ -165,14 +165,15 @@ struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & Widg
|
|||||||
// fuel, no radius. Freshen its price when a focused fetch covers
|
// fuel, no radius. Freshen its price when a focused fetch covers
|
||||||
// it. The chosen favourite wins; when it's missing or stale (fresh
|
// it. The chosen favourite wins; when it's missing or stale (fresh
|
||||||
// widget, favourite removed, fuel row changed) fall back to the
|
// widget, favourite removed, fuel row changed) fall back to the
|
||||||
// Fuel row's cheapest favourite so the face is never blank.
|
// favourites fuel's cheapest favourite so the face is never blank.
|
||||||
if let chosen = configuration.favouriteChoice {
|
if let chosen = configuration.favouriteChoice {
|
||||||
|
let favouriteFuelType = configuration.favouriteFuelChoice?.fuel ?? fuel
|
||||||
let pinned = favourites.first { $0.id == chosen.id }
|
let pinned = favourites.first { $0.id == chosen.id }
|
||||||
?? favourites
|
?? favourites
|
||||||
.filter { $0.fuel == fuel && $0.station.prices[fuel] != nil }
|
.filter { $0.fuel == favouriteFuelType && $0.station.prices[favouriteFuelType] != nil }
|
||||||
.min { lhs, rhs in
|
.min { lhs, rhs in
|
||||||
(lhs.station.prices[fuel] ?? .infinity) <
|
(lhs.station.prices[favouriteFuelType] ?? .infinity) <
|
||||||
(rhs.station.prices[fuel] ?? .infinity)
|
(rhs.station.prices[favouriteFuelType] ?? .infinity)
|
||||||
}
|
}
|
||||||
if let pinned {
|
if let pinned {
|
||||||
var station = pinned.station
|
var station = pinned.station
|
||||||
|
|||||||
@@ -21,18 +21,48 @@ enum WidgetFuel: String, AppEnum, CaseIterable, Codable {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum WidgetSort: String, AppEnum, CaseIterable, Codable {
|
// Per-widget sort order. Modelled as an AppEntity (not an AppEnum) so the
|
||||||
case cheapest
|
// options can be dynamic: "Favourites" only appears when the user actually
|
||||||
case closest
|
// has favourites — an empty favourite list makes the option dead weight.
|
||||||
case favourites
|
struct WidgetSort: AppEntity, Identifiable, Hashable, Codable {
|
||||||
|
enum Kind: String, Codable {
|
||||||
|
case cheapest
|
||||||
|
case closest
|
||||||
|
case favourites
|
||||||
|
}
|
||||||
|
|
||||||
|
let id: String
|
||||||
|
|
||||||
|
var kind: Kind { Kind(rawValue: id) ?? .cheapest }
|
||||||
|
|
||||||
|
static let cheapest = WidgetSort(id: "cheapest")
|
||||||
|
static let closest = WidgetSort(id: "closest")
|
||||||
|
static let favourites = WidgetSort(id: "favourites")
|
||||||
|
|
||||||
|
var displayRepresentation: DisplayRepresentation {
|
||||||
|
switch kind {
|
||||||
|
case .cheapest: return DisplayRepresentation(stringLiteral: "Cheapest")
|
||||||
|
case .closest: return DisplayRepresentation(stringLiteral: "Closest")
|
||||||
|
case .favourites: return DisplayRepresentation(stringLiteral: "Favourites")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Sort by"
|
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Sort by"
|
||||||
|
static var defaultQuery = WidgetSortQuery()
|
||||||
|
}
|
||||||
|
|
||||||
static var caseDisplayRepresentations: [WidgetSort: DisplayRepresentation] = [
|
struct WidgetSortQuery: EntityQuery {
|
||||||
.cheapest: "Cheapest",
|
func entities(for identifiers: [String]) async throws -> [WidgetSort] {
|
||||||
.closest: "Closest",
|
identifiers.map { WidgetSort(id: $0) }
|
||||||
.favourites: "Favourites",
|
}
|
||||||
]
|
|
||||||
|
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
|
// 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
|
// A pinned favourite station, selectable on SMALL widgets (which show a single
|
||||||
// station). Reuses FavouriteEntry's id scheme ("fuel|stationID") so the
|
// station). Reuses FavouriteEntry's id scheme ("fuel|stationID") so the
|
||||||
// provider can resolve the choice straight back to a stored favourite. Only
|
// provider can resolve the choice straight back to a stored favourite. Only
|
||||||
// surfaced when Sort = Favourites.
|
// surfaced when Sort = Favourites.
|
||||||
//
|
//
|
||||||
// The display name is EMBEDDED in the entity (stationName + showsFuel): the
|
// The display name is EMBEDDED in the entity (stationName): the config sheet
|
||||||
// config sheet can resolve entities in a process where keychain/app-group
|
// can resolve entities in a process where keychain/app-group storage is
|
||||||
// storage is unavailable, and a storage-backed lookup there made every picker
|
// unavailable, and a storage-backed lookup there made every picker row fall
|
||||||
// row fall back to the "Favourite" placeholder. With the name carried on the
|
// back to the "Favourite" placeholder. With the name carried on the value,
|
||||||
// value, rows render with no storage read at all.
|
// rows render with no storage read at all.
|
||||||
struct WidgetFavourite: AppEntity, Identifiable, Hashable, Codable {
|
struct WidgetFavourite: AppEntity, Identifiable, Hashable, Codable {
|
||||||
let fuel: FuelType
|
let fuel: FuelType
|
||||||
let stationID: String
|
let stationID: String
|
||||||
/// Station display name, embedded so picker rows render storage-free.
|
/// Station display name, embedded so picker rows render storage-free.
|
||||||
let stationName: String
|
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 id: String { "\(fuel.rawValue)|\(stationID)" }
|
||||||
|
|
||||||
var displayRepresentation: DisplayRepresentation {
|
var displayRepresentation: DisplayRepresentation {
|
||||||
if !stationName.isEmpty {
|
if !stationName.isEmpty {
|
||||||
return DisplayRepresentation(
|
return DisplayRepresentation(stringLiteral: stationName)
|
||||||
stringLiteral: showsFuel ? "\(stationName) · \(fuel.displayName)" : stationName
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
// Id-only entity (e.g. a stored default): best-effort storage lookup.
|
// Id-only entity (e.g. a stored default): best-effort storage lookup.
|
||||||
let favourites = FuelStore.loadFavourites()
|
let favourites = FuelStore.loadFavourites()
|
||||||
guard let entry = favourites.first(where: { $0.id == id }) else {
|
guard let entry = favourites.first(where: { $0.id == id }) else {
|
||||||
return DisplayRepresentation(stringLiteral: "Favourite")
|
return DisplayRepresentation(stringLiteral: "Favourite")
|
||||||
}
|
}
|
||||||
let multiFuel = Set(favourites.map(\.fuel)).count > 1
|
return DisplayRepresentation(stringLiteral: entry.station.name)
|
||||||
return DisplayRepresentation(
|
|
||||||
stringLiteral: multiFuel ? "\(entry.station.name) · \(entry.fuel.displayName)" : entry.station.name
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Favourite"
|
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Favourite"
|
||||||
@@ -111,9 +161,16 @@ struct WidgetFavourite: AppEntity, Identifiable, Hashable, Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct WidgetFavouriteQuery: EntityQuery {
|
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<FuelBoardSmallWidgetConfigurationIntent>(
|
||||||
|
\.$favouriteFuel
|
||||||
|
)
|
||||||
|
var smallIntent
|
||||||
|
|
||||||
func entities(for identifiers: [String]) async throws -> [WidgetFavourite] {
|
func entities(for identifiers: [String]) async throws -> [WidgetFavourite] {
|
||||||
let favourites = FuelStore.loadFavourites()
|
let favourites = FuelStore.loadFavourites()
|
||||||
let multiFuel = Set(favourites.map(\.fuel)).count > 1
|
|
||||||
return identifiers.map { id in
|
return identifiers.map { id in
|
||||||
let parts = id.split(separator: "|", maxSplits: 1)
|
let parts = id.split(separator: "|", maxSplits: 1)
|
||||||
let fuel = FuelType(rawValue: String(parts.first ?? "")) ?? .e10
|
let fuel = FuelType(rawValue: String(parts.first ?? "")) ?? .e10
|
||||||
@@ -122,21 +179,24 @@ struct WidgetFavouriteQuery: EntityQuery {
|
|||||||
return WidgetFavourite(
|
return WidgetFavourite(
|
||||||
fuel: fuel,
|
fuel: fuel,
|
||||||
stationID: stationID,
|
stationID: stationID,
|
||||||
stationName: entry?.station.name ?? "",
|
stationName: entry?.station.name ?? ""
|
||||||
showsFuel: multiFuel
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func suggestedEntities() async throws -> [WidgetFavourite] {
|
func suggestedEntities() async throws -> [WidgetFavourite] {
|
||||||
let favourites = FuelStore.loadFavourites()
|
let favourites = FuelStore.loadFavourites()
|
||||||
let multiFuel = Set(favourites.map(\.fuel)).count > 1
|
let filtered: [FavouriteEntry]
|
||||||
return favourites.map { entry in
|
if let fuel = smallIntent?.favouriteFuel.fuel {
|
||||||
|
filtered = favourites.filter { $0.fuel == fuel }
|
||||||
|
} else {
|
||||||
|
filtered = favourites
|
||||||
|
}
|
||||||
|
return filtered.map { entry in
|
||||||
WidgetFavourite(
|
WidgetFavourite(
|
||||||
fuel: entry.fuel,
|
fuel: entry.fuel,
|
||||||
stationID: entry.station.id,
|
stationID: entry.station.id,
|
||||||
stationName: entry.station.name,
|
stationName: entry.station.name
|
||||||
showsFuel: multiFuel
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,6 +211,10 @@ protocol WidgetConfigValues {
|
|||||||
/// The pinned favourite (small widget, Favourites sort) — nil when the
|
/// The pinned favourite (small widget, Favourites sort) — nil when the
|
||||||
/// widget lists favourites instead (medium).
|
/// widget lists favourites instead (medium).
|
||||||
var favouriteChoice: WidgetFavourite? { get }
|
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 {
|
struct FuelBoardWidgetConfigurationIntent: WidgetConfigurationIntent, WidgetConfigValues {
|
||||||
@@ -167,8 +231,9 @@ struct FuelBoardWidgetConfigurationIntent: WidgetConfigurationIntent, WidgetConf
|
|||||||
var distance: WidgetDistance
|
var distance: WidgetDistance
|
||||||
|
|
||||||
// Medium/list widgets show ALL favourites for the chosen fuel — no
|
// 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 favouriteChoice: WidgetFavourite? { nil }
|
||||||
|
var favouriteFuelChoice: FavouriteFuel? { nil }
|
||||||
|
|
||||||
/// Edit-Widget UI: the Distance picker only makes sense for Cheapest
|
/// Edit-Widget UI: the Distance picker only makes sense for Cheapest
|
||||||
/// ordering — Closest is inherently "nearest within range" and Favourites
|
/// 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
|
// 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
|
// picker for Favourites sort (a small widget shows exactly ONE station, so the
|
||||||
// user chooses which favourite). Favourites mode keeps the Fuel row — it
|
// user chooses which favourite). Favourites mode swaps the Fuel row for a
|
||||||
// mirrors the app's Favourites tab: pick the fuel, then the favourite. The
|
// favourites-scoped one — only fuels with favourites are offered, and the
|
||||||
// pinned favourite wins when set; the Fuel row picks the default favourite on
|
// 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).
|
// a fresh widget. Distance stays hidden (a pinned station has no radius).
|
||||||
struct FuelBoardSmallWidgetConfigurationIntent: WidgetConfigurationIntent, WidgetConfigValues {
|
struct FuelBoardSmallWidgetConfigurationIntent: WidgetConfigurationIntent, WidgetConfigValues {
|
||||||
static var title: LocalizedStringResource = "Fuel & Sort"
|
static var title: LocalizedStringResource = "Fuel & Sort"
|
||||||
@@ -215,16 +281,20 @@ struct FuelBoardSmallWidgetConfigurationIntent: WidgetConfigurationIntent, Widge
|
|||||||
@Parameter(title: "Distance", default: WidgetDistance(id: 5))
|
@Parameter(title: "Distance", default: WidgetDistance(id: 5))
|
||||||
var distance: WidgetDistance
|
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 favourite: WidgetFavourite
|
||||||
|
|
||||||
var favouriteChoice: WidgetFavourite? { favourite }
|
var favouriteChoice: WidgetFavourite? { favourite }
|
||||||
|
var favouriteFuelChoice: FavouriteFuel? { favouriteFuel }
|
||||||
|
|
||||||
static var parameterSummary: some ParameterSummary {
|
static var parameterSummary: some ParameterSummary {
|
||||||
When(\.$sort, .equalTo, WidgetSort.favourites) {
|
When(\.$sort, .equalTo, WidgetSort.favourites) {
|
||||||
Summary("Show \(\.$fuel) favourite \(\.$favourite)") {
|
Summary("Show \(\.$favouriteFuel) favourite \(\.$favourite)") {
|
||||||
\.$sort
|
\.$sort
|
||||||
\.$fuel
|
\.$favouriteFuel
|
||||||
\.$favourite
|
\.$favourite
|
||||||
}
|
}
|
||||||
} otherwise: {
|
} otherwise: {
|
||||||
|
|||||||
Reference in New Issue
Block a user