mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 10:13:32 -04:00 
			
		
		
		
	feat: Added a dedicated cookmode dialog that allows for individual scrolling (#4464)
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							65c35adc9d
						
					
				
				
					commit
					d419acd61e
				
			| @@ -1,14 +1,16 @@ | ||||
| <template> | ||||
|   <div v-if="value && value.length > 0"> | ||||
|     <div class="d-flex justify-start"> | ||||
|     <div v-if="!isCookMode" class="d-flex justify-start" > | ||||
|       <h2 class="mb-2 mt-1">{{ $t("recipe.ingredients") }}</h2> | ||||
|       <AppButtonCopy btn-class="ml-auto" :copy-text="ingredientCopyText" /> | ||||
|     </div> | ||||
|     <div> | ||||
|       <div v-for="(ingredient, index) in value" :key="'ingredient' + index"> | ||||
|         <h3 v-if="showTitleEditor[index]" class="mt-2">{{ ingredient.title }}</h3> | ||||
|         <v-divider v-if="showTitleEditor[index]"></v-divider> | ||||
|         <v-list-item dense @click="toggleChecked(index)"> | ||||
|         <template v-if="!isCookMode"> | ||||
|           <h3 v-if="showTitleEditor[index]" class="mt-2">{{ ingredient.title }}</h3> | ||||
|           <v-divider v-if="showTitleEditor[index]"></v-divider> | ||||
|         </template> | ||||
|         <v-list-item dense @click.stop="toggleChecked(index)"> | ||||
|           <v-checkbox hide-details :value="checked[index]" class="pt-0 my-auto py-auto" color="secondary" /> | ||||
|           <v-list-item-content :key="ingredient.quantity"> | ||||
|             <RecipeIngredientListItem :ingredient="ingredient" :disable-amount="disableAmount" :scale="scale" /> | ||||
| @@ -40,6 +42,10 @@ export default defineComponent({ | ||||
|       type: Number, | ||||
|       default: 1, | ||||
|     }, | ||||
|     isCookMode: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     } | ||||
|   }, | ||||
|   setup(props) { | ||||
|     function validateTitle(title?: string) { | ||||
|   | ||||
| @@ -1,75 +1,135 @@ | ||||
| <template> | ||||
|   <v-container :class="{ 'pa-0': $vuetify.breakpoint.smAndDown }"> | ||||
|     <v-card :flat="$vuetify.breakpoint.smAndDown" class="d-print-none"> | ||||
|       <RecipePageHeader | ||||
|   <div> | ||||
|     <v-container v-show="!isCookMode" key="recipe-page" :class="{ 'pa-0': $vuetify.breakpoint.smAndDown }"> | ||||
|       <v-card  :flat="$vuetify.breakpoint.smAndDown" class="d-print-none"> | ||||
|         <RecipePageHeader | ||||
|           :recipe="recipe" | ||||
|           :recipe-scale="scale" | ||||
|           :landscape="landscape" | ||||
|           @save="saveRecipe" | ||||
|           @delete="deleteRecipe" | ||||
|         /> | ||||
|         <LazyRecipeJsonEditor v-if="isEditJSON" v-model="recipe" class="mt-10" :options="EDITOR_OPTIONS" /> | ||||
|         <v-card-text v-else> | ||||
|           <!-- | ||||
|             This is where most of the main content is rendered. Some components include state for both Edit and View modes | ||||
|             which is why some have explicit v-if statements and others use the composition API to determine and manage | ||||
|             the shared state internally. | ||||
|  | ||||
|             The global recipe object is shared down the tree of components and _is_ mutated by child components. This is | ||||
|             some-what of a hack of the system and goes against the principles of Vue, but it _does_ seem to work and streamline | ||||
|             a significant amount of prop management. When we move to Vue 3 and have access to some of the newer API's the plan to update this | ||||
|             data management and mutation system we're using. | ||||
|           --> | ||||
|           <RecipePageEditorToolbar v-if="isEditForm" :recipe="recipe" /> | ||||
|           <RecipePageTitleContent :recipe="recipe" :landscape="landscape" /> | ||||
|           <RecipePageIngredientEditor v-if="isEditForm" :recipe="recipe" /> | ||||
|           <RecipePageScale :recipe="recipe" :scale.sync="scale" :landscape="landscape" /> | ||||
|  | ||||
|           <!-- | ||||
|             This section contains the 2 column layout for the recipe steps and other content. | ||||
|           --> | ||||
|           <v-row> | ||||
|             <!-- | ||||
|               The left column is conditionally rendered based on cook mode. | ||||
|             --> | ||||
|             <v-col v-if="!isCookMode || isEditForm" cols="12" sm="12" md="4" lg="4"> | ||||
|               <RecipePageIngredientToolsView v-if="!isEditForm" :recipe="recipe" :scale="scale" /> | ||||
|               <RecipePageOrganizers v-if="$vuetify.breakpoint.mdAndUp" :recipe="recipe" /> | ||||
|             </v-col> | ||||
|             <v-divider v-if="$vuetify.breakpoint.mdAndUp && !isCookMode" class="my-divider" :vertical="true" /> | ||||
|  | ||||
|             <!-- | ||||
|               the right column is always rendered, but it's layout width is determined by where the left column is | ||||
|               rendered. | ||||
|             --> | ||||
|             <v-col cols="12" sm="12" :md="8 + (isCookMode ? 1 : 0) * 4" :lg="8 + (isCookMode ? 1 : 0) * 4"> | ||||
|               <RecipePageInstructions | ||||
|                 v-model="recipe.recipeInstructions" | ||||
|                 :assets.sync="recipe.assets" | ||||
|                 :recipe="recipe" | ||||
|                 :scale="scale" | ||||
|               /> | ||||
|               <div v-if="isEditForm" class="d-flex"> | ||||
|                 <RecipeDialogBulkAdd class="ml-auto my-2 mr-1" @bulk-data="addStep" /> | ||||
|                 <BaseButton class="my-2" @click="addStep()"> {{ $t("general.add") }}</BaseButton> | ||||
|               </div> | ||||
|               <div v-if="!$vuetify.breakpoint.mdAndUp"> | ||||
|                 <RecipePageOrganizers :recipe="recipe" /> | ||||
|               </div> | ||||
|               <RecipeNotes v-model="recipe.notes" :edit="isEditForm" /> | ||||
|             </v-col> | ||||
|           </v-row> | ||||
|           <RecipePageFooter :recipe="recipe" /> | ||||
|         </v-card-text> | ||||
|       </v-card> | ||||
|       <WakelockSwitch/> | ||||
|       <RecipePageComments | ||||
|         v-if="!recipe.settings.disableComments && !isEditForm && !isCookMode" | ||||
|         :recipe="recipe" | ||||
|         :recipe-scale="scale" | ||||
|         :landscape="landscape" | ||||
|         @save="saveRecipe" | ||||
|         @delete="deleteRecipe" | ||||
|         class="px-1 my-4 d-print-none" | ||||
|       /> | ||||
|       <LazyRecipeJsonEditor v-if="isEditJSON" v-model="recipe" class="mt-10" :options="EDITOR_OPTIONS" /> | ||||
|       <v-card-text v-else> | ||||
|         <!-- | ||||
|           This is where most of the main content is rendered. Some components include state for both Edit and View modes | ||||
|           which is why some have explicit v-if statements and others use the composition API to determine and manage | ||||
|           the shared state internally. | ||||
|       <RecipePrintContainer :recipe="recipe" :scale="scale" /> | ||||
|     </v-container> | ||||
|     <!-- Cook mode displayes two columns with ingredients and instructions side by side, each being scrolled individually, allowing to view both at the same timer --> | ||||
|     <v-sheet v-show="isCookMode && !hasLinkedIngredients" key="cookmode" :style="{height: $vuetify.breakpoint.smAndUp ? 'calc(100vh - 48px)' : ''}"> <!-- the calc is to account for the toolbar a more dynamic solution could be needed  --> | ||||
|       <v-row  style="height: 100%;"  no-gutters class="overflow-hidden"> | ||||
|         <v-col  cols="12" sm="5" class="overflow-y-auto pl-4 pr-3 py-2" style="height: 100%;"> | ||||
|           <div class="d-flex align-center"> | ||||
|             <RecipePageScale :recipe="recipe" :scale.sync="scale" :landscape="landscape" /> | ||||
|           </div> | ||||
|           <RecipePageIngredientToolsView v-if="!isEditForm" :recipe="recipe" :scale="scale" :is-cook-mode="isCookMode" /> | ||||
|           <v-divider></v-divider> | ||||
|         </v-col> | ||||
|         <v-col class="overflow-y-auto py-2" style="height: 100%;" cols="12" sm="7"> | ||||
|           <RecipePageInstructions | ||||
|             v-model="recipe.recipeInstructions" | ||||
|             class="overflow-y-hidden px-4" | ||||
|             :assets.sync="recipe.assets" | ||||
|             :recipe="recipe" | ||||
|             :scale="scale" | ||||
|           /> | ||||
|         </v-col> | ||||
|       </v-row> | ||||
|  | ||||
|           The global recipe object is shared down the tree of components and _is_ mutated by child components. This is | ||||
|           some-what of a hack of the system and goes against the principles of Vue, but it _does_ seem to work and streamline | ||||
|           a significant amount of prop management. When we move to Vue 3 and have access to some of the newer API's the plan to update this | ||||
|           data management and mutation system we're using. | ||||
|          --> | ||||
|         <RecipePageEditorToolbar v-if="isEditForm" :recipe="recipe" /> | ||||
|         <RecipePageTitleContent :recipe="recipe" :landscape="landscape" /> | ||||
|         <RecipePageIngredientEditor v-if="isEditForm" :recipe="recipe" /> | ||||
|         <RecipePageScale :recipe="recipe" :scale.sync="scale" :landscape="landscape" /> | ||||
|  | ||||
|         <!-- | ||||
|           This section contains the 2 column layout for the recipe steps and other content. | ||||
|          --> | ||||
|         <v-row> | ||||
|           <!-- | ||||
|             The left column is conditionally rendered based on cook mode. | ||||
|            --> | ||||
|           <v-col v-if="!isCookMode || isEditForm" cols="12" sm="12" md="4" lg="4"> | ||||
|             <RecipePageIngredientToolsView v-if="!isEditForm" :recipe="recipe" :scale="scale" /> | ||||
|             <RecipePageOrganizers v-if="$vuetify.breakpoint.mdAndUp" :recipe="recipe" /> | ||||
|           </v-col> | ||||
|           <v-divider v-if="$vuetify.breakpoint.mdAndUp && !isCookMode" class="my-divider" :vertical="true" /> | ||||
|  | ||||
|           <!-- | ||||
|             the right column is always rendered, but it's layout width is determined by where the left column is | ||||
|             rendered. | ||||
|            --> | ||||
|           <v-col cols="12" sm="12" :md="8 + (isCookMode ? 1 : 0) * 4" :lg="8 + (isCookMode ? 1 : 0) * 4"> | ||||
|             <RecipePageInstructions | ||||
|               v-model="recipe.recipeInstructions" | ||||
|               :assets.sync="recipe.assets" | ||||
|               :recipe="recipe" | ||||
|     </v-sheet> | ||||
|     <v-sheet v-show="isCookMode && hasLinkedIngredients"> | ||||
|       <div class="mt-2 px-2 px-md-4"> | ||||
|         <RecipePageScale :recipe="recipe" :scale.sync="scale" :landscape="landscape"/> | ||||
|       </div> | ||||
|       <RecipePageInstructions | ||||
|         v-model="recipe.recipeInstructions" | ||||
|         class="overflow-y-hidden mt-n5 px-2 px-md-4" | ||||
|         :assets.sync="recipe.assets" | ||||
|         :recipe="recipe" | ||||
|         :scale="scale" | ||||
|       /> | ||||
|       <v-divider></v-divider> | ||||
|       <div class="px-2 px-md-4 pb-4 "> | ||||
|         <v-card flat> | ||||
|           <v-card-title>{{ $t('recipe.not-linked-ingredients') }}</v-card-title> | ||||
|             <RecipeIngredients | ||||
|               :value="notLinkedIngredients" | ||||
|               :scale="scale" | ||||
|             /> | ||||
|             <div v-if="isEditForm" class="d-flex"> | ||||
|               <RecipeDialogBulkAdd class="ml-auto my-2 mr-1" @bulk-data="addStep" /> | ||||
|               <BaseButton class="my-2" @click="addStep()"> {{ $t("general.add") }}</BaseButton> | ||||
|             </div> | ||||
|             <div v-if="!$vuetify.breakpoint.mdAndUp"> | ||||
|               <RecipePageOrganizers :recipe="recipe" /> | ||||
|             </div> | ||||
|             <RecipeNotes v-model="recipe.notes" :edit="isEditForm" /> | ||||
|           </v-col> | ||||
|         </v-row> | ||||
|         <RecipePageFooter :recipe="recipe" /> | ||||
|       </v-card-text> | ||||
|     </v-card> | ||||
|     <WakelockSwitch/> | ||||
|     <RecipePageComments | ||||
|       v-if="!recipe.settings.disableComments && !isEditForm && !isCookMode" | ||||
|       :recipe="recipe" | ||||
|       class="px-1 my-4 d-print-none" | ||||
|     /> | ||||
|     <RecipePrintContainer :recipe="recipe" :scale="scale" /> | ||||
|   </v-container> | ||||
|               :disable-amount="recipe.settings.disableAmount" | ||||
|               :is-cook-mode="isCookMode"> | ||||
|  | ||||
|             </RecipeIngredients> | ||||
|         </v-card> | ||||
|       </div> | ||||
|     </v-sheet> | ||||
|     <v-btn | ||||
|       v-if="isCookMode" | ||||
|       fab | ||||
|       small | ||||
|       color="primary" | ||||
|       style="position: fixed; right: 12px; top: 60px;" | ||||
|       @click="toggleCookMode()" | ||||
|       > | ||||
|       <v-icon>mdi-close</v-icon> | ||||
|     </v-btn> | ||||
|   </div> | ||||
|  | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| @@ -84,6 +144,7 @@ import { | ||||
| useRoute, | ||||
| } from "@nuxtjs/composition-api"; | ||||
| import { invoke, until } from "@vueuse/core"; | ||||
| import RecipeIngredients from "../RecipeIngredients.vue"; | ||||
| import RecipePageEditorToolbar from "./RecipePageParts/RecipePageEditorToolbar.vue"; | ||||
| import RecipePageFooter from "./RecipePageParts/RecipePageFooter.vue"; | ||||
| import RecipePageHeader from "./RecipePageParts/RecipePageHeader.vue"; | ||||
| @@ -133,6 +194,7 @@ export default defineComponent({ | ||||
|     RecipeNotes, | ||||
|     RecipePageInstructions, | ||||
|     RecipePageFooter, | ||||
|     RecipeIngredients | ||||
|   }, | ||||
|   props: { | ||||
|     recipe: { | ||||
| @@ -151,6 +213,13 @@ export default defineComponent({ | ||||
|     const { pageMode, editMode, setMode, isEditForm, isEditJSON, isCookMode, isEditMode, toggleCookMode } = | ||||
|       usePageState(props.recipe.slug); | ||||
|     const { deactivateNavigationWarning } = useNavigationWarning(); | ||||
|     const notLinkedIngredients = computed(() => { | ||||
|       console.log("inst",props.recipe.recipeInstruction); | ||||
|       return props.recipe.recipeIngredient.filter((ingredient) => { | ||||
|         return !props.recipe.recipeInstructions.some((step) => step.ingredientReferences?.map((ref) => ref.referenceId).includes(ingredient.referenceId)); | ||||
|       }) | ||||
|     }) | ||||
|     console.log(notLinkedIngredients); | ||||
|  | ||||
|     /** ============================================================= | ||||
|      * Recipe Snapshot on Mount | ||||
| @@ -176,11 +245,14 @@ export default defineComponent({ | ||||
|         } | ||||
|       } | ||||
|       deactivateNavigationWarning(); | ||||
|       toggleCookMode() | ||||
|  | ||||
|       clearPageState(props.recipe.slug || ""); | ||||
|       console.debug("reset RecipePage state during unmount"); | ||||
|     }); | ||||
|  | ||||
|     const hasLinkedIngredients = computed(() => { | ||||
|       return props.recipe.recipeInstructions.some((step) => step.ingredientReferences && step.ingredientReferences.length > 0); | ||||
|     }) | ||||
|     /** ============================================================= | ||||
|      * Set State onMounted | ||||
|      */ | ||||
| @@ -278,6 +350,8 @@ export default defineComponent({ | ||||
|       saveRecipe, | ||||
|       deleteRecipe, | ||||
|       addStep, | ||||
|       hasLinkedIngredients, | ||||
|       notLinkedIngredients | ||||
|     }; | ||||
|   }, | ||||
|   head: {}, | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
|       :value="recipe.recipeIngredient" | ||||
|       :scale="scale" | ||||
|       :disable-amount="recipe.settings.disableAmount" | ||||
|       :is-cook-mode="isCookMode" | ||||
|     /> | ||||
|     <div v-if="!isEditMode && recipe.tools && recipe.tools.length > 0"> | ||||
|       <h2 class="mb-2 mt-4">{{ $t('tool.required-tools') }}</h2> | ||||
| @@ -46,6 +47,10 @@ export default defineComponent({ | ||||
|       type: Number, | ||||
|       required: true, | ||||
|     }, | ||||
|     isCookMode: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     } | ||||
|   }, | ||||
|   setup(props) { | ||||
|     const { isOwnGroup } = useLoggedInState(); | ||||
|   | ||||
| @@ -65,8 +65,8 @@ | ||||
|     </v-dialog> | ||||
|  | ||||
|     <div class="d-flex justify-space-between justify-start"> | ||||
|       <h2 class="mb-4 mt-1">{{ $t("recipe.instructions") }}</h2> | ||||
|       <BaseButton v-if="!isEditForm && showCookMode" minor cancel color="primary" @click="toggleCookMode()"> | ||||
|       <h2 v-if="!isCookMode" class="mb-4 mt-1">{{ $t("recipe.instructions") }}</h2> | ||||
|       <BaseButton v-if="!isEditForm && !isCookMode" minor cancel color="primary" @click="toggleCookMode()"> | ||||
|         <template #icon> | ||||
|           {{ $globals.icons.primary }} | ||||
|         </template> | ||||
| @@ -243,16 +243,31 @@ | ||||
|               </DropZone> | ||||
|               <v-expand-transition> | ||||
|                 <div v-show="!isChecked(index) && !isEditForm" class="m-0 p-0"> | ||||
|  | ||||
|                   <v-card-text class="markdown"> | ||||
|                     <SafeMarkdown class="markdown" :source="step.text" /> | ||||
|                     <div v-if="isCookMode && step.ingredientReferences && step.ingredientReferences.length > 0"> | ||||
|                       <v-divider class="mb-2"></v-divider> | ||||
|                       <RecipeIngredientHtml | ||||
|                         v-for="ing in step.ingredientReferences" | ||||
|                         :key="ing.referenceId" | ||||
|                         :markup="getIngredientByRefId(ing.referenceId)" | ||||
|                       /> | ||||
|                     </div> | ||||
|                     <v-row> | ||||
|                       <v-col | ||||
|                         v-if="isCookMode && step.ingredientReferences && step.ingredientReferences.length > 0" | ||||
|                         cols="12" | ||||
|                         sm="5" | ||||
|                       > | ||||
|                         <div class="ml-n4"> | ||||
|                           <RecipeIngredients | ||||
|                             :value="recipe.recipeIngredient.filter((ing) => { | ||||
|                               if(!step.ingredientReferences) return false | ||||
|                               return step.ingredientReferences.map((ref) => ref.referenceId).includes(ing.referenceId || '') | ||||
|                             })" | ||||
|                             :scale="scale" | ||||
|                             :disable-amount="recipe.settings.disableAmount" | ||||
|                             :is-cook-mode="isCookMode" | ||||
|                           /> | ||||
|                         </div> | ||||
|                       </v-col> | ||||
|                       <v-divider v-if="isCookMode && step.ingredientReferences && step.ingredientReferences.length > 0 && $vuetify.breakpoint.smAndUp" vertical ></v-divider> | ||||
|                       <v-col> | ||||
|                         <SafeMarkdown class="markdown" :source="step.text" /> | ||||
|                       </v-col> | ||||
|                     </v-row> | ||||
|                   </v-card-text> | ||||
|                 </div> | ||||
|               </v-expand-transition> | ||||
| @@ -261,7 +276,7 @@ | ||||
|         </div> | ||||
|       </TransitionGroup> | ||||
|     </draggable> | ||||
|     <v-divider class="mt-10 d-flex d-md-none"/> | ||||
|     <v-divider v-if="!isCookMode" class="mt-10 d-flex d-md-none"/> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| @@ -287,7 +302,7 @@ import { usePageState } from "~/composables/recipe-page/shared-state"; | ||||
| import { useExtractIngredientReferences } from "~/composables/recipe-page/use-extract-ingredient-references"; | ||||
| import { NoUndefinedField } from "~/lib/api/types/non-generated"; | ||||
| import DropZone from "~/components/global/DropZone.vue"; | ||||
|  | ||||
| import RecipeIngredients from "~/components/Domain/Recipe/RecipeIngredients.vue"; | ||||
| interface MergerHistory { | ||||
|   target: number; | ||||
|   source: number; | ||||
| @@ -300,6 +315,7 @@ export default defineComponent({ | ||||
|     draggable, | ||||
|     RecipeIngredientHtml, | ||||
|     DropZone, | ||||
|     RecipeIngredients | ||||
|   }, | ||||
|   props: { | ||||
|     value: { | ||||
| @@ -322,7 +338,7 @@ export default defineComponent({ | ||||
|   }, | ||||
|  | ||||
|   setup(props, context) { | ||||
|     const { i18n, req } = useContext(); | ||||
|     const { i18n, req, $vuetify } = useContext(); | ||||
|     const BASE_URL = detectServerBaseUrl(req); | ||||
|  | ||||
|     const { isCookMode, toggleCookMode, isEditForm } = usePageState(props.recipe.slug); | ||||
|   | ||||
| @@ -662,7 +662,8 @@ | ||||
|       "missing-food": "Create missing food: {food}", | ||||
|       "no-food": "No Food" | ||||
|     }, | ||||
|     "reset-servings-count": "Reset Servings Count" | ||||
|     "reset-servings-count": "Reset Servings Count", | ||||
|     "not-linked-ingredients": "Additional Ingredients" | ||||
|   }, | ||||
|   "search": { | ||||
|     "advanced-search": "Advanced Search", | ||||
|   | ||||
| @@ -268,6 +268,7 @@ export interface RecipeStep { | ||||
|   title?: string | null; | ||||
|   text: string; | ||||
|   ingredientReferences?: IngredientReferences[]; | ||||
|   summary?: string | null; | ||||
| } | ||||
| export interface RecipeAsset { | ||||
|   name: string; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user