WIP - Complete App refactoring
This commit is contained in:
@@ -37,12 +37,21 @@ class DataStore {
|
||||
guard let data = try? Data(contentsOf: fileURL) else {
|
||||
return nil
|
||||
}
|
||||
let storedRecipes = try JSONDecoder().decode(D.self, from: data)
|
||||
return storedRecipes
|
||||
let decodedData = try JSONDecoder().decode(D.self, from: data)
|
||||
return decodedData
|
||||
}
|
||||
return try await task.value
|
||||
}
|
||||
|
||||
func loadDynamic(fromPath path: String, type: Decodable.Type) async throws -> Any? {
|
||||
let fileURL = try Self.fileURL(appending: path)
|
||||
guard let data = try? Data(contentsOf: fileURL) else {
|
||||
return nil
|
||||
}
|
||||
let decoded = try JSONDecoder().decode(type, from: data)
|
||||
return decoded
|
||||
}
|
||||
|
||||
func save<D: Encodable>(data: D, toPath path: String) async {
|
||||
let task = Task {
|
||||
let data = try JSONEncoder().encode(data)
|
||||
@@ -69,6 +78,27 @@ class DataStore {
|
||||
return fileManager.fileExists(atPath: folderPath + filePath)
|
||||
}
|
||||
|
||||
func listAllFolders(dir: String) -> [String] {
|
||||
guard let baseURL = try? Self.fileURL() else {
|
||||
print("Failed to retrieve documents directory.")
|
||||
return []
|
||||
}
|
||||
|
||||
let targetURL = baseURL.appendingPathComponent(dir)
|
||||
|
||||
do {
|
||||
let contents = try fileManager.contentsOfDirectory(at: targetURL, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles])
|
||||
|
||||
let folders = contents.filter { url in
|
||||
(try? url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false
|
||||
}
|
||||
return folders.map { $0.lastPathComponent }
|
||||
} catch {
|
||||
print("Error listing folders in \(dir): \(error)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
func clearAll() -> Bool {
|
||||
print("Attempting to delete all data ...")
|
||||
guard let folderPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first?.path() else { return false }
|
||||
|
||||
@@ -7,18 +7,183 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
class ObservableRecipeDetail: ObservableObject {
|
||||
// MARK: - Recipe Model
|
||||
|
||||
@Model
|
||||
class RecipeImage {
|
||||
enum RecipeImageSize {
|
||||
case THUMB, FULL
|
||||
}
|
||||
|
||||
var imageData: Data?
|
||||
|
||||
@Transient
|
||||
var image: UIImage? {
|
||||
guard let imageData else { return nil }
|
||||
return UIImage(data: imageData)
|
||||
}
|
||||
|
||||
init(imageData: Data? = nil) {
|
||||
self.imageData = imageData
|
||||
}
|
||||
}
|
||||
|
||||
@Model
|
||||
class RecipeThumbnail {
|
||||
var thumbnailData: Data?
|
||||
|
||||
@Transient
|
||||
var thumbnail: UIImage? {
|
||||
guard let thumbnailData else { return nil }
|
||||
return UIImage(data: thumbnailData)
|
||||
}
|
||||
|
||||
init(thumbnailData: Data? = nil) {
|
||||
self.thumbnailData = thumbnailData
|
||||
}
|
||||
}
|
||||
|
||||
@Model
|
||||
class Recipe {
|
||||
var id: String
|
||||
var name: String
|
||||
var keywords: [String]
|
||||
@Attribute(.externalStorage) var image: RecipeImage?
|
||||
var thumbnail: RecipeThumbnail?
|
||||
var dateCreated: String? = nil
|
||||
var dateModified: String? = nil
|
||||
var prepTime: String
|
||||
var cookTime: String
|
||||
var totalTime: String
|
||||
var recipeDescription: String
|
||||
var url: String?
|
||||
var yield: Int
|
||||
var category: String
|
||||
var tools: [String]
|
||||
var ingredients: [String]
|
||||
var instructions: [String]
|
||||
var nutrition: [String:String]
|
||||
|
||||
// Additional functionality
|
||||
@Transient
|
||||
var ingredientMultiplier: Double = 1.0
|
||||
|
||||
|
||||
init(
|
||||
id: String,
|
||||
name: String,
|
||||
keywords: [String],
|
||||
dateCreated: String? = nil,
|
||||
dateModified: String? = nil,
|
||||
prepTime: String,
|
||||
cookTime: String,
|
||||
totalTime: String,
|
||||
recipeDescription: String,
|
||||
url: String? = nil,
|
||||
yield: Int,
|
||||
category: String,
|
||||
tools: [String],
|
||||
ingredients: [String],
|
||||
instructions: [String],
|
||||
nutrition: [String : String],
|
||||
ingredientMultiplier: Double
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.keywords = keywords
|
||||
self.dateCreated = dateCreated
|
||||
self.dateModified = dateModified
|
||||
self.prepTime = prepTime
|
||||
self.cookTime = cookTime
|
||||
self.totalTime = totalTime
|
||||
self.recipeDescription = recipeDescription
|
||||
self.url = url
|
||||
self.yield = yield
|
||||
self.category = category
|
||||
self.tools = tools
|
||||
self.ingredients = ingredients
|
||||
self.instructions = instructions
|
||||
self.nutrition = nutrition
|
||||
self.ingredientMultiplier = ingredientMultiplier
|
||||
}
|
||||
|
||||
required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
name = try container.decode(String.self, forKey: .name)
|
||||
keywords = try container.decode([String].self, forKey: .keywords)
|
||||
dateCreated = try container.decodeIfPresent(String.self, forKey: .dateCreated)
|
||||
dateModified = try container.decodeIfPresent(String.self, forKey: .dateModified)
|
||||
prepTime = try container.decode(String.self, forKey: .prepTime)
|
||||
cookTime = try container.decode(String.self, forKey: .cookTime)
|
||||
totalTime = try container.decode(String.self, forKey: .totalTime)
|
||||
recipeDescription = try container.decode(String.self, forKey: .recipeDescription)
|
||||
url = try container.decodeIfPresent(String.self, forKey: .url)
|
||||
yield = try container.decode(Int.self, forKey: .yield)
|
||||
category = try container.decode(String.self, forKey: .category)
|
||||
tools = try container.decode([String].self, forKey: .tools)
|
||||
ingredients = try container.decode([String].self, forKey: .ingredients)
|
||||
instructions = try container.decode([String].self, forKey: .instructions)
|
||||
nutrition = try container.decode([String: String].self, forKey: .nutrition)
|
||||
}
|
||||
}
|
||||
|
||||
extension Recipe: Codable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, name, keywords, dateCreated, dateModified, prepTime, cookTime, totalTime, recipeDescription, url, yield, category, tools, ingredients, instructions, nutrition
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encode(keywords, forKey: .keywords)
|
||||
try container.encode(dateCreated, forKey: .dateCreated)
|
||||
try container.encode(dateModified, forKey: .dateModified)
|
||||
try container.encode(prepTime, forKey: .prepTime)
|
||||
try container.encode(cookTime, forKey: .cookTime)
|
||||
try container.encode(totalTime, forKey: .totalTime)
|
||||
try container.encode(recipeDescription, forKey: .recipeDescription)
|
||||
try container.encode(url, forKey: .url)
|
||||
try container.encode(yield, forKey: .yield)
|
||||
try container.encode(category, forKey: .category)
|
||||
try container.encode(tools, forKey: .tools)
|
||||
try container.encode(ingredients, forKey: .ingredients)
|
||||
try container.encode(instructions, forKey: .instructions)
|
||||
try container.encode(nutrition, forKey: .nutrition)
|
||||
}
|
||||
}
|
||||
// MARK: - Recipe Stub
|
||||
|
||||
struct RecipeStub: Codable, Hashable, Identifiable {
|
||||
let id: String
|
||||
let name: String
|
||||
let keywords: String?
|
||||
let dateCreated: String?
|
||||
let dateModified: String?
|
||||
let thumbnailPath: String?
|
||||
var storedLocally: Bool = false
|
||||
var lastUpdated: String?
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Recipe
|
||||
/*
|
||||
class Recipe: ObservableObject {
|
||||
// Cookbook recipe detail fields
|
||||
var id: String
|
||||
@Published var name: String
|
||||
@Published var keywords: [String]
|
||||
@Published var imageUrl: String
|
||||
@Published var imageUrl: String?
|
||||
var dateCreated: String? = nil
|
||||
var dateModified: String? = nil
|
||||
@Published var prepTime: DurationComponents
|
||||
@Published var cookTime: DurationComponents
|
||||
@Published var totalTime: DurationComponents
|
||||
@Published var description: String
|
||||
@Published var url: String
|
||||
@Published var url: String?
|
||||
@Published var recipeYield: Int
|
||||
@Published var recipeCategory: String
|
||||
@Published var tool: [String]
|
||||
@@ -51,7 +216,7 @@ class ObservableRecipeDetail: ObservableObject {
|
||||
ingredientMultiplier = 1
|
||||
}
|
||||
|
||||
init(_ recipeDetail: RecipeDetail) {
|
||||
init(_ recipeDetail: CookbookApiRecipeDetailV1) {
|
||||
id = recipeDetail.id
|
||||
name = recipeDetail.name
|
||||
keywords = recipeDetail.keywords.isEmpty ? [] : recipeDetail.keywords.components(separatedBy: ",")
|
||||
@@ -71,8 +236,70 @@ class ObservableRecipeDetail: ObservableObject {
|
||||
ingredientMultiplier = Double(recipeDetail.recipeYield == 0 ? 1 : recipeDetail.recipeYield)
|
||||
}
|
||||
|
||||
func toRecipeDetail() -> RecipeDetail {
|
||||
return RecipeDetail(
|
||||
init(
|
||||
name: String,
|
||||
keywords: [String],
|
||||
dateCreated: String?,
|
||||
dateModified: String?,
|
||||
imageUrl: String?,
|
||||
id: String,
|
||||
prepTime: DurationComponents? = nil,
|
||||
cookTime: DurationComponents? = nil,
|
||||
totalTime: DurationComponents? = 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
|
||||
self.dateModified = dateModified
|
||||
self.imageUrl = imageUrl
|
||||
self.id = id
|
||||
self.prepTime = prepTime ?? DurationComponents()
|
||||
self.cookTime = cookTime ?? DurationComponents()
|
||||
self.totalTime = totalTime ?? DurationComponents()
|
||||
self.description = description
|
||||
self.url = url
|
||||
self.recipeYield = recipeYield
|
||||
self.recipeCategory = recipeCategory
|
||||
self.tool = tool
|
||||
self.recipeIngredient = recipeIngredient
|
||||
self.recipeInstructions = recipeInstructions
|
||||
self.nutrition = nutrition
|
||||
|
||||
ingredientMultiplier = Double(recipeYield == 0 ? 1 : recipeYield)
|
||||
}
|
||||
|
||||
required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
_name = Published(initialValue: try container.decode(String.self, forKey: .name))
|
||||
_keywords = Published(initialValue: try container.decode([String].self, forKey: .keywords))
|
||||
_imageUrl = Published(initialValue: try container.decodeIfPresent(String.self, forKey: .imageUrl))
|
||||
dateCreated = try container.decodeIfPresent(String.self, forKey: .dateCreated)
|
||||
dateModified = try container.decodeIfPresent(String.self, forKey: .dateModified)
|
||||
_prepTime = Published(initialValue: try container.decode(DurationComponents.self, forKey: .prepTime))
|
||||
_cookTime = Published(initialValue: try container.decode(DurationComponents.self, forKey: .cookTime))
|
||||
_totalTime = Published(initialValue: try container.decode(DurationComponents.self, forKey: .totalTime))
|
||||
_description = Published(initialValue: try container.decode(String.self, forKey: .description))
|
||||
_url = Published(initialValue: try container.decodeIfPresent(String.self, forKey: .url))
|
||||
_recipeYield = Published(initialValue: try container.decode(Int.self, forKey: .recipeYield))
|
||||
_recipeCategory = Published(initialValue: try container.decode(String.self, forKey: .recipeCategory))
|
||||
_tool = Published(initialValue: try container.decode([String].self, forKey: .tool))
|
||||
_recipeIngredient = Published(initialValue: try container.decode([String].self, forKey: .recipeIngredient))
|
||||
_recipeInstructions = Published(initialValue: try container.decode([String].self, forKey: .recipeInstructions))
|
||||
_nutrition = Published(initialValue: try container.decode([String: String].self, forKey: .nutrition))
|
||||
_ingredientMultiplier = Published(initialValue: try container.decode(Double.self, forKey: .ingredientMultiplier))
|
||||
}
|
||||
|
||||
func toRecipeDetail() -> CookbookApiRecipeDetailV1 {
|
||||
return CookbookApiRecipeDetailV1(
|
||||
name: self.name,
|
||||
keywords: self.keywords.joined(separator: ","),
|
||||
dateCreated: "",
|
||||
@@ -98,14 +325,14 @@ class ObservableRecipeDetail: ObservableObject {
|
||||
return AttributedString(ingredient)
|
||||
}
|
||||
// Match mixed fractions first
|
||||
var matches = ObservableRecipeDetail.matchPatternAndMultiply(
|
||||
var matches = Recipe.matchPatternAndMultiply(
|
||||
.mixedFraction,
|
||||
in: ingredient,
|
||||
multFactor: factor
|
||||
)
|
||||
// Then match fractions, exclude mixed fraction ranges
|
||||
matches.append(contentsOf:
|
||||
ObservableRecipeDetail.matchPatternAndMultiply(
|
||||
Recipe.matchPatternAndMultiply(
|
||||
.fraction,
|
||||
in: ingredient,
|
||||
multFactor: factor,
|
||||
@@ -114,7 +341,7 @@ class ObservableRecipeDetail: ObservableObject {
|
||||
)
|
||||
// Match numbers at last, exclude all prior matches
|
||||
matches.append(contentsOf:
|
||||
ObservableRecipeDetail.matchPatternAndMultiply(
|
||||
Recipe.matchPatternAndMultiply(
|
||||
.number,
|
||||
in: ingredient,
|
||||
multFactor: factor,
|
||||
@@ -221,6 +448,34 @@ class ObservableRecipeDetail: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
extension Recipe: Codable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, name, keywords, imageUrl, dateCreated, dateModified, prepTime, cookTime, totalTime, description, url, recipeYield, recipeCategory, tool, recipeIngredient, recipeInstructions, nutrition, ingredientMultiplier
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encode(keywords, forKey: .keywords)
|
||||
try container.encode(imageUrl, forKey: .imageUrl)
|
||||
try container.encode(dateCreated, forKey: .dateCreated)
|
||||
try container.encode(dateModified, forKey: .dateModified)
|
||||
try container.encode(prepTime, forKey: .prepTime)
|
||||
try container.encode(cookTime, forKey: .cookTime)
|
||||
try container.encode(totalTime, forKey: .totalTime)
|
||||
try container.encode(description, forKey: .description)
|
||||
try container.encode(url, forKey: .url)
|
||||
try container.encode(recipeYield, forKey: .recipeYield)
|
||||
try container.encode(recipeCategory, forKey: .recipeCategory)
|
||||
try container.encode(tool, forKey: .tool)
|
||||
try container.encode(recipeIngredient, forKey: .recipeIngredient)
|
||||
try container.encode(recipeInstructions, forKey: .recipeInstructions)
|
||||
try container.encode(nutrition, forKey: .nutrition)
|
||||
try container.encode(ingredientMultiplier, forKey: .ingredientMultiplier)
|
||||
}
|
||||
}
|
||||
|
||||
enum RegexPattern: String, CaseIterable, Identifiable {
|
||||
case mixedFraction, fraction, number
|
||||
|
||||
@@ -249,3 +504,4 @@ enum RegexPattern: String, CaseIterable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user