Recipe creation, editing and deletion are now supported
This commit is contained in:
@@ -19,7 +19,6 @@
|
|||||||
A70171B12AB211DF00064C43 /* CustomError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171B02AB211DF00064C43 /* CustomError.swift */; };
|
A70171B12AB211DF00064C43 /* CustomError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171B02AB211DF00064C43 /* CustomError.swift */; };
|
||||||
A70171B42AB2122900064C43 /* NetworkRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171B32AB2122900064C43 /* NetworkRequests.swift */; };
|
A70171B42AB2122900064C43 /* NetworkRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171B32AB2122900064C43 /* NetworkRequests.swift */; };
|
||||||
A70171B92AB399FB00064C43 /* DateFormatterExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171B82AB399FB00064C43 /* DateFormatterExtension.swift */; };
|
A70171B92AB399FB00064C43 /* DateFormatterExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171B82AB399FB00064C43 /* DateFormatterExtension.swift */; };
|
||||||
A70171BC2AB4983500064C43 /* CategoryCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BB2AB4983500064C43 /* CategoryCardView.swift */; };
|
|
||||||
A70171BE2AB4987900064C43 /* CategoryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BD2AB4987900064C43 /* CategoryDetailView.swift */; };
|
A70171BE2AB4987900064C43 /* CategoryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BD2AB4987900064C43 /* CategoryDetailView.swift */; };
|
||||||
A70171C02AB498A900064C43 /* RecipeDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BF2AB498A900064C43 /* RecipeDetailView.swift */; };
|
A70171C02AB498A900064C43 /* RecipeDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BF2AB498A900064C43 /* RecipeDetailView.swift */; };
|
||||||
A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C12AB498C600064C43 /* RecipeCardView.swift */; };
|
A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C12AB498C600064C43 /* RecipeCardView.swift */; };
|
||||||
@@ -34,6 +33,7 @@
|
|||||||
A70D7CA12AC73CA800D53DBF /* RecipeEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */; };
|
A70D7CA12AC73CA800D53DBF /* RecipeEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */; };
|
||||||
A70D7CA32AC74B3B00D53DBF /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */; };
|
A70D7CA32AC74B3B00D53DBF /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */; };
|
||||||
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 */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -70,7 +70,6 @@
|
|||||||
A70171B02AB211DF00064C43 /* CustomError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomError.swift; sourceTree = "<group>"; };
|
A70171B02AB211DF00064C43 /* CustomError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomError.swift; sourceTree = "<group>"; };
|
||||||
A70171B32AB2122900064C43 /* NetworkRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkRequests.swift; sourceTree = "<group>"; };
|
A70171B32AB2122900064C43 /* NetworkRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkRequests.swift; sourceTree = "<group>"; };
|
||||||
A70171B82AB399FB00064C43 /* DateFormatterExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatterExtension.swift; sourceTree = "<group>"; };
|
A70171B82AB399FB00064C43 /* DateFormatterExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatterExtension.swift; sourceTree = "<group>"; };
|
||||||
A70171BB2AB4983500064C43 /* CategoryCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryCardView.swift; sourceTree = "<group>"; };
|
|
||||||
A70171BD2AB4987900064C43 /* CategoryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryDetailView.swift; sourceTree = "<group>"; };
|
A70171BD2AB4987900064C43 /* CategoryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryDetailView.swift; sourceTree = "<group>"; };
|
||||||
A70171BF2AB498A900064C43 /* RecipeDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeDetailView.swift; sourceTree = "<group>"; };
|
A70171BF2AB498A900064C43 /* RecipeDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeDetailView.swift; sourceTree = "<group>"; };
|
||||||
A70171C12AB498C600064C43 /* RecipeCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeCardView.swift; sourceTree = "<group>"; };
|
A70171C12AB498C600064C43 /* RecipeCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeCardView.swift; sourceTree = "<group>"; };
|
||||||
@@ -85,6 +84,7 @@
|
|||||||
A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeEditView.swift; sourceTree = "<group>"; };
|
A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeEditView.swift; sourceTree = "<group>"; };
|
||||||
A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = "<group>"; };
|
A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; 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>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -196,7 +196,6 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A70171832AA8E71900064C43 /* MainView.swift */,
|
A70171832AA8E71900064C43 /* MainView.swift */,
|
||||||
A70171BB2AB4983500064C43 /* CategoryCardView.swift */,
|
|
||||||
A70171BD2AB4987900064C43 /* CategoryDetailView.swift */,
|
A70171BD2AB4987900064C43 /* CategoryDetailView.swift */,
|
||||||
A70171C12AB498C600064C43 /* RecipeCardView.swift */,
|
A70171C12AB498C600064C43 /* RecipeCardView.swift */,
|
||||||
A70171BF2AB498A900064C43 /* RecipeDetailView.swift */,
|
A70171BF2AB498A900064C43 /* RecipeDetailView.swift */,
|
||||||
@@ -204,6 +203,7 @@
|
|||||||
A70171C82AB4CBB400064C43 /* OnboardingView.swift */,
|
A70171C82AB4CBB400064C43 /* OnboardingView.swift */,
|
||||||
A70171CC2AB501B100064C43 /* SettingsView.swift */,
|
A70171CC2AB501B100064C43 /* SettingsView.swift */,
|
||||||
A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */,
|
A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */,
|
||||||
|
A7F3F8E92ACC221C0076C227 /* CategoryPickerView.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -366,11 +366,11 @@
|
|||||||
A70171AF2AB2116B00064C43 /* NetworkHandler.swift in Sources */,
|
A70171AF2AB2116B00064C43 /* NetworkHandler.swift in Sources */,
|
||||||
A70171B42AB2122900064C43 /* NetworkRequests.swift in Sources */,
|
A70171B42AB2122900064C43 /* NetworkRequests.swift in Sources */,
|
||||||
A70171B92AB399FB00064C43 /* DateFormatterExtension.swift in Sources */,
|
A70171B92AB399FB00064C43 /* DateFormatterExtension.swift in Sources */,
|
||||||
A70171BC2AB4983500064C43 /* CategoryCardView.swift in Sources */,
|
|
||||||
A70171BE2AB4987900064C43 /* CategoryDetailView.swift in Sources */,
|
A70171BE2AB4987900064C43 /* CategoryDetailView.swift in Sources */,
|
||||||
A70171C62AB4C43A00064C43 /* DataModels.swift in Sources */,
|
A70171C62AB4C43A00064C43 /* DataModels.swift in Sources */,
|
||||||
A7F3F8E82ACBFC760076C227 /* KeywordPickerView.swift in Sources */,
|
A7F3F8E82ACBFC760076C227 /* KeywordPickerView.swift in Sources */,
|
||||||
A70171C02AB498A900064C43 /* RecipeDetailView.swift in Sources */,
|
A70171C02AB498A900064C43 /* RecipeDetailView.swift in Sources */,
|
||||||
|
A7F3F8EA2ACC221C0076C227 /* CategoryPickerView.swift in Sources */,
|
||||||
A70171CD2AB501B100064C43 /* SettingsView.swift in Sources */,
|
A70171CD2AB501B100064C43 /* SettingsView.swift in Sources */,
|
||||||
A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */,
|
A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */,
|
||||||
A70171842AA8E71900064C43 /* MainView.swift in Sources */,
|
A70171842AA8E71900064C43 /* MainView.swift in Sources */,
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ struct Category: Codable {
|
|||||||
let recipe_count: Int
|
let recipe_count: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Category: Identifiable, Hashable {
|
||||||
|
var id: String { name }
|
||||||
|
}
|
||||||
|
|
||||||
struct Recipe: Codable {
|
struct Recipe: Codable {
|
||||||
let name: String
|
let name: String
|
||||||
let keywords: String
|
let keywords: String
|
||||||
@@ -23,6 +27,10 @@ struct Recipe: Codable {
|
|||||||
let recipe_id: Int
|
let recipe_id: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Recipe: Identifiable, Hashable {
|
||||||
|
var id: String { name }
|
||||||
|
}
|
||||||
|
|
||||||
struct RecipeDetail: Codable {
|
struct RecipeDetail: Codable {
|
||||||
var name: String
|
var name: String
|
||||||
var keywords: String
|
var keywords: String
|
||||||
@@ -140,3 +148,9 @@ struct MetaData: Codable {
|
|||||||
let status: String
|
let status: String
|
||||||
let statuscode: Int
|
let statuscode: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Networking
|
||||||
|
struct ServerMessage: Decodable {
|
||||||
|
let msg: String
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,11 +11,30 @@ extension Date {
|
|||||||
static var zero: Date {
|
static var zero: Date {
|
||||||
let dateFormatter = DateFormatter()
|
let dateFormatter = DateFormatter()
|
||||||
dateFormatter.dateFormat = "HH:mm"
|
dateFormatter.dateFormat = "HH:mm"
|
||||||
|
|
||||||
if let date = dateFormatter.date(from:"00:00") {
|
if let date = dateFormatter.date(from:"00:00") {
|
||||||
return date
|
return date
|
||||||
} else {
|
} else {
|
||||||
return Date()
|
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,9 +13,8 @@ extension Formatter {
|
|||||||
formatter.unitsStyle = .positional
|
formatter.unitsStyle = .positional
|
||||||
return formatter
|
return formatter
|
||||||
}()
|
}()
|
||||||
}
|
|
||||||
|
|
||||||
func formatDate(duration: String) -> String {
|
static func formatDate(duration: String) -> String {
|
||||||
var duration = duration
|
var duration = duration
|
||||||
if duration.hasPrefix("PT") { duration.removeFirst(2) }
|
if duration.hasPrefix("PT") { duration.removeFirst(2) }
|
||||||
var hour: Int = 0, minute: Int = 0
|
var hour: Int = 0, minute: Int = 0
|
||||||
@@ -38,4 +37,21 @@ func formatDate(duration: String) -> String {
|
|||||||
return "\(hour)h \(minute)"
|
return "\(hour)h \(minute)"
|
||||||
}
|
}
|
||||||
return "--"
|
return "--"
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ enum RequestPath {
|
|||||||
case CATEGORIES,
|
case CATEGORIES,
|
||||||
RECIPE_LIST(categoryName: String),
|
RECIPE_LIST(categoryName: String),
|
||||||
RECIPE_DETAIL(recipeId: Int),
|
RECIPE_DETAIL(recipeId: Int),
|
||||||
|
NEW_RECIPE,
|
||||||
IMAGE(recipeId: Int, thumb: Bool)
|
IMAGE(recipeId: Int, thumb: Bool)
|
||||||
|
|
||||||
case LOGINV2REQ,
|
case LOGINV2REQ,
|
||||||
@@ -30,6 +31,7 @@ enum RequestPath {
|
|||||||
case .RECIPE_LIST(categoryName: let name): return "category/\(name)"
|
case .RECIPE_LIST(categoryName: let name): return "category/\(name)"
|
||||||
case .RECIPE_DETAIL(recipeId: let recipeId): return "recipes/\(recipeId)"
|
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 .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 .LOGINV2REQ: return "/index.php/login/v2"
|
||||||
case .CUSTOM(path: let path): return path
|
case .CUSTOM(path: let path): return path
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ struct Nextcloud_Cookbook_iOS_ClientApp: App {
|
|||||||
} else {
|
} else {
|
||||||
MainView(viewModel: mainViewModel, userSettings: userSettings)
|
MainView(viewModel: mainViewModel, userSettings: userSettings)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
mainViewModel.apiInterface = APIController(userSettings: userSettings)
|
mainViewModel.apiController = APIController(userSettings: userSettings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.transition(.slide)
|
}.transition(.slide)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import SwiftUI
|
|||||||
private var imageCache: [Int: RecipeImage] = [:]
|
private var imageCache: [Int: RecipeImage] = [:]
|
||||||
|
|
||||||
let dataStore: DataStore
|
let dataStore: DataStore
|
||||||
var apiInterface: APIController? = nil
|
var apiController: APIController? = nil
|
||||||
|
|
||||||
/// The path of an image in storage
|
/// The path of an image in storage
|
||||||
private var localImagePath: (Int, Bool) -> (String) = { recipeId, thumb in
|
private var localImagePath: (Int, Bool) -> (String) = { recipeId, thumb in
|
||||||
@@ -43,6 +43,7 @@ import SwiftUI
|
|||||||
) {
|
) {
|
||||||
self.categories = categoryList
|
self.categories = categoryList
|
||||||
}
|
}
|
||||||
|
print(self.categories)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to load the recipe list from store or the server.
|
/// Try to load the recipe list from store or the server.
|
||||||
@@ -56,7 +57,9 @@ import SwiftUI
|
|||||||
needsUpdate: needsUpdate
|
needsUpdate: needsUpdate
|
||||||
) {
|
) {
|
||||||
recipes[categoryName] = recipeList
|
recipes[categoryName] = recipeList
|
||||||
|
print(recipeList)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to load the recipe details from cache. If not found, try to load from store or the server.
|
/// 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))")
|
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 the image needs an update, request it from the server and overwrite the stored image
|
||||||
if needsUpdate {
|
if needsUpdate {
|
||||||
guard let apiInterface = apiInterface else { return nil }
|
guard let apiController = apiController else { return nil }
|
||||||
if let data = await apiInterface.imageDataFromServer(recipeId: recipeId, thumb: thumb) {
|
if let data = await apiController.imageDataFromServer(recipeId: recipeId, thumb: thumb) {
|
||||||
guard let image = UIImage(data: data) else {
|
guard let image = UIImage(data: data) else {
|
||||||
imageCache[recipeId] = RecipeImage(imageExists: false)
|
imageCache[recipeId] = RecipeImage(imageExists: false)
|
||||||
return nil
|
return nil
|
||||||
@@ -154,8 +157,8 @@ import SwiftUI
|
|||||||
|
|
||||||
// Try to load from the server. Store if successfull.
|
// Try to load from the server. Store if successfull.
|
||||||
print("Attempting to load image from server ...")
|
print("Attempting to load image from server ...")
|
||||||
guard let apiInterface = apiInterface else { return nil }
|
guard let apiController = apiController else { return nil }
|
||||||
if let data = await apiInterface.imageDataFromServer(recipeId: recipeId, thumb: thumb) {
|
if let data = await apiController.imageDataFromServer(recipeId: recipeId, thumb: thumb) {
|
||||||
print("Image data received.")
|
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.
|
// 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 {
|
guard let image = UIImage(data: data) else {
|
||||||
@@ -190,9 +193,9 @@ extension MainViewModel {
|
|||||||
print("Data found locally.")
|
print("Data found locally.")
|
||||||
return data
|
return data
|
||||||
} else {
|
} else {
|
||||||
guard let apiInterface = apiInterface else { return nil }
|
guard let apiController = apiController else { return nil }
|
||||||
let request = RequestWrapper.jsonGetRequest(path: networkPath)
|
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)
|
print(error as Any)
|
||||||
if let data = data {
|
if let data = data {
|
||||||
await dataStore.save(data: data, toPath: localPath)
|
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 categoryName: String
|
||||||
@State var searchText: String = ""
|
@State var searchText: String = ""
|
||||||
@ObservedObject var viewModel: MainViewModel
|
@ObservedObject var viewModel: MainViewModel
|
||||||
@@ -19,10 +19,13 @@ struct RecipeBookView: View {
|
|||||||
ScrollView(showsIndicators: false) {
|
ScrollView(showsIndicators: false) {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
ForEach(recipesFiltered(), id: \.recipe_id) { recipe in
|
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)
|
RecipeCardView(viewModel: viewModel, recipe: recipe)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.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,62 +8,80 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
struct Keyword: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
let name: String
|
||||||
|
|
||||||
|
init(_ name: String) {
|
||||||
|
self.name = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct KeywordPickerView: View {
|
struct KeywordPickerView: View {
|
||||||
@State var title: String
|
@State var title: String
|
||||||
@State var searchSuggestions: [String]
|
@State var searchSuggestions: [Keyword]
|
||||||
@Binding var selection: [String]
|
@Binding var selection: [String]
|
||||||
@State var searchText: 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 {
|
var body: some View {
|
||||||
VStack {
|
VStack(alignment: .leading) {
|
||||||
TextField(title, text: $searchText)
|
TextField(title, text: $searchText)
|
||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder)
|
||||||
.padding()
|
.padding()
|
||||||
|
ScrollView {
|
||||||
LazyVGrid(columns: columns, spacing: 5) {
|
LazyVGrid(columns: columns, spacing: 5) {
|
||||||
if searchText != "" {
|
if searchText != "" {
|
||||||
HStack {
|
KeywordItemView(
|
||||||
if selection.contains(searchText) {
|
keyword: Keyword(searchText),
|
||||||
Image(systemName: "checkmark.circle.fill")
|
isSelected: selection.contains(searchText)
|
||||||
}
|
) { keyword in
|
||||||
Text(searchText)
|
if selection.contains(keyword.name) {
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 15)
|
|
||||||
.foregroundStyle(Color("backgroundHighlight"))
|
|
||||||
)
|
|
||||||
.onTapGesture {
|
|
||||||
if selection.contains(searchText) {
|
|
||||||
selection.removeAll(where: { s in
|
selection.removeAll(where: { s in
|
||||||
s == searchText ? true : false
|
s == keyword.name ? true : false
|
||||||
|
})
|
||||||
|
searchSuggestions.removeAll(where: { s in
|
||||||
|
s.name == keyword.name ? true : false
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
selection.append(searchText)
|
selection.append(keyword.name)
|
||||||
searchSuggestions.append(searchText)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ForEach(suggestionsFiltered(), id: \.self) { suggestion in
|
ForEach(suggestionsFiltered(), id: \.id) { suggestion in
|
||||||
HStack {
|
KeywordItemView(
|
||||||
if selection.contains(suggestion) {
|
keyword: suggestion,
|
||||||
Image(systemName: "checkmark.circle.fill")
|
isSelected: selection.contains(suggestion.name)
|
||||||
}
|
) { keyword in
|
||||||
Text(suggestion)
|
if selection.contains(keyword.name) {
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 15)
|
|
||||||
.foregroundStyle(Color("backgroundHighlight"))
|
|
||||||
)
|
|
||||||
.onTapGesture {
|
|
||||||
if selection.contains(suggestion) {
|
|
||||||
selection.removeAll(where: { s in
|
selection.removeAll(where: { s in
|
||||||
s == suggestion ? true : false
|
s == keyword.name ? true : false
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
selection.append(suggestion)
|
selection.append(keyword.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,11 +89,39 @@ struct KeywordPickerView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.navigationTitle(title)
|
||||||
|
}
|
||||||
|
|
||||||
func suggestionsFiltered() -> [String] {
|
func suggestionsFiltered() -> [Keyword] {
|
||||||
guard searchText != "" else { return searchSuggestions }
|
guard searchText != "" else { return searchSuggestions }
|
||||||
return searchSuggestions.filter { suggestion in
|
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
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
struct MainView: View {
|
struct MainView: View {
|
||||||
@ObservedObject var viewModel: MainViewModel
|
@ObservedObject var viewModel: MainViewModel
|
||||||
@ObservedObject var userSettings: UserSettings
|
@ObservedObject var userSettings: UserSettings
|
||||||
|
|
||||||
|
@State private var selectedCategory: Category? = nil
|
||||||
@State private var showEditView: Bool = false
|
@State private var showEditView: Bool = false
|
||||||
var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 0)]
|
var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 0)]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationSplitView {
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
List(viewModel.categories, selection: $selectedCategory) { category in
|
||||||
LazyVGrid(columns: columns, spacing: 0) {
|
|
||||||
ForEach(viewModel.categories, id: \.name) { category in
|
|
||||||
if category.recipe_count != 0 {
|
if category.recipe_count != 0 {
|
||||||
NavigationLink(
|
NavigationLink(value: category) {
|
||||||
destination: RecipeBookView(
|
HStack(alignment: .center) {
|
||||||
categoryName: category.name,
|
Image(systemName: "book.closed.fill")
|
||||||
viewModel: viewModel
|
Text(category.name)
|
||||||
)
|
.font(.system(size: 20, weight: .light, design: .serif))
|
||||||
) {
|
.italic()
|
||||||
CategoryCardView(category: category)
|
}.padding(7)
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
/*.navigationDestination(isPresented: $showEditView) {
|
|
||||||
RecipeEditView()
|
|
||||||
}*/
|
|
||||||
.navigationTitle("Cookbooks")
|
.navigationTitle("Cookbooks")
|
||||||
.toolbar {
|
.toolbar {
|
||||||
Menu {
|
Menu {
|
||||||
@@ -68,9 +62,20 @@ struct MainView: View {
|
|||||||
Image(systemName: "gearshape")
|
Image(systemName: "gearshape")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(
|
.navigationDestination(isPresented: $showEditView) {
|
||||||
NavigationLink(destination: RecipeEditView(), isActive: $showEditView) { EmptyView() }
|
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)
|
.tint(.nextcloudBlue)
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ struct RecipeDetailView: View {
|
|||||||
@State var recipeImage: UIImage?
|
@State var recipeImage: UIImage?
|
||||||
@State var showTitle: Bool = false
|
@State var showTitle: Bool = false
|
||||||
@State var isDownloaded: Bool? = nil
|
@State var isDownloaded: Bool? = nil
|
||||||
|
@State private var presentEditView: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(showsIndicators: false) {
|
ScrollView(showsIndicators: false) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
@@ -69,6 +71,18 @@ struct RecipeDetailView: View {
|
|||||||
}
|
}
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.navigationTitle(showTitle ? recipe.name : "")
|
.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 {
|
.task {
|
||||||
recipeDetail = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id)
|
recipeDetail = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id)
|
||||||
recipeImage = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: false)
|
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)
|
recipeDetail = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id, needsUpdate: true)
|
||||||
recipeImage = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: false, 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 {
|
if let prepTime = recipeDetail.prepTime {
|
||||||
VStack {
|
VStack {
|
||||||
SecondaryLabel(text: "Prep time")
|
SecondaryLabel(text: "Prep time")
|
||||||
Text(formatDate(duration: prepTime))
|
Text(DateFormatter.formatDate(duration: prepTime))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}.padding()
|
}.padding()
|
||||||
}
|
}
|
||||||
@@ -99,7 +112,7 @@ struct RecipeDurationSection: View {
|
|||||||
if let cookTime = recipeDetail.cookTime {
|
if let cookTime = recipeDetail.cookTime {
|
||||||
VStack {
|
VStack {
|
||||||
SecondaryLabel(text: "Cook time")
|
SecondaryLabel(text: "Cook time")
|
||||||
Text(formatDate(duration: cookTime))
|
Text(DateFormatter.formatDate(duration: cookTime))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}.padding()
|
}.padding()
|
||||||
}
|
}
|
||||||
@@ -107,7 +120,7 @@ struct RecipeDurationSection: View {
|
|||||||
if let totalTime = recipeDetail.totalTime {
|
if let totalTime = recipeDetail.totalTime {
|
||||||
VStack {
|
VStack {
|
||||||
SecondaryLabel(text: "Total time")
|
SecondaryLabel(text: "Total time")
|
||||||
Text(formatDate(duration: totalTime))
|
Text(DateFormatter.formatDate(duration: totalTime))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}.padding()
|
}.padding()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,33 +11,56 @@ import PhotosUI
|
|||||||
|
|
||||||
|
|
||||||
struct RecipeEditView: View {
|
struct RecipeEditView: View {
|
||||||
|
@ObservedObject var viewModel: MainViewModel
|
||||||
@State var recipe: RecipeDetail = RecipeDetail()
|
@State var recipe: RecipeDetail = RecipeDetail()
|
||||||
|
@Binding var isPresented: Bool
|
||||||
|
|
||||||
@State var image: PhotosPickerItem? = nil
|
@State var image: PhotosPickerItem? = nil
|
||||||
@State var times = [Date.zero, Date.zero, Date.zero]
|
@State var times = [Date.zero, Date.zero, Date.zero]
|
||||||
|
@State var uploadNew: Bool = true
|
||||||
@State var searchText: String = ""
|
@State var searchText: String = ""
|
||||||
@State var keywords: [String] = []
|
@State var keywords: [String] = []
|
||||||
|
|
||||||
init(recipe: RecipeDetail? = nil) {
|
@State private var alertMessage: String = ""
|
||||||
self.recipe = recipe ?? RecipeDetail()
|
@State private var presentAlert: Bool = false
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
TextField("Title", text: $recipe.name)
|
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()) {
|
PhotosPicker(selection: $image, matching: .images, photoLibrary: .shared()) {
|
||||||
Image(systemName: "photo")
|
Image(systemName: "photo")
|
||||||
.symbolRenderingMode(.multicolor)
|
.symbolRenderingMode(.multicolor)
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
|
*/
|
||||||
Section() {
|
Section() {
|
||||||
|
NavigationLink(recipe.recipeCategory == "" ? "Category" : "Category: \(recipe.recipeCategory)") {
|
||||||
|
CategoryPickerView(title: "Category", searchSuggestions: [], selection: $recipe.recipeCategory)
|
||||||
|
}
|
||||||
NavigationLink("Keywords") {
|
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: {
|
} header: {
|
||||||
Text("Keywords")
|
Text("Discoverability")
|
||||||
} footer: {
|
} footer: {
|
||||||
ScrollView(.horizontal) {
|
ScrollView(.horizontal) {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -63,7 +86,131 @@ struct RecipeEditView: View {
|
|||||||
EditableListSection(title: "Ingredients", items: $recipe.recipeIngredient)
|
EditableListSection(title: "Ingredients", items: $recipe.recipeIngredient)
|
||||||
EditableListSection(title: "Tools", items: $recipe.tool)
|
EditableListSection(title: "Tools", items: $recipe.tool)
|
||||||
EditableListSection(title: "Instructions", items: $recipe.recipeInstructions)
|
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