@fy-/fws-vue 2.2.51 → 2.2.52
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/components/ui/DefaultGallery.vue +429 -111
- package/package.json +1 -1
|
@@ -2,19 +2,24 @@
|
|
|
2
2
|
import type { Component } from 'vue'
|
|
3
3
|
import type { APIPaging } from '../../composables/rest'
|
|
4
4
|
import {
|
|
5
|
-
ArrowLeftCircleIcon,
|
|
6
|
-
ArrowRightCircleIcon,
|
|
7
5
|
ChevronDoubleLeftIcon,
|
|
8
6
|
ChevronDoubleRightIcon,
|
|
9
|
-
|
|
7
|
+
ChevronLeftIcon,
|
|
8
|
+
ChevronRightIcon,
|
|
9
|
+
InformationCircleIcon,
|
|
10
|
+
XMarkIcon,
|
|
10
11
|
} from '@heroicons/vue/24/solid'
|
|
11
|
-
import { computed, h, onMounted, onUnmounted, reactive, ref } from 'vue'
|
|
12
|
+
import { computed, h, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
|
12
13
|
import { useEventBus } from '../../composables/event-bus'
|
|
13
14
|
import DefaultPaging from './DefaultPaging.vue'
|
|
14
15
|
|
|
15
16
|
const isGalleryOpen = ref<boolean>(false)
|
|
16
17
|
const eventBus = useEventBus()
|
|
17
18
|
const sidePanel = ref<boolean>(true)
|
|
19
|
+
const showControls = ref<boolean>(true)
|
|
20
|
+
const isFullscreen = ref<boolean>(false)
|
|
21
|
+
const infoPanel = ref<boolean>(false)
|
|
22
|
+
const touchStartTime = ref<number>(0)
|
|
18
23
|
|
|
19
24
|
const props = withDefaults(
|
|
20
25
|
defineProps<{
|
|
@@ -44,7 +49,7 @@ const props = withDefaults(
|
|
|
44
49
|
imageComponent: 'img',
|
|
45
50
|
mode: 'grid',
|
|
46
51
|
gridHeight: 4,
|
|
47
|
-
closeIcon: () => h(
|
|
52
|
+
closeIcon: () => h(XMarkIcon),
|
|
48
53
|
images: () => [],
|
|
49
54
|
isVideo: () => false,
|
|
50
55
|
getImageUrl: (image: any) => image.image_url,
|
|
@@ -65,6 +70,8 @@ const modelValue = computed({
|
|
|
65
70
|
|
|
66
71
|
const direction = ref<'next' | 'prev'>('next')
|
|
67
72
|
|
|
73
|
+
let controlsTimeout: number | null = null
|
|
74
|
+
|
|
68
75
|
function setModal(value: boolean) {
|
|
69
76
|
if (value === true) {
|
|
70
77
|
if (props.onOpen) props.onOpen()
|
|
@@ -73,6 +80,12 @@ function setModal(value: boolean) {
|
|
|
73
80
|
document.addEventListener('keydown', handleKeyboardInput)
|
|
74
81
|
document.addEventListener('keyup', handleKeyboardRelease)
|
|
75
82
|
}
|
|
83
|
+
// Auto-hide controls after 3 seconds on mobile
|
|
84
|
+
if (window.innerWidth < 1024) {
|
|
85
|
+
controlsTimeout = window.setTimeout(() => {
|
|
86
|
+
showControls.value = false
|
|
87
|
+
}, 3000)
|
|
88
|
+
}
|
|
76
89
|
}
|
|
77
90
|
else {
|
|
78
91
|
if (props.onClose) props.onClose()
|
|
@@ -81,8 +94,20 @@ function setModal(value: boolean) {
|
|
|
81
94
|
document.removeEventListener('keydown', handleKeyboardInput)
|
|
82
95
|
document.removeEventListener('keyup', handleKeyboardRelease)
|
|
83
96
|
}
|
|
97
|
+
// Clear timeout if modal is closed
|
|
98
|
+
if (controlsTimeout) {
|
|
99
|
+
clearTimeout(controlsTimeout)
|
|
100
|
+
controlsTimeout = null
|
|
101
|
+
}
|
|
102
|
+
// Exit fullscreen if active
|
|
103
|
+
if (isFullscreen.value && document.exitFullscreen) {
|
|
104
|
+
document.exitFullscreen().catch(() => {})
|
|
105
|
+
isFullscreen.value = false
|
|
106
|
+
}
|
|
84
107
|
}
|
|
85
108
|
isGalleryOpen.value = value
|
|
109
|
+
showControls.value = true
|
|
110
|
+
infoPanel.value = false
|
|
86
111
|
}
|
|
87
112
|
|
|
88
113
|
function openGalleryImage(index: number | undefined) {
|
|
@@ -103,6 +128,7 @@ function goNextImage() {
|
|
|
103
128
|
else {
|
|
104
129
|
modelValue.value = 0
|
|
105
130
|
}
|
|
131
|
+
resetControlsTimer()
|
|
106
132
|
}
|
|
107
133
|
|
|
108
134
|
function goPrevImage() {
|
|
@@ -114,6 +140,7 @@ function goPrevImage() {
|
|
|
114
140
|
modelValue.value
|
|
115
141
|
= props.images.length - 1 > 0 ? props.images.length - 1 : 0
|
|
116
142
|
}
|
|
143
|
+
resetControlsTimer()
|
|
117
144
|
}
|
|
118
145
|
|
|
119
146
|
const modelValueSrc = computed(() => {
|
|
@@ -122,12 +149,69 @@ const modelValueSrc = computed(() => {
|
|
|
122
149
|
return props.getImageUrl(props.images[modelValue.value])
|
|
123
150
|
})
|
|
124
151
|
|
|
152
|
+
const currentImage = computed(() => {
|
|
153
|
+
if (props.images.length === 0) return null
|
|
154
|
+
return props.images[modelValue.value]
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const imageCount = computed(() => props.images.length)
|
|
158
|
+
const currentIndex = computed(() => modelValue.value + 1)
|
|
159
|
+
|
|
125
160
|
const start = reactive({ x: 0, y: 0 })
|
|
126
161
|
|
|
162
|
+
function resetControlsTimer() {
|
|
163
|
+
// Show controls when user interacts
|
|
164
|
+
showControls.value = true
|
|
165
|
+
|
|
166
|
+
// Only set timer on mobile
|
|
167
|
+
if (window.innerWidth < 1024) {
|
|
168
|
+
if (controlsTimeout) {
|
|
169
|
+
clearTimeout(controlsTimeout)
|
|
170
|
+
}
|
|
171
|
+
controlsTimeout = window.setTimeout(() => {
|
|
172
|
+
showControls.value = false
|
|
173
|
+
}, 3000)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function toggleControls() {
|
|
178
|
+
showControls.value = !showControls.value
|
|
179
|
+
if (showControls.value && window.innerWidth < 1024) {
|
|
180
|
+
resetControlsTimer()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function toggleInfoPanel() {
|
|
185
|
+
infoPanel.value = !infoPanel.value
|
|
186
|
+
resetControlsTimer()
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function toggleFullscreen() {
|
|
190
|
+
if (!isFullscreen.value) {
|
|
191
|
+
const element = document.querySelector('.gallery-container') as HTMLElement
|
|
192
|
+
if (element && element.requestFullscreen) {
|
|
193
|
+
element.requestFullscreen().then(() => {
|
|
194
|
+
isFullscreen.value = true
|
|
195
|
+
}).catch(() => {})
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
if (document.exitFullscreen) {
|
|
200
|
+
document.exitFullscreen().then(() => {
|
|
201
|
+
isFullscreen.value = false
|
|
202
|
+
}).catch(() => {})
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
resetControlsTimer()
|
|
206
|
+
}
|
|
207
|
+
|
|
127
208
|
function touchStart(event: TouchEvent) {
|
|
128
209
|
const touch = event.touches[0]
|
|
129
210
|
const targetElement = touch.target as HTMLElement
|
|
130
211
|
|
|
212
|
+
// Store start time for tap detection
|
|
213
|
+
touchStartTime.value = Date.now()
|
|
214
|
+
|
|
131
215
|
// Check if the touch started on an interactive element
|
|
132
216
|
if (targetElement.closest('button, a, input, textarea, select')) {
|
|
133
217
|
return // Don't handle swipe if interacting with an interactive element
|
|
@@ -136,9 +220,11 @@ function touchStart(event: TouchEvent) {
|
|
|
136
220
|
start.x = touch.screenX
|
|
137
221
|
start.y = touch.screenY
|
|
138
222
|
}
|
|
223
|
+
|
|
139
224
|
function touchEnd(event: TouchEvent) {
|
|
140
225
|
const touch = event.changedTouches[0]
|
|
141
226
|
const targetElement = touch.target as HTMLElement
|
|
227
|
+
const touchDuration = Date.now() - touchStartTime.value
|
|
142
228
|
|
|
143
229
|
// Check if the touch ended on an interactive element
|
|
144
230
|
if (targetElement.closest('button, a, input, textarea, select')) {
|
|
@@ -150,6 +236,12 @@ function touchEnd(event: TouchEvent) {
|
|
|
150
236
|
const diffX = start.x - end.x
|
|
151
237
|
const diffY = start.y - end.y
|
|
152
238
|
|
|
239
|
+
// Detect tap (quick touch with minimal movement)
|
|
240
|
+
if (Math.abs(diffX) < 10 && Math.abs(diffY) < 10 && touchDuration < 300) {
|
|
241
|
+
toggleControls()
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
153
245
|
// Add a threshold to prevent accidental swipes
|
|
154
246
|
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
|
|
155
247
|
if (diffX > 0) {
|
|
@@ -191,6 +283,12 @@ function handleKeyboardInput(event: KeyboardEvent) {
|
|
|
191
283
|
direction.value = 'prev'
|
|
192
284
|
goPrevImage()
|
|
193
285
|
break
|
|
286
|
+
case 'f':
|
|
287
|
+
toggleFullscreen()
|
|
288
|
+
break
|
|
289
|
+
case 'i':
|
|
290
|
+
toggleInfoPanel()
|
|
291
|
+
break
|
|
194
292
|
default:
|
|
195
293
|
break
|
|
196
294
|
}
|
|
@@ -213,6 +311,11 @@ function handleBackdropClick(event: MouseEvent) {
|
|
|
213
311
|
}
|
|
214
312
|
}
|
|
215
313
|
|
|
314
|
+
watch(currentImage, () => {
|
|
315
|
+
// Reset info panel when image changes
|
|
316
|
+
infoPanel.value = false
|
|
317
|
+
})
|
|
318
|
+
|
|
216
319
|
onMounted(() => {
|
|
217
320
|
eventBus.on(`${props.id}GalleryImage`, openGalleryImage)
|
|
218
321
|
eventBus.on(`${props.id}Gallery`, openGalleryImage)
|
|
@@ -228,6 +331,10 @@ onUnmounted(() => {
|
|
|
228
331
|
document.removeEventListener('keyup', handleKeyboardRelease)
|
|
229
332
|
document.body.style.overflow = '' // Ensure body scrolling is restored
|
|
230
333
|
}
|
|
334
|
+
// Clear any remaining timeouts
|
|
335
|
+
if (controlsTimeout) {
|
|
336
|
+
clearTimeout(controlsTimeout)
|
|
337
|
+
}
|
|
231
338
|
})
|
|
232
339
|
</script>
|
|
233
340
|
|
|
@@ -243,43 +350,50 @@ onUnmounted(() => {
|
|
|
243
350
|
>
|
|
244
351
|
<div
|
|
245
352
|
v-if="isGalleryOpen"
|
|
246
|
-
class="fixed bg-fv-neutral-900 text-white inset-0 max-w-[100vw] overflow-
|
|
353
|
+
class="fixed bg-fv-neutral-900 text-white inset-0 max-w-[100vw] overflow-hidden gallery-container"
|
|
247
354
|
style="z-index: 37"
|
|
248
355
|
role="dialog"
|
|
249
356
|
aria-modal="true"
|
|
250
357
|
@click="handleBackdropClick"
|
|
251
358
|
>
|
|
252
359
|
<div
|
|
253
|
-
class="relative w-full max-w-full flex flex-col justify-center items-center"
|
|
360
|
+
class="relative w-full h-full max-w-full flex flex-col justify-center items-center"
|
|
254
361
|
style="z-index: 38"
|
|
255
362
|
@click.stop
|
|
256
363
|
>
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
style="z-index: 39"
|
|
262
|
-
aria-label="Close gallery"
|
|
263
|
-
@click="setModal(false)"
|
|
264
|
-
>
|
|
265
|
-
<component :is="closeIcon" class="w-8 h-8" />
|
|
266
|
-
</button>
|
|
267
|
-
|
|
364
|
+
<!-- Main Content Area -->
|
|
365
|
+
<div class="flex flex-grow gap-4 w-full h-full max-w-full">
|
|
366
|
+
<div class="flex-grow h-full flex items-center relative">
|
|
367
|
+
<!-- Image Display Area -->
|
|
268
368
|
<div
|
|
269
|
-
class="flex h-
|
|
369
|
+
class="flex h-full relative flex-grow items-center justify-center gap-2 z-[1]"
|
|
370
|
+
@touchstart="touchStart"
|
|
371
|
+
@touchend="touchEnd"
|
|
270
372
|
>
|
|
271
|
-
|
|
272
|
-
|
|
373
|
+
<!-- Image Navigation - Left -->
|
|
374
|
+
<transition
|
|
375
|
+
enter-active-class="transition-opacity duration-300"
|
|
376
|
+
enter-from-class="opacity-0"
|
|
377
|
+
enter-to-class="opacity-100"
|
|
378
|
+
leave-active-class="transition-opacity duration-300"
|
|
379
|
+
leave-from-class="opacity-100"
|
|
380
|
+
leave-to-class="opacity-0"
|
|
273
381
|
>
|
|
274
|
-
<
|
|
275
|
-
v-if="images.length > 1"
|
|
276
|
-
class="
|
|
277
|
-
aria-label="Previous image"
|
|
278
|
-
@click="goPrevImage()"
|
|
382
|
+
<div
|
|
383
|
+
v-if="showControls && images.length > 1"
|
|
384
|
+
class="absolute left-0 z-[40] h-full flex items-center px-2 md:px-4"
|
|
279
385
|
>
|
|
280
|
-
<
|
|
281
|
-
|
|
282
|
-
|
|
386
|
+
<button
|
|
387
|
+
class="btn bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 backdrop-blur-sm p-2 rounded-full transition-transform transform hover:scale-110"
|
|
388
|
+
aria-label="Previous image"
|
|
389
|
+
@click="goPrevImage()"
|
|
390
|
+
>
|
|
391
|
+
<ChevronLeftIcon class="w-6 h-6 md:w-8 md:h-8" />
|
|
392
|
+
</button>
|
|
393
|
+
</div>
|
|
394
|
+
</transition>
|
|
395
|
+
|
|
396
|
+
<!-- Main Image Container -->
|
|
283
397
|
<div
|
|
284
398
|
class="flex-1 flex flex-col z-[2] items-center justify-center max-w-full lg:max-w-[calc(100vw - 256px)] relative"
|
|
285
399
|
>
|
|
@@ -288,14 +402,11 @@ onUnmounted(() => {
|
|
|
288
402
|
mode="out-in"
|
|
289
403
|
>
|
|
290
404
|
<div
|
|
291
|
-
v-if="true"
|
|
292
405
|
:key="`image-display-${modelValue}`"
|
|
293
406
|
class="flex-1 w-full max-w-full flex flex-col items-center justify-center absolute inset-0 z-[2]"
|
|
294
407
|
>
|
|
295
408
|
<div
|
|
296
409
|
class="flex-1 w-full max-w-full flex items-center justify-center"
|
|
297
|
-
@touchstart="touchStart"
|
|
298
|
-
@touchend="touchEnd"
|
|
299
410
|
>
|
|
300
411
|
<template
|
|
301
412
|
v-if="videoComponent && isVideo(images[modelValue])"
|
|
@@ -325,42 +436,44 @@ onUnmounted(() => {
|
|
|
325
436
|
/>
|
|
326
437
|
</template>
|
|
327
438
|
</div>
|
|
439
|
+
|
|
440
|
+
<!-- Image Slot Content -->
|
|
328
441
|
<div
|
|
329
|
-
|
|
442
|
+
v-if="infoPanel"
|
|
443
|
+
class="flex-0 px-4 py-3 backdrop-blur-md bg-fv-neutral-900/70 rounded-t-lg flex items-center justify-center max-w-full w-full !z-[45] transition-all"
|
|
330
444
|
>
|
|
331
445
|
<slot :value="images[modelValue]" />
|
|
332
446
|
</div>
|
|
333
447
|
</div>
|
|
334
448
|
</transition>
|
|
335
449
|
</div>
|
|
336
|
-
|
|
337
|
-
|
|
450
|
+
|
|
451
|
+
<!-- Image Navigation - Right -->
|
|
452
|
+
<transition
|
|
453
|
+
enter-active-class="transition-opacity duration-300"
|
|
454
|
+
enter-from-class="opacity-0"
|
|
455
|
+
enter-to-class="opacity-100"
|
|
456
|
+
leave-active-class="transition-opacity duration-300"
|
|
457
|
+
leave-from-class="opacity-100"
|
|
458
|
+
leave-to-class="opacity-0"
|
|
338
459
|
>
|
|
339
|
-
<
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
'-right-4': sidePanel,
|
|
343
|
-
'right-2': !sidePanel,
|
|
344
|
-
}"
|
|
345
|
-
style="z-index: 39"
|
|
346
|
-
:aria-label="sidePanel ? 'Hide thumbnails' : 'Show thumbnails'"
|
|
347
|
-
@click="() => (sidePanel = !sidePanel)"
|
|
348
|
-
>
|
|
349
|
-
<ChevronDoubleRightIcon v-if="sidePanel" class="w-7 h-7" />
|
|
350
|
-
<ChevronDoubleLeftIcon v-else class="w-7 h-7" />
|
|
351
|
-
</button>
|
|
352
|
-
<button
|
|
353
|
-
v-if="images.length > 1"
|
|
354
|
-
class="btn p-1 rounded-full"
|
|
355
|
-
aria-label="Next image"
|
|
356
|
-
@click="goNextImage()"
|
|
460
|
+
<div
|
|
461
|
+
v-if="showControls && images.length > 1"
|
|
462
|
+
class="absolute right-0 z-[40] h-full flex items-center px-2 md:px-4"
|
|
357
463
|
>
|
|
358
|
-
<
|
|
359
|
-
|
|
360
|
-
|
|
464
|
+
<button
|
|
465
|
+
class="btn bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 backdrop-blur-sm p-2 rounded-full transition-transform transform hover:scale-110"
|
|
466
|
+
aria-label="Next image"
|
|
467
|
+
@click="goNextImage()"
|
|
468
|
+
>
|
|
469
|
+
<ChevronRightIcon class="w-6 h-6 md:w-8 md:h-8" />
|
|
470
|
+
</button>
|
|
471
|
+
</div>
|
|
472
|
+
</transition>
|
|
361
473
|
</div>
|
|
362
474
|
</div>
|
|
363
475
|
|
|
476
|
+
<!-- Side Panel for Thumbnails -->
|
|
364
477
|
<transition
|
|
365
478
|
enter-active-class="transform transition ease-in-out duration-300"
|
|
366
479
|
enter-from-class="translate-x-full"
|
|
@@ -371,27 +484,33 @@ onUnmounted(() => {
|
|
|
371
484
|
>
|
|
372
485
|
<div
|
|
373
486
|
v-if="sidePanel"
|
|
374
|
-
class="hidden lg:block flex-shrink-0 w-64 bg-fv-neutral-800 h-
|
|
487
|
+
class="hidden lg:block flex-shrink-0 w-64 bg-fv-neutral-800/90 backdrop-blur-md h-full max-h-full overflow-y-auto"
|
|
375
488
|
>
|
|
376
|
-
<!--
|
|
377
|
-
<div v-if="paging" class="flex items-center justify-center">
|
|
489
|
+
<!-- Paging Controls -->
|
|
490
|
+
<div v-if="paging" class="flex items-center justify-center pt-2">
|
|
378
491
|
<DefaultPaging :id="id" :items="paging" />
|
|
379
492
|
</div>
|
|
493
|
+
|
|
494
|
+
<!-- Thumbnail Grid -->
|
|
380
495
|
<div class="grid grid-cols-2 gap-2 p-2">
|
|
381
496
|
<div
|
|
382
497
|
v-for="i in images.length"
|
|
383
498
|
:key="`bg_${id}_${i}`"
|
|
384
|
-
class="
|
|
385
|
-
:style="{
|
|
386
|
-
filter:
|
|
387
|
-
i - 1 === modelValue ? 'brightness(1)' : 'brightness(0.5)',
|
|
388
|
-
}"
|
|
499
|
+
class="group relative"
|
|
389
500
|
>
|
|
501
|
+
<div
|
|
502
|
+
class="absolute inset-0 rounded-lg transition-colors duration-300 group-hover:bg-fv-neutral-700/40"
|
|
503
|
+
:class="{ 'bg-fv-primary-500/40': i - 1 === modelValue }"
|
|
504
|
+
/>
|
|
390
505
|
<img
|
|
391
506
|
v-if="imageComponent === 'img'"
|
|
392
|
-
:class="`h-auto max-w-full rounded-lg cursor-pointer shadow
|
|
507
|
+
:class="`h-auto max-w-full rounded-lg cursor-pointer shadow transition-all duration-300 group-hover:brightness-110 ${getBorderColor(
|
|
393
508
|
images[i - 1],
|
|
394
509
|
)}`"
|
|
510
|
+
:style="{
|
|
511
|
+
filter:
|
|
512
|
+
i - 1 === modelValue ? 'brightness(1)' : 'brightness(0.7)',
|
|
513
|
+
}"
|
|
395
514
|
:src="getThumbnailUrl(images[i - 1])"
|
|
396
515
|
:alt="`Thumbnail ${i}`"
|
|
397
516
|
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
@@ -402,9 +521,17 @@ onUnmounted(() => {
|
|
|
402
521
|
:image="getThumbnailUrl(images[i - 1]).image"
|
|
403
522
|
:variant="getThumbnailUrl(images[i - 1]).variant"
|
|
404
523
|
:alt="getThumbnailUrl(images[i - 1]).alt"
|
|
405
|
-
:class="`h-auto max-w-full rounded-lg cursor-pointer shadow ${getBorderColor(
|
|
524
|
+
:class="`h-auto max-w-full rounded-lg cursor-pointer shadow transition-all duration-300 group-hover:brightness-110 ${getBorderColor(
|
|
406
525
|
images[i - 1],
|
|
407
526
|
)}`"
|
|
527
|
+
:style="{
|
|
528
|
+
filter:
|
|
529
|
+
i - 1 === modelValue ? 'brightness(1)' : 'brightness(0.7)',
|
|
530
|
+
}"
|
|
531
|
+
:likes="getThumbnailUrl(images[i - 1]).likes"
|
|
532
|
+
:show-likes="getThumbnailUrl(images[i - 1]).showLikes"
|
|
533
|
+
:is-author="getThumbnailUrl(images[i - 1]).isAuthor"
|
|
534
|
+
:user-uuid="getThumbnailUrl(images[i - 1]).userUUID"
|
|
408
535
|
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
409
536
|
/>
|
|
410
537
|
</div>
|
|
@@ -412,18 +539,121 @@ onUnmounted(() => {
|
|
|
412
539
|
</div>
|
|
413
540
|
</transition>
|
|
414
541
|
</div>
|
|
542
|
+
|
|
543
|
+
<!-- Top Controls -->
|
|
544
|
+
<transition
|
|
545
|
+
enter-active-class="transition-opacity duration-300"
|
|
546
|
+
enter-from-class="opacity-0"
|
|
547
|
+
enter-to-class="opacity-100"
|
|
548
|
+
leave-active-class="transition-opacity duration-300"
|
|
549
|
+
leave-from-class="opacity-100"
|
|
550
|
+
leave-to-class="opacity-0"
|
|
551
|
+
>
|
|
552
|
+
<div
|
|
553
|
+
v-if="showControls"
|
|
554
|
+
class="absolute top-0 left-0 right-0 px-4 py-3 flex justify-between items-center bg-gradient-to-b from-fv-neutral-900/90 to-transparent backdrop-blur-sm z-[50] transition-opacity"
|
|
555
|
+
>
|
|
556
|
+
<!-- Title and Counter -->
|
|
557
|
+
<div class="flex items-center space-x-2">
|
|
558
|
+
<span v-if="title" class="font-medium text-lg">{{ title }}</span>
|
|
559
|
+
<span class="text-sm opacity-80">{{ currentIndex }} / {{ imageCount }}</span>
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
<!-- Control Buttons -->
|
|
563
|
+
<div class="flex items-center space-x-2">
|
|
564
|
+
<button
|
|
565
|
+
class="btn p-1.5 rounded-full bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
|
|
566
|
+
title="Toggle info"
|
|
567
|
+
@click="toggleInfoPanel"
|
|
568
|
+
>
|
|
569
|
+
<InformationCircleIcon class="w-5 h-5" />
|
|
570
|
+
</button>
|
|
571
|
+
|
|
572
|
+
<button
|
|
573
|
+
class="btn p-1.5 rounded-full lg:hidden bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
|
|
574
|
+
:title="sidePanel ? 'Hide thumbnails' : 'Show thumbnails'"
|
|
575
|
+
@click="() => (sidePanel = !sidePanel)"
|
|
576
|
+
>
|
|
577
|
+
<ChevronDoubleRightIcon v-if="sidePanel" class="w-5 h-5" />
|
|
578
|
+
<ChevronDoubleLeftIcon v-else class="w-5 h-5" />
|
|
579
|
+
</button>
|
|
580
|
+
|
|
581
|
+
<button
|
|
582
|
+
class="btn p-1.5 rounded-full hidden lg:block bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
|
|
583
|
+
:title="sidePanel ? 'Hide thumbnails' : 'Show thumbnails'"
|
|
584
|
+
@click="() => (sidePanel = !sidePanel)"
|
|
585
|
+
>
|
|
586
|
+
<ChevronDoubleRightIcon v-if="sidePanel" class="w-5 h-5" />
|
|
587
|
+
<ChevronDoubleLeftIcon v-else class="w-5 h-5" />
|
|
588
|
+
</button>
|
|
589
|
+
|
|
590
|
+
<button
|
|
591
|
+
class="btn p-1.5 rounded-full bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
|
|
592
|
+
aria-label="Close gallery"
|
|
593
|
+
@click="setModal(false)"
|
|
594
|
+
>
|
|
595
|
+
<component :is="closeIcon" class="w-5 h-5" />
|
|
596
|
+
</button>
|
|
597
|
+
</div>
|
|
598
|
+
</div>
|
|
599
|
+
</transition>
|
|
600
|
+
|
|
601
|
+
<!-- Mobile Thumbnail Preview -->
|
|
602
|
+
<transition
|
|
603
|
+
enter-active-class="transition-transform duration-300 ease-out"
|
|
604
|
+
enter-from-class="translate-y-full"
|
|
605
|
+
enter-to-class="translate-y-0"
|
|
606
|
+
leave-active-class="transition-transform duration-300 ease-in"
|
|
607
|
+
leave-from-class="translate-y-0"
|
|
608
|
+
leave-to-class="translate-y-full"
|
|
609
|
+
>
|
|
610
|
+
<div
|
|
611
|
+
v-if="showControls && images.length > 1 && !sidePanel"
|
|
612
|
+
class="absolute bottom-0 left-0 right-0 p-2 lg:hidden bg-gradient-to-t from-fv-neutral-900/90 to-transparent backdrop-blur-sm z-[50]"
|
|
613
|
+
>
|
|
614
|
+
<div class="overflow-x-auto flex space-x-2 pb-1 px-1">
|
|
615
|
+
<div
|
|
616
|
+
v-for="(image, idx) in images"
|
|
617
|
+
:key="`mobile_thumb_${id}_${idx}`"
|
|
618
|
+
class="flex-shrink-0 w-16 h-16 rounded-lg relative cursor-pointer"
|
|
619
|
+
:class="{ 'ring-2 ring-fv-primary-500 ring-offset-1 ring-offset-fv-neutral-900': idx === modelValue }"
|
|
620
|
+
@click="$eventBus.emit(`${id}GalleryImage`, idx)"
|
|
621
|
+
>
|
|
622
|
+
<img
|
|
623
|
+
v-if="imageComponent === 'img'"
|
|
624
|
+
class="w-full h-full object-cover rounded-lg transition duration-200"
|
|
625
|
+
:style="{
|
|
626
|
+
filter: idx === modelValue ? 'brightness(1)' : 'brightness(0.7)',
|
|
627
|
+
}"
|
|
628
|
+
:src="getThumbnailUrl(image)"
|
|
629
|
+
:alt="`Thumbnail ${idx + 1}`"
|
|
630
|
+
>
|
|
631
|
+
<component
|
|
632
|
+
:is="imageComponent"
|
|
633
|
+
v-else
|
|
634
|
+
:image="getThumbnailUrl(image).image"
|
|
635
|
+
:variant="getThumbnailUrl(image).variant"
|
|
636
|
+
:alt="getThumbnailUrl(image).alt"
|
|
637
|
+
class="w-full h-full object-cover rounded-lg transition duration-200"
|
|
638
|
+
:style="{
|
|
639
|
+
filter: idx === modelValue ? 'brightness(1)' : 'brightness(0.7)',
|
|
640
|
+
}"
|
|
641
|
+
/>
|
|
642
|
+
</div>
|
|
643
|
+
</div>
|
|
644
|
+
</div>
|
|
645
|
+
</transition>
|
|
415
646
|
</div>
|
|
416
647
|
</div>
|
|
417
648
|
</transition>
|
|
418
649
|
|
|
419
|
-
|
|
650
|
+
<!-- Thumbnail Grid/Mason/Custom Layouts -->
|
|
651
|
+
<div v-if="mode === 'grid' || mode === 'mason' || mode === 'custom'" class="gallery-grid">
|
|
420
652
|
<div
|
|
421
653
|
:class="{
|
|
422
|
-
'
|
|
423
|
-
|
|
424
|
-
'
|
|
425
|
-
mode === 'grid',
|
|
426
|
-
' custom-grid': mode === 'custom',
|
|
654
|
+
'masonry-grid': mode === 'mason',
|
|
655
|
+
'standard-grid': mode === 'grid',
|
|
656
|
+
'custom-grid': mode === 'custom',
|
|
427
657
|
}"
|
|
428
658
|
>
|
|
429
659
|
<slot name="thumbnail" />
|
|
@@ -431,17 +661,17 @@ onUnmounted(() => {
|
|
|
431
661
|
<template v-if="mode === 'mason'">
|
|
432
662
|
<div
|
|
433
663
|
v-if="i + (1 % gridHeight) === 0"
|
|
434
|
-
class="
|
|
664
|
+
class="masonry-column relative"
|
|
435
665
|
>
|
|
436
666
|
<div v-if="ranking" class="img-gallery-ranking">
|
|
437
667
|
{{ i }}
|
|
438
668
|
</div>
|
|
439
669
|
|
|
440
670
|
<template v-for="j in gridHeight" :key="`gi_${id}_${i + j}`">
|
|
441
|
-
<div>
|
|
671
|
+
<div class="masonry-item">
|
|
442
672
|
<img
|
|
443
673
|
v-if="i + j - 2 < images.length && imageComponent === 'img'"
|
|
444
|
-
class="h-auto max-w-full rounded-lg cursor-pointer"
|
|
674
|
+
class="h-auto max-w-full w-full rounded-lg cursor-pointer shadow-md hover:shadow-xl transition-all duration-300 hover:brightness-110 hover:scale-[1.02]"
|
|
445
675
|
:src="getThumbnailUrl(images[i + j - 2])"
|
|
446
676
|
:alt="`Gallery image ${i + j - 1}`"
|
|
447
677
|
@click="$eventBus.emit(`${id}GalleryImage`, i + j - 2)"
|
|
@@ -452,7 +682,7 @@ onUnmounted(() => {
|
|
|
452
682
|
:image="getThumbnailUrl(images[i + j - 2]).image"
|
|
453
683
|
:variant="getThumbnailUrl(images[i + j - 2]).variant"
|
|
454
684
|
:alt="getThumbnailUrl(images[i + j - 2]).alt"
|
|
455
|
-
:class="`h-auto max-w-full rounded-lg cursor-pointer ${getBorderColor(
|
|
685
|
+
:class="`h-auto max-w-full w-full rounded-lg cursor-pointer shadow-md hover:shadow-xl transition-all duration-300 hover:brightness-110 hover:scale-[1.02] ${getBorderColor(
|
|
456
686
|
images[i + j - 2],
|
|
457
687
|
)}`"
|
|
458
688
|
:likes="getThumbnailUrl(images[i + j - 2]).likes"
|
|
@@ -465,42 +695,47 @@ onUnmounted(() => {
|
|
|
465
695
|
</template>
|
|
466
696
|
</div>
|
|
467
697
|
</template>
|
|
468
|
-
<div v-else class="relative">
|
|
698
|
+
<div v-else class="grid-item relative group">
|
|
469
699
|
<div v-if="ranking" class="img-gallery-ranking">
|
|
470
700
|
{{ i }}
|
|
471
701
|
</div>
|
|
472
|
-
<
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
702
|
+
<div class="overflow-hidden rounded-lg">
|
|
703
|
+
<img
|
|
704
|
+
v-if="imageComponent === 'img'"
|
|
705
|
+
class="h-auto max-w-full w-full rounded-lg cursor-pointer shadow-md transition-all duration-300 group-hover:brightness-110 group-hover:scale-[1.03]"
|
|
706
|
+
:src="getThumbnailUrl(images[i - 1])"
|
|
707
|
+
:alt="`Gallery image ${i}`"
|
|
708
|
+
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
709
|
+
>
|
|
710
|
+
<component
|
|
711
|
+
:is="imageComponent"
|
|
712
|
+
v-else-if="imageComponent"
|
|
713
|
+
:image="getThumbnailUrl(images[i - 1]).image"
|
|
714
|
+
:variant="getThumbnailUrl(images[i - 1]).variant"
|
|
715
|
+
:alt="getThumbnailUrl(images[i - 1]).alt"
|
|
716
|
+
:class="`h-auto max-w-full w-full rounded-lg cursor-pointer shadow-md transition-all duration-300 group-hover:brightness-110 group-hover:scale-[1.03] ${getBorderColor(
|
|
717
|
+
images[i - 1],
|
|
718
|
+
)}`"
|
|
719
|
+
:likes="getThumbnailUrl(images[i - 1]).likes"
|
|
720
|
+
:show-likes="getThumbnailUrl(images[i - 1]).showLikes"
|
|
721
|
+
:is-author="getThumbnailUrl(images[i - 1]).isAuthor"
|
|
722
|
+
:user-uuid="getThumbnailUrl(images[i - 1]).userUUID"
|
|
723
|
+
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
724
|
+
/>
|
|
725
|
+
</div>
|
|
494
726
|
</div>
|
|
495
727
|
</template>
|
|
496
728
|
</div>
|
|
497
729
|
</div>
|
|
730
|
+
|
|
731
|
+
<!-- Button Mode -->
|
|
498
732
|
<button
|
|
499
733
|
v-if="mode === 'button'"
|
|
500
|
-
:class="`btn ${buttonType ? buttonType : 'primary'} defaults`"
|
|
734
|
+
:class="`btn ${buttonType ? buttonType : 'primary'} defaults relative overflow-hidden group`"
|
|
501
735
|
@click="openGalleryImage(0)"
|
|
502
736
|
>
|
|
503
|
-
{{ buttonText ? buttonText : $t("open_gallery_cta") }}
|
|
737
|
+
<span class="relative z-10">{{ buttonText ? buttonText : $t("open_gallery_cta") }}</span>
|
|
738
|
+
<span class="absolute inset-0 bg-white/10 transform -translate-x-full group-hover:translate-x-0 transition-transform duration-300" />
|
|
504
739
|
</button>
|
|
505
740
|
</div>
|
|
506
741
|
</template>
|
|
@@ -572,18 +807,101 @@ onUnmounted(() => {
|
|
|
572
807
|
filter: blur(10px);
|
|
573
808
|
}
|
|
574
809
|
|
|
575
|
-
/*
|
|
576
|
-
.
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
810
|
+
/* Modern grids */
|
|
811
|
+
.gallery-grid {
|
|
812
|
+
min-height: 200px;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.standard-grid {
|
|
816
|
+
display: grid;
|
|
817
|
+
grid-template-columns: repeat(1, 1fr);
|
|
818
|
+
gap: 0.75rem;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
@media (min-width: 480px) {
|
|
822
|
+
.standard-grid {
|
|
823
|
+
grid-template-columns: repeat(2, 1fr);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
@media (min-width: 768px) {
|
|
828
|
+
.standard-grid {
|
|
829
|
+
grid-template-columns: repeat(3, 1fr);
|
|
830
|
+
gap: 1rem;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
@media (min-width: 1024px) {
|
|
835
|
+
.standard-grid {
|
|
836
|
+
grid-template-columns: repeat(4, 1fr);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
@media (min-width: 1280px) {
|
|
841
|
+
.standard-grid {
|
|
842
|
+
grid-template-columns: repeat(5, 1fr);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
@media (min-width: 1536px) {
|
|
847
|
+
.standard-grid {
|
|
848
|
+
grid-template-columns: repeat(6, 1fr);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
.masonry-grid {
|
|
853
|
+
display: grid;
|
|
854
|
+
grid-template-columns: repeat(1, 1fr);
|
|
855
|
+
gap: 0.75rem;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
@media (min-width: 480px) {
|
|
859
|
+
.masonry-grid {
|
|
860
|
+
grid-template-columns: repeat(2, 1fr);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
@media (min-width: 768px) {
|
|
865
|
+
.masonry-grid {
|
|
866
|
+
grid-template-columns: repeat(3, 1fr);
|
|
867
|
+
gap: 1rem;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
@media (min-width: 1024px) {
|
|
872
|
+
.masonry-grid {
|
|
873
|
+
grid-template-columns: repeat(4, 1fr);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
.masonry-column {
|
|
878
|
+
display: grid;
|
|
879
|
+
gap: 0.75rem;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
.masonry-item {
|
|
883
|
+
break-inside: avoid;
|
|
884
|
+
margin-bottom: 0.75rem;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
.grid-item {
|
|
888
|
+
break-inside: avoid;
|
|
889
|
+
margin-bottom: 0.75rem;
|
|
580
890
|
}
|
|
581
891
|
|
|
582
|
-
.
|
|
892
|
+
.img-gallery-ranking {
|
|
583
893
|
position: absolute;
|
|
584
|
-
top: 0;
|
|
585
|
-
left: 0;
|
|
586
|
-
|
|
587
|
-
|
|
894
|
+
top: 0.5rem;
|
|
895
|
+
left: 0.5rem;
|
|
896
|
+
background-color: rgba(0, 0, 0, 0.6);
|
|
897
|
+
color: white;
|
|
898
|
+
width: 1.5rem;
|
|
899
|
+
height: 1.5rem;
|
|
900
|
+
display: flex;
|
|
901
|
+
align-items: center;
|
|
902
|
+
justify-content: center;
|
|
903
|
+
border-radius: 9999px;
|
|
904
|
+
font-size: 0.75rem;
|
|
905
|
+
z-index: 10;
|
|
588
906
|
}
|
|
589
907
|
</style>
|