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)?) {}
// 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) {
#if os(iOS)
// Check for the different possible message types
if message.contains(where: { $0.key == "request" && $0.value as! String == WCMeteriosMessageTypes.ConnectionInfo.rawValue }) {
// Request for ConnectionInfo
@@ -51,18 +52,17 @@ class WCManager: NSObject, WCSessionDelegate {
// Request for a list of available drinks
Task {
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
var drinkArray: Array<String> = []
for d in availableDrinks {
drinkArray.append(
try! JSONEncoder().encode(d).base64EncodedString()
)
}
let favouriteDrinkArray = favouriteDrinks.map { drink in try! JSONEncoder().encode(drink).base64EncodedString() }
let availableDrinkArray = availableDrinks.map { drink in try! JSONEncoder().encode(drink).base64EncodedString() }
replyHandler([
"drinks": drinkArray
"favouriteDrinks": favouriteDrinkArray,
"availableDrinks": availableDrinkArray
])
}
} else if message.contains(where: { $0.key == "request" && $0.value as! String == WCMeteriosMessageTypes.BookDrink.rawValue }) {
@@ -86,6 +86,7 @@ class WCManager: NSObject, WCSessionDelegate {
} else {
log("Error: received unknown request. \(message)")
}
#endif // os(iOS)
}
// 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
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([
"request": WCMeteriosMessageTypes.DrinkList.rawValue
], 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
var drinkArray: Array<Drink> = []
for d in drinks {
let jsonDecoded = Data(base64Encoded: d)
let drink = try! JSONDecoder().decode(Drink.self, from: jsonDecoded!)
drinkArray.append(drink)
}
let favouriteDrinksArray = favouriteDrinks.map { drink in try! JSONDecoder().decode(Drink.self, from: Data(base64Encoded: drink)!) }
let availableDrinksArray = availableDrinks.map { drink in try! JSONDecoder().decode(Drink.self, from: Data(base64Encoded: drink)!) }
// enjoy your drinks, replyHandler!
replyHandler(drinkArray)
replyHandler(favouriteDrinksArray, availableDrinksArray)
}, errorHandler: { error in
errorHandler(error)
})
+16 -6
View File
@@ -10,7 +10,8 @@ import SwiftUI
struct ContentView: View {
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 updateError: String = ""
@@ -30,9 +31,10 @@ struct ContentView: View {
// Update drinks list from paired device
wcMgr.requestDrinksList(
replyHandler: { drinks in
replyHandler: { favDrinks, availDrinks in
// Drinks received from backend
self.drinks = drinks
self.favouriteDrinks = favDrinks
self.availableDrinks = availDrinks
showUpdateError = false
}, errorHandler: { error in
// Error receiving drinks
@@ -46,12 +48,19 @@ struct ContentView: View {
var body: some View {
ZStack {
if drinks.count > 0 {
if favouriteDrinks.count + availableDrinks.count > 0 {
NavigationView {
List() {
// Drinks
Section("Favourites") {
ForEach(favouriteDrinks) { drink in
NavigationLink(drink.name) {
DrinkDetailView(wcMgr: wcMgr, drink: drink)
}
}
}
Section("Drinks") {
ForEach(drinks) { drink in
ForEach(availableDrinks) { drink in
NavigationLink(drink.name) {
DrinkDetailView(wcMgr: wcMgr, drink: drink)
}
@@ -61,7 +70,8 @@ struct ContentView: View {
// Controls
Section {
Button("Refresh") {
drinks = []
favouriteDrinks = []
availableDrinks = []
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 overrideDeactivatedState: Bool = false
@State var isFavourite: Bool = false
@Environment(\.dismiss) private var dismiss
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
Section {
Stepper("Amount: **\(self.drinkAmount)x**", onIncrement: {
+16 -2
View File
@@ -12,6 +12,7 @@ struct MainView: View {
@AppStorage("meteHostAddr") private var meteHostAddr = ""
@AppStorage("meteUserID") private var meteUserID = -1
@State private var favouriteDrinks: Array<Drink> = []
@State private var availableDrinks: Array<Drink> = []
@State private var outOfOrderDrinks: Array<Drink> = []
@State private var user: User = User.init(meteID: 0, displayName: "", displayImageURL: "", balance: 0.00)
@@ -24,8 +25,9 @@ struct MainView: View {
do {
user = try await BackendConnector().GetUser(baseAddr: meteHostAddr, userID: meteUserID)
let drinks = try await BackendConnector().GetDrinks(baseAddr: meteHostAddr)
availableDrinks = drinks.filter({ $0.active })
outOfOrderDrinks = drinks.filter({ !$0.active })
favouriteDrinks = drinks.filter({ FavouritesManager.default.IsFavourite($0.id) && $0.active })
availableDrinks = drinks.filter({ !FavouritesManager.default.IsFavourite($0.id) && $0.active })
outOfOrderDrinks = drinks.filter({ !FavouritesManager.default.IsFavourite($0.id) && !$0.active })
} catch {
let _ = Logger().error("Failed to update from \(meteHostAddr): \(error)")
}
@@ -73,6 +75,18 @@ struct MainView: View {
}
// 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 {
Section(header: Text("Available")) {
ForEach(availableDrinks) { drink in