Unify disabled states and preserve station verdicts
This commit is contained in:
@@ -533,7 +533,7 @@
|
|||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Config/WatchApp.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Config/WatchApp.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 27;
|
CURRENT_PROJECT_VERSION = 28;
|
||||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@@ -599,7 +599,7 @@
|
|||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Config/WatchApp.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Config/WatchApp.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 27;
|
CURRENT_PROJECT_VERSION = 28;
|
||||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@@ -663,7 +663,7 @@
|
|||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 27;
|
CURRENT_PROJECT_VERSION = 28;
|
||||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@@ -728,7 +728,7 @@
|
|||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 27;
|
CURRENT_PROJECT_VERSION = 28;
|
||||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@@ -826,7 +826,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Config/App.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Config/App.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 27;
|
CURRENT_PROJECT_VERSION = 28;
|
||||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = "Config/App-Info.plist";
|
INFOPLIST_FILE = "Config/App-Info.plist";
|
||||||
@@ -851,7 +851,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Config/App.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Config/App.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 27;
|
CURRENT_PROJECT_VERSION = 28;
|
||||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = "Config/App-Info.plist";
|
INFOPLIST_FILE = "Config/App-Info.plist";
|
||||||
@@ -875,7 +875,7 @@
|
|||||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Config/Widget.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Config/Widget.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 27;
|
CURRENT_PROJECT_VERSION = 28;
|
||||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = "Config/Widget-Info.plist";
|
INFOPLIST_FILE = "Config/Widget-Info.plist";
|
||||||
@@ -900,7 +900,7 @@
|
|||||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Config/Widget.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Config/Widget.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 27;
|
CURRENT_PROJECT_VERSION = 28;
|
||||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = "Config/Widget-Info.plist";
|
INFOPLIST_FILE = "Config/Widget-Info.plist";
|
||||||
|
|||||||
+29
-24
@@ -166,9 +166,8 @@ struct AlertsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// An option row that belongs to a toggle. When the toggle is off the control
|
/// An option row that belongs to a toggle. When the toggle is off the control
|
||||||
/// is replaced by a dimmed, static value row: `.disabled` is not enough here
|
/// is replaced by a static value row. The enclosing dependent group applies
|
||||||
/// because an inline-style picker in a List still expands its options when
|
/// FuelBoard's shared disabled interaction and appearance once.
|
||||||
/// tapped while disabled.
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func optionRow<Content: View>(_ title: String,
|
private func optionRow<Content: View>(_ title: String,
|
||||||
value: String,
|
value: String,
|
||||||
@@ -182,7 +181,6 @@ struct AlertsView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
Text(value)
|
Text(value)
|
||||||
}
|
}
|
||||||
.foregroundStyle(.tertiary)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,28 +189,30 @@ struct AlertsView: View {
|
|||||||
List {
|
List {
|
||||||
Section {
|
Section {
|
||||||
Toggle(String(localized: "Cheapest favourite", comment: "Favourite price-drop alert toggle"), isOn: $favouriteDropEnabled)
|
Toggle(String(localized: "Cheapest favourite", comment: "Favourite price-drop alert toggle"), isOn: $favouriteDropEnabled)
|
||||||
optionRow(String(localized: "Fuel", comment: "Fuel picker title"),
|
Group {
|
||||||
value: favouriteDropFuel.displayName,
|
optionRow(String(localized: "Fuel", comment: "Fuel picker title"),
|
||||||
isEnabled: favouriteDropEnabled) {
|
value: favouriteDropFuel.displayName,
|
||||||
Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $favouriteDropFuel) {
|
isEnabled: favouriteDropEnabled) {
|
||||||
ForEach(FuelType.allCases) { fuel in
|
Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $favouriteDropFuel) {
|
||||||
Text(fuel.displayName).tag(fuel)
|
ForEach(FuelType.allCases) { fuel in
|
||||||
|
Text(fuel.displayName).tag(fuel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Button(String(localized: "Check now", comment: "Manual favourite price-drop check button"), action: onCheckFavouriteDropNow)
|
||||||
|
Text(String(localized: "Get notified when a different favourite becomes the cheapest, or your cheapest favourite drops in price.", comment: "Favourite price alerts section description"))
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
Button(String(localized: "Check now", comment: "Manual favourite price-drop check button"), action: onCheckFavouriteDropNow)
|
.fuelBoardDisabledState(isEnabled: favouriteDropEnabled)
|
||||||
.disabled(!favouriteDropEnabled)
|
|
||||||
.allowsHitTesting(favouriteDropEnabled)
|
|
||||||
Text(String(localized: "Get notified when a different favourite becomes the cheapest, or your cheapest favourite drops in price.", comment: "Favourite price alerts section description"))
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
} header: {
|
} header: {
|
||||||
Text(String(localized: "Favourite price alerts", comment: "Favourite price alerts section title"))
|
Text(String(localized: "Favourite price alerts", comment: "Favourite price alerts section title"))
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Toggle(String(localized: "Cheapest station", comment: "Proximity alert toggle"), isOn: $enabled)
|
Toggle(String(localized: "Cheapest station", comment: "Proximity alert toggle"), isOn: $enabled)
|
||||||
optionRow(String(localized: "Alert type", comment: "Proximity alert type picker title"),
|
Group {
|
||||||
|
optionRow(String(localized: "Alert type", comment: "Proximity alert type picker title"),
|
||||||
value: alertMode.displayName,
|
value: alertMode.displayName,
|
||||||
isEnabled: enabled) {
|
isEnabled: enabled) {
|
||||||
Picker(String(localized: "Alert type", comment: "Proximity alert type picker title"), selection: $alertMode) {
|
Picker(String(localized: "Alert type", comment: "Proximity alert type picker title"), selection: $alertMode) {
|
||||||
@@ -265,9 +265,11 @@ struct AlertsView: View {
|
|||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
Text(String(localized: "Background alerts depend on when your iPhone allows them; each station alerts at most once an hour.", comment: "Proximity alerts background limitation note"))
|
Text(String(localized: "Background alerts depend on when your iPhone allows them; each station alerts at most once an hour.", comment: "Proximity alerts background limitation note"))
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.fuelBoardDisabledState(isEnabled: enabled)
|
||||||
} header: {
|
} header: {
|
||||||
Text(String(localized: "Proximity alerts", comment: "Proximity alerts section title"))
|
Text(String(localized: "Proximity alerts", comment: "Proximity alerts section title"))
|
||||||
}
|
}
|
||||||
@@ -281,7 +283,8 @@ struct AlertsView: View {
|
|||||||
|
|
||||||
Section {
|
Section {
|
||||||
Toggle(String(localized: "Cheapest station", comment: "Live Activity toggle"), isOn: $liveActivityEnabled)
|
Toggle(String(localized: "Cheapest station", comment: "Live Activity toggle"), isOn: $liveActivityEnabled)
|
||||||
optionRow(String(localized: "Fuel", comment: "Fuel picker title"),
|
Group {
|
||||||
|
optionRow(String(localized: "Fuel", comment: "Fuel picker title"),
|
||||||
value: liveActivityFuel.displayName,
|
value: liveActivityFuel.displayName,
|
||||||
isEnabled: liveActivityEnabled) {
|
isEnabled: liveActivityEnabled) {
|
||||||
Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $liveActivityFuel) {
|
Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $liveActivityFuel) {
|
||||||
@@ -305,9 +308,11 @@ struct AlertsView: View {
|
|||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
Text(String(localized: "See the cheapest nearby station on your Lock Screen and Dynamic Island, with updates as you travel.", comment: "Live Activity section description"))
|
Text(String(localized: "See the cheapest nearby station on your Lock Screen and Dynamic Island, with updates as you travel.", comment: "Live Activity section description"))
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.fuelBoardDisabledState(isEnabled: liveActivityEnabled)
|
||||||
} header: {
|
} header: {
|
||||||
Text(String(localized: "Live Activity", comment: "Live Activity section title"))
|
Text(String(localized: "Live Activity", comment: "Live Activity section title"))
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-31
@@ -1842,26 +1842,13 @@ struct StationRow: View {
|
|||||||
/// 22pt keeps the price the row's loudest element after the station name.
|
/// 22pt keeps the price the row's loudest element after the station name.
|
||||||
@ScaledMetric(relativeTo: .title2) private var priceSize: CGFloat = 22
|
@ScaledMetric(relativeTo: .title2) private var priceSize: CGFloat = 22
|
||||||
|
|
||||||
/// The identity column's share of the ROW, for every row and both price
|
|
||||||
/// styles. The mockup's divider sits at ~53% of its row; this is that ratio,
|
|
||||||
/// held per row rather than derived from the row's text — the point is that
|
|
||||||
/// the hairline, the price axis and the star cannot move as the list scrolls.
|
|
||||||
/// The price column takes the remainder, and its savings capsule scales down
|
|
||||||
/// instead of widening the column and dragging the hairline with it.
|
|
||||||
///
|
|
||||||
/// One value for both price styles: the star used to sit at the end of the
|
|
||||||
/// price line and hold ~28pt of the price column, so the wide form `£1.58⁷/L`
|
|
||||||
/// — ~20pt wider than the bare `158.7` at the same point size — did not fit on
|
|
||||||
/// a 393pt screen. With the star now under the station logo the price column
|
|
||||||
/// has that width back, so the identity column no longer has to give width up
|
|
||||||
/// in pounds & pence, and names keep the same room in both styles.
|
|
||||||
private static let leftSectionShare: CGFloat = 0.52
|
|
||||||
|
|
||||||
/// The comparison label sits beside the capsule only where the row is wide
|
/// The comparison label sits beside the capsule only where the row is wide
|
||||||
/// enough for both (regular width). On an iPhone the mockup's canvas is
|
/// enough for both (regular width). On an iPhone the mockup's canvas is
|
||||||
/// wider than the whole screen, so the label drops under the capsule rather
|
/// wider than the whole screen, so the label drops under the capsule rather
|
||||||
/// than starving the station name.
|
/// than starving the station name.
|
||||||
@Environment(\.horizontalSizeClass) private var sizeClass
|
@Environment(\.horizontalSizeClass) private var sizeClass
|
||||||
|
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
||||||
|
|
||||||
private var ragColor: Color {
|
private var ragColor: Color {
|
||||||
guard let price = station.prices[fuel], let baselinePrice else { return .gray }
|
guard let price = station.prices[fuel], let baselinePrice else { return .gray }
|
||||||
@@ -1931,13 +1918,9 @@ struct StationRow: View {
|
|||||||
// the detail sheet instead of being repeated on every row.
|
// the detail sheet instead of being repeated on every row.
|
||||||
HStack(alignment: .center, spacing: 12) {
|
HStack(alignment: .center, spacing: 12) {
|
||||||
// LEFT SECTION — station identity: brand logo, then name, address and
|
// LEFT SECTION — station identity: brand logo, then name, address and
|
||||||
// distance. Given a FIXED SHARE of the row (~52%, the mockup's ratio)
|
// distance. This section yields horizontal space to the financial
|
||||||
// rather than sizing to its content: sized to content, this column
|
// column first: a truncated station identity is recoverable from the
|
||||||
// grew and shrank with each row's badge and savings copy, which moved
|
// detail sheet, while a truncated saving is materially misleading.
|
||||||
// the hairline 23pt between adjacent rows (x767 on a BEST VALUE row,
|
|
||||||
// x699 on a "costs more" row) — so the divider and the price axis
|
|
||||||
// both jumped sideways as the list scrolled. One share for every row
|
|
||||||
// puts them on a single vertical line down the whole list.
|
|
||||||
HStack(alignment: .top, spacing: 12) {
|
HStack(alignment: .top, spacing: 12) {
|
||||||
// The logo, with the favourite star directly beneath it. Moving
|
// The logo, with the favourite star directly beneath it. Moving
|
||||||
// the star out of the price line is what gives the price column
|
// the star out of the price line is what gives the price column
|
||||||
@@ -1972,16 +1955,15 @@ struct StationRow: View {
|
|||||||
|
|
||||||
// Name, full address, distance
|
// Name, full address, distance
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
// The name WRAPS instead of truncating: the row is tall
|
// Keep the established compact row. Long names yield space
|
||||||
// enough now, and a name cut to "Z A Service Stat…" cannot
|
// to the complete price/verdict column rather than wrapping
|
||||||
// be told apart from the station next door. Baseline
|
// and forcing a financial amount to truncate.
|
||||||
// alignment keeps the notice symbols on the name's FIRST
|
|
||||||
// line instead of floating beside a two-line name.
|
|
||||||
HStack(alignment: .firstTextBaseline, spacing: 6) {
|
HStack(alignment: .firstTextBaseline, spacing: 6) {
|
||||||
Text(station.name)
|
Text(station.name)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
ForEach(StationNotice.notices(for: station)) { notice in
|
ForEach(StationNotice.notices(for: station)) { notice in
|
||||||
Image(systemName: notice.symbolName)
|
Image(systemName: notice.symbolName)
|
||||||
.font(.caption2.weight(.semibold))
|
.font(.caption2.weight(.semibold))
|
||||||
@@ -2019,11 +2001,10 @@ struct StationRow: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.layoutPriority(0)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.containerRelativeFrame(.horizontal, alignment: .leading) { length, _ in
|
.layoutPriority(0)
|
||||||
length * Self.leftSectionShare
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1pt hairline between the two sections. Inset top and bottom so it
|
// 1pt hairline between the two sections. Inset top and bottom so it
|
||||||
// spans the row's content without touching the row edges, and
|
// spans the row's content without touching the row edges, and
|
||||||
@@ -2075,7 +2056,12 @@ struct StationRow: View {
|
|||||||
savingsCapsule(result)
|
savingsCapsule(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
// The verdict owns its natural one-line width at normal Dynamic Type
|
||||||
|
// sizes. The identity column compresses first, preventing "Approx.
|
||||||
|
// Save £…" while keeping the same two-column visual design.
|
||||||
|
.fixedSize(horizontal: !dynamicTypeSize.isAccessibilitySize,
|
||||||
|
vertical: false)
|
||||||
|
.layoutPriority(1)
|
||||||
// The spec's ~20-24pt of air between the divider and the price
|
// The spec's ~20-24pt of air between the divider and the price
|
||||||
// column: the row's own 12pt spacing plus this 10pt.
|
// column: the row's own 12pt spacing plus this 10pt.
|
||||||
.padding(.leading, 10)
|
.padding(.leading, 10)
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Applies FuelBoard's shared unavailable appearance and native interaction state
|
||||||
|
/// to a complete dependent row or group. Apply it once at the highest dependent
|
||||||
|
/// container so labels, values, icons, descriptions and controls fade together.
|
||||||
|
extension View {
|
||||||
|
func fuelBoardDisabledState(isEnabled: Bool) -> some View {
|
||||||
|
disabled(!isEnabled)
|
||||||
|
.allowsHitTesting(isEnabled)
|
||||||
|
.opacity(isEnabled ? 1 : 0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -105,25 +105,26 @@ struct FuelSavingsCapsule: View {
|
|||||||
let result: FuelSavingsResult
|
let result: FuelSavingsResult
|
||||||
var compact: Bool = true
|
var compact: Bool = true
|
||||||
|
|
||||||
|
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: compact ? 3 : 5) {
|
HStack(spacing: compact ? 3 : 5) {
|
||||||
Image(systemName: Self.symbol(for: result.status))
|
Image(systemName: Self.symbol(for: result.status))
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
Text(result.capsuleHeadline())
|
Text(result.capsuleHeadline())
|
||||||
.lineLimit(1)
|
// Financial verdicts stay complete and single-line at ordinary
|
||||||
// Measured on a 393pt screen: the capsule is handed ~101pt, of
|
// text sizes. Accessibility sizes may use a second line rather
|
||||||
// which the icon, its spacing and the 8pt padding either side
|
// than clipping, shrinking or losing the amount to an ellipsis.
|
||||||
// take 32pt, leaving ~69pt for the text. "Closest station" needs
|
.lineLimit(dynamicTypeSize.isAccessibilitySize ? 2 : 1)
|
||||||
// ~90pt at full size, so a 0.8 floor — 72pt — could not reach it
|
.fixedSize(horizontal: !dynamicTypeSize.isAccessibilitySize,
|
||||||
// and the row showed "Closest stati…" instead. 0.7 leaves ~63pt,
|
vertical: true)
|
||||||
// which clears it at ~10pt type: scaled, never truncated.
|
|
||||||
.minimumScaleFactor(0.7)
|
|
||||||
}
|
}
|
||||||
.font(compact ? .caption.weight(.semibold) : .headline)
|
.font(compact ? .caption.weight(.semibold) : .headline)
|
||||||
.foregroundStyle(Self.color(for: result.status))
|
.foregroundStyle(Self.color(for: result.status))
|
||||||
.padding(.horizontal, compact ? 8 : 12)
|
.padding(.horizontal, compact ? 8 : 12)
|
||||||
.padding(.vertical, compact ? 4 : 7)
|
.padding(.vertical, compact ? 4 : 7)
|
||||||
.background(Capsule().fill(Self.color(for: result.status).opacity(0.15)))
|
.background(Capsule().fill(Self.color(for: result.status).opacity(0.15)))
|
||||||
|
.accessibilityElement(children: .combine)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verdict colour: green save, red worse off, grey break even, blue
|
/// Verdict colour: green save, red worse off, grey break even, blue
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ struct SettingsView: View {
|
|||||||
.foregroundStyle(.orange)
|
.foregroundStyle(.orange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.settingsDependencyEnabled(fuelSavingsSettings.enabled)
|
.fuelBoardDisabledState(isEnabled: fuelSavingsSettings.enabled)
|
||||||
} header: {
|
} header: {
|
||||||
Text("Fuel Savings")
|
Text("Fuel Savings")
|
||||||
} footer: {
|
} footer: {
|
||||||
@@ -582,7 +582,7 @@ struct SettingsView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.settingsDependencyEnabled(!tipStore.purchaseInProgress)
|
.fuelBoardDisabledState(isEnabled: !tipStore.purchaseInProgress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowBackground(Color.clear)
|
.listRowBackground(Color.clear)
|
||||||
@@ -925,16 +925,6 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Applies the native unavailable state to a complete dependent settings group
|
|
||||||
/// and also dims non-control content (read-outs, icons and explanatory text),
|
|
||||||
/// which does not automatically adopt a disabled appearance from SwiftUI.
|
|
||||||
private extension View {
|
|
||||||
func settingsDependencyEnabled(_ isEnabled: Bool) -> some View {
|
|
||||||
disabled(!isEnabled)
|
|
||||||
.opacity(isEnabled ? 1 : 0.65)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads the three tip tiers and drives their purchases.
|
/// Loads the three tip tiers and drives their purchases.
|
||||||
@MainActor
|
@MainActor
|
||||||
final class TipStore: ObservableObject {
|
final class TipStore: ObservableObject {
|
||||||
|
|||||||
Reference in New Issue
Block a user