mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-30 17:53:31 -04:00 
			
		
		
		
	feat: new create from image visuals (#5595)
This commit is contained in:
		| @@ -64,3 +64,7 @@ a { | ||||
| .fill-height { | ||||
|   min-height: 100vh; | ||||
| } | ||||
|  | ||||
| .vue-simple-handler { | ||||
|   background-color: rgb(var(--v-theme-primary)) !important; | ||||
| } | ||||
|   | ||||
| @@ -1,48 +1,58 @@ | ||||
| <template> | ||||
|   <v-container class="pa-0"> | ||||
|     <v-row no-gutters> | ||||
|       <v-col | ||||
|         cols="8" | ||||
|         align-self="center" | ||||
|   <v-card class="ma-0 pt-2" :elevation="4"> | ||||
|     <v-card-text> | ||||
|       <!-- Controls Row (Menu) --> | ||||
|       <v-row class="mb-2 mx-1"> | ||||
|         <v-btn | ||||
|           color="error" | ||||
|           :icon="$globals.icons.delete" | ||||
|           :disabled="submitted" | ||||
|           @click="$emit('delete')" | ||||
|         /> | ||||
|         <v-spacer /> | ||||
|         <v-btn | ||||
|           v-if="changed" | ||||
|           class="mr-2" | ||||
|           color="success" | ||||
|           :icon="$globals.icons.save" | ||||
|           :disabled="submitted" | ||||
|           @click="() => save()" | ||||
|         /> | ||||
|         <v-menu offset-y :close-on-content-click="false" location="bottom center"> | ||||
|           <template #activator="{ props }"> | ||||
|             <v-btn color="info" v-bind="props" :icon="$globals.icons.edit" :disabled="submitted" /> | ||||
|           </template> | ||||
|           <v-list class="mt-1"> | ||||
|             <template v-for="(row, keyRow) in controls" :key="keyRow"> | ||||
|               <v-list-item-group> | ||||
|                 <v-list-item | ||||
|                   v-for="(control, keyControl) in row" | ||||
|                   :key="keyControl" | ||||
|                   :disabled="submitted" | ||||
|                   @click="control.callback()" | ||||
|                 > | ||||
|                   <v-list-item-icon> | ||||
|                     <v-icon :color="control.color" :icon="control.icon" /> | ||||
|                   </v-list-item-icon> | ||||
|                 </v-list-item> | ||||
|               </v-list-item-group> | ||||
|             </template> | ||||
|           </v-list> | ||||
|         </v-menu> | ||||
|       </v-row> | ||||
|  | ||||
|       <!-- Image Row --> | ||||
|       <Cropper | ||||
|         ref="cropper" | ||||
|         class="cropper" | ||||
|         :src="img" | ||||
|         :default-size="defaultSize" | ||||
|         :style="`height: ${cropperHeight}; width: ${cropperWidth};`" | ||||
|         @change="changed = changed + 1" | ||||
|         @ready="changed = -1" | ||||
|       /> | ||||
|       </v-col> | ||||
|       <v-spacer /> | ||||
|       <v-col | ||||
|         cols="2" | ||||
|         align-self="center" | ||||
|       > | ||||
|         <v-container class="pa-0 mx-0"> | ||||
|           <v-row | ||||
|             v-for="(row, keyRow) in controls" | ||||
|             :key="keyRow" | ||||
|           > | ||||
|             <v-col | ||||
|               v-for="(control, keyControl) in row" | ||||
|               :key="keyControl" | ||||
|               :cols="12 / row.length" | ||||
|               class="py-2 mx-0" | ||||
|               style="display: flex; align-items: center; justify-content: center;" | ||||
|             > | ||||
|               <v-btn | ||||
|                 icon | ||||
|                 :color="control.color" | ||||
|                 @click="control.callback()" | ||||
|               > | ||||
|                 <v-icon> {{ control.icon }} </v-icon> | ||||
|               </v-btn> | ||||
|             </v-col> | ||||
|           </v-row> | ||||
|         </v-container> | ||||
|       </v-col> | ||||
|     </v-row> | ||||
|   </v-container> | ||||
|     </v-card-text> | ||||
|   </v-card> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| @@ -64,10 +74,15 @@ export default defineNuxtComponent({ | ||||
|       type: String, | ||||
|       default: undefined, | ||||
|     }, | ||||
|     submitted: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|   emits: ["save"], | ||||
|   }, | ||||
|   emits: ["save", "delete"], | ||||
|   setup(_, context) { | ||||
|     const cropper = ref<Cropper>(); | ||||
|     const cropper = ref<any>(); | ||||
|     const changed = ref(0); | ||||
|     const { $globals } = useNuxtApp(); | ||||
|  | ||||
|     interface Control { | ||||
| @@ -101,41 +116,32 @@ export default defineNuxtComponent({ | ||||
|           callback: () => rotate(90), | ||||
|         }, | ||||
|       ], | ||||
|       [ | ||||
|         { | ||||
|           color: "success", | ||||
|           icon: $globals.icons.save, | ||||
|           callback: () => save(), | ||||
|         }, | ||||
|       ], | ||||
|     ]); | ||||
|  | ||||
|     function flip(hortizontal: boolean, vertical?: boolean) { | ||||
|       if (!cropper.value) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       cropper.value.flip(hortizontal, vertical); | ||||
|       changed.value = changed.value + 1; | ||||
|     } | ||||
|  | ||||
|     function rotate(angle: number) { | ||||
|       if (!cropper.value) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       cropper.value.rotate(angle); | ||||
|       changed.value = changed.value + 1; | ||||
|     } | ||||
|  | ||||
|     function save() { | ||||
|       if (!cropper.value) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       const { canvas } = cropper.value.getResult(); | ||||
|       if (!canvas) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       canvas.toBlob((blob) => { | ||||
|         if (blob) { | ||||
|           context.emit("save", blob); | ||||
| @@ -149,11 +155,11 @@ export default defineNuxtComponent({ | ||||
|       flip, | ||||
|       rotate, | ||||
|       save, | ||||
|       changed, | ||||
|     }; | ||||
|   }, | ||||
|  | ||||
|   methods: { | ||||
|     // @ts-expect-error https://advanced-cropper.github.io/vue-advanced-cropper/guides/advanced-recipes.html | ||||
|     defaultSize({ imageSize, visibleArea }) { | ||||
|       return { | ||||
|         width: (visibleArea || imageSize).width, | ||||
|   | ||||
| @@ -7,9 +7,7 @@ | ||||
|         </v-card-title> | ||||
|         <v-card-text> | ||||
|           <p>{{ $t("recipe.create-recipe-from-an-image-description") }}</p> | ||||
|           <v-container class="pa-0"> | ||||
|             <v-row> | ||||
|               <v-col cols="auto" align-self="center"> | ||||
|           <v-container class="px-0"> | ||||
|             <AppButtonUpload | ||||
|               class="ml-auto" | ||||
|               url="none" | ||||
| @@ -21,48 +19,35 @@ | ||||
|               :multiple="true" | ||||
|               @uploaded="uploadImages" | ||||
|             /> | ||||
|               </v-col> | ||||
|               <v-spacer /> | ||||
|             </v-row> | ||||
|  | ||||
|             <div v-if="uploadedImages.length" class="mt-3"> | ||||
|               <v-row> | ||||
|                 <v-col cols="12" class="pb-0"> | ||||
|                   <v-card-text class="pa-0"> | ||||
|                     <p class="mb-0"> | ||||
|             <div v-if="uploadedImages.length > 0" class="mt-3"> | ||||
|               <p class="my-2"> | ||||
|                 {{ $t("recipe.crop-and-rotate-the-image") }} | ||||
|               </p> | ||||
|                   </v-card-text> | ||||
|                 </v-col> | ||||
|               </v-row> | ||||
|               <v-row style="max-width: 600px"> | ||||
|                 <v-spacer /> | ||||
|                 <v-col v-for="(imageUrl, index) in uploadedImagesPreviewUrls" :key="index" cols="12"> | ||||
|               <v-row> | ||||
|                     <v-col cols="auto" align-self="center"> | ||||
|                 <v-col | ||||
|                   v-for="(imageUrl, index) in uploadedImagesPreviewUrls" | ||||
|                   :key="index" | ||||
|                   cols="12" | ||||
|                   sm="6" | ||||
|                   lg="4" | ||||
|                   xl="3" | ||||
|                 > | ||||
|                   <ImageCropper | ||||
|                     :img="imageUrl" | ||||
|                     cropper-height="100%" | ||||
|                     cropper-width="100%" | ||||
|                     :submitted="loading" | ||||
|                     class="mt-4" | ||||
|                     @save="(croppedImage) => updateUploadedImage(index, croppedImage)" | ||||
|                     @delete="clearImage(index)" | ||||
|                   /> | ||||
|  | ||||
|                       <v-btn color="error" @click="() => clearImage(index)"> | ||||
|                         <v-icon start> | ||||
|                           {{ $globals.icons.close }} | ||||
|                         </v-icon> | ||||
|                         {{ $t("recipe.remove-image") }} | ||||
|                       </v-btn> | ||||
|                 </v-col> | ||||
|               </v-row> | ||||
|                 </v-col> | ||||
|                 <v-spacer /> | ||||
|               </v-row> | ||||
|             </div> | ||||
|           </v-container> | ||||
|         </v-card-text> | ||||
|         <v-card-actions v-if="uploadedImages.length"> | ||||
|           <div> | ||||
|           <div class="w-100 d-flex flex-column align-center"> | ||||
|             <p style="width: 250px"> | ||||
|               <BaseButton rounded block type="submit" :loading="loading" /> | ||||
|             </p> | ||||
| @@ -147,6 +132,7 @@ export default defineNuxtComponent({ | ||||
|  | ||||
|     function updateUploadedImage(index: number, croppedImage: Blob) { | ||||
|       uploadedImages.value[index] = croppedImage; | ||||
|       uploadedImagesPreviewUrls.value[index] = URL.createObjectURL(croppedImage); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user