Widget: per-widget distance, sanitised names, £ prices
- New Distance picker in Edit Widget (5/10/15 miles), independent of the app's distance filter; used for both the relay fetch radius and the strict in-widget filter. - Brand text is now title-cased via sanitizedStationTitle (relay sends all-caps brands like SHELL/TESCO/MORRISONS). - Prices now show as £1.399 (monospaced) matching the app, instead of 139.9p. - Directions deep-link (mapsDirectionsURL) already per-row on medium and whole-widget on small; unchanged. 35 tests pass.
This commit is contained in:
@@ -77,9 +77,10 @@ struct FuelPriceTimelineProvider: AppIntentTimelineProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func makeEntry(configuration: FuelBoardWidgetConfigurationIntent) async -> FuelPriceEntry {
|
private func makeEntry(configuration: FuelBoardWidgetConfigurationIntent) async -> FuelPriceEntry {
|
||||||
// Per-widget config: fuel + sort come from THIS widget instance.
|
// Per-widget config: fuel + sort + distance come from THIS widget instance.
|
||||||
let fuel = FuelType(rawValue: configuration.fuel.rawValue) ?? .e10
|
let fuel = FuelType(rawValue: configuration.fuel.rawValue) ?? .e10
|
||||||
let sort: SortMode = configuration.sort == .closest ? .closest : .cheapest
|
let sort: SortMode = configuration.sort == .closest ? .closest : .cheapest
|
||||||
|
let radiusKM = Double(configuration.distance.miles) * 1.60934
|
||||||
|
|
||||||
// 1) Try a fresh location fix (bounded to a few seconds).
|
// 1) Try a fresh location fix (bounded to a few seconds).
|
||||||
var location = await WidgetLocationFetcher.shared.currentLocation().map {
|
var location = await WidgetLocationFetcher.shared.currentLocation().map {
|
||||||
@@ -108,7 +109,6 @@ struct FuelPriceTimelineProvider: AppIntentTimelineProvider {
|
|||||||
// shared cache, then to samples.
|
// shared cache, then to samples.
|
||||||
var stations: [FuelStation] = []
|
var stations: [FuelStation] = []
|
||||||
if let location {
|
if let location {
|
||||||
let radiusKM = unitToKM()
|
|
||||||
if let fetched = await Self.fetchFocused(near: location, fuel: fuel, radiusKM: radiusKM) {
|
if let fetched = await Self.fetchFocused(near: location, fuel: fuel, radiusKM: radiusKM) {
|
||||||
stations = fetched
|
stations = fetched
|
||||||
}
|
}
|
||||||
@@ -116,7 +116,6 @@ struct FuelPriceTimelineProvider: AppIntentTimelineProvider {
|
|||||||
if stations.isEmpty { stations = FuelStore.loadStations() }
|
if stations.isEmpty { stations = FuelStore.loadStations() }
|
||||||
if stations.isEmpty { stations = SampleFuelProvider.sampleStations }
|
if stations.isEmpty { stations = SampleFuelProvider.sampleStations }
|
||||||
let unit = FuelStore.loadDistanceUnit()
|
let unit = FuelStore.loadDistanceUnit()
|
||||||
let radiusKM = unitToKM()
|
|
||||||
if let location {
|
if let location {
|
||||||
// STRICT: cached data fetched around another location must never
|
// STRICT: cached data fetched around another location must never
|
||||||
// leak out-of-radius stations into the widget.
|
// leak out-of-radius stations into the widget.
|
||||||
@@ -159,11 +158,6 @@ struct FuelPriceTimelineProvider: AppIntentTimelineProvider {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The search radius in km for the widget's own focused relay fetch.
|
|
||||||
private func unitToKM() -> Double {
|
|
||||||
FuelStore.loadDistanceUnit().toKM(Double(FuelStore.loadStationLimit()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One focused fetch from the relay around the widget's location — small
|
/// One focused fetch from the relay around the widget's location — small
|
||||||
/// response (radius + limit 500), bounded so a dead relay can't stall the
|
/// response (radius + limit 500), bounded so a dead relay can't stall the
|
||||||
/// timeline. Returns nil on any failure so callers fall back to cache.
|
/// timeline. Returns nil on any failure so callers fall back to cache.
|
||||||
@@ -230,12 +224,12 @@ struct FuelPriceWidgetView: View {
|
|||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
Text(station.brand)
|
Text(station.brand.sanitizedStationTitle)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
if let price = station.prices[entry.fuel] {
|
if let price = station.prices[entry.fuel] {
|
||||||
Text(String(format: "%.1fp", price))
|
Text(String(format: "£%.3f", price / 100))
|
||||||
.font(.system(size: 28, weight: .bold))
|
.font(.system(size: 26, weight: .bold).monospaced())
|
||||||
.foregroundStyle(.green)
|
.foregroundStyle(.green)
|
||||||
}
|
}
|
||||||
if let location = entry.location {
|
if let location = entry.location {
|
||||||
@@ -269,7 +263,7 @@ struct FuelPriceWidgetView: View {
|
|||||||
ForEach(entry.stations.prefix(3)) { station in
|
ForEach(entry.stations.prefix(3)) { station in
|
||||||
Link(destination: station.mapsDirectionsURL ?? URL(string: "http://maps.apple.com")!) {
|
Link(destination: station.mapsDirectionsURL ?? URL(string: "http://maps.apple.com")!) {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Text(station.brand)
|
Text(station.brand.sanitizedStationTitle)
|
||||||
.font(.caption.weight(.semibold))
|
.font(.caption.weight(.semibold))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
if let location = entry.location {
|
if let location = entry.location {
|
||||||
@@ -283,8 +277,8 @@ struct FuelPriceWidgetView: View {
|
|||||||
Circle()
|
Circle()
|
||||||
.fill(ragColor(for: price, cheapest: cheapest))
|
.fill(ragColor(for: price, cheapest: cheapest))
|
||||||
.frame(width: 6, height: 6)
|
.frame(width: 6, height: 6)
|
||||||
Text(String(format: "%.1fp", price))
|
Text(String(format: "£%.3f", price / 100))
|
||||||
.font(.caption.weight(.bold))
|
.font(.caption.weight(.bold).monospaced())
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(.primary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,13 +33,38 @@ enum WidgetSort: String, AppEnum, CaseIterable, Codable {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum WidgetDistance: String, AppEnum, CaseIterable, Codable {
|
||||||
|
case five
|
||||||
|
case ten
|
||||||
|
case fifteen
|
||||||
|
|
||||||
|
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Distance"
|
||||||
|
|
||||||
|
static var caseDisplayRepresentations: [WidgetDistance: DisplayRepresentation] = [
|
||||||
|
.five: "5 miles",
|
||||||
|
.ten: "10 miles",
|
||||||
|
.fifteen: "15 miles",
|
||||||
|
]
|
||||||
|
|
||||||
|
var miles: Int {
|
||||||
|
switch self {
|
||||||
|
case .five: return 5
|
||||||
|
case .ten: return 10
|
||||||
|
case .fifteen: return 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct FuelBoardWidgetConfigurationIntent: WidgetConfigurationIntent {
|
struct FuelBoardWidgetConfigurationIntent: WidgetConfigurationIntent {
|
||||||
static var title: LocalizedStringResource = "Fuel & Sort"
|
static var title: LocalizedStringResource = "Fuel & Sort"
|
||||||
static var description = IntentDescription("Which fuel and ordering this widget shows.")
|
static var description = IntentDescription("Which fuel, ordering and search radius this widget shows.")
|
||||||
|
|
||||||
@Parameter(title: "Fuel", default: .e10)
|
@Parameter(title: "Fuel", default: .e10)
|
||||||
var fuel: WidgetFuel
|
var fuel: WidgetFuel
|
||||||
|
|
||||||
@Parameter(title: "Sort by", default: .cheapest)
|
@Parameter(title: "Sort by", default: .cheapest)
|
||||||
var sort: WidgetSort
|
var sort: WidgetSort
|
||||||
|
|
||||||
|
@Parameter(title: "Distance", default: .five)
|
||||||
|
var distance: WidgetDistance
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user