Add Settings tab: distance units toggle, onboarding replay, £4.99 tip; drop E10/E5 from labels

- FuelType labels: 'Unleaded (E10)'/'Premium (E5)' -> 'Unleaded'/'Premium'
- New DistanceUnit (miles/km) preference; all app + widget distances,
  radii and alert text convert/display in the chosen unit
- Settings tab (4th tab): segmented miles/km picker, 'Show introduction'
  replay button (onboarding moved out of Alerts tab), StoreKit tip £4.99
- ProximityMonitor + widget notifications use the chosen unit
- Tests: label expectations updated, DistanceUnit conversion + format tests
This commit is contained in:
FuelBoard Contributor
2026-08-12 07:46:08 +01:00
parent fb79cf0788
commit 44c6c38536
10 changed files with 284 additions and 47 deletions
+9 -6
View File
@@ -36,15 +36,17 @@ struct FuelPriceEntry: TimelineEntry {
let fuel: FuelType
let location: Coordinate?
let locationSource: String // "live" | "cached" | "none"
let unit: DistanceUnit // user's display unit for distances
}
struct FuelPriceTimelineProvider: TimelineProvider {
func placeholder(in context: Context) -> FuelPriceEntry {
let fuel = FuelStore.loadSelectedFuel()
let unit = FuelStore.loadDistanceUnit()
let sample = SampleFuelProvider.sampleStations
.filter { $0.prices[fuel] != nil }
.sorted { $0.prices[fuel]! < $1.prices[fuel]! }
return FuelPriceEntry(date: Date(), stations: Array(sample.prefix(4)), fuel: fuel, location: nil, locationSource: "none")
return FuelPriceEntry(date: Date(), stations: Array(sample.prefix(4)), fuel: fuel, location: nil, locationSource: "none", unit: unit)
}
func getSnapshot(in context: Context, completion: @escaping (FuelPriceEntry) -> Void) {
@@ -85,11 +87,12 @@ struct FuelPriceTimelineProvider: TimelineProvider {
}
// 3) Load stations, then sort by price (distance tiebreak), scoped to
// the chosen search radius (miles) so the widget's "cheapest" matches
// the chosen search radius so the widget's "cheapest" matches
// the app's list.
var stations = FuelStore.loadStations()
if stations.isEmpty { stations = SampleFuelProvider.sampleStations }
let radiusKM = Double(FuelStore.loadStationLimit()) * 1.60934 // miles km
let unit = FuelStore.loadDistanceUnit()
let radiusKM = unit.toKM(Double(FuelStore.loadStationLimit())) // chosen units km
if let location {
// STRICT: cached data fetched around another location must never
// leak out-of-radius stations into the widget.
@@ -112,7 +115,7 @@ struct FuelPriceTimelineProvider: TimelineProvider {
sorted = filtered.sorted { $0.prices[fuel]! < $1.prices[fuel]! }
}
return FuelPriceEntry(date: Date(), stations: Array(sorted.prefix(4)), fuel: fuel, location: location, locationSource: source)
return FuelPriceEntry(date: Date(), stations: Array(sorted.prefix(4)), fuel: fuel, location: location, locationSource: source, unit: FuelStore.loadDistanceUnit())
}
}
@@ -158,7 +161,7 @@ struct FuelPriceWidgetView: View {
.foregroundStyle(.green)
}
if let location = entry.location {
Text(String(format: "%.1f mi away", station.distanceKM(to: location.lat, lng2: location.lng) * 0.621371))
Text(entry.unit.format(station.distanceKM(to: location.lat, lng2: location.lng)) + " away")
.font(.caption2)
.foregroundStyle(.secondary)
} else {
@@ -192,7 +195,7 @@ struct FuelPriceWidgetView: View {
.font(.caption.weight(.semibold))
.lineLimit(1)
if let location = entry.location {
Text(String(format: "%.1f km", station.distanceKM(to: location.lat, lng2: location.lng)))
Text(entry.unit.format(station.distanceKM(to: location.lat, lng2: location.lng)))
.font(.caption2)
.foregroundStyle(.secondary)
}