Redesign recipe creation and edit view with Form-based layout and URL import
Replace the single "+" button with a 2-option menu (Create New Recipe / Import from URL) across RecipeTabView, RecipeListView, and AllRecipesListView. Add ImportURLSheet for server-side recipe import with loading and error states. Completely redesign edit mode to use a native Form layout with inline editing for all sections (metadata, duration, ingredients, instructions, tools, nutrition) instead of the previous sheet-based EditableListView approach. Move delete action from edit toolbar to view mode context menu. Add recipe image display to the edit form. Refactor RecipeListView and AllRecipesListView to use closure-based callbacks instead of Binding<Bool> for the create/import actions. Add preloadedRecipeDetail support to RecipeView.ViewModel for imported recipes. Add DE/ES/FR translations for all new UI strings. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,7 @@
|
|||||||
A9CA6CF62B4C63F200F78AB5 /* TPPDF in Frameworks */ = {isa = PBXBuildFile; productRef = A9CA6CF52B4C63F200F78AB5 /* TPPDF */; };
|
A9CA6CF62B4C63F200F78AB5 /* TPPDF in Frameworks */ = {isa = PBXBuildFile; productRef = A9CA6CF52B4C63F200F78AB5 /* TPPDF */; };
|
||||||
A9D89AB02B4FE97800F49D92 /* TimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D89AAF2B4FE97800F49D92 /* TimerView.swift */; };
|
A9D89AB02B4FE97800F49D92 /* TimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D89AAF2B4FE97800F49D92 /* TimerView.swift */; };
|
||||||
A9D8F9052B99F3E5009BACAE /* RecipeImportSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D8F9042B99F3E4009BACAE /* RecipeImportSection.swift */; };
|
A9D8F9052B99F3E5009BACAE /* RecipeImportSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D8F9042B99F3E4009BACAE /* RecipeImportSection.swift */; };
|
||||||
|
C1F0AB022D0B000100000001 /* ImportURLSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F0AB012D0B000100000001 /* ImportURLSheet.swift */; };
|
||||||
A9E78A2B2BE7799F00206866 /* JsonAny.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E78A2A2BE7799F00206866 /* JsonAny.swift */; };
|
A9E78A2B2BE7799F00206866 /* JsonAny.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E78A2A2BE7799F00206866 /* JsonAny.swift */; };
|
||||||
A9FA2AB62B5079B200A43702 /* alarm_sound_0.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = A9FA2AB52B5079B200A43702 /* alarm_sound_0.mp3 */; };
|
A9FA2AB62B5079B200A43702 /* alarm_sound_0.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = A9FA2AB52B5079B200A43702 /* alarm_sound_0.mp3 */; };
|
||||||
D1A0CE012D0A000100000001 /* GroceryListMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A0CE002D0A000100000001 /* GroceryListMode.swift */; };
|
D1A0CE012D0A000100000001 /* GroceryListMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A0CE002D0A000100000001 /* GroceryListMode.swift */; };
|
||||||
@@ -153,6 +154,7 @@
|
|||||||
A9CA6CEE2B4C086100F78AB5 /* RecipeExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeExporter.swift; sourceTree = "<group>"; };
|
A9CA6CEE2B4C086100F78AB5 /* RecipeExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeExporter.swift; sourceTree = "<group>"; };
|
||||||
A9D89AAF2B4FE97800F49D92 /* TimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerView.swift; sourceTree = "<group>"; };
|
A9D89AAF2B4FE97800F49D92 /* TimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerView.swift; sourceTree = "<group>"; };
|
||||||
A9D8F9042B99F3E4009BACAE /* RecipeImportSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeImportSection.swift; sourceTree = "<group>"; };
|
A9D8F9042B99F3E4009BACAE /* RecipeImportSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeImportSection.swift; sourceTree = "<group>"; };
|
||||||
|
C1F0AB012D0B000100000001 /* ImportURLSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportURLSheet.swift; sourceTree = "<group>"; };
|
||||||
A9DA25D42B82096B0061FC2B /* Nextcloud-Cookbook-iOS-Client-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Nextcloud-Cookbook-iOS-Client-Info.plist"; sourceTree = SOURCE_ROOT; };
|
A9DA25D42B82096B0061FC2B /* Nextcloud-Cookbook-iOS-Client-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Nextcloud-Cookbook-iOS-Client-Info.plist"; sourceTree = SOURCE_ROOT; };
|
||||||
A9E78A2A2BE7799F00206866 /* JsonAny.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonAny.swift; sourceTree = "<group>"; };
|
A9E78A2A2BE7799F00206866 /* JsonAny.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonAny.swift; sourceTree = "<group>"; };
|
||||||
A9FA2AB52B5079B200A43702 /* alarm_sound_0.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = alarm_sound_0.mp3; sourceTree = "<group>"; };
|
A9FA2AB52B5079B200A43702 /* alarm_sound_0.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = alarm_sound_0.mp3; sourceTree = "<group>"; };
|
||||||
@@ -406,6 +408,7 @@
|
|||||||
A97506112B920D8100E86029 /* RecipeViewSections */,
|
A97506112B920D8100E86029 /* RecipeViewSections */,
|
||||||
A9D89AAF2B4FE97800F49D92 /* TimerView.swift */,
|
A9D89AAF2B4FE97800F49D92 /* TimerView.swift */,
|
||||||
A97B4D342B80B82A00EC1A88 /* ShareView.swift */,
|
A97B4D342B80B82A00EC1A88 /* ShareView.swift */,
|
||||||
|
C1F0AB012D0B000100000001 /* ImportURLSheet.swift */,
|
||||||
);
|
);
|
||||||
path = Recipes;
|
path = Recipes;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -578,7 +581,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
A9D8F9052B99F3E5009BACAE /* RecipeImportSection.swift in Sources */,
|
C1F0AB022D0B000100000001 /* ImportURLSheet.swift in Sources */,
|
||||||
A9BBB38E2B8E44B3002DA7FF /* BottomClipper.swift in Sources */,
|
A9BBB38E2B8E44B3002DA7FF /* BottomClipper.swift in Sources */,
|
||||||
A97506192B920EC200E86029 /* RecipeIngredientSection.swift in Sources */,
|
A97506192B920EC200E86029 /* RecipeIngredientSection.swift in Sources */,
|
||||||
A97B4D352B80B82A00EC1A88 /* ShareView.swift in Sources */,
|
A97B4D352B80B82A00EC1A88 /* ShareView.swift in Sources */,
|
||||||
|
|||||||
@@ -317,7 +317,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"%lld." : {
|
"%lld." : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -518,6 +517,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Add cooking steps for fellow chefs to follow." : {
|
"Add cooking steps for fellow chefs to follow." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -561,6 +561,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Add Ingredient" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Zutat hinzufügen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Añadir ingrediente"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Ajouter un ingrédient"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Add new recipe" : {
|
"Add new recipe" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -583,6 +605,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Add Step" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Schritt hinzufügen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Añadir paso"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Ajouter une étape"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Add Tool" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Werkzeug hinzufügen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Añadir utensilio"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Ajouter un ustensile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"All Recipes" : {
|
"All Recipes" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -694,6 +760,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Apple Reminders" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Apple Erinnerungen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Recordatorios de Apple"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Rappels Apple"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Bad URL" : {
|
"Bad URL" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -1139,6 +1227,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Create New Recipe" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Neues Rezept erstellen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Crear nueva receta"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Créer une nouvelle recette"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Created: %@" : {
|
"Created: %@" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -1360,6 +1470,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Details" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Details"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Detalles"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Détails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Done" : {
|
"Done" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -1460,6 +1592,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Duration" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Dauer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Duración"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Durée"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"e.g.: example.com" : {
|
"e.g.: example.com" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -1504,6 +1658,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Edit Recipe" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Rezept bearbeiten"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Editar receta"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Modifier la recette"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Enter a recipe name or keyword to get started." : {
|
"Enter a recipe name or keyword to get started." : {
|
||||||
"comment" : "A description under the magnifying glass icon in the \"Search for recipes\" view, encouraging the user to start searching.",
|
"comment" : "A description under the magnifying glass icon in the \"Search for recipes\" view, encouraging the user to start searching.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -1738,6 +1914,72 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Grant Reminders Access" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Zugriff auf Erinnerungen erlauben"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Permitir acceso a Recordatorios"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Autoriser l'accès aux Rappels"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Grocery items are stored locally on this device." : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Einkaufsartikel werden lokal auf diesem Gerät gespeichert."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Los artículos de la lista se almacenan localmente en este dispositivo."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Les articles sont stockés localement sur cet appareil."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Grocery items will be saved to Apple Reminders. The Grocery List tab will be hidden since you can manage items directly in the Reminders app." : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Einkaufsartikel werden in Apple Erinnerungen gespeichert. Der Einkaufslisten-Tab wird ausgeblendet, da du die Artikel direkt in der Erinnerungen-App verwalten kannst."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Los artículos de la lista se guardarán en Recordatorios de Apple. La pestaña de lista de compras se ocultará, ya que puedes gestionar los artículos directamente en la app Recordatorios."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Les articles seront enregistrés dans Rappels Apple. L'onglet Liste de courses sera masqué car vous pouvez gérer les articles directement dans l'app Rappels."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Grocery List" : {
|
"Grocery List" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -1760,6 +2002,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Grocery list storage" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Einkaufsliste Speicherort"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Almacenamiento de la lista de compras"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Stockage de la liste de courses"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Hours" : {
|
"Hours" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -1892,6 +2156,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Import Failed" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Import fehlgeschlagen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Error de importación"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Échec de l'importation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Import from URL" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Von URL importieren"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Importar desde URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Importer depuis une URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Import Recipe" : {
|
"Import Recipe" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -1914,6 +2222,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"In-App" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "In der App"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "En la app"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Dans l'app"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Ingredient" : {
|
"Ingredient" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -2005,6 +2335,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Instruction" : {
|
"Instruction" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2159,6 +2490,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"List your tools here. 🍴" : {
|
"List your tools here. 🍴" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2589,16 +2921,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"No recipes in this cookbook" : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Keine Rezepte in dieser Kategorie"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"No recipes found" : {
|
"No recipes found" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -2621,6 +2943,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"No recipes in this cookbook" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Keine Rezepte in dieser Kategorie"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"No results found" : {
|
"No results found" : {
|
||||||
"comment" : "A message indicating that no recipes were found for the current search query.",
|
"comment" : "A message indicating that no recipes were found for the current search query.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -2721,6 +3053,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Nutrition Information" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Nährwertinformationen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Información nutricional"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Informations nutritionnelles"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Offline recipes" : {
|
"Offline recipes" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -2765,6 +3119,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"OK" : {
|
||||||
|
"comment" : "The text for an OK button.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"On server" : {
|
"On server" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -2776,6 +3134,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Open Settings" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Einstellungen öffnen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Abrir Ajustes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Ouvrir les Réglages"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Other" : {
|
"Other" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -2822,6 +3202,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Paste the url of a recipe you would like to import in the above, and we will try to fill in the fields for you. This feature does not work with every website. If your favourite website is not supported, feel free to reach out for help. You can find the contact details in the app settings." : {
|
"Paste the url of a recipe you would like to import in the above, and we will try to fill in the fields for you. This feature does not work with every website. If your favourite website is not supported, feel free to reach out for help. You can find the contact details in the app settings." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2843,6 +3224,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Paste the URL of a recipe you would like to import." : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Füge die URL eines Rezepts ein, das du importieren möchtest."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Pega la URL de una receta que deseas importar."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Collez l'URL d'une recette que vous souhaitez importer."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"PDF Document" : {
|
"PDF Document" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -3118,6 +3521,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Recipe URL" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Rezept-URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "URL de la receta"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "URL de la recette"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Recipes" : {
|
"Recipes" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -3196,6 +3621,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Reminders access was denied. Please enable it in System Settings to use this feature." : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Der Zugriff auf Erinnerungen wurde verweigert. Bitte aktiviere ihn in den Systemeinstellungen, um diese Funktion zu nutzen."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Se denegó el acceso a Recordatorios. Actívalo en los Ajustes del sistema para usar esta función."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "L'accès aux Rappels a été refusé. Veuillez l'activer dans les Réglages système pour utiliser cette fonctionnalité."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Reminders list" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Erinnerungsliste"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Lista de recordatorios"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Liste de rappels"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Same as Device" : {
|
"Same as Device" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -3465,6 +3934,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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
|
||||||
|
},
|
||||||
"Settings" : {
|
"Settings" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -3622,6 +4095,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Start by adding your first ingredient! 🥬" : {
|
"Start by adding your first ingredient! 🥬" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3643,6 +4117,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Step %lld" : {
|
||||||
|
"comment" : "A text field where the user can enter a recipe instruction.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Store recipe images locally" : {
|
"Store recipe images locally" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -3842,6 +4320,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"The recipe could not be imported. Please check the URL and try again." : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Das Rezept konnte nicht importiert werden. Bitte überprüfe die URL und versuche es erneut."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "No se pudo importar la receta. Por favor, verifica la URL e inténtalo de nuevo."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "La recette n'a pas pu être importée. Veuillez vérifier l'URL et réessayer."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"The recipe has no image whose MIME type matches the Accept header" : {
|
"The recipe has no image whose MIME type matches the Accept header" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -4371,6 +4871,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"URL (e.g. example.com/recipe)" : {
|
"URL (e.g. example.com/recipe)" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -4523,204 +5024,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"In-App" : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "In der App"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "En la app"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Dans l'app"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Apple Reminders" : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Apple Erinnerungen"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Recordatorios de Apple"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Rappels Apple"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Grocery list storage" : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Einkaufsliste Speicherort"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Almacenamiento de la lista de compras"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Stockage de la liste de courses"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Reminders list" : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Erinnerungsliste"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Lista de recordatorios"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Liste de rappels"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Grant Reminders Access" : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Zugriff auf Erinnerungen erlauben"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Permitir acceso a Recordatorios"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Autoriser l'accès aux Rappels"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Open Settings" : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Einstellungen öffnen"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Abrir Ajustes"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Ouvrir les Réglages"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Reminders access was denied. Please enable it in System Settings to use this feature." : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Der Zugriff auf Erinnerungen wurde verweigert. Bitte aktiviere ihn in den Systemeinstellungen, um diese Funktion zu nutzen."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Se denegó el acceso a Recordatorios. Actívalo en los Ajustes del sistema para usar esta función."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "L'accès aux Rappels a été refusé. Veuillez l'activer dans les Réglages système pour utiliser cette fonctionnalité."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Grocery items will be saved to Apple Reminders. The Grocery List tab will be hidden since you can manage items directly in the Reminders app." : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Einkaufsartikel werden in Apple Erinnerungen gespeichert. Der Einkaufslisten-Tab wird ausgeblendet, da du die Artikel direkt in der Erinnerungen-App verwalten kannst."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Los artículos de la lista se guardarán en Recordatorios de Apple. La pestaña de lista de compras se ocultará, ya que puedes gestionar los artículos directamente en la app Recordatorios."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Les articles seront enregistrés dans Rappels Apple. L'onglet Liste de courses sera masqué car vous pouvez gérer les articles directement dans l'app Rappels."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Grocery items are stored locally on this device." : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Einkaufsartikel werden lokal auf diesem Gerät gespeichert."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Los artículos de la lista se almacenan localmente en este dispositivo."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Les articles sont stockés localement sur cet appareil."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version" : "1.1"
|
"version" : "1.1"
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import SwiftUI
|
|||||||
struct AllRecipesListView: View {
|
struct AllRecipesListView: View {
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
@EnvironmentObject var groceryList: GroceryListManager
|
@EnvironmentObject var groceryList: GroceryListManager
|
||||||
@Binding var showEditView: Bool
|
var onCreateNew: () -> Void
|
||||||
|
var onImportFromURL: () -> Void
|
||||||
@State private var allRecipes: [Recipe] = []
|
@State private var allRecipes: [Recipe] = []
|
||||||
@State private var searchText: String = ""
|
@State private var searchText: String = ""
|
||||||
|
|
||||||
@@ -67,8 +68,17 @@ struct AllRecipesListView: View {
|
|||||||
}
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .topBarTrailing) {
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
|
Menu {
|
||||||
Button {
|
Button {
|
||||||
showEditView = true
|
onCreateNew()
|
||||||
|
} label: {
|
||||||
|
Label("Create New Recipe", systemImage: "square.and.pencil")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
onImportFromURL()
|
||||||
|
} label: {
|
||||||
|
Label("Import from URL", systemImage: "link")
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "plus.circle.fill")
|
Image(systemName: "plus.circle.fill")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
//
|
||||||
|
// ImportURLSheet.swift
|
||||||
|
// Nextcloud Cookbook iOS Client
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ImportURLSheet: View {
|
||||||
|
@EnvironmentObject var appState: AppState
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
var onImport: (RecipeDetail) -> Void
|
||||||
|
|
||||||
|
@State private var url: String = ""
|
||||||
|
@State private var isLoading: Bool = false
|
||||||
|
@State private var presentAlert: Bool = false
|
||||||
|
@State private var alertMessage: String = ""
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
TextField("Recipe URL", text: $url)
|
||||||
|
.keyboardType(.URL)
|
||||||
|
.textContentType(.URL)
|
||||||
|
.autocorrectionDisabled()
|
||||||
|
.textInputAutocapitalization(.never)
|
||||||
|
} footer: {
|
||||||
|
Text("Paste the URL of a recipe you would like to import.")
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
await importRecipe()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
if isLoading {
|
||||||
|
ProgressView()
|
||||||
|
} else {
|
||||||
|
Text("Import")
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || isLoading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Import Recipe")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
|
Button("Cancel") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert("Import Failed", isPresented: $presentAlert) {
|
||||||
|
Button("OK", role: .cancel) { }
|
||||||
|
} message: {
|
||||||
|
Text(alertMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func importRecipe() async {
|
||||||
|
isLoading = true
|
||||||
|
let (recipeDetail, error) = await appState.importRecipe(url: url)
|
||||||
|
isLoading = false
|
||||||
|
|
||||||
|
if let recipeDetail {
|
||||||
|
dismiss()
|
||||||
|
onImport(recipeDetail)
|
||||||
|
} else {
|
||||||
|
alertMessage = error?.localizedDescription ?? String(localized: "The recipe could not be imported. Please check the URL and try again.")
|
||||||
|
presentAlert = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,8 @@ struct RecipeListView: View {
|
|||||||
@EnvironmentObject var groceryList: GroceryListManager
|
@EnvironmentObject var groceryList: GroceryListManager
|
||||||
@State var categoryName: String
|
@State var categoryName: String
|
||||||
@State var searchText: String = ""
|
@State var searchText: String = ""
|
||||||
@Binding var showEditView: Bool
|
var onCreateNew: () -> Void
|
||||||
|
var onImportFromURL: () -> Void
|
||||||
@State var selectedRecipe: Recipe? = nil
|
@State var selectedRecipe: Recipe? = nil
|
||||||
|
|
||||||
private let gridColumns = [GridItem(.adaptive(minimum: 160), spacing: 12)]
|
private let gridColumns = [GridItem(.adaptive(minimum: 160), spacing: 12)]
|
||||||
@@ -80,8 +81,17 @@ struct RecipeListView: View {
|
|||||||
}
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .topBarTrailing) {
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
|
Menu {
|
||||||
Button {
|
Button {
|
||||||
showEditView = true
|
onCreateNew()
|
||||||
|
} label: {
|
||||||
|
Label("Create New Recipe", systemImage: "square.and.pencil")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
onImportFromURL()
|
||||||
|
} label: {
|
||||||
|
Label("Import from URL", systemImage: "link")
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "plus.circle.fill")
|
Image(systemName: "plus.circle.fill")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,97 +28,16 @@ struct RecipeView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(showsIndicators: false) {
|
Group {
|
||||||
VStack(spacing: 0) {
|
if viewModel.editMode {
|
||||||
ParallaxHeader(
|
recipeEditForm
|
||||||
coordinateSpace: CoordinateSpaces.scrollView,
|
|
||||||
defaultHeight: imageHeight
|
|
||||||
) {
|
|
||||||
if let recipeImage = viewModel.recipeImage {
|
|
||||||
Image(uiImage: recipeImage)
|
|
||||||
.resizable()
|
|
||||||
.scaledToFill()
|
|
||||||
.frame(maxHeight: imageHeight + 200)
|
|
||||||
.clipped()
|
|
||||||
} else {
|
} else {
|
||||||
Rectangle()
|
recipeViewContent
|
||||||
.frame(height: 400)
|
|
||||||
.foregroundStyle(
|
|
||||||
LinearGradient(
|
|
||||||
gradient: Gradient(colors: [.ncGradientDark, .ncGradientLight]),
|
|
||||||
startPoint: .topLeading,
|
|
||||||
endPoint: .bottomTrailing
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
if viewModel.editMode {
|
|
||||||
RecipeImportSection(viewModel: viewModel, importRecipe: importRecipe)
|
|
||||||
}
|
|
||||||
|
|
||||||
if viewModel.editMode {
|
|
||||||
RecipeMetadataSection(viewModel: viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
EditableText(text: $viewModel.observableRecipeDetail.name, editMode: $viewModel.editMode, titleKey: "Recipe Name")
|
|
||||||
.font(.title)
|
|
||||||
.bold()
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if let isDownloaded = viewModel.isDownloaded {
|
|
||||||
Image(systemName: isDownloaded ? "checkmark.circle" : "icloud.and.arrow.down")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}.padding([.top, .horizontal])
|
|
||||||
|
|
||||||
if viewModel.observableRecipeDetail.description != "" || viewModel.editMode {
|
|
||||||
EditableText(text: $viewModel.observableRecipeDetail.description, editMode: $viewModel.editMode, titleKey: "Description", lineLimit: 0...5, axis: .vertical)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.top, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recipe Body Section
|
|
||||||
RecipeDurationSection(viewModel: viewModel)
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
|
|
||||||
if(!viewModel.observableRecipeDetail.recipeIngredient.isEmpty || viewModel.editMode) {
|
|
||||||
RecipeIngredientSection(viewModel: viewModel)
|
|
||||||
}
|
|
||||||
if(!viewModel.observableRecipeDetail.recipeInstructions.isEmpty || viewModel.editMode) {
|
|
||||||
RecipeInstructionSection(viewModel: viewModel)
|
|
||||||
}
|
|
||||||
if(!viewModel.observableRecipeDetail.tool.isEmpty || viewModel.editMode) {
|
|
||||||
RecipeToolSection(viewModel: viewModel)
|
|
||||||
}
|
|
||||||
RecipeNutritionSection(viewModel: viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !viewModel.editMode {
|
|
||||||
Divider()
|
|
||||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
|
|
||||||
RecipeKeywordSection(viewModel: viewModel)
|
|
||||||
MoreInformationSection(viewModel: viewModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 5)
|
|
||||||
.background(Rectangle().foregroundStyle(.background).shadow(radius: 5).mask(Rectangle().padding(.top, -20)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.coordinateSpace(name: CoordinateSpaces.scrollView)
|
|
||||||
.ignoresSafeArea(.container, edges: .top)
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar(.visible, for: .navigationBar)
|
.toolbar(.visible, for: .navigationBar)
|
||||||
//.toolbarTitleDisplayMode(.inline)
|
.navigationTitle(viewModel.editMode ? "Edit Recipe" : (viewModel.showTitle ? viewModel.recipe.name : ""))
|
||||||
.navigationTitle(viewModel.showTitle ? viewModel.recipe.name : "")
|
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
RecipeViewToolBar(viewModel: viewModel)
|
RecipeViewToolBar(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
@@ -127,37 +46,9 @@ struct RecipeView: View {
|
|||||||
recipeImage: viewModel.recipeImage,
|
recipeImage: viewModel.recipeImage,
|
||||||
presentShareSheet: $viewModel.presentShareSheet)
|
presentShareSheet: $viewModel.presentShareSheet)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $viewModel.presentInstructionEditView) {
|
.sheet(isPresented: $viewModel.presentKeywordSheet) {
|
||||||
EditableListView(
|
KeywordPickerView(title: "Keywords", searchSuggestions: appState.allKeywords, selection: $viewModel.observableRecipeDetail.keywords)
|
||||||
isPresented: $viewModel.presentInstructionEditView,
|
|
||||||
items: $viewModel.observableRecipeDetail.recipeInstructions,
|
|
||||||
title: "Instructions",
|
|
||||||
emptyListText: "Add cooking steps for fellow chefs to follow.",
|
|
||||||
titleKey: "Instruction",
|
|
||||||
lineLimit: 0...10,
|
|
||||||
axis: .vertical)
|
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $viewModel.presentIngredientEditView) {
|
|
||||||
EditableListView(
|
|
||||||
isPresented: $viewModel.presentIngredientEditView,
|
|
||||||
items: $viewModel.observableRecipeDetail.recipeIngredient,
|
|
||||||
title: "Ingredients",
|
|
||||||
emptyListText: "Start by adding your first ingredient! 🥬",
|
|
||||||
titleKey: "Ingredient",
|
|
||||||
lineLimit: 0...1,
|
|
||||||
axis: .horizontal)
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $viewModel.presentToolEditView) {
|
|
||||||
EditableListView(
|
|
||||||
isPresented: $viewModel.presentToolEditView,
|
|
||||||
items: $viewModel.observableRecipeDetail.tool,
|
|
||||||
title: "Tools",
|
|
||||||
emptyListText: "List your tools here. 🍴",
|
|
||||||
titleKey: "Tool",
|
|
||||||
lineLimit: 0...1,
|
|
||||||
axis: .horizontal)
|
|
||||||
}
|
|
||||||
|
|
||||||
.task {
|
.task {
|
||||||
// Load recipe detail
|
// Load recipe detail
|
||||||
if !viewModel.newRecipe {
|
if !viewModel.newRecipe {
|
||||||
@@ -186,7 +77,20 @@ struct RecipeView: View {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Prepare view for a new recipe
|
// Prepare view for a new recipe
|
||||||
|
if let preloaded = viewModel.preloadedRecipeDetail {
|
||||||
|
viewModel.setupView(recipeDetail: preloaded)
|
||||||
|
viewModel.preloadedRecipeDetail = nil
|
||||||
|
// Load image if the import created a recipe with a valid id
|
||||||
|
if let recipeId = Int(preloaded.id), recipeId > 0 {
|
||||||
|
viewModel.recipeImage = await appState.getImage(
|
||||||
|
id: recipeId,
|
||||||
|
size: .FULL,
|
||||||
|
fetchMode: .onlyServer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
viewModel.setupView(recipeDetail: RecipeDetail())
|
viewModel.setupView(recipeDetail: RecipeDetail())
|
||||||
|
}
|
||||||
viewModel.editMode = true
|
viewModel.editMode = true
|
||||||
viewModel.isDownloaded = false
|
viewModel.isDownloaded = false
|
||||||
}
|
}
|
||||||
@@ -231,6 +135,127 @@ struct RecipeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - View Mode
|
||||||
|
|
||||||
|
private var recipeViewContent: some View {
|
||||||
|
ScrollView(showsIndicators: false) {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
ParallaxHeader(
|
||||||
|
coordinateSpace: CoordinateSpaces.scrollView,
|
||||||
|
defaultHeight: imageHeight
|
||||||
|
) {
|
||||||
|
if let recipeImage = viewModel.recipeImage {
|
||||||
|
Image(uiImage: recipeImage)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(maxHeight: imageHeight + 200)
|
||||||
|
.clipped()
|
||||||
|
} else {
|
||||||
|
Rectangle()
|
||||||
|
.frame(height: 400)
|
||||||
|
.foregroundStyle(
|
||||||
|
LinearGradient(
|
||||||
|
gradient: Gradient(colors: [.ncGradientDark, .ncGradientLight]),
|
||||||
|
startPoint: .topLeading,
|
||||||
|
endPoint: .bottomTrailing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack {
|
||||||
|
Text(viewModel.observableRecipeDetail.name)
|
||||||
|
.font(.title)
|
||||||
|
.bold()
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if let isDownloaded = viewModel.isDownloaded {
|
||||||
|
Image(systemName: isDownloaded ? "checkmark.circle" : "icloud.and.arrow.down")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}.padding([.top, .horizontal])
|
||||||
|
|
||||||
|
if viewModel.observableRecipeDetail.description != "" {
|
||||||
|
Text(viewModel.observableRecipeDetail.description)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.top, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
RecipeDurationSection(viewModel: viewModel)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
|
||||||
|
if !viewModel.observableRecipeDetail.recipeIngredient.isEmpty {
|
||||||
|
RecipeIngredientSection(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
if !viewModel.observableRecipeDetail.recipeInstructions.isEmpty {
|
||||||
|
RecipeInstructionSection(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
if !viewModel.observableRecipeDetail.tool.isEmpty {
|
||||||
|
RecipeToolSection(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
RecipeNutritionSection(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
|
||||||
|
RecipeKeywordSection(viewModel: viewModel)
|
||||||
|
MoreInformationSection(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 5)
|
||||||
|
.background(Rectangle().foregroundStyle(.background).shadow(radius: 5).mask(Rectangle().padding(.top, -20)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.coordinateSpace(name: CoordinateSpaces.scrollView)
|
||||||
|
.ignoresSafeArea(.container, edges: .top)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Edit Mode Form
|
||||||
|
|
||||||
|
private var recipeEditForm: some View {
|
||||||
|
Form {
|
||||||
|
if let recipeImage = viewModel.recipeImage {
|
||||||
|
Section {
|
||||||
|
Image(uiImage: recipeImage)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(maxHeight: 200)
|
||||||
|
.clipped()
|
||||||
|
.listRowInsets(EdgeInsets())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
TextField("Recipe Name", text: $viewModel.observableRecipeDetail.name)
|
||||||
|
.font(.headline)
|
||||||
|
TextField("Description", text: $viewModel.observableRecipeDetail.description, axis: .vertical)
|
||||||
|
.lineLimit(1...5)
|
||||||
|
}
|
||||||
|
|
||||||
|
RecipeEditMetadataSection(viewModel: viewModel)
|
||||||
|
.environmentObject(appState)
|
||||||
|
|
||||||
|
RecipeEditDurationSection(
|
||||||
|
prepTime: viewModel.observableRecipeDetail.prepTime,
|
||||||
|
cookTime: viewModel.observableRecipeDetail.cookTime,
|
||||||
|
totalTime: viewModel.observableRecipeDetail.totalTime
|
||||||
|
)
|
||||||
|
|
||||||
|
RecipeEditIngredientSection(ingredients: $viewModel.observableRecipeDetail.recipeIngredient)
|
||||||
|
|
||||||
|
RecipeEditInstructionSection(instructions: $viewModel.observableRecipeDetail.recipeInstructions)
|
||||||
|
|
||||||
|
RecipeEditToolSection(tools: $viewModel.observableRecipeDetail.tool)
|
||||||
|
|
||||||
|
RecipeEditNutritionSection(nutrition: $viewModel.observableRecipeDetail.nutrition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - RecipeView ViewModel
|
// MARK: - RecipeView ViewModel
|
||||||
|
|
||||||
@@ -241,16 +266,14 @@ struct RecipeView: View {
|
|||||||
@Published var editMode: Bool = false
|
@Published var editMode: Bool = false
|
||||||
@Published var showTitle: Bool = false
|
@Published var showTitle: Bool = false
|
||||||
@Published var isDownloaded: Bool? = nil
|
@Published var isDownloaded: Bool? = nil
|
||||||
@Published var importUrl: String = ""
|
|
||||||
|
|
||||||
@Published var presentShareSheet: Bool = false
|
@Published var presentShareSheet: Bool = false
|
||||||
@Published var presentInstructionEditView: Bool = false
|
@Published var presentKeywordSheet: Bool = false
|
||||||
@Published var presentIngredientEditView: Bool = false
|
|
||||||
@Published var presentToolEditView: Bool = false
|
|
||||||
|
|
||||||
var recipe: Recipe
|
var recipe: Recipe
|
||||||
var sharedURL: URL? = nil
|
var sharedURL: URL? = nil
|
||||||
var newRecipe: Bool = false
|
var newRecipe: Bool = false
|
||||||
|
var preloadedRecipeDetail: RecipeDetail? = nil
|
||||||
|
|
||||||
// Alerts
|
// Alerts
|
||||||
@Published var presentAlert = false
|
@Published var presentAlert = false
|
||||||
@@ -290,26 +313,6 @@ struct RecipeView: View {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
extension RecipeView {
|
|
||||||
func importRecipe(from url: String) async -> UserAlert? {
|
|
||||||
let (scrapedRecipe, error) = await appState.importRecipe(url: url)
|
|
||||||
if let scrapedRecipe = scrapedRecipe {
|
|
||||||
viewModel.setupView(recipeDetail: scrapedRecipe)
|
|
||||||
// Fetch the image from the server if the import created a recipe with a valid id
|
|
||||||
if let recipeId = Int(scrapedRecipe.id), recipeId > 0 {
|
|
||||||
viewModel.recipeImage = await appState.getImage(
|
|
||||||
id: recipeId,
|
|
||||||
size: .FULL,
|
|
||||||
fetchMode: .onlyServer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Tool Bar
|
// MARK: - Tool Bar
|
||||||
|
|
||||||
|
|
||||||
@@ -321,30 +324,13 @@ struct RecipeViewToolBar: ToolbarContent {
|
|||||||
|
|
||||||
var body: some ToolbarContent {
|
var body: some ToolbarContent {
|
||||||
if viewModel.editMode {
|
if viewModel.editMode {
|
||||||
ToolbarItemGroup(placement: .topBarLeading) {
|
ToolbarItem(placement: .topBarLeading) {
|
||||||
Button("Cancel") {
|
Button("Cancel") {
|
||||||
viewModel.editMode = false
|
viewModel.editMode = false
|
||||||
if viewModel.newRecipe {
|
if viewModel.newRecipe {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !viewModel.newRecipe {
|
|
||||||
Menu {
|
|
||||||
Button(role: .destructive) {
|
|
||||||
viewModel.presentAlert(
|
|
||||||
RecipeAlert.CONFIRM_DELETE,
|
|
||||||
action: {
|
|
||||||
await handleDelete()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} label: {
|
|
||||||
Label("Delete Recipe", systemImage: "trash")
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "ellipsis.circle")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItem(placement: .topBarTrailing) {
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
@@ -375,6 +361,19 @@ struct RecipeViewToolBar: ToolbarContent {
|
|||||||
} label: {
|
} label: {
|
||||||
Label("Share Recipe", systemImage: "square.and.arrow.up")
|
Label("Share Recipe", systemImage: "square.and.arrow.up")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
Button(role: .destructive) {
|
||||||
|
viewModel.presentAlert(
|
||||||
|
RecipeAlert.CONFIRM_DELETE,
|
||||||
|
action: {
|
||||||
|
await handleDelete()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} label: {
|
||||||
|
Label("Delete Recipe", systemImage: "trash")
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "ellipsis.circle")
|
Image(systemName: "ellipsis.circle")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,23 +21,70 @@ struct RecipeDurationSection: View {
|
|||||||
DurationView(time: viewModel.observableRecipeDetail.cookTime, title: LocalizedStringKey("Cooking"))
|
DurationView(time: viewModel.observableRecipeDetail.cookTime, title: LocalizedStringKey("Cooking"))
|
||||||
DurationView(time: viewModel.observableRecipeDetail.totalTime, title: LocalizedStringKey("Total time"))
|
DurationView(time: viewModel.observableRecipeDetail.totalTime, title: LocalizedStringKey("Total time"))
|
||||||
}
|
}
|
||||||
if viewModel.editMode {
|
|
||||||
Button {
|
|
||||||
presentPopover.toggle()
|
|
||||||
} label: {
|
|
||||||
Text("Edit")
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.padding(.top, 5)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.popover(isPresented: $presentPopover) {
|
}
|
||||||
EditableDurationView(
|
}
|
||||||
prepTime: viewModel.observableRecipeDetail.prepTime,
|
|
||||||
cookTime: viewModel.observableRecipeDetail.cookTime,
|
// MARK: - Recipe Edit Duration Section (Form-based)
|
||||||
totalTime: viewModel.observableRecipeDetail.totalTime
|
|
||||||
)
|
struct RecipeEditDurationSection: View {
|
||||||
|
@ObservedObject var prepTime: DurationComponents
|
||||||
|
@ObservedObject var cookTime: DurationComponents
|
||||||
|
@ObservedObject var totalTime: DurationComponents
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section("Duration") {
|
||||||
|
DurationPickerRow(label: "Preparation", time: prepTime)
|
||||||
|
DurationPickerRow(label: "Cooking", time: cookTime)
|
||||||
|
DurationPickerRow(label: "Total time", time: totalTime)
|
||||||
|
}
|
||||||
|
.onChange(of: prepTime.hourComponent) { _ in updateTotalTime() }
|
||||||
|
.onChange(of: prepTime.minuteComponent) { _ in updateTotalTime() }
|
||||||
|
.onChange(of: cookTime.hourComponent) { _ in updateTotalTime() }
|
||||||
|
.onChange(of: cookTime.minuteComponent) { _ in updateTotalTime() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTotalTime() {
|
||||||
|
var hourComponent = prepTime.hourComponent + cookTime.hourComponent
|
||||||
|
var minuteComponent = prepTime.minuteComponent + cookTime.minuteComponent
|
||||||
|
if minuteComponent >= 60 {
|
||||||
|
hourComponent += minuteComponent / 60
|
||||||
|
minuteComponent %= 60
|
||||||
|
}
|
||||||
|
totalTime.hourComponent = hourComponent
|
||||||
|
totalTime.minuteComponent = minuteComponent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate struct DurationPickerRow: View {
|
||||||
|
let label: LocalizedStringKey
|
||||||
|
@ObservedObject var time: DurationComponents
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Text(label)
|
||||||
|
Spacer()
|
||||||
|
Menu {
|
||||||
|
ForEach(0..<25, id: \.self) { hour in
|
||||||
|
Button("\(hour) h") {
|
||||||
|
time.hourComponent = hour
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("\(time.hourComponent) h")
|
||||||
|
.monospacedDigit()
|
||||||
|
}
|
||||||
|
Menu {
|
||||||
|
ForEach(0..<60, id: \.self) { minute in
|
||||||
|
Button("\(minute) min") {
|
||||||
|
time.minuteComponent = minute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("\(time.minuteComponent) min")
|
||||||
|
.monospacedDigit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,20 +69,44 @@ struct RecipeIngredientSection: View {
|
|||||||
}.padding(.top)
|
}.padding(.top)
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewModel.editMode {
|
|
||||||
Button {
|
|
||||||
viewModel.presentIngredientEditView.toggle()
|
|
||||||
} label: {
|
|
||||||
Text("Edit")
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.animation(.easeInOut, value: viewModel.observableRecipeDetail.ingredientMultiplier)
|
.animation(.easeInOut, value: viewModel.observableRecipeDetail.ingredientMultiplier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Recipe Edit Ingredient Section (Form-based)
|
||||||
|
|
||||||
|
struct RecipeEditIngredientSection: View {
|
||||||
|
@Binding var ingredients: [String]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section {
|
||||||
|
ForEach(ingredients.indices, id: \.self) { index in
|
||||||
|
HStack {
|
||||||
|
TextField("Ingredient", text: $ingredients[index])
|
||||||
|
Image(systemName: "line.3.horizontal")
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDelete { indexSet in
|
||||||
|
ingredients.remove(atOffsets: indexSet)
|
||||||
|
}
|
||||||
|
.onMove { from, to in
|
||||||
|
ingredients.move(fromOffsets: from, toOffset: to)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
ingredients.append("")
|
||||||
|
} label: {
|
||||||
|
Label("Add Ingredient", systemImage: "plus.circle.fill")
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Ingredients")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - RecipeIngredientSection List Item
|
// MARK: - RecipeIngredientSection List Item
|
||||||
|
|
||||||
fileprivate struct IngredientListItem: View {
|
fileprivate struct IngredientListItem: View {
|
||||||
|
|||||||
@@ -22,14 +22,6 @@ struct RecipeInstructionSection: View {
|
|||||||
ForEach(viewModel.observableRecipeDetail.recipeInstructions.indices, id: \.self) { ix in
|
ForEach(viewModel.observableRecipeDetail.recipeInstructions.indices, id: \.self) { ix in
|
||||||
RecipeInstructionListItem(instruction: $viewModel.observableRecipeDetail.recipeInstructions[ix], index: ix+1)
|
RecipeInstructionListItem(instruction: $viewModel.observableRecipeDetail.recipeInstructions[ix], index: ix+1)
|
||||||
}
|
}
|
||||||
if viewModel.editMode {
|
|
||||||
Button {
|
|
||||||
viewModel.presentInstructionEditView.toggle()
|
|
||||||
} label: {
|
|
||||||
Text("Edit")
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
|
|
||||||
@@ -37,6 +29,44 @@ struct RecipeInstructionSection: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Recipe Edit Instruction Section (Form-based)
|
||||||
|
|
||||||
|
struct RecipeEditInstructionSection: View {
|
||||||
|
@Binding var instructions: [String]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section {
|
||||||
|
ForEach(instructions.indices, id: \.self) { index in
|
||||||
|
HStack(alignment: .top) {
|
||||||
|
Text("\(index + 1).")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.monospacedDigit()
|
||||||
|
TextField("Step \(index + 1)", text: $instructions[index], axis: .vertical)
|
||||||
|
.lineLimit(1...10)
|
||||||
|
Image(systemName: "line.3.horizontal")
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDelete { indexSet in
|
||||||
|
instructions.remove(atOffsets: indexSet)
|
||||||
|
}
|
||||||
|
.onMove { from, to in
|
||||||
|
instructions.move(fromOffsets: from, toOffset: to)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
instructions.append("")
|
||||||
|
} label: {
|
||||||
|
Label("Add Step", systemImage: "plus.circle.fill")
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Instructions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fileprivate struct RecipeInstructionListItem: View {
|
fileprivate struct RecipeInstructionListItem: View {
|
||||||
@Binding var instruction: String
|
@Binding var instruction: String
|
||||||
|
|||||||
@@ -87,6 +87,52 @@ struct RecipeMetadataSection: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Recipe Edit Metadata Section (Form-based)
|
||||||
|
|
||||||
|
struct RecipeEditMetadataSection: View {
|
||||||
|
@EnvironmentObject var appState: AppState
|
||||||
|
@ObservedObject var viewModel: RecipeView.ViewModel
|
||||||
|
|
||||||
|
var categories: [String] {
|
||||||
|
appState.categories.map { $0.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
|
||||||
|
Stepper("Servings: \(viewModel.observableRecipeDetail.recipeYield)", value: $viewModel.observableRecipeDetail.recipeYield, in: 1...99)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
viewModel.presentKeywordSheet = true
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text("Keywords")
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
Spacer()
|
||||||
|
if viewModel.observableRecipeDetail.keywords.isEmpty {
|
||||||
|
Text("None")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
} else {
|
||||||
|
Text(viewModel.observableRecipeDetail.keywords.joined(separator: ", "))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fileprivate struct PickerPopoverView<Item: Hashable & CustomStringConvertible, Collection: Sequence>: View where Collection.Element == Item {
|
fileprivate struct PickerPopoverView<Item: Hashable & CustomStringConvertible, Collection: Sequence>: View where Collection.Element == Item {
|
||||||
@Binding var isPresented: Bool
|
@Binding var isPresented: Bool
|
||||||
@Binding var value: Item
|
@Binding var value: Item
|
||||||
|
|||||||
@@ -63,10 +63,42 @@ struct RecipeNutritionSection: View {
|
|||||||
|
|
||||||
func nutritionEmpty() -> Bool {
|
func nutritionEmpty() -> Bool {
|
||||||
for nutrition in Nutrition.allCases {
|
for nutrition in Nutrition.allCases {
|
||||||
if let value = viewModel.observableRecipeDetail.nutrition[nutrition.dictKey] {
|
if let _ = viewModel.observableRecipeDetail.nutrition[nutrition.dictKey] {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Recipe Edit Nutrition Section (Form-based)
|
||||||
|
|
||||||
|
struct RecipeEditNutritionSection: View {
|
||||||
|
@Binding var nutrition: [String: String]
|
||||||
|
|
||||||
|
@State private var isExpanded: Bool = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section {
|
||||||
|
DisclosureGroup("Nutrition Information", isExpanded: $isExpanded) {
|
||||||
|
ForEach(Nutrition.allCases, id: \.self) { item in
|
||||||
|
HStack {
|
||||||
|
Text(item.localizedDescription)
|
||||||
|
.lineLimit(1)
|
||||||
|
Spacer()
|
||||||
|
TextField("", text: nutritionBinding(for: item.dictKey))
|
||||||
|
.multilineTextAlignment(.trailing)
|
||||||
|
.frame(maxWidth: 150)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func nutritionBinding(for key: String) -> Binding<String> {
|
||||||
|
Binding(
|
||||||
|
get: { nutrition[key, default: ""] },
|
||||||
|
set: { nutrition[key] = $0 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,17 +21,40 @@ struct RecipeToolSection: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RecipeListSection(list: $viewModel.observableRecipeDetail.tool)
|
RecipeListSection(list: $viewModel.observableRecipeDetail.tool)
|
||||||
|
|
||||||
if viewModel.editMode {
|
|
||||||
Button {
|
|
||||||
viewModel.presentToolEditView.toggle()
|
|
||||||
} label: {
|
|
||||||
Text("Edit")
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
}
|
|
||||||
}.padding()
|
}.padding()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Recipe Edit Tool Section (Form-based)
|
||||||
|
|
||||||
|
struct RecipeEditToolSection: View {
|
||||||
|
@Binding var tools: [String]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section {
|
||||||
|
ForEach(tools.indices, id: \.self) { index in
|
||||||
|
HStack {
|
||||||
|
TextField("Tool", text: $tools[index])
|
||||||
|
Image(systemName: "line.3.horizontal")
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDelete { indexSet in
|
||||||
|
tools.remove(atOffsets: indexSet)
|
||||||
|
}
|
||||||
|
.onMove { from, to in
|
||||||
|
tools.move(fromOffsets: from, toOffset: to)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
tools.append("")
|
||||||
|
} label: {
|
||||||
|
Label("Add Tool", systemImage: "plus.circle.fill")
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Tools")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,13 +18,6 @@ struct RecipeTabView: View {
|
|||||||
|
|
||||||
private let gridColumns = [GridItem(.adaptive(minimum: 160), spacing: 12)]
|
private let gridColumns = [GridItem(.adaptive(minimum: 160), spacing: 12)]
|
||||||
|
|
||||||
private var showEditViewBinding: Binding<Bool> {
|
|
||||||
Binding(
|
|
||||||
get: { false },
|
|
||||||
set: { if $0 { viewModel.navigateToNewRecipe() } }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var nonEmptyCategories: [Category] {
|
private var nonEmptyCategories: [Category] {
|
||||||
appState.categories.filter { $0.recipe_count > 0 }
|
appState.categories.filter { $0.recipe_count > 0 }
|
||||||
}
|
}
|
||||||
@@ -112,19 +105,32 @@ struct RecipeTabView: View {
|
|||||||
.environmentObject(appState)
|
.environmentObject(appState)
|
||||||
.environmentObject(groceryList)
|
.environmentObject(groceryList)
|
||||||
case .newRecipe:
|
case .newRecipe:
|
||||||
RecipeView(viewModel: RecipeView.ViewModel())
|
RecipeView(viewModel: {
|
||||||
|
let vm = RecipeView.ViewModel()
|
||||||
|
if let imported = viewModel.importedRecipeDetail {
|
||||||
|
vm.preloadedRecipeDetail = imported
|
||||||
|
}
|
||||||
|
return vm
|
||||||
|
}())
|
||||||
.environmentObject(appState)
|
.environmentObject(appState)
|
||||||
.environmentObject(groceryList)
|
.environmentObject(groceryList)
|
||||||
|
.onAppear {
|
||||||
|
viewModel.importedRecipeDetail = nil
|
||||||
|
}
|
||||||
case .category(let category):
|
case .category(let category):
|
||||||
RecipeListView(
|
RecipeListView(
|
||||||
categoryName: category.name,
|
categoryName: category.name,
|
||||||
showEditView: showEditViewBinding
|
onCreateNew: { viewModel.navigateToNewRecipe() },
|
||||||
|
onImportFromURL: { viewModel.showImportURLSheet = true }
|
||||||
)
|
)
|
||||||
.id(category.id)
|
.id(category.id)
|
||||||
.environmentObject(appState)
|
.environmentObject(appState)
|
||||||
.environmentObject(groceryList)
|
.environmentObject(groceryList)
|
||||||
case .allRecipes:
|
case .allRecipes:
|
||||||
AllRecipesListView(showEditView: showEditViewBinding)
|
AllRecipesListView(
|
||||||
|
onCreateNew: { viewModel.navigateToNewRecipe() },
|
||||||
|
onImportFromURL: { viewModel.showImportURLSheet = true }
|
||||||
|
)
|
||||||
.environmentObject(appState)
|
.environmentObject(appState)
|
||||||
.environmentObject(groceryList)
|
.environmentObject(groceryList)
|
||||||
}
|
}
|
||||||
@@ -138,17 +144,27 @@ struct RecipeTabView: View {
|
|||||||
} detail: {
|
} detail: {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
if viewModel.showAllRecipesInDetail {
|
if viewModel.showAllRecipesInDetail {
|
||||||
AllRecipesListView(showEditView: showEditViewBinding)
|
AllRecipesListView(
|
||||||
|
onCreateNew: { viewModel.navigateToNewRecipe() },
|
||||||
|
onImportFromURL: { viewModel.showImportURLSheet = true }
|
||||||
|
)
|
||||||
} else if let category = viewModel.selectedCategory {
|
} else if let category = viewModel.selectedCategory {
|
||||||
RecipeListView(
|
RecipeListView(
|
||||||
categoryName: category.name,
|
categoryName: category.name,
|
||||||
showEditView: showEditViewBinding
|
onCreateNew: { viewModel.navigateToNewRecipe() },
|
||||||
|
onImportFromURL: { viewModel.showImportURLSheet = true }
|
||||||
)
|
)
|
||||||
.id(category.id)
|
.id(category.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(.nextcloudBlue)
|
.tint(.nextcloudBlue)
|
||||||
|
.sheet(isPresented: $viewModel.showImportURLSheet) {
|
||||||
|
ImportURLSheet { recipeDetail in
|
||||||
|
viewModel.navigateToImportedRecipe(recipeDetail: recipeDetail)
|
||||||
|
}
|
||||||
|
.environmentObject(appState)
|
||||||
|
}
|
||||||
.task {
|
.task {
|
||||||
let connection = await appState.checkServerConnection()
|
let connection = await appState.checkServerConnection()
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
@@ -185,6 +201,9 @@ struct RecipeTabView: View {
|
|||||||
@Published var selectedCategory: Category? = nil
|
@Published var selectedCategory: Category? = nil
|
||||||
@Published var showAllRecipesInDetail: Bool = false
|
@Published var showAllRecipesInDetail: Bool = false
|
||||||
|
|
||||||
|
@Published var showImportURLSheet: Bool = false
|
||||||
|
@Published var importedRecipeDetail: RecipeDetail? = nil
|
||||||
|
|
||||||
func navigateToSettings() {
|
func navigateToSettings() {
|
||||||
sidebarPath.append(SidebarDestination.settings)
|
sidebarPath.append(SidebarDestination.settings)
|
||||||
}
|
}
|
||||||
@@ -193,6 +212,11 @@ struct RecipeTabView: View {
|
|||||||
sidebarPath.append(SidebarDestination.newRecipe)
|
sidebarPath.append(SidebarDestination.newRecipe)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func navigateToImportedRecipe(recipeDetail: RecipeDetail) {
|
||||||
|
importedRecipeDetail = recipeDetail
|
||||||
|
sidebarPath.append(SidebarDestination.newRecipe)
|
||||||
|
}
|
||||||
|
|
||||||
func navigateToCategory(_ category: Category) {
|
func navigateToCategory(_ category: Category) {
|
||||||
selectedCategory = category
|
selectedCategory = category
|
||||||
showAllRecipesInDetail = false
|
showAllRecipesInDetail = false
|
||||||
@@ -273,9 +297,18 @@ fileprivate struct RecipeTabViewToolBar: ToolbarContent {
|
|||||||
|
|
||||||
// Create new recipes
|
// Create new recipes
|
||||||
ToolbarItem(placement: .topBarTrailing) {
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
|
Menu {
|
||||||
Button {
|
Button {
|
||||||
Logger.view.debug("Add new recipe")
|
Logger.view.debug("Add new recipe")
|
||||||
viewModel.navigateToNewRecipe()
|
viewModel.navigateToNewRecipe()
|
||||||
|
} label: {
|
||||||
|
Label("Create New Recipe", systemImage: "square.and.pencil")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
viewModel.showImportURLSheet = true
|
||||||
|
} label: {
|
||||||
|
Label("Import from URL", systemImage: "link")
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "plus.circle.fill")
|
Image(systemName: "plus.circle.fill")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user