mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-29 17:24:31 -04:00 
			
		
		
		
	* ignore unsafe html warnings * remove unused import * re-order attrs * removed unused vars/imports * removed unused import * refactored v-html * removed more unused things
		
			
				
	
	
		
			332 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			332 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | |
|   <v-container :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.new") }}</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>
 | |
| 
 | |
|     <div
 | |
|       v-if="recipe && wakeIsSupported"
 | |
|       class="d-print-none d-flex px-2"
 | |
|       :class="$vuetify.breakpoint.smAndDown ? 'justify-center' : 'justify-end'"
 | |
|     >
 | |
|       <v-switch v-model="wakeLock" small :label="$t('recipe.screen-awake')" />
 | |
|     </div>
 | |
| 
 | |
|     <RecipePageComments
 | |
|       v-if="user.id && !recipe.settings.disableComments && !isEditForm && !isCookMode"
 | |
|       :recipe="recipe"
 | |
|       class="px-1 my-4 d-print-none"
 | |
|     />
 | |
|     <RecipePrintContainer :recipe="recipe" :scale="scale" />
 | |
|   </v-container>
 | |
| </template>
 | |
| 
 | |
| <script lang="ts">
 | |
| import {
 | |
|   defineComponent,
 | |
|   useContext,
 | |
|   useRouter,
 | |
|   computed,
 | |
|   ref,
 | |
|   onMounted,
 | |
|   onUnmounted,
 | |
| } from "@nuxtjs/composition-api";
 | |
| import { invoke, until, useWakeLock } from "@vueuse/core";
 | |
| import RecipePageEditorToolbar from "./RecipePageParts/RecipePageEditorToolbar.vue";
 | |
| import RecipePageFooter from "./RecipePageParts/RecipePageFooter.vue";
 | |
| import RecipePageHeader from "./RecipePageParts/RecipePageHeader.vue";
 | |
| import RecipePageIngredientEditor from "./RecipePageParts/RecipePageIngredientEditor.vue";
 | |
| import RecipePageIngredientToolsView from "./RecipePageParts/RecipePageIngredientToolsView.vue";
 | |
| import RecipePageInstructions from "./RecipePageParts/RecipePageInstructions.vue";
 | |
| import RecipePageOrganizers from "./RecipePageParts/RecipePageOrganizers.vue";
 | |
| import RecipePageScale from "./RecipePageParts/RecipePageScale.vue";
 | |
| import RecipePageTitleContent from "./RecipePageParts/RecipePageTitleContent.vue";
 | |
| import RecipePageComments from "./RecipePageParts/RecipePageComments.vue";
 | |
| import RecipePrintContainer from "~/components/Domain/Recipe/RecipePrintContainer.vue";
 | |
