205 lines
10 KiB
Swift
205 lines
10 KiB
Swift
import SwiftUI
|
|
|
|
/// Alerts tab — enables the cheapest-station proximity alerts. The fuel being
|
|
/// monitored is chosen HERE, independently of the Stations-tab selection:
|
|
/// users can browse any fuel without re-targeting their alerts (and vice
|
|
/// 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.
|
|
struct AlertsView: View {
|
|
@Binding var enabled: Bool
|
|
@Binding var radius: Double // stored in km (monitor + storage)
|
|
@Binding var fuel: FuelType // the fuel alerts monitor for
|
|
@Binding var favouriteDropEnabled: Bool
|
|
@Binding var favouriteDropFuel: FuelType
|
|
/// Whether alerts mirror the Stations-tab search distance (capped at 8 mi).
|
|
@Binding var followsSearch: Bool
|
|
/// Whether the "cheapest nearby" Live Activity is shown on the Lock Screen
|
|
/// / Dynamic Island. Tracks its OWN fuel + radius (below) — independent of
|
|
/// the Stations tab.
|
|
@Binding var liveActivityEnabled: Bool
|
|
/// Fuel the Live Activity tracks (independent of the Stations tab).
|
|
@Binding var liveActivityFuel: FuelType
|
|
/// Search radius (miles, 5/10/15) the Live Activity uses.
|
|
@Binding var liveActivityRadiusMiles: Int
|
|
/// Whether the Live Activity mirrors the Stations-tab search distance.
|
|
@Binding var liveActivityFollowsSearch: Bool
|
|
/// Current Stations-tab search distance (miles) — the value "Follow search"
|
|
/// mirrors.
|
|
let stationLimit: Int
|
|
let distanceUnit: DistanceUnit
|
|
let monitoredCount: Int
|
|
let lastAlert: String?
|
|
|
|
/// The Stations-tab search distance shown in the user's unit, e.g.
|
|
/// "5 miles" or "8 km" — for the "Follow search" rows.
|
|
private var searchInUnitLabel: String {
|
|
let shown = distanceUnit.displayMiles(stationLimit) // stationLimit is always miles
|
|
return "\(shown) \(distanceUnit.label(for: Double(shown)))"
|
|
}
|
|
|
|
/// A miles value (5/10/15) shown truthfully in the user's unit, e.g.
|
|
/// "5 miles" (miles unit) or "8 km" (km unit) — picker labels must match
|
|
/// what the option really means.
|
|
private func distanceLabel(forMiles miles: Int) -> String {
|
|
let shown = distanceUnit.displayMiles(miles)
|
|
return "\(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 {
|
|
switch alertChoice.wrappedValue {
|
|
case .followSearch:
|
|
return "Set-and-forget: alerts behave exactly like the Stations-tab search distance (capped at 8 miles for reliable geofencing)."
|
|
case .fixed(let value):
|
|
let optionLabel = "\(value) \(distanceUnit.label(for: Double(value)))"
|
|
let body: String
|
|
switch value {
|
|
case 1:
|
|
body = "Town driving. Fires most often — you're rarely inside the circle for long, so pings are meaningful. Best for stop-start local trips."
|
|
case 2:
|
|
body = "City or suburb commute. Slightly earlier heads-up than the smallest option; still fires regularly around town."
|
|
case 3:
|
|
body = "Mixed driving. Middle ground: heads-up a few minutes out, still works in most towns."
|
|
case 5:
|
|
body = "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."
|
|
default:
|
|
body = "Long-distance journeys. Maximum reach: a ping when a cheap station is coming up from far out. In town this essentially never fires."
|
|
}
|
|
return "\(optionLabel) — \(body)"
|
|
}
|
|
}
|
|
|
|
private enum AlertRadiusChoice: Hashable {
|
|
case followSearch
|
|
case fixed(Int)
|
|
}
|
|
|
|
/// The radius picker works in the user's chosen unit; the stored value
|
|
/// stays km so ProximityMonitor and persistence never change. Options are
|
|
/// snapped to the nearest picker value so any stored radius still selects.
|
|
/// "Follow search" mirrors the Stations-tab distance (capped at 8 miles).
|
|
private var alertChoice: Binding<AlertRadiusChoice> {
|
|
Binding(
|
|
get: {
|
|
if followsSearch { return .followSearch }
|
|
let inUnit = distanceUnit.fromKM(radius)
|
|
let snapped = FuelStore.alertRadiusOptions
|
|
.min(by: { abs(Double($0) - inUnit) < abs(Double($1) - inUnit) }) ?? 3
|
|
return .fixed(snapped)
|
|
},
|
|
set: { choice in
|
|
switch choice {
|
|
case .followSearch:
|
|
followsSearch = true
|
|
case .fixed(let value):
|
|
followsSearch = false
|
|
radius = distanceUnit.toKM(Double(value))
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
private enum LADistanceChoice: Hashable {
|
|
case followSearch
|
|
case fixed(Int)
|
|
}
|
|
|
|
private var laChoice: Binding<LADistanceChoice> {
|
|
Binding(
|
|
get: {
|
|
if liveActivityFollowsSearch { return .followSearch }
|
|
return .fixed(liveActivityRadiusMiles)
|
|
},
|
|
set: { choice in
|
|
switch choice {
|
|
case .followSearch:
|
|
liveActivityFollowsSearch = true
|
|
case .fixed(let miles):
|
|
liveActivityFollowsSearch = false
|
|
liveActivityRadiusMiles = miles
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
List {
|
|
Section {
|
|
Toggle("Cheapest favourite", isOn: $favouriteDropEnabled)
|
|
Picker("Fuel", selection: $favouriteDropFuel) {
|
|
ForEach(FuelType.allCases) { fuel in
|
|
Text(fuel.displayName).tag(fuel)
|
|
}
|
|
}
|
|
} header: {
|
|
Text("Price-drop alerts")
|
|
} footer: {
|
|
Text("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.")
|
|
}
|
|
|
|
Section {
|
|
Toggle("Cheapest station", isOn: $enabled)
|
|
Picker("Fuel", selection: $fuel) {
|
|
ForEach(FuelType.allCases) { fuel in
|
|
Text(fuel.displayName).tag(fuel)
|
|
}
|
|
}
|
|
Picker("Radius", selection: alertChoice) {
|
|
Text("Follow search (\(searchInUnitLabel))").tag(AlertRadiusChoice.followSearch)
|
|
ForEach(FuelStore.alertRadiusOptions, id: \.self) { value in
|
|
Text("\(value) \(distanceUnit.label(for: Double(value)))").tag(AlertRadiusChoice.fixed(value))
|
|
}
|
|
}
|
|
Text(alertRadiusGuidance)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
if enabled && monitoredCount == 0 {
|
|
Text("No stations monitored yet — open the Stations tab to load prices first.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
} header: {
|
|
Text("Proximity alerts")
|
|
} footer: {
|
|
Text("When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Nearby stations selling \(fuel.displayName.lowercased()) 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.")
|
|
}
|
|
|
|
if enabled, let lastAlert {
|
|
Section("Last alert") {
|
|
Label(lastAlert, systemImage: "bell.badge.fill")
|
|
.font(.footnote)
|
|
}
|
|
}
|
|
|
|
Section {
|
|
Toggle("Cheapest station", isOn: $liveActivityEnabled)
|
|
Picker("Fuel", selection: $liveActivityFuel) {
|
|
ForEach(FuelType.allCases) { fuel in
|
|
Text(fuel.displayName).tag(fuel)
|
|
}
|
|
}
|
|
Picker("Distance", selection: laChoice) {
|
|
Text("Follow search (\(searchInUnitLabel))").tag(LADistanceChoice.followSearch)
|
|
ForEach(FuelStore.stationRadiusOptions, id: \.self) { miles in
|
|
Text(distanceLabel(forMiles: miles)).tag(LADistanceChoice.fixed(miles))
|
|
}
|
|
}
|
|
} header: {
|
|
Text("Live Activity")
|
|
} footer: {
|
|
Text("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.")
|
|
}
|
|
}
|
|
.navigationTitle("Alerts")
|
|
}
|
|
}
|
|
}
|