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:
@@ -0,0 +1,8 @@
|
||||
//
|
||||
// AsyncImageWithPlaceholder.swift
|
||||
// meterios
|
||||
//
|
||||
// Created by Maximilian on 23.04.25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@@ -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"
|
||||
}
|
||||
@@ -23,7 +23,7 @@ struct User: Decodable, Identifiable, Hashable {
|
||||
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/%@",
|
||||
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.
|
||||
|
||||
@@ -52,14 +52,14 @@ struct ContentView: View {
|
||||
NavigationView {
|
||||
List() {
|
||||
// Drinks
|
||||
Section("Favourites") {
|
||||
Section(String(localized: "FAVOURITES")) {
|
||||
ForEach(favouriteDrinks) { drink in
|
||||
NavigationLink(drink.name) {
|
||||
DrinkDetailView(wcMgr: wcMgr, drink: drink)
|
||||
}
|
||||
}
|
||||
}
|
||||
Section("Drinks") {
|
||||
Section(String(localized: "DRINKS")) {
|
||||
ForEach(availableDrinks) { drink in
|
||||
NavigationLink(drink.name) {
|
||||
DrinkDetailView(wcMgr: wcMgr, drink: drink)
|
||||
@@ -69,7 +69,7 @@ struct ContentView: View {
|
||||
|
||||
// Controls
|
||||
Section {
|
||||
Button("Refresh") {
|
||||
Button(String(localized: "REFRESH")) {
|
||||
favouriteDrinks = []
|
||||
availableDrinks = []
|
||||
update()
|
||||
@@ -83,9 +83,9 @@ struct ContentView: View {
|
||||
}
|
||||
.onAppear(perform: update)
|
||||
// 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)
|
||||
Button("Retry", systemImage: "arrow.clockwise", action: update)
|
||||
Button(String(localized: "RETRY"), systemImage: "arrow.clockwise", action: update)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,12 @@ struct DrinkDetailView: View {
|
||||
HStack {
|
||||
VStack {
|
||||
Text(String(format: "%img", drink.caffeine)).fontWeight(.bold)
|
||||
Text("Caffeine").fontWeight(Font.Weight.thin)
|
||||
Text(String(localized: "CAFFEINE")).fontWeight(Font.Weight.thin)
|
||||
}
|
||||
Divider()
|
||||
VStack {
|
||||
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
|
||||
shouldShowTransactionSheet = true
|
||||
}) {
|
||||
Text("Poison picked!")
|
||||
Text(String(localized: "POISON_PICKED"))
|
||||
}.tint(.green)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ struct TransactionView: View {
|
||||
private var wcMgr: WCManager
|
||||
private var drinkID: Int
|
||||
|
||||
@State var statusLine = "Booking..."
|
||||
@State var statusLine = String(localized: "BOOKING")
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
A7FA4CCB2D9E9564005ACDBB /* Exceptions for "common" folder in "meterios-watch" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Localizable.xcstrings,
|
||||
SalutManager.swift,
|
||||
types/Drink.swift,
|
||||
types/User.swift,
|
||||
@@ -194,6 +195,7 @@
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
de,
|
||||
);
|
||||
mainGroup = A7F97A852D8DD542004EF4ED;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
@@ -312,6 +314,7 @@
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -370,6 +373,7 @@
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
@@ -398,7 +402,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.1;
|
||||
MARKETING_VERSION = 1.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
@@ -435,7 +439,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.1;
|
||||
MARKETING_VERSION = 1.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
@@ -465,7 +469,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.1;
|
||||
MARKETING_VERSION = 1.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios.watchkitapp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
@@ -494,7 +498,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.1;
|
||||
MARKETING_VERSION = 1.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios.watchkitapp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
|
||||
@@ -63,12 +63,12 @@ struct DrinkDetail: View {
|
||||
HStack {
|
||||
VStack {
|
||||
Text(String(format: "%img", drink.caffeine)).fontWeight(.bold)
|
||||
Text("Caffeine").fontWeight(Font.Weight.thin)
|
||||
Text(String(localized: "CAFFEINE")).fontWeight(Font.Weight.thin)
|
||||
}.fixedSize()
|
||||
Divider()
|
||||
VStack {
|
||||
Text(String(format: "%.2f€", drink.GetPrice())).fontWeight(.bold)
|
||||
Text("Price").fontWeight(Font.Weight.thin)
|
||||
Text(drink.GetPrice().formatted(.currency(code: "EUR"))).fontWeight(.bold)
|
||||
Text(String(localized: "PRICE")).fontWeight(Font.Weight.thin)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ struct DrinkDetail: View {
|
||||
|
||||
// Favourites
|
||||
Section() {
|
||||
Toggle("Favourite", isOn: $isFavourite).toggleStyle(SwitchToggleStyle(tint: .blue))
|
||||
Toggle(String(localized: "FAVOURITE"), isOn: $isFavourite).toggleStyle(SwitchToggleStyle(tint: .blue))
|
||||
.onAppear(perform: {
|
||||
isFavourite = FavouritesManager.default.IsFavourite(drink.id)
|
||||
})
|
||||
@@ -90,7 +90,7 @@ struct DrinkDetail: View {
|
||||
|
||||
// Summary
|
||||
Section {
|
||||
Stepper("Amount: **\(self.drinkAmount)x**", onIncrement: {
|
||||
Stepper(String(format: String(localized: "AMOUNT_FMT"), self.drinkAmount), onIncrement: {
|
||||
if self.drinkAmount < 10 {
|
||||
self.drinkAmount += 1
|
||||
}
|
||||
@@ -100,18 +100,18 @@ struct DrinkDetail: View {
|
||||
}
|
||||
})
|
||||
HStack {
|
||||
Text("Total sum:")
|
||||
Text(String(localized: "TOTAL_SUM"))
|
||||
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
|
||||
if !drink.active {
|
||||
Section(
|
||||
header: Text("\(drink.name) is marked inactive."),
|
||||
header: Text(String(format: String(localized: "INACTIVE_FMT"), drink.name)),
|
||||
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
|
||||
shouldShowTransactionSheet = true
|
||||
}) {
|
||||
Text("Poison picked!").frame(maxWidth: .infinity)
|
||||
Text(String(localized: "POISON_PICKED")).frame(maxWidth: .infinity)
|
||||
}.tint(.green).disabled(!drink.active && !overrideDeactivatedState)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,20 +27,57 @@ struct MainView: View {
|
||||
let drinks = try await BackendConnector().GetDrinks(baseAddr: meteHostAddr)
|
||||
favouriteDrinks = drinks.filter({ FavouritesManager.default.IsFavourite($0.id) && $0.active })
|
||||
availableDrinks = drinks.filter({ !FavouritesManager.default.IsFavourite($0.id) && $0.active })
|
||||
outOfOrderDrinks = drinks.filter({ !FavouritesManager.default.IsFavourite($0.id) && !$0.active })
|
||||
outOfOrderDrinks = drinks.filter({ !$0.active })
|
||||
} catch {
|
||||
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 {
|
||||
ScrollViewReader { scrollViewReader in
|
||||
NavigationView {
|
||||
VStack(alignment: .leading) {
|
||||
List () {
|
||||
// Account
|
||||
Section(header: Text("Account").id("topAnchor")) {
|
||||
Section(header: Text(String(localized: "ACCOUNT")).id("topAnchor")) {
|
||||
NavigationLink(destination: UserDetailView(user: user)) {
|
||||
Button(action: {
|
||||
dismiss()
|
||||
@@ -65,7 +102,7 @@ struct MainView: View {
|
||||
HStack {
|
||||
Text(user.displayName).fontWeight(.bold)
|
||||
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()
|
||||
Text("\(meteHostAddr)").fontWeight(.thin)
|
||||
}.fixedSize()
|
||||
@@ -76,39 +113,29 @@ struct MainView: View {
|
||||
|
||||
// Drinks list
|
||||
if !favouriteDrinks.isEmpty {
|
||||
Section(header: Text("Favourites")) {
|
||||
ForEach(favouriteDrinks) { drink in
|
||||
NavigationLink(drink.name) {
|
||||
DrinkDetail(drink: drink).onDisappear(perform: {
|
||||
update()
|
||||
scrollViewReader.scrollTo("topAnchor", anchor: .top)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
drinkSection(title: String(localized: "FAVOURITES"), drinks: favouriteDrinks)
|
||||
}
|
||||
if !availableDrinks.isEmpty {
|
||||
Section(header: Text("Available")) {
|
||||
ForEach(availableDrinks) { drink in
|
||||
NavigationLink(drink.name) {
|
||||
DrinkDetail(drink: drink).onDisappear(perform: {
|
||||
update()
|
||||
scrollViewReader.scrollTo("topAnchor", anchor: .top)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
drinkSection(title: String(localized: "AVAILABLE_DRINKS"), drinks: availableDrinks)
|
||||
}
|
||||
if !outOfOrderDrinks.isEmpty {
|
||||
Section(header: Text("Out of stock")) {
|
||||
ForEach(outOfOrderDrinks) { drink in
|
||||
NavigationLink(drink.name) {
|
||||
DrinkDetail(drink: drink)
|
||||
}
|
||||
}
|
||||
}
|
||||
drinkSection(title: String(localized: "OUT_OF_STOCK_DRINKS"), drinks: outOfOrderDrinks)
|
||||
}
|
||||
}.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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ struct SetupView: View {
|
||||
saveButtonEnabled = false
|
||||
|
||||
// Update status
|
||||
connectionStatus = "Connecting to \(tmpAddress)"
|
||||
connectionStatus = String(format: String(localized: "CONNECTING_TO_FMT"), tmpAddress)
|
||||
connectionStatusIcon = "network"
|
||||
connectionStatusColor = .blue
|
||||
connectionStatusProgressVisible = true
|
||||
@@ -55,7 +55,7 @@ struct SetupView: View {
|
||||
do {
|
||||
// Try out the connection
|
||||
tmpUsers = try await BackendConnector().GetUsers(baseAddr: tmpAddress)
|
||||
connectionStatus = "Connection successful"
|
||||
connectionStatus = String(localized: "CONNECTION_SUCCESSFUL")
|
||||
connectionStatusIcon = "checkmark.icloud.fill"
|
||||
connectionStatusColor = .green
|
||||
connectionStatusProgressVisible = false
|
||||
@@ -90,8 +90,8 @@ struct SetupView: View {
|
||||
.fixedSize()
|
||||
Spacer()
|
||||
VStack(alignment: .leading) {
|
||||
Text("Meterios").font(.title).fontWeight(.heavy).foregroundStyle(.white)
|
||||
Text("Pick your poison.").font(.title2).foregroundStyle(.white)
|
||||
Text(String(localized: "METERIOS")).font(.title).fontWeight(.heavy).foregroundStyle(.white)
|
||||
Text(String(localized: "METERIOS_SLOGAN")).font(.title2).foregroundStyle(.white)
|
||||
}.fixedSize()
|
||||
}.fixedSize()
|
||||
}
|
||||
@@ -109,11 +109,11 @@ struct SetupView: View {
|
||||
tryConnect()
|
||||
}
|
||||
|
||||
Button("Connect", action: {
|
||||
Button(String(localized: "CONNECT"), action: {
|
||||
tryConnect()
|
||||
})
|
||||
}, header: {
|
||||
Text("Connection")
|
||||
Text(String(localized: "CONNECTION"))
|
||||
}, footer: {
|
||||
// Status bar
|
||||
if connectionStatusVisible {
|
||||
@@ -133,12 +133,11 @@ struct SetupView: View {
|
||||
|
||||
// User selection
|
||||
if userListVisible {
|
||||
Picker("Account", selection: $selectedUserID, content: {
|
||||
Picker(String(localized: "ACCOUNT"), selection: $selectedUserID, content: {
|
||||
ForEach(tmpUsers, id: \.self.meteID) { (user: User) in
|
||||
Text(user.displayName).tag(user.displayName)
|
||||
}
|
||||
})
|
||||
.pickerStyle(.inline)
|
||||
.onChange(of: selectedUserID, {
|
||||
saveButtonEnabled = true
|
||||
})
|
||||
@@ -151,7 +150,7 @@ struct SetupView: View {
|
||||
meteUserID = selectedUserID
|
||||
dismiss()
|
||||
}) {
|
||||
Text("Save connection settings").frame(maxWidth: .infinity)
|
||||
Text(String(localized: "SAVE_CONNECTION_SETTINGS")).frame(maxWidth: .infinity)
|
||||
}.tint(.green).disabled(!saveButtonEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ struct TransactionView: View {
|
||||
var drinkID: Int
|
||||
var drinkAmount: Int
|
||||
|
||||
@State var statusLine = "Contacting Mete backend..."
|
||||
@State var statusLine = String(localized: "CONTACTING_BACKEND")
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@@ -50,7 +50,7 @@ struct TransactionView: View {
|
||||
maybeDismiss()
|
||||
} catch {
|
||||
// Error occurred
|
||||
statusLine = "Error in communication"
|
||||
statusLine = String(localized: "ERROR_IN_COMMUNICATION")
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,14 +40,14 @@ struct UserDetailView: View {
|
||||
// Balance and Mete backend address
|
||||
HStack {
|
||||
VStack {
|
||||
Text(String(format: "%.2f€", user.balance)).fontWeight(.bold)
|
||||
Text(user.balance.formatted(.currency(code: "EUR"))).fontWeight(.bold)
|
||||
.foregroundStyle(user.balance > 0 ? .green : .red)
|
||||
Text("Balance").fontWeight(Font.Weight.thin)
|
||||
Text(String(localized: "BALANCE")).fontWeight(Font.Weight.thin)
|
||||
}.fixedSize()
|
||||
Divider()
|
||||
VStack {
|
||||
Text(String(meteHostAddr))
|
||||
Text("Mete Server").fontWeight(Font.Weight.thin)
|
||||
Text(String(localized: "METE_SERVER")).fontWeight(Font.Weight.thin)
|
||||
}
|
||||
}
|
||||
}.fixedSize()
|
||||
@@ -61,7 +61,7 @@ struct UserDetailView: View {
|
||||
Button(action: {
|
||||
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
|
||||
dismiss()
|
||||
}) {
|
||||
Text("Log out").frame(maxWidth: .infinity)
|
||||
Text(String(localized: "LOGOUT")).frame(maxWidth: .infinity)
|
||||
}.tint(.red)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user