From fb6b16c1fc0bedec53d9970eed388ac6a0ab2cab Mon Sep 17 00:00:00 2001 From: Hendrik Hogertz Date: Sun, 15 Feb 2026 07:13:01 +0100 Subject: [PATCH] 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 --- Nextcloud Cookbook iOS Client/AppState.swift | 2 + .../Data/ObservableRecipeDetail.swift | 6 +- .../Localizable.xcstrings | 91 ++++++++++++++++++- .../Views/Recipes/RecipeView.swift | 3 + .../RecipeMetadataSection.swift | 40 +++++++- .../Views/SettingsView.swift | 2 +- .../Views/Tabs/RecipeTabView.swift | 1 - 7 files changed, 131 insertions(+), 14 deletions(-) diff --git a/Nextcloud Cookbook iOS Client/AppState.swift b/Nextcloud Cookbook iOS Client/AppState.swift index c44ec92..8ea4a10 100644 --- a/Nextcloud Cookbook iOS Client/AppState.swift +++ b/Nextcloud Cookbook iOS Client/AppState.swift @@ -374,6 +374,8 @@ import UIKit }) recipeDetails.removeValue(forKey: id) } + recentRecipes.removeAll { $0.recipe_id == id } + await saveLocal(recentRecipes, path: "recent_recipes.data") return nil } diff --git a/Nextcloud Cookbook iOS Client/Data/ObservableRecipeDetail.swift b/Nextcloud Cookbook iOS Client/Data/ObservableRecipeDetail.swift index 12fe2a9..33e0c3c 100644 --- a/Nextcloud Cookbook iOS Client/Data/ObservableRecipeDetail.swift +++ b/Nextcloud Cookbook iOS Client/Data/ObservableRecipeDetail.swift @@ -45,7 +45,7 @@ class ObservableRecipeDetail: ObservableObject { description = "" url = "" recipeYield = 1 - recipeCategory = "" + recipeCategory = "*" tool = [] recipeIngredient = [] recipeInstructions = [] @@ -67,7 +67,7 @@ class ObservableRecipeDetail: ObservableObject { description = recipeDetail.description url = recipeDetail.url ?? "" 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 recipeIngredient = recipeDetail.recipeIngredient recipeInstructions = recipeDetail.recipeInstructions @@ -92,7 +92,7 @@ class ObservableRecipeDetail: ObservableObject { description: self.description, url: self.url, recipeYield: self.recipeYield, - recipeCategory: self.recipeCategory, + recipeCategory: self.recipeCategory == "*" ? "" : self.recipeCategory, tool: self.tool, recipeIngredient: self.recipeIngredient, recipeInstructions: self.recipeInstructions, diff --git a/Nextcloud Cookbook iOS Client/Localizable.xcstrings b/Nextcloud Cookbook iOS Client/Localizable.xcstrings index 0966c6a..ebe0c1f 100644 --- a/Nextcloud Cookbook iOS Client/Localizable.xcstrings +++ b/Nextcloud Cookbook iOS Client/Localizable.xcstrings @@ -494,7 +494,6 @@ } }, "Add" : { - "extractionState" : "stale", "localizations" : { "de" : { "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: %@" : { "extractionState" : "stale", "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" : { "extractionState" : "stale", "localizations" : { @@ -3341,7 +3406,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Andere" + "value" : "Sonstige" } }, "es" : { @@ -4229,8 +4294,26 @@ } }, "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", - "isCommentAutoGenerated" : true + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Portionen: %lld" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Porciones: %lld" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Portions : %lld" + } + } + } }, "Settings" : { "localizations" : { diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeView.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeView.swift index 3da0678..e40d17b 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeView.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeView.swift @@ -487,6 +487,8 @@ struct RecipeViewToolBar: ToolbarContent { let _ = await appState.getRecipe(id: id, fetchMode: .onlyServer, save: true) // Fetch the image after upload so it displays in view mode 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 viewModel.recipe = Recipe( name: viewModel.observableRecipeDetail.name, @@ -497,6 +499,7 @@ struct RecipeViewToolBar: ToolbarContent { imagePlaceholderUrl: "", recipe_id: id ) + appState.addToRecentRecipes(viewModel.recipe) } viewModel.newRecipe = false viewModel.editMode = false diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeMetadataSection.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeMetadataSection.swift index f197aba..b77f750 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeMetadataSection.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeMetadataSection.swift @@ -33,9 +33,8 @@ struct RecipeMetadataSection: View { .textFieldStyle(.roundedBorder) Picker("Choose", selection: $viewModel.observableRecipeDetail.recipeCategory) { - Text("").tag("") ForEach(categories, id: \.self) { item in - Text(item) + Text(item == "*" ? String(localized: "Other") : item).tag(item) } } .pickerStyle(.menu) @@ -93,19 +92,50 @@ struct RecipeEditMetadataSection: View { @EnvironmentObject var appState: AppState @ObservedObject var viewModel: RecipeView.ViewModel + @State private var showNewCategoryAlert = false + @State private var newCategoryName = "" + + private let newCategoryTag = "\0_new_category_" + 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 { Section("Details") { Picker("Category", selection: $viewModel.observableRecipeDetail.recipeCategory) { - Text("None").tag("") 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) + .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) diff --git a/Nextcloud Cookbook iOS Client/Views/SettingsView.swift b/Nextcloud Cookbook iOS Client/Views/SettingsView.swift index 0fc54d4..2fd032e 100644 --- a/Nextcloud Cookbook iOS Client/Views/SettingsView.swift +++ b/Nextcloud Cookbook iOS Client/Views/SettingsView.swift @@ -26,7 +26,7 @@ struct SettingsView: View { Picker("Select a default cookbook", selection: $userSettings.defaultCategory) { Text("None").tag("None") 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: { diff --git a/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift b/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift index 5b829d1..63c3855 100644 --- a/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift +++ b/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift @@ -163,7 +163,6 @@ struct RecipeTabView: View { } } } - .tint(.primary) .sheet(isPresented: $viewModel.showImportURLSheet) { ImportURLSheet { recipeDetail in viewModel.navigateToImportedRecipe(recipeDetail: recipeDetail)