From e7c7bd3e7dcfd84762cfca20b7aa71bd43581b69 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Mon, 2 Jun 2025 22:02:25 +0200 Subject: [PATCH 01/11] WIP: Refactor architecture --- Modules/WRCore/Package.swift | 2 +- Modules/WRCore/Sources/WRCore/Alert.swift | 21 +++ Modules/WRCore/Sources/WRCore/Globals.swift | 33 +++- .../Sources/WRCore/KotlinArrayWrapper.swift | 7 +- Modules/WRCore/Sources/WRCore/Mock.swift | 10 -- .../WRCore/Sources/WRCore/Navigation.swift | 4 +- .../Sources/WRCore/ObservableViewModel.swift | 6 + .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Features/Billing/BillingScreen.swift | 168 +++++++++++------- WaiterRobot/Features/Login/LoginScreen.swift | 30 ++-- .../Features/Login/RegisterScreen.swift | 16 +- .../Features/Order/Search/ProductSearch.swift | 55 +++--- .../Order/Search/ProductSearchAllTab.swift | 4 +- WaiterRobot/Features/SwitchEvent/Event.swift | 3 +- .../TableDetail/OrderedItemView.swift | 3 +- .../TableList/TableGroupSection.swift | 14 +- .../TableList/TableListFilterRow.swift | 4 +- .../Features/UpdateApp/UpdateAppScreen.swift | 6 +- WaiterRobot/Ui/LoadingOverlayView.swift | 25 +++ WaiterRobot/Ui/ViewStateOverlayView.swift | 44 +++++ 20 files changed, 301 insertions(+), 158 deletions(-) create mode 100644 Modules/WRCore/Sources/WRCore/Alert.swift create mode 100644 WaiterRobot/Ui/LoadingOverlayView.swift create mode 100644 WaiterRobot/Ui/ViewStateOverlayView.swift diff --git a/Modules/WRCore/Package.swift b/Modules/WRCore/Package.swift index 1392efc..e870c64 100644 --- a/Modules/WRCore/Package.swift +++ b/Modules/WRCore/Package.swift @@ -15,7 +15,7 @@ let package = Package( ], dependencies: [ .package(path: "../SharedUI"), - .package(url: "https://github.com/DatepollSystems/WaiterRobot-Shared-Android.git", from: "1.6.1"), + .package(url: "https://github.com/DatepollSystems/WaiterRobot-Shared-Android.git", from: "1.7.3"), ], targets: [ .target( diff --git a/Modules/WRCore/Sources/WRCore/Alert.swift b/Modules/WRCore/Sources/WRCore/Alert.swift new file mode 100644 index 0000000..01a2306 --- /dev/null +++ b/Modules/WRCore/Sources/WRCore/Alert.swift @@ -0,0 +1,21 @@ +import shared +import SwiftUI + +public extension Alert { + init(_ dialog: DialogState) { + if let secondaryButton = dialog.secondaryButton { + self.init( + title: Text(dialog.title.localized()), + message: Text(dialog.text.localized()), + primaryButton: .default(Text(dialog.primaryButton.text.localized()), action: dialog.primaryButton.action), + secondaryButton: .cancel(Text(secondaryButton.text.localized()), action: secondaryButton.action) + ) + } else { + self.init( + title: Text(dialog.title.localized()), + message: Text(dialog.text.localized()), + dismissButton: .default(Text(dialog.primaryButton.text.localized()), action: dialog.primaryButton.action) + ) + } + } +} diff --git a/Modules/WRCore/Sources/WRCore/Globals.swift b/Modules/WRCore/Sources/WRCore/Globals.swift index 85f1812..bd23b46 100644 --- a/Modules/WRCore/Sources/WRCore/Globals.swift +++ b/Modules/WRCore/Sources/WRCore/Globals.swift @@ -4,8 +4,7 @@ import SwiftUI import UIKit public var koin: IosKoinComponent { IosKoinComponent.shared } - -public var localize: shared.L.Companion { shared.L.Companion.shared } +public var localize: shared.MR.strings { shared.MR.strings() } public enum WRCore { /// Setup of frameworks and all the other related stuff which is needed everywhere in the app @@ -29,7 +28,6 @@ public enum WRCore { let logger = koin.logger(tag: "AppDelegate") - KMMResourcesLocalizationKt.localizationBundle = Bundle(for: shared.L.self) logger.d { "initialized localization bundle" } print("finished app setup") } @@ -53,3 +51,32 @@ public extension EnvironmentValues { #endif } } + +public extension StringResource { + func callAsFunction() -> String { + desc().localized() + } + + func callAsFunction(_ args: String...) -> String { + format(args: args).localized() + } +} + +public extension StringDesc { + func callAsFunction() -> String { + localized() + } +} + +public extension Skie.Shared.Resource.__Sealed { + var data: T? { + switch self { + case let .loading(resource): + resource.data + case let .error(resource): + resource.data + case let .success(resource): + resource.data + } + } +} diff --git a/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift b/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift index 53840b4..a9e5964 100644 --- a/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift +++ b/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift @@ -2,9 +2,12 @@ import Foundation import shared public extension Array where Element: AnyObject { - init(_ kotlinArray: KotlinArray) { + init?(_ kotlinArray: KotlinArray?) { + guard let array = kotlinArray else { + return nil + } self.init() - let iterator = kotlinArray.iterator() + let iterator = array.iterator() while iterator.hasNext() { append(iterator.next() as! Element) } diff --git a/Modules/WRCore/Sources/WRCore/Mock.swift b/Modules/WRCore/Sources/WRCore/Mock.swift index 1bae963..c9d19db 100644 --- a/Modules/WRCore/Sources/WRCore/Mock.swift +++ b/Modules/WRCore/Sources/WRCore/Mock.swift @@ -14,18 +14,8 @@ public enum Mock { TableGroup( id: id, name: name, - eventId: 1, - position: Int32(id), color: "", hidden: false, - tables: [ - table(with: 1), - table(with: 2, hasOrders: true), - table(with: 3), - table(with: 4), - table(with: 5), - table(with: 6), - ] ) } diff --git a/Modules/WRCore/Sources/WRCore/Navigation.swift b/Modules/WRCore/Sources/WRCore/Navigation.swift index c0072ce..3ae2c0f 100644 --- a/Modules/WRCore/Sources/WRCore/Navigation.swift +++ b/Modules/WRCore/Sources/WRCore/Navigation.swift @@ -36,7 +36,7 @@ extension UIPilot { public extension View { func customBackNavigation( - title: String = localize.navigation.back(), + title: String = localize.navigation_back(), icon: String? = "chevron.left", action: @escaping () -> Void ) -> some View { @@ -73,7 +73,7 @@ public extension View { logger.d { "Got sideEffect: \(sideEffect)" } switch onEnum(of: sideEffect as! NavOrViewModelEffect) { case let .navEffect(navEffect): - await navigator.navigate(navEffect.action) + navigator.navigate(navEffect.action) case let .vMEffect(effect): if handler?(effect.effect) != true { logger.w { "Side effect \(effect.effect) was not handled." } diff --git a/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift b/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift index 8e43e28..89dae0b 100644 --- a/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift +++ b/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift @@ -60,6 +60,12 @@ public class ObservableOrderViewModel: ObservableViewModel { + public init() { + super.init(viewModel: koin.getProductListVM()) + } +} + public class ObservableLoginScannerViewModel: ObservableViewModel { public init() { super.init(viewModel: koin.loginScannerVM()) diff --git a/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3ecc5f8..55894a6 100644 --- a/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DatepollSystems/WaiterRobot-Shared-Android.git", "state" : { - "revision" : "43ef7e427e6cfa46c81c526b5e0e291ca227f845", - "version" : "1.6.10" + "revision" : "c01f764d92dcd7df8ee4ad4ecba55a9200ed7a00", + "version" : "1.7.3" } } ], diff --git a/WaiterRobot/Features/Billing/BillingScreen.swift b/WaiterRobot/Features/Billing/BillingScreen.swift index 1c63843..cd0e690 100644 --- a/WaiterRobot/Features/Billing/BillingScreen.swift +++ b/WaiterRobot/Features/Billing/BillingScreen.swift @@ -8,7 +8,6 @@ struct BillingScreen: View { @EnvironmentObject var navigator: UIPilot @State private var showPayDialog: Bool = false - @State private var showAbortConfirmation = false @StateObject private var viewModel: ObservableBillingViewModel private let table: shared.Table @@ -19,92 +18,122 @@ struct BillingScreen: View { } var body: some View { - let billItems = Array(viewModel.state.billItemsArray) - - content(billItems: billItems) - .navigationTitle(localize.billing.title(value0: table.groupName, value1: table.number.description)) - .navigationBarTitleDisplayMode(.inline) - .customBackNavigation( - title: localize.dialog.cancel(), - icon: nil - ) { - if viewModel.state.hasCustomSelection { - showAbortConfirmation = true - } else { - viewModel.actual.abortBill() - } + BillingScreenView( + table: table, + state: viewModel.state, + abortBill: { viewModel.actual.abortBill() }, + selectAll: { viewModel.actual.selectAll() }, + unselectAll: { viewModel.actual.unselectAll() }, + addItem: { viewModel.actual.addItem(baseProductId: $0, amount: $1) }, + paySelection: { viewModel.actual.paySelection(paymentSheetShown: $0) } + ) + // TODO: make only half screen when ios 15 is dropped + .sheet(isPresented: $showPayDialog) { + PayDialog(viewModel: viewModel) + } + .withViewModel(viewModel, navigator) { effect in + switch onEnum(of: effect) { + case .showPaymentSheet: + showPayDialog = true + case .toast: + break // TODO: add "toast" support } - .confirmationDialog( - localize.billing.notSent.title(), - isPresented: $showAbortConfirmation, - titleVisibility: .visible - ) { - Button(localize.dialog.closeAnyway(), role: .destructive) { - viewModel.actual.abortBill() + + return true + } + } +} + +private struct BillingScreenView: View { + @State private var showPayDialog: Bool = false + @State private var showAbortConfirmation = false + + let table: shared.Table + let state: BillingState + let abortBill: () -> Void + let selectAll: () -> Void + let unselectAll: () -> Void + let addItem: (_ baseProductId: Int64, _ amount: Int32) -> Void + let paySelection: (_ paymentSheetShown: Bool) -> Void + + var body: some View { + ViewStateOverlayView(state: state.paymentState) { + let billItemsState = onEnum(of: state.billItems_) + + if case let .loading(ressource) = billItemsState, ressource.data == nil { + ProgressView() + } else { + if case let .error(resource) = billItemsState { + Text("Error \(resource.userMessage())") } - } message: { - Text(localize.billing.notSent.desc()) - } - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - if !billItems.isEmpty { - Button { - viewModel.actual.selectAll() - } label: { - Image(systemName: "checkmark") - } - } - if !billItems.isEmpty { - Button { - viewModel.actual.unselectAll() - } label: { - Image(systemName: "xmark") - } - } + if let billItems = Array(state.billItems_.data), !billItems.isEmpty { + content(billItems: billItems) + } else { + Text(localize.billing_noOrder(table.groupName, table.number.description)) } } - // TODO: make only half screen when ios 15 is dropped - .sheet(isPresented: $showPayDialog) { - PayDialog(viewModel: viewModel) + } + .navigationTitle(localize.billing_title(table.groupName, table.number.description)) + .navigationBarTitleDisplayMode(.inline) + .customBackNavigation( + title: localize.dialog_cancel(), + icon: nil + ) { + if state.hasCustomSelection { + showAbortConfirmation = true + } else { + abortBill() + } + } + .confirmationDialog( + localize.billing_notSent_title(), + isPresented: $showAbortConfirmation, + titleVisibility: .visible + ) { + Button(localize.dialog_closeAnyway(), role: .destructive) { + abortBill() } - .withViewModel(viewModel, navigator) { effect in - switch onEnum(of: effect) { - case .showPaymentSheet: - showPayDialog = true - case .toast: - break // TODO: add "toast" support + } message: { + Text(localize.billing_notSent_desc()) + } + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button { + selectAll() + } label: { + Image(systemName: "checkmark") } - return true + Button { + unselectAll() + } label: { + Image(systemName: "xmark") + } } + } } @ViewBuilder - private func content(billItems: [BillItem]) -> some View { + private func content(billItems: [BillItem]?) -> some View { VStack { List { - if billItems.isEmpty { - Text(localize.billing.noOpenBill(value0: table.groupName, value1: table.number.description)) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - .padding() - } else { + if let billItems, !billItems.isEmpty { Section { - ForEach(billItems, id: \.virtualId) { item in + ForEach(billItems, id: \.baseProductId) { item in BillListItem( item: item, addOne: { - viewModel.actual.addItem(virtualId: item.virtualId, amount: 1) + addItem(item.baseProductId, 1) }, addAll: { - viewModel.actual.addItem(virtualId: item.virtualId, amount: item.ordered - item.selectedForBill) + addItem(item.baseProductId, item.ordered - item.selectedForBill) }, removeOne: { - viewModel.actual.addItem(virtualId: item.virtualId, amount: -1) + addItem(item.baseProductId, -1) }, removeAll: { - viewModel.actual.addItem(virtualId: item.virtualId, amount: -item.selectedForBill) + addItem(item.baseProductId, -item.selectedForBill) } ) } @@ -115,19 +144,24 @@ struct BillingScreen: View { Text("Selected") } } + } else { + Text(localize.billing_noOrder(table.groupName, table.number.description)) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding() } } HStack { - Text("\(localize.billing.total()):") + Text("\(localize.billing_total()):") Spacer() - Text("\(viewModel.state.priceSum)") + Text("\(state.priceSum)") } .font(.title2) .padding() .overlay(alignment: .bottom) { Button { - viewModel.actual.paySelection(paymentSheetShown: false) + paySelection(false) } label: { Image(systemName: "eurosign") .font(.system(.title)) @@ -138,7 +172,7 @@ struct BillingScreen: View { .background(.blue) .mask(Circle()) .shadow(color: Color.black.opacity(0.3), radius: 3, x: 3, y: 3) - .disabled(viewModel.state.viewState != ViewState.Idle.shared || !viewModel.state.hasSelectedItems) + .disabled(state.paymentState != ViewState.Idle.shared || !state.hasSelectedItems) } } } diff --git a/WaiterRobot/Features/Login/LoginScreen.swift b/WaiterRobot/Features/Login/LoginScreen.swift index 8b08669..d5057a3 100644 --- a/WaiterRobot/Features/Login/LoginScreen.swift +++ b/WaiterRobot/Features/Login/LoginScreen.swift @@ -13,22 +13,16 @@ struct LoginScreen: View { @State private var debugLoginLink = "" var body: some View { - switch viewModel.state.viewState { - case is ViewState.Loading: + switch onEnum(of: viewModel.state.viewState) { + case .loading: ProgressView() - case is ViewState.Idle: + case .idle: content() - case let error as ViewState.Error: + case let .error(error): content() .alert(isPresented: Binding.constant(true)) { - Alert( - title: Text(error.title), - message: Text(error.message), - dismissButton: .cancel(Text("OK"), action: error.onDismiss) - ) + Alert(error.dialog) } - default: - fatalError("Unexpected ViewState: \(viewModel.state.viewState.description)") } } @@ -44,11 +38,11 @@ struct LoginScreen: View { .onLongPressGesture { showLinkInput = true } - Text(localize.login.title()) + Text(localize.login_title()) .font(.title) .padding() - Text(localize.login.desc()) + Text(localize.login_desc()) .font(.body) .padding() .multilineTextAlignment(.center) @@ -56,19 +50,19 @@ struct LoginScreen: View { Button { viewModel.actual.openScanner() } label: { - Label(localize.login.withQrCode(), systemImage: "qrcode.viewfinder") + Label(localize.login_withQrCode(), systemImage: "qrcode.viewfinder") .font(.title3) } .padding() Spacer() } - .alert(localize.login.title(), isPresented: $showLinkInput) { - TextField(localize.login.debugDialog.inputLabel(), text: $debugLoginLink) - Button(localize.dialog.cancel(), role: .cancel) { + .alert(localize.login_title(), isPresented: $showLinkInput) { + TextField(localize.login_scanner_debugDialog_inputLabel(), text: $debugLoginLink) + Button(localize.dialog_cancel(), role: .cancel) { showLinkInput = false } - Button(localize.login.title()) { + Button(localize.login_title()) { viewModel.actual.onDebugLogin(link: debugLoginLink) } } diff --git a/WaiterRobot/Features/Login/RegisterScreen.swift b/WaiterRobot/Features/Login/RegisterScreen.swift index 66336cd..39f66f2 100644 --- a/WaiterRobot/Features/Login/RegisterScreen.swift +++ b/WaiterRobot/Features/Login/RegisterScreen.swift @@ -21,11 +21,7 @@ struct RegisterScreen: View { case let error as ViewState.Error: content() .alert(isPresented: Binding.constant(true)) { - Alert( - title: Text(error.title), - message: Text(error.message), - dismissButton: .cancel(Text("OK"), action: error.onDismiss) - ) + Alert(error.dialog) } default: fatalError("Unexpected ViewState: \(viewModel.state.viewState.description)") @@ -34,10 +30,10 @@ struct RegisterScreen: View { private func content() -> some View { VStack { - Text(localize.register.name.desc()) + Text(localize.register_name_desc()) .font(.body) - TextField(localize.register.name.title(), text: $name) + TextField(localize.register_name_title(), text: $name) .font(.body) .fixedSize() .padding() @@ -46,7 +42,7 @@ struct RegisterScreen: View { Button { viewModel.actual.cancel() } label: { - Text(localize.dialog.cancel()) + Text(localize.dialog_cancel()) } Spacer() @@ -57,12 +53,12 @@ struct RegisterScreen: View { registerLink: deepLink ) } label: { - Text(localize.register.login()) + Text(localize.register_login()) } } .padding() - Label(localize.register.alreadyRegisteredInfo(), systemImage: "info.circle.fill") + Label(localize.register_alreadyRegisteredInfo(), systemImage: "info.circle.fill") } .padding() .navigationBarHidden(true) diff --git a/WaiterRobot/Features/Order/Search/ProductSearch.swift b/WaiterRobot/Features/Order/Search/ProductSearch.swift index 21d8b3c..8dde89b 100644 --- a/WaiterRobot/Features/Order/Search/ProductSearch.swift +++ b/WaiterRobot/Features/Order/Search/ProductSearch.swift @@ -5,7 +5,8 @@ import WRCore struct ProductSearch: View { @Environment(\.dismiss) private var dismiss - @ObservedObject var viewModel: ObservableOrderViewModel + @ObservedObject var viewModel: ObservableProductListViewModel + let addItem: (_ product: Product, _ amount: Int32) -> Void @State private var search: String = "" @State private var selectedTab: Int = 0 @@ -30,16 +31,8 @@ struct ProductSearch: View { } @ViewBuilder - private func productsGroupsList(productGroups: KotlinArray) -> some View { - let productGroups = Array(productGroups) - - if productGroups.isEmpty { - Text(localize.productSearch.noProductFound()) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - .padding() - - } else { + private func productsGroupsList(productGroups: KotlinArray) -> some View { + if let productGroups = Array(productGroups), !productGroups.isEmpty { VStack { ProducSearchTabBarHeader(currentTab: $selectedTab, tabBarOptions: getGroupNames(productGroups)) @@ -48,7 +41,7 @@ struct ProductSearch: View { productGroups: productGroups, columns: layout, onProductClick: { - viewModel.actual.addItem(product: $0, amount: 1) + addItem($0, 1) dismiss() } ) @@ -56,17 +49,10 @@ struct ProductSearch: View { .padding() let enumeratedProductGroups = Array(productGroups.enumerated()) - ForEach(enumeratedProductGroups, id: \.element.id) { index, groupWithProducts in + ForEach(enumeratedProductGroups, id: \.element.id) { index, _ in ScrollView { LazyVGrid(columns: layout, spacing: 0) { - ProductSearchGroupList( - products: groupWithProducts.products, - backgroundColor: Color(hex: groupWithProducts.color), - onProductClick: { - viewModel.actual.addItem(product: $0, amount: 1) - dismiss() - } - ) + productGroup(groupedProducts: groupedProducts) Spacer() } .padding() @@ -80,23 +66,40 @@ struct ProductSearch: View { .onChange(of: search, perform: { viewModel.actual.filterProducts(filter: $0) }) .toolbar { ToolbarItem(placement: .navigationBarLeading) { - Button(localize.dialog.cancel()) { + Button(localize.dialog_cancel()) { dismiss() } } } + } else { + Text(localize.productSearch_noProductFound()) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding() } } - private func productGroupsError(error: ResourceError>) -> some View { - Text(error.userMessage) + @ViewBuilder + private func productGroup(groupedProducts: GroupedProducts) -> some View { + ProductSearchGroupList( + products: groupedProducts.products, + backgroundColor: Color(hex: groupedProducts.color), + onProductClick: { + addItem($0, 1) + dismiss() + } + ) + } + + private func productGroupsError(error: ResourceError>) -> some View { + Text(error.userMessage()) } - private func getGroupNames(_ productGroups: [ProductGroup]) -> [String] { + private func getGroupNames(_ productGroups: [GroupedProducts]) -> [String] { var groupNames = productGroups.map { productGroup in productGroup.name } - groupNames.insert(localize.productSearch.allGroups(), at: 0) + groupNames.insert(localize.productSearch_groups_all(), at: 0) return groupNames } } diff --git a/WaiterRobot/Features/Order/Search/ProductSearchAllTab.swift b/WaiterRobot/Features/Order/Search/ProductSearchAllTab.swift index b41313b..66fad7b 100644 --- a/WaiterRobot/Features/Order/Search/ProductSearchAllTab.swift +++ b/WaiterRobot/Features/Order/Search/ProductSearchAllTab.swift @@ -2,7 +2,7 @@ import shared import SwiftUI struct ProductSearchAllTab: View { - let productGroups: [ProductGroup] + let productGroups: [GroupedProducts] let columns: [GridItem] let onProductClick: (Product) -> Void @@ -35,7 +35,7 @@ struct ProductSearchAllTab: View { #Preview { ProductSearchAllTab( productGroups: [ - ProductGroup( + GroupedProducts( id: 1, name: "Test Group 1", position: 1, diff --git a/WaiterRobot/Features/SwitchEvent/Event.swift b/WaiterRobot/Features/SwitchEvent/Event.swift index 3466694..6289dd6 100644 --- a/WaiterRobot/Features/SwitchEvent/Event.swift +++ b/WaiterRobot/Features/SwitchEvent/Event.swift @@ -34,7 +34,8 @@ struct Event: View { endDate: nil, city: "Graz", organisationId: 1, - stripeSettings: shared.Event.StripeSettingsDisabled() + stripeSettings: shared.Event.StripeSettingsDisabled(), + isDemo: false ) ) } diff --git a/WaiterRobot/Features/TableDetail/OrderedItemView.swift b/WaiterRobot/Features/TableDetail/OrderedItemView.swift index 669fc37..6413f9d 100644 --- a/WaiterRobot/Features/TableDetail/OrderedItemView.swift +++ b/WaiterRobot/Features/TableDetail/OrderedItemView.swift @@ -26,7 +26,8 @@ struct OrderedItemView: View { baseProductId: 1, name: "Test", amount: 1, - virtualId: 2 + virtualId: 2, + note: "" ), tabbed: {} ) diff --git a/WaiterRobot/Features/TableList/TableGroupSection.swift b/WaiterRobot/Features/TableList/TableGroupSection.swift index d994504..3952346 100644 --- a/WaiterRobot/Features/TableList/TableGroupSection.swift +++ b/WaiterRobot/Features/TableList/TableGroupSection.swift @@ -4,16 +4,16 @@ import SwiftUI import WRCore struct TableGroupSection: View { - let tableGroup: TableGroup + let groupedTables: GroupedTables let onTableClick: (shared.Table) -> Void var body: some View { Section { - ForEach(tableGroup.tables, id: \.id) { table in + ForEach(groupedTables.tables, id: \.id) { table in TableView( text: table.number.description, hasOrders: table.hasOrders, - backgroundColor: Color(hex: tableGroup.color), + backgroundColor: Color(hex: groupedTables.color), onClick: { onTableClick(table) } @@ -22,7 +22,7 @@ struct TableGroupSection: View { } } header: { HStack { - if let background = Color(hex: tableGroup.color) { + if let background = Color(hex: groupedTables.color) { title(backgroundColor: background) } else { title(backgroundColor: .gray.opacity(0.3)) @@ -36,7 +36,7 @@ struct TableGroupSection: View { } private func title(backgroundColor: Color) -> some View { - Text(tableGroup.name) + Text(groupedTables.name) .font(.title2) .foregroundStyle(backgroundColor.getContentColor(lightColorScheme: .black, darkColorScheme: .white)) .padding(6) @@ -50,13 +50,11 @@ struct TableGroupSection: View { #Preview { LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) { TableGroupSection( - tableGroup: TableGroup( + groupedTables: GroupedTables( id: 1, name: "Test Group", eventId: 1, - position: 1, color: nil, - hidden: false, tables: [ shared.Table(id: 1, number: 1, groupName: "Test Group", hasOrders: true), shared.Table(id: 2, number: 2, groupName: "Test Group", hasOrders: false), diff --git a/WaiterRobot/Features/TableList/TableListFilterRow.swift b/WaiterRobot/Features/TableList/TableListFilterRow.swift index a3de937..a8e7aa3 100644 --- a/WaiterRobot/Features/TableList/TableListFilterRow.swift +++ b/WaiterRobot/Features/TableList/TableListFilterRow.swift @@ -106,8 +106,8 @@ struct TableListFilterRow: View { #Preview { TableListFilterRow( tableGroups: [ - TableGroup(id: 1, name: "Test Group1", eventId: 1, position: 1, color: nil, hidden: true, tables: []), - TableGroup(id: 2, name: "Test Group2", eventId: 1, position: 1, color: nil, hidden: false, tables: []), + TableGroup(id: 1, name: "Test Group1", color: nil, hidden: true), + TableGroup(id: 2, name: "Test Group2", color: nil, hidden: false), ], onToggleFilter: { _ in }, onSelectAll: {}, diff --git a/WaiterRobot/Features/UpdateApp/UpdateAppScreen.swift b/WaiterRobot/Features/UpdateApp/UpdateAppScreen.swift index 2132f9e..1659d94 100644 --- a/WaiterRobot/Features/UpdateApp/UpdateAppScreen.swift +++ b/WaiterRobot/Features/UpdateApp/UpdateAppScreen.swift @@ -5,7 +5,7 @@ import WRCore struct UpdateAppScreen: View { var body: some View { VStack { - Text(localize.app.forceUpdate.message()) + Text(localize.app_forceUpdate_message()) .multilineTextAlignment(.center) Button { @@ -19,11 +19,11 @@ struct UpdateAppScreen: View { UIApplication.shared.open(url, options: [:], completionHandler: nil) } } label: { - Text(localize.app.forceUpdate.openStore(value0: "App Store")) + Text(localize.app_forceUpdate_openStore("App Store")) }.padding() } .padding() - .navigationTitle(localize.app.forceUpdate.title()) + .navigationTitle(localize.app_forceUpdate_title()) .navigationBarTitleDisplayMode(.inline) } } diff --git a/WaiterRobot/Ui/LoadingOverlayView.swift b/WaiterRobot/Ui/LoadingOverlayView.swift new file mode 100644 index 0000000..c478bec --- /dev/null +++ b/WaiterRobot/Ui/LoadingOverlayView.swift @@ -0,0 +1,25 @@ +import SwiftUI + +struct LoadingOverlayView: View { + let isLoading: Bool + let content: () -> Content + + init(isLoading: Bool, @ViewBuilder content: @escaping () -> Content) { + self.isLoading = isLoading + self.content = content + } + + var body: some View { + ZStack { + content() + .opacity(isLoading ? 0.5 : 1.0) + + if isLoading { + Color.black.opacity(0.2) + .edgesIgnoringSafeArea(.all) + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + } + } + } +} diff --git a/WaiterRobot/Ui/ViewStateOverlayView.swift b/WaiterRobot/Ui/ViewStateOverlayView.swift new file mode 100644 index 0000000..1c6a49f --- /dev/null +++ b/WaiterRobot/Ui/ViewStateOverlayView.swift @@ -0,0 +1,44 @@ +import shared +import SwiftUI + +struct ViewStateOverlayView: View { + let state: Skie.Shared.ViewState.__Sealed + let content: () -> Content + + init(state: ViewState, @ViewBuilder content: @escaping () -> Content) { + self.state = onEnum(of: state) + self.content = content + } + + var body: some View { + ZStack { + LoadingOverlayView(isLoading: isLoading) { + VStack(alignment: .leading) { + content() + } + } + } + .alert(item: Binding( + get: { dialogState }, + set: { _ in dialogState?.onDismiss() } + )) { dialog in + Alert(dialog) + } + } + + private var isLoading: Bool { + if case .loading = state { + return true + } + return false + } + + private var dialogState: DialogState? { + if case let .error(error) = state { + return error.dialog + } + return nil + } +} + +extension DialogState: Identifiable {} From 64d0fd15d9aefaa677cf33b871a287ba2d7ebb13 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Mon, 9 Jun 2025 16:50:57 +0200 Subject: [PATCH 02/11] Refactor architecture: First working version --- .../SharedUI/Sources/SharedUI/Images.swift | 2 +- .../Resources/Images.xcassets/Contents.json | 6 - .../LogoRounded.imageset/Contents.json | 12 -- Modules/WRCore/Sources/WRCore/ErrorBar.swift | 51 ++++++++ .../Sources/WRCore/KotlinArrayWrapper.swift | 6 +- Modules/WRCore/Sources/WRCore/Mock.swift | 32 +++-- .../Sources/WRCore/ObservableViewModel.swift | 8 +- .../LogoRounded.imageset/Contents.json | 14 +++ .../LogoRounded.imageset/wr-round-yellow.svg | 56 +++++++++ Targets/Lava/WaiterRobotLava.plist | 6 +- .../LogoRounded.imageset/Contents.json | 14 +++ .../LogoRounded.imageset/wr-round.svg | 0 Targets/Prod/WaiterRobot.plist | 12 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Features/Billing/BillingScreen.swift | 4 +- WaiterRobot/Features/Billing/PayDialog.swift | 10 +- .../Features/Login/LoginScannerScreen.swift | 10 +- .../Features/Order/OrderProductNoteView.swift | 14 +-- WaiterRobot/Features/Order/OrderScreen.swift | 37 ++---- .../Features/Order/Search/ProductSearch.swift | 59 +++++---- .../Features/Settings/SettingsScreen.swift | 64 ++++------ .../Features/Settings/SwitchThemeView.swift | 4 +- .../SwitchEvent/SwitchEventScreen.swift | 50 ++++---- .../TableDetail/TableDetailScreen.swift | 13 +- .../TableList/TableGroupFilterSheet.swift | 91 ++++++++++++++ .../TableList/TableListFilterRow.swift | 117 ------------------ .../Features/TableList/TableListScreen.swift | 93 +++++--------- WaiterRobot/MainView.swift | 10 +- .../Resources/Images.xcassets/Contents.json | 6 - .../LogoRounded.imageset/Contents.json | 12 -- .../LogoRounded.imageset/wr-round.svg | 34 ----- WaiterRobot/Ui/LoadingOverlayView.swift | 4 +- WaiterRobot/Ui/ViewStateOverlayView.swift | 4 +- 33 files changed, 426 insertions(+), 433 deletions(-) delete mode 100644 Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/Contents.json delete mode 100644 Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/Contents.json create mode 100644 Modules/WRCore/Sources/WRCore/ErrorBar.swift create mode 100644 Targets/Lava/Images.xcassets/LogoRounded.imageset/Contents.json create mode 100644 Targets/Lava/Images.xcassets/LogoRounded.imageset/wr-round-yellow.svg create mode 100644 Targets/Prod/Images.xcassets/LogoRounded.imageset/Contents.json rename {Modules/SharedUI/Sources/SharedUI/Resources => Targets/Prod}/Images.xcassets/LogoRounded.imageset/wr-round.svg (100%) create mode 100644 WaiterRobot/Features/TableList/TableGroupFilterSheet.swift delete mode 100644 WaiterRobot/Features/TableList/TableListFilterRow.swift delete mode 100644 WaiterRobot/Resources/Images.xcassets/Contents.json delete mode 100644 WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/Contents.json delete mode 100644 WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg diff --git a/Modules/SharedUI/Sources/SharedUI/Images.swift b/Modules/SharedUI/Sources/SharedUI/Images.swift index bcfe6f5..4873187 100644 --- a/Modules/SharedUI/Sources/SharedUI/Images.swift +++ b/Modules/SharedUI/Sources/SharedUI/Images.swift @@ -2,6 +2,6 @@ import SwiftUI public extension Image { static var logoRounded: Image { - Image(.logoRounded) + Image("LogoRounded") } } diff --git a/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/Contents.json deleted file mode 100644 index e4164e5..0000000 --- a/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "wr-round.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Modules/WRCore/Sources/WRCore/ErrorBar.swift b/Modules/WRCore/Sources/WRCore/ErrorBar.swift new file mode 100644 index 0000000..e4d7cbb --- /dev/null +++ b/Modules/WRCore/Sources/WRCore/ErrorBar.swift @@ -0,0 +1,51 @@ +import shared +import SharedUI +import SwiftUI + +public struct ErrorBar: View { + let message: StringDesc + let initialLines: Int + let retryAction: (() -> Void)? + + @State private var expanded = false + + public init(message: StringDesc, initialLines: Int = 2, retryAction: (() -> Void)? = nil) { + self.message = message + self.initialLines = initialLines + self.retryAction = retryAction + } + + public var body: some View { + HStack(alignment: .center) { + Text(message()) + .lineLimit(expanded ? nil : initialLines) + .multilineTextAlignment(.leading) + // .foregroundColor(Color.onErrorContainer) + .frame(maxWidth: .infinity, alignment: .leading) + + if retryAction != nil { + Spacer().frame(width: 16) + Button(action: { + retryAction?() + }) { + Text(localize.exceptions_retry()) + .bold() + .multilineTextAlignment(.center) + .lineLimit(expanded ? nil : initialLines) + } + // .foregroundColor(Color.onErrorContainer) + } + } + .padding(.leading, 16) + .padding(.top, 8) + .padding(.trailing, retryAction == nil ? 16 : 8) + .padding(.bottom, 8) + // .background(Color.errorContainer) + .onTapGesture { + withAnimation { + expanded.toggle() + } + } + .animation(.default, value: expanded) + } +} diff --git a/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift b/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift index a9e5964..952c771 100644 --- a/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift +++ b/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift @@ -6,8 +6,12 @@ public extension Array where Element: AnyObject { guard let array = kotlinArray else { return nil } + self.init(array) + } + + init(_ kotlinArray: KotlinArray) { self.init() - let iterator = array.iterator() + let iterator = kotlinArray.iterator() while iterator.hasNext() { append(iterator.next() as! Element) } diff --git a/Modules/WRCore/Sources/WRCore/Mock.swift b/Modules/WRCore/Sources/WRCore/Mock.swift index c9d19db..55a6fd4 100644 --- a/Modules/WRCore/Sources/WRCore/Mock.swift +++ b/Modules/WRCore/Sources/WRCore/Mock.swift @@ -2,21 +2,31 @@ import Foundation import shared public enum Mock { - public static func tableGroups() -> [TableGroup] { + public static func groupedTables() -> [GroupedTables] { [ - tableGroup(with: 1, name: "Hof"), - tableGroup(with: 2, name: "Terasse"), - tableGroup(with: 3, name: "Zimmer A"), + GroupedTables( + id: 1, + name: "Hof", + eventId: 1, + color: nil, + tables: [ + table(with: 1), + table(with: 2), + table(with: 3), + ] + ), ] } - public static func tableGroup(with id: Int64, name: String = "Hof") -> TableGroup { - TableGroup( - id: id, - name: name, - color: "", - hidden: false, - ) + public static func tableGroups() -> [TableGroup] { + groupedTables().map { + TableGroup( + id: $0.id, + name: $0.name, + color: $0.color, + hidden: false, + ) + } } public static func table(with id: Int64, hasOrders: Bool = false) -> shared.Table { diff --git a/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift b/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift index 89dae0b..a658886 100644 --- a/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift +++ b/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift @@ -36,6 +36,12 @@ public class ObservableTableListViewModel: ObservableViewModel { + public init() { + super.init(viewModel: koin.tableGroupFilterVM()) + } +} + public class ObservableTableDetailViewModel: ObservableViewModel { public init(table: Table) { super.init(viewModel: koin.tableDetailVM(table: table)) @@ -62,7 +68,7 @@ public class ObservableOrderViewModel: ObservableViewModel { public init() { - super.init(viewModel: koin.getProductListVM()) + super.init(viewModel: koin.productListVM()) } } diff --git a/Targets/Lava/Images.xcassets/LogoRounded.imageset/Contents.json b/Targets/Lava/Images.xcassets/LogoRounded.imageset/Contents.json new file mode 100644 index 0000000..e576538 --- /dev/null +++ b/Targets/Lava/Images.xcassets/LogoRounded.imageset/Contents.json @@ -0,0 +1,14 @@ +{ + "images": + [ + { + "filename": "wr-round-yellow.svg", + "idiom": "universal" + } + ], + "info": + { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Targets/Lava/Images.xcassets/LogoRounded.imageset/wr-round-yellow.svg b/Targets/Lava/Images.xcassets/LogoRounded.imageset/wr-round-yellow.svg new file mode 100644 index 0000000..1a14ba8 --- /dev/null +++ b/Targets/Lava/Images.xcassets/LogoRounded.imageset/wr-round-yellow.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Targets/Lava/WaiterRobotLava.plist b/Targets/Lava/WaiterRobotLava.plist index b35ff2a..cb19726 100644 --- a/Targets/Lava/WaiterRobotLava.plist +++ b/Targets/Lava/WaiterRobotLava.plist @@ -26,7 +26,7 @@ CFBundleShortVersionString 2.5.0 CFBundleVersion - 28998383 + 29158000 ITSAppUsesNonExemptEncryption NSAppTransportSecurity @@ -54,10 +54,8 @@ UIImageName LogoRounded UIImageRespectsSafeAreaInsets - + - UILaunchStoryboardName - LaunchScreen.storyboard UIRequiredDeviceCapabilities armv7 diff --git a/Targets/Prod/Images.xcassets/LogoRounded.imageset/Contents.json b/Targets/Prod/Images.xcassets/LogoRounded.imageset/Contents.json new file mode 100644 index 0000000..988ec93 --- /dev/null +++ b/Targets/Prod/Images.xcassets/LogoRounded.imageset/Contents.json @@ -0,0 +1,14 @@ +{ + "images": + [ + { + "filename": "wr-round.svg", + "idiom": "universal" + } + ], + "info": + { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg b/Targets/Prod/Images.xcassets/LogoRounded.imageset/wr-round.svg similarity index 100% rename from Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg rename to Targets/Prod/Images.xcassets/LogoRounded.imageset/wr-round.svg diff --git a/Targets/Prod/WaiterRobot.plist b/Targets/Prod/WaiterRobot.plist index 8dfafcc..713b445 100644 --- a/Targets/Prod/WaiterRobot.plist +++ b/Targets/Prod/WaiterRobot.plist @@ -35,15 +35,15 @@ NSBluetoothAlwaysUsageDescription - We don't use bluetooth + We don't use bluetooth NSCameraUsageDescription Camera is needed to scan QR-Codes NSContactsUsageDescription - We don't use your contacts + We don't use your contacts NSLocationWhenInUseUsageDescription - We don't use your location + We don't use your location NSMotionUsageDescription - We don't use your motion sensors + We don't use your motion sensors UIApplicationSceneManifest UIApplicationSupportsMultipleScenes @@ -54,10 +54,8 @@ UIImageName LogoRounded UIImageRespectsSafeAreaInsets - + - UILaunchStoryboardName - LaunchScreen.storyboard UIRequiredDeviceCapabilities armv7 diff --git a/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 55894a6..4fd8fe6 100644 --- a/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DatepollSystems/WaiterRobot-Shared-Android.git", "state" : { - "revision" : "c01f764d92dcd7df8ee4ad4ecba55a9200ed7a00", - "version" : "1.7.3" + "revision" : "f2ff44dc52e8df3f4e68da4d05e27ad0b0487a33", + "version" : "1.7.6" } } ], diff --git a/WaiterRobot/Features/Billing/BillingScreen.swift b/WaiterRobot/Features/Billing/BillingScreen.swift index cd0e690..800de70 100644 --- a/WaiterRobot/Features/Billing/BillingScreen.swift +++ b/WaiterRobot/Features/Billing/BillingScreen.swift @@ -58,7 +58,7 @@ private struct BillingScreenView: View { var body: some View { ViewStateOverlayView(state: state.paymentState) { - let billItemsState = onEnum(of: state.billItems_) + let billItemsState = onEnum(of: state.billItems) if case let .loading(ressource) = billItemsState, ressource.data == nil { ProgressView() @@ -67,7 +67,7 @@ private struct BillingScreenView: View { Text("Error \(resource.userMessage())") } - if let billItems = Array(state.billItems_.data), !billItems.isEmpty { + if let billItems = Array(state.billItems.data), !billItems.isEmpty { content(billItems: billItems) } else { Text(localize.billing_noOrder(table.groupName, table.number.description)) diff --git a/WaiterRobot/Features/Billing/PayDialog.swift b/WaiterRobot/Features/Billing/PayDialog.swift index ae3048a..79d0e06 100644 --- a/WaiterRobot/Features/Billing/PayDialog.swift +++ b/WaiterRobot/Features/Billing/PayDialog.swift @@ -15,14 +15,14 @@ struct PayDialog: View { NavigationView { VStack { HStack { - Text(localize.billing.total() + ":") + Text(localize.billing_total() + ":") .font(.title2) Spacer() Text(viewModel.state.priceSum.description) .font(.title2) } - TextField(localize.billing.given(), text: $moneyGiven) + TextField(localize.billing_given(), text: $moneyGiven) .font(.title) .keyboardType(.numbersAndPunctuation) .onChange(of: moneyGiven) { value in @@ -37,7 +37,7 @@ struct PayDialog: View { ) HStack { - Text(localize.billing.change() + ":") + Text(localize.billing_change() + ":") .font(.title2) Spacer() @@ -52,14 +52,14 @@ struct PayDialog: View { .padding() .toolbar { ToolbarItem(placement: .navigationBarLeading) { - Button(localize.dialog.cancel()) { + Button(localize.dialog_cancel()) { dismiss() } } } .toolbar { ToolbarItem(placement: .confirmationAction) { - Button(localize.billing.pay()) { + Button(localize.billing_pay_cash()) { viewModel.actual.paySelection(paymentSheetShown: true) dismiss() } diff --git a/WaiterRobot/Features/Login/LoginScannerScreen.swift b/WaiterRobot/Features/Login/LoginScannerScreen.swift index a47fe0a..c0c884b 100644 --- a/WaiterRobot/Features/Login/LoginScannerScreen.swift +++ b/WaiterRobot/Features/Login/LoginScannerScreen.swift @@ -20,11 +20,7 @@ struct LoginScannerScreen: View { case let error as ViewState.Error: content() .alert(isPresented: Binding.constant(true)) { - Alert( - title: Text(error.title), - message: Text(error.message), - dismissButton: .cancel(Text("OK"), action: error.onDismiss) - ) + Alert(error.dialog) } default: fatalError("Unexpected ViewState: \(viewModel.state.viewState.description)") @@ -45,14 +41,14 @@ struct LoginScannerScreen: View { } } - Text(localize.login.scanner.desc()) + Text(localize.login_scanner_desc()) .padding() .multilineTextAlignment(.center) Button { viewModel.actual.goBack() } label: { - Text(localize.dialog.cancel()) + Text(localize.dialog_cancel()) } } .withViewModel(viewModel, navigator) diff --git a/WaiterRobot/Features/Order/OrderProductNoteView.swift b/WaiterRobot/Features/Order/OrderProductNoteView.swift index 3e01073..93498de 100644 --- a/WaiterRobot/Features/Order/OrderProductNoteView.swift +++ b/WaiterRobot/Features/Order/OrderProductNoteView.swift @@ -18,7 +18,7 @@ struct OrderProductNoteView: View { var body: some View { NavigationView { content() - .navigationTitle(localize.order.addNoteDialog.title(value0: name)) + .navigationTitle(localize.order_add_note_title(name)) .navigationBarTitleDisplayMode(.inline) } } @@ -26,16 +26,16 @@ struct OrderProductNoteView: View { @ViewBuilder private func content() -> some View { VStack { - Text(localize.order.addNoteDialog.inputLabel()) + Text(localize.order_add_note_input_label()) Group { if #available(iOS 16, *) { - TextField(localize.order.addNoteDialog.inputPlaceholder(), text: $noteText, axis: .vertical) + TextField(localize.order_add_note_input_placeholder(), text: $noteText, axis: .vertical) .lineLimit(5, reservesSpace: true) .toolbarBackground(.visible, for: .bottomBar) } else { // TODO: Maybe change to TextEditor - TextField(localize.order.addNoteDialog.inputPlaceholder(), text: $noteText) + TextField(localize.order_add_note_input_placeholder(), text: $noteText) .lineLimit(5) } } @@ -82,14 +82,14 @@ struct OrderProductNoteView: View { @ViewBuilder private func cancelButton() -> some View { - Button(localize.dialog.cancel(), role: .cancel) { + Button(localize.dialog_cancel(), role: .cancel) { dismiss() } } @ViewBuilder private func clearButton() -> some View { - Button(localize.dialog.clear(), role: .destructive) { + Button(localize.dialog_clear(), role: .destructive) { noteText = "" onSaveNote(nil) dismiss() @@ -99,7 +99,7 @@ struct OrderProductNoteView: View { @ViewBuilder private func saveButton() -> some View { - Button(localize.dialog.save()) { + Button(localize.dialog_save()) { onSaveNote(noteText) dismiss() } diff --git a/WaiterRobot/Features/Order/OrderScreen.swift b/WaiterRobot/Features/Order/OrderScreen.swift index 8e5ed52..e4189c8 100644 --- a/WaiterRobot/Features/Order/OrderScreen.swift +++ b/WaiterRobot/Features/Order/OrderScreen.swift @@ -23,38 +23,27 @@ struct OrderScreen: View { } var body: some View { - VStack { - switch onEnum(of: viewModel.state.currentOrder) { - case .loading: - ProgressView() - - case let .error(error): - Text(error.userMessage) - .foregroundStyle(.red) - .padding(.horizontal) - - currentOder(error.data) - - case let .success(resource): - currentOder(resource.data) - } + ViewStateOverlayView(state: viewModel.state.orderingState) { + currentOder(Array(viewModel.state.currentOrder)) } - .navigationTitle(localize.order.title(value0: table.groupName, value1: table.number.description)) + .navigationTitle(localize.order_title(table.groupName, table.number.description)) .navigationBarTitleDisplayMode(.large) .navigationBarBackButtonHidden() .confirmationDialog( - localize.order.notSent.title(), + localize.order_notSent_title(), isPresented: $showAbortOrderConfirmationDialog, titleVisibility: .visible ) { - Button(localize.dialog.closeAnyway(), role: .destructive) { + Button(localize.dialog_closeAnyway(), role: .destructive) { viewModel.actual.abortOrder() } } message: { - Text(localize.order.notSent.desc()) + Text(localize.order_notSent_desc()) } .sheet(isPresented: $showProductSearch) { - ProductSearch(viewModel: viewModel) + ProductSearch( + addItem: { viewModel.actual.addItem(product: $0, amount: $1) } + ) } .animation(.default, value: viewModel.state.currentOrder) .withViewModel(viewModel, navigator) @@ -62,15 +51,13 @@ struct OrderScreen: View { @ViewBuilder private func currentOder( - _ currentOrderArray: KotlinArray? + _ currentOrder: [OrderItem] ) -> some View { - let currentOrder = currentOrderArray.map { Array($0) } ?? Array() - VStack(spacing: 0) { if currentOrder.isEmpty { Spacer() - Text(localize.order.addProduct()) + Text(localize.order_product_add()) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) .padding() @@ -116,7 +103,7 @@ struct OrderScreen: View { } .buttonStyle(.primary) } - .customBackNavigation(title: localize.dialog.cancel(), icon: "chevron.backward") { + .customBackNavigation(title: localize.dialog_cancel(), icon: "chevron.backward") { if currentOrder.isEmpty { viewModel.actual.abortOrder() } else { diff --git a/WaiterRobot/Features/Order/Search/ProductSearch.swift b/WaiterRobot/Features/Order/Search/ProductSearch.swift index 8dde89b..2ad9869 100644 --- a/WaiterRobot/Features/Order/Search/ProductSearch.swift +++ b/WaiterRobot/Features/Order/Search/ProductSearch.swift @@ -3,10 +3,11 @@ import SwiftUI import WRCore struct ProductSearch: View { + let addItem: (_ product: Product, _ amount: Int32) -> Void + @Environment(\.dismiss) private var dismiss - @ObservedObject var viewModel: ObservableProductListViewModel - let addItem: (_ product: Product, _ amount: Int32) -> Void + @ObservedObject private var viewModel = ObservableProductListViewModel() @State private var search: String = "" @State private var selectedTab: Int = 0 @@ -17,22 +18,40 @@ struct ProductSearch: View { var body: some View { NavigationView { - switch onEnum(of: viewModel.state.productGroups) { - case .loading: - ProgressView() - case let .error(resource): - productGroupsError(error: resource) - case let .success(resource): - if let productGroups = resource.data { - productsGroupsList(productGroups: productGroups) + content() + .observeState(of: viewModel) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button(localize.dialog_cancel()) { + dismiss() + } + } } + } + } + + @ViewBuilder + private func content() -> some View { + switch onEnum(of: viewModel.state.productGroups) { + case .loading: + ProgressView() + case let .error(resource): + productGroupsError(error: resource) + case let .success(resource): + if let productGroups = Array(resource.data) { + productsGroupsList(productGroups: productGroups) } - }.observeState(of: viewModel) + } } @ViewBuilder - private func productsGroupsList(productGroups: KotlinArray) -> some View { - if let productGroups = Array(productGroups), !productGroups.isEmpty { + private func productsGroupsList(productGroups: [GroupedProducts]) -> some View { + if productGroups.isEmpty { + Text(localize.productSearch_noProductFound()) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding() + } else { VStack { ProducSearchTabBarHeader(currentTab: $selectedTab, tabBarOptions: getGroupNames(productGroups)) @@ -49,7 +68,7 @@ struct ProductSearch: View { .padding() let enumeratedProductGroups = Array(productGroups.enumerated()) - ForEach(enumeratedProductGroups, id: \.element.id) { index, _ in + ForEach(enumeratedProductGroups, id: \.element.id) { index, groupedProducts in ScrollView { LazyVGrid(columns: layout, spacing: 0) { productGroup(groupedProducts: groupedProducts) @@ -64,18 +83,6 @@ struct ProductSearch: View { .tabViewStyle(.page(indexDisplayMode: .never)) .searchable(text: $search, placement: .navigationBarDrawer(displayMode: .always)) .onChange(of: search, perform: { viewModel.actual.filterProducts(filter: $0) }) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button(localize.dialog_cancel()) { - dismiss() - } - } - } - } else { - Text(localize.productSearch_noProductFound()) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - .padding() } } diff --git a/WaiterRobot/Features/Settings/SettingsScreen.swift b/WaiterRobot/Features/Settings/SettingsScreen.swift index 20d9a7b..7b451c9 100644 --- a/WaiterRobot/Features/Settings/SettingsScreen.swift +++ b/WaiterRobot/Features/Settings/SettingsScreen.swift @@ -12,44 +12,24 @@ struct SettingsScreen: View { @StateObject private var viewModel = ObservableSettingsViewModel() var body: some View { - switch viewModel.state.viewState { - case is ViewState.Loading: - ProgressView() - case is ViewState.Idle: - content() - case let error as ViewState.Error: - content() - .alert(isPresented: Binding.constant(true)) { - Alert( - title: Text(error.title), - message: Text(error.message), - dismissButton: .cancel(Text("OK"), action: error.onDismiss) - ) - } - default: - fatalError("Unexpected ViewState: \(viewModel.state.viewState.description)") - } - } - - private func content() -> some View { List { general() payment() - Section(header: Text(localize.settings.about.title())) { - Link(localize.settings.about.privacyPolicy(), destination: URL(string: CommonApp.shared.privacyPolicyUrl)!) + Section(header: Text(localize.settings_about_title())) { + Link(localize.settings_about_privacyPolicy(), destination: URL(string: CommonApp.shared.privacyPolicyUrl)!) } HStack { Spacer() - Text(viewModel.state.versionString) + Text(viewModel.state.versionString()) .font(.footnote) Spacer() } .listRowBackground(Color.clear) } - .navigationTitle(localize.settings.title()) + .navigationTitle(localize.settings_title()) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { @@ -60,28 +40,28 @@ struct SettingsScreen: View { } } .confirmationDialog( - localize.settings.general.logout.title(value0: CommonApp.shared.settings.organisationName), + localize.settings_general_logout_title(CommonApp.shared.settings.organisationName), isPresented: $showConfirmLogout, titleVisibility: .visible ) { - Button(localize.settings.general.logout.action(), role: .destructive, action: { viewModel.actual.logout() }) - Button(localize.settings.general.keepLoggedIn(), role: .cancel, action: { showConfirmLogout = false }) + Button(localize.settings_general_logout_action(), role: .destructive, action: { viewModel.actual.logout() }) + Button(localize.settings_general_logout_cancel(), role: .cancel, action: { showConfirmLogout = false }) } message: { - Text(localize.settings.general.logout.desc(value0: CommonApp.shared.settings.organisationName)) + Text(localize.settings_general_logout_desc(CommonApp.shared.settings.organisationName)) } .confirmationDialog( - localize.settings.payment.skipMoneyBackDialog.title(), + localize.settings_payment_skipMoneyBackDialog_title(), isPresented: $showConfirmSkipMoneyBackDialog, titleVisibility: .visible ) { - Button(localize.settings.payment.skipMoneyBackDialog.confirmAction(), role: .destructive) { + Button(localize.settings_payment_skipMoneyBackDialog_confirm_action(), role: .destructive) { viewModel.actual.toggleSkipMoneyBackDialog(value: true, confirmed: true) } - Button(localize.dialog.cancel(), role: .cancel) { + Button(localize.dialog_cancel(), role: .cancel) { showConfirmSkipMoneyBackDialog = false } } message: { - Text(localize.settings.payment.skipMoneyBackDialog.confirmDesc()) + Text(localize.settings_payment_skipMoneyBackDialog_confirm_desc()) } .withViewModel(viewModel, navigator) { effect in switch onEnum(of: effect) { @@ -94,10 +74,10 @@ struct SettingsScreen: View { } private func general() -> some View { - Section(header: Text(localize.settings.general.title())) { + Section(header: Text(localize.settings_general_title())) { SettingsItem( icon: "rectangle.portrait.and.arrow.right", - title: localize.settings.general.logout.action(), + title: localize.settings_general_logout_action(), subtitle: "\"\(CommonApp.shared.settings.organisationName)\" / \"\(CommonApp.shared.settings.waiterName)\"", onClick: { showConfirmLogout = true @@ -106,7 +86,7 @@ struct SettingsScreen: View { SettingsItem( icon: "person.3", - title: localize.switchEvent.title(), + title: localize.switchEvent_title(), subtitle: CommonApp.shared.settings.eventName, onClick: { viewModel.actual.switchEvent() @@ -120,8 +100,8 @@ struct SettingsScreen: View { SettingsItem( icon: "arrow.triangle.2.circlepath", - title: localize.settings.general.refresh.title(), - subtitle: localize.settings.general.refresh.desc(), + title: localize.settings_general_refresh_title(), + subtitle: localize.settings_general_refresh_desc(), onClick: { viewModel.actual.refreshAll() } @@ -130,11 +110,11 @@ struct SettingsScreen: View { } private func payment() -> some View { - Section(header: Text(localize.settings.payment.title())) { + Section(header: Text(localize.settings_payment_title())) { SettingsItem( icon: "dollarsign.arrow.circlepath", - title: localize.settings.payment.skipMoneyBackDialog.title(), - subtitle: localize.settings.payment.skipMoneyBackDialog.desc(), + title: localize.settings_payment_skipMoneyBackDialog_title(), + subtitle: localize.settings_payment_skipMoneyBackDialog_desc(), action: { Toggle( isOn: .init( @@ -154,8 +134,8 @@ struct SettingsScreen: View { SettingsItem( icon: "checkmark.square", - title: localize.settings.payment.selectAllProductsByDefault.title(), - subtitle: localize.settings.payment.selectAllProductsByDefault.desc(), + title: localize.settings_payment_selectAllProductsByDefault_title(), + subtitle: localize.settings_payment_selectAllProductsByDefault_desc(), action: { Toggle( isOn: .init( diff --git a/WaiterRobot/Features/Settings/SwitchThemeView.swift b/WaiterRobot/Features/Settings/SwitchThemeView.swift index 623e0a5..c35bd19 100644 --- a/WaiterRobot/Features/Settings/SwitchThemeView.swift +++ b/WaiterRobot/Features/Settings/SwitchThemeView.swift @@ -21,9 +21,9 @@ struct SwitchThemeView: View { .padding(.trailing) .foregroundColor(.blue) - Picker(localize.settings.general.darkMode.title(), selection: $selectedTheme) { + Picker(localize.settings_general_darkMode_title(), selection: $selectedTheme) { ForEach(AppTheme.companion.valueList(), id: \.name) { theme in - Text(theme.settingsText()).tag(theme) + Text(theme.settingsText().localized()).tag(theme) } } .onChange(of: selectedTheme, perform: onChange) diff --git a/WaiterRobot/Features/SwitchEvent/SwitchEventScreen.swift b/WaiterRobot/Features/SwitchEvent/SwitchEventScreen.swift index b32caf3..b138276 100644 --- a/WaiterRobot/Features/SwitchEvent/SwitchEventScreen.swift +++ b/WaiterRobot/Features/SwitchEvent/SwitchEventScreen.swift @@ -11,26 +11,6 @@ struct SwitchEventScreen: View { @State private var selectedEvent: Event? var body: some View { - VStack { - switch onEnum(of: viewModel.state.viewState) { - case .loading: - ProgressView() - case .idle: - content() - case let .error(error): - content() - .alert(isPresented: Binding.constant(true)) { - Alert( - title: Text(error.title), - message: Text(error.message), - dismissButton: .cancel(Text("OK"), action: error.onDismiss) - ) - } - } - }.withViewModel(viewModel, navigator) - } - - private func content() -> some View { VStack { Image(systemName: "person.3") .resizable() @@ -38,22 +18,37 @@ struct SwitchEventScreen: View { .frame(maxHeight: 100) .padding() - Text(localize.switchEvent.desc()) + Text(localize.switchEvent_desc()) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) .padding() Divider() - ScrollView { - if viewModel.state.events.isEmpty { - Text(localize.switchEvent.noEventFound()) + content(viewModel.state.events) + .refreshable { + try? await viewModel.actual.loadEvents().join() + } + + }.withViewModel(viewModel, navigator) + } + + private func content(_ eventResource: shared.Resource>) -> some View { + ScrollView { + let resource = onEnum(of: eventResource) + + if case let .error(error) = resource { + ErrorBar(message: error.userMessage, retryAction: { viewModel.actual.loadEvents() }) + } + if let events = Array(resource.data) { + if events.isEmpty { + Text(localize.switchEvent_noEventFound()) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) .padding() } else { LazyVStack { - ForEach(viewModel.state.events, id: \.id) { event in + ForEach(events, id: \.id) { event in Button { viewModel.actual.onEventSelected(event: event) } label: { @@ -64,9 +59,8 @@ struct SwitchEventScreen: View { } } } - } - .refreshable { - viewModel.actual.loadEvents() + } else { + ProgressView() } } } diff --git a/WaiterRobot/Features/TableDetail/TableDetailScreen.swift b/WaiterRobot/Features/TableDetail/TableDetailScreen.swift index 17d00d2..ac6a337 100644 --- a/WaiterRobot/Features/TableDetail/TableDetailScreen.swift +++ b/WaiterRobot/Features/TableDetail/TableDetailScreen.swift @@ -18,14 +18,14 @@ struct TableDetailScreen: View { var body: some View { content() - .navigationTitle(localize.tableDetail.title(value0: table.groupName, value1: table.number.description)) + .navigationTitle(localize.tableDetail_title(table.groupName, table.number.description)) .withViewModel(viewModel, navigator) } // TODO: add refreshing and loading indicator (also check android) private func content() -> some View { VStack { - switch onEnum(of: viewModel.state.orderedItemsResource) { + switch onEnum(of: viewModel.state.orderedItems) { case .loading: ProgressView() @@ -33,7 +33,7 @@ struct TableDetailScreen: View { tableDetailsError(error) case let .success(resource): - if let orderedItems = resource.data as? [OrderedItem] { + if let orderedItems = Array(resource.data) { tableDetails(orderedItems: orderedItems) } } @@ -41,12 +41,11 @@ struct TableDetailScreen: View { } private func tableDetails(orderedItems: [OrderedItem]) -> some View { - // TODO: we need KotlinArray here in shared VStack { if orderedItems.isEmpty { Spacer() - Text(localize.tableDetail.noOrder(value0: table.groupName, value1: table.number.description)) + Text(localize.tableDetail_noOrder(table.groupName, table.number.description)) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) .padding() @@ -85,7 +84,7 @@ struct TableDetailScreen: View { } } - private func tableDetailsError(_ error: ResourceError) -> some View { - Text(error.userMessage) + private func tableDetailsError(_ error: ResourceError>) -> some View { + Text(error.userMessage()) } } diff --git a/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift b/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift new file mode 100644 index 0000000..924422d --- /dev/null +++ b/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift @@ -0,0 +1,91 @@ +import shared +import SwiftUI +import UIPilot +import WRCore + +struct TableGroupFilterSheet: View { + @Environment(\.dismiss) private var dismiss + + @StateObject private var viewModel = ObservableTableGroupFilterViewModel() + + var body: some View { + NavigationView { + content() + .observeState(of: viewModel) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(localize.dialog_cancel()) { + dismiss() + } + } + } + } + } + + @ViewBuilder + private func content() -> some View { + switch onEnum(of: viewModel.state.groups) { + case .loading: + ProgressView() + case let .error(resource): + Text(resource.userMessage()) + case let .success(resource): + TableGroupFilter( + groups: Array(resource.data) ?? [], + showAll: { viewModel.actual.showAll() }, + hideAll: { viewModel.actual.hideAll() }, + onToggle: { viewModel.actual.toggleFilter(tableGroup: $0) } + ) + } + } +} + +private struct TableGroupFilter: View { + let groups: [TableGroup] + let showAll: () -> Void + let hideAll: () -> Void + let onToggle: (TableGroup) -> Void + + var body: some View { + if groups.isEmpty { + // Should not happen as open filter is only shown when there are groups + Text(localize.tableList_noTableFound()) + } else { + ScrollView { + LazyVStack(alignment: .leading) { + ForEach(groups, id: \.id) { group in + HStack { + Circle() + .fill(Color(hex: group.color) ?? Color.gray.opacity(0.3)) + .frame(height: 40) + + Text(group.name) + + Spacer() + + Toggle( + isOn: .init( + get: { !group.hidden }, + set: { _ in onToggle(group) } + ), + label: {} + ).labelsHidden() + }.padding(.horizontal) + } + } + } + } + } +} + +#Preview { + TableGroupFilter( + groups: [ + shared.TableGroup(id: 1, name: "Group 1", color: "ffaaff", hidden: false), + shared.TableGroup(id: 2, name: "Group 2", color: "aaffaa", hidden: false), + ], + showAll: {}, + hideAll: {}, + onToggle: { _ in } + ) +} diff --git a/WaiterRobot/Features/TableList/TableListFilterRow.swift b/WaiterRobot/Features/TableList/TableListFilterRow.swift deleted file mode 100644 index a8e7aa3..0000000 --- a/WaiterRobot/Features/TableList/TableListFilterRow.swift +++ /dev/null @@ -1,117 +0,0 @@ -import shared -import SwiftUI - -struct TableListFilterRow: View { - let tableGroups: [TableGroup] - let onToggleFilter: (TableGroup) -> Void - let onSelectAll: () -> Void - let onUnselectAll: () -> Void - - var body: some View { - if #available(iOS 16, *) { - newFilter() - } else { - oldFilter() - } - } - - @available(iOS 16, *) - private func newFilter() -> some View { - VStack(spacing: 20) { - DynamicGrid( - horizontalSpacing: 5, - verticalSpacing: 5 - ) { - ForEach(tableGroups, id: \.id) { group in - if group.hidden { - Button { - onToggleFilter(group) - } label: { - Text(group.name) - .padding() - } - .buttonStyle(.gray) - } else { - Button { - onToggleFilter(group) - } label: { - Text(group.name) - .padding() - } - .buttonStyle(.primary) - } - } - } - - HStack { - Button { - onSelectAll() - } label: { - Image(systemName: "rectangle.badge.checkmark") - .imageScale(.large) - .padding(8) - } - .buttonStyle(.primary) - - Button { - onUnselectAll() - } label: { - Image(systemName: "rectangle.badge.xmark") - .imageScale(.large) - .padding(8) - } - .buttonStyle(.gray) - } - .frame(maxWidth: .infinity) - } - } - - private func oldFilter() -> some View { - HStack { - ScrollView(.horizontal) { - HStack { - ForEach(tableGroups, id: \.id) { group in - Button { - onToggleFilter(group) // viewModel.actual.toggleFilter(tableGroup: group) - } label: { - Text(group.name) - } - .buttonStyle(.bordered) - .tint(group.hidden ? .primary : .blue) - } - } - .padding(.horizontal) - } - .padding(.bottom, 4) - - Button { - onSelectAll() - } label: { - Image(systemName: "checkmark") - } - .padding(.trailing) - .disabled(tableGroups.allSatisfy { !$0.hidden }) - - Button { - onUnselectAll() - } label: { - Image(systemName: "xmark") - } - .padding(.trailing) - .disabled(tableGroups.allSatisfy(\.hidden)) - } - } -} - -#Preview { - TableListFilterRow( - tableGroups: [ - TableGroup(id: 1, name: "Test Group1", color: nil, hidden: true), - TableGroup(id: 2, name: "Test Group2", color: nil, hidden: false), - ], - onToggleFilter: { _ in }, - onSelectAll: {}, - onUnselectAll: {} - ) - .padding() -} diff --git a/WaiterRobot/Features/TableList/TableListScreen.swift b/WaiterRobot/Features/TableList/TableListScreen.swift index 012956e..b4d2c43 100644 --- a/WaiterRobot/Features/TableList/TableListScreen.swift +++ b/WaiterRobot/Features/TableList/TableListScreen.swift @@ -16,6 +16,13 @@ struct TableListScreen: View { if #available(iOS 16.0, *) { content() .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + showFilters.toggle() + } label: { + Image(systemName: "line.3.horizontal.decrease") + } + } ToolbarItem(placement: .topBarTrailing) { Button { viewModel.actual.openSettings() @@ -28,6 +35,13 @@ struct TableListScreen: View { } else { content() .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + showFilters.toggle() + } label: { + Image(systemName: "line.3.horizontal.decrease") + } + } ToolbarItem(placement: .navigationBarTrailing) { Button { viewModel.actual.openSettings() @@ -56,23 +70,31 @@ struct TableListScreen: View { } } .navigationBarTitleDisplayMode(.inline) - .animation(.spring, value: viewModel.state.tableGroupsArray) + .animation(.spring, value: viewModel.state.tableGroups) + .sheet(isPresented: $showFilters) { + TableGroupFilterSheet() + } .withViewModel(viewModel, navigator) } private func content() -> some View { ZStack { - if let data = viewModel.state.tableGroupsArray.data { - tableList(data: data) + if let tableGroups = Array(viewModel.state.tableGroups.data) { + TableListView( + tableGroups: tableGroups, + onTableSelect: { viewModel.actual.onTableClick(table: $0) } + ) + } else { + ProgressView() } - switch onEnum(of: viewModel.state.tableGroupsArray) { + switch onEnum(of: viewModel.state.tableGroups) { case let .error(resource): VStack { Spacer() HStack { - Text(resource.userMessage) + Text(resource.userMessage()) .padding() Spacer() @@ -92,28 +114,10 @@ struct TableListScreen: View { } } } - - @ViewBuilder - private func tableList(data: KotlinArray) -> some View { - let tableGroups = Array(data) - - TableListView( - showFilters: $showFilters, - tableGroups: tableGroups, - onToggleFilter: { viewModel.actual.toggleFilter(tableGroup: $0) }, - onSelectAll: { viewModel.actual.showAll() }, - onUnselectAll: { viewModel.actual.hideAll() }, - onTableSelect: { viewModel.actual.onTableClick(table: $0) } - ) - } } struct TableListView: View { - @Binding var showFilters: Bool - let tableGroups: [TableGroup] - let onToggleFilter: (TableGroup) -> Void - let onSelectAll: () -> Void - let onUnselectAll: () -> Void + let tableGroups: [GroupedTables] let onTableSelect: (shared.Table) -> Void private let layout = [ @@ -122,25 +126,10 @@ struct TableListView: View { var body: some View { VStack(spacing: 0) { - if tableGroups.count > 1, showFilters { - VStack { - TableListFilterRow( - tableGroups: tableGroups, - onToggleFilter: onToggleFilter, - onSelectAll: onSelectAll, - onUnselectAll: onUnselectAll - ) - } - .padding() - .background(Color(UIColor.systemBackground)) - } - - Divider() - if tableGroups.isEmpty { Spacer() - Text(localize.tableList.noTableFound()) + Text(localize.tableList_noTableFound()) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) .padding() @@ -152,10 +141,10 @@ struct TableListView: View { columns: layout, pinnedViews: [.sectionHeaders] ) { - ForEach(tableGroups.filter { !$0.hidden }, id: \.id) { group in + ForEach(tableGroups, id: \.id) { group in if !group.tables.isEmpty { TableGroupSection( - tableGroup: group, + groupedTables: group, onTableClick: onTableSelect ) } @@ -163,20 +152,8 @@ struct TableListView: View { } .padding() } - .toolbar { - ToolbarItem(placement: .topBarLeading) { - if tableGroups.count > 1 { - Button { - showFilters.toggle() - } label: { - Image(systemName: "slider.horizontal.3") - } - } - } - } } } - .animation(.easeIn, value: showFilters) } } @@ -192,11 +169,9 @@ struct TableListView: View { PreviewView { NavigationView { TableListView( - showFilters: .constant(false), - tableGroups: Mock.tableGroups() - ) { _ in - } onSelectAll: {} onUnselectAll: {} onTableSelect: { _ in - } + tableGroups: Mock.groupedTables(), + onTableSelect: { _ in } + ) } } } diff --git a/WaiterRobot/MainView.swift b/WaiterRobot/MainView.swift index 17d12e1..d7fc025 100644 --- a/WaiterRobot/MainView.swift +++ b/WaiterRobot/MainView.swift @@ -63,7 +63,7 @@ struct MainView: View { .withViewModel(viewModel, navigator) { effect in switch onEnum(of: effect) { case let .showSnackBar(snackBar): - snackBarMessage = snackBar.message + snackBarMessage = snackBar.message() DispatchQueue.main.asyncAfter(deadline: .now() + 5) { snackBarMessage = nil } @@ -74,14 +74,14 @@ struct MainView: View { viewModel.actual.onDeepLink(url: url.absoluteString) } .alert( - localize.app.updateAvailable.title(), + localize.app_updateAvailable_title(), isPresented: $showUpdateAvailableAlert ) { - Button(localize.dialog.cancel(), role: .cancel) { + Button(localize.dialog_cancel(), role: .cancel) { showUpdateAvailableAlert = false } - Button(localize.app.forceUpdate.openStore(value0: "App Store")) { + Button(localize.app_forceUpdate_openStore("App Store")) { guard let storeUrl = VersionChecker.shared.storeUrl, let url = URL(string: storeUrl) else { @@ -93,7 +93,7 @@ struct MainView: View { } } } message: { - Text(localize.app.updateAvailable.message()) + Text(localize.app_updateAvailable_message()) } .onAppear { VersionChecker.shared.checkVersion { diff --git a/WaiterRobot/Resources/Images.xcassets/Contents.json b/WaiterRobot/Resources/Images.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/WaiterRobot/Resources/Images.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/Contents.json b/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/Contents.json deleted file mode 100644 index e4164e5..0000000 --- a/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "wr-round.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg b/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg deleted file mode 100644 index 14b173f..0000000 --- a/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WaiterRobot/Ui/LoadingOverlayView.swift b/WaiterRobot/Ui/LoadingOverlayView.swift index c478bec..c9567ee 100644 --- a/WaiterRobot/Ui/LoadingOverlayView.swift +++ b/WaiterRobot/Ui/LoadingOverlayView.swift @@ -1,6 +1,6 @@ import SwiftUI -struct LoadingOverlayView: View { +public struct LoadingOverlayView: View { let isLoading: Bool let content: () -> Content @@ -9,7 +9,7 @@ struct LoadingOverlayView: View { self.content = content } - var body: some View { + public var body: some View { ZStack { content() .opacity(isLoading ? 0.5 : 1.0) diff --git a/WaiterRobot/Ui/ViewStateOverlayView.swift b/WaiterRobot/Ui/ViewStateOverlayView.swift index 1c6a49f..4772e7d 100644 --- a/WaiterRobot/Ui/ViewStateOverlayView.swift +++ b/WaiterRobot/Ui/ViewStateOverlayView.swift @@ -1,7 +1,7 @@ import shared import SwiftUI -struct ViewStateOverlayView: View { +public struct ViewStateOverlayView: View { let state: Skie.Shared.ViewState.__Sealed let content: () -> Content @@ -10,7 +10,7 @@ struct ViewStateOverlayView: View { self.content = content } - var body: some View { + public var body: some View { ZStack { LoadingOverlayView(isLoading: isLoading) { VStack(alignment: .leading) { From 7f6adaf0b027ae15988c8db26cb8f88838a8e65f Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Mon, 23 Jun 2025 21:15:11 +0200 Subject: [PATCH 03/11] Fixed launch logo and readme --- README.md | 6 +- .../LogoRounded.imageset/Contents.json | 20 ++-- .../LogoRounded.imageset/wr-round-yellow.svg | 92 ++++++++----------- Targets/Lava/WaiterRobotLava.plist | 2 +- .../LogoRounded.imageset/Contents.json | 20 ++-- .../Features/TableList/TableListScreen.swift | 5 +- 6 files changed, 61 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 899d38c..760d2c6 100644 --- a/README.md +++ b/README.md @@ -20,16 +20,14 @@ The KMM module is integrated as a Swift-Package (shared). This project uses XcodeGen for generating the Xcode project. -1. Xcodegen +1. Gems Run in your terminal: ```bash -swift run xcodegen +bundle install ``` -> This command must also be run after switching branches and it's advisable to also run it after a `git pull` - 2. Git pre-commit hook To have unified formatting, we use SwiftFormat. The pre-commit hook can be installed if the code should be formatted automatically before every commit. Execute following command in your terminal: diff --git a/Targets/Lava/Images.xcassets/LogoRounded.imageset/Contents.json b/Targets/Lava/Images.xcassets/LogoRounded.imageset/Contents.json index e576538..c7d86da 100644 --- a/Targets/Lava/Images.xcassets/LogoRounded.imageset/Contents.json +++ b/Targets/Lava/Images.xcassets/LogoRounded.imageset/Contents.json @@ -1,14 +1,12 @@ { - "images": - [ - { - "filename": "wr-round-yellow.svg", - "idiom": "universal" - } - ], - "info": + "images" : [ { - "author": "xcode", - "version": 1 + "filename" : "wr-round-yellow.svg", + "idiom" : "universal" } -} \ No newline at end of file + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Targets/Lava/Images.xcassets/LogoRounded.imageset/wr-round-yellow.svg b/Targets/Lava/Images.xcassets/LogoRounded.imageset/wr-round-yellow.svg index 1a14ba8..7a9af26 100644 --- a/Targets/Lava/Images.xcassets/LogoRounded.imageset/wr-round-yellow.svg +++ b/Targets/Lava/Images.xcassets/LogoRounded.imageset/wr-round-yellow.svg @@ -1,56 +1,38 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Targets/Lava/WaiterRobotLava.plist b/Targets/Lava/WaiterRobotLava.plist index cb19726..2a61768 100644 --- a/Targets/Lava/WaiterRobotLava.plist +++ b/Targets/Lava/WaiterRobotLava.plist @@ -26,7 +26,7 @@ CFBundleShortVersionString 2.5.0 CFBundleVersion - 29158000 + 29178434 ITSAppUsesNonExemptEncryption NSAppTransportSecurity diff --git a/Targets/Prod/Images.xcassets/LogoRounded.imageset/Contents.json b/Targets/Prod/Images.xcassets/LogoRounded.imageset/Contents.json index 988ec93..e4164e5 100644 --- a/Targets/Prod/Images.xcassets/LogoRounded.imageset/Contents.json +++ b/Targets/Prod/Images.xcassets/LogoRounded.imageset/Contents.json @@ -1,14 +1,12 @@ { - "images": - [ - { - "filename": "wr-round.svg", - "idiom": "universal" - } - ], - "info": + "images" : [ { - "author": "xcode", - "version": 1 + "filename" : "wr-round.svg", + "idiom" : "universal" } -} \ No newline at end of file + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WaiterRobot/Features/TableList/TableListScreen.swift b/WaiterRobot/Features/TableList/TableListScreen.swift index b4d2c43..75013d5 100644 --- a/WaiterRobot/Features/TableList/TableListScreen.swift +++ b/WaiterRobot/Features/TableList/TableListScreen.swift @@ -35,14 +35,15 @@ struct TableListScreen: View { } else { content() .toolbar { - ToolbarItem(placement: .topBarTrailing) { + ToolbarItem(placement: .topBarLeading) { Button { showFilters.toggle() } label: { Image(systemName: "line.3.horizontal.decrease") } } - ToolbarItem(placement: .navigationBarTrailing) { + + ToolbarItem(placement: .topBarTrailing) { Button { viewModel.actual.openSettings() } label: { From 8a20317d4108f7480b56799d72cdfdfca36e302e Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Tue, 24 Jun 2025 22:08:45 +0200 Subject: [PATCH 04/11] Reafactor architecture: Cleanup --- .../Sources/SharedUI/TabBarHeader.swift | 16 +++- Modules/WRCore/Sources/WRCore/Mock.swift | 73 ++++++++++++++----- Targets/Lava/WaiterRobotLava.plist | 2 +- WaiterRobot/Features/Order/OrderScreen.swift | 7 +- .../Order/Search/AllProductGroupList.swift | 38 ++++++++++ .../Order/Search/ProductGroupList.swift | 40 ++++++++++ .../Order/{ => Search}/ProductListItem.swift | 47 +++--------- .../Features/Order/Search/ProductSearch.swift | 66 ++--------------- .../Order/Search/ProductSearchAllTab.swift | 60 --------------- .../Order/Search/ProductSearchGroupList.swift | 38 ---------- .../Order/Search/ProductTabView.swift | 62 ++++++++++++++++ .../TableList/TableGroupFilterSheet.swift | 5 +- .../TableList/TableGroupSection.swift | 13 +--- .../Features/TableList/TableView.swift | 29 +++++--- WaiterRobot/Util/Extensions/Color.swift | 5 +- 15 files changed, 256 insertions(+), 245 deletions(-) rename WaiterRobot/Features/Order/Search/ProducSearchTabBarHeader.swift => Modules/SharedUI/Sources/SharedUI/TabBarHeader.swift (80%) create mode 100644 WaiterRobot/Features/Order/Search/AllProductGroupList.swift create mode 100644 WaiterRobot/Features/Order/Search/ProductGroupList.swift rename WaiterRobot/Features/Order/{ => Search}/ProductListItem.swift (63%) delete mode 100644 WaiterRobot/Features/Order/Search/ProductSearchAllTab.swift delete mode 100644 WaiterRobot/Features/Order/Search/ProductSearchGroupList.swift create mode 100644 WaiterRobot/Features/Order/Search/ProductTabView.swift diff --git a/WaiterRobot/Features/Order/Search/ProducSearchTabBarHeader.swift b/Modules/SharedUI/Sources/SharedUI/TabBarHeader.swift similarity index 80% rename from WaiterRobot/Features/Order/Search/ProducSearchTabBarHeader.swift rename to Modules/SharedUI/Sources/SharedUI/TabBarHeader.swift index cc76630..ced4f6d 100644 --- a/WaiterRobot/Features/Order/Search/ProducSearchTabBarHeader.swift +++ b/Modules/SharedUI/Sources/SharedUI/TabBarHeader.swift @@ -1,11 +1,16 @@ import SwiftUI -struct ProducSearchTabBarHeader: View { +public struct TabBarHeader: View { @Namespace var namespace @Binding var currentTab: Int var tabBarOptions: [String] - var body: some View { + public init(currentTab: Binding, tabBarOptions: [String]) { + _currentTab = currentTab + self.tabBarOptions = tabBarOptions + } + + public var body: some View { VStack(spacing: 0) { ScrollView(.horizontal) { HStack { @@ -48,8 +53,11 @@ struct ProducSearchTabBarHeader: View { } } +@available(iOS 17.0, *) #Preview { - ProducSearchTabBarHeader( - currentTab: .constant(4), tabBarOptions: ["All", "Food", "Drinks", "more", "One more"] + @Previewable @State var currentTab = 3 + TabBarHeader( + currentTab: $currentTab, + tabBarOptions: ["All", "Food", "Drinks", "more", "One more"] ) } diff --git a/Modules/WRCore/Sources/WRCore/Mock.swift b/Modules/WRCore/Sources/WRCore/Mock.swift index 55a6fd4..af7fc79 100644 --- a/Modules/WRCore/Sources/WRCore/Mock.swift +++ b/Modules/WRCore/Sources/WRCore/Mock.swift @@ -2,24 +2,26 @@ import Foundation import shared public enum Mock { - public static func groupedTables() -> [GroupedTables] { - [ - GroupedTables( - id: 1, - name: "Hof", + public static func groupedTables(groups: Int = 1) -> [GroupedTables] { + let colors = ["ffaaee", "ffeeaa", "eeaaff", nil] + return (1 ... groups).map { groupId in + let tableCount = groupId % 3 == 0 ? 4 : 3 + let groupName = "Table Group \(groupId)" + + return GroupedTables( + id: Int64(groupId), + name: groupName, eventId: 1, - color: nil, - tables: [ - table(with: 1), - table(with: 2), - table(with: 3), - ] - ), - ] + color: colors[groupId % colors.count], + tables: (1 ... tableCount).map { + table(with: groupId * 10 + $0, hasOrders: $0 % 2 == 0, groupName: groupName) + } + ) + } } - public static func tableGroups() -> [TableGroup] { - groupedTables().map { + public static func tableGroups(groups: Int = 1) -> [TableGroup] { + groupedTables(groups: groups).map { TableGroup( id: $0.id, name: $0.name, @@ -29,12 +31,47 @@ public enum Mock { } } - public static func table(with id: Int64, hasOrders: Bool = false) -> shared.Table { + public static func table(with id: Int, hasOrders: Bool = false, groupName: String = "Hof") -> shared.Table { shared.Table( - id: id, + id: Int64(id), number: Int32(id), - groupName: "Hof", + groupName: groupName, hasOrders: hasOrders ) } + + public static func product(with id: Int, soldOut: Bool = false, color: String? = nil, allergens: Set = []) -> Product { + Product( + id: Int64(id), + name: "Product \(id)", + price: Money(cents: Int32(id * 10)), + soldOut: soldOut, + color: color, + allergens: allergens.enumerated().map { index, shortName in + Allergen(id: Int64(index), name: shortName.description, shortName: shortName.description) + }.filter { $0.shortName.isEmpty == false }, + position: Int32(id), + ) + } + + public static func productGroups(groups: Int = 1) -> [GroupedProducts] { + let colors = ["ffaaee", "ffeeaa", "eeaaff", nil].shuffled() + let allergenList = "ABCDEFG " + return (1 ... groups).map { groupId in + let productCount = groupId % 3 == 0 ? 4 : 3 + let groupName = "Product Group \(groupId)" + return GroupedProducts( + id: Int64(groupId), + name: groupName, + position: Int32(groupId), + color: colors[groupId % colors.count], + products: (1 ... productCount).map { + let allergens = (0 ... ($0 % 3)).map { _ in + allergenList.randomElement()! + } + return product(with: groupId * 10 + $0, soldOut: $0 % 5 == 2, allergens: Set(allergens)) + }, + ) + } + } } diff --git a/Targets/Lava/WaiterRobotLava.plist b/Targets/Lava/WaiterRobotLava.plist index cb19726..812752f 100644 --- a/Targets/Lava/WaiterRobotLava.plist +++ b/Targets/Lava/WaiterRobotLava.plist @@ -26,7 +26,7 @@ CFBundleShortVersionString 2.5.0 CFBundleVersion - 29158000 + 29174937 ITSAppUsesNonExemptEncryption NSAppTransportSecurity diff --git a/WaiterRobot/Features/Order/OrderScreen.swift b/WaiterRobot/Features/Order/OrderScreen.swift index e4189c8..2066d63 100644 --- a/WaiterRobot/Features/Order/OrderScreen.swift +++ b/WaiterRobot/Features/Order/OrderScreen.swift @@ -6,7 +6,6 @@ import WRCore struct OrderScreen: View { @EnvironmentObject var navigator: UIPilot - @State private var productName: String = "" @State private var showProductSearch: Bool @State private var showAbortOrderConfirmationDialog = false @@ -112,3 +111,9 @@ struct OrderScreen: View { } } } + +#Preview { + PreviewView { + OrderScreen(table: Mock.table(with: 1), initialItemId: 1) + } +} diff --git a/WaiterRobot/Features/Order/Search/AllProductGroupList.swift b/WaiterRobot/Features/Order/Search/AllProductGroupList.swift new file mode 100644 index 0000000..e54823e --- /dev/null +++ b/WaiterRobot/Features/Order/Search/AllProductGroupList.swift @@ -0,0 +1,38 @@ +import shared +import SwiftUI +import WRCore + +struct AllProductGroupList: View { + let productGroups: [GroupedProducts] + let onProductClick: (Product) -> Void + + var body: some View { + ScrollView { + ForEach(productGroups, id: \.id) { productGroup in + if !productGroup.products.isEmpty { + Section { + ProductGroupList( + products: productGroup.products, + backgroundColor: Color(hex: productGroup.color), + onProductClick: onProductClick + ) + } header: { + HStack { + Color(UIColor.lightGray).frame(height: 1) + Text(productGroup.name) + Color(UIColor.lightGray).frame(height: 1) + } + } + } + } + } + } +} + +#Preview { + AllProductGroupList( + productGroups: Mock.productGroups(groups: 3), + onProductClick: { _ in } + ) + .padding() +} diff --git a/WaiterRobot/Features/Order/Search/ProductGroupList.swift b/WaiterRobot/Features/Order/Search/ProductGroupList.swift new file mode 100644 index 0000000..bb1eb58 --- /dev/null +++ b/WaiterRobot/Features/Order/Search/ProductGroupList.swift @@ -0,0 +1,40 @@ +import shared +import SwiftUI +import WRCore + +struct ProductGroupList: View { + let products: [Product] + let backgroundColor: Color? + let onProductClick: (Product) -> Void + + private let layout = [ + GridItem(.adaptive(minimum: 110)), + ] + + var body: some View { + ScrollView { + LazyVGrid(columns: layout, spacing: 0) { + ForEach(products, id: \.id) { product in + ProductListItem(product: product, backgroundColor: backgroundColor) { + onProductClick(product) + } + .foregroundColor(.blackWhite) + .padding(10) + } + } + } + } +} + +#Preview { + ProductGroupList( + products: [ + Mock.product(with: 1), + Mock.product(with: 2, soldOut: true, allergens: ["A"]), + Mock.product(with: 3, color: "ffaa00", allergens: ["A"]), + Mock.product(with: 4, soldOut: true, color: "ffaa00"), + ], + backgroundColor: .yellow, + onProductClick: { _ in } + ) +} diff --git a/WaiterRobot/Features/Order/ProductListItem.swift b/WaiterRobot/Features/Order/Search/ProductListItem.swift similarity index 63% rename from WaiterRobot/Features/Order/ProductListItem.swift rename to WaiterRobot/Features/Order/Search/ProductListItem.swift index 600dd4c..d0aaf81 100644 --- a/WaiterRobot/Features/Order/ProductListItem.swift +++ b/WaiterRobot/Features/Order/Search/ProductListItem.swift @@ -1,5 +1,6 @@ import shared import SwiftUI +import WRCore struct ProductListItem: View { let product: Product @@ -14,19 +15,23 @@ struct ProductListItem: View { onClick: @escaping () -> Void ) { self.product = product - self.backgroundColor = backgroundColor - self.onClick = onClick + if let color = product.color { + self.backgroundColor = Color(hex: color) + } else { + self.backgroundColor = backgroundColor + } var allergens = "" for allergen in self.product.allergens { allergens += "\(allergen.shortName), " } - if allergens.count > 2 { self.allergens = String(allergens.prefix(allergens.count - 2)) } else { self.allergens = "" } + + self.onClick = onClick } var foregroundColor: Color { @@ -69,22 +74,8 @@ struct ProductListItem: View { #Preview { ProductListItem( - product: Product( - id: 2, - name: "Wine", - price: Money(cents: 290), - soldOut: true, - color: nil, - allergens: [ - Allergen(id: 1, name: "Egg", shortName: "E"), - Allergen(id: 2, name: "Egg2", shortName: "A"), - Allergen(id: 3, name: "Egg3", shortName: "B"), - Allergen(id: 4, name: "Egg4", shortName: "C"), - Allergen(id: 5, name: "Egg5", shortName: "D"), - ], - position: 1 - ), - backgroundColor: .yellow, + product: Mock.product(with: 1, soldOut: false, color: "ffaaee", allergens: ["A"]), + backgroundColor: .red, onClick: {} ) .frame(maxWidth: 100, maxHeight: 100) @@ -92,22 +83,8 @@ struct ProductListItem: View { #Preview { ProductListItem( - product: Product( - id: 2, - name: "Wine", - price: Money(cents: 290), - soldOut: false, - color: nil, - allergens: [ - Allergen(id: 1, name: "Egg", shortName: "E"), - Allergen(id: 2, name: "Egg2", shortName: "A"), - Allergen(id: 3, name: "Egg3", shortName: "B"), - Allergen(id: 4, name: "Egg4", shortName: "C"), - Allergen(id: 5, name: "Egg5", shortName: "D"), - ], - position: 1 - ), - backgroundColor: .yellow, + product: Mock.product(with: 1, soldOut: true, color: "ffaaee", allergens: ["A", "B"]), + backgroundColor: .red, onClick: {} ) .frame(maxWidth: 100, maxHeight: 100) diff --git a/WaiterRobot/Features/Order/Search/ProductSearch.swift b/WaiterRobot/Features/Order/Search/ProductSearch.swift index 2ad9869..b213e7a 100644 --- a/WaiterRobot/Features/Order/Search/ProductSearch.swift +++ b/WaiterRobot/Features/Order/Search/ProductSearch.swift @@ -12,10 +12,6 @@ struct ProductSearch: View { @State private var search: String = "" @State private var selectedTab: Int = 0 - private let layout = [ - GridItem(.adaptive(minimum: 110)), - ] - var body: some View { NavigationView { content() @@ -39,65 +35,19 @@ struct ProductSearch: View { productGroupsError(error: resource) case let .success(resource): if let productGroups = Array(resource.data) { - productsGroupsList(productGroups: productGroups) - } - } - } - - @ViewBuilder - private func productsGroupsList(productGroups: [GroupedProducts]) -> some View { - if productGroups.isEmpty { - Text(localize.productSearch_noProductFound()) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - .padding() - } else { - VStack { - ProducSearchTabBarHeader(currentTab: $selectedTab, tabBarOptions: getGroupNames(productGroups)) - - TabView(selection: $selectedTab) { - ProductSearchAllTab( - productGroups: productGroups, - columns: layout, - onProductClick: { - addItem($0, 1) - dismiss() - } - ) - .tag(0) - .padding() - - let enumeratedProductGroups = Array(productGroups.enumerated()) - ForEach(enumeratedProductGroups, id: \.element.id) { index, groupedProducts in - ScrollView { - LazyVGrid(columns: layout, spacing: 0) { - productGroup(groupedProducts: groupedProducts) - Spacer() - } - .padding() - } - .tag(index + 1) + ProductTabView( + productGroups: productGroups, + addItem: { + addItem($0, $1) + dismiss() } - } + ) + .searchable(text: $search, placement: .navigationBarDrawer(displayMode: .always)) + .onChange(of: search, perform: { viewModel.actual.filterProducts(filter: $0) }) } - .tabViewStyle(.page(indexDisplayMode: .never)) - .searchable(text: $search, placement: .navigationBarDrawer(displayMode: .always)) - .onChange(of: search, perform: { viewModel.actual.filterProducts(filter: $0) }) } } - @ViewBuilder - private func productGroup(groupedProducts: GroupedProducts) -> some View { - ProductSearchGroupList( - products: groupedProducts.products, - backgroundColor: Color(hex: groupedProducts.color), - onProductClick: { - addItem($0, 1) - dismiss() - } - ) - } - private func productGroupsError(error: ResourceError>) -> some View { Text(error.userMessage()) } diff --git a/WaiterRobot/Features/Order/Search/ProductSearchAllTab.swift b/WaiterRobot/Features/Order/Search/ProductSearchAllTab.swift deleted file mode 100644 index 66fad7b..0000000 --- a/WaiterRobot/Features/Order/Search/ProductSearchAllTab.swift +++ /dev/null @@ -1,60 +0,0 @@ -import shared -import SwiftUI - -struct ProductSearchAllTab: View { - let productGroups: [GroupedProducts] - let columns: [GridItem] - let onProductClick: (Product) -> Void - - var body: some View { - ScrollView { - LazyVGrid(columns: columns) { - ForEach(productGroups, id: \.id) { productGroup in - if !productGroup.products.isEmpty { - Section { - ProductSearchGroupList( - products: productGroup.products, - backgroundColor: Color(hex: productGroup.color), - onProductClick: onProductClick - ) - } header: { - HStack { - Color(UIColor.lightGray).frame(height: 1) - Text(productGroup.name) - Color(UIColor.lightGray).frame(height: 1) - } - } - } - } - Spacer() - } - } - } -} - -#Preview { - ProductSearchAllTab( - productGroups: [ - GroupedProducts( - id: 1, - name: "Test Group 1", - position: 1, - color: "", - products: [ - Product( - id: 1, - name: "Beer", - price: Money(cents: 450), - soldOut: false, - color: nil, - allergens: [], - position: 1 - ), - ] - ), - ], - columns: [GridItem(.adaptive(minimum: 110))], - onProductClick: { _ in } - ) - .padding() -} diff --git a/WaiterRobot/Features/Order/Search/ProductSearchGroupList.swift b/WaiterRobot/Features/Order/Search/ProductSearchGroupList.swift deleted file mode 100644 index effc113..0000000 --- a/WaiterRobot/Features/Order/Search/ProductSearchGroupList.swift +++ /dev/null @@ -1,38 +0,0 @@ -import shared -import SwiftUI - -struct ProductSearchGroupList: View { - let products: [Product] - let backgroundColor: Color? - let onProductClick: (Product) -> Void - - var body: some View { - ForEach(products, id: \.id) { product in - ProductListItem(product: product, backgroundColor: backgroundColor) { - onProductClick(product) - } - .foregroundColor(.blackWhite) - .padding(10) - } - } -} - -#Preview { - LazyVGrid(columns: [GridItem(.adaptive(minimum: 110))]) { - ProductSearchGroupList( - products: [ - Product( - id: 1, - name: "Beer", - price: Money(cents: 450), - soldOut: false, - color: nil, - allergens: [], - position: 1 - ), - ], - backgroundColor: .yellow, - onProductClick: { _ in } - ) - } -} diff --git a/WaiterRobot/Features/Order/Search/ProductTabView.swift b/WaiterRobot/Features/Order/Search/ProductTabView.swift new file mode 100644 index 0000000..e2e5204 --- /dev/null +++ b/WaiterRobot/Features/Order/Search/ProductTabView.swift @@ -0,0 +1,62 @@ +import shared +import SharedUI +import SwiftUI +import WRCore + +struct ProductTabView: View { + let productGroups: [GroupedProducts] + let addItem: (_ product: Product, _ amount: Int32) -> Void + + @State private var selectedTab: Int = 0 + + var body: some View { + if productGroups.isEmpty { + Text(localize.productSearch_noProductFound()) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding() + } else { + VStack { + TabBarHeader( + currentTab: $selectedTab, + tabBarOptions: getGroupNames(productGroups) + ) + + TabView(selection: $selectedTab) { + AllProductGroupList( + productGroups: productGroups, + onProductClick: { addItem($0, 1) } + ) + .tag(0) + .padding() + + let enumeratedProductGroups = Array(productGroups.enumerated()) + ForEach(enumeratedProductGroups, id: \.element.id) { index, groupedProducts in + ProductGroupList( + products: groupedProducts.products, + backgroundColor: Color(hex: groupedProducts.color), + onProductClick: { addItem($0, 1) } + ).padding() + .tag(index + 1) + } + } + } + .tabViewStyle(.page(indexDisplayMode: .never)) + } + } + + private func getGroupNames(_ productGroups: [GroupedProducts]) -> [String] { + var groupNames = productGroups.map { productGroup in + productGroup.name + } + groupNames.insert(localize.productSearch_groups_all(), at: 0) + return groupNames + } +} + +#Preview { + ProductTabView( + productGroups: Mock.productGroups(groups: 3), + addItem: { _, _ in } + ) +} diff --git a/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift b/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift index 924422d..6ea82a2 100644 --- a/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift +++ b/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift @@ -80,10 +80,7 @@ private struct TableGroupFilter: View { #Preview { TableGroupFilter( - groups: [ - shared.TableGroup(id: 1, name: "Group 1", color: "ffaaff", hidden: false), - shared.TableGroup(id: 2, name: "Group 2", color: "aaffaa", hidden: false), - ], + groups: Mock.tableGroups(groups: 10), showAll: {}, hideAll: {}, onToggle: { _ in } diff --git a/WaiterRobot/Features/TableList/TableGroupSection.swift b/WaiterRobot/Features/TableList/TableGroupSection.swift index 3952346..89ec6a4 100644 --- a/WaiterRobot/Features/TableList/TableGroupSection.swift +++ b/WaiterRobot/Features/TableList/TableGroupSection.swift @@ -50,18 +50,7 @@ struct TableGroupSection: View { #Preview { LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) { TableGroupSection( - groupedTables: GroupedTables( - id: 1, - name: "Test Group", - eventId: 1, - color: nil, - tables: [ - shared.Table(id: 1, number: 1, groupName: "Test Group", hasOrders: true), - shared.Table(id: 2, number: 2, groupName: "Test Group", hasOrders: false), - shared.Table(id: 3, number: 3, groupName: "Test Group", hasOrders: false), - shared.Table(id: 4, number: 4, groupName: "Test Group", hasOrders: true), - ] - ), + groupedTables: Mock.groupedTables().first!, onTableClick: { _ in } ) } diff --git a/WaiterRobot/Features/TableList/TableView.swift b/WaiterRobot/Features/TableList/TableView.swift index 43fa12a..761ee38 100644 --- a/WaiterRobot/Features/TableList/TableView.swift +++ b/WaiterRobot/Features/TableList/TableView.swift @@ -4,11 +4,15 @@ import SwiftUI struct TableView: View { let text: String let hasOrders: Bool - let backgroundColor: Color? + let backgroundColor: Color let onClick: () -> Void - @Environment(\.colorScheme) - var colorScheme + init(text: String, hasOrders: Bool, backgroundColor: Color?, onClick: @escaping () -> Void) { + self.text = text + self.hasOrders = hasOrders + self.backgroundColor = backgroundColor ?? .gray.opacity(0.3) + self.onClick = onClick + } var body: some View { Button(action: onClick) { @@ -23,7 +27,7 @@ struct TableView: View { Spacer() Circle() - .foregroundColor(backgroundColor?.getContentColor(lightColorScheme: Color(.darkRed), darkColorScheme: Color(.lightRed))) + .foregroundColor(backgroundColor.getContentColor(lightColorScheme: Color(.darkRed), darkColorScheme: Color(.lightRed))) .frame(width: 12) } @@ -36,20 +40,21 @@ struct TableView: View { } .aspectRatio(1.0, contentMode: .fit) .background { - if let backgroundColor { - RoundedRectangle(cornerRadius: 20) - .foregroundColor(backgroundColor) - } else { - RoundedRectangle(cornerRadius: 20) - .foregroundColor(.gray.opacity(0.3)) - } + RoundedRectangle(cornerRadius: 20) + .foregroundColor(backgroundColor) } - .foregroundStyle(backgroundColor?.getContentColor(lightColorScheme: .black, darkColorScheme: .white) ?? .blackWhite) + .foregroundStyle(backgroundColor.getContentColor(lightColorScheme: .white, darkColorScheme: .black)) } } #Preview { VStack { + TableView(text: "1", hasOrders: false, backgroundColor: .black) {} + .frame(maxWidth: 100) + + TableView(text: "1", hasOrders: false, backgroundColor: .gray.opacity(0.1)) {} + .frame(maxWidth: 100) + TableView(text: "1", hasOrders: false, backgroundColor: .green) {} .frame(maxWidth: 100) diff --git a/WaiterRobot/Util/Extensions/Color.swift b/WaiterRobot/Util/Extensions/Color.swift index 1da311a..c877195 100644 --- a/WaiterRobot/Util/Extensions/Color.swift +++ b/WaiterRobot/Util/Extensions/Color.swift @@ -64,9 +64,10 @@ extension Color { } // Adjust color based on contrast + // TODO: this does not respect opacity func getContentColor(lightColorScheme: Color, darkColorScheme: Color) -> Color { - let lightContrast = contrastRatio(with: lightColorScheme) - let darkContrast = contrastRatio(with: darkColorScheme) + let lightContrast = lightColorScheme.contrastRatio(with: self) + let darkContrast = darkColorScheme.contrastRatio(with: self) return lightContrast > darkContrast ? lightColorScheme : darkColorScheme } From 8d5e0b15d4529ffe8cb3858aab9b6b15bb04b79e Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Sun, 20 Jul 2025 12:16:11 +0200 Subject: [PATCH 05/11] Fix bestContrastColor --- WaiterRobot.xcodeproj/project.pbxproj | 4 +- .../Order/Search/ProductListItem.swift | 4 +- .../TableList/TableGroupSection.swift | 6 +- .../Features/TableList/TableView.swift | 32 ++++------ WaiterRobot/Util/Extensions/Color.swift | 60 ++++++++++++------- 5 files changed, 61 insertions(+), 45 deletions(-) diff --git a/WaiterRobot.xcodeproj/project.pbxproj b/WaiterRobot.xcodeproj/project.pbxproj index cd41971..c42c9bb 100644 --- a/WaiterRobot.xcodeproj/project.pbxproj +++ b/WaiterRobot.xcodeproj/project.pbxproj @@ -432,7 +432,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -582,7 +582,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; diff --git a/WaiterRobot/Features/Order/Search/ProductListItem.swift b/WaiterRobot/Features/Order/Search/ProductListItem.swift index d0aaf81..50f2347 100644 --- a/WaiterRobot/Features/Order/Search/ProductListItem.swift +++ b/WaiterRobot/Features/Order/Search/ProductListItem.swift @@ -3,6 +3,8 @@ import SwiftUI import WRCore struct ProductListItem: View { + @Environment(\.self) var env + let product: Product let backgroundColor: Color? let onClick: () -> Void @@ -38,7 +40,7 @@ struct ProductListItem: View { if product.soldOut { .blackWhite } else if let backgroundColor { - backgroundColor.getContentColor(lightColorScheme: .black, darkColorScheme: .white) + backgroundColor.bestContrastColor(.black, .white, in: env) } else { .blackWhite } diff --git a/WaiterRobot/Features/TableList/TableGroupSection.swift b/WaiterRobot/Features/TableList/TableGroupSection.swift index 89ec6a4..ba35708 100644 --- a/WaiterRobot/Features/TableList/TableGroupSection.swift +++ b/WaiterRobot/Features/TableList/TableGroupSection.swift @@ -4,6 +4,8 @@ import SwiftUI import WRCore struct TableGroupSection: View { + @Environment(\.self) var env + let groupedTables: GroupedTables let onTableClick: (shared.Table) -> Void @@ -25,7 +27,7 @@ struct TableGroupSection: View { if let background = Color(hex: groupedTables.color) { title(backgroundColor: background) } else { - title(backgroundColor: .gray.opacity(0.3)) + title(backgroundColor: .lightGray) } Spacer() @@ -38,7 +40,7 @@ struct TableGroupSection: View { private func title(backgroundColor: Color) -> some View { Text(groupedTables.name) .font(.title2) - .foregroundStyle(backgroundColor.getContentColor(lightColorScheme: .black, darkColorScheme: .white)) + .foregroundStyle(backgroundColor.bestContrastColor(.black, .white, in: env)) .padding(6) .background { RoundedRectangle(cornerRadius: 8.0) diff --git a/WaiterRobot/Features/TableList/TableView.swift b/WaiterRobot/Features/TableList/TableView.swift index 761ee38..0d0c50a 100644 --- a/WaiterRobot/Features/TableList/TableView.swift +++ b/WaiterRobot/Features/TableList/TableView.swift @@ -2,6 +2,8 @@ import SharedUI import SwiftUI struct TableView: View { + @Environment(\.self) var env + let text: String let hasOrders: Bool let backgroundColor: Color @@ -10,31 +12,23 @@ struct TableView: View { init(text: String, hasOrders: Bool, backgroundColor: Color?, onClick: @escaping () -> Void) { self.text = text self.hasOrders = hasOrders - self.backgroundColor = backgroundColor ?? .gray.opacity(0.3) + self.backgroundColor = backgroundColor ?? .lightGray self.onClick = onClick } var body: some View { Button(action: onClick) { - ZStack { + ZStack(alignment: .topTrailing) { Text(text) .font(.title) .frame(maxWidth: .infinity, maxHeight: .infinity) if hasOrders { - VStack(alignment: .trailing) { - HStack { - Spacer() - - Circle() - .foregroundColor(backgroundColor.getContentColor(lightColorScheme: Color(.darkRed), darkColorScheme: Color(.lightRed))) - .frame(width: 12) - } - - Spacer() - } - .padding(.top, 10) - .padding(.trailing, 10) + Circle() + .foregroundColor(backgroundColor.bestContrastColor(Color(.darkRed), Color(.lightRed), in: env)) + .frame(width: 12, height: 12) + .padding(.top, 10) + .padding(.trailing, 10) } } } @@ -43,19 +37,19 @@ struct TableView: View { RoundedRectangle(cornerRadius: 20) .foregroundColor(backgroundColor) } - .foregroundStyle(backgroundColor.getContentColor(lightColorScheme: .white, darkColorScheme: .black)) + .foregroundStyle(backgroundColor.bestContrastColor(.white, .black, in: env)) } } #Preview { VStack { - TableView(text: "1", hasOrders: false, backgroundColor: .black) {} + TableView(text: "1", hasOrders: true, backgroundColor: .blackWhite) {} .frame(maxWidth: 100) - TableView(text: "1", hasOrders: false, backgroundColor: .gray.opacity(0.1)) {} + TableView(text: "1", hasOrders: false, backgroundColor: .gray) {} .frame(maxWidth: 100) - TableView(text: "1", hasOrders: false, backgroundColor: .green) {} + TableView(text: "1", hasOrders: true, backgroundColor: .green) {} .frame(maxWidth: 100) TableView(text: "2", hasOrders: true, backgroundColor: nil) {} diff --git a/WaiterRobot/Util/Extensions/Color.swift b/WaiterRobot/Util/Extensions/Color.swift index c877195..8a258a7 100644 --- a/WaiterRobot/Util/Extensions/Color.swift +++ b/WaiterRobot/Util/Extensions/Color.swift @@ -8,6 +8,10 @@ import SwiftUI extension Color { + static var lightGray: Color { + Color(hex: "#D1D1D6") + } + init(hex: String) { let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var int: UInt64 = 0 @@ -63,33 +67,47 @@ extension Color { ) } - // Adjust color based on contrast - // TODO: this does not respect opacity - func getContentColor(lightColorScheme: Color, darkColorScheme: Color) -> Color { - let lightContrast = lightColorScheme.contrastRatio(with: self) - let darkContrast = darkColorScheme.contrastRatio(with: self) + /// Adjust color based on contrast + func bestContrastColor(_ color1: Color, _ color2: Color, in env: EnvironmentValues) -> Color { + let backgroundResolved = resolve(in: env) + let color1Resolved = color1.resolve(in: env) + let color2Resolved = color2.resolve(in: env) - return lightContrast > darkContrast ? lightColorScheme : darkColorScheme - } + let contrast1 = Color.Resolved.contrastRatio(foreground: color1Resolved, background: backgroundResolved) + let contrast2 = Color.Resolved.contrastRatio(foreground: color2Resolved, background: backgroundResolved) - // Calculate contrast ratio - private func contrastRatio(with other: Color) -> Double { - let l1 = luminance() - let l2 = other.luminance() - return (max(l1, l2) + 0.05) / (min(l1, l2) + 0.05) + return contrast1 > contrast2 ? color1 : color2 } +} - // Calculate luminance - private func luminance() -> Double { - let components = cgColor?.components ?? [0, 0, 0, 1] - let red = Color.convertSRGBToLinear(components[0]) - let green = Color.convertSRGBToLinear(components[1]) - let blue = Color.convertSRGBToLinear(components[2]) +extension Color.Resolved { + static func contrastRatio(foreground: Color.Resolved, background: Color.Resolved) -> Float { + #if DEBUG + if background.opacity != 1 { + fatalError("Background can not be translucent") + } + #endif + let lum1 = foreground.composite(on: background).luminance() // calculate the luminance when composed on top of background to account for alpha + let lum2 = background.luminance() + let lighter = max(lum1, lum2) + let darker = min(lum1, lum2) + return (lighter + 0.05) / (darker + 0.05) + } - return 0.2126 * red + 0.7152 * green + 0.0722 * blue + func luminance() -> Float { + 0.2126 * linearRed + 0.7152 * linearGreen + 0.0722 * linearBlue } - private static func convertSRGBToLinear(_ component: CGFloat) -> Double { - component <= 0.03928 ? Double(component) / 12.92 : pow((Double(component) + 0.055) / 1.055, 2.4) + private func composite(on background: Color.Resolved) -> Color.Resolved { + if opacity == 1 { return self } + if opacity == 0 { return self } + + let alpha = opacity + background.opacity * (1 - opacity) + + let r = (red * opacity + background.red * background.opacity * (1 - opacity)) / alpha + let g = (green * opacity + background.green * background.opacity * (1 - opacity)) / alpha + let b = (blue * opacity + background.blue * background.opacity * (1 - opacity)) / alpha + + return Color.Resolved(red: r, green: g, blue: b, opacity: alpha) } } From 93e73abea77f01a9bf92736c91a6ac0b6c76ed35 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Sun, 20 Jul 2025 12:17:34 +0200 Subject: [PATCH 06/11] Fix toolbar background --- WaiterRobot/Features/TableList/TableListScreen.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/WaiterRobot/Features/TableList/TableListScreen.swift b/WaiterRobot/Features/TableList/TableListScreen.swift index 75013d5..abcee87 100644 --- a/WaiterRobot/Features/TableList/TableListScreen.swift +++ b/WaiterRobot/Features/TableList/TableListScreen.swift @@ -31,7 +31,6 @@ struct TableListScreen: View { } } } - .toolbarBackground(.hidden, for: .navigationBar) } else { content() .toolbar { From 1218532ce5a53e91d59f6e9c33d259133a1626d8 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Sun, 20 Jul 2025 12:52:48 +0200 Subject: [PATCH 07/11] Indicate Demo events --- Modules/WRCore/Sources/WRCore/ErrorBar.swift | 2 +- WaiterRobot/Features/SwitchEvent/Event.swift | 21 +++++++++++++++++-- .../Features/TableList/TableListScreen.swift | 7 +++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Modules/WRCore/Sources/WRCore/ErrorBar.swift b/Modules/WRCore/Sources/WRCore/ErrorBar.swift index e4d7cbb..21f3d17 100644 --- a/Modules/WRCore/Sources/WRCore/ErrorBar.swift +++ b/Modules/WRCore/Sources/WRCore/ErrorBar.swift @@ -40,7 +40,7 @@ public struct ErrorBar: View { .padding(.top, 8) .padding(.trailing, retryAction == nil ? 16 : 8) .padding(.bottom, 8) - // .background(Color.errorContainer) + .background(Color.red) .onTapGesture { withAnimation { expanded.toggle() diff --git a/WaiterRobot/Features/SwitchEvent/Event.swift b/WaiterRobot/Features/SwitchEvent/Event.swift index 6289dd6..c5e0597 100644 --- a/WaiterRobot/Features/SwitchEvent/Event.swift +++ b/WaiterRobot/Features/SwitchEvent/Event.swift @@ -1,12 +1,29 @@ import shared import SwiftUI +import WRCore struct Event: View { let event: shared.Event var body: some View { VStack(alignment: .leading) { - Text(event.name) + HStack { + Text(event.name) + + if event.isDemo { + Spacer() + + Text(localize.switchEvent_demoEvent()) + .font(.caption) + .foregroundColor(.white) + .padding(.horizontal, 10) + .padding(.vertical, 2) + .background { + Capsule() + .fill(.darkRed) + } + } + } HStack { Text(event.city) @@ -35,7 +52,7 @@ struct Event: View { city: "Graz", organisationId: 1, stripeSettings: shared.Event.StripeSettingsDisabled(), - isDemo: false + isDemo: true ) ) } diff --git a/WaiterRobot/Features/TableList/TableListScreen.swift b/WaiterRobot/Features/TableList/TableListScreen.swift index abcee87..5b55e45 100644 --- a/WaiterRobot/Features/TableList/TableListScreen.swift +++ b/WaiterRobot/Features/TableList/TableListScreen.swift @@ -82,6 +82,7 @@ struct TableListScreen: View { if let tableGroups = Array(viewModel.state.tableGroups.data) { TableListView( tableGroups: tableGroups, + isDemoEvent: viewModel.state.isDemoEvent, onTableSelect: { viewModel.actual.onTableClick(table: $0) } ) } else { @@ -118,6 +119,7 @@ struct TableListScreen: View { struct TableListView: View { let tableGroups: [GroupedTables] + let isDemoEvent: Bool let onTableSelect: (shared.Table) -> Void private let layout = [ @@ -153,6 +155,10 @@ struct TableListView: View { .padding() } } + + if isDemoEvent { + ErrorBar(message: localize.tableList_demoEventWarning.desc(), initialLines: 1) + } } } } @@ -170,6 +176,7 @@ struct TableListView: View { NavigationView { TableListView( tableGroups: Mock.groupedTables(), + isDemoEvent: true, onTableSelect: { _ in } ) } From 4b1613eaf3805b814695ba9b2fca7943c664152f Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Sun, 20 Jul 2025 13:26:48 +0200 Subject: [PATCH 08/11] Possible fix --- Modules/WRCore/Sources/WRCore/Mock.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/WRCore/Sources/WRCore/Mock.swift b/Modules/WRCore/Sources/WRCore/Mock.swift index af7fc79..45407f6 100644 --- a/Modules/WRCore/Sources/WRCore/Mock.swift +++ b/Modules/WRCore/Sources/WRCore/Mock.swift @@ -26,7 +26,7 @@ public enum Mock { id: $0.id, name: $0.name, color: $0.color, - hidden: false, + hidden: false ) } } @@ -50,7 +50,7 @@ public enum Mock { allergens: allergens.enumerated().map { index, shortName in Allergen(id: Int64(index), name: shortName.description, shortName: shortName.description) }.filter { $0.shortName.isEmpty == false }, - position: Int32(id), + position: Int32(id) ) } @@ -70,7 +70,7 @@ public enum Mock { allergenList.randomElement()! } return product(with: groupId * 10 + $0, soldOut: $0 % 5 == 2, allergens: Set(allergens)) - }, + } ) } } From 4c55dabf86f2cc5bf11d2ae01f807f0527bc02d4 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Mon, 11 Aug 2025 18:04:57 +0200 Subject: [PATCH 09/11] Apply suggestions from code review Co-authored-by: Alexander Kauer --- WaiterRobot/Features/TableList/TableGroupSection.swift | 3 ++- WaiterRobot/Features/TableList/TableView.swift | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/WaiterRobot/Features/TableList/TableGroupSection.swift b/WaiterRobot/Features/TableList/TableGroupSection.swift index ba35708..240f366 100644 --- a/WaiterRobot/Features/TableList/TableGroupSection.swift +++ b/WaiterRobot/Features/TableList/TableGroupSection.swift @@ -4,7 +4,8 @@ import SwiftUI import WRCore struct TableGroupSection: View { - @Environment(\.self) var env + @Environment(\.self) + private var env let groupedTables: GroupedTables let onTableClick: (shared.Table) -> Void diff --git a/WaiterRobot/Features/TableList/TableView.swift b/WaiterRobot/Features/TableList/TableView.swift index 0d0c50a..ce90771 100644 --- a/WaiterRobot/Features/TableList/TableView.swift +++ b/WaiterRobot/Features/TableList/TableView.swift @@ -2,7 +2,8 @@ import SharedUI import SwiftUI struct TableView: View { - @Environment(\.self) var env + @Environment(\.self) + private var env let text: String let hasOrders: Bool From 84dc18c684e92c0a5237bea7388a195005664e16 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Mon, 11 Aug 2025 18:19:57 +0200 Subject: [PATCH 10/11] Requested PR changes --- .../SharedUI/Sources/SharedUI/Colors.swift | 4 ++++ .../lightGray.colorset/Contents.json | 20 +++++++++++++++++++ Modules/WRCore/Sources/WRCore/ErrorBar.swift | 6 +----- .../Order/Search/AllProductGroupList.swift | 5 +++-- .../TableList/TableGroupFilterSheet.swift | 2 +- .../TableList/TableGroupSection.swift | 4 ++-- .../Features/TableList/TableView.swift | 4 ++-- WaiterRobot/Ui/ViewStateOverlayView.swift | 4 ++-- WaiterRobot/Util/Extensions/Color.swift | 4 ---- 9 files changed, 35 insertions(+), 18 deletions(-) create mode 100644 Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/lightGray.colorset/Contents.json diff --git a/Modules/SharedUI/Sources/SharedUI/Colors.swift b/Modules/SharedUI/Sources/SharedUI/Colors.swift index 81e9e9a..c353982 100644 --- a/Modules/SharedUI/Sources/SharedUI/Colors.swift +++ b/Modules/SharedUI/Sources/SharedUI/Colors.swift @@ -30,4 +30,8 @@ public extension Color { static var palletOrange: Color { Color(.palletOrange) } + + static var lightGray: Color { + Color(.lightGray) + } } diff --git a/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/lightGray.colorset/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/lightGray.colorset/Contents.json new file mode 100644 index 0000000..055eeee --- /dev/null +++ b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/lightGray.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xD6", + "green" : "0xD1", + "red" : "0xD1" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/WRCore/Sources/WRCore/ErrorBar.swift b/Modules/WRCore/Sources/WRCore/ErrorBar.swift index 21f3d17..a70dae5 100644 --- a/Modules/WRCore/Sources/WRCore/ErrorBar.swift +++ b/Modules/WRCore/Sources/WRCore/ErrorBar.swift @@ -20,7 +20,6 @@ public struct ErrorBar: View { Text(message()) .lineLimit(expanded ? nil : initialLines) .multilineTextAlignment(.leading) - // .foregroundColor(Color.onErrorContainer) .frame(maxWidth: .infinity, alignment: .leading) if retryAction != nil { @@ -33,7 +32,6 @@ public struct ErrorBar: View { .multilineTextAlignment(.center) .lineLimit(expanded ? nil : initialLines) } - // .foregroundColor(Color.onErrorContainer) } } .padding(.leading, 16) @@ -42,9 +40,7 @@ public struct ErrorBar: View { .padding(.bottom, 8) .background(Color.red) .onTapGesture { - withAnimation { - expanded.toggle() - } + expanded.toggle() } .animation(.default, value: expanded) } diff --git a/WaiterRobot/Features/Order/Search/AllProductGroupList.swift b/WaiterRobot/Features/Order/Search/AllProductGroupList.swift index e54823e..32ae392 100644 --- a/WaiterRobot/Features/Order/Search/AllProductGroupList.swift +++ b/WaiterRobot/Features/Order/Search/AllProductGroupList.swift @@ -1,4 +1,5 @@ import shared +import SharedUI import SwiftUI import WRCore @@ -18,9 +19,9 @@ struct AllProductGroupList: View { ) } header: { HStack { - Color(UIColor.lightGray).frame(height: 1) + Color.lightGray.frame(height: 1) Text(productGroup.name) - Color(UIColor.lightGray).frame(height: 1) + Color.lightGray.frame(height: 1) } } } diff --git a/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift b/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift index 6ea82a2..77e1b35 100644 --- a/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift +++ b/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift @@ -13,7 +13,7 @@ struct TableGroupFilterSheet: View { content() .observeState(of: viewModel) .toolbar { - ToolbarItem(placement: .topBarLeading) { + ToolbarItem(placement: .cancellationAction) { Button(localize.dialog_cancel()) { dismiss() } diff --git a/WaiterRobot/Features/TableList/TableGroupSection.swift b/WaiterRobot/Features/TableList/TableGroupSection.swift index 240f366..6bf3357 100644 --- a/WaiterRobot/Features/TableList/TableGroupSection.swift +++ b/WaiterRobot/Features/TableList/TableGroupSection.swift @@ -4,7 +4,7 @@ import SwiftUI import WRCore struct TableGroupSection: View { - @Environment(\.self) + @Environment(\.self) private var env let groupedTables: GroupedTables @@ -28,7 +28,7 @@ struct TableGroupSection: View { if let background = Color(hex: groupedTables.color) { title(backgroundColor: background) } else { - title(backgroundColor: .lightGray) + title(backgroundColor: Color.lightGray) } Spacer() diff --git a/WaiterRobot/Features/TableList/TableView.swift b/WaiterRobot/Features/TableList/TableView.swift index ce90771..2622c95 100644 --- a/WaiterRobot/Features/TableList/TableView.swift +++ b/WaiterRobot/Features/TableList/TableView.swift @@ -2,7 +2,7 @@ import SharedUI import SwiftUI struct TableView: View { - @Environment(\.self) + @Environment(\.self) private var env let text: String @@ -13,7 +13,7 @@ struct TableView: View { init(text: String, hasOrders: Bool, backgroundColor: Color?, onClick: @escaping () -> Void) { self.text = text self.hasOrders = hasOrders - self.backgroundColor = backgroundColor ?? .lightGray + self.backgroundColor = backgroundColor ?? Color.lightGray self.onClick = onClick } diff --git a/WaiterRobot/Ui/ViewStateOverlayView.swift b/WaiterRobot/Ui/ViewStateOverlayView.swift index 4772e7d..7d1520d 100644 --- a/WaiterRobot/Ui/ViewStateOverlayView.swift +++ b/WaiterRobot/Ui/ViewStateOverlayView.swift @@ -2,8 +2,8 @@ import shared import SwiftUI public struct ViewStateOverlayView: View { - let state: Skie.Shared.ViewState.__Sealed - let content: () -> Content + private let state: Skie.Shared.ViewState.__Sealed + private let content: () -> Content init(state: ViewState, @ViewBuilder content: @escaping () -> Content) { self.state = onEnum(of: state) diff --git a/WaiterRobot/Util/Extensions/Color.swift b/WaiterRobot/Util/Extensions/Color.swift index 8a258a7..72be7b7 100644 --- a/WaiterRobot/Util/Extensions/Color.swift +++ b/WaiterRobot/Util/Extensions/Color.swift @@ -8,10 +8,6 @@ import SwiftUI extension Color { - static var lightGray: Color { - Color(hex: "#D1D1D6") - } - init(hex: String) { let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var int: UInt64 = 0 From 85bf66d72062aeac38bedde9bcd52d391ab1b2fb Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Mon, 11 Aug 2025 19:15:25 +0200 Subject: [PATCH 11/11] Increase fastlane xcode version --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 48f3c40..3b6fee6 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -2,7 +2,7 @@ default_platform(:ios) platform :ios do before_all do - xcodes(version: "16.1", select_for_current_build_only: true) + xcodes(version: "16.4", select_for_current_build_only: true) end desc "Run all iOS unit and ui tests."