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 0000000..3f41541 Binary files /dev/null and b/meterios-watch/Assets.xcassets/AppIcon.appiconset/mete-new.jpg differ diff --git a/meterios-watch/Assets.xcassets/Contents.json b/meterios-watch/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/meterios-watch/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/meterios-watch/meteriosWatchApp.swift b/meterios-watch/meteriosWatchApp.swift new file mode 100644 index 0000000..107a9af --- /dev/null +++ b/meterios-watch/meteriosWatchApp.swift @@ -0,0 +1,19 @@ +// +// meteriosWatchApp.swift +// meterios-watch +// +// (c) 2025 Martin "maride" Dessauer +// + +import SwiftUI + +@main +struct meteriosWatchApp: App { + private var wcMgr = WCManager() + + var body: some Scene { + WindowGroup { + ContentView(wcMgr) + } + } +} diff --git a/meterios-watch/views/ContentView.swift b/meterios-watch/views/ContentView.swift new file mode 100644 index 0000000..fd00742 --- /dev/null +++ b/meterios-watch/views/ContentView.swift @@ -0,0 +1,81 @@ +// +// ContentView.swift +// meterios-watch +// +// (c) 2025 Martin "maride" Dessauer +// + +import SwiftUI + +struct ContentView: View { + private var wcMgr: WCManager + + @State private var drinks: Array = [] + + @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..