WIP - Complete App refactoring

This commit is contained in:
VincentMeilinger
2025-05-26 15:52:24 +02:00
parent 29fd3c668b
commit 5acf3b9c4f
49 changed files with 1996 additions and 543 deletions

View File

@@ -32,7 +32,7 @@ protocol CookbookApi {
static func importRecipe(
auth: String,
data: Data
) async -> (RecipeDetail?, NetworkError?)
) async -> (Recipe?, NetworkError?)
/// Get either the full image or a thumbnail sized version.
/// - Parameters:
@@ -42,7 +42,7 @@ protocol CookbookApi {
/// - Returns: The image of the recipe with the specified id. A NetworkError if the request fails, otherwise nil.
static func getImage(
auth: String,
id: Int,
id: String,
size: RecipeImage.RecipeImageSize
) async -> (UIImage?, NetworkError?)
@@ -52,7 +52,7 @@ protocol CookbookApi {
/// - Returns: A list of all recipes.
static func getRecipes(
auth: String
) async -> ([Recipe]?, NetworkError?)
) async -> ([RecipeStub]?, NetworkError?)
/// Create a new recipe.
/// - Parameters:
@@ -60,7 +60,7 @@ protocol CookbookApi {
/// - Returns: A NetworkError if the request fails. Nil otherwise.
static func createRecipe(
auth: String,
recipe: RecipeDetail
recipe: Recipe
) async -> (NetworkError?)
/// Get the recipe with the specified id.
@@ -69,8 +69,9 @@ protocol CookbookApi {
/// - id: The recipe id.
/// - Returns: The recipe if it exists. A NetworkError if the request fails.
static func getRecipe(
auth: String, id: Int
) async -> (RecipeDetail?, NetworkError?)
auth: String,
id: String
) async -> (Recipe?, NetworkError?)
/// Update an existing recipe with new entries.
/// - Parameters:
@@ -79,7 +80,7 @@ protocol CookbookApi {
/// - Returns: A NetworkError if the request fails. Nil otherwise.
static func updateRecipe(
auth: String,
recipe: RecipeDetail
recipe: Recipe
) async -> (NetworkError?)
/// Delete the recipe with the specified id.
@@ -89,7 +90,7 @@ protocol CookbookApi {
/// - Returns: A NetworkError if the request fails. Nil otherwise.
static func deleteRecipe(
auth: String,
id: Int
id: String
) async -> (NetworkError?)
/// Get all categories.
@@ -108,7 +109,7 @@ protocol CookbookApi {
static func getCategory(
auth: String,
named categoryName: String
) async -> ([Recipe]?, NetworkError?)
) async -> ([RecipeStub]?, NetworkError?)
/// Rename an existing category.
/// - Parameters:
@@ -138,7 +139,7 @@ protocol CookbookApi {
static func getRecipesTagged(
auth: String,
keyword: String
) async -> ([Recipe]?, NetworkError?)
) async -> ([RecipeStub]?, NetworkError?)
/// Get the servers api version.
/// - Parameters:
@@ -176,3 +177,4 @@ protocol CookbookApi {

View File

@@ -12,7 +12,7 @@ import UIKit
class CookbookApiV1: CookbookApi {
static let basePath: String = "/index.php/apps/cookbook/api/v1"
static func importRecipe(auth: String, data: Data) async -> (RecipeDetail?, NetworkError?) {
static func importRecipe(auth: String, data: Data) async -> (Recipe?, NetworkError?) {
let request = ApiRequest(
path: basePath + "/import",
method: .POST,
@@ -22,10 +22,12 @@ class CookbookApiV1: CookbookApi {
let (data, error) = await request.send()
guard let data = data else { return (nil, error) }
return (JSONDecoder.safeDecode(data), nil)
let recipe: CookbookApiRecipeDetailV1? = JSONDecoder.safeDecode(data)
return (recipe?.toRecipe(), error)
}
static func getImage(auth: String, id: Int, size: RecipeImage.RecipeImageSize) async -> (UIImage?, NetworkError?) {
static func getImage(auth: String, id: String, size: RecipeImage.RecipeImageSize) async -> (UIImage?, NetworkError?) {
guard let id = Int(id) else {return (nil, .unknownError)}
let imageSize = (size == .FULL ? "full" : "thumb")
let request = ApiRequest(
path: basePath + "/recipes/\(id)/image?size=\(imageSize)",
@@ -39,7 +41,7 @@ class CookbookApiV1: CookbookApi {
return (UIImage(data: data), error)
}
static func getRecipes(auth: String) async -> ([Recipe]?, NetworkError?) {
static func getRecipes(auth: String) async -> ([RecipeStub]?, NetworkError?) {
let request = ApiRequest(
path: basePath + "/recipes",
method: .GET,
@@ -50,10 +52,12 @@ class CookbookApiV1: CookbookApi {
let (data, error) = await request.send()
guard let data = data else { return (nil, error) }
print("\n\nRECIPE: ", String(data: data, encoding: .utf8))
return (JSONDecoder.safeDecode(data), nil)
let recipes: [CookbookApiRecipeV1]? = JSONDecoder.safeDecode(data)
return (recipes?.map({ recipe in recipe.toRecipeStub() }), nil)
}
static func createRecipe(auth: String, recipe: RecipeDetail) async -> (NetworkError?) {
static func createRecipe(auth: String, recipe: Recipe) async -> (NetworkError?) {
let recipe = CookbookApiRecipeDetailV1.fromRecipe(recipe)
guard let recipeData = JSONEncoder.safeEncode(recipe) else {
return .dataError
}
@@ -81,7 +85,8 @@ class CookbookApiV1: CookbookApi {
return nil
}
static func getRecipe(auth: String, id: Int) async -> (RecipeDetail?, NetworkError?) {
static func getRecipe(auth: String, id: String) async -> (Recipe?, NetworkError?) {
guard let id = Int(id) else {return (nil, .unknownError)}
let request = ApiRequest(
path: basePath + "/recipes/\(id)",
method: .GET,
@@ -91,10 +96,13 @@ class CookbookApiV1: CookbookApi {
let (data, error) = await request.send()
guard let data = data else { return (nil, error) }
return (JSONDecoder.safeDecode(data), nil)
let recipe: CookbookApiRecipeDetailV1? = JSONDecoder.safeDecode(data)
return (recipe?.toRecipe(), nil)
}
static func updateRecipe(auth: String, recipe: RecipeDetail) async -> (NetworkError?) {
static func updateRecipe(auth: String, recipe: Recipe) async -> (NetworkError?) {
let cookbookRecipe = CookbookApiRecipeDetailV1.fromRecipe(recipe)
guard let recipeData = JSONEncoder.safeEncode(recipe) else {
return .dataError
}
@@ -121,7 +129,8 @@ class CookbookApiV1: CookbookApi {
return nil
}
static func deleteRecipe(auth: String, id: Int) async -> (NetworkError?) {
static func deleteRecipe(auth: String, id: String) async -> (NetworkError?) {
guard let id = Int(id) else {return .unknownError}
let request = ApiRequest(
path: basePath + "/recipes/\(id)",
method: .DELETE,
@@ -147,7 +156,7 @@ class CookbookApiV1: CookbookApi {
return (JSONDecoder.safeDecode(data), nil)
}
static func getCategory(auth: String, named categoryName: String) async -> ([Recipe]?, NetworkError?) {
static func getCategory(auth: String, named categoryName: String) async -> ([RecipeStub]?, NetworkError?) {
let request = ApiRequest(
path: basePath + "/category/\(categoryName)",
method: .GET,
@@ -157,7 +166,8 @@ class CookbookApiV1: CookbookApi {
let (data, error) = await request.send()
guard let data = data else { return (nil, error) }
return (JSONDecoder.safeDecode(data), nil)
let recipes: [CookbookApiRecipeV1]? = JSONDecoder.safeDecode(data)
return (recipes?.map({ recipe in recipe.toRecipeStub() }), nil)
}
static func renameCategory(auth: String, named categoryName: String, newName: String) async -> (NetworkError?) {
@@ -186,7 +196,7 @@ class CookbookApiV1: CookbookApi {
return (JSONDecoder.safeDecode(data), nil)
}
static func getRecipesTagged(auth: String, keyword: String) async -> ([Recipe]?, NetworkError?) {
static func getRecipesTagged(auth: String, keyword: String) async -> ([RecipeStub]?, NetworkError?) {
let request = ApiRequest(
path: basePath + "/tags/\(keyword)",
method: .GET,
@@ -196,7 +206,8 @@ class CookbookApiV1: CookbookApi {
let (data, error) = await request.send()
guard let data = data else { return (nil, error) }
return (JSONDecoder.safeDecode(data), nil)
let recipes: [CookbookApiRecipeV1]? = JSONDecoder.safeDecode(data)
return (recipes?.map({ recipe in recipe.toRecipeStub() }), nil)
}
static func getApiVersion(auth: String) async -> (NetworkError?) {
@@ -215,3 +226,4 @@ class CookbookApiV1: CookbookApi {
return .none
}
}

View File

@@ -1,5 +1,5 @@
//
// Models.swift
// CookbookLoginModels.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 11.05.24.
@@ -9,10 +9,7 @@ import Foundation
import SwiftUI
// MARK: - Login flow
// MARK: - Login Models
struct LoginV2Request: Codable {
let poll: LoginV2Poll

View File

@@ -8,8 +8,18 @@
import Foundation
import SwiftUI
struct Category: Codable, Identifiable, Hashable {
var id: String { name }
let name: String
let recipe_count: Int
private enum CodingKeys: String, CodingKey {
case name, recipe_count
}
}
struct CookbookApiRecipeV1: Codable {
struct CookbookApiRecipeV1: CookbookApiRecipe, Codable, Identifiable, Hashable {
var id: String { name + String(recipe_id) }
let name: String
let keywords: String?
let dateCreated: String?
@@ -24,15 +34,22 @@ struct CookbookApiRecipeV1: Codable {
private enum CodingKeys: String, CodingKey {
case name, keywords, dateCreated, dateModified, imageUrl, imagePlaceholderUrl, recipe_id
}
func toRecipeStub() -> RecipeStub {
return RecipeStub(
id: String(recipe_id),
name: name,
keywords: keywords,
dateCreated: dateCreated,
dateModified: dateModified,
thumbnailPath: nil
)
}
}
extension CookbookApiRecipeV1: Identifiable, Hashable {
var id: String { name }
}
struct CookbookApiRecipeDetailV1: Codable {
struct CookbookApiRecipeDetailV1: CookbookApiRecipeDetail {
var name: String
var keywords: String
var dateCreated: String?
@@ -51,7 +68,7 @@ struct CookbookApiRecipeDetailV1: Codable {
var recipeInstructions: [String]
var nutrition: [String:String]
init(name: String, keywords: String, dateCreated: String, dateModified: String, imageUrl: String, id: String, prepTime: String? = nil, cookTime: String? = nil, totalTime: String? = nil, description: String, url: String, recipeYield: Int, recipeCategory: String, tool: [String], recipeIngredient: [String], recipeInstructions: [String], nutrition: [String:String]) {
init(name: String, keywords: String, dateCreated: String?, dateModified: String?, imageUrl: String?, id: String, prepTime: String? = nil, cookTime: String? = nil, totalTime: String? = nil, description: String, url: String?, recipeYield: Int, recipeCategory: String, tool: [String], recipeIngredient: [String], recipeInstructions: [String], nutrition: [String:String]) {
self.name = name
self.keywords = keywords
self.dateCreated = dateCreated
@@ -117,6 +134,47 @@ struct CookbookApiRecipeDetailV1: Codable {
nutrition = try container.decode(Dictionary<String, JSONAny>.self, forKey: .nutrition).mapValues { String(describing: $0.value) }
}
func toRecipe() -> Recipe {
return Recipe(
id: self.id,
name: self.name,
keywords: keywords.components(separatedBy: ","),
dateCreated: self.dateCreated,
dateModified: self.dateModified,
prepTime: self.prepTime ?? "",
cookTime: self.cookTime ?? "",
totalTime: self.totalTime ?? "",
recipeDescription: self.description,
url: self.url,
yield: self.recipeYield,
category: self.recipeCategory,
tools: self.tool,
ingredients: self.recipeIngredient,
instructions: self.recipeInstructions,
nutrition: self.nutrition,
ingredientMultiplier: 1.0
)
}
static func fromRecipe(_ recipe: Recipe) -> any CookbookApiRecipeDetail {
return CookbookApiRecipeDetailV1(
name: recipe.name,
keywords: recipe.keywords.joined(separator: ","),
dateCreated: recipe.dateCreated,
dateModified: recipe.dateModified,
imageUrl: "",
id: recipe.id,
description: recipe.recipeDescription,
url: recipe.url,
recipeYield: recipe.yield,
recipeCategory: recipe.category,
tool: recipe.tools,
recipeIngredient: recipe.ingredients,
recipeInstructions: recipe.instructions,
nutrition: recipe.nutrition
)
}
}
@@ -155,7 +213,7 @@ extension CookbookApiRecipeDetailV1 {
}
}
/*
struct RecipeImage {
enum RecipeImageSize: String {
case THUMB="thumb", FULL="full"
@@ -164,7 +222,7 @@ struct RecipeImage {
var thumb: UIImage?
var full: UIImage?
}
*/
struct RecipeKeyword: Codable {
let name: String
@@ -244,3 +302,4 @@ enum Nutrition: CaseIterable {
}
}
}

View File

@@ -6,3 +6,13 @@
//
import Foundation
protocol CookbookApiRecipe {
func toRecipeStub() -> RecipeStub
}
protocol CookbookApiRecipeDetail: Codable {
func toRecipe() -> Recipe
static func fromRecipe(_ recipe: Recipe) -> CookbookApiRecipeDetail
}

View File

@@ -6,18 +6,53 @@
//
import Foundation
import SwiftUI
public enum NetworkError: String, Error {
case missingUrl = "Missing URL."
case parametersNil = "Parameters are nil."
case encodingFailed = "Parameter encoding failed."
case decodingFailed = "Data decoding failed."
case redirectionError = "Redirection error"
case clientError = "Client error"
case serverError = "Server error"
case invalidRequest = "Invalid request"
case unknownError = "Unknown error"
case dataError = "Invalid data error."
public enum NetworkError: UserAlert {
case missingUrl
case parametersNil
case encodingFailed
case decodingFailed
case redirectionError
case clientError
case serverError
case invalidRequest
case unknownError
case dataError
var localizedTitle: LocalizedStringKey {
switch self {
case .missingUrl:
"Missing URL."
case .parametersNil:
"Parameters are nil."
case .encodingFailed:
"Parameter encoding failed."
case .decodingFailed:
"Data decoding failed."
case .redirectionError:
"Redirection error"
case .clientError:
"Client error"
case .serverError:
"Server error"
case .invalidRequest:
"Invalid request"
case .unknownError:
"Unknown error"
case .dataError:
"Invalid data error."
}
}
var localizedDescription: LocalizedStringKey {
return "" // TODO: Add description
}
var alertButtons: [AlertButton] {
return [.OK]
}
}