Transform to multi-account support
This commit is contained in:
+104
-8
@@ -5,20 +5,116 @@
|
||||
// (c) 2025 Martin "maride" Dessauer
|
||||
//
|
||||
|
||||
struct Account: Encodable, Decodable {
|
||||
private let address: String
|
||||
private let userID: Int
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
||||
init(address: String, userID: Int) {
|
||||
enum AccountState: Int {
|
||||
case Unknown // No connection attempt has been made yet; basically the startup state
|
||||
case Disconnected // Not connected as per user request
|
||||
case Failure // Last connection failed
|
||||
case Connected // Last connection succeeded
|
||||
}
|
||||
|
||||
/// **Account** resembles an user account in Mete and provides relevant functions to Meterios, like encoding and decoding, the set-up client for this very account and favourites management
|
||||
@Observable class Account: Identifiable, Codable {
|
||||
public let id: UUID = UUID()
|
||||
|
||||
private(set) var state: AccountState = .Unknown
|
||||
|
||||
private let client: MeteClient
|
||||
public let address: String
|
||||
public let userID: Int
|
||||
|
||||
private(set) var displayName: String?
|
||||
private(set) var displayImageURL: URL?
|
||||
private(set) var balance: Float?
|
||||
|
||||
private var favourites: [Int]
|
||||
|
||||
enum CodingKeys: String, CodingKey { case address, userID, favourites }
|
||||
|
||||
init(address: String, userID: Int, favourites: [Int] = []) {
|
||||
self.address = address
|
||||
self.userID = userID
|
||||
self.favourites = favourites
|
||||
self.client = MeteClient(address: address, userID: userID)
|
||||
}
|
||||
|
||||
required convenience init(from decoder : Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let address = try container.decode(String.self, forKey: .address)
|
||||
let userID = try container.decode(Int.self, forKey: .userID)
|
||||
let favourites = try container.decode([Int].self, forKey: .favourites)
|
||||
self.init(address: address, userID: userID, favourites: favourites)
|
||||
}
|
||||
|
||||
func GetAddress() -> String {
|
||||
return address
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(address, forKey: .address)
|
||||
try container.encode(userID, forKey: .userID)
|
||||
try container.encode(favourites, forKey: .favourites)
|
||||
}
|
||||
|
||||
func GetUserID() -> Int {
|
||||
return userID
|
||||
// Ping updates the state of the account by trying to connect to the backend
|
||||
func Ping() async {
|
||||
do {
|
||||
state = try await client.CanConnect() ? .Connected : .Disconnected
|
||||
} catch {
|
||||
state = .Failure
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh updates the account metadata; failure to do so changes the account state
|
||||
func Refresh() async {
|
||||
do {
|
||||
let user = try await client.GetUser()
|
||||
displayName = user.displayName
|
||||
displayImageURL = user.displayImageURL
|
||||
balance = user.balance
|
||||
state = .Connected
|
||||
} catch {
|
||||
let _ = Logger().error("Unable to refresh user account: \(error)")
|
||||
state = .Failure
|
||||
}
|
||||
}
|
||||
|
||||
// GetDrinks returns all drinks for this account and backend
|
||||
func GetDrinks() async throws -> Array<MeteClientDrink> {
|
||||
return try await client.GetDrinks()
|
||||
}
|
||||
|
||||
// BookDrink books the specified drink using this account
|
||||
func BookDrink(_ drinkID: Int) async throws {
|
||||
try await client.BookDrink(drinkID)
|
||||
await Refresh()
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if !IsFavourite(drinkID) {
|
||||
favourites.append(drinkID)
|
||||
}
|
||||
}
|
||||
|
||||
// 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!)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
//
|
||||
// AccountManagerEntry.swift
|
||||
// meterios
|
||||
//
|
||||
// (c) 2025 Martin "maride" Dessauer
|
||||
//
|
||||
|
||||
struct AccountManagerEntry {
|
||||
private let account: Account
|
||||
private let client: MeteClient
|
||||
|
||||
init(_ account: Account) {
|
||||
self.account = account
|
||||
self.client = MeteClient(account: account)
|
||||
}
|
||||
|
||||
func GetAccount() -> Account {
|
||||
return account
|
||||
}
|
||||
|
||||
func GetClient() -> MeteClient {
|
||||
return client
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
//
|
||||
// Drink.swift
|
||||
// meterios
|
||||
//
|
||||
// (c) 2025 Martin "maride" Dessauer
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct Drink: Identifiable, Encodable, Decodable {
|
||||
public var id: Int
|
||||
public var name: String
|
||||
public var logoURL: String
|
||||
private var price: String
|
||||
private var bottle_size: String
|
||||
public var caffeine: Int
|
||||
public var active: Bool
|
||||
|
||||
// KeyMap is an enum covering the field names as used by SpaceMarket API
|
||||
private enum KeyMap : String, CodingKey { case id, name, bottle_size, caffeine, price, active, logo_url }
|
||||
|
||||
// Init function to be used with JSONDecoder using SpaceMarket API's strange types
|
||||
init(from decoder : Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: KeyMap.self)
|
||||
self.id = try container.decode(Int.self, forKey: .id)
|
||||
self.name = try container.decode(String.self, forKey: .name)
|
||||
self.bottle_size = (try? container.decode(String.self, forKey: .bottle_size)) ?? ""
|
||||
self.caffeine = (try? container.decode(Int.self, forKey: .caffeine)) ?? 0
|
||||
self.price = (try? container.decode(String.self, forKey: .price)) ?? ""
|
||||
self.active = (try? container.decode(Bool.self, forKey: .active)) ?? false
|
||||
self.logoURL = (try? container.decode(String.self, forKey: .logo_url)) ?? ""
|
||||
}
|
||||
|
||||
// Init function, as straight-forward as it gets
|
||||
init(id: Int, name: String, logoURL: String = "", price: String = "", bottle_size: String = "", caffeine: Int = 0, active: Bool = true) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.logoURL = logoURL
|
||||
self.price = price
|
||||
self.bottle_size = bottle_size
|
||||
self.caffeine = caffeine
|
||||
self.active = active
|
||||
}
|
||||
|
||||
func GetPrice() -> Float {
|
||||
return Float(self.price) ?? 9999.99
|
||||
}
|
||||
|
||||
func GetBottleSize() -> Float {
|
||||
return Float(self.bottle_size) ?? 9999.99
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
//
|
||||
// User.swift
|
||||
// meterios
|
||||
//
|
||||
// (c) 2025 Martin "maride" Dessauer
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CryptoKit
|
||||
|
||||
struct User: Decodable, Identifiable, Hashable {
|
||||
var id: UUID = UUID() // Careful: meterios-internal ID, not Mete/Backend user accoint ID!
|
||||
var meteID: Int
|
||||
var displayName: String
|
||||
var displayImageURL: String
|
||||
var balance: Float
|
||||
|
||||
private enum KeyMap : String, CodingKey { case id, name, email, balance }
|
||||
|
||||
// Init function to be used with JSONDecoder, converting from SpaceMarket API's strange types to proper types
|
||||
init(from decoder : Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: KeyMap.self)
|
||||
self.meteID = try container.decode(Int.self, forKey: .id)
|
||||
self.displayName = try container.decode(String.self, forKey: .name)
|
||||
self.displayImageURL = String(
|
||||
format: "https://secure.gravatar.com/avatar/%@?s=300",
|
||||
Insecure.MD5.hash(
|
||||
// e-mails may be empty as they are not required during the registration process in Mete
|
||||
// however, Gravatar requires them for the profile picture functionality.
|
||||
// Default to the profile picture of md5("0"), which is simply the Gravatar logo on blue background
|
||||
data: (((try? container.decode(String.self, forKey: .email)) ?? "0").data(using: .utf8)!)
|
||||
).compactMap {
|
||||
String(format: "%02x", $0)
|
||||
}.joined()
|
||||
)
|
||||
self.balance = Float(try container.decode(String.self, forKey: .balance)) ?? -9999.99
|
||||
}
|
||||
|
||||
// Init function, as straight-forward as it gets
|
||||
init(meteID: Int, displayName: String, displayImageURL: String, balance: Float) {
|
||||
self.meteID = meteID
|
||||
self.displayName = displayName
|
||||
self.displayImageURL = displayImageURL
|
||||
self.balance = balance
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user