Make Closest a station label and retire Top
Top and Cheapest were two words for one state: both meant "the lowest price among the stations on screen", differing only in the candidate set (radius vs Favourites). Which word a surface used had drifted by device — the phone's rows said Cheapest while the watch said Top for the identical state. Top is gone from the enum, the Key legend, the badge and the watch's hand-rolled label; Cheapest survives because it names the measure and is already the word the favourites copy uses. Closest is now a real station label rather than a verdict. StationRecommendationBadge gains .closest with a total precedence — Best value, then Cheapest, then Closest — because a row has room for one label and proximity is the weakest claim: a short drive says nothing about the price, and unlike the other two it reports no saving at all. The badge sits above the price, where the other two sit. The closest row's verdict capsule is suppressed: its only word for that state was "Closest", so printing it below as well would state the same fact twice, three lines apart. Other rows keep their Saves / Break even / Costs capsules unchanged. "Closest station" becomes "Closest", read from the single FuelSavingsResult.baselineHeadline constant so the badge, the verdict and the Key cannot word it differently. The Key legend renders all three station labels with the shared StationLabelBadge; the old KeyStationLabelIndicator, which special-cased the closest station to a wider verdict pill so it read as a different class of thing from the two above it, is deleted. One closest icon everywhere. The listing and detail verdict symbols used location.circle and location.circle.fill for the state the Key taught as location.north.fill; both now read StationLabelKind.closest.symbol, so the three surfaces resolve one glyph from one place. The detail sheet gains the Closest capsule below the map, ordered to match the Key. Favourites gains the nearest favourite as its Closest, measured on the same one-way distance its rows print and restricted to favourites with a price for the active fuel — matching the radius tab, where the closest station is by definition the savings baseline. closestStationID flows from the one scaffold baseline the savings lookup already compares against, so a row's badge and the reference station cannot disagree. No savings calculation, threshold, ranking, eligibility or station-selection logic changed. The Key sheet's detent is now bound rather than fixed, so dragging it up to read the Station labels section sticks instead of snapping back; a QA launch argument opens it at full height, since the 0.6 detent puts that section below the fold on every device. That argument is read unconditionally, unlike the other harness hooks which are #if DEBUG-gated. Tests: 278 pass, 9 new — the label precedence ladder across every combination, capsule order in the sheet, Closest alone, and the retired label pinned absent.
This commit is contained in:
@@ -64,17 +64,10 @@ extension CapsuleTone {
|
||||
/// Neutral grey: proximity. Same family as `breakEven` — both mean "no
|
||||
/// judgement" — but kept as its own pair so proximity and a break-even
|
||||
/// verdict stay independently tunable.
|
||||
static let closestStation = CapsuleTone(
|
||||
static let closest = CapsuleTone(
|
||||
lightForeground: Color(hex: 0x525967), lightBackground: Color(hex: 0xE8EAF0),
|
||||
darkForeground: Color(hex: 0xC0C3CC), darkBackground: Color(hex: 0x424349)
|
||||
)
|
||||
|
||||
/// Blue, like `cheapest`: the top favourite IS the cheapest favourite, so it
|
||||
/// must not read as a different class of thing.
|
||||
static let top = CapsuleTone(
|
||||
lightForeground: Color(hex: 0x225F9B), lightBackground: Color(hex: 0xE0EFFB),
|
||||
darkForeground: Color(hex: 0x8BC5FA), darkBackground: Color(hex: 0x293F53)
|
||||
)
|
||||
}
|
||||
|
||||
/// The one shape rule every capsule in the app follows.
|
||||
@@ -83,9 +76,10 @@ extension CapsuleTone {
|
||||
/// wraps. A pill's semicircular ends crowd the text corners at two lines or
|
||||
/// more, while a 10pt radius does not. The shape follows what the content
|
||||
/// actually DREW, not how many lines it declared, because a label can wrap in a
|
||||
/// narrow column without the data changing — "Closest station" is the case that
|
||||
/// forced this: one declared line that wraps to two in the listing's price
|
||||
/// column.
|
||||
/// narrow column without the data changing — the baseline label was the case that
|
||||
/// forced this: "Closest station" was one declared line that wrapped to two in the
|
||||
/// listing's price column, which is also why that label is now the single word
|
||||
/// "Closest".
|
||||
///
|
||||
/// Measuring happens on the content BEFORE padding, so the drawn line count is
|
||||
/// never inflated by the capsule's own insets.
|
||||
|
||||
+38
-25
@@ -321,29 +321,29 @@ struct ContentView: View {
|
||||
return pool.compactMap { $0.prices[selectedFuel] }.min()
|
||||
}
|
||||
|
||||
/// The "best" station — the cheapest in the pool — gets the TOP badge.
|
||||
/// The cheapest station in the pool — the one that wears the Cheapest badge.
|
||||
/// First occurrence in display order wins on price ties.
|
||||
private var topStationID: String? {
|
||||
private var cheapestStationID: String? {
|
||||
guard let baselinePrice else { return nil }
|
||||
return topStationID(in: displayedStations, baselinePrice: baselinePrice)
|
||||
return cheapestStationID(in: displayedStations, baselinePrice: baselinePrice)
|
||||
}
|
||||
|
||||
/// The TOP-badge station within an ALREADY-SORTED list. Split out so a caller
|
||||
/// that has the sorted list (the Stations tab) does not sort the whole
|
||||
/// The Cheapest-badge station within an ALREADY-SORTED list. Split out so a
|
||||
/// caller that has the sorted list (the Stations tab) does not sort the whole
|
||||
/// country a second time just to find one id.
|
||||
private func topStationID(in list: [FuelStation], baselinePrice: Double) -> String? {
|
||||
private func cheapestStationID(in list: [FuelStation], baselinePrice: Double) -> String? {
|
||||
list.first { $0.prices[selectedFuel] == baselinePrice }?.id
|
||||
}
|
||||
|
||||
/// The app's own "cheapest in range" — the TOP-badge station — mirrored
|
||||
/// The app's own "cheapest in range" — the Cheapest-badge station — mirrored
|
||||
/// into the Settings → Debug section so its "Cheapest in range" row
|
||||
/// matches the app list (and the widget when the widget's fuel + distance
|
||||
/// match the app's). The debug section previously showed the ALERT
|
||||
/// prediction (alertsFuel + alert radius), which is a different pool by
|
||||
/// design — that mismatch is what this fixes.
|
||||
private var appCheapestStatus: DebugAppCheapest? {
|
||||
guard let topID = topStationID,
|
||||
let station = stations.first(where: { $0.id == topID }),
|
||||
guard let cheapestID = cheapestStationID,
|
||||
let station = stations.first(where: { $0.id == cheapestID }),
|
||||
let location else { return nil }
|
||||
return DebugAppCheapest(
|
||||
station: station,
|
||||
@@ -428,7 +428,7 @@ struct ContentView: View {
|
||||
///
|
||||
/// The baseline is chosen with the same journey guards as
|
||||
/// `FuelSavingsEstimator.baselineStation` when configured; with the feature
|
||||
/// off, the ordinary Closest station still names that row.
|
||||
/// off, the ordinary closest station still names that row.
|
||||
private func savingsScaffold() -> (baseline: FuelStation?, journeys: [String: FuelSavingsJourney]) {
|
||||
guard let location else { return (nil, [:]) }
|
||||
guard fuelSavingsSettings.isConfigured else {
|
||||
@@ -833,7 +833,10 @@ struct ContentView: View {
|
||||
// The same "cheapest in range" test this view gives its
|
||||
// rows, so the sheet's Cheapest capsule matches the badge on
|
||||
// the list it was opened from.
|
||||
isCheapestInRange: station.id == topStationID,
|
||||
isCheapestInRange: station.id == cheapestStationID,
|
||||
// The same "closest" test the rows get, from the ONE
|
||||
// scaffold baseline the savings lookup also compares against.
|
||||
isClosestInRange: station.id == scaffold.baseline?.id,
|
||||
isFavourite: favouriteIDs.contains(station.id),
|
||||
onToggleFavourite: { toggleFavourite(station, fuel: selectedFuel) }
|
||||
)
|
||||
@@ -1528,15 +1531,15 @@ struct ContentView: View {
|
||||
/// within the compiler's type-check budget.
|
||||
private var stationsTab: some View {
|
||||
// ONE scaffold (baseline + journeys) and ONE sort serve the list, its
|
||||
// count, the TOP badge and every savings label. Asking for
|
||||
// count, the Cheapest badge and every savings label. Asking for
|
||||
// `nearestSavingsBaseline`, `displayedStations`, `sortedStations.count`,
|
||||
// `topStationID` and `savingsLookup` separately walked the full ~8k list
|
||||
// six times per render — seconds of main-thread work in Closest mode,
|
||||
// `cheapestStationID` and `savingsLookup` separately walked the full ~8k
|
||||
// list six times per render — seconds of main-thread work in Closest mode,
|
||||
// where the pool is every station in the UK.
|
||||
let scaffold = savingsScaffold()
|
||||
let baselineStation = scaffold.baseline
|
||||
let displayed = displayedStations
|
||||
let topID = baselinePrice.flatMap { topStationID(in: displayed, baselinePrice: $0) }
|
||||
let cheapestID = baselinePrice.flatMap { cheapestStationID(in: displayed, baselinePrice: $0) }
|
||||
return StationsView(
|
||||
stations: displayed,
|
||||
totalCount: displayed.count,
|
||||
@@ -1547,7 +1550,8 @@ struct ContentView: View {
|
||||
distanceUnit: distanceUnit,
|
||||
priceDisplayStyle: priceDisplayStyle,
|
||||
baselinePrice: baselinePrice,
|
||||
topStationID: topID,
|
||||
cheapestStationID: cheapestID,
|
||||
closestStationID: baselineStation?.id,
|
||||
location: location,
|
||||
favouriteIDs: favouriteIDs,
|
||||
stationTapAction: stationTapAction,
|
||||
@@ -1801,8 +1805,11 @@ struct StationRow: View {
|
||||
let priceDisplayStyle: PriceDisplayStyle
|
||||
let baselinePrice: Double?
|
||||
/// This row is the cheapest price in range — the caller's existing
|
||||
/// `topStationID` test. Feeds the CHEAPEST badge.
|
||||
let isTopResult: Bool
|
||||
/// `cheapestStationID` test. Feeds the Cheapest badge.
|
||||
let isCheapestResult: Bool
|
||||
/// This row is the station savings are measured against — the caller's
|
||||
/// `closestStationID` test. Feeds the Closest badge.
|
||||
let isClosestResult: Bool
|
||||
let isFavourite: Bool
|
||||
var tapAction: StationTapAction = .openMap
|
||||
var fuelSavings: FuelSavingsAvailability?
|
||||
@@ -1862,12 +1869,13 @@ struct StationRow: View {
|
||||
}
|
||||
|
||||
/// The row's single recommendation badge, if any. BEST VALUE (worth the
|
||||
/// drive by the alert threshold) outranks CHEAPEST; NEAREST never gets one,
|
||||
/// being context rather than a recommendation.
|
||||
/// drive by the alert threshold) outranks CHEAPEST, which outranks CLOSEST —
|
||||
/// see `StationRecommendationBadge.badge` for why proximity ranks last.
|
||||
private var recommendationBadge: StationRecommendationBadge? {
|
||||
StationRecommendationBadge.badge(
|
||||
netSavingGBP: fuelSavings?.result?.netSavingGBP,
|
||||
isCheapest: isTopResult,
|
||||
isCheapest: isCheapestResult,
|
||||
isClosest: isClosestResult,
|
||||
bestValueThresholdGBP: bestValueThresholdGBP
|
||||
)
|
||||
}
|
||||
@@ -2051,7 +2059,11 @@ struct StationRow: View {
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(priceSpokenLabel(price))
|
||||
}
|
||||
if let result = fuelSavings?.result {
|
||||
// The closest station wears the Closest badge above its price
|
||||
// and gets no verdict capsule: the capsule's only word for that
|
||||
// state was "Closest", so printing it here as well would say
|
||||
// the same thing twice, three lines apart.
|
||||
if let result = fuelSavings?.result, !isClosestResult {
|
||||
savingsCapsule(result)
|
||||
}
|
||||
}
|
||||
@@ -2080,9 +2092,9 @@ struct StationRow: View {
|
||||
}
|
||||
|
||||
/// The listing's verdict capsule — ONE component for every status, so the
|
||||
/// baseline's "Closest station" pill carries the same soft blue fill, white
|
||||
/// text and shape rules as Saves, Costs and Break even. The detail sheet
|
||||
/// keeps `FuelSavingsCapsule` to itself, unchanged.
|
||||
/// baseline's "Closest" pill carries the same fill, text and shape rules as
|
||||
/// Saves, Costs and Break even. The detail sheet keeps `FuelSavingsCapsule` to
|
||||
/// itself, unchanged.
|
||||
private func savingsCapsule(_ result: FuelSavingsResult) -> some View {
|
||||
ListingFuelSavingsCapsule(result: result)
|
||||
}
|
||||
@@ -2106,6 +2118,7 @@ private extension StationRecommendationBadge {
|
||||
switch self {
|
||||
case .bestValue: return .bestValue
|
||||
case .cheapest: return .cheapest
|
||||
case .closest: return .closest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,26 @@ struct FavouritesView: View {
|
||||
}
|
||||
private var cheapestPrice: Double? { cheapest?.prices[activeFuel] }
|
||||
|
||||
/// Nearest favourited station for the active fuel — this tab's Closest.
|
||||
///
|
||||
/// Measured on the same one-way distance the rows print, so the badge cannot
|
||||
/// point at a station the list shows as further away than its neighbour. Only
|
||||
/// favourites with a price for the active fuel are eligible, matching the
|
||||
/// radius tab, where the closest station is by definition the savings
|
||||
/// baseline; an unpriced favourite appears last and wears no label. Ties keep
|
||||
/// the user's own order, which is what `ordered` already encodes.
|
||||
private var nearest: FuelStation? {
|
||||
guard let location else { return nil }
|
||||
var best: (station: FuelStation, km: Double)?
|
||||
for station in ordered where station.prices[activeFuel] != nil {
|
||||
let km = FuelStore.stationDistanceDisplay(
|
||||
station: station, userLat: location.lat, userLng: location.lng
|
||||
).km
|
||||
if best == nil || km < best!.km { best = (station, km) }
|
||||
}
|
||||
return best?.station
|
||||
}
|
||||
|
||||
/// The baseline note is useful only when at least one visible favourite has
|
||||
/// an actual savings comparison. An unavailable result (for example, while
|
||||
/// location is missing) must not leave an orphaned explanation below the list.
|
||||
@@ -183,7 +203,8 @@ struct FavouritesView: View {
|
||||
distanceUnit: distanceUnit,
|
||||
priceDisplayStyle: priceDisplayStyle,
|
||||
baselinePrice: cheapestPrice,
|
||||
isTopResult: station.id == cheapest?.id,
|
||||
isCheapestResult: station.id == cheapest?.id,
|
||||
isClosestResult: station.id == nearest?.id,
|
||||
isFavourite: activeFuelFavouriteIDs.contains(station.id),
|
||||
tapAction: stationTapAction,
|
||||
fuelSavings: savingsForStation(station),
|
||||
@@ -261,9 +282,11 @@ struct FavouritesView: View {
|
||||
fuelSavings: savingsForStation(station),
|
||||
fuelSavingsAmount: fuelSavingsAmount,
|
||||
journeyDiagnostics: journeyDiagnosticsForStation(station),
|
||||
// This tab's cheapest favourite — the station its rows badge,
|
||||
// and the one the Key calls "Top".
|
||||
isTopFavourite: station.id == cheapest?.id,
|
||||
// This tab's cheapest favourite — the same flag its rows get,
|
||||
// and the same label they wear.
|
||||
isCheapestInRange: station.id == cheapest?.id,
|
||||
// This tab's nearest favourite — the same flag its rows get.
|
||||
isClosestInRange: station.id == nearest?.id,
|
||||
// Every station listed here IS a favourite for the tab's
|
||||
// fuel, so the star starts filled; tapping it unstars.
|
||||
isFavourite: true,
|
||||
|
||||
@@ -140,7 +140,7 @@ struct FuelSavingsCapsule: View {
|
||||
case .worthTheDrive: return .saves
|
||||
case .notWorthTheDrive: return .costs
|
||||
case .breakEven: return .breakEven
|
||||
case .baseline: return .closestStation
|
||||
case .baseline: return .closest
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,11 @@ struct FuelSavingsCapsule: View {
|
||||
case .worthTheDrive: return "checkmark.circle.fill"
|
||||
case .breakEven: return "equal.circle.fill"
|
||||
case .notWorthTheDrive: return "xmark.circle.fill"
|
||||
case .baseline: return "location.circle.fill"
|
||||
// The ONE closest glyph, shared with the Key legend and the station
|
||||
// badges (`StationLabelKind.closest`). The other three verdicts are
|
||||
// circular because they report an outcome; closest reports a position,
|
||||
// and a north arrow is what the legend teaches for it.
|
||||
case .baseline: return StationLabelKind.closest.symbol
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,7 +246,9 @@ struct ListingFuelSavingsCapsule: View {
|
||||
case .worthTheDrive: return "checkmark.circle"
|
||||
case .notWorthTheDrive: return "arrow.up.circle"
|
||||
case .breakEven: return "equal.circle"
|
||||
case .baseline: return "location.circle"
|
||||
// Same closest glyph as the detail verdict and the Key — this capsule is
|
||||
// the listing's copy of the same state, not a third icon for it.
|
||||
case .baseline: return StationLabelKind.closest.symbol
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ struct BestValueDetailCard: View {
|
||||
|
||||
/// The working: what the extra travel adds, what the pump saves, and the
|
||||
/// total. A baseline station has nothing to save against itself, so it stops
|
||||
/// after the panels — the capsule above already says "Closest station".
|
||||
/// after the panels — the capsule above already says "Closest".
|
||||
@ViewBuilder
|
||||
private func workings(_ result: FuelSavingsResult) -> some View {
|
||||
if result.status != .baseline {
|
||||
|
||||
@@ -26,13 +26,15 @@ struct StationDetailView: View {
|
||||
var fuelSavingsAmount: FuelSavingsFillAmount = FuelSavingsSettings.defaultPreferredAmount
|
||||
var savingsBaselineStationName: String? = nil
|
||||
var journeyDiagnostics: FuelSavingsJourneyDiagnostics? = nil
|
||||
/// Ranking flags, supplied by the list that opened this sheet — the SAME flags
|
||||
/// Ranking flags, supplied by the list that opened this sheet — the SAME flag
|
||||
/// it hands its own rows, so this sheet cannot award a capsule the row behind it
|
||||
/// did not. `isCheapestInRange` is the Stations tab's cheapest price in range;
|
||||
/// `isTopFavourite` is the cheapest of your Favourites. A host passes one or the
|
||||
/// other, never both, so one achievement is never labelled twice.
|
||||
/// did not. Both tabs mean the same thing by "cheapest": the lowest price among
|
||||
/// the stations they are listing, whether that is the radius or the Favourites.
|
||||
var isCheapestInRange: Bool = false
|
||||
var isTopFavourite: Bool = false
|
||||
/// The same "closest" test the host gives its own rows. Its meaning follows
|
||||
/// the tab, exactly as Cheapest does: the nearest station in the radius, or
|
||||
/// the nearest favourite.
|
||||
var isClosestInRange: Bool = false
|
||||
/// The user's Best Value alert threshold — the app's existing gate, defaulted
|
||||
/// exactly as `StationRow` defaults it, so the capsule here and the badge on the
|
||||
/// list agree to the penny.
|
||||
@@ -84,7 +86,7 @@ struct StationDetailView: View {
|
||||
StationRanking.kinds(
|
||||
netSavingGBP: fuelSavings?.result?.netSavingGBP,
|
||||
isCheapestInRange: isCheapestInRange,
|
||||
isTopFavourite: isTopFavourite,
|
||||
isClosestInRange: isClosestInRange,
|
||||
bestValueThresholdGBP: bestValueThresholdGBP
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,16 @@ struct KeySheetView: View {
|
||||
@Binding var showImportantInformation: Bool
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
/// Where the user last dragged the sheet. Bound rather than fixed at 0.6 so
|
||||
/// dragging it up to read the Station labels section sticks, instead of
|
||||
/// snapping back every time the sheet is reopened.
|
||||
///
|
||||
/// The QA launch argument sets the INITIAL detent rather than assigning one
|
||||
/// after the sheet appears: assigning a new selection while the sheet is still
|
||||
/// presenting is ignored, which left the legend stuck at 0.6 and the Station
|
||||
/// labels section off screen.
|
||||
@State private var detent: PresentationDetent =
|
||||
ProcessInfo.processInfo.arguments.contains("-qaKeySheetLarge") ? .large : .fraction(0.6)
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
@@ -46,7 +56,7 @@ struct KeySheetView: View {
|
||||
Section {
|
||||
ForEach(StationLabelKind.allCases, id: \.self) { kind in
|
||||
KeySheetDescriptionRow(item: item(for: kind)) {
|
||||
KeyStationLabelIndicator(kind: kind)
|
||||
StationLabelBadge(kind: kind)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -81,7 +91,11 @@ struct KeySheetView: View {
|
||||
// Nested sheet: the Key sheet stays behind the disclaimer.
|
||||
.importantInformationSheet(isPresented: $showImportantInformation)
|
||||
}
|
||||
.presentationDetents([.fraction(0.6), .large])
|
||||
// QA hook: `-qaKeySheetLarge` opens the legend at full height — see the
|
||||
// `detent` state. The 0.6 detent puts the Station labels section below the
|
||||
// fold on every device, including the tallest iPhone, so the three label
|
||||
// badges cannot otherwise be seen on screen.
|
||||
.presentationDetents([.fraction(0.6), .large], selection: $detent)
|
||||
.presentationBackground(Color(UIColor.systemGroupedBackground))
|
||||
}
|
||||
|
||||
@@ -107,7 +121,11 @@ struct StationsView: View {
|
||||
let distanceUnit: DistanceUnit
|
||||
let priceDisplayStyle: PriceDisplayStyle
|
||||
let baselinePrice: Double?
|
||||
let topStationID: String?
|
||||
let cheapestStationID: String?
|
||||
/// The station the savings are measured against — the closest station with an
|
||||
/// eligible price. Passed in from the owner of the scaffold baseline so this
|
||||
/// list's Closest badge and the reference station cannot disagree.
|
||||
let closestStationID: String?
|
||||
let location: Coordinate?
|
||||
let favouriteIDs: Set<String>
|
||||
let stationTapAction: StationTapAction
|
||||
@@ -119,7 +137,7 @@ struct StationsView: View {
|
||||
var onRefresh: () async -> Void = {}
|
||||
|
||||
/// Pagination: one page = 10 rows, reset whenever the underlying list
|
||||
/// changes (new fetch, fuel/sort/radius switch). RAG/TOP/deltas still come
|
||||
/// changes (new fetch, fuel/sort/radius switch). RAG/badge/deltas still come
|
||||
/// from the whole radius pool via `cheapestPrice` — paging never changes
|
||||
/// which station is "best".
|
||||
@State private var pageSize = 10
|
||||
@@ -386,8 +404,10 @@ struct StationsView: View {
|
||||
fuelSavings: savingsForStation(station),
|
||||
fuelSavingsAmount: fuelSavingsAmount,
|
||||
journeyDiagnostics: journeyDiagnosticsForStation(station),
|
||||
// The same flag the row gets from `topStationID`.
|
||||
isCheapestInRange: station.id == topStationID,
|
||||
// The same flag the row gets from `cheapestStationID`.
|
||||
isCheapestInRange: station.id == cheapestStationID,
|
||||
// The same flag the row gets from `closestStationID`.
|
||||
isClosestInRange: station.id == closestStationID,
|
||||
isFavourite: favouriteIDs.contains(station.id),
|
||||
onToggleFavourite: { onToggleFavourite(station, selectedFuel) }
|
||||
)
|
||||
@@ -411,7 +431,8 @@ struct StationsView: View {
|
||||
distanceUnit: distanceUnit,
|
||||
priceDisplayStyle: priceDisplayStyle,
|
||||
baselinePrice: baselinePrice,
|
||||
isTopResult: station.id == topStationID,
|
||||
isCheapestResult: station.id == cheapestStationID,
|
||||
isClosestResult: station.id == closestStationID,
|
||||
isFavourite: favouriteIDs.contains(station.id),
|
||||
tapAction: stationTapAction,
|
||||
fuelSavings: savingsForStation(station),
|
||||
@@ -557,8 +578,7 @@ struct StationLabelBadge: View {
|
||||
switch kind {
|
||||
case .bestValue: return .bestValue
|
||||
case .cheapest: return .cheapest
|
||||
case .closestStation: return .closestStation
|
||||
case .top: return .top
|
||||
case .closest: return .closest
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,34 +598,14 @@ struct StationLabelBadge: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// The Key sheet's indicator for a station label. The closest station is the one
|
||||
/// exception: it renders the listing's real verdict capsule, so the legend
|
||||
/// demonstrates the same fill, text and shape the rows show. Everything else is
|
||||
/// the shared badge.
|
||||
private struct KeyStationLabelIndicator: View {
|
||||
let kind: StationLabelKind
|
||||
|
||||
var body: some View {
|
||||
if kind == .closestStation {
|
||||
ListingFuelSavingsCapsule(result: baselineResult)
|
||||
} else {
|
||||
StationLabelBadge(kind: kind)
|
||||
}
|
||||
}
|
||||
|
||||
private var baselineResult: FuelSavingsResult {
|
||||
FuelSavingsResult(
|
||||
fillLitres: 30,
|
||||
grossSavingGBP: 0,
|
||||
extraDrivingCostGBP: 0,
|
||||
extraDistanceMiles: 0,
|
||||
netSavingGBP: 0,
|
||||
status: .baseline,
|
||||
journeyConfidence: .confirmed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Every station label in the Key is the SAME `StationLabelBadge` the listing rows
|
||||
/// and the More info sheet draw.
|
||||
///
|
||||
/// The closest station used to be the exception — the legend rendered the real
|
||||
/// verdict capsule for it, a wider pill with different fill and shape, so it read as
|
||||
/// a different class of thing from Best value and Cheapest sitting above it. Now
|
||||
/// that its label is the single word "Closest", it belongs in the same column, in
|
||||
/// the same capsule, at the same size.
|
||||
// MARK: - Fuel-type iconography (app target only — FuelStore.swift is Foundation-only)
|
||||
|
||||
extension FuelType {
|
||||
|
||||
@@ -9,10 +9,9 @@
|
||||
"Alerts" = "Alerts";
|
||||
"Settings" = "Settings";
|
||||
"FuelBoard" = "FuelBoard";
|
||||
"Top" = "Top";
|
||||
"Best value" = "Best value";
|
||||
"Cheapest" = "Cheapest";
|
||||
"Closest station" = "Closest station";
|
||||
"Closest" = "Closest";
|
||||
|
||||
/* Stations tab */
|
||||
"Closest %@ stations — closest first, lowest price within %lld %@ · %lld stations" = "Closest %@ stations — closest first, lowest price within %lld %@ · %lld stations";
|
||||
@@ -44,11 +43,9 @@
|
||||
"(approx.)" = "(approx.)";
|
||||
"%@ more" = "%@ more";
|
||||
"Station labels" = "Station labels";
|
||||
"Closest station" = "Closest station";
|
||||
"Meets your minimum saving after travel costs." = "Meets your minimum saving after travel costs.";
|
||||
"The lowest fuel price per litre within your selected range." = "The lowest fuel price per litre within your selected range.";
|
||||
"The nearest station by journey distance." = "The nearest station by journey distance.";
|
||||
"The cheapest station in your Favourites for the selected fuel." = "The cheapest station in your Favourites for the selected fuel.";
|
||||
"The closest station by journey distance." = "The closest station by journey distance.";
|
||||
"Star a station to save it to your Favourites." = "Star a station to save it to your Favourites.";
|
||||
"How does FuelBoard calculate value?" = "How does FuelBoard calculate value?";
|
||||
/* Title of the explanation sheet the row above opens (a short form of it). */
|
||||
|
||||
Reference in New Issue
Block a user