Compare commits
5 Commits
1.10.1
...
512d534edf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
512d534edf | ||
|
|
c4be0e98b9 | ||
|
|
b4b6afb45a | ||
|
|
d6cfa6b01d | ||
|
|
498ed0d8ff |
@@ -797,7 +797,7 @@
|
|||||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
MARKETING_VERSION = 1.10.2;
|
MARKETING_VERSION = 1.10.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client";
|
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = auto;
|
SDKROOT = auto;
|
||||||
@@ -841,7 +841,7 @@
|
|||||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
MARKETING_VERSION = 1.10.2;
|
MARKETING_VERSION = 1.10.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client";
|
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = auto;
|
SDKROOT = auto;
|
||||||
|
|||||||
Binary file not shown.
@@ -170,9 +170,6 @@ import UIKit
|
|||||||
}
|
}
|
||||||
var allRecipes: [Recipe] = []
|
var allRecipes: [Recipe] = []
|
||||||
for category in categories {
|
for category in categories {
|
||||||
if self.recipes[category.name] == nil {
|
|
||||||
await getCategory(named: category.name, fetchMode: .preferLocal)
|
|
||||||
}
|
|
||||||
if let recipeArray = self.recipes[category.name] {
|
if let recipeArray = self.recipes[category.name] {
|
||||||
allRecipes.append(contentsOf: recipeArray)
|
allRecipes.append(contentsOf: recipeArray)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
var id: String
|
var id: String
|
||||||
@Published var name: String
|
@Published var name: String
|
||||||
@Published var keywords: [String]
|
@Published var keywords: [String]
|
||||||
@Published var image: String
|
|
||||||
@Published var imageUrl: String
|
@Published var imageUrl: String
|
||||||
@Published var prepTime: DurationComponents
|
@Published var prepTime: DurationComponents
|
||||||
@Published var cookTime: DurationComponents
|
@Published var cookTime: DurationComponents
|
||||||
@@ -36,7 +35,6 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
id = ""
|
id = ""
|
||||||
name = String(localized: "New Recipe")
|
name = String(localized: "New Recipe")
|
||||||
keywords = []
|
keywords = []
|
||||||
image = ""
|
|
||||||
imageUrl = ""
|
imageUrl = ""
|
||||||
prepTime = DurationComponents()
|
prepTime = DurationComponents()
|
||||||
cookTime = DurationComponents()
|
cookTime = DurationComponents()
|
||||||
@@ -57,7 +55,6 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
id = recipeDetail.id
|
id = recipeDetail.id
|
||||||
name = recipeDetail.name
|
name = recipeDetail.name
|
||||||
keywords = recipeDetail.keywords.isEmpty ? [] : recipeDetail.keywords.components(separatedBy: ",")
|
keywords = recipeDetail.keywords.isEmpty ? [] : recipeDetail.keywords.components(separatedBy: ",")
|
||||||
image = recipeDetail.image ?? ""
|
|
||||||
imageUrl = recipeDetail.imageUrl ?? ""
|
imageUrl = recipeDetail.imageUrl ?? ""
|
||||||
prepTime = DurationComponents.fromPTString(recipeDetail.prepTime ?? "")
|
prepTime = DurationComponents.fromPTString(recipeDetail.prepTime ?? "")
|
||||||
cookTime = DurationComponents.fromPTString(recipeDetail.cookTime ?? "")
|
cookTime = DurationComponents.fromPTString(recipeDetail.cookTime ?? "")
|
||||||
@@ -80,7 +77,6 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
keywords: self.keywords.joined(separator: ","),
|
keywords: self.keywords.joined(separator: ","),
|
||||||
dateCreated: "",
|
dateCreated: "",
|
||||||
dateModified: "",
|
dateModified: "",
|
||||||
image: self.image,
|
|
||||||
imageUrl: self.imageUrl,
|
imageUrl: self.imageUrl,
|
||||||
id: self.id,
|
id: self.id,
|
||||||
prepTime: self.prepTime.toPTString(),
|
prepTime: self.prepTime.toPTString(),
|
||||||
@@ -97,59 +93,21 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func applyMarkdownStyling(_ text: String) -> AttributedString {
|
static func adjustIngredient(_ ingredient: String, by factor: Double) -> AttributedString {
|
||||||
if var markdownString = try? AttributedString(
|
|
||||||
markdown: text,
|
|
||||||
options: .init(
|
|
||||||
allowsExtendedAttributes: true,
|
|
||||||
interpretedSyntax: .full,
|
|
||||||
failurePolicy: .returnPartiallyParsedIfPossible
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
for (intentBlock, intentRange) in markdownString.runs[AttributeScopes.FoundationAttributes.PresentationIntentAttribute.self].reversed() {
|
|
||||||
guard let intentBlock = intentBlock else { continue }
|
|
||||||
for intent in intentBlock.components {
|
|
||||||
switch intent.kind {
|
|
||||||
case .header(level: let level):
|
|
||||||
switch level {
|
|
||||||
case 1:
|
|
||||||
markdownString[intentRange].font = .system(.title).bold()
|
|
||||||
case 2:
|
|
||||||
markdownString[intentRange].font = .system(.title2).bold()
|
|
||||||
case 3:
|
|
||||||
markdownString[intentRange].font = .system(.title3).bold()
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if intentRange.lowerBound != markdownString.startIndex {
|
|
||||||
markdownString.characters.insert(contentsOf: "\n", at: intentRange.lowerBound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return markdownString
|
|
||||||
}
|
|
||||||
return AttributedString(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func adjustIngredient(_ ingredient: AttributedString, by factor: Double) -> AttributedString {
|
|
||||||
if factor == 0 {
|
if factor == 0 {
|
||||||
return ingredient
|
return AttributedString(ingredient)
|
||||||
}
|
}
|
||||||
// Match mixed fractions first
|
// Match mixed fractions first
|
||||||
var matches = ObservableRecipeDetail.matchPatternAndMultiply(
|
var matches = ObservableRecipeDetail.matchPatternAndMultiply(
|
||||||
.mixedFraction,
|
.mixedFraction,
|
||||||
in: String(ingredient.characters),
|
in: ingredient,
|
||||||
multFactor: factor
|
multFactor: factor
|
||||||
)
|
)
|
||||||
// Then match fractions, exclude mixed fraction ranges
|
// Then match fractions, exclude mixed fraction ranges
|
||||||
matches.append(contentsOf:
|
matches.append(contentsOf:
|
||||||
ObservableRecipeDetail.matchPatternAndMultiply(
|
ObservableRecipeDetail.matchPatternAndMultiply(
|
||||||
.fraction,
|
.fraction,
|
||||||
in: String(ingredient.characters),
|
in: ingredient,
|
||||||
multFactor: factor,
|
multFactor: factor,
|
||||||
excludedRanges: matches.map({ tuple in tuple.1 })
|
excludedRanges: matches.map({ tuple in tuple.1 })
|
||||||
)
|
)
|
||||||
@@ -158,7 +116,7 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
matches.append(contentsOf:
|
matches.append(contentsOf:
|
||||||
ObservableRecipeDetail.matchPatternAndMultiply(
|
ObservableRecipeDetail.matchPatternAndMultiply(
|
||||||
.number,
|
.number,
|
||||||
in: String(ingredient.characters),
|
in: ingredient,
|
||||||
multFactor: factor,
|
multFactor: factor,
|
||||||
excludedRanges: matches.map({ tuple in tuple.1 })
|
excludedRanges: matches.map({ tuple in tuple.1 })
|
||||||
)
|
)
|
||||||
@@ -166,8 +124,7 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
// Sort matches by match range lower bound, descending.
|
// Sort matches by match range lower bound, descending.
|
||||||
matches.sort(by: { a, b in a.1.lowerBound > b.1.lowerBound})
|
matches.sort(by: { a, b in a.1.lowerBound > b.1.lowerBound})
|
||||||
|
|
||||||
var attributedString = ingredient
|
var attributedString = AttributedString(ingredient)
|
||||||
|
|
||||||
for (newSubstring, matchRange) in matches {
|
for (newSubstring, matchRange) in matches {
|
||||||
guard let range = Range(matchRange, in: attributedString) else { continue }
|
guard let range = Range(matchRange, in: attributedString) else { continue }
|
||||||
var attributedSubString = AttributedString(newSubstring)
|
var attributedSubString = AttributedString(newSubstring)
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ struct RecipeDetail: Codable {
|
|||||||
var dateCreated: String?
|
var dateCreated: String?
|
||||||
var dateModified: String?
|
var dateModified: String?
|
||||||
var imageUrl: String?
|
var imageUrl: String?
|
||||||
var image: String?
|
|
||||||
var id: String
|
var id: String
|
||||||
var prepTime: String?
|
var prepTime: String?
|
||||||
var cookTime: String?
|
var cookTime: String?
|
||||||
@@ -52,12 +51,11 @@ struct RecipeDetail: Codable {
|
|||||||
var recipeInstructions: [String]
|
var recipeInstructions: [String]
|
||||||
var nutrition: [String:String]
|
var nutrition: [String:String]
|
||||||
|
|
||||||
init(name: String, keywords: String, dateCreated: String, dateModified: String, image: 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.name = name
|
||||||
self.keywords = keywords
|
self.keywords = keywords
|
||||||
self.dateCreated = dateCreated
|
self.dateCreated = dateCreated
|
||||||
self.dateModified = dateModified
|
self.dateModified = dateModified
|
||||||
self.image = image
|
|
||||||
self.imageUrl = imageUrl
|
self.imageUrl = imageUrl
|
||||||
self.id = id
|
self.id = id
|
||||||
self.prepTime = prepTime
|
self.prepTime = prepTime
|
||||||
@@ -78,7 +76,6 @@ struct RecipeDetail: Codable {
|
|||||||
keywords = ""
|
keywords = ""
|
||||||
dateCreated = ""
|
dateCreated = ""
|
||||||
dateModified = ""
|
dateModified = ""
|
||||||
image = ""
|
|
||||||
imageUrl = ""
|
imageUrl = ""
|
||||||
id = ""
|
id = ""
|
||||||
prepTime = ""
|
prepTime = ""
|
||||||
@@ -96,7 +93,7 @@ struct RecipeDetail: Codable {
|
|||||||
|
|
||||||
// Custom decoder to handle value type ambiguity
|
// Custom decoder to handle value type ambiguity
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case name, keywords, dateCreated, dateModified, image, imageUrl, id, prepTime, cookTime, totalTime, description, url, recipeYield, recipeCategory, tool, recipeIngredient, recipeInstructions, nutrition
|
case name, keywords, dateCreated, dateModified, imageUrl, id, prepTime, cookTime, totalTime, description, url, recipeYield, recipeCategory, tool, recipeIngredient, recipeInstructions, nutrition
|
||||||
}
|
}
|
||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
init(from decoder: Decoder) throws {
|
||||||
@@ -105,7 +102,6 @@ struct RecipeDetail: Codable {
|
|||||||
keywords = try container.decode(String.self, forKey: .keywords)
|
keywords = try container.decode(String.self, forKey: .keywords)
|
||||||
dateCreated = try container.decodeIfPresent(String.self, forKey: .dateCreated)
|
dateCreated = try container.decodeIfPresent(String.self, forKey: .dateCreated)
|
||||||
dateModified = try container.decodeIfPresent(String.self, forKey: .dateModified)
|
dateModified = try container.decodeIfPresent(String.self, forKey: .dateModified)
|
||||||
image = try container.decodeIfPresent(String.self, forKey: .image)
|
|
||||||
imageUrl = try container.decodeIfPresent(String.self, forKey: .imageUrl)
|
imageUrl = try container.decodeIfPresent(String.self, forKey: .imageUrl)
|
||||||
id = try container.decode(String.self, forKey: .id)
|
id = try container.decode(String.self, forKey: .id)
|
||||||
prepTime = try container.decodeIfPresent(String.self, forKey: .prepTime)
|
prepTime = try container.decodeIfPresent(String.self, forKey: .prepTime)
|
||||||
@@ -113,14 +109,13 @@ struct RecipeDetail: Codable {
|
|||||||
totalTime = try container.decodeIfPresent(String.self, forKey: .totalTime)
|
totalTime = try container.decodeIfPresent(String.self, forKey: .totalTime)
|
||||||
description = try container.decode(String.self, forKey: .description)
|
description = try container.decode(String.self, forKey: .description)
|
||||||
url = try container.decode(String.self, forKey: .url)
|
url = try container.decode(String.self, forKey: .url)
|
||||||
recipeYield = try container.decode(Int?.self, forKey: .recipeYield) ?? 1
|
recipeYield = try container.decode(Int.self, forKey: .recipeYield)
|
||||||
recipeCategory = try container.decode(String.self, forKey: .recipeCategory)
|
recipeCategory = try container.decode(String.self, forKey: .recipeCategory)
|
||||||
tool = try container.decode([String].self, forKey: .tool)
|
tool = try container.decode([String].self, forKey: .tool)
|
||||||
recipeIngredient = try container.decode([String].self, forKey: .recipeIngredient)
|
recipeIngredient = try container.decode([String].self, forKey: .recipeIngredient)
|
||||||
recipeInstructions = try container.decode([String].self, forKey: .recipeInstructions)
|
recipeInstructions = try container.decode([String].self, forKey: .recipeInstructions)
|
||||||
|
|
||||||
nutrition = try container.decode(Dictionary<String, JSONAny>.self, forKey: .nutrition).mapValues { String(describing: $0.value) }
|
nutrition = try container.decode(Dictionary<String, JSONAny>.self, forKey: .nutrition).mapValues { String(describing: $0.value) }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +127,6 @@ extension RecipeDetail {
|
|||||||
keywords: "",
|
keywords: "",
|
||||||
dateCreated: "",
|
dateCreated: "",
|
||||||
dateModified: "",
|
dateModified: "",
|
||||||
image: "",
|
|
||||||
imageUrl: "",
|
imageUrl: "",
|
||||||
id: "",
|
id: "",
|
||||||
prepTime: "",
|
prepTime: "",
|
||||||
|
|||||||
@@ -485,6 +485,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Add" : {
|
"Add" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3831,28 +3832,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"To add grocieries manually, type them in the box below and press the button. To add multiple items at once, separate them by a new line." : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Über das Textfeld können Einkäufe manuell hinzugefügt werden. Durch Zeilenumbrüche können mehrere Artikel auf einmal hinzugefügt werden."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Para añadir comestibles manualmente, escríbelos en el cuadro de abajo y pulsa el botón.\nPara añadir varios artículos a la vez, sepáralos con una nueva línea."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Pour ajouter des courses manuellement, tape-les dans la case ci-dessous et appuie sur le bouton.\nPour ajouter plusieurs articles à la fois, sépare-les par un saut de ligne."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Tool" : {
|
"Tool" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ struct RecipeListSection: View {
|
|||||||
ForEach(list, id: \.self) { item in
|
ForEach(list, id: \.self) { item in
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
Text("\u{2022}")
|
Text("\u{2022}")
|
||||||
Text(ObservableRecipeDetail.applyMarkdownStyling(item))
|
Text("\(item)")
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
}
|
}
|
||||||
.padding(4)
|
.padding(4)
|
||||||
@@ -53,7 +53,7 @@ struct EditableText: View {
|
|||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder)
|
||||||
.lineLimit(lineLimit)
|
.lineLimit(lineLimit)
|
||||||
} else {
|
} else {
|
||||||
Text(ObservableRecipeDetail.applyMarkdownStyling(text))
|
Text(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ fileprivate struct IngredientListItem: View {
|
|||||||
.foregroundStyle(.red)
|
.foregroundStyle(.red)
|
||||||
}
|
}
|
||||||
if unmodified {
|
if unmodified {
|
||||||
Text(ObservableRecipeDetail.applyMarkdownStyling(ingredient))
|
Text(ingredient)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
.lineLimit(5)
|
.lineLimit(5)
|
||||||
} else {
|
} else {
|
||||||
@@ -142,9 +142,9 @@ fileprivate struct IngredientListItem: View {
|
|||||||
}
|
}
|
||||||
.onChange(of: servings) { newServings in
|
.onChange(of: servings) { newServings in
|
||||||
if recipeYield == 0 {
|
if recipeYield == 0 {
|
||||||
modifiedIngredient = ObservableRecipeDetail.adjustIngredient(ObservableRecipeDetail.applyMarkdownStyling(ingredient), by: newServings)
|
modifiedIngredient = ObservableRecipeDetail.adjustIngredient(ingredient, by: newServings)
|
||||||
} else {
|
} else {
|
||||||
modifiedIngredient = ObservableRecipeDetail.adjustIngredient(ObservableRecipeDetail.applyMarkdownStyling(ingredient), by: newServings/recipeYield)
|
modifiedIngredient = ObservableRecipeDetail.adjustIngredient(ingredient, by: newServings/recipeYield)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.foregroundStyle(isSelected ? Color.secondary : Color.primary)
|
.foregroundStyle(isSelected ? Color.secondary : Color.primary)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ fileprivate struct RecipeInstructionListItem: View {
|
|||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
Text("\(index)")
|
Text("\(index)")
|
||||||
.monospaced()
|
.monospaced()
|
||||||
Text(ObservableRecipeDetail.applyMarkdownStyling(instruction))
|
Text(instruction)
|
||||||
}.padding(4)
|
}.padding(4)
|
||||||
.foregroundStyle(isSelected ? Color.secondary : Color.primary)
|
.foregroundStyle(isSelected ? Color.secondary : Color.primary)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
|
|||||||
@@ -11,46 +11,23 @@ import SwiftUI
|
|||||||
|
|
||||||
struct GroceryListTabView: View {
|
struct GroceryListTabView: View {
|
||||||
@EnvironmentObject var groceryList: GroceryList
|
@EnvironmentObject var groceryList: GroceryList
|
||||||
@State var newGroceries: String = ""
|
|
||||||
@FocusState private var isFocused: Bool
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
if groceryList.groceryDict.isEmpty {
|
if groceryList.groceryDict.isEmpty {
|
||||||
EmptyGroceryListView(newGroceries: $newGroceries)
|
EmptyGroceryListView()
|
||||||
} else {
|
} else {
|
||||||
List {
|
List {
|
||||||
HStack(alignment: .top) {
|
|
||||||
TextEditor(text: $newGroceries)
|
|
||||||
.padding(4)
|
|
||||||
.overlay(RoundedRectangle(cornerRadius: 8)
|
|
||||||
.stroke(Color.secondary).opacity(0.5))
|
|
||||||
.focused($isFocused)
|
|
||||||
Button {
|
|
||||||
if !newGroceries.isEmpty {
|
|
||||||
let items = newGroceries
|
|
||||||
.split(separator: "\n")
|
|
||||||
.compactMap { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
||||||
.filter { !$0.isEmpty }
|
|
||||||
groceryList.addItems(items, toRecipe: "Other", recipeName: String(localized: "Other"))
|
|
||||||
}
|
|
||||||
newGroceries = ""
|
|
||||||
|
|
||||||
} label: {
|
|
||||||
Text("Add")
|
|
||||||
}
|
|
||||||
.disabled(newGroceries.isEmpty)
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
}
|
|
||||||
|
|
||||||
ForEach(groceryList.groceryDict.keys.sorted(), id: \.self) { key in
|
ForEach(groceryList.groceryDict.keys.sorted(), id: \.self) { key in
|
||||||
Section {
|
Section {
|
||||||
ForEach(groceryList.groceryDict[key]!.items) { item in
|
ForEach(groceryList.groceryDict[key]!.items) { item in
|
||||||
GroceryListItemView(item: item, toggleAction: {
|
GroceryListItemView(item: item, toggleAction: {
|
||||||
groceryList.toggleItemChecked(item)
|
groceryList.toggleItemChecked(item)
|
||||||
|
groceryList.objectWillChange.send()
|
||||||
}, deleteAction: {
|
}, deleteAction: {
|
||||||
|
groceryList.deleteItem(item.name, fromRecipe: key)
|
||||||
withAnimation {
|
withAnimation {
|
||||||
groceryList.deleteItem(item.name, fromRecipe: key)
|
groceryList.objectWillChange.send()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -80,9 +57,6 @@ struct GroceryListTabView: View {
|
|||||||
.foregroundStyle(Color.nextcloudBlue)
|
.foregroundStyle(Color.nextcloudBlue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onTapGesture {
|
|
||||||
isFocused = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,10 +97,6 @@ fileprivate struct GroceryListItemView: View {
|
|||||||
|
|
||||||
|
|
||||||
fileprivate struct EmptyGroceryListView: View {
|
fileprivate struct EmptyGroceryListView: View {
|
||||||
@EnvironmentObject var groceryList: GroceryList
|
|
||||||
@Binding var newGroceries: String
|
|
||||||
@FocusState private var isFocused: Bool
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
Text("You're all set for cooking 🍓")
|
Text("You're all set for cooking 🍓")
|
||||||
@@ -135,35 +105,8 @@ fileprivate struct EmptyGroceryListView: View {
|
|||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
Text("Your grocery list is stored locally and therefore not synchronized across your devices.")
|
Text("Your grocery list is stored locally and therefore not synchronized across your devices.")
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
Text("To add grocieries manually, type them in the box below and press the button. To add multiple items at once, separate them by a new line.")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
HStack(alignment: .top) {
|
|
||||||
TextEditor(text: $newGroceries)
|
|
||||||
.padding(4)
|
|
||||||
.overlay(RoundedRectangle(cornerRadius: 8)
|
|
||||||
.stroke(Color.secondary).opacity(0.5))
|
|
||||||
.focused($isFocused)
|
|
||||||
Button {
|
|
||||||
if !newGroceries.isEmpty {
|
|
||||||
let items = newGroceries
|
|
||||||
.split(separator: "\n")
|
|
||||||
.compactMap { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
||||||
.filter { !$0.isEmpty }
|
|
||||||
groceryList.addItems(items, toRecipe: "Other", recipeName: String(localized: "Other"))
|
|
||||||
}
|
|
||||||
newGroceries = ""
|
|
||||||
} label: {
|
|
||||||
Text("Add")
|
|
||||||
}
|
|
||||||
.disabled(newGroceries.isEmpty)
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
}
|
|
||||||
.padding(.bottom, 4)
|
|
||||||
}
|
}
|
||||||
.navigationTitle("Grocery List")
|
.navigationTitle("Grocery List")
|
||||||
.onTapGesture {
|
|
||||||
isFocused = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,8 +152,6 @@ class GroceryRecipeItem: Identifiable, Codable {
|
|||||||
func addItem(_ itemName: String, toRecipe recipeId: String, recipeName: String? = nil, saveGroceryDict: Bool = true) {
|
func addItem(_ itemName: String, toRecipe recipeId: String, recipeName: String? = nil, saveGroceryDict: Bool = true) {
|
||||||
print("Adding item of recipe \(String(describing: recipeName))")
|
print("Adding item of recipe \(String(describing: recipeName))")
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|
||||||
|
|
||||||
if self.groceryDict[recipeId] != nil {
|
if self.groceryDict[recipeId] != nil {
|
||||||
self.groceryDict[recipeId]?.items.append(GroceryRecipeItem(itemName))
|
self.groceryDict[recipeId]?.items.append(GroceryRecipeItem(itemName))
|
||||||
} else {
|
} else {
|
||||||
@@ -219,9 +160,8 @@ class GroceryRecipeItem: Identifiable, Codable {
|
|||||||
}
|
}
|
||||||
if saveGroceryDict {
|
if saveGroceryDict {
|
||||||
self.save()
|
self.save()
|
||||||
|
self.objectWillChange.send()
|
||||||
}
|
}
|
||||||
self.objectWillChange.send()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,8 +169,8 @@ class GroceryRecipeItem: Identifiable, Codable {
|
|||||||
for item in items {
|
for item in items {
|
||||||
addItem(item, toRecipe: recipeId, recipeName: recipeName, saveGroceryDict: false)
|
addItem(item, toRecipe: recipeId, recipeName: recipeName, saveGroceryDict: false)
|
||||||
}
|
}
|
||||||
self.save()
|
save()
|
||||||
self.objectWillChange.send()
|
objectWillChange.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteItem(_ itemName: String, fromRecipe recipeId: String) {
|
func deleteItem(_ itemName: String, fromRecipe recipeId: String) {
|
||||||
@@ -242,28 +182,26 @@ class GroceryRecipeItem: Identifiable, Codable {
|
|||||||
groceryDict.removeValue(forKey: recipeId)
|
groceryDict.removeValue(forKey: recipeId)
|
||||||
}
|
}
|
||||||
save()
|
save()
|
||||||
self.objectWillChange.send()
|
objectWillChange.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteGroceryRecipe(_ recipeId: String) {
|
func deleteGroceryRecipe(_ recipeId: String) {
|
||||||
print("Deleting grocery recipe with id \(recipeId)")
|
print("Deleting grocery recipe with id \(recipeId)")
|
||||||
groceryDict.removeValue(forKey: recipeId)
|
groceryDict.removeValue(forKey: recipeId)
|
||||||
save()
|
save()
|
||||||
self.objectWillChange.send()
|
objectWillChange.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteAll() {
|
func deleteAll() {
|
||||||
print("Deleting all grocery items")
|
print("Deleting all grocery items")
|
||||||
groceryDict = [:]
|
groceryDict = [:]
|
||||||
save()
|
save()
|
||||||
self.objectWillChange.send()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggleItemChecked(_ groceryItem: GroceryRecipeItem) {
|
func toggleItemChecked(_ groceryItem: GroceryRecipeItem) {
|
||||||
print("Item checked: \(groceryItem.name)")
|
print("Item checked: \(groceryItem.name)")
|
||||||
groceryItem.isChecked.toggle()
|
groceryItem.isChecked.toggle()
|
||||||
save()
|
save()
|
||||||
self.objectWillChange.send()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func containsItem(at recipeId: String, item: String) -> Bool {
|
func containsItem(at recipeId: String, item: String) -> Bool {
|
||||||
|
|||||||
@@ -32,15 +32,17 @@ You can download the app from the AppStore:
|
|||||||
|
|
||||||
- [x] **Version 1.9**: Enhancements to recipe editing for better intuitiveness; user interface design improvements for recipe viewing.
|
- [x] **Version 1.9**: Enhancements to recipe editing for better intuitiveness; user interface design improvements for recipe viewing.
|
||||||
|
|
||||||
- [ ] **Version 1.10**: Recipe ingredient calculator: Enables calculation of ingredient quantities based on a specifiable yield number.
|
- [x] **Version 1.10**: Recipe ingredient calculator: Enables calculation of ingredient quantities based on a specifiable yield number.
|
||||||
|
|
||||||
- [ ] **Version 1.11**: Decoupling of internal recipe representation from the Nextcloud Cookbook recipe representation. This change provides increased flexibility for API updates and enables the introduction of features not currently supported by the Cookbook API, such as uploading images.
|
- [ ] **Version 1.11**: Decoupling of internal recipe representation from the Nextcloud Cookbook recipe representation. This change provides increased flexibility for API updates and enables the introduction of features not currently supported by the Cookbook API, such as uploading images. This update will take some time, but will therefore result in simpler, better maintainable code. Update: I will continue to work on this update in January 2024.
|
||||||
|
|
||||||
- [ ] **Version 1.12 and beyond** (Ideas for the future; integration not guaranteed!):
|
- [ ] **Version 1.12 and beyond** (Ideas for the future; integration not guaranteed!):
|
||||||
|
|
||||||
|
- Allow adding custom items to the grocery list.
|
||||||
|
|
||||||
- Fuzzy search for recipe names and keywords.
|
- Fuzzy search for recipe names and keywords.
|
||||||
|
|
||||||
- In-app timer for the cook time specified in a recipe.
|
- An in-app timer for the cook time specified in a recipe.
|
||||||
|
|
||||||
- Search for recipes based on left-over ingredients.
|
- Search for recipes based on left-over ingredients.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user