| import { EditorMode, PageMode, usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
 | |
| import { NoUndefinedField } from "~/lib/api/types/non-generated";
 | |
| import { Recipe } from "~/lib/api/types/recipe";
 | |
| import { useRouteQuery } from "~/composables/use-router";
 | |
| import { useUserApi } from "~/composables/api";
 | |
| import { uuid4, deepCopy } from "~/composables/use-utils";
 | |
| import RecipeDialogBulkAdd from "~/components/Domain/Recipe/RecipeDialogBulkAdd.vue";
 | |
| import RecipeNotes from "~/components/Domain/Recipe/RecipeNotes.vue";
 | |
| 
 | |
| const EDITOR_OPTIONS = {
 | |
|   mode: "code",
 | |
|   search: false,
 | |
|   mainMenuBar: false,
 | |
| };
 | |
| 
 | |
| export default defineComponent({
 | |
|   components: {
 | |
|     RecipePageHeader,
 | |
|     RecipePrintContainer,
 | |
|     RecipePageComments,
 | |
|     RecipePageTitleContent,
 | |
|     RecipePageEditorToolbar,
 | |
|     RecipePageIngredientEditor,
 | |
|     RecipePageOrganizers,
 | |
|     RecipePageScale,
 | |
|     RecipePageIngredientToolsView,
 | |
|     RecipeDialogBulkAdd,
 | |
|     RecipeNotes,
 | |
|     RecipePageInstructions,
 | |
|     RecipePageFooter,
 | |
|   },
 | |
|   props: {
 | |
|     recipe: {
 | |
|       type: Object as () => NoUndefinedField<Recipe>,
 | |
|       required: true,
 | |
|     },
 | |
|   },
 | |
|   setup(props) {
 | |
|     const router = useRouter();
 | |
|     const api = useUserApi();
 | |
|     const { pageMode, editMode, setMode, isEditForm, isEditJSON, isCookMode, isEditMode, toggleCookMode } =
 | |
|       usePageState(props.recipe.slug);
 | |
| 
 | |
|     /** =============================================================
 | |
|      * Recipe Snapshot on Mount
 | |
|      * this is used to determine if the recipe has been changed since the last save
 | |
|      * and prompts the user to save if they have unsaved changes.
 | |
|      */
 | |
|     const originalRecipe = ref<Recipe | null>(null);
 | |
| 
 | |
|     invoke(async () => {
 | |
|       await until(props.recipe).not.toBeNull();
 | |
|       originalRecipe.value = deepCopy(props.recipe);
 | |
|     });
 | |
| 
 | |
|     onUnmounted(async () => {
 | |
|       const isSame = JSON.stringify(props.recipe) === JSON.stringify(originalRecipe.value);
 | |
|       if (isEditMode.value && !isSame && props.recipe?.slug !== undefined) {
 | |
|         const save = window.confirm(
 | |
|           "You have unsaved changes. Do you want to save before leaving?\n\nOkay to save, Cancel to discard changes."
 | |
|         );
 | |
| 
 | |
|         if (save) {
 | |
|           await api.recipes.updateOne(props.recipe.slug, props.recipe);
 | |
|         }
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     /** =============================================================
 | |
|      * Set State onMounted
 | |
|      */
 | |
| 
 | |
|     type BooleanString = "true" | "false" | "";
 | |
| 
 | |
|     const edit = useRouteQuery<BooleanString>("edit", "");
 | |
| 
 | |
|     onMounted(() => {
 | |
|       if (edit.value === "true") {
 | |
|         setMode(PageMode.EDIT);
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     /** =============================================================
 | |
|      * Wake Lock
 | |
|      */
 | |
| 
 | |
|     const { isSupported: wakeIsSupported, isActive, request, release } = useWakeLock();
 | |
| 
 | |
|     const wakeLock = computed({
 | |
|       get: () => isActive,
 | |
|       set: () => {
 | |
|         if (isActive.value) {
 | |
|           unlockScreen();
 | |
|         } else {
 | |
|           lockScreen();
 | |
|         }
 | |
|       },
 | |
|     });
 | |
| 
 | |
|     async function lockScreen() {
 | |
|       if (wakeIsSupported) {
 | |
|         console.log("Wake Lock Requested");
 | |
|         await request("screen");
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     async function unlockScreen() {
 | |
|       if (wakeIsSupported || isActive) {
 | |
|         console.log("Wake Lock Released");
 | |
|         await release();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     onMounted(() => lockScreen());
 | |
|     onUnmounted(() => unlockScreen());
 | |
| 
 | |
|     /** =============================================================
 | |
|      * Recipe Save Delete
 | |
|      */
 | |
| 
 | |
|     async function saveRecipe() {
 | |
|       const { data } = await api.recipes.updateOne(props.recipe.slug, props.recipe);
 | |
|       setMode(PageMode.VIEW);
 | |
|       if (data?.slug) {
 | |
|         router.push("/recipe/" + data.slug);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     async function deleteRecipe() {
 | |
|       const { data } = await api.recipes.deleteOne(props.recipe.slug);
 | |
|       if (data?.slug) {
 | |
|         router.push("/");
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     /** =============================================================
 | |
|      * View Preferences
 | |
|      */
 | |
|     const { $vuetify } = useContext();
 | |
| 
 | |
|     const landscape = computed(() => {
 | |
|       const preferLandscape = props.recipe.settings.landscapeView;
 | |
|       const smallScreen = !$vuetify.breakpoint.smAndUp;
 | |
| 
 | |
|       if (preferLandscape) {
 | |
|         return true;
 | |
|       } else if (smallScreen) {
 | |
|         return true;
 | |
|       }
 | |
| 
 | |
|       return false;
 | |
|     });
 | |
| 
 | |
|     /** =============================================================
 | |
|      * Bulk Step Editor
 | |
|      * TODO: Move to RecipePageInstructions component
 | |
|      */
 | |
| 
 | |
|     function addStep(steps: Array<string> | null = null) {
 | |
|       if (!props.recipe.recipeInstructions) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if (steps) {
 | |
|         const cleanedSteps = steps.map((step) => {
 | |
|           return { id: uuid4(), text: step, title: "", ingredientReferences: [] };
 | |
|         });
 | |
| 
 | |
|         props.recipe.recipeInstructions.push(...cleanedSteps);
 | |
|       } else {
 | |
|         props.recipe.recipeInstructions.push({ id: uuid4(), text: "", title: "", ingredientReferences: [] });
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     /** =============================================================
 | |
|      * Meta Tags
 | |
|      */
 | |
|     const { user } = usePageUser();
 | |
| 
 | |
|     return {
 | |
|       user,
 | |
|       api,
 | |
|       scale: ref(1),
 | |
|       EDITOR_OPTIONS,
 | |
|       landscape,
 | |
| 
 | |
|       pageMode,
 | |
|       editMode,
 | |
|       PageMode,
 | |
|       EditorMode,
 | |
|       isEditMode,
 | |
|       isEditForm,
 | |
|       isEditJSON,
 | |
|       isCookMode,
 | |
|       toggleCookMode,
 | |
| 
 | |
|       wakeLock,
 | |
|       wakeIsSupported,
 | |
|       saveRecipe,
 | |
|       deleteRecipe,
 | |
|       addStep,
 | |
|     };
 | |
|   },
 | |
|   head: {},
 | |
| });
 | |
| </script>
 | |
| 
 | |
| <style lang="css">
 | |
| .flip-list-move {
 | |
|   transition: transform 0.5s;
 | |
| }
 | |
| .no-move {
 | |
|   transition: transform 0s;
 | |
| }
 | |
| .ghost {
 | |
|   opacity: 0.5;
 | |
| }
 | |
| .list-group {
 | |
|   min-height: 38px;
 | |
| }
 | |
| .list-group-item {
 | |
|   cursor: move;
 | |
| }
 | |
| .list-group-item i {
 | |
|   cursor: pointer;
 | |
| }
 | |
| </style>
 |