3434
3535 <!-- Thumbnails -->
3636 <div class =" mt-2 flex flex-wrap gap-2" >
37- <img
38- v-for =" (img, idx) in attachmentFiles"
39- :key =" idx"
40- :src =" img"
41- class =" w-20 h-20 object-cover rounded cursor-pointer border hover:border-blue-500 transition"
42- :alt =" `Generated image ${idx + 1}`"
43- @click =" zoomImage(img)"
37+ <div class =" group relative" v-for =" (img, key) in requestAttachmentFilesUrls" >
38+ <img
39+ :key =" key"
40+ :src =" img"
41+ class =" w-20 h-20 object-cover rounded cursor-pointer border hover:border-blue-500 transition"
42+ :alt =" `Generated image ${key + 1}`"
43+ @click =" zoomImage(img)"
44+ />
45+ <div
46+ class =" opacity-0 group-hover:opacity-100 flex items-center justify-center w-5 h-5 bg-black absolute -top-2 -end-2 rounded-full border-2 border-white cursor-pointer hover:border-gray-300 hover:scale-110"
47+ @click =" removeFileFromList(key)"
48+ >
49+ <Tooltip class =" absolute top-0 end-0" >
50+ <div >
51+ <div class =" w-4 h-4 absolute" ></div >
52+ <IconCloseOutline class =" w-3 h-3 text-white hover:text-gray-300" />
53+ </div >
54+ <template #tooltip >
55+ Remove file
56+ </template >
57+ </Tooltip >
58+ </div >
59+ </div >
60+ <input
61+ ref =" fileInput"
62+ class =" hidden"
63+ type =" file"
64+ @change =" handleAddFile"
65+ accept =" image/*"
4466 />
67+ <button v-if =" !uploading" @click =" fileInput?.click()" type =" button" class =" relative group hover:border-gray-500 transition border-gray-300 flex items-center justify-center w-20 h-20 border-2 border-dashed rounded-md" >
68+ <div class =" flex flex-col items-center justify-center gap-2 mt-4 mb-4" >
69+ <IconCloseOutline class =" group-hover:text-gray-500 transition rotate-45 w-6 h-6 text-gray-300 hover:text-gray-300" />
70+ <p class =" text-gray-300 group-hover:text-gray-500 transition bottom-0" >Ctrl + v</p >
71+ </div >
72+ </button >
73+ <Skeleton v-else type =" image" class =" w-20 h-20" />
4574 </div >
4675
4776 <!-- Fullscreen Modal -->
175204
176205<script setup lang="ts">
177206
178- import { ref , onMounted , nextTick , Ref , h , computed , watch , reactive } from ' vue'
207+ import { ref , onMounted , nextTick , Ref , watch , onUnmounted } from ' vue'
179208import { Carousel } from ' flowbite' ;
180209import { callAdminForthApi } from ' @/utils' ;
181210import { useI18n } from ' vue-i18n' ;
182211import adminforth from ' @/adminforth' ;
183212import { ProgressBar } from ' @/afcl' ;
184213import * as Handlebars from ' handlebars' ;
214+ import { IconCloseOutline } from ' @iconify-prerendered/vue-flowbite' ;
215+ import { Tooltip , Skeleton } from ' @/afcl'
216+ import { useRoute } from ' vue-router' ;
217+
218+ const { t : $t, t } = useI18n ();
219+ const route = useRoute ();
185220
186- const { t : $t } = useI18n ();
187221
188222const prompt = ref (' ' );
189223const emit = defineEmits ([' close' , ' uploadImage' ]);
190- const props = defineProps ([' meta' , ' record' ]);
224+ const props = defineProps ({
225+ meta: Object ,
226+ record: Object ,
227+ humanifySize: Function ,
228+ });
191229const images = ref ([]);
192230const loading = ref (false );
193231const attachmentFiles = ref <string []>([])
232+ const requestAttachmentFiles = ref <Blob [] | null >([]);
233+ const requestAttachmentFilesUrls = ref <string []>([]);
194234const stopGeneration = ref (false );
195-
196- function minifyField(field : string ): string {
197- if (field .length > 100 ) {
198- return field .slice (0 , 100 ) + ' ...' ;
199- }
200- return field ;
201- }
235+ const fileInput = ref <HTMLInputElement | null >(null );
202236
203237const caurosel = ref (null );
204238onMounted (async () => {
205239 // Initialize carousel
206- const context = {
207- field: props .meta .pathColumnLabel ,
208- resource: props .meta .resourceLabel ,
209- };
210240 let template = ' ' ;
211241 if (props .meta .generationPrompt ) {
212242 template = props .meta .generationPrompt ;
@@ -230,13 +260,20 @@ onMounted(async () => {
230260 });
231261
232262 if (resp ?.files ?.length ) {
233- attachmentFiles .value = resp .files ;
263+ requestAttachmentFilesUrls .value = resp .files ;
234264 }
235265 } catch (err ) {
236266 console .error (' Failed to fetch attachment files' , err );
237267 }
268+
269+ for ( const fileUrl in requestAttachmentFilesUrls .value ) {
270+ const image = await fetch (fileUrl );
271+ const imageBlob = await image .blob ();
272+ requestAttachmentFiles .value ! .push (imageBlob );
273+ }
238274});
239275
276+
240277async function slide(direction : number ) {
241278 if (! caurosel .value ) return ;
242279 const curPos = caurosel .value .getActiveItem ().position ;
@@ -323,11 +360,13 @@ async function generateImages() {
323360 method: ' POST' ,
324361 body: {
325362 prompt: prompt .value ,
326- recordId: props .record [props .meta .recorPkFieldName ]
363+ recordId: props .record [props .meta .recorPkFieldName ],
364+ requestAttachmentFiles: requestAttachmentFilesUrls .value ,
327365 },
328366 });
329367 } catch (e ) {
330368 console .error (e );
369+ return ;
331370 }
332371
333372 if (resp ?.error ) {
@@ -451,4 +490,129 @@ watch(zoomedImage, async (val) => {
451490 }).show ()
452491 }
453492})
493+
494+ async function handleAddFile(event , clipboardFile = null ) {
495+ if (clipboardFile ) {
496+ clipboardFile = renameFile (clipboardFile , ` pasted_image_${Date .now ()}.png ` );
497+ }
498+ const files = event ?.target ?.files || (clipboardFile ? [clipboardFile ] : []);
499+ for (let i = 0 ; i < files .length ; i ++ ) {
500+ if (requestAttachmentFiles .value .find ((f : any ) => f .name === files [i ].name )) {
501+ adminforth .alert ({
502+ message: $t (' File with the same name already added' ),
503+ variant: ' warning' ,
504+ timeout: 5000 ,
505+ });
506+ continue ;
507+ }
508+ const file = files [i ];
509+ const fileUrl = await uploadFile (file );
510+ if (! fileUrl ) continue ;
511+ requestAttachmentFiles .value ! .push (file );
512+ requestAttachmentFilesUrls .value .push (fileUrl );
513+ }
514+ fileInput .value ! .value = ' ' ;
515+ }
516+
517+ function removeFileFromList(index : number ) {
518+ requestAttachmentFiles .value ! .splice (index , 1 );
519+ requestAttachmentFilesUrls .value .splice (index , 1 );
520+ }
521+
522+ const uploading = ref (false );
523+
524+ async function uploadFile(file : any ): Promise <string > {
525+ const { name, size, type } = file ;
526+
527+ let imgPreview = ' ' ;
528+
529+ const extension = name .split (' .' ).pop ();
530+ const nameNoExtension = name .replace (` .${extension } ` , ' ' );
531+
532+ if (props .meta .maxFileSize && size > props .meta .maxFileSize ) {
533+ adminforth .alert ({
534+ message: t (' Sorry but the file size {size} is too large. Please upload a file with a maximum size of {maxFileSize}' , {
535+ size: props .humanifySize (size ),
536+ maxFileSize: props .humanifySize (props .meta .maxFileSize ),
537+ }),
538+ variant: ' danger'
539+ });
540+ return ;
541+ }
542+
543+ try {
544+ uploading .value = true ;
545+ const { uploadUrl, uploadExtraParams, filePath, error, previewUrl } = await callAdminForthApi ({
546+ path: ` /plugin/${props .meta .pluginInstanceId }/get_file_upload_url ` ,
547+ method: ' POST' ,
548+ body: {
549+ originalFilename: nameNoExtension ,
550+ contentType: type ,
551+ size ,
552+ originalExtension: extension ,
553+ recordPk: route ?.params ?.primaryKey ,
554+ },
555+ });
556+ if (error ) {
557+ adminforth .alert ({
558+ message: t (' File was not uploaded because of error: {error}' , { error }),
559+ variant: ' danger'
560+ });
561+ imgPreview = null ;
562+ return ;
563+ }
564+
565+ const xhr = new XMLHttpRequest ();
566+ const success = await new Promise ((resolve ) => {
567+ xhr .addEventListener (' loadend' , () => {
568+ const success = xhr .readyState === 4 && xhr .status === 200 ;
569+ // try to read response
570+ resolve (success );
571+ });
572+ xhr .open (' PUT' , uploadUrl , true );
573+ xhr .setRequestHeader (' Content-Type' , type );
574+ uploadExtraParams && Object .entries (uploadExtraParams ).forEach (([key , value ]: [string , string ]) => {
575+ xhr .setRequestHeader (key , value );
576+ })
577+ xhr .send (file );
578+ });
579+
580+ if (success ) {
581+ imgPreview = previewUrl ;
582+ } else {
583+ throw new Error (' File upload failed' );
584+ }
585+ } catch (err ) {
586+ uploading .value = false ;
587+ console .error (' File upload failed' , err );
588+ }
589+ uploading .value = false ;
590+ return imgPreview ;
591+ }
592+
593+ async function uploadImageOnPaste(event ) {
594+ const items = event .clipboardData ?.items ;
595+ if (! items ) return ;
596+
597+ for (let item of items ) {
598+ if (item .type .startsWith (' image/' )) {
599+ const file = item .getAsFile ();
600+ if (file ) {
601+ await handleAddFile (null , file );
602+ }
603+ }
604+ }
605+ }
606+
607+ function renameFile(file , newName ) {
608+ return new File ([file ], newName , { type: file .type });
609+ }
610+
611+ onMounted (() => {
612+ document .addEventListener (' paste' , uploadImageOnPaste );
613+ });
614+
615+ onUnmounted (() => {
616+ document .removeEventListener (' paste' , uploadImageOnPaste );
617+ });
454618 </script >
0 commit comments