Add Favourites feature
This commit is contained in:
+16
-18
@@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user