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.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.
+5 -5
View File
@@ -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)
}
}
}
+3 -3
View File
@@ -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)
}
+1 -1
View File
@@ -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
+8 -4
View File
@@ -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;
+10 -10
View File
@@ -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)
}
+56 -29
View File
@@ -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)
}
// 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("Meterios").refreshable(action: {
}.navigationTitle(String(localized: "METERIOS")).refreshable(action: {
update()
})
}
+8 -9
View File
@@ -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)
}
}
+2 -2
View File
@@ -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)
}
}
+5 -5
View File
@@ -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)
}
}