@adminforth/upload 2.15.15 → 2.15.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build.log CHANGED
@@ -12,5 +12,5 @@ custom/preview.vue
12
12
  custom/tsconfig.json
13
13
  custom/uploader.vue
14
14
 
15
- sent 68,748 bytes received 153 bytes 137,802.00 bytes/sec
16
- total size is 68,186 speedup is 0.99
15
+ sent 66,735 bytes received 153 bytes 133,776.00 bytes/sec
16
+ total size is 66,181 speedup is 0.99
@@ -1,217 +1,215 @@
1
1
 
2
2
  <template>
3
3
  <!-- Main modal -->
4
- <div tabindex="-1" class="overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 bottom-0 z-50 flex justify-center items-center w-full md:inset-0 h-full max-h-full bg-white bg-opacity-50 dark:bg-gray-900 dark:bg-opacity-50">
5
- <div class="relative p-4 w-10/12 max-w-full max-h-full ">
6
- <!-- Modal content -->
7
- <div class="relative bg-white rounded-lg shadow-xl dark:bg-gray-700">
8
- <!-- Modal header -->
9
- <div class="flex items-center justify-between p-3 md:p-4 border-b rounded-t dark:border-gray-600">
10
- <h3 class="text-xl font-semibold text-gray-900 dark:text-white">
11
- {{ $t('Generate image with AI') }}
12
- </h3>
13
- <button type="button"
14
- @click="() => {stopGeneration = true; emit('close')}"
15
- class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" >
16
- <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
17
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
18
- </svg>
19
- <span class="sr-only">{{ $t('Close modal') }}</span>
20
- </button>
21
- </div>
22
- <!-- Modal body -->
23
- <div class="p-4 md:p-5 space-y-4">
24
- <!-- PROMPT TEXTAREA -->
25
- <!-- Textarea -->
26
- <textarea
27
- id="message"
28
- rows="3"
29
- class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
30
- :placeholder="$t('Prompt which will be passed to AI network')"
31
- v-model="prompt"
32
- :title="$t('Prompt which will be passed to AI network')"
33
- ></textarea>
34
-
35
- <!-- Thumbnails -->
36
- <div class="mt-2 flex flex-wrap gap-2">
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/*"
66
- />
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" />
74
- </div>
75
-
76
- <!-- Fullscreen Modal -->
4
+ <div tabindex="-1" class="overflow-y-auto overflow-x-hidden fixed inset-0 z-50 flex justify-center items-center w-full h-full bg-black/50">
5
+ <div class="relative p-4 w-11/12 max-w-5xl max-h-full ">
6
+ <!-- Modal content -->
7
+ <div class="relative bg-white rounded-xl shadow-2xl dark:bg-gray-800">
8
+ <!-- Header -->
9
+ <div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
10
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-white">
11
+ {{ $t('Generate image with AI') }}
12
+ </h3>
13
+ <button type="button"
14
+ @click="() => {stopGeneration = true; emit('close') }"
15
+ class="text-gray-400 bg-transparent hover:bg-gray-100 hover:text-gray-900 rounded-lg w-8 h-8 inline-flex justify-center items-center dark:hover:bg-gray-700 dark:hover:text-white transition" >
16
+ <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
17
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
18
+ </svg>
19
+ <span class="sr-only">{{ $t('Close modal') }}</span>
20
+ </button>
21
+ </div>
22
+ <!-- Modal body -->
23
+ <div class="px-6 py-5 space-y-4">
24
+ <!-- PROMPT TEXTAREA -->
25
+ <!-- Textarea -->
26
+ <textarea
27
+ rows="3"
28
+ class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-200 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 resize-none"
29
+ :placeholder="$t('Prompt which will be passed to AI network')"
30
+ v-model="prompt"
31
+ :title="$t('Prompt which will be passed to AI network')"
32
+ ></textarea>
33
+
34
+ <!-- Thumbnails -->
35
+ <div class="flex flex-wrap gap-2">
36
+ <div class="group relative" v-for="(img, key) in requestAttachmentFilesUrls" :key="key">
37
+ <img
38
+ :src="img"
39
+ class="w-14 h-14 object-cover rounded-lg cursor-pointer border border-gray-200 hover:border-blue-500 transition"
40
+ :alt="`Attachment ${key + 1}`"
41
+ @click="zoomImage(img)"
42
+ />
77
43
  <div
78
- v-if="zoomedImage"
79
- class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-80"
80
- @click.self="closeZoom"
44
+ 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"
45
+ @click="removeFileFromList(key)"
81
46
  >
82
- <img
83
- :src="zoomedImage"
84
- ref="zoomedImg"
85
- class="max-w-full max-h-full rounded-lg object-contain cursor-grab z-75"
47
+ <Tooltip class="absolute top-0 end-0">
48
+ <div>
49
+ <div class="w-4 h-4 absolute"></div>
50
+ <IconCloseOutline class="w-3 h-3 text-white hover:text-gray-300" />
51
+ </div>
52
+ <template #tooltip>Remove file</template>
53
+ </Tooltip>
54
+ </div>
55
+ </div>
56
+ <input ref="fileInput" class="hidden" type="file" @change="handleAddFile" accept="image/*" />
57
+ <button v-if="!uploading" @click="fileInput?.click()" type="button" :disabled="requestAttachmentFilesUrls.length >= 1" class=" upload-btn relative group hover:border-gray-400 disabled:opacity-40 disabled:cursor-not-allowed transition border-gray-300 flex items-center justify-center w-14 h-14 border-2 border-dashed rounded-lg">
58
+ <div class="flex flex-col items-center justify-center gap-1">
59
+ <IconCloseOutline class="group-hover:text-gray-500 transition rotate-45 w-5 h-5 text-gray-300" />
60
+ <p class="text-gray-300 group-hover:text-gray-500 transition text-[10px] leading-none">Ctrl+v</p>
61
+ </div>
62
+ </button>
63
+ <Skeleton v-else type="image" class="w-14 h-14" />
64
+ </div>
65
+
66
+ <div class="grid grid-cols-[1fr_52px_1fr]">
67
+ <span class="pb-2 text-sm font-medium text-gray-500 dark:text-gray-400">{{ $t('Original image') }}</span>
68
+ <div></div>
69
+ <span class="pb-2 text-sm font-medium text-gray-500 dark:text-gray-400">{{ $t('Generated image') }}</span>
70
+ <div
71
+ class="rounded-xl overflow-hidden bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 aspect-[4/3] flex items-center justify-center"
72
+ :class="requestAttachmentFilesUrls.length ? 'cursor-zoom-in' : ''"
73
+ @click="requestAttachmentFilesUrls.length && zoomImage(requestAttachmentFilesUrls[0])"
74
+ >
75
+ <img
76
+ v-if="requestAttachmentFilesUrls.length"
77
+ :src="requestAttachmentFilesUrls[0]"
78
+ class="w-full h-full object-cover"
86
79
  />
80
+ <span v-else class="text-gray-400 dark:text-gray-500 text-sm">{{ $t('No image') }}</span>
87
81
  </div>
88
82
 
89
- <div class="flex flex-col items-center justify-center w-full relative">
90
- <div
91
- v-if="loading"
92
- class=" absolute flex items-center justify-center w-full h-full z-40 bg-white/80 dark:bg-gray-900/80 rounded-lg"
93
- >
94
- <div role="status" class="absolute -translate-x-1/2 -translate-y-1/2 top-2/4 left-1/2">
95
- <svg aria-hidden="true" class="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/><path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/></svg>
96
- <span class="sr-only">{{ $t('Loading...') }}</span>
97
- </div>
98
- </div>
83
+ <div class="flex items-center justify-center">
84
+ <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
85
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/>
86
+ </svg>
87
+ </div>
99
88
 
100
- <div v-if="loadingTimer" class="absolute pt-12 flex items-center justify-center w-full h-full z-40 bg-white/80 dark:bg-gray-900/80 rounded-lg">
101
- <div class="text-gray-800 dark:text-gray-100 text-lg font-semibold"
102
- v-if="!historicalAverage"
103
- >
104
- {{ formatTime(loadingTimer) }} {{ $t('passed...') }}
105
- </div>
106
- <div class="w-64" v-else>
107
- <ProgressBar
108
- class="absolute max-w-full"
109
- :currentValue="loadingTimer < historicalAverage ? loadingTimer : historicalAverage"
110
- :minValue="0"
111
- :maxValue="historicalAverage"
112
- :showValues="false"
113
- :progressFormatter="(value: number, percentage: number) => `${ formatTime(loadingTimer) } ( ~ ${ Math.floor( (
114
- loadingTimer < historicalAverage ? loadingTimer : historicalAverage
115
- ) / historicalAverage * 100) }% )`"
116
- />
117
- </div>
118
- </div>
89
+ <div class="relative aspect-[4/3]">
119
90
 
120
- <div v-if="errorMessage" class="absolute flex items-center justify-center w-full h-full z-40 bg-white/80 dark:bg-gray-900/80 rounded-lg">
121
- <div class="pt-20 text-red-500 dark:text-red-400 text-lg font-semibold">
122
- {{ errorMessage }}
123
- </div>
91
+ <div
92
+ v-if="loading"
93
+ class="spinner absolute flex items-center justify-center inset-0 z-40 rounded-xl">
94
+ <Spinner class="w-8 h-8" />
95
+ </div>
96
+ <div v-if="loadingTimer" class=" spinner-description absolute inset-0 z-40 flex items-center justify-center">
97
+ <div class="text-gray-800 dark:text-gray-100 text-base font-semibold mt-20" v-if="!historicalAverage">
98
+ {{ formatTime(loadingTimer) }} {{ $t('passed...') }}
99
+ </div>
100
+ <div class="w-48" v-else>
101
+ <ProgressBar
102
+ class="max-w-full"
103
+ :currentValue="loadingTimer < historicalAverage ? loadingTimer : historicalAverage"
104
+ :minValue="0"
105
+ :maxValue="historicalAverage"
106
+ :showValues="false"
107
+ :progressFormatter="(_value: number, _percentage: number) => `${ formatTime(loadingTimer) } ( ~ ${ Math.floor( (loadingTimer < historicalAverage ? loadingTimer : historicalAverage) / historicalAverage * 100) }% )`"
108
+ />
124
109
  </div>
110
+ </div>
111
+
112
+ <div v-if="errorMessage" class="absolute inset-0 z-40 flex items-center justify-center bg-white/80 dark:bg-gray-900/80 rounded-xl">
113
+ <div class="text-red-500 dark:text-red-400 font-semibold text-sm text-center px-4">{{ errorMessage }}</div>
114
+ </div>
125
115
 
126
-
127
- <div id="gallery" class="relative w-full" data-carousel="static">
128
- <!-- Carousel wrapper -->
129
- <div class="relative h-56 overflow-hidden rounded-lg md:h-[calc(100vh-400px)]">
130
- <!-- Item 1 -->
131
- <div v-for="(img, index) in images" :key="index" class="hidden duration-700 ease-in-out" data-carousel-item>
132
- <img :src="img" class="absolute block max-w-full max-h-full -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2 object-cover"
133
- :alt="`Generated image ${index + 1}`"
134
- />
135
- </div>
136
-
137
- <div v-if="images.length === 0" class="flex items-center justify-center w-full h-full">
138
-
139
- <button @click="generateImages" type="button" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4
140
- focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center
141
- dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 ms-2">{{ $t('Generate images') }}</button>
142
-
143
- </div>
144
-
116
+ <div id="gallery" class="relative w-full h-full" data-carousel="static">
117
+ <div
118
+ class="relative w-full h-full overflow-hidden rounded-xl"
119
+ :class="images.length > 0 ? 'cursor-zoom-in' : ''"
120
+ @click="zoomCurrentImage"
121
+ >
122
+ <div v-for="(img, index) in images" :key="index" class="hidden duration-700 ease-in-out" data-carousel-item>
123
+ <img :src="img" class="absolute block w-full h-full object-cover -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2"
124
+ :alt="`Generated image ${index + 1}`"
125
+ />
126
+ </div>
127
+ <div v-if="images.length === 0" class="w-full h-full flex items-center justify-center border-2 border-dashed border-gray-200 dark:border-gray-600 rounded-xl bg-gray-50 dark:bg-gray-700">
128
+ <span v-if="!loadingTimer" class="text-gray-400 dark:text-gray-500 text-sm">{{ $t('Your generated image will appear here') }}</span>
145
129
  </div>
146
- <!-- Slider controls -->
147
- <button type="button" class="absolute top-0 start-0 z-30 flex items-center justify-center h-full px-4 cursor-pointer group focus:outline-none"
148
- @click="slide(-1)"
149
- :disabled="images.length === 0"
150
- >
151
- <span class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-white/30 dark:bg-gray-800/30 group-hover:bg-white/50 dark:group-hover:bg-gray-800/60 group-focus:ring-4 group-focus:ring-white dark:group-focus:ring-gray-800/70 group-focus:outline-none ">
152
- <svg class="w-4 h-4 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10"
153
- :class="{
154
- 'text-gray-800 dark:text-gray-200': images.length > 0,
155
- 'text-gray-200 dark:text-gray-800': images.length === 0
156
- }"
157
- >
158
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1 1 5l4 4"/>
159
- </svg>
160
- <span class="sr-only">{{ $t('Previous') }}</span>
161
- </span>
162
- </button>
163
- <button type="button" class="absolute top-0 end-0 z-30 flex items-center justify-center h-full px-4 cursor-pointer group focus:outline-none "
164
- :disabled="images.length === 0"
165
- @click="slide(1)"
166
- >
167
- <span class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-white/30 dark:bg-gray-800/30 group-hover:bg-white/50 dark:group-hover:bg-gray-800/60 group-focus:ring-4 group-focus:ring-white dark:group-focus:ring-gray-800/70 group-focus:outline-none ">
168
- <svg class="w-4 h-4 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10"
169
- :class="{
170
- 'text-gray-800 dark:text-gray-200': images.length > 0,
171
- 'text-gray-200 dark:text-gray-800': images.length === 0
172
- }"
173
- >
174
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4"/>
175
- </svg>
176
- <span class="sr-only">{{ $t('Next') }}</span>
177
- </span>
178
- </button>
179
-
180
-
181
130
  </div>
182
131
  </div>
183
132
  </div>
184
- <!-- Modal footer -->
185
- <div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
186
- <button type="button" @click="confirmImage"
187
- :disabled="loading || images.length === 0"
188
- class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center
189
- dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
190
- disabled:opacity-50 disabled:cursor-not-allowed"
191
- >{{ $t('Use image') }}</button>
192
- <button type="button" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
193
- @click="() => {stopGeneration = true; emit('close')}"
194
- >{{ $t('Cancel') }}</button>
133
+ <div class="col-start-3 flex items-center justify-center gap-3 pt-3">
134
+ <button
135
+ type="button"
136
+ @click="slide(-1)"
137
+ :disabled="images.length === 0"
138
+ class="inline-flex items-center justify-center w-8 h-8 rounded-full border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-30 disabled:cursor-not-allowed transition shadow-sm"
139
+ >
140
+ <svg class="w-4 h-4 text-gray-700 dark:text-gray-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
141
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1 1 5l4 4"/>
142
+ </svg>
143
+ <span class="sr-only">{{ $t('Previous') }}</span>
144
+ </button>
145
+ <button type="button" class="inline-flex items-center justify-center w-8 h-8 rounded-full border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-30 disabled:cursor-not-allowed transition shadow-sm"
146
+ :disabled="loading"
147
+ @click="slide(1)"
148
+ >
149
+ <svg class="w-4 h-4 text-gray-700 dark:text-gray-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
150
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4"/>
151
+ </svg>
152
+ <span class="sr-only">{{ $t('Next') }}</span>
153
+ </button>
195
154
  </div>
155
+ </div>
196
156
  </div>
157
+ <!-- Modal footer -->
158
+ <div class="flex items-center justify-between px-6 py-4 border-t border-gray-200 dark:border-gray-700">
159
+ <div class="flex items-center gap-2">
160
+ <Button
161
+ @click="confirmImage"
162
+ :disabled="loading || images.length === 0"
163
+ variant="primary"
164
+ >{{ $t('Use image') }}</Button>
165
+ <Button
166
+ @click="() => { stopGeneration = true; emit('close') }"
167
+ variant="secondary"
168
+ >{{ $t('Cancel') }}</Button>
169
+ </div>
170
+ <Button
171
+ @click="generateImages"
172
+ :disabled="loading"
173
+ variant="primary"
174
+ >{{ $t('Generate images') }}</Button>
175
+ </div>
176
+
177
+ </div>
197
178
  </div>
198
179
  </div>
199
180
 
200
-
201
-
181
+ <div
182
+ v-if="zoomedImage"
183
+ class="fixed inset-0 z-[100] flex items-center justify-center bg-black/90 cursor-zoom-out"
184
+ @click="closeZoom"
185
+ >
186
+ <img
187
+ :src="zoomedImage"
188
+ class="max-w-[90vw] max-h-[90vh] rounded-lg object-contain select-none"
189
+ />
190
+ <button
191
+ class="absolute top-4 right-4 text-white/70 hover:text-white w-9 h-9 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 transition"
192
+ >
193
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 14 14">
194
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
195
+ </svg>
196
+ </button>
197
+ </div>
202
198
 
203
199
  </template>
204
200
 
205
201
  <script setup lang="ts">
206
202
 
207
- import { ref, onMounted, nextTick, Ref, watch, onUnmounted } from 'vue'
203
+ import { ref, onMounted, nextTick, Ref, onUnmounted } from 'vue'
208
204
  import { Carousel } from 'flowbite';
209
205
  import { callAdminForthApi } from '@/utils';
210
206
  import { useI18n } from 'vue-i18n';
211
207
  import adminforth from '@/adminforth';
208
+ import { ProgressBar, Button } from '@/afcl';
209
+ import * as Handlebars from 'handlebars';
212
210
  import { ProgressBar } from '@/afcl';
213
211
  import { IconCloseOutline } from '@iconify-prerendered/vue-flowbite';
214
- import { Tooltip, Skeleton } from '@/afcl'
212
+ import { Tooltip, Skeleton, Spinner } from '@/afcl'
215
213
  import { useRoute } from 'vue-router';
216
214
 
217
215
  const { t: $t, t } = useI18n();
@@ -468,29 +466,25 @@ async function generateImages() {
468
466
  loading.value = false;
469
467
  }
470
468
 
471
- import mediumZoom from 'medium-zoom'
472
-
473
469
  const zoomedImage = ref(null)
474
- const zoomedImg = ref(null)
475
470
 
476
471
  function zoomImage(img) {
477
472
  zoomedImage.value = img
478
473
  }
479
474
 
475
+ function zoomCurrentImage() {
476
+ if (images.value.length === 0) return;
477
+ const index = caurosel.value?.getActiveItem()?.position || 0;
478
+ zoomImage(images.value[index]);
479
+ }
480
+
480
481
  function closeZoom() {
481
482
  zoomedImage.value = null
482
483
  }
483
484
 
484
- watch(zoomedImage, async (val) => {
485
- await nextTick()
486
- if (val && zoomedImg.value) {
487
- mediumZoom(zoomedImg.value, {
488
- margin: 24,
489
- background: 'rgba(0, 0, 0, 0.9)',
490
- scrollOffset: 150
491
- }).show()
492
- }
493
- })
485
+ function onZoomKeyDown(e: KeyboardEvent) {
486
+ if (e.key === 'Escape') closeZoom();
487
+ }
494
488
 
495
489
  async function handleAddFile(event, clipboardFile = null) {
496
490
  if (clipboardFile) {
@@ -611,9 +605,11 @@ function renameFile(file, newName) {
611
605
 
612
606
  onMounted(() => {
613
607
  document.addEventListener('paste', uploadImageOnPaste);
608
+ document.addEventListener('keydown', onZoomKeyDown);
614
609
  });
615
610
 
616
611
  onUnmounted(() => {
617
612
  document.removeEventListener('paste', uploadImageOnPaste);
613
+ document.removeEventListener('keydown', onZoomKeyDown);
618
614
  });
619
615
  </script>
@@ -1,217 +1,215 @@
1
1
 
2
2
  <template>
3
3
  <!-- Main modal -->
4
- <div tabindex="-1" class="overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 bottom-0 z-50 flex justify-center items-center w-full md:inset-0 h-full max-h-full bg-white bg-opacity-50 dark:bg-gray-900 dark:bg-opacity-50">
5
- <div class="relative p-4 w-10/12 max-w-full max-h-full ">
6
- <!-- Modal content -->
7
- <div class="relative bg-white rounded-lg shadow-xl dark:bg-gray-700">
8
- <!-- Modal header -->
9
- <div class="flex items-center justify-between p-3 md:p-4 border-b rounded-t dark:border-gray-600">
10
- <h3 class="text-xl font-semibold text-gray-900 dark:text-white">
11
- {{ $t('Generate image with AI') }}
12
- </h3>
13
- <button type="button"
14
- @click="() => {stopGeneration = true; emit('close')}"
15
- class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" >
16
- <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
17
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
18
- </svg>
19
- <span class="sr-only">{{ $t('Close modal') }}</span>
20
- </button>
21
- </div>
22
- <!-- Modal body -->
23
- <div class="p-4 md:p-5 space-y-4">
24
- <!-- PROMPT TEXTAREA -->
25
- <!-- Textarea -->
26
- <textarea
27
- id="message"
28
- rows="3"
29
- class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
30
- :placeholder="$t('Prompt which will be passed to AI network')"
31
- v-model="prompt"
32
- :title="$t('Prompt which will be passed to AI network')"
33
- ></textarea>
34
-
35
- <!-- Thumbnails -->
36
- <div class="mt-2 flex flex-wrap gap-2">
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/*"
66
- />
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" />
74
- </div>
75
-
76
- <!-- Fullscreen Modal -->
4
+ <div tabindex="-1" class="overflow-y-auto overflow-x-hidden fixed inset-0 z-50 flex justify-center items-center w-full h-full bg-black/50">
5
+ <div class="relative p-4 w-11/12 max-w-5xl max-h-full ">
6
+ <!-- Modal content -->
7
+ <div class="relative bg-white rounded-xl shadow-2xl dark:bg-gray-800">
8
+ <!-- Header -->
9
+ <div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
10
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-white">
11
+ {{ $t('Generate image with AI') }}
12
+ </h3>
13
+ <button type="button"
14
+ @click="() => {stopGeneration = true; emit('close') }"
15
+ class="text-gray-400 bg-transparent hover:bg-gray-100 hover:text-gray-900 rounded-lg w-8 h-8 inline-flex justify-center items-center dark:hover:bg-gray-700 dark:hover:text-white transition" >
16
+ <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
17
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
18
+ </svg>
19
+ <span class="sr-only">{{ $t('Close modal') }}</span>
20
+ </button>
21
+ </div>
22
+ <!-- Modal body -->
23
+ <div class="px-6 py-5 space-y-4">
24
+ <!-- PROMPT TEXTAREA -->
25
+ <!-- Textarea -->
26
+ <textarea
27
+ rows="3"
28
+ class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-200 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 resize-none"
29
+ :placeholder="$t('Prompt which will be passed to AI network')"
30
+ v-model="prompt"
31
+ :title="$t('Prompt which will be passed to AI network')"
32
+ ></textarea>
33
+
34
+ <!-- Thumbnails -->
35
+ <div class="flex flex-wrap gap-2">
36
+ <div class="group relative" v-for="(img, key) in requestAttachmentFilesUrls" :key="key">
37
+ <img
38
+ :src="img"
39
+ class="w-14 h-14 object-cover rounded-lg cursor-pointer border border-gray-200 hover:border-blue-500 transition"
40
+ :alt="`Attachment ${key + 1}`"
41
+ @click="zoomImage(img)"
42
+ />
77
43
  <div
78
- v-if="zoomedImage"
79
- class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-80"
80
- @click.self="closeZoom"
44
+ 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"
45
+ @click="removeFileFromList(key)"
81
46
  >
82
- <img
83
- :src="zoomedImage"
84
- ref="zoomedImg"
85
- class="max-w-full max-h-full rounded-lg object-contain cursor-grab z-75"
47
+ <Tooltip class="absolute top-0 end-0">
48
+ <div>
49
+ <div class="w-4 h-4 absolute"></div>
50
+ <IconCloseOutline class="w-3 h-3 text-white hover:text-gray-300" />
51
+ </div>
52
+ <template #tooltip>Remove file</template>
53
+ </Tooltip>
54
+ </div>
55
+ </div>
56
+ <input ref="fileInput" class="hidden" type="file" @change="handleAddFile" accept="image/*" />
57
+ <button v-if="!uploading" @click="fileInput?.click()" type="button" :disabled="requestAttachmentFilesUrls.length >= 1" class=" upload-btn relative group hover:border-gray-400 disabled:opacity-40 disabled:cursor-not-allowed transition border-gray-300 flex items-center justify-center w-14 h-14 border-2 border-dashed rounded-lg">
58
+ <div class="flex flex-col items-center justify-center gap-1">
59
+ <IconCloseOutline class="group-hover:text-gray-500 transition rotate-45 w-5 h-5 text-gray-300" />
60
+ <p class="text-gray-300 group-hover:text-gray-500 transition text-[10px] leading-none">Ctrl+v</p>
61
+ </div>
62
+ </button>
63
+ <Skeleton v-else type="image" class="w-14 h-14" />
64
+ </div>
65
+
66
+ <div class="grid grid-cols-[1fr_52px_1fr]">
67
+ <span class="pb-2 text-sm font-medium text-gray-500 dark:text-gray-400">{{ $t('Original image') }}</span>
68
+ <div></div>
69
+ <span class="pb-2 text-sm font-medium text-gray-500 dark:text-gray-400">{{ $t('Generated image') }}</span>
70
+ <div
71
+ class="rounded-xl overflow-hidden bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 aspect-[4/3] flex items-center justify-center"
72
+ :class="requestAttachmentFilesUrls.length ? 'cursor-zoom-in' : ''"
73
+ @click="requestAttachmentFilesUrls.length && zoomImage(requestAttachmentFilesUrls[0])"
74
+ >
75
+ <img
76
+ v-if="requestAttachmentFilesUrls.length"
77
+ :src="requestAttachmentFilesUrls[0]"
78
+ class="w-full h-full object-cover"
86
79
  />
80
+ <span v-else class="text-gray-400 dark:text-gray-500 text-sm">{{ $t('No image') }}</span>
87
81
  </div>
88
82
 
89
- <div class="flex flex-col items-center justify-center w-full relative">
90
- <div
91
- v-if="loading"
92
- class=" absolute flex items-center justify-center w-full h-full z-40 bg-white/80 dark:bg-gray-900/80 rounded-lg"
93
- >
94
- <div role="status" class="absolute -translate-x-1/2 -translate-y-1/2 top-2/4 left-1/2">
95
- <svg aria-hidden="true" class="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/><path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/></svg>
96
- <span class="sr-only">{{ $t('Loading...') }}</span>
97
- </div>
98
- </div>
83
+ <div class="flex items-center justify-center">
84
+ <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
85
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/>
86
+ </svg>
87
+ </div>
99
88
 
100
- <div v-if="loadingTimer" class="absolute pt-12 flex items-center justify-center w-full h-full z-40 bg-white/80 dark:bg-gray-900/80 rounded-lg">
101
- <div class="text-gray-800 dark:text-gray-100 text-lg font-semibold"
102
- v-if="!historicalAverage"
103
- >
104
- {{ formatTime(loadingTimer) }} {{ $t('passed...') }}
105
- </div>
106
- <div class="w-64" v-else>
107
- <ProgressBar
108
- class="absolute max-w-full"
109
- :currentValue="loadingTimer < historicalAverage ? loadingTimer : historicalAverage"
110
- :minValue="0"
111
- :maxValue="historicalAverage"
112
- :showValues="false"
113
- :progressFormatter="(value: number, percentage: number) => `${ formatTime(loadingTimer) } ( ~ ${ Math.floor( (
114
- loadingTimer < historicalAverage ? loadingTimer : historicalAverage
115
- ) / historicalAverage * 100) }% )`"
116
- />
117
- </div>
118
- </div>
89
+ <div class="relative aspect-[4/3]">
119
90
 
120
- <div v-if="errorMessage" class="absolute flex items-center justify-center w-full h-full z-40 bg-white/80 dark:bg-gray-900/80 rounded-lg">
121
- <div class="pt-20 text-red-500 dark:text-red-400 text-lg font-semibold">
122
- {{ errorMessage }}
123
- </div>
91
+ <div
92
+ v-if="loading"
93
+ class="spinner absolute flex items-center justify-center inset-0 z-40 rounded-xl">
94
+ <Spinner class="w-8 h-8" />
95
+ </div>
96
+ <div v-if="loadingTimer" class=" spinner-description absolute inset-0 z-40 flex items-center justify-center">
97
+ <div class="text-gray-800 dark:text-gray-100 text-base font-semibold mt-20" v-if="!historicalAverage">
98
+ {{ formatTime(loadingTimer) }} {{ $t('passed...') }}
99
+ </div>
100
+ <div class="w-48" v-else>
101
+ <ProgressBar
102
+ class="max-w-full"
103
+ :currentValue="loadingTimer < historicalAverage ? loadingTimer : historicalAverage"
104
+ :minValue="0"
105
+ :maxValue="historicalAverage"
106
+ :showValues="false"
107
+ :progressFormatter="(_value: number, _percentage: number) => `${ formatTime(loadingTimer) } ( ~ ${ Math.floor( (loadingTimer < historicalAverage ? loadingTimer : historicalAverage) / historicalAverage * 100) }% )`"
108
+ />
124
109
  </div>
110
+ </div>
111
+
112
+ <div v-if="errorMessage" class="absolute inset-0 z-40 flex items-center justify-center bg-white/80 dark:bg-gray-900/80 rounded-xl">
113
+ <div class="text-red-500 dark:text-red-400 font-semibold text-sm text-center px-4">{{ errorMessage }}</div>
114
+ </div>
125
115
 
126
-
127
- <div id="gallery" class="relative w-full" data-carousel="static">
128
- <!-- Carousel wrapper -->
129
- <div class="relative h-56 overflow-hidden rounded-lg md:h-[calc(100vh-400px)]">
130
- <!-- Item 1 -->
131
- <div v-for="(img, index) in images" :key="index" class="hidden duration-700 ease-in-out" data-carousel-item>
132
- <img :src="img" class="absolute block max-w-full max-h-full -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2 object-cover"
133
- :alt="`Generated image ${index + 1}`"
134
- />
135
- </div>
136
-
137
- <div v-if="images.length === 0" class="flex items-center justify-center w-full h-full">
138
-
139
- <button @click="generateImages" type="button" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4
140
- focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center
141
- dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 ms-2">{{ $t('Generate images') }}</button>
142
-
143
- </div>
144
-
116
+ <div id="gallery" class="relative w-full h-full" data-carousel="static">
117
+ <div
118
+ class="relative w-full h-full overflow-hidden rounded-xl"
119
+ :class="images.length > 0 ? 'cursor-zoom-in' : ''"
120
+ @click="zoomCurrentImage"
121
+ >
122
+ <div v-for="(img, index) in images" :key="index" class="hidden duration-700 ease-in-out" data-carousel-item>
123
+ <img :src="img" class="absolute block w-full h-full object-cover -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2"
124
+ :alt="`Generated image ${index + 1}`"
125
+ />
126
+ </div>
127
+ <div v-if="images.length === 0" class="w-full h-full flex items-center justify-center border-2 border-dashed border-gray-200 dark:border-gray-600 rounded-xl bg-gray-50 dark:bg-gray-700">
128
+ <span v-if="!loadingTimer" class="text-gray-400 dark:text-gray-500 text-sm">{{ $t('Your generated image will appear here') }}</span>
145
129
  </div>
146
- <!-- Slider controls -->
147
- <button type="button" class="absolute top-0 start-0 z-30 flex items-center justify-center h-full px-4 cursor-pointer group focus:outline-none"
148
- @click="slide(-1)"
149
- :disabled="images.length === 0"
150
- >
151
- <span class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-white/30 dark:bg-gray-800/30 group-hover:bg-white/50 dark:group-hover:bg-gray-800/60 group-focus:ring-4 group-focus:ring-white dark:group-focus:ring-gray-800/70 group-focus:outline-none ">
152
- <svg class="w-4 h-4 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10"
153
- :class="{
154
- 'text-gray-800 dark:text-gray-200': images.length > 0,
155
- 'text-gray-200 dark:text-gray-800': images.length === 0
156
- }"
157
- >
158
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1 1 5l4 4"/>
159
- </svg>
160
- <span class="sr-only">{{ $t('Previous') }}</span>
161
- </span>
162
- </button>
163
- <button type="button" class="absolute top-0 end-0 z-30 flex items-center justify-center h-full px-4 cursor-pointer group focus:outline-none "
164
- :disabled="images.length === 0"
165
- @click="slide(1)"
166
- >
167
- <span class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-white/30 dark:bg-gray-800/30 group-hover:bg-white/50 dark:group-hover:bg-gray-800/60 group-focus:ring-4 group-focus:ring-white dark:group-focus:ring-gray-800/70 group-focus:outline-none ">
168
- <svg class="w-4 h-4 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10"
169
- :class="{
170
- 'text-gray-800 dark:text-gray-200': images.length > 0,
171
- 'text-gray-200 dark:text-gray-800': images.length === 0
172
- }"
173
- >
174
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4"/>
175
- </svg>
176
- <span class="sr-only">{{ $t('Next') }}</span>
177
- </span>
178
- </button>
179
-
180
-
181
130
  </div>
182
131
  </div>
183
132
  </div>
184
- <!-- Modal footer -->
185
- <div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
186
- <button type="button" @click="confirmImage"
187
- :disabled="loading || images.length === 0"
188
- class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center
189
- dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
190
- disabled:opacity-50 disabled:cursor-not-allowed"
191
- >{{ $t('Use image') }}</button>
192
- <button type="button" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
193
- @click="() => {stopGeneration = true; emit('close')}"
194
- >{{ $t('Cancel') }}</button>
133
+ <div class="col-start-3 flex items-center justify-center gap-3 pt-3">
134
+ <button
135
+ type="button"
136
+ @click="slide(-1)"
137
+ :disabled="images.length === 0"
138
+ class="inline-flex items-center justify-center w-8 h-8 rounded-full border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-30 disabled:cursor-not-allowed transition shadow-sm"
139
+ >
140
+ <svg class="w-4 h-4 text-gray-700 dark:text-gray-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
141
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1 1 5l4 4"/>
142
+ </svg>
143
+ <span class="sr-only">{{ $t('Previous') }}</span>
144
+ </button>
145
+ <button type="button" class="inline-flex items-center justify-center w-8 h-8 rounded-full border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-30 disabled:cursor-not-allowed transition shadow-sm"
146
+ :disabled="loading"
147
+ @click="slide(1)"
148
+ >
149
+ <svg class="w-4 h-4 text-gray-700 dark:text-gray-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
150
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4"/>
151
+ </svg>
152
+ <span class="sr-only">{{ $t('Next') }}</span>
153
+ </button>
195
154
  </div>
155
+ </div>
196
156
  </div>
157
+ <!-- Modal footer -->
158
+ <div class="flex items-center justify-between px-6 py-4 border-t border-gray-200 dark:border-gray-700">
159
+ <div class="flex items-center gap-2">
160
+ <Button
161
+ @click="confirmImage"
162
+ :disabled="loading || images.length === 0"
163
+ variant="primary"
164
+ >{{ $t('Use image') }}</Button>
165
+ <Button
166
+ @click="() => { stopGeneration = true; emit('close') }"
167
+ variant="secondary"
168
+ >{{ $t('Cancel') }}</Button>
169
+ </div>
170
+ <Button
171
+ @click="generateImages"
172
+ :disabled="loading"
173
+ variant="primary"
174
+ >{{ $t('Generate images') }}</Button>
175
+ </div>
176
+
177
+ </div>
197
178
  </div>
198
179
  </div>
199
180
 
200
-
201
-
181
+ <div
182
+ v-if="zoomedImage"
183
+ class="fixed inset-0 z-[100] flex items-center justify-center bg-black/90 cursor-zoom-out"
184
+ @click="closeZoom"
185
+ >
186
+ <img
187
+ :src="zoomedImage"
188
+ class="max-w-[90vw] max-h-[90vh] rounded-lg object-contain select-none"
189
+ />
190
+ <button
191
+ class="absolute top-4 right-4 text-white/70 hover:text-white w-9 h-9 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 transition"
192
+ >
193
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 14 14">
194
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
195
+ </svg>
196
+ </button>
197
+ </div>
202
198
 
203
199
  </template>
204
200
 
205
201
  <script setup lang="ts">
206
202
 
207
- import { ref, onMounted, nextTick, Ref, watch, onUnmounted } from 'vue'
203
+ import { ref, onMounted, nextTick, Ref, onUnmounted } from 'vue'
208
204
  import { Carousel } from 'flowbite';
209
205
  import { callAdminForthApi } from '@/utils';
210
206
  import { useI18n } from 'vue-i18n';
211
207
  import adminforth from '@/adminforth';
208
+ import { ProgressBar, Button } from '@/afcl';
209
+ import * as Handlebars from 'handlebars';
212
210
  import { ProgressBar } from '@/afcl';
213
211
  import { IconCloseOutline } from '@iconify-prerendered/vue-flowbite';
214
- import { Tooltip, Skeleton } from '@/afcl'
212
+ import { Tooltip, Skeleton, Spinner } from '@/afcl'
215
213
  import { useRoute } from 'vue-router';
216
214
 
217
215
  const { t: $t, t } = useI18n();
@@ -468,29 +466,25 @@ async function generateImages() {
468
466
  loading.value = false;
469
467
  }
470
468
 
471
- import mediumZoom from 'medium-zoom'
472
-
473
469
  const zoomedImage = ref(null)
474
- const zoomedImg = ref(null)
475
470
 
476
471
  function zoomImage(img) {
477
472
  zoomedImage.value = img
478
473
  }
479
474
 
475
+ function zoomCurrentImage() {
476
+ if (images.value.length === 0) return;
477
+ const index = caurosel.value?.getActiveItem()?.position || 0;
478
+ zoomImage(images.value[index]);
479
+ }
480
+
480
481
  function closeZoom() {
481
482
  zoomedImage.value = null
482
483
  }
483
484
 
484
- watch(zoomedImage, async (val) => {
485
- await nextTick()
486
- if (val && zoomedImg.value) {
487
- mediumZoom(zoomedImg.value, {
488
- margin: 24,
489
- background: 'rgba(0, 0, 0, 0.9)',
490
- scrollOffset: 150
491
- }).show()
492
- }
493
- })
485
+ function onZoomKeyDown(e: KeyboardEvent) {
486
+ if (e.key === 'Escape') closeZoom();
487
+ }
494
488
 
495
489
  async function handleAddFile(event, clipboardFile = null) {
496
490
  if (clipboardFile) {
@@ -611,9 +605,11 @@ function renameFile(file, newName) {
611
605
 
612
606
  onMounted(() => {
613
607
  document.addEventListener('paste', uploadImageOnPaste);
608
+ document.addEventListener('keydown', onZoomKeyDown);
614
609
  });
615
610
 
616
611
  onUnmounted(() => {
617
612
  document.removeEventListener('paste', uploadImageOnPaste);
613
+ document.removeEventListener('keydown', onZoomKeyDown);
618
614
  });
619
615
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/upload",
3
- "version": "2.15.15",
3
+ "version": "2.15.16",
4
4
  "description": "Plugin for uploading files for adminforth",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",