Compare commits
2
Commits
244f9be640
...
ab93f6286a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab93f6286a | ||
|
|
6edab35400 |
@@ -48,6 +48,14 @@ struct FavouritesView: View {
|
||||
/// Trends sheet (price history chart) presentation state.
|
||||
@State private var showTrends = false
|
||||
|
||||
/// The Key (legend) sheet — the same sheet the Stations tab presents, opened
|
||||
/// from the info button beside Edit.
|
||||
@State private var showKey = false
|
||||
/// The Key sheet's two nested sheets, owned here because its rows set them
|
||||
/// (same arrangement as StationsView, so the QA hooks behave identically).
|
||||
@State private var showBestValueInfo = false
|
||||
@State private var showImportantInformation = false
|
||||
|
||||
/// Station picked for the More-info sheet (tap action = showDetails).
|
||||
@State private var detailStation: FuelStation?
|
||||
|
||||
@@ -206,8 +214,8 @@ struct FavouritesView: View {
|
||||
}
|
||||
.navigationTitle(String(localized: "Favourites", comment: "Favourites tab title"))
|
||||
.toolbar {
|
||||
if !favourites.isEmpty {
|
||||
ToolbarItemGroup(placement: .topBarTrailing) {
|
||||
ToolbarItemGroup(placement: .topBarTrailing) {
|
||||
if !favourites.isEmpty {
|
||||
Button {
|
||||
showTrends = true
|
||||
} label: {
|
||||
@@ -216,8 +224,25 @@ struct FavouritesView: View {
|
||||
}
|
||||
EditButton()
|
||||
}
|
||||
// Same button and same slot as the Stations tab: last in the
|
||||
// group, so it sits to the right of Edit. Shown even with no
|
||||
// favourites — the legend explains the prices and labels, which
|
||||
// matter before anything is starred.
|
||||
Button {
|
||||
showKey = true
|
||||
} label: {
|
||||
Image(systemName: "info.circle")
|
||||
}
|
||||
.accessibilityLabel(String(localized: "Key", comment: "Key button accessibility label"))
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showKey) {
|
||||
KeySheetView(
|
||||
fuelTint: activeFuel.tintColor,
|
||||
showBestValueInfo: $showBestValueInfo,
|
||||
showImportantInformation: $showImportantInformation
|
||||
)
|
||||
}
|
||||
.sheet(isPresented: $showTrends) {
|
||||
TrendsView(
|
||||
favourites: favourites,
|
||||
|
||||
@@ -160,6 +160,12 @@ struct ListingFuelSavingsCapsule: View {
|
||||
/// Station rows carry the figure; the Key sheet's legend shows the wording only.
|
||||
var showsAmount: Bool = true
|
||||
|
||||
/// The height the text actually drew, so the shape can follow the drawn line
|
||||
/// count rather than the data's. See `drawnLineCount`.
|
||||
@State private var textHeight: CGFloat = 0
|
||||
/// One caption line's height, scaled with Dynamic Type, to divide that by.
|
||||
@ScaledMetric(relativeTo: .caption) private var captionLineHeight: CGFloat = 14
|
||||
|
||||
private var verdict: FuelSavingsResult.ListingVerdict {
|
||||
result.listingVerdict(showsAmount: showsAmount)
|
||||
}
|
||||
@@ -185,6 +191,13 @@ struct ListingFuelSavingsCapsule: View {
|
||||
.multilineTextAlignment(.leading)
|
||||
// Never compress or truncate: the capsule wraps and the row grows taller.
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.background(
|
||||
GeometryReader { proxy in
|
||||
Color.clear
|
||||
.preference(key: CapsuleTextHeightKey.self,
|
||||
value: proxy.size.height)
|
||||
}
|
||||
)
|
||||
}
|
||||
// White on the muted fill. These capsules carry a figure the eye is asked
|
||||
// to read at a glance, which is why they are the only filled-and-white
|
||||
@@ -200,6 +213,7 @@ struct ListingFuelSavingsCapsule: View {
|
||||
.background(shape.fill(fill))
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel(verdict.spokenText)
|
||||
.onPreferenceChange(CapsuleTextHeightKey.self) { textHeight = $0 }
|
||||
}
|
||||
|
||||
/// Saves and Costs only — the two verdicts that carry a figure. See the
|
||||
@@ -216,12 +230,22 @@ struct ListingFuelSavingsCapsule: View {
|
||||
ListingCapsuleFill.color(for: result.status)
|
||||
}
|
||||
|
||||
/// How many lines the text DREW, not how many the verdict declares.
|
||||
/// "Closest station" is one declared line that wraps to two in the row's
|
||||
/// narrow column, and the shape has to follow what is on screen or the pill's
|
||||
/// semicircular ends crowd the text they are wrapping. Falls back to the
|
||||
/// declared count on the first layout pass, before there is a height.
|
||||
private var drawnLineCount: Int {
|
||||
guard textHeight > 0, captionLineHeight > 0 else { return verdict.lines.count }
|
||||
return max(1, Int((textHeight / captionLineHeight).rounded()))
|
||||
}
|
||||
|
||||
/// One line keeps the capsule. Two or more switch to a rounded rect: a pill's
|
||||
/// semicircular ends crowd the text corners at that height, while an 8pt
|
||||
/// radius does not. Keyed to the LINE COUNT, so Break even flips shape only
|
||||
/// when "(approx.)" pushes it onto a second line.
|
||||
/// radius does not. Keyed to the DRAWN line count, so a wrapped "Closest
|
||||
/// station" gets the same rounded rect as a figure pushed onto its own line.
|
||||
private var shape: AnyShape {
|
||||
verdict.lines.count > 1
|
||||
drawnLineCount > 1
|
||||
? AnyShape(RoundedRectangle(cornerRadius: 8))
|
||||
: AnyShape(Capsule())
|
||||
}
|
||||
@@ -236,6 +260,14 @@ struct ListingFuelSavingsCapsule: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Carries the drawn height of a capsule's text block up to `drawnLineCount`.
|
||||
private struct CapsuleTextHeightKey: PreferenceKey {
|
||||
static let defaultValue: CGFloat = 0
|
||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||
value = max(value, nextValue())
|
||||
}
|
||||
}
|
||||
|
||||
/// The station listing's verdict-capsule fills: muted semantic colours carrying
|
||||
/// white text and icon.
|
||||
///
|
||||
|
||||
@@ -1,6 +1,94 @@
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
/// The "Key" legend: price ratings, savings capsules, station labels, the
|
||||
/// favourite glyph, then the two deeper explanations. One sheet for both list
|
||||
/// tabs — Stations opens it from the info button in the navigation bar,
|
||||
/// Favourites from the button beside Edit — so the legend cannot drift between
|
||||
/// them.
|
||||
///
|
||||
/// Dismisses through the environment rather than the presenting view's state, and
|
||||
/// takes the two nested-sheet flags as bindings, so each tab keeps owning its own
|
||||
/// QA launch-argument hooks.
|
||||
struct KeySheetView: View {
|
||||
/// The selected fuel's colour. `KeyStationLabelIndicator` draws its pump badge
|
||||
/// in it, so the legend matches the rows the reader is looking at.
|
||||
let fuelTint: Color
|
||||
@Binding var showBestValueInfo: Bool
|
||||
@Binding var showImportantInformation: Bool
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section {
|
||||
KeySheetDescriptionRow(item: KeySheetContent.priceRatings[0]) {
|
||||
KeyPriceRatingIndicator(item: KeySheetContent.priceRatings[0], color: .green)
|
||||
}
|
||||
KeySheetDescriptionRow(item: KeySheetContent.priceRatings[1]) {
|
||||
KeyPriceRatingIndicator(item: KeySheetContent.priceRatings[1], color: .orange)
|
||||
}
|
||||
KeySheetDescriptionRow(item: KeySheetContent.priceRatings[2]) {
|
||||
KeyPriceRatingIndicator(item: KeySheetContent.priceRatings[2], color: .red)
|
||||
}
|
||||
} header: {
|
||||
Text("Price ratings")
|
||||
}
|
||||
|
||||
Section {
|
||||
ForEach(KeySheetContent.savings, id: \.label) { item in
|
||||
KeySheetDescriptionRow(item: item) {
|
||||
KeySavingsCapsule(label: item.label)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Savings")
|
||||
}
|
||||
|
||||
Section {
|
||||
ForEach(KeySheetContent.stationLabels, id: \.label) { item in
|
||||
KeySheetDescriptionRow(item: item) {
|
||||
KeyStationLabelIndicator(label: item.label, fuelColor: fuelTint)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Station labels")
|
||||
}
|
||||
|
||||
Section {
|
||||
KeySheetDescriptionRow(item: KeySheetContent.favourite) {
|
||||
FavouriteStarGlyph(isFavourite: true)
|
||||
}
|
||||
} header: {
|
||||
Text("Favourites")
|
||||
}
|
||||
|
||||
Section {
|
||||
HowBestValueRow { showBestValueInfo = true }
|
||||
}
|
||||
|
||||
Section {
|
||||
ImportantInformationRow { showImportantInformation = true }
|
||||
}
|
||||
}
|
||||
.navigationTitle("Key")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Done") { dismiss() }
|
||||
}
|
||||
}
|
||||
// Nested sheet: the Key sheet stays behind the explanation.
|
||||
.sheet(isPresented: $showBestValueInfo) { HowBestValueSheet() }
|
||||
// Nested sheet: the Key sheet stays behind the disclaimer.
|
||||
.importantInformationSheet(isPresented: $showImportantInformation)
|
||||
}
|
||||
.presentationDetents([.fraction(0.6), .large])
|
||||
.presentationBackground(Color(UIColor.systemGroupedBackground))
|
||||
}
|
||||
}
|
||||
|
||||
/// Stations tab — the main list with fuel picker, distance filter, sort, pagination.
|
||||
struct StationsView: View {
|
||||
let stations: [FuelStation]
|
||||
@@ -296,73 +384,15 @@ struct StationsView: View {
|
||||
)
|
||||
}
|
||||
|
||||
/// The legend sheet, shared with the Favourites tab. Only the fuel differs:
|
||||
/// its colour tints the station-label row, so the legend matches the rows the
|
||||
/// reader is actually looking at.
|
||||
private var keySheet: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section {
|
||||
KeySheetDescriptionRow(item: KeySheetContent.priceRatings[0]) {
|
||||
KeyPriceRatingIndicator(item: KeySheetContent.priceRatings[0], color: .green)
|
||||
}
|
||||
KeySheetDescriptionRow(item: KeySheetContent.priceRatings[1]) {
|
||||
KeyPriceRatingIndicator(item: KeySheetContent.priceRatings[1], color: .orange)
|
||||
}
|
||||
KeySheetDescriptionRow(item: KeySheetContent.priceRatings[2]) {
|
||||
KeyPriceRatingIndicator(item: KeySheetContent.priceRatings[2], color: .red)
|
||||
}
|
||||
} header: {
|
||||
Text("Price ratings")
|
||||
}
|
||||
|
||||
Section {
|
||||
ForEach(KeySheetContent.savings, id: \.label) { item in
|
||||
KeySheetDescriptionRow(item: item) {
|
||||
KeySavingsCapsule(label: item.label)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Savings")
|
||||
}
|
||||
|
||||
Section {
|
||||
ForEach(KeySheetContent.stationLabels, id: \.label) { item in
|
||||
KeySheetDescriptionRow(item: item) {
|
||||
KeyStationLabelIndicator(label: item.label, fuelColor: selectedFuel.tintColor)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Station labels")
|
||||
}
|
||||
|
||||
Section {
|
||||
KeySheetDescriptionRow(item: KeySheetContent.favourite) {
|
||||
FavouriteStarGlyph(isFavourite: true)
|
||||
}
|
||||
} header: {
|
||||
Text("Favourites")
|
||||
}
|
||||
|
||||
Section {
|
||||
HowBestValueRow { showBestValueInfo = true }
|
||||
}
|
||||
|
||||
Section {
|
||||
ImportantInformationRow { showImportantInformation = true }
|
||||
}
|
||||
}
|
||||
.navigationTitle("Key")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Done") { showKey = false }
|
||||
}
|
||||
}
|
||||
// Nested sheet: the Key sheet stays behind the explanation.
|
||||
.sheet(isPresented: $showBestValueInfo) { HowBestValueSheet() }
|
||||
// Nested sheet: the Key sheet stays behind the disclaimer.
|
||||
.importantInformationSheet(isPresented: $showImportantInformation)
|
||||
}
|
||||
.presentationDetents([.fraction(0.6), .large])
|
||||
.presentationBackground(Color(UIColor.systemGroupedBackground))
|
||||
KeySheetView(
|
||||
fuelTint: selectedFuel.tintColor,
|
||||
showBestValueInfo: $showBestValueInfo,
|
||||
showImportantInformation: $showImportantInformation
|
||||
)
|
||||
}
|
||||
|
||||
private func stationRow(_ station: FuelStation) -> some View {
|
||||
|
||||
Reference in New Issue
Block a user