Add watchOS support
This commit is contained in:
@@ -2,6 +2,6 @@
|
||||
|
||||
# meterios
|
||||
|
||||
*Pick your poison*, now on iOS.
|
||||
*Pick your poison*, now on iOS & watchOS.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// SalutManager.swift
|
||||
// meterios
|
||||
//
|
||||
// (c) 2025 Martin "maride" Dessauer
|
||||
//
|
||||
|
||||
class SalutManager {
|
||||
static let salutes: Array<String> = [
|
||||
"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..<salutes.count)]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
//
|
||||
// WCManager.swift
|
||||
// meterios
|
||||
//
|
||||
// (c) 2025 Martin "maride" Dessauer
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import WatchConnectivity
|
||||
|
||||
enum WCMeteriosMessageTypes: String {
|
||||
case ConnectionInfo
|
||||
case DrinkList
|
||||
case BookDrink
|
||||
}
|
||||
|
||||
class WCManager: NSObject, WCSessionDelegate {
|
||||
@AppStorage("meteHostAddr") private var meteHostAddr = ""
|
||||
@AppStorage("meteUserID") private var meteUserID = -1
|
||||
|
||||
private let session: WCSession = WCSession.default
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
if (WCSession.isSupported()) {
|
||||
session.delegate = self
|
||||
session.activate()
|
||||
log("Activated session")
|
||||
}
|
||||
}
|
||||
|
||||
// Functions required by WCSessionDelegate protocol, but not of interest for Meterios
|
||||
#if os(iOS)
|
||||
func sessionDidBecomeInactive(_ session: WCSession) {}
|
||||
func sessionDidDeactivate(_ session: WCSession) {}
|
||||
#endif // os(iOS)
|
||||
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: (any Error)?) {}
|
||||
|
||||
// session(...) gets called for every incoming message through the WatchConnectivity channel
|
||||
// As Meterios currently only needs messages from watch to device and responses back to the watch, this function currently only implements device-related responses.
|
||||
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> 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<String> = []
|
||||
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<Drink>) -> Void, errorHandler: @escaping ((any Error)?) -> Void) {
|
||||
session.sendMessage([
|
||||
"request": WCMeteriosMessageTypes.DrinkList.rawValue
|
||||
], replyHandler: { reply in
|
||||
let drinks = reply["drinks"] as! Array<String>
|
||||
|
||||
// Create Drink objects from response
|
||||
var drinkArray: Array<Drink> = []
|
||||
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)")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "mete-new.jpg",
|
||||
"idiom" : "universal",
|
||||
"platform" : "watchos",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Drink> = []
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 = "<group>";
|
||||
};
|
||||
A7FA4CAB2D9E8FF1005ACDBB /* meterios-watch */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = "meterios-watch";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A7FA4CC62D9E94BD005ACDBB /* common */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
A7FA4CCB2D9E9564005ACDBB /* Exceptions for "common" folder in "meterios-watch" target */,
|
||||
);
|
||||
path = common;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* 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 = "<group>";
|
||||
@@ -41,6 +115,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A7F97A8E2D8DD542004EF4ED /* meterios.app */,
|
||||
A7FA4CAA2D9E8FF1005ACDBB /* meterios-watch.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -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 */;
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1620"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A7FA4CA92D9E8FF1005ACDBB"
|
||||
BuildableName = "meterios-watch.app"
|
||||
BlueprintName = "meterios-watch"
|
||||
ReferencedContainer = "container:meterios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A7F97A8D2D8DD542004EF4ED"
|
||||
BuildableName = "meterios.app"
|
||||
BlueprintName = "meterios"
|
||||
ReferencedContainer = "container:meterios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A7FA4CA92D9E8FF1005ACDBB"
|
||||
BuildableName = "meterios-watch.app"
|
||||
BlueprintName = "meterios-watch"
|
||||
ReferencedContainer = "container:meterios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A7FA4CA92D9E8FF1005ACDBB"
|
||||
BuildableName = "meterios-watch.app"
|
||||
BlueprintName = "meterios-watch"
|
||||
ReferencedContainer = "container:meterios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1620"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A7F97A8D2D8DD542004EF4ED"
|
||||
BuildableName = "meterios.app"
|
||||
BlueprintName = "meterios"
|
||||
ReferencedContainer = "container:meterios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A7F97A8D2D8DD542004EF4ED"
|
||||
BuildableName = "meterios.app"
|
||||
BlueprintName = "meterios"
|
||||
ReferencedContainer = "container:meterios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A7F97A8D2D8DD542004EF4ED"
|
||||
BuildableName = "meterios.app"
|
||||
BlueprintName = "meterios"
|
||||
ReferencedContainer = "container:meterios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -12,6 +12,8 @@ struct meteriosApp: App {
|
||||
@AppStorage("meteHostAddr") private var meteHostAddr = ""
|
||||
@AppStorage("meteUserID") private var meteUserID = -1
|
||||
|
||||
private var wcMgr = WCManager()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
if meteHostAddr == "" || meteUserID == -1 {
|
||||
|
||||
@@ -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)) ?? ""
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,15 +25,6 @@ struct TransactionView: View {
|
||||
@State var backendBookTimer: Timer?
|
||||
@State var backendTimerReady: Bool = false
|
||||
|
||||
let salutes: Array<String> = [
|
||||
"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..<salutes.count)]
|
||||
}
|
||||
|
||||
init(drinkID: Int, drinkAmount: Int) {
|
||||
self.drinkID = drinkID
|
||||
self.drinkAmount = drinkAmount
|
||||
@@ -54,7 +45,7 @@ struct TransactionView: View {
|
||||
for _ in 1...drinkAmount {
|
||||
try await BackendConnector().BookDrink(baseAddr: meteHostAddr, userID: meteUserID, drinkID: drinkID)
|
||||
}
|
||||
statusLine = getSalute()
|
||||
statusLine = SalutManager.getSalute()
|
||||
backendTimerReady = true
|
||||
maybeDismiss()
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user