From 384ee97cb77aa76e3c2a74d81ee653fabbeb99a0 Mon Sep 17 00:00:00 2001 From: maride Date: Fri, 4 Apr 2025 14:59:35 +0200 Subject: [PATCH] Add watchOS support --- README.md | 2 +- common/SalutManager.swift | 18 ++ common/WCManager.swift | 147 +++++++++++++ common/types/Drink.swift | 52 +++++ {meterios => common}/types/User.swift | 0 .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 14 ++ .../AppIcon.appiconset/mete-new.jpg | Bin 0 -> 53895 bytes meterios-watch/Assets.xcassets/Contents.json | 6 + meterios-watch/meteriosWatchApp.swift | 19 ++ meterios-watch/views/ContentView.swift | 81 +++++++ meterios-watch/views/DrinkDetailView.swift | 58 ++++++ meterios-watch/views/TransactionView.swift | 80 +++++++ meterios.xcodeproj/project.pbxproj | 197 +++++++++++++++++- .../xcschemes/meterios-watch.xcscheme | 92 ++++++++ .../xcshareddata/xcschemes/meterios.xcscheme | 78 +++++++ meterios/meteriosApp.swift | 2 + meterios/types/Drink.swift | 33 --- meterios/views/DrinkDetailView.swift | 4 +- meterios/views/TransactionView.swift | 11 +- 20 files changed, 857 insertions(+), 48 deletions(-) create mode 100644 common/SalutManager.swift create mode 100644 common/WCManager.swift create mode 100644 common/types/Drink.swift rename {meterios => common}/types/User.swift (100%) create mode 100644 meterios-watch/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 meterios-watch/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 meterios-watch/Assets.xcassets/AppIcon.appiconset/mete-new.jpg create mode 100644 meterios-watch/Assets.xcassets/Contents.json create mode 100644 meterios-watch/meteriosWatchApp.swift create mode 100644 meterios-watch/views/ContentView.swift create mode 100644 meterios-watch/views/DrinkDetailView.swift create mode 100644 meterios-watch/views/TransactionView.swift create mode 100644 meterios.xcodeproj/xcshareddata/xcschemes/meterios-watch.xcscheme create mode 100644 meterios.xcodeproj/xcshareddata/xcschemes/meterios.xcscheme delete mode 100644 meterios/types/Drink.swift diff --git a/README.md b/README.md index d6da4b5..4e6457e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,6 @@ # meterios -*Pick your poison*, now on iOS. +*Pick your poison*, now on iOS & watchOS. diff --git a/common/SalutManager.swift b/common/SalutManager.swift new file mode 100644 index 0000000..4a7ffa4 --- /dev/null +++ b/common/SalutManager.swift @@ -0,0 +1,18 @@ +// +// SalutManager.swift +// meterios +// +// (c) 2025 Martin "maride" Dessauer +// + +class SalutManager { + static let salutes: Array = [ + "Prost!", "Guten!", "Kippis!", "Cheers!", "Chin-Chin!", "Zum Wohl!", "Salut!", "לחיים", + "Jamas!", // thanks cocorilla + "Budmo!", "за здоровье!", "Sláinte!", "乾杯", "Noroc!", // thanks ChaosAyumi + ] + + static func getSalute() -> String { + return salutes[Int.random(in: 0.. Void) { + // Check for the different possible message types + if message.contains(where: { $0.key == "request" && $0.value as! String == WCMeteriosMessageTypes.ConnectionInfo.rawValue }) { + // Request for ConnectionInfo + replyHandler([ + "userID": meteUserID, + "hostAddr": meteHostAddr + ]) + } else if message.contains(where: { $0.key == "request" && $0.value as! String == WCMeteriosMessageTypes.DrinkList.rawValue }) { + // Request for a list of available drinks + Task { + let drinks = try await BackendConnector().GetDrinks(baseAddr: meteHostAddr) + let availableDrinks = drinks.filter({ $0.active }) + + // Create a plist-compatible variant of the Drink object containing the required information + var drinkArray: Array = [] + for d in availableDrinks { + drinkArray.append( + try! JSONEncoder().encode(d).base64EncodedString() + ) + } + + replyHandler([ + "drinks": drinkArray + ]) + } + } else if message.contains(where: { $0.key == "request" && $0.value as! String == WCMeteriosMessageTypes.BookDrink.rawValue }) { + // Book the given drink at the backend + Task { + do { + // Book + let drinkID = message["drinkID"] as! Int + try await BackendConnector().BookDrink(baseAddr: meteHostAddr, userID: meteUserID, drinkID: drinkID) + replyHandler([ + "salut": SalutManager.getSalute() + ]) + } catch { + // Error booking against the backend + replyHandler([ + "error": error, + "salut": "" + ]) + } + } + } else { + log("Error: received unknown request. \(message)") + } + } + + // requestConnectionInfo requests User ID and Host address from the paired device + func requestConnectionInfo(replyHandler: @escaping (Int, String) -> Void, errorHandler: @escaping ((any Error)?) -> Void) { + session.sendMessage([ + "request": WCMeteriosMessageTypes.ConnectionInfo.rawValue + ], replyHandler: { reply in + replyHandler(reply["userID"] as! Int, reply["hostAddr"] as! String) + }, errorHandler: { error in + errorHandler(error) + }) + } + + // requestDrinksList requests the list of drinks available at the backend from the paired device + func requestDrinksList(replyHandler: @escaping (Array) -> Void, errorHandler: @escaping ((any Error)?) -> Void) { + session.sendMessage([ + "request": WCMeteriosMessageTypes.DrinkList.rawValue + ], replyHandler: { reply in + let drinks = reply["drinks"] as! Array + + // Create Drink objects from response + var drinkArray: Array = [] + for d in drinks { + let jsonDecoded = Data(base64Encoded: d) + let drink = try! JSONDecoder().decode(Drink.self, from: jsonDecoded!) + drinkArray.append(drink) + } + + // enjoy your drinks, replyHandler! + replyHandler(drinkArray) + }, errorHandler: { error in + errorHandler(error) + }) + } + + // bookDrink books the drink given by drinkID + func bookDrink(drinkID: Int, replyHandler: @escaping (String) -> Void, errorHandler: @escaping ((any Error)?) -> Void) { + session.sendMessage([ + "request": WCMeteriosMessageTypes.BookDrink.rawValue, + "drinkID": drinkID, + ], replyHandler: { reply in + let salut = reply["salut"] as! String + replyHandler(salut) + }, errorHandler: { error in + errorHandler(error) + }) + } + + private func log(_ line: String) { +#if os(iOS) + let prefix = "[WCM@iOS]" +#elseif os(watchOS) + let prefix = "[WCM@wOS]" +#else + let prefix = "[WCM@?OS]" +#endif // os(...) + print("\(prefix) \(line)") + } +} diff --git a/common/types/Drink.swift b/common/types/Drink.swift new file mode 100644 index 0000000..3ce4ef4 --- /dev/null +++ b/common/types/Drink.swift @@ -0,0 +1,52 @@ +// +// Drink.swift +// meterios +// +// (c) 2025 Martin "maride" Dessauer +// + +import SwiftUI + +struct Drink: Identifiable, Encodable, Decodable { + public var id: Int + public var name: String + public var logoURL: String + private var price: String + private var bottle_size: String + public var caffeine: Int + public var active: Bool + + // KeyMap is an enum covering the field names as used by SpaceMarket API + private enum KeyMap : String, CodingKey { case id, name, bottle_size, caffeine, price, active, logo_url } + + // Init function to be used with JSONDecoder using SpaceMarket API's strange types + init(from decoder : Decoder) throws { + let container = try decoder.container(keyedBy: KeyMap.self) + self.id = try container.decode(Int.self, forKey: .id) + self.name = try container.decode(String.self, forKey: .name) + self.bottle_size = (try? container.decode(String.self, forKey: .bottle_size)) ?? "" + self.caffeine = (try? container.decode(Int.self, forKey: .caffeine)) ?? 0 + self.price = (try? container.decode(String.self, forKey: .price)) ?? "" + self.active = (try? container.decode(Bool.self, forKey: .active)) ?? false + self.logoURL = (try? container.decode(String.self, forKey: .logo_url)) ?? "" + } + + // Init function, as straight-forward as it gets + init(id: Int, name: String, logoURL: String = "", price: String = "", bottle_size: String = "", caffeine: Int = 0, active: Bool = true) { + self.id = id + self.name = name + self.logoURL = logoURL + self.price = price + self.bottle_size = bottle_size + self.caffeine = caffeine + self.active = active + } + + func GetPrice() -> Float { + return Float(self.price) ?? 9999.99 + } + + func GetBottleSize() -> Float { + return Float(self.bottle_size) ?? 9999.99 + } +} diff --git a/meterios/types/User.swift b/common/types/User.swift similarity index 100% rename from meterios/types/User.swift rename to common/types/User.swift diff --git a/meterios-watch/Assets.xcassets/AccentColor.colorset/Contents.json b/meterios-watch/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/meterios-watch/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/meterios-watch/Assets.xcassets/AppIcon.appiconset/Contents.json b/meterios-watch/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..7e5277c --- /dev/null +++ b/meterios-watch/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "mete-new.jpg", + "idiom" : "universal", + "platform" : "watchos", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/meterios-watch/Assets.xcassets/AppIcon.appiconset/mete-new.jpg b/meterios-watch/Assets.xcassets/AppIcon.appiconset/mete-new.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3f41541a2f40544bf874b257d0fb5ceb15052cba GIT binary patch literal 53895 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 xqbK4QA8 = [] + + @State private var showUpdateError: Bool = false + @State private var updateError: String = "" + @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 + func update() { + if updateErrorCount >= 3 { + showUpdateError = true + updateErrorCount = 0 + return + } + + // Update drinks list from paired device + wcMgr.requestDrinksList( + replyHandler: { drinks in + // Drinks received from backend + self.drinks = drinks + showUpdateError = false + }, errorHandler: { error in + // Error receiving drinks + print("Error receiving drinks: \(error)") + updateError = error?.localizedDescription ?? "Unknown error" + updateErrorCount += 1 + update() + } + ) + } + + var body: some View { + ZStack { + if drinks.count > 0 { + NavigationView { + List() { + // Drinks + Section("Drinks") { + ForEach(drinks) { drink in + NavigationLink(drink.name) { + DrinkDetailView(wcMgr: wcMgr, drink: drink) + } + } + } + + // Controls + Section { + Button("Refresh") { + drinks = [] + update() + } + } + } + } + } else { + ProgressView() + } + } + .onAppear(perform: update) + // Alert to communicate an error in communication with the paired device + .alert("Connection Error", isPresented: $showUpdateError) { + Text(updateError).tint(.red).font(.footnote) + Button("Retry", systemImage: "arrow.clockwise", action: update) + } + } +} diff --git a/meterios-watch/views/DrinkDetailView.swift b/meterios-watch/views/DrinkDetailView.swift new file mode 100644 index 0000000..f4c34a4 --- /dev/null +++ b/meterios-watch/views/DrinkDetailView.swift @@ -0,0 +1,58 @@ +// +// DrinkDetailView.swift +// meterios-watch +// +// (c) 2025 Martin "maride" Dessauer +// + +import SwiftUI + +struct DrinkDetailView: View { + private var wcMgr: WCManager + private var drink: Drink + + @State var shouldShowTransactionSheet = false + + @Environment(\.dismiss) private var dismiss + + init(wcMgr: WCManager, drink: Drink) { + self.wcMgr = wcMgr + self.drink = drink + } + + var body: some View { + VStack { + // Title + Text(drink.name).font(.title) + + // Basic info + HStack { + VStack { + Text(String(format: "%img", drink.caffeine)).fontWeight(.bold) + Text("Caffeine").fontWeight(Font.Weight.thin) + } + Divider() + VStack { + Text(String(format: "%.2f€", drink.GetPrice())).fontWeight(.bold) + Text("Price").fontWeight(Font.Weight.thin) + } + } + + // Pick button + Button(action: { + // Open the transaction sheet + shouldShowTransactionSheet = true + }) { + Text("Poison picked!") + }.tint(.green) + } + + // Transaction sheet + .sheet(isPresented: $shouldShowTransactionSheet) { + // Sheet dismissed - dismiss detail perspective as well. + dismiss() + } content: { + TransactionView(wcMgr: wcMgr, drinkID: drink.id).interactiveDismissDisabled() + } + } +} diff --git a/meterios-watch/views/TransactionView.swift b/meterios-watch/views/TransactionView.swift new file mode 100644 index 0000000..d53cbf8 --- /dev/null +++ b/meterios-watch/views/TransactionView.swift @@ -0,0 +1,80 @@ +// +// TransactionView.swift +// meterios-watch +// +// (c) 2025 Martin "maride" Dessauer +// + +import SwiftUI + +struct TransactionView: View { + private var wcMgr: WCManager + private var drinkID: Int + + @State var statusLine = "Booking..." + + @Environment(\.dismiss) private var dismiss + + // If the backend reacts too fast (we hate that, huh?), the user is confused because of the short life of this view + // closeTimer takes care of that confusion and gives the user the good old 56k modem feeling. + @State var closeTimer: Timer? + @State var closeTimerReady: Bool = false + @State var backendBookTimer: Timer? + @State var backendTimerReady: Bool = false + + init(wcMgr: WCManager, drinkID: Int) { + self.wcMgr = wcMgr + self.drinkID = drinkID + } + + // maybeDismiss dismisses this view after the backend call(s) finished AND the timer(s) ran out + func maybeDismiss() { + if closeTimerReady && backendTimerReady { + dismiss() + } + } + + // Books the drinks handed over to this view + func book() { + // Try to book the drink(s) + wcMgr.bookDrink(drinkID: drinkID) { salut in + statusLine = salut + backendTimerReady = true + maybeDismiss() + } errorHandler: { error in + statusLine = error!.localizedDescription + print("Error booking drinks: \(error)") + } + } + + var body: some View { + // Green checkmark or progress indicator + VStack { + if backendTimerReady { + Image(systemName: "checkmark.circle.fill") + .foregroundStyle(.green) + } else { + ProgressView() + } + + // Status line, on success with salute + Text(statusLine) + .font(.title) + + // Let watch vibrate on success + if #available(watchOS 10.0, *) { + Text("") + .opacity(0) + .sensoryFeedback(.success, trigger: backendTimerReady) + } + }.onAppear(perform: { + closeTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { timer in + closeTimerReady = true + maybeDismiss() + } + backendBookTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { timer in + book() + } + }) + } +} diff --git a/meterios.xcodeproj/project.pbxproj b/meterios.xcodeproj/project.pbxproj index 1e22652..03619d9 100644 --- a/meterios.xcodeproj/project.pbxproj +++ b/meterios.xcodeproj/project.pbxproj @@ -6,16 +6,81 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + A7FA4CB72D9E8FF5005ACDBB /* meterios-watch.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = A7FA4CAA2D9E8FF1005ACDBB /* meterios-watch.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + A7FA4CB52D9E8FF5005ACDBB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A7F97A862D8DD542004EF4ED /* Project object */; + proxyType = 1; + remoteGlobalIDString = A7FA4CA92D9E8FF1005ACDBB; + remoteInfo = "meterios-watch Watch App"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + A7C3C98C2D9D67EB00731E27 /* Embed Watch Content */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; + dstSubfolderSpec = 16; + files = ( + A7FA4CB72D9E8FF5005ACDBB /* meterios-watch.app in Embed Watch Content */, + ); + name = "Embed Watch Content"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ A7F97A8E2D8DD542004EF4ED /* meterios.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = meterios.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A7FA4CAA2D9E8FF1005ACDBB /* meterios-watch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "meterios-watch.app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference 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 */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + SalutManager.swift, + types/Drink.swift, + types/User.swift, + WCManager.swift, + ); + target = A7FA4CA92D9E8FF1005ACDBB /* meterios-watch */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ A7F97A902D8DD542004EF4ED /* meterios */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + A7FA4CBE2D9E90B0005ACDBB /* Exceptions for "meterios" folder in "meterios-watch" target */, + ); path = meterios; sourceTree = ""; }; + A7FA4CAB2D9E8FF1005ACDBB /* meterios-watch */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = "meterios-watch"; + sourceTree = ""; + }; + A7FA4CC62D9E94BD005ACDBB /* common */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + A7FA4CCB2D9E9564005ACDBB /* Exceptions for "common" folder in "meterios-watch" target */, + ); + path = common; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -26,13 +91,22 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A7FA4CA72D9E8FF1005ACDBB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ A7F97A852D8DD542004EF4ED = { isa = PBXGroup; children = ( + A7FA4CC62D9E94BD005ACDBB /* common */, A7F97A902D8DD542004EF4ED /* meterios */, + A7FA4CAB2D9E8FF1005ACDBB /* meterios-watch */, A7F97A8F2D8DD542004EF4ED /* Products */, ); sourceTree = ""; @@ -41,6 +115,7 @@ isa = PBXGroup; children = ( A7F97A8E2D8DD542004EF4ED /* meterios.app */, + A7FA4CAA2D9E8FF1005ACDBB /* meterios-watch.app */, ); name = Products; sourceTree = ""; @@ -55,13 +130,16 @@ A7F97A8A2D8DD542004EF4ED /* Sources */, A7F97A8B2D8DD542004EF4ED /* Frameworks */, A7F97A8C2D8DD542004EF4ED /* Resources */, + A7C3C98C2D9D67EB00731E27 /* Embed Watch Content */, ); buildRules = ( ); dependencies = ( + A7FA4CB62D9E8FF5005ACDBB /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( A7F97A902D8DD542004EF4ED /* meterios */, + A7FA4CC62D9E94BD005ACDBB /* common */, ); name = meterios; packageProductDependencies = ( @@ -70,6 +148,28 @@ productReference = A7F97A8E2D8DD542004EF4ED /* meterios.app */; productType = "com.apple.product-type.application"; }; + A7FA4CA92D9E8FF1005ACDBB /* meterios-watch */ = { + isa = PBXNativeTarget; + buildConfigurationList = A7FA4CB82D9E8FF5005ACDBB /* Build configuration list for PBXNativeTarget "meterios-watch" */; + buildPhases = ( + A7FA4CA62D9E8FF1005ACDBB /* Sources */, + A7FA4CA72D9E8FF1005ACDBB /* Frameworks */, + A7FA4CA82D9E8FF1005ACDBB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + A7FA4CAB2D9E8FF1005ACDBB /* meterios-watch */, + ); + name = "meterios-watch"; + packageProductDependencies = ( + ); + productName = "meterios-watch Watch App"; + productReference = A7FA4CAA2D9E8FF1005ACDBB /* meterios-watch.app */; + productType = "com.apple.product-type.application"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -83,6 +183,9 @@ A7F97A8D2D8DD542004EF4ED = { CreatedOnToolsVersion = 16.2; }; + A7FA4CA92D9E8FF1005ACDBB = { + CreatedOnToolsVersion = 16.2; + }; }; }; buildConfigurationList = A7F97A892D8DD542004EF4ED /* Build configuration list for PBXProject "meterios" */; @@ -100,6 +203,7 @@ projectRoot = ""; targets = ( A7F97A8D2D8DD542004EF4ED /* meterios */, + A7FA4CA92D9E8FF1005ACDBB /* meterios-watch */, ); }; /* End PBXProject section */ @@ -112,6 +216,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A7FA4CA82D9E8FF1005ACDBB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -122,8 +233,23 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A7FA4CA62D9E8FF1005ACDBB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + A7FA4CB62D9E8FF5005ACDBB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A7FA4CA92D9E8FF1005ACDBB /* meterios-watch */; + targetProxy = A7FA4CB52D9E8FF5005ACDBB /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ A7F97A9A2D8DD54B004EF4ED /* Debug */ = { isa = XCBuildConfiguration; @@ -272,7 +398,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.1; PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -309,7 +435,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.1; PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -322,6 +448,64 @@ }; name = Release; }; + A7FA4CB92D9E8FF5005ACDBB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 3RNVF52786; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Meterios; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_WKCompanionAppBundleIdentifier = space.chaosdorf.meterios; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.1; + PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios.watchkitapp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 8.7; + }; + name = Debug; + }; + A7FA4CBA2D9E8FF5005ACDBB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 3RNVF52786; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Meterios; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_WKCompanionAppBundleIdentifier = space.chaosdorf.meterios; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.1; + PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios.watchkitapp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 8.7; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -343,6 +527,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + A7FA4CB82D9E8FF5005ACDBB /* Build configuration list for PBXNativeTarget "meterios-watch" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A7FA4CB92D9E8FF5005ACDBB /* Debug */, + A7FA4CBA2D9E8FF5005ACDBB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = A7F97A862D8DD542004EF4ED /* Project object */; diff --git a/meterios.xcodeproj/xcshareddata/xcschemes/meterios-watch.xcscheme b/meterios.xcodeproj/xcshareddata/xcschemes/meterios-watch.xcscheme new file mode 100644 index 0000000..4b2f34e --- /dev/null +++ b/meterios.xcodeproj/xcshareddata/xcschemes/meterios-watch.xcscheme @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/meterios.xcodeproj/xcshareddata/xcschemes/meterios.xcscheme b/meterios.xcodeproj/xcshareddata/xcschemes/meterios.xcscheme new file mode 100644 index 0000000..4c337fe --- /dev/null +++ b/meterios.xcodeproj/xcshareddata/xcschemes/meterios.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/meterios/meteriosApp.swift b/meterios/meteriosApp.swift index def0620..7dafdad 100644 --- a/meterios/meteriosApp.swift +++ b/meterios/meteriosApp.swift @@ -11,6 +11,8 @@ import SwiftUI struct meteriosApp: App { @AppStorage("meteHostAddr") private var meteHostAddr = "" @AppStorage("meteUserID") private var meteUserID = -1 + + private var wcMgr = WCManager() var body: some Scene { WindowGroup { diff --git a/meterios/types/Drink.swift b/meterios/types/Drink.swift deleted file mode 100644 index f4e832c..0000000 --- a/meterios/types/Drink.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// Drink.swift -// meterios -// -// (c) 2025 Martin "maride" Dessauer -// - -import SwiftUI - -struct Drink: Identifiable, Decodable { - var id: Int - var name: String - var logoURL: String = "" - var price: Float = 0.00 - var bottle_size: Float = 0.00 - var caffeine: Int = 0 - var active: Bool = true - - // KeyMap is an enum covering the field names as used by SpaceMarket API - private enum KeyMap : String, CodingKey { case id, name, bottle_size, caffeine, price, active, logo_url } - - // Init function to be used with JSONDecoder, converting from SpaceMarket API's strange types to proper types - init(from decoder : Decoder) throws { - let container = try decoder.container(keyedBy: KeyMap.self) - self.id = try container.decode(Int.self, forKey: .id) - self.name = try container.decode(String.self, forKey: .name) - self.bottle_size = Float(try container.decode(String.self, forKey: .bottle_size)) ?? 0.00 - self.caffeine = (try? container.decode(Int.self, forKey: .caffeine)) ?? 0 - self.price = Float(try container.decode(String.self, forKey: .price)) ?? 9999.99 - self.active = (try? container.decode(Bool.self, forKey: .active)) ?? false - self.logoURL = (try? container.decode(String.self, forKey: .logo_url)) ?? "" - } -} diff --git a/meterios/views/DrinkDetailView.swift b/meterios/views/DrinkDetailView.swift index 8d0d32d..1a94c9d 100644 --- a/meterios/views/DrinkDetailView.swift +++ b/meterios/views/DrinkDetailView.swift @@ -65,7 +65,7 @@ struct DrinkDetail: View { }.fixedSize() Divider() VStack { - Text(String(format: "%.2f€", drink.price)).fontWeight(.bold) + Text(String(format: "%.2f€", drink.GetPrice())).fontWeight(.bold) Text("Price").fontWeight(Font.Weight.thin) } } @@ -89,7 +89,7 @@ struct DrinkDetail: View { HStack { Text("Total sum:") Spacer() - Text(String(format: "%.2f€", self.drink.price * Float(self.drinkAmount))).fontWeight(.bold) + Text(String(format: "%.2f€", self.drink.GetPrice() * Float(self.drinkAmount))).fontWeight(.bold) } } diff --git a/meterios/views/TransactionView.swift b/meterios/views/TransactionView.swift index 235abdf..b0b3861 100644 --- a/meterios/views/TransactionView.swift +++ b/meterios/views/TransactionView.swift @@ -25,15 +25,6 @@ struct TransactionView: View { @State var backendBookTimer: Timer? @State var backendTimerReady: Bool = false - let salutes: Array = [ - "Prost!", "Guten!", "Kippis!", "Cheers!", "Chin-Chin!", "Zum Wohl!", "Salut!", "לחיים", - "Jamas!", // thanks cocorilla - "Budmo!", "за здоровье!", "Sláinte!", "乾杯", "Noroc!", // thanks ChaosAyumi - ] - func getSalute() -> String { - return salutes[Int.random(in: 0..