Fix category handling, recipe management, and dark mode toggle tint
- Map uncategorized category between * (internal) and empty string (API) so selecting Sonstige/Other correctly persists to the server - Default new recipes to Other (*) category and remove None option - Add "New Category" option to category picker in recipe edit view - Include newly created/imported recipes in recently viewed list and pre-fetch thumbnails so images display immediately - Remove deleted recipes from recently viewed list - Remove broad .tint(.primary) from RecipeTabView that caused white toggles in Settings during dark mode - Rename German "Other" translation from Andere to Sonstige - Add missing translations for Servings stepper and new category strings Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -374,6 +374,8 @@ import UIKit
|
|||||||
})
|
})
|
||||||
recipeDetails.removeValue(forKey: id)
|
recipeDetails.removeValue(forKey: id)
|
||||||
}
|
}
|
||||||
|
recentRecipes.removeAll { $0.recipe_id == id }
|
||||||
|
await saveLocal(recentRecipes, path: "recent_recipes.data")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
description = ""
|
description = ""
|
||||||
url = ""
|
url = ""
|
||||||
recipeYield = 1
|
recipeYield = 1
|
||||||
recipeCategory = ""
|
recipeCategory = "*"
|
||||||
tool = []
|
tool = []
|
||||||
recipeIngredient = []
|
recipeIngredient = []
|
||||||
recipeInstructions = []
|
recipeInstructions = []
|
||||||
@@ -67,7 +67,7 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
description = recipeDetail.description
|
description = recipeDetail.description
|
||||||
url = recipeDetail.url ?? ""
|
url = recipeDetail.url ?? ""
|
||||||
recipeYield = recipeDetail.recipeYield == 0 ? 1 : recipeDetail.recipeYield // Recipe yield should not be zero
|
recipeYield = recipeDetail.recipeYield == 0 ? 1 : recipeDetail.recipeYield // Recipe yield should not be zero
|
||||||
recipeCategory = recipeDetail.recipeCategory
|
recipeCategory = recipeDetail.recipeCategory.isEmpty ? "*" : recipeDetail.recipeCategory
|
||||||
tool = recipeDetail.tool
|
tool = recipeDetail.tool
|
||||||
recipeIngredient = recipeDetail.recipeIngredient
|
recipeIngredient = recipeDetail.recipeIngredient
|
||||||
recipeInstructions = recipeDetail.recipeInstructions
|
recipeInstructions = recipeDetail.recipeInstructions
|
||||||
@@ -92,7 +92,7 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
description: self.description,
|
description: self.description,
|
||||||
url: self.url,
|
url: self.url,
|
||||||
recipeYield: self.recipeYield,
|
recipeYield: self.recipeYield,
|
||||||
recipeCategory: self.recipeCategory,
|
recipeCategory: self.recipeCategory == "*" ? "" : self.recipeCategory,
|
||||||
tool: self.tool,
|
tool: self.tool,
|
||||||
recipeIngredient: self.recipeIngredient,
|
recipeIngredient: self.recipeIngredient,
|
||||||
recipeInstructions: self.recipeInstructions,
|
recipeInstructions: self.recipeInstructions,
|
||||||
|
|||||||
@@ -494,7 +494,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Add" : {
|
"Add" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -950,6 +949,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Category name" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Kategoriename"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Nombre de categoría"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Nom de catégorie"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Category: %@" : {
|
"Category: %@" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -2958,6 +2979,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"New Category" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Neue Kategorie"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Nueva categoría"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Nouvelle catégorie"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"New Category…" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Neue Kategorie …"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Nueva categoría…"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Nouvelle catégorie…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"New recipe" : {
|
"New recipe" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -3341,7 +3406,7 @@
|
|||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Andere"
|
"value" : "Sonstige"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"es" : {
|
"es" : {
|
||||||
@@ -4229,8 +4294,26 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Servings: %lld" : {
|
"Servings: %lld" : {
|
||||||
"comment" : "A stepper that allows the user to select the number of servings for a recipe. The first argument is a label describing the number of servings. The second argument is a binding to the number of servings, which is",
|
"localizations" : {
|
||||||
"isCommentAutoGenerated" : true
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Portionen: %lld"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Porciones: %lld"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Portions : %lld"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"Settings" : {
|
"Settings" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
@@ -487,6 +487,8 @@ struct RecipeViewToolBar: ToolbarContent {
|
|||||||
let _ = await appState.getRecipe(id: id, fetchMode: .onlyServer, save: true)
|
let _ = await appState.getRecipe(id: id, fetchMode: .onlyServer, save: true)
|
||||||
// Fetch the image after upload so it displays in view mode
|
// Fetch the image after upload so it displays in view mode
|
||||||
viewModel.recipeImage = await appState.getImage(id: id, size: .FULL, fetchMode: .onlyServer)
|
viewModel.recipeImage = await appState.getImage(id: id, size: .FULL, fetchMode: .onlyServer)
|
||||||
|
// Pre-fetch thumbnail so it's cached for recents and category lists
|
||||||
|
let _ = await appState.getImage(id: id, size: .THUMB, fetchMode: .onlyServer)
|
||||||
// Update recipe reference so the view tracks the server-assigned id
|
// Update recipe reference so the view tracks the server-assigned id
|
||||||
viewModel.recipe = Recipe(
|
viewModel.recipe = Recipe(
|
||||||
name: viewModel.observableRecipeDetail.name,
|
name: viewModel.observableRecipeDetail.name,
|
||||||
@@ -497,6 +499,7 @@ struct RecipeViewToolBar: ToolbarContent {
|
|||||||
imagePlaceholderUrl: "",
|
imagePlaceholderUrl: "",
|
||||||
recipe_id: id
|
recipe_id: id
|
||||||
)
|
)
|
||||||
|
appState.addToRecentRecipes(viewModel.recipe)
|
||||||
}
|
}
|
||||||
viewModel.newRecipe = false
|
viewModel.newRecipe = false
|
||||||
viewModel.editMode = false
|
viewModel.editMode = false
|
||||||
|
|||||||
@@ -33,9 +33,8 @@ struct RecipeMetadataSection: View {
|
|||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder)
|
||||||
|
|
||||||
Picker("Choose", selection: $viewModel.observableRecipeDetail.recipeCategory) {
|
Picker("Choose", selection: $viewModel.observableRecipeDetail.recipeCategory) {
|
||||||
Text("").tag("")
|
|
||||||
ForEach(categories, id: \.self) { item in
|
ForEach(categories, id: \.self) { item in
|
||||||
Text(item)
|
Text(item == "*" ? String(localized: "Other") : item).tag(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pickerStyle(.menu)
|
.pickerStyle(.menu)
|
||||||
@@ -93,19 +92,50 @@ struct RecipeEditMetadataSection: View {
|
|||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
@ObservedObject var viewModel: RecipeView.ViewModel
|
@ObservedObject var viewModel: RecipeView.ViewModel
|
||||||
|
|
||||||
|
@State private var showNewCategoryAlert = false
|
||||||
|
@State private var newCategoryName = ""
|
||||||
|
|
||||||
|
private let newCategoryTag = "\0_new_category_"
|
||||||
|
|
||||||
var categories: [String] {
|
var categories: [String] {
|
||||||
appState.categories.map { $0.name }
|
var list = appState.categories.map { $0.name }
|
||||||
|
let current = viewModel.observableRecipeDetail.recipeCategory
|
||||||
|
if !current.isEmpty && current != newCategoryTag && !list.contains(current) {
|
||||||
|
list.append(current)
|
||||||
|
}
|
||||||
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section("Details") {
|
Section("Details") {
|
||||||
Picker("Category", selection: $viewModel.observableRecipeDetail.recipeCategory) {
|
Picker("Category", selection: $viewModel.observableRecipeDetail.recipeCategory) {
|
||||||
Text("None").tag("")
|
|
||||||
ForEach(categories, id: \.self) { item in
|
ForEach(categories, id: \.self) { item in
|
||||||
Text(item).tag(item)
|
Text(item == "*" ? String(localized: "Other") : item).tag(item)
|
||||||
}
|
}
|
||||||
|
Divider()
|
||||||
|
Text("New Category…").tag(newCategoryTag)
|
||||||
}
|
}
|
||||||
.pickerStyle(.menu)
|
.pickerStyle(.menu)
|
||||||
|
.onChange(of: viewModel.observableRecipeDetail.recipeCategory) { _, newValue in
|
||||||
|
if newValue == newCategoryTag {
|
||||||
|
newCategoryName = ""
|
||||||
|
showNewCategoryAlert = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert("New Category", isPresented: $showNewCategoryAlert) {
|
||||||
|
TextField("Category name", text: $newCategoryName)
|
||||||
|
Button("Cancel", role: .cancel) {
|
||||||
|
viewModel.observableRecipeDetail.recipeCategory = "*"
|
||||||
|
}
|
||||||
|
Button("Add") {
|
||||||
|
let trimmed = newCategoryName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if !trimmed.isEmpty {
|
||||||
|
viewModel.observableRecipeDetail.recipeCategory = trimmed
|
||||||
|
} else {
|
||||||
|
viewModel.observableRecipeDetail.recipeCategory = "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Stepper("Servings: \(viewModel.observableRecipeDetail.recipeYield)", value: $viewModel.observableRecipeDetail.recipeYield, in: 1...99)
|
Stepper("Servings: \(viewModel.observableRecipeDetail.recipeYield)", value: $viewModel.observableRecipeDetail.recipeYield, in: 1...99)
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ struct SettingsView: View {
|
|||||||
Picker("Select a default cookbook", selection: $userSettings.defaultCategory) {
|
Picker("Select a default cookbook", selection: $userSettings.defaultCategory) {
|
||||||
Text("None").tag("None")
|
Text("None").tag("None")
|
||||||
ForEach(appState.categories, id: \.name) { category in
|
ForEach(appState.categories, id: \.name) { category in
|
||||||
Text(category.name == "*" ? "Other" : category.name).tag(category)
|
Text(category.name == "*" ? String(localized: "Other") : category.name).tag(category)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} header: {
|
} header: {
|
||||||
|
|||||||
@@ -163,7 +163,6 @@ struct RecipeTabView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(.primary)
|
|
||||||
.sheet(isPresented: $viewModel.showImportURLSheet) {
|
.sheet(isPresented: $viewModel.showImportURLSheet) {
|
||||||
ImportURLSheet { recipeDetail in
|
ImportURLSheet { recipeDetail in
|
||||||
viewModel.navigateToImportedRecipe(recipeDetail: recipeDetail)
|
viewModel.navigateToImportedRecipe(recipeDetail: recipeDetail)
|
||||||
|
|||||||
Reference in New Issue
Block a user