Replace lazy AppStorage calls with AccountManager
This commit is contained in:
@@ -0,0 +1,84 @@
|
|||||||
|
//
|
||||||
|
// AccountManager.swift
|
||||||
|
// meterios
|
||||||
|
//
|
||||||
|
// (c) 2025 Martin "maride" Dessauer
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// **AccountManager** stores connection details and the corresponding client
|
||||||
|
class AccountManager: ObservableObject {
|
||||||
|
// default contains a ready-to-use instance of AccountManager, read from AppStorage
|
||||||
|
static public var `default` = AccountManager()
|
||||||
|
|
||||||
|
// accounts holds all saved accounts
|
||||||
|
@Published private(set) var Accounts: Array<AccountManagerEntry> = []
|
||||||
|
|
||||||
|
// init creates a new AccountManager, reading saved account data from AppStorage
|
||||||
|
init() {
|
||||||
|
self.Accounts = AccountManager.readFromRaw()
|
||||||
|
}
|
||||||
|
|
||||||
|
// readFromRaw reads the current accounts from the stored data and transforms it to a handy Account Array
|
||||||
|
static private func readFromRaw() -> Array<AccountManagerEntry> {
|
||||||
|
@AppStorage("accounts") var rawAccs = Data()
|
||||||
|
do {
|
||||||
|
let simpleAccs = try JSONDecoder().decode([Account].self, from: rawAccs)
|
||||||
|
let richAccs = simpleAccs.map({ AccountManagerEntry($0) })
|
||||||
|
return richAccs
|
||||||
|
} catch {
|
||||||
|
print("Error parsing stored accounts from JSON: \(error)")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeToRaw writes the current account data to the AppStorage
|
||||||
|
private func writeToRaw() {
|
||||||
|
@AppStorage("accounts") var rawAccs = Data()
|
||||||
|
do {
|
||||||
|
let simpleAccs = Accounts.map({ $0.GetAccount() })
|
||||||
|
rawAccs = try JSONEncoder().encode(simpleAccs)
|
||||||
|
} catch {
|
||||||
|
print("Error writing current accounts to JSON: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentAccount returns the currently selected account if there is one
|
||||||
|
func CurrentAccount() -> AccountManagerEntry? {
|
||||||
|
if self.Accounts.count > 0 {
|
||||||
|
return self.Accounts.first
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAccount adds the given account if it doesn't exist yet
|
||||||
|
func AddAccount(_ account: Account) {
|
||||||
|
if IndexOfAccount(account) == nil {
|
||||||
|
Accounts.append(AccountManagerEntry(account))
|
||||||
|
writeToRaw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexOfAccount returns the index of the specified account identified by address and user ID
|
||||||
|
func IndexOfAccount(_ account: Account) -> Int? {
|
||||||
|
return Accounts.firstIndex(where: {
|
||||||
|
$0.GetAccount().GetAddress() == account.GetAddress() &&
|
||||||
|
$0.GetAccount().GetUserID() == account.GetUserID()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAccount removes the account specified by address and user ID of the given Account, if present
|
||||||
|
func RemoveAccount(_ account: Account) {
|
||||||
|
if IndexOfAccount(account) != nil {
|
||||||
|
Accounts.remove(at: IndexOfAccount(account)!)
|
||||||
|
writeToRaw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAccount removes the account specified by address and user ID of the given AccountManagerEntry, if present
|
||||||
|
func RemoveAccount(_ account: AccountManagerEntry) {
|
||||||
|
RemoveAccount(account.GetAccount())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// BackendConnector.swift
|
// MeteClient.swift
|
||||||
// meterios
|
// meterios
|
||||||
//
|
//
|
||||||
// (c) 2025 Martin "maride" Dessauer
|
// (c) 2025 Martin "maride" Dessauer
|
||||||
@@ -7,15 +7,28 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum BackendConnectorError: Error {
|
enum MeteClientError: Error {
|
||||||
case StatusCodeLooksBad(explanation: String)
|
case StatusCodeLooksBad(explanation: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
class BackendConnector {
|
/// **MeteClient** is the client-side implementation of the Space Market API. Pure Swift, no platform-specific frameworks used.
|
||||||
|
class MeteClient {
|
||||||
|
private let address: String
|
||||||
|
private let userID: Int
|
||||||
|
|
||||||
|
convenience init(account: Account) {
|
||||||
|
self.init(address: account.GetAddress(), userID: account.GetUserID())
|
||||||
|
}
|
||||||
|
|
||||||
|
init(address: String, userID: Int) {
|
||||||
|
self.address = address
|
||||||
|
self.userID = userID
|
||||||
|
}
|
||||||
|
|
||||||
// GetDrinks returns the list of drinks available at the backend
|
// GetDrinks returns the list of drinks available at the backend
|
||||||
func GetDrinks(baseAddr: String) async throws -> Array<Drink> {
|
func GetDrinks() async throws -> Array<Drink> {
|
||||||
// Send HTTP request to the drinks endpoint
|
// Send HTTP request to the drinks endpoint
|
||||||
let url = URL(string: "https://\(baseAddr)/api/v1/drinks")
|
let url = URL(string: "https://\(address)/api/v1/drinks")
|
||||||
let (data, _) = try await URLSession.shared.data(from: url!)
|
let (data, _) = try await URLSession.shared.data(from: url!)
|
||||||
|
|
||||||
// Decode JSON to array of drinks
|
// Decode JSON to array of drinks
|
||||||
@@ -25,10 +38,10 @@ class BackendConnector {
|
|||||||
return drinks
|
return drinks
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUser returns the user object for the given ID<
|
// GetUser returns the user object for the given ID
|
||||||
func GetUser(baseAddr: String, userID: Int) async throws -> User {
|
func GetUser() async throws -> User {
|
||||||
// Send HTTP request to the user endpoint
|
// Send HTTP request to the user endpoint
|
||||||
let url = URL(string: "https://\(baseAddr)/api/v1/users/\(userID).json")
|
let url = URL(string: "https://\(address)/api/v1/users/\(userID).json")
|
||||||
let (data, _) = try await URLSession.shared.data(from: url!)
|
let (data, _) = try await URLSession.shared.data(from: url!)
|
||||||
|
|
||||||
// Decode JSON to array of drinks
|
// Decode JSON to array of drinks
|
||||||
@@ -39,9 +52,9 @@ class BackendConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetUsers returns all users registered at the backend
|
// GetUsers returns all users registered at the backend
|
||||||
func GetUsers(baseAddr: String) async throws -> Array<User> {
|
func GetUsers() async throws -> Array<User> {
|
||||||
// Send HTTP request to the user endpoint
|
// Send HTTP request to the user endpoint
|
||||||
let url = URL(string: "https://\(baseAddr)/api/v1/users.json")
|
let url = URL(string: "https://\(address)/api/v1/users.json")
|
||||||
let (data, _) = try await URLSession.shared.data(from: url!)
|
let (data, _) = try await URLSession.shared.data(from: url!)
|
||||||
|
|
||||||
// Decode JSON to array of drinks
|
// Decode JSON to array of drinks
|
||||||
@@ -52,16 +65,15 @@ class BackendConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BookDrink subtracts the recommended price off the balance from the user given by ID
|
// BookDrink subtracts the recommended price off the balance from the user given by ID
|
||||||
func BookDrink(baseAddr: String, userID: Int, drinkID: Int) async throws {
|
func BookDrink(_ drinkID: Int) async throws {
|
||||||
// Send HTTP request to the user endpoint
|
// Send HTTP request to the user endpoint
|
||||||
let url = URL(string: "https://\(baseAddr)/api/v1/users/\(userID)/buy.json?drink=\(drinkID)")
|
let url = URL(string: "https://\(address)/api/v1/users/\(userID)/buy.json?drink=\(drinkID)")
|
||||||
let (_, response) = try await URLSession.shared.data(from: url!)
|
let (_, response) = try await URLSession.shared.data(from: url!)
|
||||||
|
|
||||||
// Check status code
|
// Check status code
|
||||||
let statusCode = (response as? HTTPURLResponse)?.statusCode
|
let statusCode = (response as? HTTPURLResponse)?.statusCode
|
||||||
if statusCode != 204 {
|
if statusCode != 204 {
|
||||||
throw BackendConnectorError.StatusCodeLooksBad(explanation: "Failed to book drink \(drinkID) for user \(userID)@\(baseAddr): HTTP status code \(statusCode) received for \(url)")
|
throw MeteClientError.StatusCodeLooksBad(explanation: "Failed to book drink \(drinkID) for user \(userID)@\(address): HTTP status code \(statusCode) received for \(url)")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+12
-6
@@ -13,13 +13,13 @@ enum WCMeteriosMessageTypes: String {
|
|||||||
case BookDrink
|
case BookDrink
|
||||||
}
|
}
|
||||||
|
|
||||||
class WCManager: NSObject, WCSessionDelegate {
|
/// **WCManager** managed the Meterios-specific communication between an iOS and an watchOS device
|
||||||
@AppStorage("meteHostAddr") private var meteHostAddr = ""
|
class WCManager: NSObject, WCSessionDelegate, ObservableObject {
|
||||||
@AppStorage("meteUserID") private var meteUserID = -1
|
private var accountManager: AccountManager
|
||||||
|
|
||||||
private let session: WCSession = WCSession.default
|
private let session: WCSession = WCSession.default
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
|
self.accountManager = AccountManager.default
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
if (WCSession.isSupported()) {
|
if (WCSession.isSupported()) {
|
||||||
@@ -39,12 +39,18 @@ class WCManager: NSObject, WCSessionDelegate {
|
|||||||
// As Meterios currently only needs messages from watchOS to iOS and responses back to the watch, this function currently only implements iOS-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)
|
#if os(iOS)
|
||||||
|
// Prevent crashes when WCSession is received before AccountManager is ready
|
||||||
|
if accountManager.CurrentAccount() == nil {
|
||||||
|
print("Request encountered in WCSession, but AccountManager is not ready.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check for the different possible message types
|
// Check for the different possible message types
|
||||||
switch message["request"] as! String {
|
switch message["request"] as! String {
|
||||||
case WCMeteriosMessageTypes.DrinkList.rawValue:
|
case WCMeteriosMessageTypes.DrinkList.rawValue:
|
||||||
// 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 accountManager.CurrentAccount()!.GetClient().GetDrinks()
|
||||||
let favouriteDrinks = drinks.filter({ FavouritesManager.default.IsFavourite($0.id) && $0.active })
|
let favouriteDrinks = drinks.filter({ FavouritesManager.default.IsFavourite($0.id) && $0.active })
|
||||||
let availableDrinks = 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.
|
// note that drinks which are not active/available are not sent to the watch - the screen is too small to waste pixels.
|
||||||
@@ -64,7 +70,7 @@ class WCManager: NSObject, WCSessionDelegate {
|
|||||||
do {
|
do {
|
||||||
// Book
|
// Book
|
||||||
let drinkID = message["drinkID"] as! Int
|
let drinkID = message["drinkID"] as! Int
|
||||||
try await BackendConnector().BookDrink(baseAddr: meteHostAddr, userID: meteUserID, drinkID: drinkID)
|
try await accountManager.CurrentAccount()!.GetClient().BookDrink(drinkID)
|
||||||
replyHandler([
|
replyHandler([
|
||||||
"salut": SalutManager.getSalute()
|
"salut": SalutManager.getSalute()
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// Account.swift
|
||||||
|
// meterios
|
||||||
|
//
|
||||||
|
// (c) 2025 Martin "maride" Dessauer
|
||||||
|
//
|
||||||
|
|
||||||
|
struct Account: Encodable, Decodable {
|
||||||
|
private let address: String
|
||||||
|
private let userID: Int
|
||||||
|
|
||||||
|
init(address: String, userID: Int) {
|
||||||
|
self.address = address
|
||||||
|
self.userID = userID
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAddress() -> String {
|
||||||
|
return address
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserID() -> Int {
|
||||||
|
return userID
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,11 +9,11 @@ import SwiftUI
|
|||||||
|
|
||||||
@main
|
@main
|
||||||
struct meteriosWatchApp: App {
|
struct meteriosWatchApp: App {
|
||||||
private var wcMgr = WCManager()
|
@StateObject var wcMgr = WCManager()
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView(wcMgr)
|
ContentView().environmentObject(wcMgr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
private var wcMgr: WCManager
|
@EnvironmentObject var wcMgr: WCManager
|
||||||
|
|
||||||
@State private var favouriteDrinks: Array<Drink> = []
|
@State private var favouriteDrinks: Array<Drink> = []
|
||||||
@State private var availableDrinks: Array<Drink> = []
|
@State private var availableDrinks: Array<Drink> = []
|
||||||
@@ -17,10 +17,6 @@ struct ContentView: View {
|
|||||||
@State private var updateError: String = ""
|
@State private var updateError: String = ""
|
||||||
@State private var updateErrorCount: Int = 0
|
@State private var updateErrorCount: Int = 0
|
||||||
|
|
||||||
init(_ wcMgr: WCManager) {
|
|
||||||
self.wcMgr = wcMgr
|
|
||||||
}
|
|
||||||
|
|
||||||
// update performs a full update from the paired device, covering connection info (user ID and backend address) and list of drinks
|
// update performs a full update from the paired device, covering connection info (user ID and backend address) and list of drinks
|
||||||
func update() {
|
func update() {
|
||||||
if updateErrorCount >= 3 {
|
if updateErrorCount >= 3 {
|
||||||
@@ -55,14 +51,14 @@ struct ContentView: View {
|
|||||||
Section(String(localized: "FAVOURITES")) {
|
Section(String(localized: "FAVOURITES")) {
|
||||||
ForEach(favouriteDrinks) { drink in
|
ForEach(favouriteDrinks) { drink in
|
||||||
NavigationLink(drink.name) {
|
NavigationLink(drink.name) {
|
||||||
DrinkDetailView(wcMgr: wcMgr, drink: drink)
|
DrinkDetailView(drink: drink)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Section(String(localized: "DRINKS")) {
|
Section(String(localized: "DRINKS")) {
|
||||||
ForEach(availableDrinks) { drink in
|
ForEach(availableDrinks) { drink in
|
||||||
NavigationLink(drink.name) {
|
NavigationLink(drink.name) {
|
||||||
DrinkDetailView(wcMgr: wcMgr, drink: drink)
|
DrinkDetailView(drink: drink)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,15 +8,15 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct DrinkDetailView: View {
|
struct DrinkDetailView: View {
|
||||||
private var wcMgr: WCManager
|
@EnvironmentObject var wcMgr: WCManager
|
||||||
|
|
||||||
private var drink: Drink
|
private var drink: Drink
|
||||||
|
|
||||||
@State var shouldShowTransactionSheet = false
|
@State var shouldShowTransactionSheet = false
|
||||||
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
init(wcMgr: WCManager, drink: Drink) {
|
init(drink: Drink) {
|
||||||
self.wcMgr = wcMgr
|
|
||||||
self.drink = drink
|
self.drink = drink
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ struct DrinkDetailView: View {
|
|||||||
// Sheet dismissed - dismiss detail perspective as well.
|
// Sheet dismissed - dismiss detail perspective as well.
|
||||||
dismiss()
|
dismiss()
|
||||||
} content: {
|
} content: {
|
||||||
TransactionView(wcMgr: wcMgr, drinkID: drink.id).interactiveDismissDisabled()
|
TransactionView(drinkID: drink.id).interactiveDismissDisabled()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct TransactionView: View {
|
struct TransactionView: View {
|
||||||
private var wcMgr: WCManager
|
@EnvironmentObject var wcMgr: WCManager
|
||||||
|
|
||||||
private var drinkID: Int
|
private var drinkID: Int
|
||||||
|
|
||||||
@State var statusLine = String(localized: "BOOKING")
|
@State var statusLine = String(localized: "BOOKING")
|
||||||
@@ -22,8 +23,7 @@ struct TransactionView: View {
|
|||||||
@State var backendBookTimer: Timer?
|
@State var backendBookTimer: Timer?
|
||||||
@State var backendTimerReady: Bool = false
|
@State var backendTimerReady: Bool = false
|
||||||
|
|
||||||
init(wcMgr: WCManager, drinkID: Int) {
|
init(drinkID: Int) {
|
||||||
self.wcMgr = wcMgr
|
|
||||||
self.drinkID = drinkID
|
self.drinkID = drinkID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,19 +40,16 @@
|
|||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
A7FA4CBE2D9E90B0005ACDBB /* Exceptions for "meterios" folder in "meterios-watch" target */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
|
||||||
membershipExceptions = (
|
|
||||||
BackendConnector.swift,
|
|
||||||
);
|
|
||||||
target = A7FA4CA92D9E8FF1005ACDBB /* meterios-watch */;
|
|
||||||
};
|
|
||||||
A7FA4CCB2D9E9564005ACDBB /* Exceptions for "common" folder in "meterios-watch" target */ = {
|
A7FA4CCB2D9E9564005ACDBB /* Exceptions for "common" folder in "meterios-watch" target */ = {
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
membershipExceptions = (
|
membershipExceptions = (
|
||||||
|
AccountManager.swift,
|
||||||
AsyncImageWithPlaceholder.swift,
|
AsyncImageWithPlaceholder.swift,
|
||||||
Localizable.xcstrings,
|
Localizable.xcstrings,
|
||||||
|
MeteClient.swift,
|
||||||
SalutManager.swift,
|
SalutManager.swift,
|
||||||
|
types/Account.swift,
|
||||||
|
types/AccountManagerEntry.swift,
|
||||||
types/Drink.swift,
|
types/Drink.swift,
|
||||||
types/User.swift,
|
types/User.swift,
|
||||||
WCManager.swift,
|
WCManager.swift,
|
||||||
@@ -64,9 +61,6 @@
|
|||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
A7F97A902D8DD542004EF4ED /* meterios */ = {
|
A7F97A902D8DD542004EF4ED /* meterios */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
|
||||||
A7FA4CBE2D9E90B0005ACDBB /* Exceptions for "meterios" folder in "meterios-watch" target */,
|
|
||||||
);
|
|
||||||
path = meterios;
|
path = meterios;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,17 +9,14 @@ import SwiftUI
|
|||||||
|
|
||||||
@main
|
@main
|
||||||
struct meteriosApp: App {
|
struct meteriosApp: App {
|
||||||
@AppStorage("meteHostAddr") private var meteHostAddr = ""
|
|
||||||
@AppStorage("meteUserID") private var meteUserID = -1
|
|
||||||
|
|
||||||
private var wcMgr = WCManager()
|
private var wcMgr = WCManager()
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
if meteHostAddr == "" || meteUserID == -1 {
|
if AccountManager.default.CurrentAccount() != nil {
|
||||||
SetupView()
|
|
||||||
} else {
|
|
||||||
MainView()
|
MainView()
|
||||||
|
} else {
|
||||||
|
SetupView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ import SwiftUI
|
|||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
struct DrinkDetail: View {
|
struct DrinkDetail: View {
|
||||||
@AppStorage("meteHostAddr") private var meteHostAddr = ""
|
|
||||||
@AppStorage("meteUserID") private var meteUserID = -1
|
|
||||||
|
|
||||||
private var drink: Drink
|
private var drink: Drink
|
||||||
@State private var drinkAmount: Int = 1
|
@State private var drinkAmount: Int = 1
|
||||||
@State var shouldShowTransactionSheet = false
|
@State var shouldShowTransactionSheet = false
|
||||||
@@ -26,88 +23,90 @@ struct DrinkDetail: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List() {
|
if AccountManager.default.CurrentAccount() != nil {
|
||||||
// Drink details
|
List() {
|
||||||
Section() {
|
// Drink details
|
||||||
HStack {
|
Section() {
|
||||||
Spacer()
|
HStack {
|
||||||
VStack() {
|
Spacer()
|
||||||
// Displays the drink image asynchronously
|
VStack() {
|
||||||
AsyncImageWithPlaceholder(url: URL(string: "https://\(meteHostAddr)/\(drink.logoURL)"), width: 300, height: 300).fixedSize()
|
// Displays the drink image asynchronously
|
||||||
|
AsyncImageWithPlaceholder(url: URL(string: "https://\(AccountManager.default.CurrentAccount()!.GetAccount().GetAddress())/\(drink.logoURL)"), width: 300, height: 300).fixedSize()
|
||||||
|
|
||||||
// Basic drink information
|
// Basic drink information
|
||||||
VStack(alignment: .center) {
|
VStack(alignment: .center) {
|
||||||
Text("\(drink.name)").font(.largeTitle).fontWeight(.bold)
|
Text("\(drink.name)").font(.largeTitle).fontWeight(.bold)
|
||||||
HStack {
|
HStack {
|
||||||
VStack {
|
VStack {
|
||||||
Text(String(format: "%img", drink.caffeine)).fontWeight(.bold)
|
Text(String(format: "%img", drink.caffeine)).fontWeight(.bold)
|
||||||
Text(String(localized: "CAFFEINE")).fontWeight(Font.Weight.thin)
|
Text(String(localized: "CAFFEINE")).fontWeight(Font.Weight.thin)
|
||||||
}.fixedSize()
|
}.fixedSize()
|
||||||
Divider()
|
Divider()
|
||||||
VStack {
|
VStack {
|
||||||
Text(drink.GetPrice().formatted(.currency(code: "EUR"))).fontWeight(.bold)
|
Text(drink.GetPrice().formatted(.currency(code: "EUR"))).fontWeight(.bold)
|
||||||
Text(String(localized: "PRICE")).fontWeight(Font.Weight.thin)
|
Text(String(localized: "PRICE")).fontWeight(Font.Weight.thin)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}.fixedSize()
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Favourites
|
||||||
|
Section() {
|
||||||
|
Toggle(String(localized: "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(String(format: String(localized: "AMOUNT_FMT"), self.drinkAmount), onIncrement: {
|
||||||
|
if self.drinkAmount < 10 {
|
||||||
|
self.drinkAmount += 1
|
||||||
|
}
|
||||||
|
}, onDecrement: {
|
||||||
|
if self.drinkAmount > 1 {
|
||||||
|
self.drinkAmount -= 1
|
||||||
}
|
}
|
||||||
}.fixedSize()
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Favourites
|
|
||||||
Section() {
|
|
||||||
Toggle(String(localized: "FAVOURITE"), isOn: $isFavourite).toggleStyle(SwitchToggleStyle(tint: .blue))
|
|
||||||
.onAppear(perform: {
|
|
||||||
isFavourite = FavouritesManager.default.IsFavourite(drink.id)
|
|
||||||
})
|
})
|
||||||
.onDisappear(perform: {
|
HStack {
|
||||||
FavouritesManager.default.SetFavourite(drinkID: self.drink.id, shouldBeFavourite: isFavourite)
|
Text(String(localized: "TOTAL_SUM"))
|
||||||
})
|
Spacer()
|
||||||
}
|
Text((self.drink.GetPrice() * Float(self.drinkAmount)).formatted(.currency(code: "EUR"))).fontWeight(.bold)
|
||||||
|
|
||||||
// Summary
|
|
||||||
Section {
|
|
||||||
Stepper(String(format: String(localized: "AMOUNT_FMT"), self.drinkAmount), onIncrement: {
|
|
||||||
if self.drinkAmount < 10 {
|
|
||||||
self.drinkAmount += 1
|
|
||||||
}
|
}
|
||||||
}, onDecrement: {
|
|
||||||
if self.drinkAmount > 1 {
|
|
||||||
self.drinkAmount -= 1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
HStack {
|
|
||||||
Text(String(localized: "TOTAL_SUM"))
|
|
||||||
Spacer()
|
|
||||||
Text((self.drink.GetPrice() * Float(self.drinkAmount)).formatted(.currency(code: "EUR"))).fontWeight(.bold)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Warning for inactive bewerages
|
||||||
|
if !drink.active {
|
||||||
|
Section(
|
||||||
|
header: Text(String(format: String(localized: "INACTIVE_FMT"), drink.name)),
|
||||||
|
content: {
|
||||||
|
Toggle(String(format: String(localized: "AVAILABLE_FMT"), drink.name), isOn: $overrideDeactivatedState).toggleStyle(SwitchToggleStyle(tint: .green))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick button
|
||||||
|
Button(action: {
|
||||||
|
// Open the transaction sheet
|
||||||
|
shouldShowTransactionSheet = true
|
||||||
|
}) {
|
||||||
|
Text(String(localized: "POISON_PICKED")).frame(maxWidth: .infinity)
|
||||||
|
}.tint(.green).disabled(!drink.active && !overrideDeactivatedState)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warning for inactive bewerages
|
// Transaction sheet
|
||||||
if !drink.active {
|
.sheet(isPresented: $shouldShowTransactionSheet) {
|
||||||
Section(
|
// Sheet dismissed - dismiss detail perspective as well.
|
||||||
header: Text(String(format: String(localized: "INACTIVE_FMT"), drink.name)),
|
dismiss()
|
||||||
content: {
|
} content: {
|
||||||
Toggle(String(format: String(localized: "AVAILABLE_FMT"), drink.name), isOn: $overrideDeactivatedState).toggleStyle(SwitchToggleStyle(tint: .green))
|
TransactionView(drinkID: drink.id, drinkAmount: drinkAmount).interactiveDismissDisabled()
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick button
|
|
||||||
Button(action: {
|
|
||||||
// Open the transaction sheet
|
|
||||||
shouldShowTransactionSheet = true
|
|
||||||
}) {
|
|
||||||
Text(String(localized: "POISON_PICKED")).frame(maxWidth: .infinity)
|
|
||||||
}.tint(.green).disabled(!drink.active && !overrideDeactivatedState)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transaction sheet
|
|
||||||
.sheet(isPresented: $shouldShowTransactionSheet) {
|
|
||||||
// Sheet dismissed - dismiss detail perspective as well.
|
|
||||||
dismiss()
|
|
||||||
} content: {
|
|
||||||
TransactionView(drinkID: drink.id, drinkAmount: drinkAmount).interactiveDismissDisabled()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ import SwiftUI
|
|||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
struct MainView: View {
|
struct MainView: View {
|
||||||
@AppStorage("meteHostAddr") private var meteHostAddr = ""
|
|
||||||
@AppStorage("meteUserID") private var meteUserID = -1
|
|
||||||
|
|
||||||
@State private var favouriteDrinks: Array<Drink> = []
|
@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> = []
|
||||||
@@ -24,14 +21,14 @@ struct MainView: View {
|
|||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
// Load user
|
// Load user
|
||||||
user = try await BackendConnector().GetUser(baseAddr: meteHostAddr, userID: meteUserID)
|
user = try await AccountManager.default.CurrentAccount()!.GetClient().GetUser()
|
||||||
// Load drinks
|
// Load drinks
|
||||||
let drinks = try await BackendConnector().GetDrinks(baseAddr: meteHostAddr)
|
let drinks = try await AccountManager.default.CurrentAccount()!.GetClient().GetDrinks()
|
||||||
favouriteDrinks = drinks.filter({ FavouritesManager.default.IsFavourite($0.id) && $0.active })
|
favouriteDrinks = drinks.filter({ FavouritesManager.default.IsFavourite($0.id) && $0.active })
|
||||||
availableDrinks = drinks.filter({ !FavouritesManager.default.IsFavourite($0.id) && $0.active })
|
availableDrinks = drinks.filter({ !FavouritesManager.default.IsFavourite($0.id) && $0.active })
|
||||||
outOfOrderDrinks = drinks.filter({ !$0.active })
|
outOfOrderDrinks = drinks.filter({ !$0.active })
|
||||||
} catch {
|
} catch {
|
||||||
let _ = Logger().error("Failed to update from \(meteHostAddr): \(error)")
|
let _ = Logger().error("Failed to update from \(AccountManager.default.CurrentAccount()!.GetAccount().GetAddress()): \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,7 +41,7 @@ struct MainView: View {
|
|||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
}, label: {
|
}, label: {
|
||||||
AsyncImageWithPlaceholder(url: URL(string: "https://\(meteHostAddr)/\(drink.logoURL)"), width: 32, height: 32)
|
AsyncImageWithPlaceholder(url: URL(string: "https://\(AccountManager.default.CurrentAccount()!.GetAccount().GetAddress())/\(drink.logoURL)"), width: 32, height: 32)
|
||||||
Text(drink.name)
|
Text(drink.name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -52,11 +49,11 @@ struct MainView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollViewReader { scrollViewReader in
|
if AccountManager.default.CurrentAccount() != nil {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
VStack(alignment: .leading) {
|
List () {
|
||||||
List () {
|
// Account
|
||||||
// Account
|
if user != nil {
|
||||||
Section(header: Text(String(localized: "ACCOUNT")).id("topAnchor")) {
|
Section(header: Text(String(localized: "ACCOUNT")).id("topAnchor")) {
|
||||||
NavigationLink(destination: UserDetailView(user: user!)) {
|
NavigationLink(destination: UserDetailView(user: user!)) {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
@@ -70,45 +67,45 @@ struct MainView: View {
|
|||||||
// Account, Address & Balance
|
// Account, Address & Balance
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack {
|
HStack {
|
||||||
Text(user.displayName).fontWeight(.bold)
|
Text(user!.displayName).fontWeight(.bold)
|
||||||
Divider()
|
Divider()
|
||||||
Text(user.balance.formatted(.currency(code: "EUR"))).foregroundStyle(user.balance > 0 ? .green : .red)
|
Text(user!.balance.formatted(.currency(code: "EUR"))).foregroundStyle(user!.balance > 0 ? .green : .red)
|
||||||
}.fixedSize()
|
}.fixedSize()
|
||||||
Text("\(meteHostAddr)").fontWeight(.thin)
|
Text("\(AccountManager.default.CurrentAccount()!.GetAccount().GetAddress())").fontWeight(.thin)
|
||||||
}.fixedSize()
|
}.fixedSize()
|
||||||
}.fixedSize()
|
}.fixedSize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Drinks list
|
// Drinks list
|
||||||
if !favouriteDrinks.isEmpty {
|
if !favouriteDrinks.isEmpty {
|
||||||
drinkSection(title: String(localized: "FAVOURITES"), drinks: favouriteDrinks)
|
drinkSection(title: String(localized: "FAVOURITES"), drinks: favouriteDrinks)
|
||||||
}
|
}
|
||||||
if !availableDrinks.isEmpty {
|
if !availableDrinks.isEmpty {
|
||||||
drinkSection(title: String(localized: "AVAILABLE_DRINKS"), drinks: availableDrinks)
|
drinkSection(title: String(localized: "AVAILABLE_DRINKS"), drinks: availableDrinks)
|
||||||
}
|
}
|
||||||
if !outOfOrderDrinks.isEmpty {
|
if !outOfOrderDrinks.isEmpty {
|
||||||
drinkSection(title: String(localized: "OUT_OF_STOCK_DRINKS"), drinks: outOfOrderDrinks)
|
drinkSection(title: String(localized: "OUT_OF_STOCK_DRINKS"), drinks: outOfOrderDrinks)
|
||||||
}
|
}
|
||||||
// Catch a backend without drinks
|
// Catch a backend without drinks
|
||||||
if favouriteDrinks.isEmpty && availableDrinks.isEmpty && outOfOrderDrinks.isEmpty {
|
if favouriteDrinks.isEmpty && availableDrinks.isEmpty && outOfOrderDrinks.isEmpty {
|
||||||
Section(content: {}, footer: {
|
Section(content: {}, footer: {
|
||||||
HStack(alignment: .center, content: {
|
HStack(alignment: .center, content: {
|
||||||
Spacer()
|
Spacer()
|
||||||
VStack() {
|
VStack() {
|
||||||
Image(systemName: "bolt.horizontal.circle.fill").font(.title).foregroundStyle(.gray)
|
Image(systemName: "bolt.horizontal.circle.fill").font(.title).foregroundStyle(.gray)
|
||||||
Spacer(minLength: 8)
|
Spacer(minLength: 8)
|
||||||
Text(String(localized: "NO_DRINKS")).font(.title).foregroundStyle(.gray)
|
Text(String(localized: "NO_DRINKS")).font(.title).foregroundStyle(.gray)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
}.navigationTitle(String(localized: "METERIOS")).refreshable(action: {
|
}
|
||||||
update()
|
}.navigationTitle(String(localized: "METERIOS")).refreshable(action: {
|
||||||
})
|
update()
|
||||||
}
|
})
|
||||||
}.onAppear(perform: {
|
}.onAppear(perform: {
|
||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ import SwiftUI
|
|||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
struct SetupView: View {
|
struct SetupView: View {
|
||||||
@AppStorage("meteHostAddr") private var meteHostAddr = ""
|
|
||||||
@AppStorage("meteUserID") private var meteUserID = -1
|
|
||||||
|
|
||||||
@State private var tmpAddress: String = "mete.chaosdorf.space"
|
@State private var tmpAddress: String = "mete.chaosdorf.space"
|
||||||
@State private var tmpUsers: Array<User> = []
|
@State private var tmpUsers: Array<User> = []
|
||||||
@State private var selectedUserID: Int = 0
|
@State private var selectedUserID: Int = 0
|
||||||
@@ -28,12 +25,6 @@ struct SetupView: View {
|
|||||||
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
init() {
|
|
||||||
if meteHostAddr != "" {
|
|
||||||
self.tmpAddress = meteHostAddr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func tryConnect() {
|
func tryConnect() {
|
||||||
// If there was a previous connection attempt, the user list and save button might still be visible - hide them again
|
// If there was a previous connection attempt, the user list and save button might still be visible - hide them again
|
||||||
userListVisible = false
|
userListVisible = false
|
||||||
@@ -54,7 +45,8 @@ struct SetupView: View {
|
|||||||
// Create test connector
|
// Create test connector
|
||||||
do {
|
do {
|
||||||
// Try out the connection
|
// Try out the connection
|
||||||
tmpUsers = try await BackendConnector().GetUsers(baseAddr: tmpAddress)
|
let tmpConnector = MeteClient(address: tmpAddress, userID: -1)
|
||||||
|
tmpUsers = try await tmpConnector.GetUsers()
|
||||||
connectionStatus = String(localized: "CONNECTION_SUCCESSFUL")
|
connectionStatus = String(localized: "CONNECTION_SUCCESSFUL")
|
||||||
connectionStatusIcon = "checkmark.icloud.fill"
|
connectionStatusIcon = "checkmark.icloud.fill"
|
||||||
connectionStatusColor = .green
|
connectionStatusColor = .green
|
||||||
@@ -101,7 +93,7 @@ struct SetupView: View {
|
|||||||
// Connection
|
// Connection
|
||||||
Section(content: {
|
Section(content: {
|
||||||
// Address field
|
// Address field
|
||||||
TextField(text: $tmpAddress, label: { Text(meteHostAddr) })
|
TextField(text: $tmpAddress, label: { Text(tmpAddress) })
|
||||||
.textInputAutocapitalization(.never)
|
.textInputAutocapitalization(.never)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
.focused($connectionFiledFocused)
|
.focused($connectionFiledFocused)
|
||||||
@@ -146,8 +138,7 @@ struct SetupView: View {
|
|||||||
// Save button
|
// Save button
|
||||||
Section {
|
Section {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
meteHostAddr = tmpAddress
|
AccountManager.default.AddAccount(Account(address: tmpAddress, userID: selectedUserID))
|
||||||
meteUserID = selectedUserID
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}) {
|
}) {
|
||||||
Text(String(localized: "SAVE_CONNECTION_SETTINGS")).frame(maxWidth: .infinity)
|
Text(String(localized: "SAVE_CONNECTION_SETTINGS")).frame(maxWidth: .infinity)
|
||||||
|
|||||||
@@ -8,9 +8,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct TransactionView: View {
|
struct TransactionView: View {
|
||||||
@AppStorage("meteHostAddr") private var meteHostAddr = ""
|
|
||||||
@AppStorage("meteUserID") private var meteUserID = -1
|
|
||||||
|
|
||||||
var drinkID: Int
|
var drinkID: Int
|
||||||
var drinkAmount: Int
|
var drinkAmount: Int
|
||||||
|
|
||||||
@@ -43,7 +40,7 @@ struct TransactionView: View {
|
|||||||
do {
|
do {
|
||||||
// Try to book the drink(s)
|
// Try to book the drink(s)
|
||||||
for _ in 1...drinkAmount {
|
for _ in 1...drinkAmount {
|
||||||
try await BackendConnector().BookDrink(baseAddr: meteHostAddr, userID: meteUserID, drinkID: drinkID)
|
try await AccountManager.default.CurrentAccount()!.GetClient().BookDrink(drinkID)
|
||||||
}
|
}
|
||||||
statusLine = SalutManager.getSalute()
|
statusLine = SalutManager.getSalute()
|
||||||
backendTimerReady = true
|
backendTimerReady = true
|
||||||
@@ -57,27 +54,29 @@ struct TransactionView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
// Green checkmark or progress indicator
|
if AccountManager.default.CurrentAccount() != nil {
|
||||||
if backendTimerReady {
|
// Green checkmark or progress indicator
|
||||||
Image(systemName: "checkmark.circle.fill")
|
if backendTimerReady {
|
||||||
.font(.title)
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.foregroundStyle(.green)
|
.font(.title)
|
||||||
} else {
|
.foregroundStyle(.green)
|
||||||
ProgressView()
|
} else {
|
||||||
}
|
ProgressView()
|
||||||
|
}
|
||||||
|
|
||||||
// Status line, on success with salute
|
// Status line, on success with salute
|
||||||
Text(statusLine)
|
Text(statusLine)
|
||||||
.font(.title)
|
.font(.title)
|
||||||
.onAppear(perform: {
|
.onAppear(perform: {
|
||||||
closeTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { timer in
|
closeTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { timer in
|
||||||
closeTimerReady = true
|
closeTimerReady = true
|
||||||
maybeDismiss()
|
maybeDismiss()
|
||||||
}
|
}
|
||||||
backendBookTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { timer in
|
backendBookTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { timer in
|
||||||
book()
|
book()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.sensoryFeedback(.success, trigger: backendTimerReady)
|
.sensoryFeedback(.success, trigger: backendTimerReady)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,64 +8,63 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct UserDetailView: View {
|
struct UserDetailView: View {
|
||||||
@AppStorage("meteHostAddr") private var meteHostAddr = ""
|
|
||||||
@AppStorage("meteUserID") private var meteUserID = -1
|
|
||||||
|
|
||||||
var user: User
|
var user: User
|
||||||
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List() {
|
if AccountManager.default.CurrentAccount() != nil {
|
||||||
Section {
|
List() {
|
||||||
HStack(alignment: .center) {
|
Section {
|
||||||
Spacer()
|
HStack(alignment: .center) {
|
||||||
VStack(alignment: .center) {
|
Spacer()
|
||||||
// User image
|
|
||||||
AsyncImageWithPlaceholder(url: URL(string: user.displayImageURL), width: 256, height: 256).clipShape(Circle())
|
|
||||||
|
|
||||||
// Basic info about user
|
|
||||||
VStack(alignment: .center) {
|
VStack(alignment: .center) {
|
||||||
// User name
|
// User image
|
||||||
Text("\(user.displayName)").font(.largeTitle).fontWeight(.bold)
|
AsyncImageWithPlaceholder(url: URL(string: user.displayImageURL), width: 256, height: 256).clipShape(Circle())
|
||||||
// Balance and Mete backend address
|
|
||||||
HStack {
|
// Basic info about user
|
||||||
VStack {
|
VStack(alignment: .center) {
|
||||||
Text(user.balance.formatted(.currency(code: "EUR"))).fontWeight(.bold)
|
// User name
|
||||||
.foregroundStyle(user.balance > 0 ? .green : .red)
|
Text("\(user.displayName)").font(.largeTitle).fontWeight(.bold)
|
||||||
Text(String(localized: "BALANCE")).fontWeight(Font.Weight.thin)
|
// Balance and Mete backend address
|
||||||
}.fixedSize()
|
HStack {
|
||||||
Divider()
|
VStack {
|
||||||
VStack {
|
Text(user.balance.formatted(.currency(code: "EUR"))).fontWeight(.bold)
|
||||||
Text(String(meteHostAddr))
|
.foregroundStyle(user.balance > 0 ? .green : .red)
|
||||||
Text(String(localized: "METE_SERVER")).fontWeight(Font.Weight.thin)
|
Text(String(localized: "BALANCE")).fontWeight(Font.Weight.thin)
|
||||||
|
}.fixedSize()
|
||||||
|
Divider()
|
||||||
|
VStack {
|
||||||
|
Text(String(AccountManager.default.CurrentAccount()!.GetAccount().GetAddress()))
|
||||||
|
Text(String(localized: "METE_SERVER")).fontWeight(Font.Weight.thin)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}.fixedSize()
|
||||||
}.fixedSize()
|
}
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Open profile in browser
|
// Open profile in browser
|
||||||
Section() {
|
Section() {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
UIApplication.shared.open(URL(string: "https://\(meteHostAddr)/users/\(meteUserID)")!)
|
UIApplication.shared.open(URL(string: "https://\(AccountManager.default.CurrentAccount()!.GetAccount().GetAddress())/users/\(AccountManager.default.CurrentAccount()!.GetAccount().GetUserID())")!)
|
||||||
}) {
|
}) {
|
||||||
Text(String(localized: "OPEN_PROFILE_IN_BROWSER")).frame(maxWidth: .infinity)
|
Text(String(localized: "OPEN_PROFILE_IN_BROWSER")).frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Logout button
|
// Logout button
|
||||||
Section() {
|
Section() {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
// Log out. Save the User ID as -1, which is forbidden by the backend.
|
// Log out. Save the User ID as -1, which is forbidden by the backend.
|
||||||
// This forces Meterios to display the setup screen.
|
// This forces Meterios to display the setup screen.
|
||||||
meteUserID = -1
|
AccountManager.default.RemoveAccount(AccountManager.default.CurrentAccount()!)
|
||||||
dismiss()
|
dismiss()
|
||||||
}) {
|
}) {
|
||||||
Text(String(localized: "LOGOUT")).frame(maxWidth: .infinity)
|
Text(String(localized: "LOGOUT")).frame(maxWidth: .infinity)
|
||||||
}.tint(.red)
|
}.tint(.red)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user