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:
FuelBoard Contributor
2026-08-12 12:25:30 +01:00
parent 5ae0f945b3
commit 2a8de27571
2 changed files with 34 additions and 15 deletions
+8 -14
View File
@@ -77,9 +77,10 @@ struct FuelPriceTimelineProvider: AppIntentTimelineProvider {
}
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 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).
var location = await WidgetLocationFetcher.shared.currentLocation().map {
@@ -108,7 +109,6 @@ struct FuelPriceTimelineProvider: AppIntentTimelineProvider {
// shared cache, then to samples.
var stations: [FuelStation] = []
if let location {
let radiusKM = unitToKM()
if let fetched = await Self.fetchFocused(near: location, fuel: fuel, radiusKM: radiusKM) {
stations = fetched
}
@@ -116,7 +116,6 @@ struct FuelPriceTimelineProvider: AppIntentTimelineProvider {
if stations.isEmpty { stations = FuelStore.loadStations() }
if stations.isEmpty { stations = SampleFuelProvider.sampleStations }
let unit = FuelStore.loadDistanceUnit()
let radiusKM = unitToKM()
if let location {
// STRICT: cached data fetched around another location must never
// 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
/// 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.
@@ -230,12 +224,12 @@ struct FuelPriceWidgetView: View {
.font(.caption2)
.foregroundStyle(.secondary)
}
Text(station.brand)
Text(station.brand.sanitizedStationTitle)
.font(.headline)
.lineLimit(1)
if let price = station.prices[entry.fuel] {
Text(String(format: "%.1fp", price))
.font(.system(size: 28, weight: .bold))
Text(String(format: "£%.3f", price / 100))
.font(.system(size: 26, weight: .bold).monospaced())
.foregroundStyle(.green)
}
if let location = entry.location {
@@ -269,7 +263,7 @@ struct FuelPriceWidgetView: View {
ForEach(entry.stations.prefix(3)) { station in
Link(destination: station.mapsDirectionsURL ?? URL(string: "http://maps.apple.com")!) {
HStack(spacing: 8) {
Text(station.brand)
Text(station.brand.sanitizedStationTitle)
.font(.caption.weight(.semibold))
.lineLimit(1)
if let location = entry.location {
@@ -283,8 +277,8 @@ struct FuelPriceWidgetView: View {
Circle()
.fill(ragColor(for: price, cheapest: cheapest))
.frame(width: 6, height: 6)
Text(String(format: "%.1fp", price))
.font(.caption.weight(.bold))
Text(String(format: "£%.3f", price / 100))
.font(.caption.weight(.bold).monospaced())
.foregroundStyle(.primary)
}
}