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.
|
/// Trends sheet (price history chart) presentation state.
|
||||||
@State private var showTrends = false
|
@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).
|
/// Station picked for the More-info sheet (tap action = showDetails).
|
||||||
@State private var detailStation: FuelStation?
|
@State private var detailStation: FuelStation?
|
||||||
|
|
||||||
@@ -206,8 +214,8 @@ struct FavouritesView: View {
|
|||||||
}
|
}
|
||||||
.navigationTitle(String(localized: "Favourites", comment: "Favourites tab title"))
|
.navigationTitle(String(localized: "Favourites", comment: "Favourites tab title"))
|
||||||
.toolbar {
|
.toolbar {
|
||||||
if !favourites.isEmpty {
|
ToolbarItemGroup(placement: .topBarTrailing) {
|
||||||
ToolbarItemGroup(placement: .topBarTrailing) {
|
if !favourites.isEmpty {
|
||||||
Button {
|
Button {
|
||||||
showTrends = true
|
showTrends = true
|
||||||
} label: {
|
} label: {
|
||||||
@@ -216,8 +224,25 @@ struct FavouritesView: View {
|
|||||||
}
|
}
|
||||||
EditButton()
|
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) {
|
.sheet(isPresented: $showTrends) {
|
||||||
TrendsView(
|
TrendsView(
|
||||||
favourites: favourites,
|
favourites: favourites,
|
||||||
|
|||||||
@@ -160,6 +160,12 @@ struct ListingFuelSavingsCapsule: View {
|
|||||||
/// Station rows carry the figure; the Key sheet's legend shows the wording only.
|
/// Station rows carry the figure; the Key sheet's legend shows the wording only.
|
||||||
var showsAmount: Bool = true
|
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 {
|
private var verdict: FuelSavingsResult.ListingVerdict {
|
||||||
result.listingVerdict(showsAmount: showsAmount)
|
result.listingVerdict(showsAmount: showsAmount)
|
||||||
}
|
}
|
||||||
@@ -185,6 +191,13 @@ struct ListingFuelSavingsCapsule: View {
|
|||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
// Never compress or truncate: the capsule wraps and the row grows taller.
|
// Never compress or truncate: the capsule wraps and the row grows taller.
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.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
|
// 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
|
// 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))
|
.background(shape.fill(fill))
|
||||||
.accessibilityElement(children: .combine)
|
.accessibilityElement(children: .combine)
|
||||||
.accessibilityLabel(verdict.spokenText)
|
.accessibilityLabel(verdict.spokenText)
|
||||||
|
.onPreferenceChange(CapsuleTextHeightKey.self) { textHeight = $0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Saves and Costs only — the two verdicts that carry a figure. See the
|
/// 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)
|
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
|
/// 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
|
/// 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
|
/// radius does not. Keyed to the DRAWN line count, so a wrapped "Closest
|
||||||
/// when "(approx.)" pushes it onto a second line.
|
/// station" gets the same rounded rect as a figure pushed onto its own line.
|
||||||
private var shape: AnyShape {
|
private var shape: AnyShape {
|
||||||
verdict.lines.count > 1
|
drawnLineCount > 1
|
||||||
? AnyShape(RoundedRectangle(cornerRadius: 8))
|
? AnyShape(RoundedRectangle(cornerRadius: 8))
|
||||||
: AnyShape(Capsule())
|
: 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
|
/// The station listing's verdict-capsule fills: muted semantic colours carrying
|
||||||
/// white text and icon.
|
/// white text and icon.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,6 +1,94 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WidgetKit
|
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.
|
/// Stations tab — the main list with fuel picker, distance filter, sort, pagination.
|
||||||
struct StationsView: View {
|
struct StationsView: View {
|
||||||
let stations: [FuelStation]
|
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 {
|
private var keySheet: some View {
|
||||||
NavigationStack {
|
KeySheetView(
|
||||||
List {
|
fuelTint: selectedFuel.tintColor,
|
||||||
Section {
|
showBestValueInfo: $showBestValueInfo,
|
||||||
KeySheetDescriptionRow(item: KeySheetContent.priceRatings[0]) {
|
showImportantInformation: $showImportantInformation
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func stationRow(_ station: FuelStation) -> some View {
|
private func stationRow(_ station: FuelStation) -> some View {
|
||||||
|
|||||||
Reference in New Issue
Block a user