Recipe edit view polish
This commit is contained in:
Binary file not shown.
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" : {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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? {
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUpload() async {
|
||||||
|
if viewModel.newRecipe {
|
||||||
print("Uploading new recipe.")
|
print("Uploading new recipe.")
|
||||||
if let recipeValidationError = recipeValid() {
|
if let recipeValidationError = recipeValid() {
|
||||||
return recipeValidationError
|
viewModel.presentAlert(recipeValidationError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return await appState.uploadRecipe(recipeDetail: viewModel.observableRecipeDetail.toRecipeDetail(), createNew: true)
|
if let alert = await appState.uploadRecipe(recipeDetail: viewModel.observableRecipeDetail.toRecipeDetail(), createNew: true) {
|
||||||
|
viewModel.presentAlert(alert)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
func uploadEditedRecipe() async -> UserAlert? {
|
|
||||||
print("Uploading changed recipe.")
|
print("Uploading changed recipe.")
|
||||||
|
|
||||||
guard let recipeId = Int(viewModel.observableRecipeDetail.id) else { return RequestAlert.REQUEST_DROPPED }
|
guard let _ = Int(viewModel.observableRecipeDetail.id) else {
|
||||||
|
viewModel.presentAlert(RequestAlert.REQUEST_DROPPED)
|
||||||
return await appState.uploadRecipe(recipeDetail: viewModel.observableRecipeDetail.toRecipeDetail(), createNew: false)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteRecipe() async -> RequestAlert? {
|
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 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(
|
||||||
|
RecipeAlert.CUSTOM(
|
||||||
title: res.localizedTitle,
|
title: res.localizedTitle,
|
||||||
description: res.localizedDescription
|
description: res.localizedDescription
|
||||||
)
|
)
|
||||||
viewModel.alertAction = { }
|
)
|
||||||
viewModel.presentAlert = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ 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"))
|
||||||
}
|
}
|
||||||
|
if viewModel.editMode {
|
||||||
Button {
|
Button {
|
||||||
presentPopover.toggle()
|
presentPopover.toggle()
|
||||||
} label: {
|
} label: {
|
||||||
@@ -29,6 +30,7 @@ struct RecipeDurationSection: View {
|
|||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
.padding(.top, 5)
|
.padding(.top, 5)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.popover(isPresented: $presentPopover) {
|
.popover(isPresented: $presentPopover) {
|
||||||
EditableDurationView(
|
EditableDurationView(
|
||||||
|
|||||||
@@ -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
|
ForEach(items.indices, id: \.self) { ix in
|
||||||
TextField(titleKey, text: $items[ix], axis: axis)
|
TextField(titleKey, text: $items[ix], axis: axis)
|
||||||
.lineLimit(lineLimit)
|
.lineLimit(lineLimit)
|
||||||
|
.padding(5)
|
||||||
}
|
}
|
||||||
.onDelete(perform: deleteItem)
|
.onDelete(perform: deleteItem)
|
||||||
.onMove(perform: moveItem)
|
.onMove(perform: moveItem)
|
||||||
|
.scrollDismissesKeyboard(.immediately)
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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."))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user