From fcb83f1eedb9312238ce4cbe98185f5678a281a4 Mon Sep 17 00:00:00 2001 From: FuelBoard Contributor Date: Wed, 12 Aug 2026 20:58:05 +0100 Subject: [PATCH] Live Activity: own fuel + distance config, independent of Stations tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Settings → Live Activity now has Fuel and Distance pickers (5/10/15, unit-aware) stored under fuelboard.liveActivityFuel / liveActivityRadiusMiles - LiveActivityManager uses these instead of the Stations-tab selectedFuel / stationLimit; Stations changes no longer re-target the pill - Fuel moved from Activity attributes into ContentState so changing fuel in Settings updates the RUNNING activity in place (no end+restart needed) - Stations tab stays a pure data-interrogation + favourites surface --- FuelBoard/ContentView.swift | 24 ++++++++++++---- FuelBoard/LiveActivityManager.swift | 3 +- FuelBoard/SettingsView.swift | 26 +++++++++++++++-- .../FuelBoardLiveActivityView.swift | 4 +-- Shared/FuelBoardLiveActivity.swift | 17 +++++------ Shared/FuelStore.swift | 28 +++++++++++++++++++ 6 files changed, 82 insertions(+), 20 deletions(-) diff --git a/FuelBoard/ContentView.swift b/FuelBoard/ContentView.swift index 86cd914..2b4b7ec 100644 --- a/FuelBoard/ContentView.swift +++ b/FuelBoard/ContentView.swift @@ -15,6 +15,8 @@ struct ContentView: View { @State private var alertsRadius: Double = FuelStore.loadAlertsRadius() @State private var alertsFuel: FuelType = FuelStore.loadAlertsFuel() @State private var liveActivityEnabled: Bool = FuelStore.loadLiveActivityEnabled() + @State private var liveActivityFuel: FuelType = FuelStore.loadLiveActivityFuel() + @State private var liveActivityRadiusMiles: Int = FuelStore.loadLiveActivityRadiusMiles() @State private var location: Coordinate? = { if let loc = FuelStore.loadLocation() { return Coordinate(lat: loc.lat, lng: loc.lng) } return nil @@ -251,7 +253,6 @@ struct ContentView: View { .onChange(of: selectedFuel) { _, _ in // No re-fetch needed — one response carries E5/E10/DIESEL prices. WidgetCenter.shared.reloadAllTimelines() - updateLiveActivity() } .onChange(of: stationLimit) { _, newValue in // Distance filter is LOCAL math now — the cache holds the full-UK @@ -260,7 +261,6 @@ struct ContentView: View { // next render. FuelStore.saveStationLimit(newValue) WidgetCenter.shared.reloadAllTimelines() - updateLiveActivity() } .onChange(of: alertsEnabled) { _, newValue in FuelStore.saveAlertsEnabled(newValue) @@ -289,6 +289,16 @@ struct ContentView: View { FuelStore.saveLiveActivityEnabled(newValue) updateLiveActivity() } + .onChange(of: liveActivityFuel) { _, newValue in + // The pill tracks its OWN fuel — independent of the Stations tab. + FuelStore.saveLiveActivityFuel(newValue) + updateLiveActivity() + } + .onChange(of: liveActivityRadiusMiles) { _, newValue in + // Same for its own radius (miles, converted at use). + FuelStore.saveLiveActivityRadiusMiles(newValue) + updateLiveActivity() + } .onChange(of: distanceUnit) { _, _ in // Distance unit changes the radius — mirror the new radius in the // Live Activity immediately. @@ -298,13 +308,13 @@ struct ContentView: View { /// Pushes the current best-in-radius station into the Live Activity. /// No-op (or ends the activity) when the toggle is off or there's no - /// location/data yet. Mirrors the app list's TOP badge: selected fuel + - /// chosen distance radius, cheapest then nearest on ties. + /// location/data yet. Uses the Live Activity's OWN fuel + radius (set in + /// Settings) — independent of the Stations-tab fuel/distance. private func updateLiveActivity() { LiveActivityManager.update( stations: stations, - fuel: selectedFuel, - radiusKM: distanceUnit.toKM(Double(stationLimit)), + fuel: liveActivityFuel, + radiusKM: distanceUnit.toKM(Double(liveActivityRadiusMiles)), location: location, enabled: liveActivityEnabled ) @@ -316,6 +326,8 @@ struct ContentView: View { SettingsView( distanceUnit: $distanceUnit, liveActivityEnabled: $liveActivityEnabled, + liveActivityFuel: $liveActivityFuel, + liveActivityRadiusMiles: $liveActivityRadiusMiles, alertsFuel: alertsFuel, alertsRadiusKM: alertsRadius, testAlertResult: monitor.lastTestResult, diff --git a/FuelBoard/LiveActivityManager.swift b/FuelBoard/LiveActivityManager.swift index 1629d3c..cc9daf5 100644 --- a/FuelBoard/LiveActivityManager.swift +++ b/FuelBoard/LiveActivityManager.swift @@ -48,6 +48,7 @@ enum LiveActivityManager { } let state = FuelBoardLiveActivityAttributes.ContentState( + fuel: fuel, stationID: best.id, stationName: best.name, brand: best.brand, @@ -57,7 +58,7 @@ enum LiveActivityManager { lng: best.lng, updatedAt: Date() ) - let attributes = FuelBoardLiveActivityAttributes(fuel: fuel) + let attributes = FuelBoardLiveActivityAttributes() if let current { // Near the 8 h cap? End and re-request so the pill keeps living. diff --git a/FuelBoard/SettingsView.swift b/FuelBoard/SettingsView.swift index 824c124..f2fee0b 100644 --- a/FuelBoard/SettingsView.swift +++ b/FuelBoard/SettingsView.swift @@ -13,9 +13,13 @@ import WidgetKit struct SettingsView: View { @Binding var distanceUnit: DistanceUnit /// Whether the "cheapest nearby" Live Activity is shown on the Lock Screen - /// / Dynamic Island. Mirrors the app list's TOP badge (selected fuel + - /// distance radius) and re-updates as the user drives. + /// / 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 /// The fuel + radius currently configured for alerts (mirrors the Alerts /// tab) so the test notification matches what real alerts will say. var alertsFuel: FuelType = .e10 @@ -72,10 +76,26 @@ struct SettingsView: View { Section { Toggle("Live Activity", isOn: $liveActivityEnabled) + Picker("Fuel", selection: $liveActivityFuel) { + ForEach(FuelType.allCases) { fuel in + Text(fuel.displayName).tag(fuel) + } + } + .onChange(of: liveActivityFuel) { _, newValue in + FuelStore.saveLiveActivityFuel(newValue) + } + Picker("Distance", selection: $liveActivityRadiusMiles) { + ForEach(FuelStore.stationRadiusOptions, id: \.self) { miles in + Text("\(miles) \(distanceUnit.label)").tag(miles) + } + } + .onChange(of: liveActivityRadiusMiles) { _, newValue in + FuelStore.saveLiveActivityRadiusMiles(newValue) + } } header: { Text("Live Activity") } footer: { - Text("Shows the cheapest station for your selected fuel within your chosen distance on the Lock Screen and Dynamic Island. Updates as you drive; tap to open directions. Live Activities can be disabled in System Settings → Live Activities.") + 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. Updates as you drive; tap to open directions. Live Activities can be disabled in System Settings → Live Activities.") } .onChange(of: liveActivityEnabled) { _, newValue in FuelStore.saveLiveActivityEnabled(newValue) diff --git a/FuelBoardWidgets/FuelBoardLiveActivityView.swift b/FuelBoardWidgets/FuelBoardLiveActivityView.swift index 802b264..7758a6d 100644 --- a/FuelBoardWidgets/FuelBoardLiveActivityView.swift +++ b/FuelBoardWidgets/FuelBoardLiveActivityView.swift @@ -55,7 +55,7 @@ private struct FuelBoardLiveActivityView: View { // MIDDLE — fuel + station VStack(alignment: .leading, spacing: 2) { - Text("Cheapest \(context.attributes.fuel.displayName)") + Text("Cheapest \(context.state.fuel.displayName)") .font(.headline) Text("\(context.state.stationName) · \(context.state.distanceText)") .font(.subheadline) @@ -94,7 +94,7 @@ private struct FuelBoardLiveActivityCompactView: View { var body: some View { VStack(alignment: .leading, spacing: 2) { - Text("Cheapest \(context.attributes.fuel.displayName)") + Text("Cheapest \(context.state.fuel.displayName)") .font(.caption2) .foregroundStyle(.secondary) Text(context.state.stationName) diff --git a/Shared/FuelBoardLiveActivity.swift b/Shared/FuelBoardLiveActivity.swift index 78cc066..b24fc17 100644 --- a/Shared/FuelBoardLiveActivity.swift +++ b/Shared/FuelBoardLiveActivity.swift @@ -5,20 +5,21 @@ // is deliberately NOT symlinked into FuelBoardTests/Sources — the SPM test // target runs on macOS where ActivityKit does not exist. // -// The activity mirrors the app's TOP badge: the cheapest station selling the -// app's selected fuel within the app's chosen distance radius. Tapping the -// activity opens Apple Maps driving directions to that station (maps://). +// The activity tracks the cheapest station for the fuel + radius configured +// in Settings → Live Activity (INDEPENDENT of the Stations-tab selection). +// Everything dynamic lives in ContentState so updates — including a fuel +// change — apply to the running activity without needing to restart it. +// Tapping the activity opens Apple Maps driving directions (maps://). import ActivityKit import Foundation -/// Attributes are fixed at request time. Here they pin the FUEL being -/// tracked; everything that can change while driving (station, price, -/// distance) lives in ContentState so updates don't need a new activity. +/// Attributes are fixed at request time. Kept empty: the fuel itself lives in +/// ContentState so a fuel change in Settings updates the running activity +/// in place instead of requiring an end + restart. struct FuelBoardLiveActivityAttributes: ActivityAttributes { - var fuel: FuelType - public struct ContentState: Codable, Hashable { + var fuel: FuelType var stationID: String var stationName: String var brand: String diff --git a/Shared/FuelStore.swift b/Shared/FuelStore.swift index f08f928..4f40ccc 100644 --- a/Shared/FuelStore.swift +++ b/Shared/FuelStore.swift @@ -263,6 +263,8 @@ struct FuelStore { static let alertsFuelKey = "fuelboard.alertsFuel" // FuelType raw value static let debugModeKey = "fuelboard.debugMode" // Bool — hidden dev flag static let liveActivityKey = "fuelboard.liveActivity" // Bool — Live Activity toggle + static let liveActivityFuelKey = "fuelboard.liveActivityFuel" // FuelType raw value + static let liveActivityRadiusKey = "fuelboard.liveActivityRadiusMiles" // Int miles (5/10/15) static let onboardingCompletedKey = "fuelboard.onboardingCompleted" // Bool static let lastRefreshKey = "fuelboard.lastRefresh" // TimeInterval (seconds since 1970) static let relaySourceKey = "fuelboard.relaySource" // String — "api" | "csv" @@ -501,6 +503,32 @@ struct FuelStore { saveString(enabled ? "1" : "0", service: liveActivityKey) } + /// The fuel the Live Activity tracks — independent of the Stations-tab + /// selection so the Lock Screen pill keeps its own target. + static func loadLiveActivityFuel() -> FuelType { + if let raw = loadString(service: liveActivityFuelKey), let fuel = FuelType(rawValue: raw) { + return fuel + } + return .e10 + } + + static func saveLiveActivityFuel(_ fuel: FuelType) { + saveString(fuel.rawValue, service: liveActivityFuelKey) + } + + /// The Live Activity's search radius in miles (5/10/15), converted to the + /// chosen unit at use — same options as the Stations list. + static func loadLiveActivityRadiusMiles() -> Int { + if let raw = loadString(service: liveActivityRadiusKey), let value = Int(raw), stationRadiusOptions.contains(value) { + return value + } + return 5 + } + + static func saveLiveActivityRadiusMiles(_ miles: Int) { + saveString(String(miles), service: liveActivityRadiusKey) + } + // MARK: Refresh policy — data is cached; the app only auto-refreshes // twice a day (pull-to-refresh is the manual override).