diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate index d9cc0c8..48570a4 100644 Binary files a/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate and b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Nextcloud Cookbook iOS Client/AppState.swift b/Nextcloud Cookbook iOS Client/AppState.swift index 7a52f46..adfa01f 100644 --- a/Nextcloud Cookbook iOS Client/AppState.swift +++ b/Nextcloud Cookbook iOS Client/AppState.swift @@ -430,7 +430,7 @@ import UIKit dataStore.delete(path: path) if recipes[categoryName] != nil { recipes[categoryName]!.removeAll(where: { recipe in - recipe.recipe_id == id ? true : false + recipe.recipe_id == id }) recipeDetails.removeValue(forKey: id) } diff --git a/Nextcloud Cookbook iOS Client/Localizable.xcstrings b/Nextcloud Cookbook iOS Client/Localizable.xcstrings index dbcc04b..db55e15 100644 --- a/Nextcloud Cookbook iOS Client/Localizable.xcstrings +++ b/Nextcloud Cookbook iOS Client/Localizable.xcstrings @@ -977,6 +977,7 @@ } }, "Delete recipe" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -997,6 +998,9 @@ } } } + }, + "Delete Recipe" : { + }, "Delete recipe?" : { "localizations" : { @@ -2693,6 +2697,7 @@ } }, "Share recipe" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -2713,6 +2718,9 @@ } } } + }, + "Share Recipe" : { + }, "Show help" : { "localizations" : { diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeListView.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeListView.swift index 7487218..b9ebd5c 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeListView.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeListView.swift @@ -36,7 +36,7 @@ struct RecipeListView: View { } } .navigationDestination(for: Recipe.self) { recipe in - RecipeView(viewModel: RecipeView.ViewModel(recipe: recipe)) + RecipeView(isPresented: .constant(true), viewModel: RecipeView.ViewModel(recipe: recipe)) } .navigationTitle(categoryName == "*" ? String(localized: "Other") : categoryName) .toolbar { diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeView.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeView.swift index 6fd44e4..67d45e7 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeView.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeView.swift @@ -10,13 +10,11 @@ import SwiftUI struct RecipeView: View { - @Environment(\.presentationMode) var presentationMode @EnvironmentObject var appState: AppState + @Binding var isPresented: Bool @StateObject var viewModel: ViewModel @State var imageHeight: CGFloat = 350 - - private enum CoordinateSpaces { case scrollView } @@ -112,98 +110,7 @@ struct RecipeView: View { //.toolbarTitleDisplayMode(.inline) .navigationTitle(viewModel.showTitle ? viewModel.recipe.name : "") .toolbar { - if viewModel.editMode { - // Cancel Button - ToolbarItem(placement: .topBarLeading) { - Button("Cancel") { - viewModel.editMode = false - } - } - - // Upload Button - ToolbarItem(placement: .topBarTrailing) { - Button { - Task { - if viewModel.newRecipe { - if let res = await uploadNewRecipe() { - viewModel.alertType = res - viewModel.presentAlert = true - } else { - presentationMode.wrappedValue.dismiss() - } - } else { - if let res = await uploadEditedRecipe() { - viewModel.alertType = res - viewModel.presentAlert = true - } else { - viewModel.editMode = false - } - } - } - } label: { - if viewModel.newRecipe { - Text("Upload Recipe") - } else { - Text("Upload Changes") - } - } - } - - // Delete Button - if !viewModel.newRecipe { - ToolbarItem(placement: .topBarTrailing) { - Menu { - Button { - print("Delete recipe.") - viewModel.alertType = RecipeAlert.CONFIRM_DELETE - viewModel.alertAction = { - if let res = await deleteRecipe() { - viewModel.alertType = res - viewModel.alertAction = { } - viewModel.presentAlert = true - } else { - presentationMode.wrappedValue.dismiss() - } - } - viewModel.presentAlert = true - } label: { - Image(systemName: "trash") - .foregroundStyle(.red) - Text("Delete recipe") - .foregroundStyle(.red) - } - } label: { - Image(systemName: "ellipsis.circle") - .font(.title3) - .padding() - } - } - } - } else { - ToolbarItem(placement: .topBarTrailing) { - Menu { - Button { - viewModel.editMode = true - } label: { - HStack { - Text("Edit") - Image(systemName: "pencil") - } - } - - Button { - print("Sharing recipe ...") - viewModel.presentShareSheet = true - } label: { - Text("Share recipe") - Image(systemName: "square.and.arrow.up") - } - } label: { - Image(systemName: "ellipsis.circle") - } - - } - } + RecipeViewToolBar(isPresented: $isPresented, viewModel: viewModel) } .sheet(isPresented: $viewModel.presentShareSheet) { ShareView(recipeDetail: viewModel.observableRecipeDetail.toRecipeDetail(), @@ -363,6 +270,12 @@ struct RecipeView: View { self.recipeDetail = recipeDetail self.observableRecipeDetail = ObservableRecipeDetail(recipeDetail) } + + func presentAlert(_ type: UserAlert, action: @escaping () async -> () = {}) { + alertType = type + alertAction = action + presentAlert = true + } } } @@ -390,28 +303,123 @@ extension RecipeView { return nil } - func uploadNewRecipe() async -> UserAlert? { - print("Uploading new recipe.") - if let recipeValidationError = recipeValid() { - return recipeValidationError + +} + + +// MARK: - Tool Bar + + +struct RecipeViewToolBar: ToolbarContent { + @EnvironmentObject var appState: AppState + @Binding var isPresented: Bool + @ObservedObject var viewModel: RecipeView.ViewModel + + + var body: some ToolbarContent { + if viewModel.editMode { + ToolbarItemGroup(placement: .topBarLeading){ + Button("Cancel") { + viewModel.editMode = false + isPresented = false + } + + if !viewModel.newRecipe { + Menu { + Button(role: .destructive) { + viewModel.presentAlert( + RecipeAlert.CONFIRM_DELETE, + action: { + await handleDelete() + } + ) + } label: { + Label("Delete Recipe", systemImage: "trash") + } + } label: { + Image(systemName: "ellipsis.circle") + } + } + } + + ToolbarItem(placement: .topBarTrailing) { + Button { + Task { + await handleUpload() + } + } label: { + if viewModel.newRecipe { + Text("Upload Recipe") + } else { + Text("Upload Changes") + } + } + } + } else { + ToolbarItem(placement: .topBarTrailing) { + Menu { + Button { + viewModel.editMode = true + } label: { + Label("Edit", systemImage: "pencil") + } + + Button { + print("Sharing recipe ...") + viewModel.presentShareSheet = true + } label: { + Label("Share Recipe", systemImage: "square.and.arrow.up") + } + } label: { + Image(systemName: "ellipsis.circle") + } + } } - - return await appState.uploadRecipe(recipeDetail: viewModel.observableRecipeDetail.toRecipeDetail(), createNew: true) } - func uploadEditedRecipe() async -> UserAlert? { - print("Uploading changed recipe.") - - guard let recipeId = Int(viewModel.observableRecipeDetail.id) else { return RequestAlert.REQUEST_DROPPED } - - return await appState.uploadRecipe(recipeDetail: viewModel.observableRecipeDetail.toRecipeDetail(), createNew: false) + func handleUpload() async { + if viewModel.newRecipe { + print("Uploading new recipe.") + if let recipeValidationError = recipeValid() { + viewModel.presentAlert(recipeValidationError) + return + } + + if let alert = await appState.uploadRecipe(recipeDetail: viewModel.observableRecipeDetail.toRecipeDetail(), createNew: true) { + viewModel.presentAlert(alert) + return + } + } else { + print("Uploading changed recipe.") + + guard let _ = Int(viewModel.observableRecipeDetail.id) else { + viewModel.presentAlert(RequestAlert.REQUEST_DROPPED) + return + } + + if let alert = await appState.uploadRecipe(recipeDetail: viewModel.observableRecipeDetail.toRecipeDetail(), createNew: false) { + viewModel.presentAlert(alert) + return + } + } + await appState.getCategories() + await appState.getCategory(named: viewModel.observableRecipeDetail.recipeCategory, fetchMode: .preferServer) + viewModel.editMode = false } - func deleteRecipe() async -> RequestAlert? { + func handleDelete() async { + let category = viewModel.observableRecipeDetail.recipeCategory guard let id = Int(viewModel.observableRecipeDetail.id) else { - return .REQUEST_DROPPED + viewModel.presentAlert(RequestAlert.REQUEST_DROPPED) + return } - return await appState.deleteRecipe(withId: id, categoryName: viewModel.observableRecipeDetail.recipeCategory) + if let alert = await appState.deleteRecipe(withId: id, categoryName: viewModel.observableRecipeDetail.recipeCategory) { + viewModel.presentAlert(alert) + return + } + await appState.getCategories() + await appState.getCategory(named: category, fetchMode: .preferServer) + self.isPresented = false } func recipeValid() -> RecipeAlert? { @@ -434,7 +442,6 @@ extension RecipeView { } } } - return nil } } @@ -461,12 +468,12 @@ fileprivate struct RecipeImportSection: View { Button { Task { if let res = await importRecipe(viewModel.importUrl) { - viewModel.alertType = RecipeAlert.CUSTOM( - title: res.localizedTitle, - description: res.localizedDescription + viewModel.presentAlert( + RecipeAlert.CUSTOM( + title: res.localizedTitle, + description: res.localizedDescription + ) ) - viewModel.alertAction = { } - viewModel.presentAlert = true } } } label: { diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeDurationSection.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeDurationSection.swift index 08c8c69..19749d6 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeDurationSection.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeDurationSection.swift @@ -21,13 +21,15 @@ struct RecipeDurationSection: View { DurationView(time: viewModel.observableRecipeDetail.cookTime, title: LocalizedStringKey("Cooking")) DurationView(time: viewModel.observableRecipeDetail.totalTime, title: LocalizedStringKey("Total time")) } - Button { - presentPopover.toggle() - } label: { - Text("Edit") + if viewModel.editMode { + Button { + presentPopover.toggle() + } label: { + Text("Edit") + } + .buttonStyle(.borderedProminent) + .padding(.top, 5) } - .buttonStyle(.borderedProminent) - .padding(.top, 5) } .padding() .popover(isPresented: $presentPopover) { diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeGenericViews.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeGenericViews.swift index b556489..e56a61e 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeGenericViews.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeGenericViews.swift @@ -12,7 +12,7 @@ import SwiftUI struct RecipeListSection: View { - @State var list: [String] + @Binding var list: [String] var body: some View { VStack(alignment: .leading) { @@ -74,14 +74,17 @@ struct EditableListView: View { List { if items.isEmpty { Text(emptyListText) + } else { + ForEach(items.indices, id: \.self) { ix in + TextField(titleKey, text: $items[ix], axis: axis) + .lineLimit(lineLimit) + .padding(5) + } + .onDelete(perform: deleteItem) + .onMove(perform: moveItem) + .scrollDismissesKeyboard(.immediately) + } - - ForEach(items.indices, id: \.self) { ix in - TextField(titleKey, text: $items[ix], axis: axis) - .lineLimit(lineLimit) - } - .onDelete(perform: deleteItem) - .onMove(perform: moveItem) } VStack { Spacer() @@ -104,7 +107,7 @@ struct EditableListView: View { Text("Done") } ) - .environment(\.editMode, .constant(.active)) // Bind edit mode to your state variable + .environment(\.editMode, .constant(.active)) } } diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeKeywordSection.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeKeywordSection.swift index 5206eba..9dc72e1 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeKeywordSection.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeKeywordSection.swift @@ -18,7 +18,7 @@ struct RecipeKeywordSection: View { CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandKeywordSection) { Group { if !viewModel.observableRecipeDetail.keywords.isEmpty && !viewModel.editMode { - RecipeListSection(list: viewModel.observableRecipeDetail.keywords) + RecipeListSection(list: $viewModel.observableRecipeDetail.keywords) } else { Text(LocalizedStringKey("No keywords.")) } diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeToolSection.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeToolSection.swift index 7e0f983..04dfb6d 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeToolSection.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeToolSection.swift @@ -20,7 +20,7 @@ struct RecipeToolSection: View { Spacer() } - RecipeListSection(list: viewModel.observableRecipeDetail.tool) + RecipeListSection(list: $viewModel.observableRecipeDetail.tool) if viewModel.editMode { Button { diff --git a/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift b/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift index 3fc5837..1b20290 100644 --- a/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift +++ b/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift @@ -51,7 +51,7 @@ struct RecipeTabView: View { SettingsView() } .navigationDestination(isPresented: $viewModel.presentEditView) { - RecipeView(viewModel: RecipeView.ViewModel()) + RecipeView(isPresented: $viewModel.presentEditView, viewModel: RecipeView.ViewModel()) } } detail: { NavigationStack { diff --git a/Nextcloud Cookbook iOS Client/Views/Tabs/SearchTabView.swift b/Nextcloud Cookbook iOS Client/Views/Tabs/SearchTabView.swift index 7a4cfae..e648ea0 100644 --- a/Nextcloud Cookbook iOS Client/Views/Tabs/SearchTabView.swift +++ b/Nextcloud Cookbook iOS Client/Views/Tabs/SearchTabView.swift @@ -34,7 +34,7 @@ struct SearchTabView: View { } } .navigationDestination(for: Recipe.self) { recipe in - RecipeView(viewModel: RecipeView.ViewModel(recipe: recipe)) + RecipeView(isPresented: .constant(true), viewModel: RecipeView.ViewModel(recipe: recipe)) } .searchable(text: $viewModel.searchText, prompt: "Search recipes/keywords") }