Add cheapest-favourite price-drop alerts
This commit is contained in:
+122
-64
@@ -15,6 +15,8 @@ struct ContentView: View {
|
||||
@State private var alertsEnabled: Bool = FuelStore.loadAlertsEnabled()
|
||||
@State private var alertsRadius: Double = FuelStore.loadAlertsRadius()
|
||||
@State private var alertsFuel: FuelType = FuelStore.loadAlertsFuel()
|
||||
@State private var favouriteDropAlertsEnabled: Bool = FuelStore.loadFavouriteDropAlertsEnabled()
|
||||
@State private var favouriteDropAlertsFuel: FuelType = FuelStore.loadFavouriteDropAlertsFuel()
|
||||
@State private var alertsFollowsSearch: Bool = FuelStore.loadAlertsFollowsSearch()
|
||||
@State private var liveActivityEnabled: Bool = FuelStore.loadLiveActivityEnabled()
|
||||
@State private var liveActivityFuel: FuelType = FuelStore.loadLiveActivityFuel()
|
||||
@@ -152,71 +154,34 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
switch dataStatus {
|
||||
case .offlineDump(let date):
|
||||
let title = offlineTitle(date: date)
|
||||
statusBanner(
|
||||
icon: "wifi.slash",
|
||||
tint: .orange,
|
||||
title: title,
|
||||
subtitle: NSLocalizedString("Pull to refresh on the Stations tab", comment: ""),
|
||||
accessibilityLabel: date.isEmpty
|
||||
? NSLocalizedString("Offline data. Pull to refresh on the Stations tab", comment: "")
|
||||
: String(format: NSLocalizedString("Offline data from %@. Pull to refresh on the Stations tab", comment: ""), date)
|
||||
)
|
||||
case .connectionProblem:
|
||||
statusBanner(
|
||||
icon: "wifi.exclamationmark",
|
||||
tint: .red,
|
||||
title: NSLocalizedString("Check your internet connection", comment: ""),
|
||||
subtitle: NSLocalizedString("Tap to try again", comment: ""),
|
||||
accessibilityLabel: NSLocalizedString("Check your internet connection. Tap to try again", comment: "")
|
||||
)
|
||||
case .live:
|
||||
EmptyView()
|
||||
}
|
||||
TabView(selection: $selectedTab) {
|
||||
stationsTab
|
||||
.tabItem { Label("Stations", systemImage: "fuelpump.fill") }
|
||||
.tag(0)
|
||||
configuredContent
|
||||
}
|
||||
|
||||
favouritesTab
|
||||
.tabItem { Label("Favourites", systemImage: "star.fill") }
|
||||
.tag(1)
|
||||
private var configuredContent: AnyView {
|
||||
interactionObservedContent
|
||||
}
|
||||
|
||||
AlertsView(
|
||||
enabled: $alertsEnabled,
|
||||
radius: $alertsRadius,
|
||||
fuel: $alertsFuel,
|
||||
followsSearch: $alertsFollowsSearch,
|
||||
liveActivityEnabled: $liveActivityEnabled,
|
||||
liveActivityFuel: $liveActivityFuel,
|
||||
liveActivityRadiusMiles: $liveActivityRadiusMiles,
|
||||
liveActivityFollowsSearch: $liveActivityFollowsSearch,
|
||||
stationLimit: stationLimit,
|
||||
distanceUnit: distanceUnit,
|
||||
monitoredCount: monitor.monitoredStationIDs.count,
|
||||
lastAlert: monitor.lastAlert
|
||||
)
|
||||
.tabItem { Label("Alerts", systemImage: "bell.fill") }
|
||||
.tag(2)
|
||||
private var presentedContent: AnyView {
|
||||
AnyView(
|
||||
mainContent
|
||||
.fullScreenCover(isPresented: $showOnboarding) {
|
||||
OnboardingView {
|
||||
showOnboarding = false
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $showWidgetMock) {
|
||||
WidgetMockScreen()
|
||||
}
|
||||
.sheet(item: $monitor.pendingStationMap) { request in
|
||||
StationMapView(request: request)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
settingsTab
|
||||
.tag(3)
|
||||
}
|
||||
.fullScreenCover(isPresented: $showOnboarding) {
|
||||
OnboardingView {
|
||||
showOnboarding = false
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $showWidgetMock) {
|
||||
WidgetMockScreen()
|
||||
}
|
||||
.sheet(item: $monitor.pendingStationMap) { request in
|
||||
StationMapView(request: request)
|
||||
}
|
||||
.onAppear {
|
||||
private var lifecycleObservedContent: AnyView {
|
||||
AnyView(
|
||||
presentedContent
|
||||
.onAppear {
|
||||
// Launch-arg hooks for the screenshot/UI-test harness (same
|
||||
// pattern as StationsView's `-showKeySheet`): `-tab <name>`
|
||||
// opens the given tab; `-skipOnboarding` skips onboarding
|
||||
@@ -379,7 +344,13 @@ struct ContentView: View {
|
||||
updateLiveActivity()
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedFuel) { _, _ in
|
||||
)
|
||||
}
|
||||
|
||||
private var interactionObservedContent: AnyView {
|
||||
AnyView(
|
||||
lifecycleObservedContent
|
||||
.onChange(of: selectedFuel) { _, _ in
|
||||
// No re-fetch needed — one response carries E5/E10/DIESEL prices.
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
@@ -425,6 +396,12 @@ struct ContentView: View {
|
||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
||||
}
|
||||
.onChange(of: favouriteDropAlertsEnabled) { _, newValue in
|
||||
FuelStore.saveFavouriteDropAlertsEnabled(newValue)
|
||||
}
|
||||
.onChange(of: favouriteDropAlertsFuel) { _, newValue in
|
||||
FuelStore.saveFavouriteDropAlertsFuel(newValue)
|
||||
}
|
||||
.onChange(of: liveActivityEnabled) { _, newValue in
|
||||
// Toggling the Live Activity on starts it with the current best
|
||||
// station; toggling off ends any running activity.
|
||||
@@ -464,10 +441,63 @@ struct ContentView: View {
|
||||
// so reading storage here could push the OLD style.
|
||||
updateLiveActivity(priceDisplayStyleOverride: newValue)
|
||||
}
|
||||
} // VStack: status banner + TabView
|
||||
)
|
||||
}
|
||||
|
||||
private var mainContent: some View {
|
||||
VStack(spacing: 0) {
|
||||
statusBannerView
|
||||
rootTabView
|
||||
}
|
||||
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: dataStatus)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var statusBannerView: some View {
|
||||
switch dataStatus {
|
||||
case .offlineDump(let date):
|
||||
let title = offlineTitle(date: date)
|
||||
statusBanner(
|
||||
icon: "wifi.slash",
|
||||
tint: .orange,
|
||||
title: title,
|
||||
subtitle: NSLocalizedString("Pull to refresh on the Stations tab", comment: ""),
|
||||
accessibilityLabel: date.isEmpty
|
||||
? NSLocalizedString("Offline data. Pull to refresh on the Stations tab", comment: "")
|
||||
: String(format: NSLocalizedString("Offline data from %@. Pull to refresh on the Stations tab", comment: ""), date)
|
||||
)
|
||||
case .connectionProblem:
|
||||
statusBanner(
|
||||
icon: "wifi.exclamationmark",
|
||||
tint: .red,
|
||||
title: NSLocalizedString("Check your internet connection", comment: ""),
|
||||
subtitle: NSLocalizedString("Tap to try again", comment: ""),
|
||||
accessibilityLabel: NSLocalizedString("Check your internet connection. Tap to try again", comment: "")
|
||||
)
|
||||
case .live:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
private var rootTabView: some View {
|
||||
TabView(selection: $selectedTab) {
|
||||
stationsTab
|
||||
.tabItem { Label("Stations", systemImage: "fuelpump.fill") }
|
||||
.tag(0)
|
||||
|
||||
favouritesTab
|
||||
.tabItem { Label("Favourites", systemImage: "star.fill") }
|
||||
.tag(1)
|
||||
|
||||
alertsTab
|
||||
.tabItem { Label("Alerts", systemImage: "bell.fill") }
|
||||
.tag(2)
|
||||
|
||||
settingsTab
|
||||
.tag(3)
|
||||
}
|
||||
}
|
||||
|
||||
/// The banner title for the bundled-snapshot case: date when the stamp
|
||||
/// parsed, plain "Offline data" otherwise.
|
||||
private func offlineTitle(date: String) -> String {
|
||||
@@ -576,6 +606,27 @@ struct ContentView: View {
|
||||
)
|
||||
}
|
||||
|
||||
/// The Alerts tab, extracted from `body` so the TabView expression stays
|
||||
/// within the compiler's type-check budget.
|
||||
private var alertsTab: some View {
|
||||
AlertsView(
|
||||
enabled: $alertsEnabled,
|
||||
radius: $alertsRadius,
|
||||
fuel: $alertsFuel,
|
||||
favouriteDropEnabled: $favouriteDropAlertsEnabled,
|
||||
favouriteDropFuel: $favouriteDropAlertsFuel,
|
||||
followsSearch: $alertsFollowsSearch,
|
||||
liveActivityEnabled: $liveActivityEnabled,
|
||||
liveActivityFuel: $liveActivityFuel,
|
||||
liveActivityRadiusMiles: $liveActivityRadiusMiles,
|
||||
liveActivityFollowsSearch: $liveActivityFollowsSearch,
|
||||
stationLimit: stationLimit,
|
||||
distanceUnit: distanceUnit,
|
||||
monitoredCount: monitor.monitoredStationIDs.count,
|
||||
lastAlert: monitor.lastAlert
|
||||
)
|
||||
}
|
||||
|
||||
/// The Settings tab, extracted from `body` so the TabView expression stays
|
||||
/// within the compiler's type-check budget.
|
||||
private var settingsTab: some View {
|
||||
@@ -635,6 +686,7 @@ struct ContentView: View {
|
||||
isLoading = true
|
||||
defer { isLoading = false }
|
||||
do {
|
||||
let previousFavouriteSnapshots = FuelStore.loadFavouriteAlertSnapshots()
|
||||
let fetched = try await FuelPriceProvider.active.fetchStations(
|
||||
near: location?.lat, lng: location?.lng,
|
||||
fuel: selectedFuel,
|
||||
@@ -654,6 +706,12 @@ struct ContentView: View {
|
||||
// channel shared on SideStore free), so stale star-time snapshots
|
||||
// would otherwise show old prices.
|
||||
FuelStore.saveFavourites(refreshedFavourites)
|
||||
monitor.evaluateFavouritePriceDropAlert(
|
||||
favourites: refreshedFavourites,
|
||||
previousSnapshots: previousFavouriteSnapshots,
|
||||
monitoredFuel: favouriteDropAlertsFuel,
|
||||
enabled: favouriteDropAlertsEnabled
|
||||
)
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
statusMessage = "Loaded \(fetched.count) stations · \(Date().formatted(date: .omitted, time: .shortened))"
|
||||
// Live data restored — any status banner no longer applies.
|
||||
|
||||
Reference in New Issue
Block a user