Favourites: drag-and-drop reorder per fuel; stored order drives single-widget favourite

This commit is contained in:
FuelBoard Contributor
2026-08-14 10:48:53 +01:00
parent aa95d214e8
commit fbf8c79feb
5 changed files with 139 additions and 43 deletions
+13 -1
View File
@@ -159,7 +159,8 @@ struct ContentView: View {
selectedFuel: selectedFuel,
location: location,
distanceUnit: distanceUnit,
onToggleFavourite: toggleFavourite
onToggleFavourite: toggleFavourite,
onReorder: reorderFavourites
)
.tabItem { Label("Favourites", systemImage: "star.fill") }
@@ -395,6 +396,17 @@ struct ContentView: View {
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
}
/// Drag-and-drop reorder from the Favourites tab persists the new order
/// and refreshes every surface that consumes it (widgets + geofence
/// priority, which follows the stored order).
private func reorderFavourites(_ newOrder: [FavouriteEntry]) {
favourites = newOrder
FuelStore.saveFavourites(favourites)
WidgetCenter.shared.reloadAllTimelines()
monitor.update(stations: stations, favourites: refreshedFavourites,
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
}
/// Fetches fresh prices, but only when the cache is stale unless
/// `force` is true (pull-to-refresh is the manual override).
private func refresh(force: Bool = false) async {
+46 -22
View File
@@ -1,10 +1,12 @@
import SwiftUI
/// Favourites tab starred stations, split by fuel type. Each fuel type
/// with at least one favourite appears as its own tab; that tab's favourites
/// are ranked cheapest-first for that fuel, with the cheapest called out.
/// Favourites are fuel-scoped entries starring a station only pins it for
/// the fuel you were viewing, so an Unleaded favourite never shows as Diesel.
/// with at least one favourite appears as its own tab. The list shows the
/// USER'S manual order (drag to reorder Edit button in the toolbar): the
/// FIRST station in each fuel's list is the one used as "the favourite" in
/// single widgets. Favourites are fuel-scoped entries starring a station
/// only pins it for the fuel you were viewing, so an Unleaded favourite
/// never shows as Diesel.
struct FavouritesView: View {
let favourites: [FavouriteEntry]
/// The app's currently selected fuel used only as the initial tab.
@@ -12,6 +14,8 @@ struct FavouritesView: View {
let location: Coordinate?
let distanceUnit: DistanceUnit
var onToggleFavourite: (FuelStation, FuelType) -> Void = { _, _ in }
/// Persists a reordered favourites array (after drag-and-drop).
var onReorder: ([FavouriteEntry]) -> Void = { _ in }
/// Fuel types that currently have at least one favourite these are the
/// only tabs shown (a fuel with no favourites gets no tab).
@@ -29,40 +33,50 @@ struct FavouritesView: View {
availableFuels.contains(fuel) ? fuel : (availableFuels.first ?? .e10)
}
/// Stations favourited for the active fuel, cheapest first (distance tiebreak).
private var ranked: [FuelStation] {
let available = favourites.filter { $0.fuel == activeFuel }.map(\.station)
return available.sorted { lhs, rhs in
let lPrice = lhs.prices[activeFuel]!
let rPrice = rhs.prices[activeFuel]!
if lPrice != rPrice { return lPrice < rPrice }
guard let location else { return false }
return lhs.distanceKM(to: location.lat, lng2: location.lng) <
rhs.distanceKM(to: location.lat, lng2: location.lng)
}
/// Stations favourited for the active fuel, in the USER'S stored order
/// NOT price-sorted. The first row is the favourite used by single
/// widgets (see footer note).
private var ordered: [FuelStation] {
favourites.filter { $0.fuel == activeFuel }.map(\.station)
}
/// Cheapest favourited station for the active fuel the RAG baseline and
/// the "cheapest favourite" callout, independent of the manual order.
private var cheapest: FuelStation? {
ordered.min { ($0.prices[activeFuel] ?? .infinity) < ($1.prices[activeFuel] ?? .infinity) }
}
private var cheapestPrice: Double? { cheapest?.prices[activeFuel] }
/// IDs favourited for the active fuel the star state on each row.
private var activeFuelFavouriteIDs: Set<String> {
Set(favourites.filter { $0.fuel == activeFuel }.map(\.station.id))
}
private var cheapest: FuelStation? { ranked.first }
private var cheapestPrice: Double? { cheapest?.prices[activeFuel] }
init(favourites: [FavouriteEntry],
selectedFuel: FuelType,
location: Coordinate?,
distanceUnit: DistanceUnit,
onToggleFavourite: @escaping (FuelStation, FuelType) -> Void = { _, _ in }) {
onToggleFavourite: @escaping (FuelStation, FuelType) -> Void = { _, _ in },
onReorder: @escaping ([FavouriteEntry]) -> Void = { _ in }) {
self.favourites = favourites
self.selectedFuel = selectedFuel
self.location = location
self.distanceUnit = distanceUnit
self.onToggleFavourite = onToggleFavourite
self.onReorder = onReorder
_fuel = State(initialValue: selectedFuel)
}
/// Drag-and-drop reorder (List edit mode): the active fuel's entries are
/// reordered within the global array; the fuel's block keeps its original
/// position and other fuels keep their relative order (FuelStore helper).
private func moveFavourite(fromOffsets source: IndexSet, toOffset destination: Int) {
onReorder(FuelStore.reorderedFavourites(
favourites, fuel: activeFuel,
fromOffsets: source, toOffset: destination
))
}
var body: some View {
NavigationStack {
List {
@@ -74,7 +88,7 @@ struct FavouritesView: View {
.foregroundStyle(.secondary)
Text("No favourites yet")
.font(.headline)
Text("Tap the star on any station in the Stations tab to pin it here, ranked by which is cheapest for that fuel.")
Text("Tap the star on any station in the Stations tab to pin it here. The first station in each list becomes the favourite used in single widgets — drag to put your pick first.")
.font(.caption)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
@@ -90,8 +104,8 @@ struct FavouritesView: View {
)
}
Section("\(activeFuel.displayName) favourites — cheapest first") {
ForEach(Array(ranked.enumerated()), id: \.element.id) { index, station in
Section {
ForEach(Array(ordered.enumerated()), id: \.element.id) { index, station in
StationRow(
station: station,
fuel: activeFuel,
@@ -103,6 +117,11 @@ struct FavouritesView: View {
onToggleFavourite: { onToggleFavourite(station, activeFuel) }
)
}
.onMove(perform: moveFavourite)
} header: {
Text("\(activeFuel.displayName) favourites")
} footer: {
Text("The first station is the favourite used in single widgets. Tap Edit, then drag to reorder.")
}
if let cheapest = cheapest {
@@ -115,6 +134,11 @@ struct FavouritesView: View {
}
}
.navigationTitle("Favourites")
.toolbar {
if !favourites.isEmpty {
EditButton()
}
}
}
}
}