diff --git a/Nextcloud Cookbook iOS Client/Localizable.xcstrings b/Nextcloud Cookbook iOS Client/Localizable.xcstrings index ae5e881..3b7adc0 100644 --- a/Nextcloud Cookbook iOS Client/Localizable.xcstrings +++ b/Nextcloud Cookbook iOS Client/Localizable.xcstrings @@ -516,6 +516,28 @@ } } }, + "Add All to Grocery List" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle zur Einkaufsliste hinzufügen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir todo a la lista de compras" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tout ajouter à la liste de courses" + } + } + } + }, "Add cooking steps for fellow chefs to follow." : { "extractionState" : "stale", "localizations" : { @@ -3665,6 +3687,28 @@ } } }, + "Remove from Grocery List" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Von der Einkaufsliste entfernen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminar de la lista de compras" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retirer de la liste de courses" + } + } + } + }, "Same as Device" : { "localizations" : { "de" : { diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeIngredientSection.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeIngredientSection.swift index 8347e42..74fe6f4 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeIngredientSection.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeIngredientSection.swift @@ -13,53 +13,32 @@ import SwiftUI struct RecipeIngredientSection: View { @EnvironmentObject var groceryList: GroceryListManager @ObservedObject var viewModel: RecipeView.ViewModel - + var body: some View { VStack(alignment: .leading) { HStack { - Button { - withAnimation { - if groceryList.containsRecipe(viewModel.observableRecipeDetail.id) { - groceryList.deleteGroceryRecipe(viewModel.observableRecipeDetail.id) - } else { - groceryList.addItems( - viewModel.observableRecipeDetail.recipeIngredient, - toRecipe: viewModel.observableRecipeDetail.id, - recipeName: viewModel.observableRecipeDetail.name - ) - } - } - } label: { - Image(systemName: "storefront") - }.disabled(viewModel.editMode) - SecondaryLabel(text: LocalizedStringKey("Ingredients")) - + Spacer() - + Image(systemName: "person.2") .foregroundStyle(.secondary) .bold() - + ServingPickerView(selectedServingSize: $viewModel.observableRecipeDetail.ingredientMultiplier) } - + ForEach(0.. Void - + var recipeName: String + @State var modifiedIngredient: AttributedString = "" - @State var isSelected: Bool = false var unmodified: Bool { servings == Double(recipeYield) || servings == 0 } - - // Drag animation + + // Swipe state @State private var dragOffset: CGFloat = 0 @State private var animationStartOffset: CGFloat = 0 - let maxDragDistance = 50.0 - + private let maxDragDistance: CGFloat = 80 + private let swipeThreshold: CGFloat = 0.4 + + private var isInGroceryList: Bool { + groceryList.containsItem(at: recipeId, item: ingredient) + } + var body: some View { - HStack(alignment: .top) { - if groceryList.containsItem(at: recipeId, item: ingredient) { - Image(systemName: "storefront") - .foregroundStyle(Color.green) - } else if isSelected { - Image(systemName: "checkmark.circle") - } else { - Image(systemName: "circle") + ZStack(alignment: .leading) { + // Swipe background + if dragOffset > 0 { + Image(systemName: isInGroceryList ? "cart.badge.minus" : "cart.badge.plus") + .font(.caption) + .bold() + .foregroundStyle(.white) + .frame(width: dragOffset, alignment: .center) + .frame(maxHeight: .infinity) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(isInGroceryList ? Color.red : Color.green) + ) } - if !unmodified && String(modifiedIngredient.characters) == ingredient { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundStyle(.red) + + // Ingredient row + HStack(alignment: .center) { + Text("•") + .foregroundStyle(.secondary) + if !unmodified && String(modifiedIngredient.characters) == ingredient { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundStyle(.red) + } + if unmodified { + Text(ingredient) + .multilineTextAlignment(.leading) + .lineLimit(5) + } else { + Text(modifiedIngredient) + .multilineTextAlignment(.leading) + .lineLimit(5) + } + if isInGroceryList { + Image(systemName: "cart") + .font(.caption2) + .foregroundStyle(.green) + } + Spacer() } - if unmodified { - Text(ingredient) - .multilineTextAlignment(.leading) - .lineLimit(5) - } else { - Text(modifiedIngredient) - .multilineTextAlignment(.leading) - .lineLimit(5) - //.foregroundStyle(String(modifiedIngredient.characters) == ingredient ? .red : .primary) - } - Spacer() + .background(Color(.systemBackground)) + .offset(x: dragOffset) } + .clipped() .onChange(of: servings) { newServings in if recipeYield == 0 { modifiedIngredient = ObservableRecipeDetail.adjustIngredient(ingredient, by: newServings) @@ -161,34 +184,29 @@ fileprivate struct IngredientListItem: View { modifiedIngredient = ObservableRecipeDetail.adjustIngredient(ingredient, by: newServings/recipeYield) } } - .foregroundStyle(isSelected ? Color.secondary : Color.primary) - .onTapGesture { - isSelected.toggle() - } - .offset(x: dragOffset, y: 0) - .animation(.easeInOut, value: isSelected) - .gesture( DragGesture() .onChanged { gesture in - // Update drag offset as the user drags if animationStartOffset == 0 { animationStartOffset = gesture.translation.width } let dragAmount = gesture.translation.width - let offset = min(dragAmount, maxDragDistance + pow(dragAmount - maxDragDistance, 0.7)) - animationStartOffset + let offset = min(dragAmount, maxDragDistance + pow(max(0, dragAmount - maxDragDistance), 0.7)) - animationStartOffset self.dragOffset = max(0, offset) } - .onEnded { gesture in + .onEnded { _ in withAnimation { - if dragOffset > maxDragDistance * 0.3 { // Swipe threshold - if groceryList.containsItem(at: recipeId, item: ingredient) { + if dragOffset > maxDragDistance * swipeThreshold { + if isInGroceryList { groceryList.deleteItem(ingredient, fromRecipe: recipeId) - } else { - addToGroceryListAction() - } + } else { + groceryList.addItem( + ingredient, + toRecipe: recipeId, + recipeName: recipeName + ) + } } - // Animate back to original position self.dragOffset = 0 self.animationStartOffset = 0 }