Recipe edit view polish

This commit is contained in:
VincentMeilinger
2024-03-05 21:42:02 +01:00
parent b5dbaad9aa
commit 11359e11d4
11 changed files with 157 additions and 137 deletions

View File

@@ -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)
}

View File

@@ -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" : {

View File

@@ -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 {

View File

@@ -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.")
func handleUpload() async {
if viewModel.newRecipe {
print("Uploading new recipe.")
if let recipeValidationError = recipeValid() {
viewModel.presentAlert(recipeValidationError)
return
}
guard let recipeId = Int(viewModel.observableRecipeDetail.id) else { return RequestAlert.REQUEST_DROPPED }
if let alert = await appState.uploadRecipe(recipeDetail: viewModel.observableRecipeDetail.toRecipeDetail(), createNew: true) {
viewModel.presentAlert(alert)
return
}
} else {
print("Uploading changed recipe.")
return await appState.uploadRecipe(recipeDetail: viewModel.observableRecipeDetail.toRecipeDetail(), createNew: false)
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: {

View File

@@ -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) {

View File

@@ -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))
}
}

View File

@@ -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."))
}

View File

@@ -20,7 +20,7 @@ struct RecipeToolSection: View {
Spacer()
}
RecipeListSection(list: viewModel.observableRecipeDetail.tool)
RecipeListSection(list: $viewModel.observableRecipeDetail.tool)
if viewModel.editMode {
Button {

View File

@@ -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 {

View File

@@ -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")
}