mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 10:13:32 -04:00 
			
		
		
		
	feat: add custom scaling option (#1345)
* Added custom scaling option * Allow custom scaling with no yield set * Made edit-scale translated * fixed merge conflict * Refactored scale editor to use menu * replaced vslot with # * linter issues * fixed linter issues * fixed one more linter issue * format files + minor UI changes * remove console.log * move buttons into component and setup v-model * drop servings text Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
This commit is contained in:
		
							
								
								
									
										102
									
								
								frontend/components/Domain/Recipe/RecipeScaleEditButton.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								frontend/components/Domain/Recipe/RecipeScaleEditButton.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <div class="text-center d-flex align-center"> | ||||||
|  |       <div> | ||||||
|  |         <v-menu v-model="menu" offset-y top nudge-top="6" :close-on-content-click="false"> | ||||||
|  |           <template #activator="{ on, attrs }"> | ||||||
|  |             <v-card class="pa-1 px-2" dark color="secondary darken-1" small v-bind="attrs" v-on="on"> | ||||||
|  |               <span v-if="recipeYield"> {{ scaledYield }} </span> | ||||||
|  |               <span v-if="!recipeYield"> x {{ scale }} </span> | ||||||
|  |             </v-card> | ||||||
|  |           </template> | ||||||
|  |           <v-card min-width="300px"> | ||||||
|  |             <v-card-title class="mb-0"> | ||||||
|  |               {{ $t("recipe.edit-scale") }} | ||||||
|  |             </v-card-title> | ||||||
|  |             <v-card-text class="mt-n5"> | ||||||
|  |               <div class="mt-4 d-flex align-center"> | ||||||
|  |                 <v-text-field v-model.number="scale" type="number" :min="0" :label="$t('recipe.edit-scale')" /> | ||||||
|  |                 <v-tooltip right color="secondary darken-1"> | ||||||
|  |                   <template #activator="{ on, attrs }"> | ||||||
|  |                     <v-btn v-bind="attrs" icon class="mx-1" small v-on="on" @click="scale = 1"> | ||||||
|  |                       <v-icon> | ||||||
|  |                         {{ $globals.icons.undo }} | ||||||
|  |                       </v-icon> | ||||||
|  |                     </v-btn> | ||||||
|  |                   </template> | ||||||
|  |                   <span> Reset Scale </span> | ||||||
|  |                 </v-tooltip> | ||||||
|  |               </div> | ||||||
|  |             </v-card-text> | ||||||
|  |           </v-card> | ||||||
|  |         </v-menu> | ||||||
|  |       </div> | ||||||
|  |       <BaseButtonGroup | ||||||
|  |         class="pl-2" | ||||||
|  |         :large="false" | ||||||
|  |         :buttons="[ | ||||||
|  |           { | ||||||
|  |             icon: $globals.icons.minus, | ||||||
|  |             text: 'Decrease Scale by 1', | ||||||
|  |             event: 'decrement', | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             icon: $globals.icons.createAlt, | ||||||
|  |             text: 'Increase Scale by 1', | ||||||
|  |             event: 'increment', | ||||||
|  |           }, | ||||||
|  |         ]" | ||||||
|  |         @decrement="scale > 1 ? scale-- : null" | ||||||
|  |         @increment="scale++" | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script lang="ts"> | ||||||
|  | import { defineComponent, reactive, toRefs, computed } from "@nuxtjs/composition-api"; | ||||||
|  |  | ||||||
|  | export default defineComponent({ | ||||||
|  |   props: { | ||||||
|  |     recipeYield: { | ||||||
|  |       type: String, | ||||||
|  |       default: null, | ||||||
|  |     }, | ||||||
|  |     basicYield: { | ||||||
|  |       type: String, | ||||||
|  |       default: null, | ||||||
|  |     }, | ||||||
|  |     scaledYield: { | ||||||
|  |       type: String, | ||||||
|  |       default: null, | ||||||
|  |     }, | ||||||
|  |     editScale: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|  |     value: { | ||||||
|  |       type: Number, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   setup(props, { emit }) { | ||||||
|  |     const state = reactive({ | ||||||
|  |       tempScale: 1, | ||||||
|  |       menu: false, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const scale = computed({ | ||||||
|  |       get: () => props.value, | ||||||
|  |       set: (value) => { | ||||||
|  |         const newScaleNumber = parseFloat(`${value}`); | ||||||
|  |         emit("input", isNaN(newScaleNumber) ? 0 : newScaleNumber); | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       scale, | ||||||
|  |       ...toRefs(state), | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
| @@ -23,7 +23,6 @@ export function parseIngredientText(ingredient: RecipeIngredient, disableAmount: | |||||||
|  |  | ||||||
|   // casting to number is required as sometimes quantity is a string |   // casting to number is required as sometimes quantity is a string | ||||||
|   if (quantity && Number(quantity) !== 0) { |   if (quantity && Number(quantity) !== 0) { | ||||||
|     console.log("Using Quantity", quantity, typeof quantity); |  | ||||||
|     if (unit?.fraction) { |     if (unit?.fraction) { | ||||||
|       const fraction = frac(quantity * scale, 10, true); |       const fraction = frac(quantity * scale, 10, true); | ||||||
|       if (fraction[0] !== undefined && fraction[0] > 0) { |       if (fraction[0] !== undefined && fraction[0] > 0) { | ||||||
|   | |||||||
| @@ -248,6 +248,7 @@ | |||||||
|     "description": "Beschreibung", |     "description": "Beschreibung", | ||||||
|     "disable-amount": "Zutatenmenge deaktivieren", |     "disable-amount": "Zutatenmenge deaktivieren", | ||||||
|     "disable-comments": "Kommentare deaktivieren", |     "disable-comments": "Kommentare deaktivieren", | ||||||
|  |     "edit-scale": "Skalierung bearbeiten", | ||||||
|     "fat-content": "Fett", |     "fat-content": "Fett", | ||||||
|     "fiber-content": "Ballaststoffe", |     "fiber-content": "Ballaststoffe", | ||||||
|     "grams": "g", |     "grams": "g", | ||||||
|   | |||||||
| @@ -248,6 +248,7 @@ | |||||||
|     "description": "Description", |     "description": "Description", | ||||||
|     "disable-amount": "Disable Ingredient Amounts", |     "disable-amount": "Disable Ingredient Amounts", | ||||||
|     "disable-comments": "Disable Comments", |     "disable-comments": "Disable Comments", | ||||||
|  |     "edit-scale": "Edit Scale", | ||||||
|     "fat-content": "Fat", |     "fat-content": "Fat", | ||||||
|     "fiber-content": "Fiber", |     "fiber-content": "Fiber", | ||||||
|     "grams": "grams", |     "grams": "grams", | ||||||
|   | |||||||
| @@ -178,38 +178,18 @@ | |||||||
|           <div class="d-flex justify-space-between align-center pt-2 pb-3"> |           <div class="d-flex justify-space-between align-center pt-2 pb-3"> | ||||||
|             <v-tooltip v-if="!form" small top color="secondary darken-1"> |             <v-tooltip v-if="!form" small top color="secondary darken-1"> | ||||||
|               <template #activator="{ on, attrs }"> |               <template #activator="{ on, attrs }"> | ||||||
|                 <v-btn |                 <RecipeScaleEditButton | ||||||
|                   v-if="recipe.recipeYield" |                   v-model.number="scale" | ||||||
|                   dense |  | ||||||
|                   small |  | ||||||
|                   :hover="false" |  | ||||||
|                   type="label" |  | ||||||
|                   :ripple="false" |  | ||||||
|                   elevation="0" |  | ||||||
|                   color="secondary darken-1" |  | ||||||
|                   class="rounded-sm static" |  | ||||||
|                   v-bind="attrs" |                   v-bind="attrs" | ||||||
|                   @click="scale = 1" |                   :recipe-yield="recipe.recipeYield" | ||||||
|  |                   :basic-yield="basicYield" | ||||||
|  |                   :scaled-yield="scaledYield" | ||||||
|  |                   :edit-scale="!recipe.settings.disableAmount && !form" | ||||||
|                   v-on="on" |                   v-on="on" | ||||||
|                 > |                 /> | ||||||
|                   {{ scaledYield }} |  | ||||||
|                 </v-btn> |  | ||||||
|               </template> |               </template> | ||||||
|               <span> Reset Scale </span> |               <span> {{ $t("recipe.edit-scale") }} </span> | ||||||
|             </v-tooltip> |             </v-tooltip> | ||||||
|  |  | ||||||
|             <template v-if="!recipe.settings.disableAmount && !form"> |  | ||||||
|               <v-btn color="secondary darken-1" class="mx-1" small @click="scale > 1 ? scale-- : null"> |  | ||||||
|                 <v-icon> |  | ||||||
|                   {{ $globals.icons.minus }} |  | ||||||
|                 </v-icon> |  | ||||||
|               </v-btn> |  | ||||||
|               <v-btn color="secondary darken-1" small @click="scale++"> |  | ||||||
|                 <v-icon> |  | ||||||
|                   {{ $globals.icons.createAlt }} |  | ||||||
|                 </v-icon> |  | ||||||
|               </v-btn> |  | ||||||
|             </template> |  | ||||||
|             <v-spacer></v-spacer> |             <v-spacer></v-spacer> | ||||||
|  |  | ||||||
|             <RecipeRating |             <RecipeRating | ||||||
| @@ -500,6 +480,7 @@ import RecipeNutrition from "~/components/Domain/Recipe/RecipeNutrition.vue"; | |||||||
| import RecipeInstructions from "~/components/Domain/Recipe/RecipeInstructions.vue"; | import RecipeInstructions from "~/components/Domain/Recipe/RecipeInstructions.vue"; | ||||||
| import RecipeNotes from "~/components/Domain/Recipe/RecipeNotes.vue"; | import RecipeNotes from "~/components/Domain/Recipe/RecipeNotes.vue"; | ||||||
| import RecipeImageUploadBtn from "~/components/Domain/Recipe/RecipeImageUploadBtn.vue"; | import RecipeImageUploadBtn from "~/components/Domain/Recipe/RecipeImageUploadBtn.vue"; | ||||||
|  | import RecipeScaleEditButton from "~/components/Domain/Recipe/RecipeScaleEditButton.vue"; | ||||||
| import RecipeSettingsMenu from "~/components/Domain/Recipe/RecipeSettingsMenu.vue"; | import RecipeSettingsMenu from "~/components/Domain/Recipe/RecipeSettingsMenu.vue"; | ||||||
| import RecipeIngredientEditor from "~/components/Domain/Recipe/RecipeIngredientEditor.vue"; | import RecipeIngredientEditor from "~/components/Domain/Recipe/RecipeIngredientEditor.vue"; | ||||||
| import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue"; | import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue"; | ||||||
| @@ -534,6 +515,7 @@ export default defineComponent({ | |||||||
|     RecipeSettingsMenu, |     RecipeSettingsMenu, | ||||||
|     RecipeTimeCard, |     RecipeTimeCard, | ||||||
|     RecipeTools, |     RecipeTools, | ||||||
|  |     RecipeScaleEditButton, | ||||||
|     VueMarkdown, |     VueMarkdown, | ||||||
|   }, |   }, | ||||||
|   async beforeRouteLeave(_to, _from, next) { |   async beforeRouteLeave(_to, _from, next) { | ||||||
| @@ -610,6 +592,8 @@ export default defineComponent({ | |||||||
|     const state = reactive({ |     const state = reactive({ | ||||||
|       form: false, |       form: false, | ||||||
|       scale: 1, |       scale: 1, | ||||||
|  |       scaleTemp: 1, | ||||||
|  |       scaleDialog: false, | ||||||
|       hideImage: false, |       hideImage: false, | ||||||
|       imageKey: 1, |       imageKey: 1, | ||||||
|       skeleton: false, |       skeleton: false, | ||||||
| @@ -701,6 +685,19 @@ export default defineComponent({ | |||||||
|       return recipe.value?.recipeYield; |       return recipe.value?.recipeYield; | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     const basicYield = computed(() => { | ||||||
|  |       const regMatchNum = /\d+/; | ||||||
|  |       const yieldString = recipe.value?.recipeYield; | ||||||
|  |       const num = yieldString?.match(regMatchNum); | ||||||
|  |  | ||||||
|  |       if (num && num?.length > 0) { | ||||||
|  |         const yieldAsInt = parseInt(num[0]); | ||||||
|  |         return yieldString?.replace(num[0], String(yieldAsInt)); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return recipe.value?.recipeYield; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     async function uploadImage(fileObject: File) { |     async function uploadImage(fileObject: File) { | ||||||
|       if (!recipe.value || !recipe.value.slug) { |       if (!recipe.value || !recipe.value.slug) { | ||||||
|         return; |         return; | ||||||
| @@ -830,6 +827,13 @@ export default defineComponent({ | |||||||
|  |  | ||||||
|     const drag = ref(false); |     const drag = ref(false); | ||||||
|  |  | ||||||
|  |     // =============================================================== | ||||||
|  |     // Scale | ||||||
|  |  | ||||||
|  |     const setScale = (newScale: number) => { | ||||||
|  |       state.scale = newScale; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|       // Wake Lock |       // Wake Lock | ||||||
|       drag, |       drag, | ||||||
| @@ -847,12 +851,14 @@ export default defineComponent({ | |||||||
|       enableLandscape, |       enableLandscape, | ||||||
|       imageHeight, |       imageHeight, | ||||||
|       scaledYield, |       scaledYield, | ||||||
|  |       basicYield, | ||||||
|       toggleJson, |       toggleJson, | ||||||
|       ...toRefs(state), |       ...toRefs(state), | ||||||
|       recipe, |       recipe, | ||||||
|       api, |       api, | ||||||
|       loading, |       loading, | ||||||
|       addStep, |       addStep, | ||||||
|  |       setScale, | ||||||
|       deleteRecipe, |       deleteRecipe, | ||||||
|       printRecipe, |       printRecipe, | ||||||
|       closeEditor, |       closeEditor, | ||||||
|   | |||||||
| @@ -90,6 +90,7 @@ export interface Icon { | |||||||
|   webhook: string; |   webhook: string; | ||||||
|   windowClose: string; |   windowClose: string; | ||||||
|   zip: string; |   zip: string; | ||||||
|  |   undo: string; | ||||||
|  |  | ||||||
|   // Crud |   // Crud | ||||||
|   backArrow: string; |   backArrow: string; | ||||||
|   | |||||||
| @@ -109,6 +109,7 @@ import { | |||||||
|   mdiChartLine, |   mdiChartLine, | ||||||
|   mdiHelpCircleOutline, |   mdiHelpCircleOutline, | ||||||
|   mdiDocker, |   mdiDocker, | ||||||
|  |   mdiUndo, | ||||||
| } from "@mdi/js"; | } from "@mdi/js"; | ||||||
|  |  | ||||||
| export const icons = { | export const icons = { | ||||||
| @@ -206,6 +207,7 @@ export const icons = { | |||||||
|   webhook: mdiWebhook, |   webhook: mdiWebhook, | ||||||
|   windowClose: mdiWindowClose, |   windowClose: mdiWindowClose, | ||||||
|   zip: mdiFolderZipOutline, |   zip: mdiFolderZipOutline, | ||||||
|  |   undo: mdiUndo, | ||||||
|  |  | ||||||
|   // Crud |   // Crud | ||||||
|   backArrow: mdiArrowLeftBoldOutline, |   backArrow: mdiArrowLeftBoldOutline, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user