Add localization support, add drink icons in list, fix display error for out-of-stock favourites, fix low-res profile picture

This commit is contained in:
2025-04-23 22:35:46 +02:00
parent 5121d14b55
commit d6af1a7438
12 changed files with 674 additions and 71 deletions
+8
View File
@@ -0,0 +1,8 @@
//
// AsyncImageWithPlaceholder.swift
// meterios
//
// Created by Maximilian on 23.04.25.
//
import Foundation
+565
View File
@@ -0,0 +1,565 @@
{
"sourceLanguage" : "en",
"strings" : {
"" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : ""
}
}
},
"shouldTranslate" : false
},
"?" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "?"
}
}
},
"shouldTranslate" : false
},
"%@" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@"
}
}
},
"shouldTranslate" : false
},
"ACCOUNT" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Konto"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Account"
}
}
}
},
"AMOUNT_FMT" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Anzahl: %ix"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Amount: %ix"
}
}
}
},
"AVAILABLE_DRINKS" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Verfügbar"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Available"
}
}
}
},
"AVAILABLE_FMT" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@ ist verfügbar"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@ is available"
}
}
}
},
"BALANCE" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kontostand"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Balance"
}
}
}
},
"BOOKING" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Buchen…"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Booking..."
}
}
}
},
"CAFFEINE" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Koffein"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Caffeine"
}
}
}
},
"CONNECT" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Verbinden"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Connect"
}
}
}
},
"CONNECTING_TO_FMT" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Zu %@ verbinden"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Connecting to %@"
}
}
}
},
"CONNECTION" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Verbindung"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Connection"
}
}
}
},
"CONNECTION_ERROR" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Verbindungsfehler"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Connection Error"
}
}
}
},
"CONNECTION_SUCCESSFUL" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Verbindung erfolgreich"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Connection successful"
}
}
}
},
"CONTACTING_BACKEND" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Verbinden zu Mete…"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Contacting Mete backend..."
}
}
}
},
"DRINKS" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Getränke"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Drinks"
}
}
}
},
"ERROR_IN_COMMUNICATION" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Fehler in Kommunikation"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Error in communication"
}
}
}
},
"FAVOURITE" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Favorit"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Favourite"
}
}
}
},
"FAVOURITES" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Favoriten"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Favourites"
}
}
}
},
"INACTIVE_FMT" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@ ist als inaktiv markiert."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@ is marked inactive."
}
}
}
},
"LOGOUT" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ausloggen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Log out"
}
}
}
},
"METE_SERVER" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Mete Server"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Mete Server"
}
}
}
},
"METERIOS" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Meterios"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Meterios"
}
}
}
},
"METERIOS_SLOGAN" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pick your poison."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pick your poison."
}
}
}
},
"NO_DRINKS" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Keine Drinks auf Server"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Server has no drinks"
}
}
}
},
"OPEN_PROFILE_IN_BROWSER" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Profil im Browser öffnen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Open profile in browser"
}
}
}
},
"OUT_OF_STOCK_DRINKS" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nicht im Haus"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Out of stock"
}
}
}
},
"POISON_PICKED" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sieht gut aus!"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Poison picked!"
}
}
}
},
"PRICE" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Preis"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Price"
}
}
}
},
"REFRESH" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Aktualisieren"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Refresh"
}
}
}
},
"RETRY" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Erneut versuchen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Retry"
}
}
}
},
"SAVE_CONNECTION_SETTINGS" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Verbindungsdaten speichern"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Save connection settings"
}
}
}
},
"TOTAL_SUM" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Summe"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Total sum"
}
}
}
}
},
"version" : "1.0"
}
+1 -1
View File
@@ -23,7 +23,7 @@ struct User: Decodable, Identifiable, Hashable {
self.meteID = try container.decode(Int.self, forKey: .id) self.meteID = try container.decode(Int.self, forKey: .id)
self.displayName = try container.decode(String.self, forKey: .name) self.displayName = try container.decode(String.self, forKey: .name)
self.displayImageURL = String( self.displayImageURL = String(
format: "https://secure.gravatar.com/avatar/%@", format: "https://secure.gravatar.com/avatar/%@?s=300",
Insecure.MD5.hash( Insecure.MD5.hash(
// e-mails may be empty as they are not required during the registration process in Mete // 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. // however, Gravatar requires them for the profile picture functionality.
+5 -5
View File
@@ -52,14 +52,14 @@ struct ContentView: View {
NavigationView { NavigationView {
List() { List() {
// Drinks // Drinks
Section("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(wcMgr: wcMgr, drink: drink)
} }
} }
} }
Section("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(wcMgr: wcMgr, drink: drink)
@@ -69,7 +69,7 @@ struct ContentView: View {
// Controls // Controls
Section { Section {
Button("Refresh") { Button(String(localized: "REFRESH")) {
favouriteDrinks = [] favouriteDrinks = []
availableDrinks = [] availableDrinks = []
update() update()
@@ -83,9 +83,9 @@ struct ContentView: View {
} }
.onAppear(perform: update) .onAppear(perform: update)
// Alert to communicate an error in communication with the paired device // Alert to communicate an error in communication with the paired device
.alert("Connection Error", isPresented: $showUpdateError) { .alert(String(localized: "CONNECTION_ERROR"), isPresented: $showUpdateError) {
Text(updateError).tint(.red).font(.footnote) Text(updateError).tint(.red).font(.footnote)
Button("Retry", systemImage: "arrow.clockwise", action: update) Button(String(localized: "RETRY"), systemImage: "arrow.clockwise", action: update)
} }
} }
} }
+3 -3
View File
@@ -29,12 +29,12 @@ struct DrinkDetailView: View {
HStack { HStack {
VStack { VStack {
Text(String(format: "%img", drink.caffeine)).fontWeight(.bold) Text(String(format: "%img", drink.caffeine)).fontWeight(.bold)
Text("Caffeine").fontWeight(Font.Weight.thin) Text(String(localized: "CAFFEINE")).fontWeight(Font.Weight.thin)
} }
Divider() Divider()
VStack { VStack {
Text(String(format: "%.2f€", drink.GetPrice())).fontWeight(.bold) Text(String(format: "%.2f€", drink.GetPrice())).fontWeight(.bold)
Text("Price").fontWeight(Font.Weight.thin) Text(String(localized: "PRICE")).fontWeight(Font.Weight.thin)
} }
} }
@@ -43,7 +43,7 @@ struct DrinkDetailView: View {
// Open the transaction sheet // Open the transaction sheet
shouldShowTransactionSheet = true shouldShowTransactionSheet = true
}) { }) {
Text("Poison picked!") Text(String(localized: "POISON_PICKED"))
}.tint(.green) }.tint(.green)
} }
+1 -1
View File
@@ -11,7 +11,7 @@ struct TransactionView: View {
private var wcMgr: WCManager private var wcMgr: WCManager
private var drinkID: Int private var drinkID: Int
@State var statusLine = "Booking..." @State var statusLine = String(localized: "BOOKING")
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
+8 -4
View File
@@ -50,6 +50,7 @@
A7FA4CCB2D9E9564005ACDBB /* Exceptions for "common" folder in "meterios-watch" target */ = { A7FA4CCB2D9E9564005ACDBB /* Exceptions for "common" folder in "meterios-watch" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet; isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = ( membershipExceptions = (
Localizable.xcstrings,
SalutManager.swift, SalutManager.swift,
types/Drink.swift, types/Drink.swift,
types/User.swift, types/User.swift,
@@ -194,6 +195,7 @@
knownRegions = ( knownRegions = (
en, en,
Base, Base,
de,
); );
mainGroup = A7F97A852D8DD542004EF4ED; mainGroup = A7F97A852D8DD542004EF4ED;
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;
@@ -312,6 +314,7 @@
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
}; };
name = Debug; name = Debug;
@@ -370,6 +373,7 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
VALIDATE_PRODUCT = YES; VALIDATE_PRODUCT = YES;
}; };
name = Release; name = Release;
@@ -398,7 +402,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.1; MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios; PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -435,7 +439,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.1; MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios; PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -465,7 +469,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.1; MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios.watchkitapp; PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos; SDKROOT = watchos;
@@ -494,7 +498,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.1; MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios.watchkitapp; PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos; SDKROOT = watchos;
+10 -10
View File
@@ -63,12 +63,12 @@ struct DrinkDetail: View {
HStack { HStack {
VStack { VStack {
Text(String(format: "%img", drink.caffeine)).fontWeight(.bold) Text(String(format: "%img", drink.caffeine)).fontWeight(.bold)
Text("Caffeine").fontWeight(Font.Weight.thin) Text(String(localized: "CAFFEINE")).fontWeight(Font.Weight.thin)
}.fixedSize() }.fixedSize()
Divider() Divider()
VStack { VStack {
Text(String(format: "%.2f€", drink.GetPrice())).fontWeight(.bold) Text(drink.GetPrice().formatted(.currency(code: "EUR"))).fontWeight(.bold)
Text("Price").fontWeight(Font.Weight.thin) Text(String(localized: "PRICE")).fontWeight(Font.Weight.thin)
} }
} }
} }
@@ -79,7 +79,7 @@ struct DrinkDetail: View {
// Favourites // Favourites
Section() { Section() {
Toggle("Favourite", isOn: $isFavourite).toggleStyle(SwitchToggleStyle(tint: .blue)) Toggle(String(localized: "FAVOURITE"), isOn: $isFavourite).toggleStyle(SwitchToggleStyle(tint: .blue))
.onAppear(perform: { .onAppear(perform: {
isFavourite = FavouritesManager.default.IsFavourite(drink.id) isFavourite = FavouritesManager.default.IsFavourite(drink.id)
}) })
@@ -90,7 +90,7 @@ struct DrinkDetail: View {
// Summary // Summary
Section { Section {
Stepper("Amount: **\(self.drinkAmount)x**", onIncrement: { Stepper(String(format: String(localized: "AMOUNT_FMT"), self.drinkAmount), onIncrement: {
if self.drinkAmount < 10 { if self.drinkAmount < 10 {
self.drinkAmount += 1 self.drinkAmount += 1
} }
@@ -100,18 +100,18 @@ struct DrinkDetail: View {
} }
}) })
HStack { HStack {
Text("Total sum:") Text(String(localized: "TOTAL_SUM"))
Spacer() Spacer()
Text(String(format: "%.2f€", self.drink.GetPrice() * Float(self.drinkAmount))).fontWeight(.bold) Text((self.drink.GetPrice() * Float(self.drinkAmount)).formatted(.currency(code: "EUR"))).fontWeight(.bold)
} }
} }
// Warning for inactive bewerages // Warning for inactive bewerages
if !drink.active { if !drink.active {
Section( Section(
header: Text("\(drink.name) is marked inactive."), header: Text(String(format: String(localized: "INACTIVE_FMT"), drink.name)),
content: { content: {
Toggle("\(drink.name) is available", isOn: $overrideDeactivatedState).toggleStyle(SwitchToggleStyle(tint: .green)) Toggle(String(format: String(localized: "AVAILABLE_FMT"), drink.name), isOn: $overrideDeactivatedState).toggleStyle(SwitchToggleStyle(tint: .green))
}) })
} }
@@ -120,7 +120,7 @@ struct DrinkDetail: View {
// Open the transaction sheet // Open the transaction sheet
shouldShowTransactionSheet = true shouldShowTransactionSheet = true
}) { }) {
Text("Poison picked!").frame(maxWidth: .infinity) Text(String(localized: "POISON_PICKED")).frame(maxWidth: .infinity)
}.tint(.green).disabled(!drink.active && !overrideDeactivatedState) }.tint(.green).disabled(!drink.active && !overrideDeactivatedState)
} }
+58 -31
View File
@@ -27,20 +27,57 @@ struct MainView: View {
let drinks = try await BackendConnector().GetDrinks(baseAddr: meteHostAddr) let drinks = try await BackendConnector().GetDrinks(baseAddr: meteHostAddr)
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({ !FavouritesManager.default.IsFavourite($0.id) && !$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 \(meteHostAddr): \(error)")
} }
} }
} }
func drinkSection(title: String, drinks: Array<Drink>) -> some View {
Section(header: Text(title)) {
ForEach(drinks) { drink in
NavigationLink(destination: {
DrinkDetail(drink: drink).onDisappear(perform: {
update()
})
}, label: {
AsyncImage(url: URL(string: "https://\(meteHostAddr)/\(drink.logoURL)")) { phase in
if let image = phase.image {
// Image loaded
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 32, height: 32)
.clipShape(RoundedRectangle(cornerRadius: 8.0))
} else if phase.error != nil {
// Error, display a dummy image (question mark)
let _ = Logger().error("Failed to load drink image for \(drink.name): \(phase.error)")
ZStack {
RoundedRectangle(cornerRadius: 8.0).foregroundStyle(.white).tint(.gray).frame(width: 32, height: 32)
Text("?").fontWeight(.bold)
}
} else {
// Loading Placeholder
ZStack {
RoundedRectangle(cornerRadius: 8.0).foregroundStyle(.white).tint(.gray).frame(width: 32, height: 32)
ProgressView()
}
}
}.fixedSize()
Text(drink.name)
})
}
}
}
var body: some View { var body: some View {
ScrollViewReader { scrollViewReader in ScrollViewReader { scrollViewReader in
NavigationView { NavigationView {
VStack(alignment: .leading) { VStack(alignment: .leading) {
List () { List () {
// Account // Account
Section(header: Text("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: {
dismiss() dismiss()
@@ -65,7 +102,7 @@ struct MainView: View {
HStack { HStack {
Text(user.displayName).fontWeight(.bold) Text(user.displayName).fontWeight(.bold)
Divider() Divider()
Text(String(format: "%.2f€", user.balance)).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("\(meteHostAddr)").fontWeight(.thin)
}.fixedSize() }.fixedSize()
@@ -76,39 +113,29 @@ struct MainView: View {
// Drinks list // Drinks list
if !favouriteDrinks.isEmpty { if !favouriteDrinks.isEmpty {
Section(header: Text("Favourites")) { drinkSection(title: String(localized: "FAVOURITES"), drinks: favouriteDrinks)
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")) { drinkSection(title: String(localized: "AVAILABLE_DRINKS"), drinks: availableDrinks)
ForEach(availableDrinks) { drink in
NavigationLink(drink.name) {
DrinkDetail(drink: drink).onDisappear(perform: {
update()
scrollViewReader.scrollTo("topAnchor", anchor: .top)
})
}
}
}
} }
if !outOfOrderDrinks.isEmpty { if !outOfOrderDrinks.isEmpty {
Section(header: Text("Out of stock")) { drinkSection(title: String(localized: "OUT_OF_STOCK_DRINKS"), drinks: outOfOrderDrinks)
ForEach(outOfOrderDrinks) { drink in
NavigationLink(drink.name) {
DrinkDetail(drink: drink)
}
}
}
} }
}.navigationTitle("Meterios").refreshable(action: { // Catch a backend without drinks
if favouriteDrinks.isEmpty && availableDrinks.isEmpty && outOfOrderDrinks.isEmpty {
Section(content: {}, footer: {
HStack(alignment: .center, content: {
Spacer()
VStack() {
Image(systemName: "bolt.horizontal.circle.fill").font(.title).foregroundStyle(.gray)
Spacer(minLength: 8)
Text(String(localized: "NO_DRINKS")).font(.title).foregroundStyle(.gray)
}
Spacer()
})
})
}
}.navigationTitle(String(localized: "METERIOS")).refreshable(action: {
update() update()
}) })
} }
+8 -9
View File
@@ -40,7 +40,7 @@ struct SetupView: View {
saveButtonEnabled = false saveButtonEnabled = false
// Update status // Update status
connectionStatus = "Connecting to \(tmpAddress)" connectionStatus = String(format: String(localized: "CONNECTING_TO_FMT"), tmpAddress)
connectionStatusIcon = "network" connectionStatusIcon = "network"
connectionStatusColor = .blue connectionStatusColor = .blue
connectionStatusProgressVisible = true connectionStatusProgressVisible = true
@@ -55,7 +55,7 @@ struct SetupView: View {
do { do {
// Try out the connection // Try out the connection
tmpUsers = try await BackendConnector().GetUsers(baseAddr: tmpAddress) tmpUsers = try await BackendConnector().GetUsers(baseAddr: tmpAddress)
connectionStatus = "Connection successful" connectionStatus = String(localized: "CONNECTION_SUCCESSFUL")
connectionStatusIcon = "checkmark.icloud.fill" connectionStatusIcon = "checkmark.icloud.fill"
connectionStatusColor = .green connectionStatusColor = .green
connectionStatusProgressVisible = false connectionStatusProgressVisible = false
@@ -90,8 +90,8 @@ struct SetupView: View {
.fixedSize() .fixedSize()
Spacer() Spacer()
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Meterios").font(.title).fontWeight(.heavy).foregroundStyle(.white) Text(String(localized: "METERIOS")).font(.title).fontWeight(.heavy).foregroundStyle(.white)
Text("Pick your poison.").font(.title2).foregroundStyle(.white) Text(String(localized: "METERIOS_SLOGAN")).font(.title2).foregroundStyle(.white)
}.fixedSize() }.fixedSize()
}.fixedSize() }.fixedSize()
} }
@@ -109,11 +109,11 @@ struct SetupView: View {
tryConnect() tryConnect()
} }
Button("Connect", action: { Button(String(localized: "CONNECT"), action: {
tryConnect() tryConnect()
}) })
}, header: { }, header: {
Text("Connection") Text(String(localized: "CONNECTION"))
}, footer: { }, footer: {
// Status bar // Status bar
if connectionStatusVisible { if connectionStatusVisible {
@@ -133,12 +133,11 @@ struct SetupView: View {
// User selection // User selection
if userListVisible { if userListVisible {
Picker("Account", selection: $selectedUserID, content: { Picker(String(localized: "ACCOUNT"), selection: $selectedUserID, content: {
ForEach(tmpUsers, id: \.self.meteID) { (user: User) in ForEach(tmpUsers, id: \.self.meteID) { (user: User) in
Text(user.displayName).tag(user.displayName) Text(user.displayName).tag(user.displayName)
} }
}) })
.pickerStyle(.inline)
.onChange(of: selectedUserID, { .onChange(of: selectedUserID, {
saveButtonEnabled = true saveButtonEnabled = true
}) })
@@ -151,7 +150,7 @@ struct SetupView: View {
meteUserID = selectedUserID meteUserID = selectedUserID
dismiss() dismiss()
}) { }) {
Text("Save connection settings").frame(maxWidth: .infinity) Text(String(localized: "SAVE_CONNECTION_SETTINGS")).frame(maxWidth: .infinity)
}.tint(.green).disabled(!saveButtonEnabled) }.tint(.green).disabled(!saveButtonEnabled)
} }
} }
+2 -2
View File
@@ -14,7 +14,7 @@ struct TransactionView: View {
var drinkID: Int var drinkID: Int
var drinkAmount: Int var drinkAmount: Int
@State var statusLine = "Contacting Mete backend..." @State var statusLine = String(localized: "CONTACTING_BACKEND")
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@@ -50,7 +50,7 @@ struct TransactionView: View {
maybeDismiss() maybeDismiss()
} catch { } catch {
// Error occurred // Error occurred
statusLine = "Error in communication" statusLine = String(localized: "ERROR_IN_COMMUNICATION")
print(error) print(error)
} }
} }
+5 -5
View File
@@ -40,14 +40,14 @@ struct UserDetailView: View {
// Balance and Mete backend address // Balance and Mete backend address
HStack { HStack {
VStack { VStack {
Text(String(format: "%.2f€", user.balance)).fontWeight(.bold) Text(user.balance.formatted(.currency(code: "EUR"))).fontWeight(.bold)
.foregroundStyle(user.balance > 0 ? .green : .red) .foregroundStyle(user.balance > 0 ? .green : .red)
Text("Balance").fontWeight(Font.Weight.thin) Text(String(localized: "BALANCE")).fontWeight(Font.Weight.thin)
}.fixedSize() }.fixedSize()
Divider() Divider()
VStack { VStack {
Text(String(meteHostAddr)) Text(String(meteHostAddr))
Text("Mete Server").fontWeight(Font.Weight.thin) Text(String(localized: "METE_SERVER")).fontWeight(Font.Weight.thin)
} }
} }
}.fixedSize() }.fixedSize()
@@ -61,7 +61,7 @@ struct UserDetailView: View {
Button(action: { Button(action: {
UIApplication.shared.open(URL(string: "https://\(meteHostAddr)/users/\(meteUserID)")!) UIApplication.shared.open(URL(string: "https://\(meteHostAddr)/users/\(meteUserID)")!)
}) { }) {
Text("Open profile in browser").frame(maxWidth: .infinity) Text(String(localized: "OPEN_PROFILE_IN_BROWSER")).frame(maxWidth: .infinity)
} }
} }
@@ -73,7 +73,7 @@ struct UserDetailView: View {
meteUserID = -1 meteUserID = -1
dismiss() dismiss()
}) { }) {
Text("Log out").frame(maxWidth: .infinity) Text(String(localized: "LOGOUT")).frame(maxWidth: .infinity)
}.tint(.red) }.tint(.red)
} }
} }