Recipe creation, editing and deletion are now supported
This commit is contained in:
@@ -13,6 +13,10 @@ struct Category: Codable {
|
||||
let recipe_count: Int
|
||||
}
|
||||
|
||||
extension Category: Identifiable, Hashable {
|
||||
var id: String { name }
|
||||
}
|
||||
|
||||
struct Recipe: Codable {
|
||||
let name: String
|
||||
let keywords: String
|
||||
@@ -23,6 +27,10 @@ struct Recipe: Codable {
|
||||
let recipe_id: Int
|
||||
}
|
||||
|
||||
extension Recipe: Identifiable, Hashable {
|
||||
var id: String { name }
|
||||
}
|
||||
|
||||
struct RecipeDetail: Codable {
|
||||
var name: String
|
||||
var keywords: String
|
||||
@@ -140,3 +148,9 @@ struct MetaData: Codable {
|
||||
let status: String
|
||||
let statuscode: Int
|
||||
}
|
||||
|
||||
|
||||
// Networking
|
||||
struct ServerMessage: Decodable {
|
||||
let msg: String
|
||||
}
|
||||
|
||||
@@ -11,11 +11,30 @@ extension Date {
|
||||
static var zero: Date {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "HH:mm"
|
||||
|
||||
if let date = dateFormatter.date(from:"00:00") {
|
||||
return date
|
||||
} else {
|
||||
return Date()
|
||||
}
|
||||
}
|
||||
|
||||
static func toPTRepresentation(date: Date) -> String? {
|
||||
// PT0H18M0S
|
||||
let dateComponents = Calendar.current.dateComponents([.hour, .minute], from: date)
|
||||
if let hour = dateComponents.hour, let minute = dateComponents.minute {
|
||||
return "PT\(hour)H\(minute)M0S"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func fromPTRepresentation(_ representation: String) -> Date {
|
||||
let (hour, minute) = DateFormatter.stringToComponents(duration: representation)
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "HH:mm"
|
||||
if let date = dateFormatter.date(from:"\(hour):\(minute)") {
|
||||
return date
|
||||
} else {
|
||||
return Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,29 +13,45 @@ extension Formatter {
|
||||
formatter.unitsStyle = .positional
|
||||
return formatter
|
||||
}()
|
||||
}
|
||||
|
||||
func formatDate(duration: String) -> String {
|
||||
var duration = duration
|
||||
if duration.hasPrefix("PT") { duration.removeFirst(2) }
|
||||
var hour: Int = 0, minute: Int = 0
|
||||
if let index = duration.firstIndex(of: "H") {
|
||||
hour = Int(duration[..<index]) ?? 0
|
||||
duration.removeSubrange(...index)
|
||||
}
|
||||
if let index = duration.firstIndex(of: "M") {
|
||||
minute = Int(duration[..<index]) ?? 0
|
||||
duration.removeSubrange(...index)
|
||||
}
|
||||
|
||||
if hour == 0 && minute != 0 {
|
||||
return "\(minute)min"
|
||||
static func formatDate(duration: String) -> String {
|
||||
var duration = duration
|
||||
if duration.hasPrefix("PT") { duration.removeFirst(2) }
|
||||
var hour: Int = 0, minute: Int = 0
|
||||
if let index = duration.firstIndex(of: "H") {
|
||||
hour = Int(duration[..<index]) ?? 0
|
||||
duration.removeSubrange(...index)
|
||||
}
|
||||
if let index = duration.firstIndex(of: "M") {
|
||||
minute = Int(duration[..<index]) ?? 0
|
||||
duration.removeSubrange(...index)
|
||||
}
|
||||
|
||||
if hour == 0 && minute != 0 {
|
||||
return "\(minute)min"
|
||||
}
|
||||
if hour != 0 && minute == 0 {
|
||||
return "\(hour)h"
|
||||
}
|
||||
if hour != 0 && minute != 0 {
|
||||
return "\(hour)h \(minute)"
|
||||
}
|
||||
return "--"
|
||||
}
|
||||
if hour != 0 && minute == 0 {
|
||||
return "\(hour)h"
|
||||
|
||||
static func stringToComponents(duration: String) -> (Int, Int) {
|
||||
var duration = duration
|
||||
if duration.hasPrefix("PT") { duration.removeFirst(2) }
|
||||
var hour: Int = 0, minute: Int = 0
|
||||
if let index = duration.firstIndex(of: "H") {
|
||||
hour = Int(duration[..<index]) ?? 0
|
||||
duration.removeSubrange(...index)
|
||||
}
|
||||
if let index = duration.firstIndex(of: "M") {
|
||||
minute = Int(duration[..<index]) ?? 0
|
||||
duration.removeSubrange(...index)
|
||||
}
|
||||
|
||||
return (hour, minute)
|
||||
}
|
||||
if hour != 0 && minute != 0 {
|
||||
return "\(hour)h \(minute)"
|
||||
}
|
||||
return "--"
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ enum RequestPath {
|
||||
case CATEGORIES,
|
||||
RECIPE_LIST(categoryName: String),
|
||||
RECIPE_DETAIL(recipeId: Int),
|
||||
NEW_RECIPE,
|
||||
IMAGE(recipeId: Int, thumb: Bool)
|
||||
|
||||
case LOGINV2REQ,
|
||||
@@ -30,6 +31,7 @@ enum RequestPath {
|
||||
case .RECIPE_LIST(categoryName: let name): return "category/\(name)"
|
||||
case .RECIPE_DETAIL(recipeId: let recipeId): return "recipes/\(recipeId)"
|
||||
case .IMAGE(recipeId: let recipeId, thumb: let thumb): return "recipes/\(recipeId)/image?size=\(thumb ? "thumb" : "full")"
|
||||
case .NEW_RECIPE: return "recipes"
|
||||
|
||||
case .LOGINV2REQ: return "/index.php/login/v2"
|
||||
case .CUSTOM(path: let path): return path
|
||||
|
||||
@@ -19,7 +19,7 @@ struct Nextcloud_Cookbook_iOS_ClientApp: App {
|
||||
} else {
|
||||
MainView(viewModel: mainViewModel, userSettings: userSettings)
|
||||
.onAppear {
|
||||
mainViewModel.apiInterface = APIController(userSettings: userSettings)
|
||||
mainViewModel.apiController = APIController(userSettings: userSettings)
|
||||
}
|
||||
}
|
||||
}.transition(.slide)
|
||||
|
||||
@@ -16,7 +16,7 @@ import SwiftUI
|
||||
private var imageCache: [Int: RecipeImage] = [:]
|
||||
|
||||
let dataStore: DataStore
|
||||
var apiInterface: APIController? = nil
|
||||
var apiController: APIController? = nil
|
||||
|
||||
/// The path of an image in storage
|
||||
private var localImagePath: (Int, Bool) -> (String) = { recipeId, thumb in
|
||||
@@ -43,6 +43,7 @@ import SwiftUI
|
||||
) {
|
||||
self.categories = categoryList
|
||||
}
|
||||
print(self.categories)
|
||||
}
|
||||
|
||||
/// Try to load the recipe list from store or the server.
|
||||
@@ -56,7 +57,9 @@ import SwiftUI
|
||||
needsUpdate: needsUpdate
|
||||
) {
|
||||
recipes[categoryName] = recipeList
|
||||
print(recipeList)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Try to load the recipe details from cache. If not found, try to load from store or the server.
|
||||
@@ -116,8 +119,8 @@ import SwiftUI
|
||||
print("loadImage(recipeId: \(recipeId), thumb: \(thumb), needsUpdate: \(needsUpdate))")
|
||||
// If the image needs an update, request it from the server and overwrite the stored image
|
||||
if needsUpdate {
|
||||
guard let apiInterface = apiInterface else { return nil }
|
||||
if let data = await apiInterface.imageDataFromServer(recipeId: recipeId, thumb: thumb) {
|
||||
guard let apiController = apiController else { return nil }
|
||||
if let data = await apiController.imageDataFromServer(recipeId: recipeId, thumb: thumb) {
|
||||
guard let image = UIImage(data: data) else {
|
||||
imageCache[recipeId] = RecipeImage(imageExists: false)
|
||||
return nil
|
||||
@@ -154,8 +157,8 @@ import SwiftUI
|
||||
|
||||
// Try to load from the server. Store if successfull.
|
||||
print("Attempting to load image from server ...")
|
||||
guard let apiInterface = apiInterface else { return nil }
|
||||
if let data = await apiInterface.imageDataFromServer(recipeId: recipeId, thumb: thumb) {
|
||||
guard let apiController = apiController else { return nil }
|
||||
if let data = await apiController.imageDataFromServer(recipeId: recipeId, thumb: thumb) {
|
||||
print("Image data received.")
|
||||
// Create empty RecipeImage for each recipe even if no image found, so that further server requests are only sent if explicitly requested.
|
||||
guard let image = UIImage(data: data) else {
|
||||
@@ -190,9 +193,9 @@ extension MainViewModel {
|
||||
print("Data found locally.")
|
||||
return data
|
||||
} else {
|
||||
guard let apiInterface = apiInterface else { return nil }
|
||||
guard let apiController = apiController else { return nil }
|
||||
let request = RequestWrapper.jsonGetRequest(path: networkPath)
|
||||
let (data, error): (T?, Error?) = await apiInterface.sendDataRequest(request)
|
||||
let (data, error): (T?, Error?) = await apiController.sendDataRequest(request)
|
||||
print(error as Any)
|
||||
if let data = data {
|
||||
await dataStore.save(data: data, toPath: localPath)
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
//
|
||||
// CategoryCardView.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 15.09.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct CategoryCardView: View {
|
||||
@State var category: Category
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Image("cookbook-category")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.overlay(
|
||||
VStack {
|
||||
Spacer()
|
||||
Text(category.name == "*" ? "Other" : category.name)
|
||||
.font(.headline)
|
||||
.lineLimit(2)
|
||||
.foregroundStyle(.white)
|
||||
.padding()
|
||||
}
|
||||
)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
|
||||
|
||||
|
||||
struct RecipeBookView: View {
|
||||
struct CategoryDetailView: View {
|
||||
@State var categoryName: String
|
||||
@State var searchText: String = ""
|
||||
@ObservedObject var viewModel: MainViewModel
|
||||
@@ -19,10 +19,13 @@ struct RecipeBookView: View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
LazyVStack {
|
||||
ForEach(recipesFiltered(), id: \.recipe_id) { recipe in
|
||||
NavigationLink(destination: RecipeDetailView(viewModel: viewModel, recipe: recipe)) {
|
||||
NavigationLink() {
|
||||
RecipeDetailView(viewModel: viewModel, recipe: recipe).id(recipe.recipe_id)
|
||||
} label: {
|
||||
RecipeCardView(viewModel: viewModel, recipe: recipe)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
71
Nextcloud Cookbook iOS Client/Views/CategoryPickerView.swift
Normal file
71
Nextcloud Cookbook iOS Client/Views/CategoryPickerView.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// CategoryPickerView.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 03.10.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
|
||||
|
||||
struct CategoryPickerView: View {
|
||||
@State var title: String
|
||||
@State var searchSuggestions: [String]
|
||||
@Binding var selection: String
|
||||
@State var searchText: String = ""
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
TextField(title, text: $searchText)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding()
|
||||
List {
|
||||
if searchText != "" {
|
||||
HStack {
|
||||
if selection.contains(searchText) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
}
|
||||
Text(searchText)
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.foregroundStyle(Color("backgroundHighlight"))
|
||||
)
|
||||
.onTapGesture {
|
||||
selection = searchText
|
||||
}
|
||||
}
|
||||
ForEach(suggestionsFiltered(), id: \.self) { suggestion in
|
||||
HStack {
|
||||
if selection.contains(suggestion) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
}
|
||||
Text(suggestion)
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.foregroundStyle(Color("backgroundHighlight"))
|
||||
)
|
||||
.onTapGesture {
|
||||
selection = suggestion
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.navigationTitle(title)
|
||||
}
|
||||
|
||||
func suggestionsFiltered() -> [String] {
|
||||
guard searchText != "" else { return searchSuggestions }
|
||||
return searchSuggestions.filter { suggestion in
|
||||
suggestion.lowercased().contains(searchText.lowercased())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,74 +8,120 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct Keyword: Identifiable {
|
||||
let id = UUID()
|
||||
let name: String
|
||||
|
||||
init(_ name: String) {
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
struct KeywordPickerView: View {
|
||||
@State var title: String
|
||||
@State var searchSuggestions: [String]
|
||||
@State var searchSuggestions: [Keyword]
|
||||
@Binding var selection: [String]
|
||||
@State var searchText: String = ""
|
||||
var columns: [GridItem] = [GridItem(.adaptive(minimum: 120), spacing: 0)]
|
||||
var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 5)]
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
VStack(alignment: .leading) {
|
||||
TextField(title, text: $searchText)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding()
|
||||
LazyVGrid(columns: columns, spacing: 5) {
|
||||
if searchText != "" {
|
||||
HStack {
|
||||
if selection.contains(searchText) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
ScrollView {
|
||||
LazyVGrid(columns: columns, spacing: 5) {
|
||||
if searchText != "" {
|
||||
KeywordItemView(
|
||||
keyword: Keyword(searchText),
|
||||
isSelected: selection.contains(searchText)
|
||||
) { keyword in
|
||||
if selection.contains(keyword.name) {
|
||||
selection.removeAll(where: { s in
|
||||
s == keyword.name ? true : false
|
||||
})
|
||||
searchSuggestions.removeAll(where: { s in
|
||||
s.name == keyword.name ? true : false
|
||||
})
|
||||
} else {
|
||||
selection.append(keyword.name)
|
||||
}
|
||||
}
|
||||
Text(searchText)
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.foregroundStyle(Color("backgroundHighlight"))
|
||||
)
|
||||
.onTapGesture {
|
||||
if selection.contains(searchText) {
|
||||
selection.removeAll(where: { s in
|
||||
s == searchText ? true : false
|
||||
})
|
||||
} else {
|
||||
selection.append(searchText)
|
||||
searchSuggestions.append(searchText)
|
||||
ForEach(suggestionsFiltered(), id: \.id) { suggestion in
|
||||
KeywordItemView(
|
||||
keyword: suggestion,
|
||||
isSelected: selection.contains(suggestion.name)
|
||||
) { keyword in
|
||||
if selection.contains(keyword.name) {
|
||||
selection.removeAll(where: { s in
|
||||
s == keyword.name ? true : false
|
||||
})
|
||||
} else {
|
||||
selection.append(keyword.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ForEach(suggestionsFiltered(), id: \.self) { suggestion in
|
||||
HStack {
|
||||
if selection.contains(suggestion) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
}
|
||||
Text(suggestion)
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.foregroundStyle(Color("backgroundHighlight"))
|
||||
)
|
||||
.onTapGesture {
|
||||
if selection.contains(suggestion) {
|
||||
selection.removeAll(where: { s in
|
||||
s == suggestion ? true : false
|
||||
})
|
||||
} else {
|
||||
selection.append(suggestion)
|
||||
Divider().padding()
|
||||
HStack {
|
||||
Text("Selected keywords:")
|
||||
.font(.headline)
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
LazyVGrid(columns: columns, spacing: 5) {
|
||||
ForEach(selection, id: \.self) { suggestion in
|
||||
KeywordItemView(
|
||||
keyword: Keyword(suggestion),
|
||||
isSelected: true
|
||||
) { keyword in
|
||||
if selection.contains(keyword.name) {
|
||||
selection.removeAll(where: { s in
|
||||
s == keyword.name ? true : false
|
||||
})
|
||||
} else {
|
||||
selection.append(keyword.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.navigationTitle(title)
|
||||
}
|
||||
|
||||
func suggestionsFiltered() -> [String] {
|
||||
func suggestionsFiltered() -> [Keyword] {
|
||||
guard searchText != "" else { return searchSuggestions }
|
||||
return searchSuggestions.filter { suggestion in
|
||||
suggestion.lowercased().contains(searchText.lowercased())
|
||||
suggestion.name.lowercased().contains(searchText.lowercased())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct KeywordItemView: View {
|
||||
var keyword: Keyword
|
||||
var isSelected: Bool
|
||||
var tapped: (Keyword) -> ()
|
||||
var body: some View {
|
||||
HStack {
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
}
|
||||
Text(keyword.name)
|
||||
.lineLimit(2)
|
||||
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.foregroundStyle(Color("backgroundHighlight"))
|
||||
)
|
||||
.onTapGesture {
|
||||
tapped(keyword)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,35 +7,29 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
struct MainView: View {
|
||||
@ObservedObject var viewModel: MainViewModel
|
||||
@ObservedObject var userSettings: UserSettings
|
||||
|
||||
@State private var selectedCategory: Category? = nil
|
||||
@State private var showEditView: Bool = false
|
||||
var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 0)]
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
LazyVGrid(columns: columns, spacing: 0) {
|
||||
ForEach(viewModel.categories, id: \.name) { category in
|
||||
if category.recipe_count != 0 {
|
||||
NavigationLink(
|
||||
destination: RecipeBookView(
|
||||
categoryName: category.name,
|
||||
viewModel: viewModel
|
||||
)
|
||||
) {
|
||||
CategoryCardView(category: category)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
NavigationSplitView {
|
||||
List(viewModel.categories, selection: $selectedCategory) { category in
|
||||
if category.recipe_count != 0 {
|
||||
NavigationLink(value: category) {
|
||||
HStack(alignment: .center) {
|
||||
Image(systemName: "book.closed.fill")
|
||||
Text(category.name)
|
||||
.font(.system(size: 20, weight: .light, design: .serif))
|
||||
.italic()
|
||||
}.padding(7)
|
||||
}
|
||||
}
|
||||
}
|
||||
/*.navigationDestination(isPresented: $showEditView) {
|
||||
RecipeEditView()
|
||||
}*/
|
||||
.navigationTitle("Cookbooks")
|
||||
.toolbar {
|
||||
Menu {
|
||||
@@ -68,9 +62,20 @@ struct MainView: View {
|
||||
Image(systemName: "gearshape")
|
||||
}
|
||||
}
|
||||
.background(
|
||||
NavigationLink(destination: RecipeEditView(), isActive: $showEditView) { EmptyView() }
|
||||
)
|
||||
.navigationDestination(isPresented: $showEditView) {
|
||||
RecipeEditView(viewModel: viewModel, isPresented: $showEditView)
|
||||
}
|
||||
|
||||
} detail: {
|
||||
NavigationStack {
|
||||
if let category = selectedCategory {
|
||||
CategoryDetailView(
|
||||
categoryName: category.name,
|
||||
viewModel: viewModel
|
||||
)
|
||||
.id(category.id) // Workaround: This is needed to update the detail view when the selection changes
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.tint(.nextcloudBlue)
|
||||
|
||||
@@ -16,6 +16,8 @@ struct RecipeDetailView: View {
|
||||
@State var recipeImage: UIImage?
|
||||
@State var showTitle: Bool = false
|
||||
@State var isDownloaded: Bool? = nil
|
||||
@State private var presentEditView: Bool = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading) {
|
||||
@@ -69,6 +71,18 @@ struct RecipeDetailView: View {
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle(showTitle ? recipe.name : "")
|
||||
.toolbar {
|
||||
if let recipeDetail = recipeDetail {
|
||||
NavigationLink {
|
||||
RecipeEditView(viewModel: viewModel, recipe: recipeDetail, isPresented: $presentEditView, uploadNew: false).tag("RecipeEditView")
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "pencil")
|
||||
Text("Edit")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
recipeDetail = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id)
|
||||
recipeImage = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: false)
|
||||
@@ -78,7 +92,6 @@ struct RecipeDetailView: View {
|
||||
recipeDetail = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id, needsUpdate: true)
|
||||
recipeImage = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: false, needsUpdate: true)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +104,7 @@ struct RecipeDurationSection: View {
|
||||
if let prepTime = recipeDetail.prepTime {
|
||||
VStack {
|
||||
SecondaryLabel(text: "Prep time")
|
||||
Text(formatDate(duration: prepTime))
|
||||
Text(DateFormatter.formatDate(duration: prepTime))
|
||||
.lineLimit(1)
|
||||
}.padding()
|
||||
}
|
||||
@@ -99,7 +112,7 @@ struct RecipeDurationSection: View {
|
||||
if let cookTime = recipeDetail.cookTime {
|
||||
VStack {
|
||||
SecondaryLabel(text: "Cook time")
|
||||
Text(formatDate(duration: cookTime))
|
||||
Text(DateFormatter.formatDate(duration: cookTime))
|
||||
.lineLimit(1)
|
||||
}.padding()
|
||||
}
|
||||
@@ -107,7 +120,7 @@ struct RecipeDurationSection: View {
|
||||
if let totalTime = recipeDetail.totalTime {
|
||||
VStack {
|
||||
SecondaryLabel(text: "Total time")
|
||||
Text(formatDate(duration: totalTime))
|
||||
Text(DateFormatter.formatDate(duration: totalTime))
|
||||
.lineLimit(1)
|
||||
}.padding()
|
||||
}
|
||||
|
||||
@@ -11,33 +11,56 @@ import PhotosUI
|
||||
|
||||
|
||||
struct RecipeEditView: View {
|
||||
@ObservedObject var viewModel: MainViewModel
|
||||
@State var recipe: RecipeDetail = RecipeDetail()
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
@State var image: PhotosPickerItem? = nil
|
||||
@State var times = [Date.zero, Date.zero, Date.zero]
|
||||
@State var uploadNew: Bool = true
|
||||
@State var searchText: String = ""
|
||||
@State var keywords: [String] = []
|
||||
|
||||
init(recipe: RecipeDetail? = nil) {
|
||||
self.recipe = recipe ?? RecipeDetail()
|
||||
}
|
||||
@State private var alertMessage: String = ""
|
||||
@State private var presentAlert: Bool = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
TextField("Title", text: $recipe.name)
|
||||
TextField("Description", text: $recipe.description)
|
||||
Section {
|
||||
TextEditor(text: $recipe.description)
|
||||
} header: {
|
||||
Text("Description")
|
||||
}
|
||||
/*
|
||||
PhotosPicker(selection: $image, matching: .images, photoLibrary: .shared()) {
|
||||
Image(systemName: "photo")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
|
||||
*/
|
||||
Section() {
|
||||
NavigationLink(recipe.recipeCategory == "" ? "Category" : "Category: \(recipe.recipeCategory)") {
|
||||
CategoryPickerView(title: "Category", searchSuggestions: [], selection: $recipe.recipeCategory)
|
||||
}
|
||||
NavigationLink("Keywords") {
|
||||
KeywordPickerView(title: "Keyword", searchSuggestions: [], selection: $keywords)
|
||||
KeywordPickerView(
|
||||
title: "Keywords",
|
||||
searchSuggestions: [
|
||||
Keyword("Hauptspeisen"),
|
||||
Keyword("Lecker"),
|
||||
Keyword("Trinken"),
|
||||
Keyword("Essen"),
|
||||
Keyword("Nachspeisen"),
|
||||
Keyword("Futter"),
|
||||
Keyword("Apfel"),
|
||||
Keyword("test")
|
||||
],
|
||||
selection: $keywords
|
||||
)
|
||||
}
|
||||
} header: {
|
||||
Text("Keywords")
|
||||
Text("Discoverability")
|
||||
} footer: {
|
||||
ScrollView(.horizontal) {
|
||||
HStack {
|
||||
@@ -63,7 +86,131 @@ struct RecipeEditView: View {
|
||||
EditableListSection(title: "Ingredients", items: $recipe.recipeIngredient)
|
||||
EditableListSection(title: "Tools", items: $recipe.tool)
|
||||
EditableListSection(title: "Instructions", items: $recipe.recipeInstructions)
|
||||
}.navigationTitle("New Recipe")
|
||||
}.navigationTitle("Edit your recipe")
|
||||
.toolbar {
|
||||
Menu {
|
||||
Button {
|
||||
print("Delete recipe.")
|
||||
deleteRecipe()
|
||||
self.isPresented = false
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
.foregroundStyle(.red)
|
||||
Text("Delete recipe")
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
}
|
||||
Button() {
|
||||
if uploadNew {
|
||||
uploadNewRecipe()
|
||||
} else {
|
||||
uploadEditedRecipe()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "icloud.and.arrow.up")
|
||||
Text(uploadNew ? "Upload" : "Update")
|
||||
.bold()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if uploadNew { return }
|
||||
if let prepTime = recipe.prepTime {
|
||||
self.times[0] = Date.fromPTRepresentation(prepTime)
|
||||
}
|
||||
if let cookTime = recipe.cookTime {
|
||||
self.times[1] = Date.fromPTRepresentation(cookTime)
|
||||
}
|
||||
if let totalTime = recipe.totalTime {
|
||||
self.times[2] = Date.fromPTRepresentation(totalTime)
|
||||
}
|
||||
|
||||
for keyword in recipe.keywords.components(separatedBy: ",") {
|
||||
keywords.append(keyword)
|
||||
}
|
||||
}
|
||||
.alert(alertMessage, isPresented: $presentAlert) {
|
||||
Button("Ok", role: .cancel) {
|
||||
self.isPresented = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createRecipe() {
|
||||
print(self.recipe.name)
|
||||
if let date = Date.toPTRepresentation(date: times[0]) {
|
||||
self.recipe.prepTime = date
|
||||
}
|
||||
if let date = Date.toPTRepresentation(date: times[1]) {
|
||||
self.recipe.cookTime = date
|
||||
}
|
||||
if let date = Date.toPTRepresentation(date: times[2]) {
|
||||
self.recipe.totalTime = date
|
||||
}
|
||||
self.recipe.keywords = self.keywords.joined(separator: ",")
|
||||
}
|
||||
|
||||
func uploadNewRecipe() {
|
||||
print("Uploading new recipe.")
|
||||
createRecipe()
|
||||
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)
|
||||
}
|
||||
|
||||
func uploadEditedRecipe() {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
alertMessage = error.msg
|
||||
presentAlert = true
|
||||
} catch {
|
||||
self.isPresented = false
|
||||
await self.viewModel.loadRecipeList(categoryName: self.recipe.recipeCategory, needsUpdate: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user