Compare commits
4 Commits
285e91a429
...
1.10.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
078c01808d | ||
|
|
31dd6c6926 | ||
|
|
d7272026bb | ||
|
|
6cecdcf1fd |
@@ -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.1;
|
MARKETING_VERSION = 1.10.2;
|
||||||
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.1;
|
MARKETING_VERSION = 1.10.2;
|
||||||
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,6 +170,9 @@ 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,6 +13,7 @@ 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
|
||||||
@@ -35,6 +36,7 @@ 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()
|
||||||
@@ -55,6 +57,7 @@ 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 ?? "")
|
||||||
@@ -77,6 +80,7 @@ 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(),
|
||||||
@@ -93,21 +97,59 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func adjustIngredient(_ ingredient: String, by factor: Double) -> AttributedString {
|
static func applyMarkdownStyling(_ text: String) -> 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 AttributedString(ingredient)
|
return ingredient
|
||||||
}
|
}
|
||||||
// Match mixed fractions first
|
// Match mixed fractions first
|
||||||
var matches = ObservableRecipeDetail.matchPatternAndMultiply(
|
var matches = ObservableRecipeDetail.matchPatternAndMultiply(
|
||||||
.mixedFraction,
|
.mixedFraction,
|
||||||
in: ingredient,
|
in: String(ingredient.characters),
|
||||||
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: ingredient,
|
in: String(ingredient.characters),
|
||||||
multFactor: factor,
|
multFactor: factor,
|
||||||
excludedRanges: matches.map({ tuple in tuple.1 })
|
excludedRanges: matches.map({ tuple in tuple.1 })
|
||||||
)
|
)
|
||||||
@@ -116,7 +158,7 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
matches.append(contentsOf:
|
matches.append(contentsOf:
|
||||||
ObservableRecipeDetail.matchPatternAndMultiply(
|
ObservableRecipeDetail.matchPatternAndMultiply(
|
||||||
.number,
|
.number,
|
||||||
in: ingredient,
|
in: String(ingredient.characters),
|
||||||
multFactor: factor,
|
multFactor: factor,
|
||||||
excludedRanges: matches.map({ tuple in tuple.1 })
|
excludedRanges: matches.map({ tuple in tuple.1 })
|
||||||
)
|
)
|
||||||
@@ -124,7 +166,8 @@ 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 = AttributedString(ingredient)
|
var 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,6 +38,7 @@ 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?
|
||||||
@@ -51,11 +52,12 @@ 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, 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, 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]) {
|
||||||
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
|
||||||
@@ -76,6 +78,7 @@ struct RecipeDetail: Codable {
|
|||||||
keywords = ""
|
keywords = ""
|
||||||
dateCreated = ""
|
dateCreated = ""
|
||||||
dateModified = ""
|
dateModified = ""
|
||||||
|
image = ""
|
||||||
imageUrl = ""
|
imageUrl = ""
|
||||||
id = ""
|
id = ""
|
||||||
prepTime = ""
|
prepTime = ""
|
||||||
@@ -93,7 +96,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, imageUrl, id, prepTime, cookTime, totalTime, description, url, recipeYield, recipeCategory, tool, recipeIngredient, recipeInstructions, nutrition
|
case name, keywords, dateCreated, dateModified, image, imageUrl, id, prepTime, cookTime, totalTime, description, url, recipeYield, recipeCategory, tool, recipeIngredient, recipeInstructions, nutrition
|
||||||
}
|
}
|
||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
init(from decoder: Decoder) throws {
|
||||||
@@ -102,6 +105,7 @@ 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)
|
||||||
@@ -109,13 +113,14 @@ 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)
|
recipeYield = try container.decode(Int?.self, forKey: .recipeYield) ?? 1
|
||||||
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) }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +132,7 @@ extension RecipeDetail {
|
|||||||
keywords: "",
|
keywords: "",
|
||||||
dateCreated: "",
|
dateCreated: "",
|
||||||
dateModified: "",
|
dateModified: "",
|
||||||
|
image: "",
|
||||||
imageUrl: "",
|
imageUrl: "",
|
||||||
id: "",
|
id: "",
|
||||||
prepTime: "",
|
prepTime: "",
|
||||||
|
|||||||
@@ -485,7 +485,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Add" : {
|
"Add" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3832,6 +3831,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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("\(item)")
|
Text(ObservableRecipeDetail.applyMarkdownStyling(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(text)
|
Text(ObservableRecipeDetail.applyMarkdownStyling(text))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ fileprivate struct IngredientListItem: View {
|
|||||||
.foregroundStyle(.red)
|
.foregroundStyle(.red)
|
||||||
}
|
}
|
||||||
if unmodified {
|
if unmodified {
|
||||||
Text(ingredient)
|
Text(ObservableRecipeDetail.applyMarkdownStyling(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(ingredient, by: newServings)
|
modifiedIngredient = ObservableRecipeDetail.adjustIngredient(ObservableRecipeDetail.applyMarkdownStyling(ingredient), by: newServings)
|
||||||
} else {
|
} else {
|
||||||
modifiedIngredient = ObservableRecipeDetail.adjustIngredient(ingredient, by: newServings/recipeYield)
|
modifiedIngredient = ObservableRecipeDetail.adjustIngredient(ObservableRecipeDetail.applyMarkdownStyling(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(instruction)
|
Text(ObservableRecipeDetail.applyMarkdownStyling(instruction))
|
||||||
}.padding(4)
|
}.padding(4)
|
||||||
.foregroundStyle(isSelected ? Color.secondary : Color.primary)
|
.foregroundStyle(isSelected ? Color.secondary : Color.primary)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
|
|||||||
@@ -11,23 +11,46 @@ 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()
|
EmptyGroceryListView(newGroceries: $newGroceries)
|
||||||
} 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.objectWillChange.send()
|
groceryList.deleteItem(item.name, fromRecipe: key)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -57,6 +80,9 @@ struct GroceryListTabView: View {
|
|||||||
.foregroundStyle(Color.nextcloudBlue)
|
.foregroundStyle(Color.nextcloudBlue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
isFocused = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,6 +123,10 @@ 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 🍓")
|
||||||
@@ -105,8 +135,35 @@ 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +209,8 @@ 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 {
|
||||||
@@ -160,8 +219,9 @@ class GroceryRecipeItem: Identifiable, Codable {
|
|||||||
}
|
}
|
||||||
if saveGroceryDict {
|
if saveGroceryDict {
|
||||||
self.save()
|
self.save()
|
||||||
self.objectWillChange.send()
|
|
||||||
}
|
}
|
||||||
|
self.objectWillChange.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,8 +229,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)
|
||||||
}
|
}
|
||||||
save()
|
self.save()
|
||||||
objectWillChange.send()
|
self.objectWillChange.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteItem(_ itemName: String, fromRecipe recipeId: String) {
|
func deleteItem(_ itemName: String, fromRecipe recipeId: String) {
|
||||||
@@ -182,26 +242,28 @@ class GroceryRecipeItem: Identifiable, Codable {
|
|||||||
groceryDict.removeValue(forKey: recipeId)
|
groceryDict.removeValue(forKey: recipeId)
|
||||||
}
|
}
|
||||||
save()
|
save()
|
||||||
objectWillChange.send()
|
self.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()
|
||||||
objectWillChange.send()
|
self.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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user