Add Favourites feature

This commit is contained in:
2025-04-04 19:45:10 +02:00
parent 384ee97cb7
commit 9984f2c943
5 changed files with 125 additions and 26 deletions
+16 -18
View File
@@ -38,8 +38,9 @@ class WCManager: NSObject, WCSessionDelegate {
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: (any Error)?) {} func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: (any Error)?) {}
// session(...) gets called for every incoming message through the WatchConnectivity channel // session(...) gets called for every incoming message through the WatchConnectivity channel
// As Meterios currently only needs messages from watch to device and responses back to the watch, this function currently only implements device-related responses. // As Meterios currently only needs messages from watchOS to iOS and responses back to the watch, this function currently only implements iOS-related responses.
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
#if os(iOS)
// Check for the different possible message types // Check for the different possible message types
if message.contains(where: { $0.key == "request" && $0.value as! String == WCMeteriosMessageTypes.ConnectionInfo.rawValue }) { if message.contains(where: { $0.key == "request" && $0.value as! String == WCMeteriosMessageTypes.ConnectionInfo.rawValue }) {
// Request for ConnectionInfo // Request for ConnectionInfo
@@ -51,18 +52,17 @@ class WCManager: NSObject, WCSessionDelegate {
// Request for a list of available drinks // Request for a list of available drinks
Task { Task {
let drinks = try await BackendConnector().GetDrinks(baseAddr: meteHostAddr) let drinks = try await BackendConnector().GetDrinks(baseAddr: meteHostAddr)
let availableDrinks = drinks.filter({ $0.active }) let favouriteDrinks = drinks.filter({ FavouritesManager.default.IsFavourite($0.id) && $0.active })
let availableDrinks = drinks.filter({ !FavouritesManager.default.IsFavourite($0.id) && $0.active })
// note that drinks which are not active/available are not sent to the watch - the screen is too small to waste pixels.
// Create a plist-compatible variant of the Drink object containing the required information // Create a plist-compatible variant of the Drink object containing the required information
var drinkArray: Array<String> = [] let favouriteDrinkArray = favouriteDrinks.map { drink in try! JSONEncoder().encode(drink).base64EncodedString() }
for d in availableDrinks { let availableDrinkArray = availableDrinks.map { drink in try! JSONEncoder().encode(drink).base64EncodedString() }
drinkArray.append(
try! JSONEncoder().encode(d).base64EncodedString()
)
}
replyHandler([ replyHandler([
"drinks": drinkArray "favouriteDrinks": favouriteDrinkArray,
"availableDrinks": availableDrinkArray
]) ])
} }
} else if message.contains(where: { $0.key == "request" && $0.value as! String == WCMeteriosMessageTypes.BookDrink.rawValue }) { } else if message.contains(where: { $0.key == "request" && $0.value as! String == WCMeteriosMessageTypes.BookDrink.rawValue }) {
@@ -86,6 +86,7 @@ class WCManager: NSObject, WCSessionDelegate {
} else { } else {
log("Error: received unknown request. \(message)") log("Error: received unknown request. \(message)")
} }
#endif // os(iOS)
} }
// requestConnectionInfo requests User ID and Host address from the paired device // requestConnectionInfo requests User ID and Host address from the paired device
@@ -100,22 +101,19 @@ class WCManager: NSObject, WCSessionDelegate {
} }
// requestDrinksList requests the list of drinks available at the backend from the paired device // requestDrinksList requests the list of drinks available at the backend from the paired device
func requestDrinksList(replyHandler: @escaping (Array<Drink>) -> Void, errorHandler: @escaping ((any Error)?) -> Void) { func requestDrinksList(replyHandler: @escaping (Array<Drink>, Array<Drink>) -> Void, errorHandler: @escaping ((any Error)?) -> Void) {
session.sendMessage([ session.sendMessage([
"request": WCMeteriosMessageTypes.DrinkList.rawValue "request": WCMeteriosMessageTypes.DrinkList.rawValue
], replyHandler: { reply in ], replyHandler: { reply in
let drinks = reply["drinks"] as! Array<String> let favouriteDrinks = reply["favouriteDrinks"] as! Array<String>
let availableDrinks = reply["availableDrinks"] as! Array<String>
// Create Drink objects from response // Create Drink objects from response
var drinkArray: Array<Drink> = [] let favouriteDrinksArray = favouriteDrinks.map { drink in try! JSONDecoder().decode(Drink.self, from: Data(base64Encoded: drink)!) }
for d in drinks { let availableDrinksArray = availableDrinks.map { drink in try! JSONDecoder().decode(Drink.self, from: Data(base64Encoded: drink)!) }
let jsonDecoded = Data(base64Encoded: d)
let drink = try! JSONDecoder().decode(Drink.self, from: jsonDecoded!)
drinkArray.append(drink)
}
// enjoy your drinks, replyHandler! // enjoy your drinks, replyHandler!
replyHandler(drinkArray) replyHandler(favouriteDrinksArray, availableDrinksArray)
}, errorHandler: { error in }, errorHandler: { error in
errorHandler(error) errorHandler(error)
}) })
+16 -6
View File
@@ -10,7 +10,8 @@ import SwiftUI
struct ContentView: View { struct ContentView: View {
private var wcMgr: WCManager private var wcMgr: WCManager
@State private var drinks: Array<Drink> = [] @State private var favouriteDrinks: Array<Drink> = []
@State private var availableDrinks: Array<Drink> = []
@State private var showUpdateError: Bool = false @State private var showUpdateError: Bool = false
@State private var updateError: String = "" @State private var updateError: String = ""
@@ -30,9 +31,10 @@ struct ContentView: View {
// Update drinks list from paired device // Update drinks list from paired device
wcMgr.requestDrinksList( wcMgr.requestDrinksList(
replyHandler: { drinks in replyHandler: { favDrinks, availDrinks in
// Drinks received from backend // Drinks received from backend
self.drinks = drinks self.favouriteDrinks = favDrinks
self.availableDrinks = availDrinks
showUpdateError = false showUpdateError = false
}, errorHandler: { error in }, errorHandler: { error in
// Error receiving drinks // Error receiving drinks
@@ -46,12 +48,19 @@ struct ContentView: View {
var body: some View { var body: some View {
ZStack { ZStack {
if drinks.count > 0 { if favouriteDrinks.count + availableDrinks.count > 0 {
NavigationView { NavigationView {
List() { List() {
// Drinks // Drinks
Section("Favourites") {
ForEach(favouriteDrinks) { drink in
NavigationLink(drink.name) {
DrinkDetailView(wcMgr: wcMgr, drink: drink)
}
}
}
Section("Drinks") { Section("Drinks") {
ForEach(drinks) { drink in ForEach(availableDrinks) { drink in
NavigationLink(drink.name) { NavigationLink(drink.name) {
DrinkDetailView(wcMgr: wcMgr, drink: drink) DrinkDetailView(wcMgr: wcMgr, drink: drink)
} }
@@ -61,7 +70,8 @@ struct ContentView: View {
// Controls // Controls
Section { Section {
Button("Refresh") { Button("Refresh") {
drinks = [] favouriteDrinks = []
availableDrinks = []
update() update()
} }
} }
+64
View File
@@ -0,0 +1,64 @@
//
// FavouritesManager.swift
// meterios
//
// (c) 2025 Martin "maride" Dessauer
//
import Foundation
import SwiftUI
class FavouritesManager {
// default contains a ready-to-use instance of FavouritesManager.
// Not that it would make sense to have multiple instances anyway.
static public var `default` = FavouritesManager()
// favourites contain the list of drink IDs marked as favourites
private var favourites: Array<Int>
init() {
self.favourites = FavouritesManager.readFromRaw()
}
// readFromRaw reads the current favourites from the stored string and transforms it to a handy Int Array
static private func readFromRaw() -> Array<Int> {
@AppStorage("favourites") var favs = ""
return favs.split(separator: ";").map({ Int($0)! })
}
// writeToRaw writes the current favourites to the AppStorage as String
private func writeToRaw() {
@AppStorage("favourites") var favs = ""
let strFavs: Array<String> = self.favourites.map({ String($0) })
favs = strFavs.joined(separator: ";")
}
// IsFavourite returns true if the given drink ID is a favourite
func IsFavourite(_ drinkID: Int) -> Bool {
return favourites.contains(drinkID)
}
// AddFavourite adds the given drink ID to the list of favourites
func AddFavourite(_ drinkID: Int) {
favourites.append(drinkID)
writeToRaw()
}
// RemoveFavourite removes the given drink ID from the favourites list
func RemoveFavourite(_ drinkID: Int) {
let index = favourites.firstIndex(where: { $0 == drinkID })
if index != nil {
favourites.remove(at: index!)
}
writeToRaw()
}
// SetFavourite sets if the given drink ID is a favourite
func SetFavourite(drinkID: Int, shouldBeFavourite: Bool) {
if IsFavourite(drinkID) && !shouldBeFavourite {
RemoveFavourite(drinkID)
} else if !IsFavourite(drinkID) && shouldBeFavourite {
AddFavourite(drinkID)
}
}
}
+13
View File
@@ -17,6 +17,8 @@ struct DrinkDetail: View {
@State var shouldShowTransactionSheet = false @State var shouldShowTransactionSheet = false
@State var overrideDeactivatedState: Bool = false @State var overrideDeactivatedState: Bool = false
@State var isFavourite: Bool = false
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
init(drink: Drink) { init(drink: Drink) {
@@ -75,6 +77,17 @@ struct DrinkDetail: View {
} }
} }
// Favourites
Section() {
Toggle("Favourite", isOn: $isFavourite).toggleStyle(SwitchToggleStyle(tint: .blue))
.onAppear(perform: {
isFavourite = FavouritesManager.default.IsFavourite(drink.id)
})
.onDisappear(perform: {
FavouritesManager.default.SetFavourite(drinkID: self.drink.id, shouldBeFavourite: isFavourite)
})
}
// Summary // Summary
Section { Section {
Stepper("Amount: **\(self.drinkAmount)x**", onIncrement: { Stepper("Amount: **\(self.drinkAmount)x**", onIncrement: {
+16 -2
View File
@@ -12,6 +12,7 @@ struct MainView: View {
@AppStorage("meteHostAddr") private var meteHostAddr = "" @AppStorage("meteHostAddr") private var meteHostAddr = ""
@AppStorage("meteUserID") private var meteUserID = -1 @AppStorage("meteUserID") private var meteUserID = -1
@State private var favouriteDrinks: Array<Drink> = []
@State private var availableDrinks: Array<Drink> = [] @State private var availableDrinks: Array<Drink> = []
@State private var outOfOrderDrinks: Array<Drink> = [] @State private var outOfOrderDrinks: Array<Drink> = []
@State private var user: User = User.init(meteID: 0, displayName: "", displayImageURL: "", balance: 0.00) @State private var user: User = User.init(meteID: 0, displayName: "", displayImageURL: "", balance: 0.00)
@@ -24,8 +25,9 @@ struct MainView: View {
do { do {
user = try await BackendConnector().GetUser(baseAddr: meteHostAddr, userID: meteUserID) user = try await BackendConnector().GetUser(baseAddr: meteHostAddr, userID: meteUserID)
let drinks = try await BackendConnector().GetDrinks(baseAddr: meteHostAddr) let drinks = try await BackendConnector().GetDrinks(baseAddr: meteHostAddr)
availableDrinks = drinks.filter({ $0.active }) favouriteDrinks = drinks.filter({ FavouritesManager.default.IsFavourite($0.id) && $0.active })
outOfOrderDrinks = drinks.filter({ !$0.active }) availableDrinks = drinks.filter({ !FavouritesManager.default.IsFavourite($0.id) && $0.active })
outOfOrderDrinks = drinks.filter({ !FavouritesManager.default.IsFavourite($0.id) && !$0.active })
} catch { } catch {
let _ = Logger().error("Failed to update from \(meteHostAddr): \(error)") let _ = Logger().error("Failed to update from \(meteHostAddr): \(error)")
} }
@@ -73,6 +75,18 @@ struct MainView: View {
} }
// Drinks list // Drinks list
if !favouriteDrinks.isEmpty {
Section(header: Text("Favourites")) {
ForEach(favouriteDrinks) { drink in
NavigationLink(drink.name) {
DrinkDetail(drink: drink).onDisappear(perform: {
update()
scrollViewReader.scrollTo("topAnchor", anchor: .top)
})
}
}
}
}
if !availableDrinks.isEmpty { if !availableDrinks.isEmpty {
Section(header: Text("Available")) { Section(header: Text("Available")) {
ForEach(availableDrinks) { drink in ForEach(availableDrinks) { drink in