Live Activity: own fuel + distance config, independent of Stations tab
- 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
This commit is contained in:
@@ -15,6 +15,8 @@ struct ContentView: View {
|
|||||||
@State private var alertsRadius: Double = FuelStore.loadAlertsRadius()
|
@State private var alertsRadius: Double = FuelStore.loadAlertsRadius()
|
||||||
@State private var alertsFuel: FuelType = FuelStore.loadAlertsFuel()
|
@State private var alertsFuel: FuelType = FuelStore.loadAlertsFuel()
|
||||||
@State private var liveActivityEnabled: Bool = FuelStore.loadLiveActivityEnabled()
|
@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? = {
|
@State private var location: Coordinate? = {
|
||||||
if let loc = FuelStore.loadLocation() { return Coordinate(lat: loc.lat, lng: loc.lng) }
|
if let loc = FuelStore.loadLocation() { return Coordinate(lat: loc.lat, lng: loc.lng) }
|
||||||
return nil
|
return nil
|
||||||
@@ -251,7 +253,6 @@ struct ContentView: View {
|
|||||||
.onChange(of: selectedFuel) { _, _ in
|
.onChange(of: selectedFuel) { _, _ in
|
||||||
// No re-fetch needed — one response carries E5/E10/DIESEL prices.
|
// No re-fetch needed — one response carries E5/E10/DIESEL prices.
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
updateLiveActivity()
|
|
||||||
}
|
}
|
||||||
.onChange(of: stationLimit) { _, newValue in
|
.onChange(of: stationLimit) { _, newValue in
|
||||||
// Distance filter is LOCAL math now — the cache holds the full-UK
|
// Distance filter is LOCAL math now — the cache holds the full-UK
|
||||||
@@ -260,7 +261,6 @@ struct ContentView: View {
|
|||||||
// next render.
|
// next render.
|
||||||
FuelStore.saveStationLimit(newValue)
|
FuelStore.saveStationLimit(newValue)
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
updateLiveActivity()
|
|
||||||
}
|
}
|
||||||
.onChange(of: alertsEnabled) { _, newValue in
|
.onChange(of: alertsEnabled) { _, newValue in
|
||||||
FuelStore.saveAlertsEnabled(newValue)
|
FuelStore.saveAlertsEnabled(newValue)
|
||||||
@@ -289,6 +289,16 @@ struct ContentView: View {
|
|||||||
FuelStore.saveLiveActivityEnabled(newValue)
|
FuelStore.saveLiveActivityEnabled(newValue)
|
||||||
updateLiveActivity()
|
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
|
.onChange(of: distanceUnit) { _, _ in
|
||||||
// Distance unit changes the radius — mirror the new radius in the
|
// Distance unit changes the radius — mirror the new radius in the
|
||||||
// Live Activity immediately.
|
// Live Activity immediately.
|
||||||
@@ -298,13 +308,13 @@ struct ContentView: View {
|
|||||||
|
|
||||||
/// Pushes the current best-in-radius station into the Live Activity.
|
/// 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
|
/// 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 +
|
/// location/data yet. Uses the Live Activity's OWN fuel + radius (set in
|
||||||
/// chosen distance radius, cheapest then nearest on ties.
|
/// Settings) — independent of the Stations-tab fuel/distance.
|
||||||
private func updateLiveActivity() {
|
private func updateLiveActivity() {
|
||||||
LiveActivityManager.update(
|
LiveActivityManager.update(
|
||||||
stations: stations,
|
stations: stations,
|
||||||
fuel: selectedFuel,
|
fuel: liveActivityFuel,
|
||||||
radiusKM: distanceUnit.toKM(Double(stationLimit)),
|
radiusKM: distanceUnit.toKM(Double(liveActivityRadiusMiles)),
|
||||||
location: location,
|
location: location,
|
||||||
enabled: liveActivityEnabled
|
enabled: liveActivityEnabled
|
||||||
)
|
)
|
||||||
@@ -316,6 +326,8 @@ struct ContentView: View {
|
|||||||
SettingsView(
|
SettingsView(
|
||||||
distanceUnit: $distanceUnit,
|
distanceUnit: $distanceUnit,
|
||||||
liveActivityEnabled: $liveActivityEnabled,
|
liveActivityEnabled: $liveActivityEnabled,
|
||||||
|
liveActivityFuel: $liveActivityFuel,
|
||||||
|
liveActivityRadiusMiles: $liveActivityRadiusMiles,
|
||||||
alertsFuel: alertsFuel,
|
alertsFuel: alertsFuel,
|
||||||
alertsRadiusKM: alertsRadius,
|
alertsRadiusKM: alertsRadius,
|
||||||
testAlertResult: monitor.lastTestResult,
|
testAlertResult: monitor.lastTestResult,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ enum LiveActivityManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let state = FuelBoardLiveActivityAttributes.ContentState(
|
let state = FuelBoardLiveActivityAttributes.ContentState(
|
||||||
|
fuel: fuel,
|
||||||
stationID: best.id,
|
stationID: best.id,
|
||||||
stationName: best.name,
|
stationName: best.name,
|
||||||
brand: best.brand,
|
brand: best.brand,
|
||||||
@@ -57,7 +58,7 @@ enum LiveActivityManager {
|
|||||||
lng: best.lng,
|
lng: best.lng,
|
||||||
updatedAt: Date()
|
updatedAt: Date()
|
||||||
)
|
)
|
||||||
let attributes = FuelBoardLiveActivityAttributes(fuel: fuel)
|
let attributes = FuelBoardLiveActivityAttributes()
|
||||||
|
|
||||||
if let current {
|
if let current {
|
||||||
// Near the 8 h cap? End and re-request so the pill keeps living.
|
// Near the 8 h cap? End and re-request so the pill keeps living.
|
||||||
|
|||||||
@@ -13,9 +13,13 @@ import WidgetKit
|
|||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@Binding var distanceUnit: DistanceUnit
|
@Binding var distanceUnit: DistanceUnit
|
||||||
/// Whether the "cheapest nearby" Live Activity is shown on the Lock Screen
|
/// Whether the "cheapest nearby" Live Activity is shown on the Lock Screen
|
||||||
/// / Dynamic Island. Mirrors the app list's TOP badge (selected fuel +
|
/// / Dynamic Island. Tracks its OWN fuel + radius (below) — independent of
|
||||||
/// distance radius) and re-updates as the user drives.
|
/// the Stations tab.
|
||||||
@Binding var liveActivityEnabled: Bool
|
@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
|
/// The fuel + radius currently configured for alerts (mirrors the Alerts
|
||||||
/// tab) so the test notification matches what real alerts will say.
|
/// tab) so the test notification matches what real alerts will say.
|
||||||
var alertsFuel: FuelType = .e10
|
var alertsFuel: FuelType = .e10
|
||||||
@@ -72,10 +76,26 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
Section {
|
Section {
|
||||||
Toggle("Live Activity", isOn: $liveActivityEnabled)
|
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: {
|
} header: {
|
||||||
Text("Live Activity")
|
Text("Live Activity")
|
||||||
} footer: {
|
} 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
|
.onChange(of: liveActivityEnabled) { _, newValue in
|
||||||
FuelStore.saveLiveActivityEnabled(newValue)
|
FuelStore.saveLiveActivityEnabled(newValue)
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ private struct FuelBoardLiveActivityView: View {
|
|||||||
|
|
||||||
// MIDDLE — fuel + station
|
// MIDDLE — fuel + station
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("Cheapest \(context.attributes.fuel.displayName)")
|
Text("Cheapest \(context.state.fuel.displayName)")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
Text("\(context.state.stationName) · \(context.state.distanceText)")
|
Text("\(context.state.stationName) · \(context.state.distanceText)")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
@@ -94,7 +94,7 @@ private struct FuelBoardLiveActivityCompactView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("Cheapest \(context.attributes.fuel.displayName)")
|
Text("Cheapest \(context.state.fuel.displayName)")
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
Text(context.state.stationName)
|
Text(context.state.stationName)
|
||||||
|
|||||||
@@ -5,20 +5,21 @@
|
|||||||
// is deliberately NOT symlinked into FuelBoardTests/Sources — the SPM test
|
// is deliberately NOT symlinked into FuelBoardTests/Sources — the SPM test
|
||||||
// target runs on macOS where ActivityKit does not exist.
|
// target runs on macOS where ActivityKit does not exist.
|
||||||
//
|
//
|
||||||
// The activity mirrors the app's TOP badge: the cheapest station selling the
|
// The activity tracks the cheapest station for the fuel + radius configured
|
||||||
// app's selected fuel within the app's chosen distance radius. Tapping the
|
// in Settings → Live Activity (INDEPENDENT of the Stations-tab selection).
|
||||||
// activity opens Apple Maps driving directions to that station (maps://).
|
// 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 ActivityKit
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Attributes are fixed at request time. Here they pin the FUEL being
|
/// Attributes are fixed at request time. Kept empty: the fuel itself lives in
|
||||||
/// tracked; everything that can change while driving (station, price,
|
/// ContentState so a fuel change in Settings updates the running activity
|
||||||
/// distance) lives in ContentState so updates don't need a new activity.
|
/// in place instead of requiring an end + restart.
|
||||||
struct FuelBoardLiveActivityAttributes: ActivityAttributes {
|
struct FuelBoardLiveActivityAttributes: ActivityAttributes {
|
||||||
var fuel: FuelType
|
|
||||||
|
|
||||||
public struct ContentState: Codable, Hashable {
|
public struct ContentState: Codable, Hashable {
|
||||||
|
var fuel: FuelType
|
||||||
var stationID: String
|
var stationID: String
|
||||||
var stationName: String
|
var stationName: String
|
||||||
var brand: String
|
var brand: String
|
||||||
|
|||||||
@@ -263,6 +263,8 @@ struct FuelStore {
|
|||||||
static let alertsFuelKey = "fuelboard.alertsFuel" // FuelType raw value
|
static let alertsFuelKey = "fuelboard.alertsFuel" // FuelType raw value
|
||||||
static let debugModeKey = "fuelboard.debugMode" // Bool — hidden dev flag
|
static let debugModeKey = "fuelboard.debugMode" // Bool — hidden dev flag
|
||||||
static let liveActivityKey = "fuelboard.liveActivity" // Bool — Live Activity toggle
|
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 onboardingCompletedKey = "fuelboard.onboardingCompleted" // Bool
|
||||||
static let lastRefreshKey = "fuelboard.lastRefresh" // TimeInterval (seconds since 1970)
|
static let lastRefreshKey = "fuelboard.lastRefresh" // TimeInterval (seconds since 1970)
|
||||||
static let relaySourceKey = "fuelboard.relaySource" // String — "api" | "csv"
|
static let relaySourceKey = "fuelboard.relaySource" // String — "api" | "csv"
|
||||||
@@ -501,6 +503,32 @@ struct FuelStore {
|
|||||||
saveString(enabled ? "1" : "0", service: liveActivityKey)
|
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
|
// MARK: Refresh policy — data is cached; the app only auto-refreshes
|
||||||
// twice a day (pull-to-refresh is the manual override).
|
// twice a day (pull-to-refresh is the manual override).
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user