Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a9b5df4f1 | ||
|
|
1cd4d86a69 | ||
|
|
d2143d255f | ||
|
|
31eb441d0e | ||
|
|
e5a2b2ebdb |
+146
-63
@@ -6,8 +6,8 @@ import SwiftUI
|
||||
/// versa). The Live Activity section also lives here — it mirrors this page's
|
||||
/// shape (toggle + fuel + distance) so the two "notify me while driving"
|
||||
/// surfaces sit together. Geofence plumbing (count, 18-limit) lives in
|
||||
/// Settings → Debug; the user-facing page keeps a plain-language explainer
|
||||
/// plus a 0-stations hint.
|
||||
/// Settings → Debug; the user-facing page keeps short descriptions that follow
|
||||
/// the selected alert type and radius, plus a 0-stations hint.
|
||||
struct AlertsView: View {
|
||||
@Binding var enabled: Bool
|
||||
@Binding var radius: Double // stored in km (monitor + storage)
|
||||
@@ -55,31 +55,40 @@ struct AlertsView: View {
|
||||
return String(format: String(localized: "%lld %@", comment: "Distance label"), shown, distanceUnit.label(for: Double(shown)))
|
||||
}
|
||||
|
||||
/// Situational guidance for the currently-selected alert radius, shown
|
||||
/// under the picker so users know what each choice is best for. Labels
|
||||
/// mirror the picker row text (unit-aware) and explain the trade-off:
|
||||
/// smaller radii ping more often but later; larger radii ping rarely in
|
||||
/// town but earlier from afar.
|
||||
private var alertRadiusGuidance: String {
|
||||
/// The distance the geofence actually caps "Follow search" at, in miles
|
||||
/// (FuelStore stores the cap in km so the monitor and storage are unit-free).
|
||||
private var followSearchCapMiles: Int {
|
||||
Int((FuelStore.alertFollowCapKM / FuelSavingsCalculator.kilometresPerMile).rounded())
|
||||
}
|
||||
|
||||
/// What the selected alert type actually does. Swapped with the picker so
|
||||
/// the copy can never describe the wrong mode.
|
||||
private var alertModeDescription: String {
|
||||
switch alertMode {
|
||||
case .cheapest:
|
||||
return String(localized: "Get notified when you're near one of the cheapest stations for your chosen fuel.", comment: "Cheapest station proximity alert description")
|
||||
case .bestValue:
|
||||
return String(localized: "Get notified when you're near a station that could save you money after travel costs.", comment: "Best Value proximity alert description")
|
||||
}
|
||||
}
|
||||
|
||||
/// The radius the selected option really monitors, stated in the user's
|
||||
/// unit — e.g. "Get alerts when you're within 2 miles of a matching
|
||||
/// station." "Follow search" mirrors the Stations-tab distance, which the
|
||||
/// geofence caps; said plainly, and only when the search distance really
|
||||
/// exceeds the cap, so the sentence never contradicts the picker row.
|
||||
private var alertRadiusDescription: String {
|
||||
switch alertChoice.wrappedValue {
|
||||
case .followSearch:
|
||||
return String(localized: "Set-and-forget: alerts behave exactly like the Stations-tab search distance (capped at 8 miles for reliable geofencing).", comment: "Alert radius guidance for following search distance")
|
||||
case .fixed(let value):
|
||||
let optionLabel = String(format: String(localized: "%lld %@", comment: "Distance label"), value, distanceUnit.label(for: Double(value)))
|
||||
let body: String
|
||||
switch value {
|
||||
case 1:
|
||||
body = String(localized: "Town driving. Fires most often — you're rarely inside the circle for long, so pings are meaningful. Best for stop-start local trips.", comment: "Alert radius guidance")
|
||||
case 2:
|
||||
body = String(localized: "City or suburb commute. Slightly earlier heads-up than the smallest option; still fires regularly around town.", comment: "Alert radius guidance")
|
||||
case 3:
|
||||
body = String(localized: "Mixed driving. Middle ground: heads-up a few minutes out, still works in most towns.", comment: "Alert radius guidance")
|
||||
case 5:
|
||||
body = String(localized: "A-roads and motorway exits. Wide early warning for longer trips — but in town it rarely fires, because you're usually already inside the circle.", comment: "Alert radius guidance")
|
||||
default:
|
||||
body = String(localized: "Long-distance journeys. Maximum reach: a ping when a cheap station is coming up from far out. In town this essentially never fires.", comment: "Alert radius guidance")
|
||||
let capped = stationLimit > followSearchCapMiles
|
||||
let label = distanceLabel(forMiles: capped ? followSearchCapMiles : stationLimit)
|
||||
if capped {
|
||||
return String(format: String(localized: "Get alerts when you're within %@ of a matching station (your search distance is capped for reliable alerts).", comment: "Proximity alert radius description, follow search capped"), label)
|
||||
}
|
||||
return String(format: String(localized: "%@ — %@", comment: "Alert radius guidance with option label"), optionLabel, body)
|
||||
return String(format: String(localized: "Get alerts when you're within %@ of a matching station.", comment: "Proximity alert radius description"), label)
|
||||
case .fixed(let value):
|
||||
let label = String(format: String(localized: "%lld %@", comment: "Distance label"), value, distanceUnit.label(for: Double(value)))
|
||||
return String(format: String(localized: "Get alerts when you're within %@ of a matching station.", comment: "Proximity alert radius description"), label)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,49 +145,89 @@ struct AlertsView: View {
|
||||
)
|
||||
}
|
||||
|
||||
/// The radius currently selected, in the user's unit — shown by the disabled
|
||||
/// row in place of the picker.
|
||||
private var alertRadiusValueLabel: String {
|
||||
switch alertChoice.wrappedValue {
|
||||
case .followSearch:
|
||||
return String(format: String(localized: "Follow search (%@)", comment: "Follow current station search distance option"), searchInUnitLabel)
|
||||
case .fixed(let value):
|
||||
return String(format: String(localized: "%lld %@", comment: "Distance label"), value, distanceUnit.label(for: Double(value)))
|
||||
}
|
||||
}
|
||||
|
||||
private var laDistanceValueLabel: String {
|
||||
switch laChoice.wrappedValue {
|
||||
case .followSearch:
|
||||
return String(format: String(localized: "Follow search (%@)", comment: "Follow current station search distance option"), searchInUnitLabel)
|
||||
case .fixed(let miles):
|
||||
return distanceLabel(forMiles: miles)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// because an inline-style picker in a List still expands its options when
|
||||
/// tapped while disabled.
|
||||
@ViewBuilder
|
||||
private func optionRow<Content: View>(_ title: String,
|
||||
value: String,
|
||||
isEnabled: Bool,
|
||||
@ViewBuilder picker: () -> Content) -> some View {
|
||||
if isEnabled {
|
||||
picker()
|
||||
} else {
|
||||
HStack {
|
||||
Text(title)
|
||||
Spacer()
|
||||
Text(value)
|
||||
}
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section {
|
||||
Toggle(String(localized: "Cheapest favourite", comment: "Favourite price-drop alert toggle"), isOn: $favouriteDropEnabled)
|
||||
Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $favouriteDropFuel) {
|
||||
ForEach(FuelType.allCases) { fuel in
|
||||
Text(fuel.displayName).tag(fuel)
|
||||
optionRow(String(localized: "Fuel", comment: "Fuel picker title"),
|
||||
value: favouriteDropFuel.displayName,
|
||||
isEnabled: favouriteDropEnabled) {
|
||||
Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $favouriteDropFuel) {
|
||||
ForEach(FuelType.allCases) { fuel in
|
||||
Text(fuel.displayName).tag(fuel)
|
||||
}
|
||||
}
|
||||
}
|
||||
Button(String(localized: "Check now", comment: "Manual favourite price-drop check button"), action: onCheckFavouriteDropNow)
|
||||
.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: {
|
||||
Text(String(localized: "Price-drop alerts", comment: "Favourite price-drop alerts section title"))
|
||||
} footer: {
|
||||
Text(String(localized: "Watches your cheapest saved favourite for the fuel you pick here. FuelBoard alerts when a different favourite becomes the cheapest, or when your current cheapest favourite drops by at least 1.0p. This is separate from the nearby-geofence alert, so you can monitor favourites without extra location noise.", comment: "Favourite price-drop alerts section footer"))
|
||||
Text(String(localized: "Favourite price alerts", comment: "Favourite price alerts section title"))
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle(String(localized: "Cheapest station", comment: "Proximity alert toggle"), isOn: $enabled)
|
||||
Picker(String(localized: "Alert type", comment: "Proximity alert type picker title"), selection: $alertMode) {
|
||||
ForEach(AlertMode.allCases.filter { mode in mode != .bestValue || bestValueConfigured }) { mode in
|
||||
Text(mode.displayName).tag(mode)
|
||||
optionRow(String(localized: "Alert type", comment: "Proximity alert type picker title"),
|
||||
value: alertMode.displayName,
|
||||
isEnabled: enabled) {
|
||||
Picker(String(localized: "Alert type", comment: "Proximity alert type picker title"), selection: $alertMode) {
|
||||
ForEach(AlertMode.allCases.filter { mode in mode != .bestValue || bestValueConfigured }) { mode in
|
||||
Text(mode.displayName).tag(mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !bestValueConfigured {
|
||||
Text(String(localized: "Set up Best Value first so Best Value alerts can account for your fill size, MPG and travel cost.", comment: "Best Value alert unavailable hint"))
|
||||
.font(.caption)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $fuel) {
|
||||
ForEach(FuelType.allCases) { fuel in
|
||||
Text(fuel.displayName).tag(fuel)
|
||||
}
|
||||
}
|
||||
Picker(String(localized: "Radius", comment: "Alert radius picker title"), selection: alertChoice) {
|
||||
Text(String(format: String(localized: "Follow search (%@)", comment: "Follow current station search distance option"), searchInUnitLabel)).tag(AlertRadiusChoice.followSearch)
|
||||
ForEach(FuelStore.alertRadiusOptions, id: \.self) { value in
|
||||
Text(String(format: String(localized: "%lld %@", comment: "Distance label"), value, distanceUnit.label(for: Double(value)))).tag(AlertRadiusChoice.fixed(value))
|
||||
}
|
||||
}
|
||||
Text(alertRadiusGuidance)
|
||||
.font(.caption)
|
||||
Text(alertModeDescription)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
if alertMode == .bestValue {
|
||||
// The threshold is edited in Settings → Fuel Savings: it
|
||||
@@ -186,21 +235,41 @@ struct AlertsView: View {
|
||||
// alert, so it lives with the fill size and MPG that
|
||||
// produce the saving. Read back here, not offered twice.
|
||||
Text(String(format: String(localized: "Minimum saving %@ — change in Settings, Fuel Savings.", comment: "Best Value alert threshold read-back"), FuelSavingsFormat.money(bestValueThresholdGBP)))
|
||||
.font(.caption)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
optionRow(String(localized: "Fuel", comment: "Fuel picker title"),
|
||||
value: fuel.displayName,
|
||||
isEnabled: enabled) {
|
||||
Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $fuel) {
|
||||
ForEach(FuelType.allCases) { fuel in
|
||||
Text(fuel.displayName).tag(fuel)
|
||||
}
|
||||
}
|
||||
}
|
||||
optionRow(String(localized: "Radius", comment: "Alert radius picker title"),
|
||||
value: alertRadiusValueLabel,
|
||||
isEnabled: enabled) {
|
||||
Picker(String(localized: "Radius", comment: "Alert radius picker title"), selection: alertChoice) {
|
||||
Text(String(format: String(localized: "Follow search (%@)", comment: "Follow current station search distance option"), searchInUnitLabel)).tag(AlertRadiusChoice.followSearch)
|
||||
ForEach(FuelStore.alertRadiusOptions, id: \.self) { value in
|
||||
Text(String(format: String(localized: "%lld %@", comment: "Distance label"), value, distanceUnit.label(for: Double(value)))).tag(AlertRadiusChoice.fixed(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(alertRadiusDescription)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
if enabled && monitoredCount == 0 {
|
||||
Text(String(localized: "No stations monitored yet — open the Stations tab to load prices first.", comment: "No monitored stations hint"))
|
||||
.font(.caption)
|
||||
.font(.footnote)
|
||||
.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"))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
} header: {
|
||||
Text(String(localized: "Proximity alerts", comment: "Proximity alerts section title"))
|
||||
} footer: {
|
||||
let modeDescription = alertMode == .bestValue
|
||||
? String(format: String(localized: "When you approach the Best Value station within the radius, FuelBoard sends a notification if it could leave you at least %@ better off after extra return travel. Nearby stations selling %@ are watched — favourites get priority — and each station alerts at most once per hour.", comment: "Best Value proximity alerts section footer"), FuelSavingsFormat.money(bestValueThresholdGBP), fuel.displayName.lowercased())
|
||||
: String(format: String(localized: "When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Nearby stations selling %@ are watched — favourites get priority — and each station alerts at most once per hour. Follow search mirrors the Stations-tab distance, capped at 8 miles for reliable geofencing. Smaller radii ping more often but later; larger radii ping rarely in town but earlier from afar.", comment: "Proximity alerts section footer"), fuel.displayName.lowercased())
|
||||
Text(modeDescription)
|
||||
}
|
||||
|
||||
if enabled, let lastAlert {
|
||||
@@ -212,21 +281,35 @@ struct AlertsView: View {
|
||||
|
||||
Section {
|
||||
Toggle(String(localized: "Cheapest station", comment: "Live Activity toggle"), isOn: $liveActivityEnabled)
|
||||
Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $liveActivityFuel) {
|
||||
ForEach(FuelType.allCases) { fuel in
|
||||
Text(fuel.displayName).tag(fuel)
|
||||
optionRow(String(localized: "Fuel", comment: "Fuel picker title"),
|
||||
value: liveActivityFuel.displayName,
|
||||
isEnabled: liveActivityEnabled) {
|
||||
Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $liveActivityFuel) {
|
||||
ForEach(FuelType.allCases) { fuel in
|
||||
Text(fuel.displayName).tag(fuel)
|
||||
}
|
||||
}
|
||||
}
|
||||
Picker(String(localized: "Distance", comment: "Distance picker title"), selection: laChoice) {
|
||||
Text(String(format: String(localized: "Follow search (%@)", comment: "Follow current station search distance option"), searchInUnitLabel)).tag(LADistanceChoice.followSearch)
|
||||
ForEach(FuelStore.stationRadiusOptions, id: \.self) { miles in
|
||||
Text(distanceLabel(forMiles: miles)).tag(LADistanceChoice.fixed(miles))
|
||||
optionRow(String(localized: "Distance", comment: "Distance picker title"),
|
||||
value: laDistanceValueLabel,
|
||||
isEnabled: liveActivityEnabled) {
|
||||
Picker(String(localized: "Distance", comment: "Distance picker title"), selection: laChoice) {
|
||||
Text(String(format: String(localized: "Follow search (%@)", comment: "Follow current station search distance option"), searchInUnitLabel)).tag(LADistanceChoice.followSearch)
|
||||
ForEach(FuelStore.stationRadiusOptions, id: \.self) { miles in
|
||||
Text(distanceLabel(forMiles: miles)).tag(LADistanceChoice.fixed(miles))
|
||||
}
|
||||
}
|
||||
}
|
||||
if liveActivityFollowsSearch {
|
||||
Text(String(localized: "Uses the same distance as your station search.", comment: "Live Activity distance description, follow search"))
|
||||
.font(.footnote)
|
||||
.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"))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
} header: {
|
||||
Text(String(localized: "Live Activity", comment: "Live Activity section title"))
|
||||
} footer: {
|
||||
Text(String(localized: "Shows the cheapest station for the fuel and distance you pick here on the Lock Screen and Dynamic Island — independent of the Stations tab (choose Follow search to mirror its distance instead). Updates as you drive; tap to open directions. Live Activities can be disabled in System Settings → Live Activities.", comment: "Live Activity section footer"))
|
||||
}
|
||||
}
|
||||
.navigationTitle(String(localized: "Alerts", comment: "Alerts tab title"))
|
||||
|
||||
@@ -83,7 +83,7 @@ struct BestValueIntroContent: View {
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
slider(
|
||||
title: String(localized: "Typical fill-up", comment: "Best Value fill-up slider label"),
|
||||
title: String(localized: "Fill-up amount", comment: "Best Value fill-up slider label"),
|
||||
readOut: fillUpText,
|
||||
value: amountBinding,
|
||||
range: FuelSavingsCalculator.fillAmountSliderRange(for: settings.preferredAmount.unit),
|
||||
|
||||
@@ -132,7 +132,7 @@ enum RoadDistanceService {
|
||||
// visible to show for it: no pass, no requests, no widget reloads.
|
||||
guard FuelStore.accurateDistancesEnabled else {
|
||||
await updateDebug(baseDebugStatus(state: .gated,
|
||||
reason: "Accurate distances off",
|
||||
reason: "Driving distances off",
|
||||
stations: stations,
|
||||
lat: lat,
|
||||
lng: lng,
|
||||
|
||||
@@ -70,7 +70,7 @@ struct SettingsView: View {
|
||||
/// Hidden developer flag — the Debug section only appears when on. Toggled
|
||||
/// by tapping the About → Version row five times (NOT a user-facing switch).
|
||||
@State private var debugMode: Bool = FuelStore.loadDebugMode()
|
||||
/// "Accurate distances" — read once on appear and written straight through to
|
||||
/// "Driving distances" — read once on appear and written straight through to
|
||||
/// the shared store, which is what every display surface actually consults.
|
||||
@State private var accurateDistances: Bool = FuelStore.accurateDistancesEnabled
|
||||
/// Slider drafts — the value under the thumb while it is down. `nil` means
|
||||
@@ -92,7 +92,6 @@ struct SettingsView: View {
|
||||
/// Return key, so the keyboard's Done button clears this to dismiss AND
|
||||
/// commit the typed value. Both Fuel Savings controls are sliders now, so
|
||||
/// nothing sets it — it stays for any numeric field this screen takes again.
|
||||
@State private var settingsUnitChangeNotice: String?
|
||||
@FocusState private var focusedSavingsField: SavingsField?
|
||||
|
||||
private enum SavingsField: Hashable {
|
||||
@@ -124,6 +123,9 @@ struct SettingsView: View {
|
||||
List {
|
||||
Section {
|
||||
Toggle("Best Value", isOn: fuelSavingsEnabledBinding)
|
||||
Text("Find stations that could save you money, even after the cost of driving there.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
// The feature's own controls follow its toggle: switched
|
||||
// off they grey out but stay on screen (same rule as the
|
||||
// distance picker in Closest mode) and keep their values, so
|
||||
@@ -150,6 +152,9 @@ struct SettingsView: View {
|
||||
}
|
||||
.accessibilityLabel("Average fuel economy")
|
||||
.accessibilityValue(mpgText)
|
||||
Text("How many miles your car travels per gallon.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
Picker("Fill-up unit", selection: fuelSavingsUnitBinding) {
|
||||
ForEach(FuelSavingsFillUnit.allCases) { unit in
|
||||
Text(unit.displayName).tag(unit)
|
||||
@@ -157,7 +162,7 @@ struct SettingsView: View {
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
HStack {
|
||||
Text("Typical fill-up")
|
||||
Text("Fill-up amount")
|
||||
Spacer()
|
||||
Text(fillUpText)
|
||||
.monospacedDigit()
|
||||
@@ -168,8 +173,11 @@ struct SettingsView: View {
|
||||
step: FuelSavingsCalculator.fillAmountSliderStep(for: fuelSavingsSettings.preferredAmount.unit)) { editing in
|
||||
if !editing { commitFillUp() }
|
||||
}
|
||||
.accessibilityLabel("Typical fill-up")
|
||||
.accessibilityLabel("Fill-up amount")
|
||||
.accessibilityValue(fillUpText)
|
||||
Text(fillUpAmountDescription)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
Picker("Minimum saving", selection: $bestValueThresholdGBP) {
|
||||
ForEach(FuelStore.bestValueAlertThresholdOptionsGBP, id: \.self) { value in
|
||||
Text(FuelSavingsFormat.money(value)).tag(value)
|
||||
@@ -181,17 +189,12 @@ struct SettingsView: View {
|
||||
// needs no handler of its own. The store snaps to the
|
||||
// nearest option, so the picker and the stored value
|
||||
// cannot disagree.
|
||||
Text("A saving worth driving for: the net saving left after the extra travel cost. Stations above it are labelled BEST VALUE in the station list, and Best Value alerts use the same figure.")
|
||||
.font(.caption)
|
||||
Text("The smallest saving you'd consider worth the extra drive.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
if let settingsUnitChangeNotice {
|
||||
Text(settingsUnitChangeNotice)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
if !fuelSavingsSettings.isConfigured && fuelSavingsSettings.enabled {
|
||||
Text("Set a realistic UK MPG and fill-up amount before Best Value appears in sorting.")
|
||||
.font(.caption)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
}
|
||||
@@ -199,7 +202,7 @@ struct SettingsView: View {
|
||||
} header: {
|
||||
Text("Fuel Savings")
|
||||
} footer: {
|
||||
Text("Best Value estimates fuel cost only, using UK imperial MPG, your typical purchase amount, fuel prices and Apple Maps road distances. Real-world average MPG is better than manufacturer figures; the fill-up amount is your usual purchase, not tank capacity.")
|
||||
Text("FuelBoard compares fuel prices and travel costs to find stations that could save you money. Results are estimates based on your settings and driving distance.")
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -214,6 +217,9 @@ struct SettingsView: View {
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
WatchSyncManager.shared.pushSnapshot()
|
||||
}
|
||||
Text("Choose how distances are displayed throughout FuelBoard.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
Picker("Price display", selection: $priceDisplayStyle) {
|
||||
ForEach(PriceDisplayStyle.allCases) { style in
|
||||
Text(style.displayName).tag(style)
|
||||
@@ -225,24 +231,26 @@ struct SettingsView: View {
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
WatchSyncManager.shared.pushSnapshot()
|
||||
}
|
||||
Text("Show prices as 129.9p or £1.299 per litre.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
} header: {
|
||||
Text("Units")
|
||||
} footer: {
|
||||
Text("Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (129.9) or in pounds and pence (£1.29⁹/L).")
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle("Accurate distances", isOn: $accurateDistances)
|
||||
Toggle("Driving distances", isOn: $accurateDistances)
|
||||
.onChange(of: accurateDistances) { _, newValue in
|
||||
FuelStore.setAccurateDistancesEnabled(newValue)
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
NotificationCenter.default.post(name: .fuelBoardAccurateDistancesChanged,
|
||||
object: nil)
|
||||
}
|
||||
Text("Use actual driving distances instead of straight-line estimates. Routes are calculated while the app is open.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
} header: {
|
||||
Text("Distances")
|
||||
} footer: {
|
||||
Text("Off (default): distances are straight-line estimates marked Approx. and the app makes no route requests. On: Apple Maps driving distances for every station in range, filled only while this app is open. Search radii then mean miles of driving.")
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -255,10 +263,11 @@ struct SettingsView: View {
|
||||
.onChange(of: stationTapAction) { _, newValue in
|
||||
FuelStore.saveStationTapAction(newValue)
|
||||
}
|
||||
Text("Choose what happens when you tap a station: get directions or view its details.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
} header: {
|
||||
Text("Tap action")
|
||||
} footer: {
|
||||
Text("Tapping a station either opens directions straight away or shows its details first.")
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -275,14 +284,12 @@ struct SettingsView: View {
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(dataRefreshMode.summary)
|
||||
Text("Background checks are best-effort and happen only when iOS allows, so timing is not exact.")
|
||||
Text("Background updates depend on when your iPhone allows them.")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.font(.footnote)
|
||||
} header: {
|
||||
Text("Data checking")
|
||||
} footer: {
|
||||
Text("Standard saves battery and refreshes price data up to twice daily. Smart checks for newer data more often in the background and only refreshes full prices when an update is available.")
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -291,8 +298,9 @@ struct SettingsView: View {
|
||||
} label: {
|
||||
Label("Show introduction", systemImage: "arrow.clockwise.circle")
|
||||
}
|
||||
} footer: {
|
||||
Text("Replay the welcome screen, including the location and notification permission prompts.")
|
||||
Text("View the welcome screens and set up permissions again.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
if debugMode {
|
||||
@@ -469,7 +477,7 @@ struct SettingsView: View {
|
||||
} header: {
|
||||
Text("Road distance fill")
|
||||
} footer: {
|
||||
Text("Shows whether Accurate distances is running or gated. Distances stay Approx. until Ready is yes for the current origin; gated reasons explain why no fill is happening.")
|
||||
Text("Shows whether Driving distances is running or gated. Distances stay Approx. until Ready is yes for the current origin; gated reasons explain why no fill is happening.")
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -782,7 +790,6 @@ struct SettingsView: View {
|
||||
guard newUnit != fuelSavingsSettings.preferredAmount.unit else { return }
|
||||
fuelSavingsSettings.preferredAmount = FuelSavingsCalculator.newComparisonAmount(
|
||||
afterSwitching: fuelSavingsSettings.preferredAmount, to: newUnit)
|
||||
settingsUnitChangeNotice = "New comparison basis — adjust the \(newUnit.displayName) amount to your intended fill-up."
|
||||
// The unit and the amount move together, so both keys are written.
|
||||
saveFuelSavingsField(.unit)
|
||||
saveFuelSavingsField(.amount)
|
||||
@@ -792,6 +799,15 @@ struct SettingsView: View {
|
||||
)
|
||||
}
|
||||
|
||||
/// Describes the fill-up amount in the unit the user picked, so the copy stays
|
||||
/// true whether the amount is a budget in pounds or a volume in litres.
|
||||
private var fillUpAmountDescription: String {
|
||||
switch fuelSavingsSettings.preferredAmount.unit {
|
||||
case .pounds: return "How much you plan to spend on fuel."
|
||||
case .litres: return "How many litres you usually buy."
|
||||
}
|
||||
}
|
||||
|
||||
/// "45 MPG" / "45.5 MPG". The slider is the only control, so this read-out
|
||||
/// is the one place the figure is stated. It follows the draft while the
|
||||
/// thumb is down.
|
||||
|
||||
@@ -58,19 +58,18 @@
|
||||
"Radius" = "Radius";
|
||||
"%lld %@" = "%lld %@";
|
||||
"Follow search (%@)" = "Follow search (%@)";
|
||||
"When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Nearby stations selling %@ are watched — favourites get priority — and each station alerts at most once per hour. Follow search mirrors the Stations-tab distance, capped at 8 miles for reliable geofencing." = "When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Nearby stations selling %@ are watched — favourites get priority — and each station alerts at most once per hour. Follow search mirrors the Stations-tab distance, capped at 8 miles for reliable geofencing.";
|
||||
"No stations monitored yet — open the Stations tab to load prices first." = "No stations monitored yet — open the Stations tab to load prices first.";
|
||||
"Last alert" = "Last alert";
|
||||
"Live Activity" = "Live Activity";
|
||||
"Shows the cheapest station for the fuel and distance you pick here on the Lock Screen and Dynamic Island — independent of the Stations tab (choose Follow search to mirror its distance instead). Updates as you drive; tap to open directions. Live Activities can be disabled in System Settings → Live Activities." = "Shows the cheapest station for the fuel and distance you pick here on the Lock Screen and Dynamic Island — independent of the Stations tab (choose Follow search to mirror its distance instead). Updates as you drive; tap to open directions. Live Activities can be disabled in System Settings → Live Activities.";
|
||||
|
||||
/* Settings tab */
|
||||
"Units" = "Units";
|
||||
"Price display" = "Price display";
|
||||
"Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (129.9) or in pounds and pence (£1.29⁹/L)." = "Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (129.9) or in pounds and pence (£1.29⁹/L).";
|
||||
"Choose how distances are displayed throughout FuelBoard." = "Choose how distances are displayed throughout FuelBoard.";
|
||||
"Show prices as 129.9p or £1.299 per litre." = "Show prices as 129.9p or £1.299 per litre.";
|
||||
"Tap station" = "Tap station";
|
||||
"Tap action" = "Tap action";
|
||||
"Tapping a station either opens directions straight away or shows its details first." = "Tapping a station either opens directions straight away or shows its details first.";
|
||||
"Choose what happens when you tap a station: get directions or view its details." = "Choose what happens when you tap a station: get directions or view its details.";
|
||||
"Open map" = "Open map";
|
||||
"More info" = "More info";
|
||||
"Name" = "Name";
|
||||
@@ -81,7 +80,18 @@
|
||||
"Directions to %@" = "Directions to %@";
|
||||
"Opens Apple Maps with driving directions" = "Opens Apple Maps with driving directions";
|
||||
"Show introduction" = "Replay onboarding";
|
||||
"Replay the welcome screen, including the location and notification permission prompts." = "Replay the welcome screen, including the location and notification permission prompts.";
|
||||
"View the welcome screens and set up permissions again." = "View the welcome screens and set up permissions again.";
|
||||
|
||||
/* Settings — refined copy (Fuel Savings, Distances, Data checking) */
|
||||
"Fill-up amount" = "Fill-up amount";
|
||||
"Find stations that could save you money, even after the cost of driving there." = "Find stations that could save you money, even after the cost of driving there.";
|
||||
"How many miles your car travels per gallon." = "How many miles your car travels per gallon.";
|
||||
"How much you plan to spend on fuel." = "How much you plan to spend on fuel.";
|
||||
"How many litres you usually buy." = "How many litres you usually buy.";
|
||||
"The smallest saving you'd consider worth the extra drive." = "The smallest saving you'd consider worth the extra drive.";
|
||||
"FuelBoard compares fuel prices and travel costs to find stations that could save you money. Results are estimates based on your settings and driving distance." = "FuelBoard compares fuel prices and travel costs to find stations that could save you money. Results are estimates based on your settings and driving distance.";
|
||||
"Use actual driving distances instead of straight-line estimates. Routes are calculated while the app is open." = "Use actual driving distances instead of straight-line estimates. Routes are calculated while the app is open.";
|
||||
"Background updates depend on when your iPhone allows them." = "Background updates depend on when your iPhone allows them.";
|
||||
"Test alert notification (real data)" = "Test alert notification (real data)";
|
||||
"Send plain test notification" = "Send plain test notification";
|
||||
"Debug" = "Debug";
|
||||
@@ -202,8 +212,6 @@
|
||||
/* Alerts and favourites localisation pass */
|
||||
"Cheapest favourite" = "Cheapest favourite";
|
||||
"Check now" = "Check now";
|
||||
"Price-drop alerts" = "Price-drop alerts";
|
||||
"Watches your cheapest saved favourite for the fuel you pick here. FuelBoard alerts when a different favourite becomes the cheapest, or when your current cheapest favourite drops by at least 1.0p. This is separate from the nearby-geofence alert, so you can monitor favourites without extra location noise." = "Watches your cheapest saved favourite for the fuel you pick here. FuelBoard alerts when a different favourite becomes the cheapest, or when your current cheapest favourite drops by at least 1.0p. This is separate from the nearby-geofence alert, so you can monitor favourites without extra location noise.";
|
||||
"Cheapest station" = "Cheapest station";
|
||||
"Alert type" = "Alert type";
|
||||
"Best Value" = "Best Value";
|
||||
@@ -212,16 +220,7 @@
|
||||
"Minimum saving %@ — change in Settings, Fuel Savings." = "Minimum saving %@ — change in Settings, Fuel Savings.";
|
||||
"Set up Best Value first so FuelBoard can account for your fill size, MPG and travel cost." = "Set up Best Value first so FuelBoard can account for your fill size, MPG and travel cost.";
|
||||
"Set up Best Value first so Best Value alerts can account for your fill size, MPG and travel cost." = "Set up Best Value first so Best Value alerts can account for your fill size, MPG and travel cost.";
|
||||
"When you approach the Best Value station within the radius, FuelBoard sends a notification if it could leave you at least %@ better off after extra return travel. Nearby stations selling %@ are watched — favourites get priority — and each station alerts at most once per hour." = "When you approach the Best Value station within the radius, FuelBoard sends a notification if it could leave you at least %@ better off after extra return travel. Nearby stations selling %@ are watched — favourites get priority — and each station alerts at most once per hour.";
|
||||
"Proximity alerts" = "Proximity alerts";
|
||||
"When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Nearby stations selling %@ are watched — favourites get priority — and each station alerts at most once per hour. Follow search mirrors the Stations-tab distance, capped at 8 miles for reliable geofencing. Smaller radii ping more often but later; larger radii ping rarely in town but earlier from afar." = "When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Nearby stations selling %@ are watched — favourites get priority — and each station alerts at most once per hour. Follow search mirrors the Stations-tab distance, capped at 8 miles for reliable geofencing. Smaller radii ping more often but later; larger radii ping rarely in town but earlier from afar.";
|
||||
"Set-and-forget: alerts behave exactly like the Stations-tab search distance (capped at 8 miles for reliable geofencing)." = "Set-and-forget: alerts behave exactly like the Stations-tab search distance (capped at 8 miles for reliable geofencing).";
|
||||
"Town driving. Fires most often — you're rarely inside the circle for long, so pings are meaningful. Best for stop-start local trips." = "Town driving. Fires most often — you're rarely inside the circle for long, so pings are meaningful. Best for stop-start local trips.";
|
||||
"City or suburb commute. Slightly earlier heads-up than the smallest option; still fires regularly around town." = "City or suburb commute. Slightly earlier heads-up than the smallest option; still fires regularly around town.";
|
||||
"Mixed driving. Middle ground: heads-up a few minutes out, still works in most towns." = "Mixed driving. Middle ground: heads-up a few minutes out, still works in most towns.";
|
||||
"A-roads and motorway exits. Wide early warning for longer trips — but in town it rarely fires, because you're usually already inside the circle." = "A-roads and motorway exits. Wide early warning for longer trips — but in town it rarely fires, because you're usually already inside the circle.";
|
||||
"Long-distance journeys. Maximum reach: a ping when a cheap station is coming up from far out. In town this essentially never fires." = "Long-distance journeys. Maximum reach: a ping when a cheap station is coming up from far out. In town this essentially never fires.";
|
||||
"%@ — %@" = "%@ — %@";
|
||||
|
||||
/* Best Value calculation and station price age */
|
||||
"£ compares the litres your budget buys at each station; Litres compares one fixed quantity." = "£ compares the litres your budget buys at each station; Litres compares one fixed quantity.";
|
||||
@@ -234,3 +233,14 @@
|
||||
"May be outdated" = "May be outdated";
|
||||
"Fuel you'll receive" = "Fuel you'll receive";
|
||||
"Compared with your closest station." = "Compared with your closest station.";
|
||||
|
||||
/* Alerts tab — refined copy (sections, descriptions, limitations) */
|
||||
"Favourite price alerts" = "Favourite price alerts";
|
||||
"Get notified when a different favourite becomes the cheapest, or your cheapest favourite drops in price." = "Get notified when a different favourite becomes the cheapest, or your cheapest favourite drops in price.";
|
||||
"Get notified when you're near one of the cheapest stations for your chosen fuel." = "Get notified when you're near one of the cheapest stations for your chosen fuel.";
|
||||
"Get notified when you're near a station that could save you money after travel costs." = "Get notified when you're near a station that could save you money after travel costs.";
|
||||
"Get alerts when you're within %@ of a matching station." = "Get alerts when you're within %@ of a matching station.";
|
||||
"Get alerts when you're within %@ of a matching station (your search distance is capped for reliable alerts)." = "Get alerts when you're within %@ of a matching station (your search distance is capped for reliable alerts).";
|
||||
"Uses the same distance as your station search." = "Uses the same distance as your station search.";
|
||||
"See the cheapest nearby station on your Lock Screen and Dynamic Island, with updates as you travel." = "See the cheapest nearby station on your Lock Screen and Dynamic Island, with updates as you travel.";
|
||||
"Background alerts depend on when your iPhone allows them; each station alerts at most once an hour." = "Background alerts depend on when your iPhone allows them; each station alerts at most once an hour.";
|
||||
|
||||
@@ -16,9 +16,9 @@ enum DataRefreshMode: String, CaseIterable, Identifiable {
|
||||
var summary: String {
|
||||
switch self {
|
||||
case .standard:
|
||||
return "Saves battery. Refreshes price data up to twice daily."
|
||||
return "Saves battery by checking for new prices up to twice a day."
|
||||
case .smart:
|
||||
return "Checks for newer data more often in the background and refreshes full prices only when an update is available."
|
||||
return "Checks for new prices more often, keeping your data fresher."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ enum PriceDisplayStyle: String, Codable, CaseIterable, Identifiable {
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .stationSign: return "Station sign"
|
||||
case .stationSign: return "Pence per litre"
|
||||
case .poundsPence: return "Pounds & pence"
|
||||
}
|
||||
}
|
||||
@@ -255,8 +255,8 @@ enum StationTapAction: String, Codable, CaseIterable, Identifiable {
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .openMap: return "Open map"
|
||||
case .showDetails: return "More info"
|
||||
case .openMap: return "Directions"
|
||||
case .showDetails: return "Station details"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user