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) dataStore.delete(path: path)
if recipes[categoryName] != nil { if recipes[categoryName] != nil {
recipes[categoryName]!.removeAll(where: { recipe in recipes[categoryName]!.removeAll(where: { recipe in
recipe.recipe_id == id ? true : false recipe.recipe_id == id
}) })
recipeDetails.removeValue(forKey: id) recipeDetails.removeValue(forKey: id)
} }

View File

@@ -977,6 +977,7 @@
} }
}, },
"Delete recipe" : { "Delete recipe" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
@@ -997,6 +998,9 @@
} }
} }
} }
},
"Delete Recipe" : {
}, },
"Delete recipe?" : { "Delete recipe?" : {
"localizations" : { "localizations" : {
@@ -2693,6 +2697,7 @@
} }
}, },
"Share recipe" : { "Share recipe" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
@@ -2713,6 +2718,9 @@
} }
} }
} }
},
"Share Recipe" : {
}, },
"Show help" : { "Show help" : {
"localizations" : { "localizations" : {

View File

@@ -36,7 +36,7 @@ struct RecipeListView: View {
} }
} }
.navigationDestination(for: Recipe.self) { recipe in .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) .navigationTitle(categoryName == "*" ? String(localized: "Other") : categoryName)
.toolbar { .toolbar {

View File

@@ -10,13 +10,11 @@ import SwiftUI
struct RecipeView: View { struct RecipeView: View {
@Environment(\.presentationMode) var presentationMode
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@Binding var isPresented: Bool
@StateObject var viewModel: ViewModel @StateObject var viewModel: ViewModel
@State var imageHeight: CGFloat = 350 @State var imageHeight: CGFloat = 350
private enum CoordinateSpaces { private enum CoordinateSpaces {
case scrollView case scrollView
} }
@@ -112,98 +110,7 @@ struct RecipeView: View {
//.toolbarTitleDisplayMode(.inline) //.toolbarTitleDisplayMode(.inline)
.navigationTitle(viewModel.showTitle ? viewModel.recipe.name : "") .navigationTitle(viewModel.showTitle ? viewModel.recipe.name : "")
.toolbar { .toolbar {
if viewModel.editMode { RecipeViewToolBar(isPresented: $isPresented, viewModel: viewModel)
// 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")
}
}
}
} }
.sheet(isPresented: $viewModel.presentShareSheet) { .sheet(isPresented: $viewModel.presentShareSheet) {
ShareView(recipeDetail: viewModel.observableRecipeDetail.toRecipeDetail(), ShareView(recipeDetail: viewModel.observableRecipeDetail.toRecipeDetail(),
@@ -363,6 +270,12 @@ struct RecipeView: View {
self.recipeDetail = recipeDetail self.recipeDetail = recipeDetail
self.observableRecipeDetail = ObservableRecipeDetail(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 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? { func handleUpload() async {
print("Uploading changed recipe.") if viewModel.newRecipe {
print("Uploading new recipe.")
guard let recipeId = Int(viewModel.observableRecipeDetail.id) else { return RequestAlert.REQUEST_DROPPED } if let recipeValidationError = recipeValid() {
viewModel.presentAlert(recipeValidationError)
return await appState.uploadRecipe(recipeDetail: viewModel.observableRecipeDetail.toRecipeDetail(), createNew: false) 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 { 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? { func recipeValid() -> RecipeAlert? {
@@ -434,7 +442,6 @@ extension RecipeView {
} }
} }
} }
return nil return nil
} }
} }
@@ -461,12 +468,12 @@ fileprivate struct RecipeImportSection: View {
Button { Button {
Task { Task {
if let res = await importRecipe(viewModel.importUrl) { if let res = await importRecipe(viewModel.importUrl) {
viewModel.alertType = RecipeAlert.CUSTOM( viewModel.presentAlert(
title: res.localizedTitle, RecipeAlert.CUSTOM(
description: res.localizedDescription title: res.localizedTitle,
description: res.localizedDescription
)
) )
viewModel.alertAction = { }
viewModel.presentAlert = true
} }
} }
} label: { } label: {

View File

@@ -21,13 +21,15 @@ struct RecipeDurationSection: View {
DurationView(time: viewModel.observableRecipeDetail.cookTime, title: LocalizedStringKey("Cooking")) DurationView(time: viewModel.observableRecipeDetail.cookTime, title: LocalizedStringKey("Cooking"))
DurationView(time: viewModel.observableRecipeDetail.totalTime, title: LocalizedStringKey("Total time")) DurationView(time: viewModel.observableRecipeDetail.totalTime, title: LocalizedStringKey("Total time"))
} }
Button { if viewModel.editMode {
presentPopover.toggle() Button {
} label: { presentPopover.toggle()
Text("Edit") } label: {
Text("Edit")
}
.buttonStyle(.borderedProminent)
.padding(.top, 5)
} }
.buttonStyle(.borderedProminent)
.padding(.top, 5)
} }
.padding() .padding()
.popover(isPresented: $presentPopover) { .popover(isPresented: $presentPopover) {

View File

@@ -12,7 +12,7 @@ import SwiftUI
struct RecipeListSection: View { struct RecipeListSection: View {
@State var list: [String] @Binding var list: [String]
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
@@ -74,14 +74,17 @@ struct EditableListView: View {
List { List {
if items.isEmpty { if items.isEmpty {
Text(emptyListText) 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 { VStack {
Spacer() Spacer()
@@ -104,7 +107,7 @@ struct EditableListView: View {
Text("Done") 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) { CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandKeywordSection) {
Group { Group {
if !viewModel.observableRecipeDetail.keywords.isEmpty && !viewModel.editMode { if !viewModel.observableRecipeDetail.keywords.isEmpty && !viewModel.editMode {
RecipeListSection(list: viewModel.observableRecipeDetail.keywords) RecipeListSection(list: $viewModel.observableRecipeDetail.keywords)
} else { } else {
Text(LocalizedStringKey("No keywords.")) Text(LocalizedStringKey("No keywords."))
} }

View File

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

View File

@@ -51,7 +51,7 @@ struct RecipeTabView: View {
SettingsView() SettingsView()
} }
.navigationDestination(isPresented: $viewModel.presentEditView) { .navigationDestination(isPresented: $viewModel.presentEditView) {
RecipeView(viewModel: RecipeView.ViewModel()) RecipeView(isPresented: $viewModel.presentEditView, viewModel: RecipeView.ViewModel())
} }
} detail: { } detail: {
NavigationStack { NavigationStack {

View File

@@ -34,7 +34,7 @@ struct SearchTabView: View {
} }
} }
.navigationDestination(for: Recipe.self) { recipe in .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") .searchable(text: $viewModel.searchText, prompt: "Search recipes/keywords")
} }