Created RecipeEditViewModel to cleanup RecipeEditView
This commit is contained in:
@@ -35,6 +35,7 @@
|
|||||||
A76B8A6F2ADFFA8800096CEC /* SupportedLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */; };
|
A76B8A6F2ADFFA8800096CEC /* SupportedLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */; };
|
||||||
A76B8A712AE002AE00096CEC /* Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76B8A702AE002AE00096CEC /* Alerts.swift */; };
|
A76B8A712AE002AE00096CEC /* Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76B8A702AE002AE00096CEC /* Alerts.swift */; };
|
||||||
A79AA8E02AFF80E3007D25F2 /* DurationComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79AA8DF2AFF80E3007D25F2 /* DurationComponents.swift */; };
|
A79AA8E02AFF80E3007D25F2 /* DurationComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79AA8DF2AFF80E3007D25F2 /* DurationComponents.swift */; };
|
||||||
|
A79AA8E22AFF8C14007D25F2 /* RecipeEditViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79AA8E12AFF8C14007D25F2 /* RecipeEditViewModel.swift */; };
|
||||||
A7AEAE642AD5521400135378 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = A7AEAE632AD5521400135378 /* Localizable.xcstrings */; };
|
A7AEAE642AD5521400135378 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = A7AEAE632AD5521400135378 /* Localizable.xcstrings */; };
|
||||||
A7F3F8E82ACBFC760076C227 /* KeywordPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */; };
|
A7F3F8E82ACBFC760076C227 /* KeywordPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */; };
|
||||||
A7F3F8EA2ACC221C0076C227 /* CategoryPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F3F8E92ACC221C0076C227 /* CategoryPickerView.swift */; };
|
A7F3F8EA2ACC221C0076C227 /* CategoryPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F3F8E92ACC221C0076C227 /* CategoryPickerView.swift */; };
|
||||||
@@ -90,6 +91,7 @@
|
|||||||
A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedLanguage.swift; sourceTree = "<group>"; };
|
A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedLanguage.swift; sourceTree = "<group>"; };
|
||||||
A76B8A702AE002AE00096CEC /* Alerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alerts.swift; sourceTree = "<group>"; };
|
A76B8A702AE002AE00096CEC /* Alerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alerts.swift; sourceTree = "<group>"; };
|
||||||
A79AA8DF2AFF80E3007D25F2 /* DurationComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DurationComponents.swift; sourceTree = "<group>"; };
|
A79AA8DF2AFF80E3007D25F2 /* DurationComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DurationComponents.swift; sourceTree = "<group>"; };
|
||||||
|
A79AA8E12AFF8C14007D25F2 /* RecipeEditViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeEditViewModel.swift; sourceTree = "<group>"; };
|
||||||
A7AEAE632AD5521400135378 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
A7AEAE632AD5521400135378 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||||
A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordPickerView.swift; sourceTree = "<group>"; };
|
A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordPickerView.swift; sourceTree = "<group>"; };
|
||||||
A7F3F8E92ACC221C0076C227 /* CategoryPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerView.swift; sourceTree = "<group>"; };
|
A7F3F8E92ACC221C0076C227 /* CategoryPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerView.swift; sourceTree = "<group>"; };
|
||||||
@@ -201,6 +203,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A70171AC2AA8EF4700064C43 /* MainViewModel.swift */,
|
A70171AC2AA8EF4700064C43 /* MainViewModel.swift */,
|
||||||
|
A79AA8E12AFF8C14007D25F2 /* RecipeEditViewModel.swift */,
|
||||||
);
|
);
|
||||||
path = ViewModels;
|
path = ViewModels;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -398,6 +401,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
A79AA8E22AFF8C14007D25F2 /* RecipeEditViewModel.swift in Sources */,
|
||||||
A70D7CA12AC73CA800D53DBF /* RecipeEditView.swift in Sources */,
|
A70D7CA12AC73CA800D53DBF /* RecipeEditView.swift in Sources */,
|
||||||
A70171B12AB211DF00064C43 /* CustomError.swift in Sources */,
|
A70171B12AB211DF00064C43 /* CustomError.swift in Sources */,
|
||||||
A76B8A712AE002AE00096CEC /* Alerts.swift in Sources */,
|
A76B8A712AE002AE00096CEC /* Alerts.swift in Sources */,
|
||||||
|
|||||||
@@ -883,6 +883,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Import" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Import Recipe" : {
|
"Import Recipe" : {
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
//
|
||||||
|
// RecipeEditViewModel.swift
|
||||||
|
// Nextcloud Cookbook iOS Client
|
||||||
|
//
|
||||||
|
// Created by Vincent Meilinger on 11.11.23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor class RecipeEditViewModel: ObservableObject {
|
||||||
|
@ObservedObject var mainViewModel: MainViewModel
|
||||||
|
@Published var isPresented: Binding<Bool>
|
||||||
|
@Published var recipe: RecipeDetail = RecipeDetail()
|
||||||
|
|
||||||
|
@Published var prepDuration: DurationComponents = DurationComponents()
|
||||||
|
@Published var cookDuration: DurationComponents = DurationComponents()
|
||||||
|
@Published var totalDuration: DurationComponents = DurationComponents()
|
||||||
|
|
||||||
|
@Published var searchText: String = ""
|
||||||
|
@Published var keywords: [String] = []
|
||||||
|
@Published var keywordSuggestions: [String] = []
|
||||||
|
|
||||||
|
@Published var showImportSection: Bool = false
|
||||||
|
@Published var importURL: String = ""
|
||||||
|
|
||||||
|
@Published var presentAlert = false
|
||||||
|
var alertType: UserAlert = RecipeCreationError.GENERIC
|
||||||
|
var alertAction: @MainActor () -> () = {}
|
||||||
|
|
||||||
|
var uploadNew: Bool = true
|
||||||
|
var waitingForUpload: Bool = false
|
||||||
|
|
||||||
|
|
||||||
|
init(mainViewModel: MainViewModel, isPresented: Binding<Bool>, uploadNew: Bool) {
|
||||||
|
self.mainViewModel = mainViewModel
|
||||||
|
self.isPresented = isPresented
|
||||||
|
self.uploadNew = uploadNew
|
||||||
|
}
|
||||||
|
|
||||||
|
init(mainViewModel: MainViewModel, recipeDetail: RecipeDetail, isPresented: Binding<Bool>, uploadNew: Bool) {
|
||||||
|
self.mainViewModel = mainViewModel
|
||||||
|
self.recipe = recipeDetail
|
||||||
|
self.isPresented = isPresented
|
||||||
|
self.uploadNew = uploadNew
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func createRecipe() {
|
||||||
|
self.recipe.prepTime = prepDuration.toPTString()
|
||||||
|
self.recipe.cookTime = cookDuration.toPTString()
|
||||||
|
self.recipe.totalTime = totalDuration.toPTString()
|
||||||
|
self.recipe.setKeywordsFromArray(keywords)
|
||||||
|
}
|
||||||
|
|
||||||
|
func recipeValid() -> Bool {
|
||||||
|
// Check if the recipe has a name
|
||||||
|
if recipe.name.replacingOccurrences(of: " ", with: "") == "" {
|
||||||
|
alertType = RecipeCreationError.NO_TITLE
|
||||||
|
alertAction = {}
|
||||||
|
presentAlert = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Check if the recipe has a unique name
|
||||||
|
for recipeList in mainViewModel.recipes.values {
|
||||||
|
for r in recipeList {
|
||||||
|
if r.name
|
||||||
|
.replacingOccurrences(of: " ", with: "")
|
||||||
|
.lowercased() ==
|
||||||
|
recipe.name
|
||||||
|
.replacingOccurrences(of: " ", with: "")
|
||||||
|
.lowercased()
|
||||||
|
{
|
||||||
|
alertType = RecipeCreationError.DUPLICATE
|
||||||
|
alertAction = {}
|
||||||
|
presentAlert = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func uploadNewRecipe() {
|
||||||
|
print("Uploading new recipe.")
|
||||||
|
waitingForUpload = true
|
||||||
|
createRecipe()
|
||||||
|
guard recipeValid() else { return }
|
||||||
|
let request = RequestWrapper.customRequest(
|
||||||
|
method: .POST,
|
||||||
|
path: .NEW_RECIPE,
|
||||||
|
headerFields: [
|
||||||
|
HeaderField.accept(value: .JSON),
|
||||||
|
HeaderField.ocsRequest(value: true),
|
||||||
|
HeaderField.contentType(value: .JSON)
|
||||||
|
],
|
||||||
|
body: JSONEncoder.safeEncode(self.recipe)
|
||||||
|
)
|
||||||
|
sendRequest(request)
|
||||||
|
dismissEditView()
|
||||||
|
}
|
||||||
|
|
||||||
|
func uploadEditedRecipe() {
|
||||||
|
waitingForUpload = true
|
||||||
|
print("Uploading changed recipe.")
|
||||||
|
guard let recipeId = Int(recipe.id) else { return }
|
||||||
|
createRecipe()
|
||||||
|
let request = RequestWrapper.customRequest(
|
||||||
|
method: .PUT,
|
||||||
|
path: .RECIPE_DETAIL(recipeId: recipeId),
|
||||||
|
headerFields: [
|
||||||
|
HeaderField.accept(value: .JSON),
|
||||||
|
HeaderField.ocsRequest(value: true),
|
||||||
|
HeaderField.contentType(value: .JSON)
|
||||||
|
],
|
||||||
|
body: JSONEncoder.safeEncode(self.recipe)
|
||||||
|
)
|
||||||
|
sendRequest(request)
|
||||||
|
dismissEditView()
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteRecipe() {
|
||||||
|
guard let recipeId = Int(recipe.id) else { return }
|
||||||
|
let request = RequestWrapper.customRequest(
|
||||||
|
method: .DELETE,
|
||||||
|
path: .RECIPE_DETAIL(recipeId: recipeId),
|
||||||
|
headerFields: [
|
||||||
|
HeaderField.accept(value: .JSON),
|
||||||
|
HeaderField.ocsRequest(value: true)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
sendRequest(request)
|
||||||
|
if let recipeIdInt = Int(recipe.id) {
|
||||||
|
mainViewModel.deleteRecipe(withId: recipeIdInt, categoryName: recipe.recipeCategory)
|
||||||
|
}
|
||||||
|
dismissEditView()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendRequest(_ request: RequestWrapper) {
|
||||||
|
Task {
|
||||||
|
guard let apiController = mainViewModel.apiController else { return }
|
||||||
|
let (data, _): (Data?, Error?) = await apiController.sendDataRequest(request)
|
||||||
|
guard let data = data else { return }
|
||||||
|
do {
|
||||||
|
let error = try JSONDecoder().decode(ServerMessage.self, from: data)
|
||||||
|
// TODO: Better error handling (Show error to user!)
|
||||||
|
} catch {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dismissEditView() {
|
||||||
|
Task {
|
||||||
|
await mainViewModel.loadCategoryList(needsUpdate: true)
|
||||||
|
await mainViewModel.loadRecipeList(categoryName: recipe.recipeCategory, needsUpdate: true)
|
||||||
|
}
|
||||||
|
isPresented.wrappedValue = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareView() {
|
||||||
|
if let prepTime = recipe.prepTime {
|
||||||
|
prepDuration.fromPTString(prepTime)
|
||||||
|
}
|
||||||
|
if let cookTime = recipe.cookTime {
|
||||||
|
cookDuration.fromPTString(cookTime)
|
||||||
|
}
|
||||||
|
if let totalTime = recipe.totalTime {
|
||||||
|
totalDuration.fromPTString(totalTime)
|
||||||
|
}
|
||||||
|
self.keywords = recipe.getKeywordsArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
func importRecipe() {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let (scrapedRecipe, error) = try await RecipeScraper().scrape(url: importURL)
|
||||||
|
if let scrapedRecipe = scrapedRecipe {
|
||||||
|
self.recipe = scrapedRecipe
|
||||||
|
prepareView()
|
||||||
|
}
|
||||||
|
if let error = error {
|
||||||
|
self.alertType = error
|
||||||
|
self.alertAction = {}
|
||||||
|
self.presentAlert = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,7 +80,13 @@ struct MainView: View {
|
|||||||
}
|
}
|
||||||
.tint(.nextcloudBlue)
|
.tint(.nextcloudBlue)
|
||||||
.sheet(isPresented: $showEditView) {
|
.sheet(isPresented: $showEditView) {
|
||||||
RecipeEditView(viewModel: viewModel, isPresented: $showEditView)
|
RecipeEditView(viewModel:
|
||||||
|
RecipeEditViewModel(
|
||||||
|
mainViewModel: viewModel,
|
||||||
|
isPresented: $showEditView,
|
||||||
|
uploadNew: true
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
self.serverConnection = await viewModel.checkServerConnection()
|
self.serverConnection = await viewModel.checkServerConnection()
|
||||||
|
|||||||
@@ -95,7 +95,14 @@ struct RecipeDetailView: View {
|
|||||||
}
|
}
|
||||||
.sheet(isPresented: $presentEditView) {
|
.sheet(isPresented: $presentEditView) {
|
||||||
if let recipeDetail = recipeDetail {
|
if let recipeDetail = recipeDetail {
|
||||||
RecipeEditView(viewModel: viewModel, recipe: recipeDetail, isPresented: $presentEditView, uploadNew: false)
|
RecipeEditView(viewModel:
|
||||||
|
RecipeEditViewModel(
|
||||||
|
mainViewModel: viewModel,
|
||||||
|
recipeDetail: recipeDetail,
|
||||||
|
isPresented: $presentEditView,
|
||||||
|
uploadNew: false
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
|
|||||||
@@ -12,43 +12,25 @@ import PhotosUI
|
|||||||
|
|
||||||
|
|
||||||
struct RecipeEditView: View {
|
struct RecipeEditView: View {
|
||||||
@ObservedObject var viewModel: MainViewModel
|
@ObservedObject var viewModel: RecipeEditViewModel
|
||||||
@State var recipe: RecipeDetail = RecipeDetail()
|
|
||||||
@Binding var isPresented: Bool
|
|
||||||
@State var uploadNew: Bool = true
|
|
||||||
|
|
||||||
@State private var presentAlert = false
|
|
||||||
@State private var alertType: UserAlert = RecipeCreationError.GENERIC
|
|
||||||
@State private var alertAction: () -> () = {}
|
|
||||||
|
|
||||||
@StateObject private var prepDuration: DurationComponents = DurationComponents()
|
|
||||||
@StateObject private var cookDuration: DurationComponents = DurationComponents()
|
|
||||||
@StateObject private var totalDuration: DurationComponents = DurationComponents()
|
|
||||||
@State private var searchText: String = ""
|
|
||||||
@State private var keywords: [String] = []
|
|
||||||
@State private var keywordSuggestions: [String] = []
|
|
||||||
|
|
||||||
@State private var importURL: String = ""
|
|
||||||
@State private var showImportSection: Bool = false
|
|
||||||
@State private var waitingForUpload: Bool = false
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
Button() {
|
Button() {
|
||||||
isPresented = false
|
viewModel.isPresented.wrappedValue = false
|
||||||
} label: {
|
} label: {
|
||||||
Text("Cancel")
|
Text("Cancel")
|
||||||
.bold()
|
.bold()
|
||||||
}
|
}
|
||||||
if !uploadNew {
|
if !viewModel.uploadNew {
|
||||||
Menu {
|
Menu {
|
||||||
Button {
|
Button {
|
||||||
print("Delete recipe.")
|
print("Delete recipe.")
|
||||||
alertType = RecipeCreationError.CONFIRM_DELETE
|
viewModel.alertType = RecipeCreationError.CONFIRM_DELETE
|
||||||
alertAction = deleteRecipe
|
viewModel.alertAction = viewModel.deleteRecipe
|
||||||
presentAlert = true
|
viewModel.presentAlert = true
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "trash")
|
Image(systemName: "trash")
|
||||||
.foregroundStyle(.red)
|
.foregroundStyle(.red)
|
||||||
@@ -63,10 +45,10 @@ struct RecipeEditView: View {
|
|||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Button() {
|
Button() {
|
||||||
if uploadNew {
|
if viewModel.uploadNew {
|
||||||
uploadNewRecipe()
|
viewModel.uploadNewRecipe()
|
||||||
} else {
|
} else {
|
||||||
uploadEditedRecipe()
|
viewModel.uploadEditedRecipe()
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Text("Upload")
|
Text("Upload")
|
||||||
@@ -74,34 +56,24 @@ struct RecipeEditView: View {
|
|||||||
}
|
}
|
||||||
}.padding()
|
}.padding()
|
||||||
HStack {
|
HStack {
|
||||||
Text(recipe.name == "" ? LocalizedStringKey("New recipe") : LocalizedStringKey(recipe.name))
|
Text(viewModel.recipe.name == "" ? LocalizedStringKey("New recipe") : LocalizedStringKey(viewModel.recipe.name))
|
||||||
.font(.title)
|
.font(.title)
|
||||||
.bold()
|
.bold()
|
||||||
.padding()
|
.padding()
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
Form {
|
Form {
|
||||||
if showImportSection {
|
if viewModel.showImportSection {
|
||||||
Section {
|
Section {
|
||||||
TextField("URL (e.g. example.com/recipe)", text: $importURL)
|
TextField("URL (e.g. example.com/recipe)", text: $viewModel.importURL)
|
||||||
.onSubmit {
|
.onSubmit {
|
||||||
Task {
|
viewModel.importRecipe()
|
||||||
do {
|
|
||||||
let (scrapedRecipe, error) = try await RecipeScraper().scrape(url: importURL)
|
|
||||||
if let scrapedRecipe = scrapedRecipe {
|
|
||||||
self.recipe = scrapedRecipe
|
|
||||||
prepareView()
|
|
||||||
}
|
|
||||||
if let error = error {
|
|
||||||
self.alertType = error
|
|
||||||
self.alertAction = {}
|
|
||||||
self.presentAlert = true
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
print("Error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Button {
|
||||||
|
viewModel.importRecipe()
|
||||||
|
} label: {
|
||||||
|
Text("Import")
|
||||||
|
}
|
||||||
} header: {
|
} header: {
|
||||||
Text("Import Recipe")
|
Text("Import Recipe")
|
||||||
} footer: {
|
} footer: {
|
||||||
@@ -112,7 +84,7 @@ struct RecipeEditView: View {
|
|||||||
Section {
|
Section {
|
||||||
Button() {
|
Button() {
|
||||||
withAnimation{
|
withAnimation{
|
||||||
showImportSection = true
|
viewModel.showImportSection = true
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Text("Import recipe from a website")
|
Text("Import recipe from a website")
|
||||||
@@ -120,33 +92,33 @@ struct RecipeEditView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextField("Title", text: $recipe.name)
|
TextField("Title", text: $viewModel.recipe.name)
|
||||||
Section {
|
Section {
|
||||||
TextEditor(text: $recipe.description)
|
TextEditor(text: $viewModel.recipe.description)
|
||||||
} header: {
|
} header: {
|
||||||
Text("Description")
|
Text("Description")
|
||||||
}
|
}
|
||||||
|
|
||||||
Section() {
|
Section() {
|
||||||
NavigationLink(recipe.recipeCategory == "" ? "Category" : "Category: \(recipe.recipeCategory)") {
|
NavigationLink(viewModel.recipe.recipeCategory == "" ? "Category" : "Category: \(viewModel.recipe.recipeCategory)") {
|
||||||
CategoryPickerView(
|
CategoryPickerView(
|
||||||
title: "Category",
|
title: "Category",
|
||||||
searchSuggestions: viewModel.categories.map({ category in
|
searchSuggestions: viewModel.mainViewModel.categories.map({ category in
|
||||||
category.name == "*" ? "Other" : category.name
|
category.name == "*" ? "Other" : category.name
|
||||||
}),
|
}),
|
||||||
selection: $recipe.recipeCategory)
|
selection: $viewModel.recipe.recipeCategory)
|
||||||
}
|
}
|
||||||
NavigationLink("Keywords") {
|
NavigationLink("Keywords") {
|
||||||
KeywordPickerView(
|
KeywordPickerView(
|
||||||
title: "Keywords",
|
title: "Keywords",
|
||||||
searchSuggestions: keywordSuggestions,
|
searchSuggestions: viewModel.keywordSuggestions,
|
||||||
selection: $keywords
|
selection: $viewModel.keywords
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} footer: {
|
} footer: {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
HStack {
|
HStack {
|
||||||
ForEach(keywords, id: \.self) { keyword in
|
ForEach(viewModel.keywords, id: \.self) { keyword in
|
||||||
Text(keyword)
|
Text(keyword)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,173 +126,47 @@ struct RecipeEditView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Section() {
|
Section() {
|
||||||
Picker("Servings:", selection: $recipe.recipeYield) {
|
Picker("Servings:", selection: $viewModel.recipe.recipeYield) {
|
||||||
ForEach(0..<99, id: \.self) { i in
|
ForEach(0..<99, id: \.self) { i in
|
||||||
Text("\(i)").tag(i)
|
Text("\(i)").tag(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pickerStyle(.menu)
|
.pickerStyle(.menu)
|
||||||
DurationPicker(title: LocalizedStringKey("Preparation duration:"), duration: prepDuration)
|
DurationPicker(title: LocalizedStringKey("Preparation duration:"), duration: viewModel.prepDuration)
|
||||||
DurationPicker(title: LocalizedStringKey("Cooking duration:"), duration: cookDuration)
|
DurationPicker(title: LocalizedStringKey("Cooking duration:"), duration: viewModel.cookDuration)
|
||||||
DurationPicker(title: LocalizedStringKey("Total duration:"), duration: totalDuration)
|
DurationPicker(title: LocalizedStringKey("Total duration:"), duration: viewModel.totalDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
EditableListSection(title: LocalizedStringKey("Ingredients"), items: $recipe.recipeIngredient)
|
EditableListSection(title: LocalizedStringKey("Ingredients"), items: $viewModel.recipe.recipeIngredient)
|
||||||
EditableListSection(title: LocalizedStringKey("Tools"), items: $recipe.tool)
|
EditableListSection(title: LocalizedStringKey("Tools"), items: $viewModel.recipe.tool)
|
||||||
EditableListSection(title: LocalizedStringKey("Instructions"), items: $recipe.recipeInstructions)
|
EditableListSection(title: LocalizedStringKey("Instructions"), items: $viewModel.recipe.recipeInstructions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
self.keywordSuggestions = await viewModel.getKeywords()
|
viewModel.keywordSuggestions = await viewModel.mainViewModel.getKeywords()
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
prepareView()
|
viewModel.prepareView()
|
||||||
}
|
}
|
||||||
.alert(alertType.localizedTitle, isPresented: $presentAlert) {
|
.alert(viewModel.alertType.localizedTitle, isPresented: $viewModel.presentAlert) {
|
||||||
ForEach(alertType.alertButtons) { buttonType in
|
ForEach(viewModel.alertType.alertButtons) { buttonType in
|
||||||
if buttonType == .OK {
|
if buttonType == .OK {
|
||||||
Button(AlertButton.OK.rawValue, role: .cancel) {
|
Button(AlertButton.OK.rawValue, role: .cancel) {
|
||||||
alertAction()
|
viewModel.alertAction()
|
||||||
}
|
}
|
||||||
} else if buttonType == .CANCEL {
|
} else if buttonType == .CANCEL {
|
||||||
Button(AlertButton.CANCEL.rawValue, role: .cancel) { }
|
Button(AlertButton.CANCEL.rawValue, role: .cancel) { }
|
||||||
} else if buttonType == .DELETE {
|
} else if buttonType == .DELETE {
|
||||||
Button(AlertButton.DELETE.rawValue, role: .destructive) {
|
Button(AlertButton.DELETE.rawValue, role: .destructive) {
|
||||||
alertAction()
|
viewModel.alertAction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} message: {
|
} message: {
|
||||||
Text(alertType.localizedDescription)
|
Text(viewModel.alertType.localizedDescription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createRecipe() {
|
|
||||||
self.recipe.prepTime = prepDuration.toPTString()
|
|
||||||
self.recipe.cookTime = cookDuration.toPTString()
|
|
||||||
self.recipe.totalTime = totalDuration.toPTString()
|
|
||||||
self.recipe.setKeywordsFromArray(keywords)
|
|
||||||
}
|
|
||||||
|
|
||||||
func recipeValid() -> Bool {
|
|
||||||
// Check if the recipe has a name
|
|
||||||
if recipe.name.replacingOccurrences(of: " ", with: "") == "" {
|
|
||||||
alertType = RecipeCreationError.NO_TITLE
|
|
||||||
alertAction = {}
|
|
||||||
presentAlert = true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Check if the recipe has a unique name
|
|
||||||
for recipeList in viewModel.recipes.values {
|
|
||||||
for r in recipeList {
|
|
||||||
if r.name
|
|
||||||
.replacingOccurrences(of: " ", with: "")
|
|
||||||
.lowercased() ==
|
|
||||||
recipe.name
|
|
||||||
.replacingOccurrences(of: " ", with: "")
|
|
||||||
.lowercased()
|
|
||||||
{
|
|
||||||
alertType = RecipeCreationError.DUPLICATE
|
|
||||||
alertAction = {}
|
|
||||||
presentAlert = true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func uploadNewRecipe() {
|
|
||||||
print("Uploading new recipe.")
|
|
||||||
waitingForUpload = true
|
|
||||||
createRecipe()
|
|
||||||
guard recipeValid() else { return }
|
|
||||||
let request = RequestWrapper.customRequest(
|
|
||||||
method: .POST,
|
|
||||||
path: .NEW_RECIPE,
|
|
||||||
headerFields: [
|
|
||||||
HeaderField.accept(value: .JSON),
|
|
||||||
HeaderField.ocsRequest(value: true),
|
|
||||||
HeaderField.contentType(value: .JSON)
|
|
||||||
],
|
|
||||||
body: JSONEncoder.safeEncode(self.recipe)
|
|
||||||
)
|
|
||||||
sendRequest(request)
|
|
||||||
dismissEditView()
|
|
||||||
}
|
|
||||||
|
|
||||||
func uploadEditedRecipe() {
|
|
||||||
waitingForUpload = true
|
|
||||||
print("Uploading changed recipe.")
|
|
||||||
guard let recipeId = Int(recipe.id) else { return }
|
|
||||||
createRecipe()
|
|
||||||
let request = RequestWrapper.customRequest(
|
|
||||||
method: .PUT,
|
|
||||||
path: .RECIPE_DETAIL(recipeId: recipeId),
|
|
||||||
headerFields: [
|
|
||||||
HeaderField.accept(value: .JSON),
|
|
||||||
HeaderField.ocsRequest(value: true),
|
|
||||||
HeaderField.contentType(value: .JSON)
|
|
||||||
],
|
|
||||||
body: JSONEncoder.safeEncode(self.recipe)
|
|
||||||
)
|
|
||||||
sendRequest(request)
|
|
||||||
dismissEditView()
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteRecipe() {
|
|
||||||
guard let recipeId = Int(recipe.id) else { return }
|
|
||||||
let request = RequestWrapper.customRequest(
|
|
||||||
method: .DELETE,
|
|
||||||
path: .RECIPE_DETAIL(recipeId: recipeId),
|
|
||||||
headerFields: [
|
|
||||||
HeaderField.accept(value: .JSON),
|
|
||||||
HeaderField.ocsRequest(value: true)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
sendRequest(request)
|
|
||||||
if let recipeIdInt = Int(recipe.id) {
|
|
||||||
viewModel.deleteRecipe(withId: recipeIdInt, categoryName: recipe.recipeCategory)
|
|
||||||
}
|
|
||||||
dismissEditView()
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendRequest(_ request: RequestWrapper) {
|
|
||||||
Task {
|
|
||||||
guard let apiController = viewModel.apiController else { return }
|
|
||||||
let (data, _): (Data?, Error?) = await apiController.sendDataRequest(request)
|
|
||||||
guard let data = data else { return }
|
|
||||||
do {
|
|
||||||
let error = try JSONDecoder().decode(ServerMessage.self, from: data)
|
|
||||||
// TODO: Better error handling (Show error to user!)
|
|
||||||
} catch {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func dismissEditView() {
|
|
||||||
Task {
|
|
||||||
await self.viewModel.loadCategoryList(needsUpdate: true)
|
|
||||||
await self.viewModel.loadRecipeList(categoryName: self.recipe.recipeCategory, needsUpdate: true)
|
|
||||||
}
|
|
||||||
self.isPresented = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func prepareView() {
|
|
||||||
if let prepTime = recipe.prepTime {
|
|
||||||
prepDuration.fromPTString(prepTime)
|
|
||||||
}
|
|
||||||
if let cookTime = recipe.cookTime {
|
|
||||||
cookDuration.fromPTString(cookTime)
|
|
||||||
}
|
|
||||||
if let totalTime = recipe.totalTime {
|
|
||||||
totalDuration.fromPTString(totalTime)
|
|
||||||
}
|
|
||||||
self.keywords = recipe.getKeywordsArray()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user