@fy-/fws-vue 2.3.13 → 2.3.14
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 +437 -393
- package/components/ui/DefaultGalleryOld.vue +1073 -0
- package/package.json +1 -1
|
@@ -9,35 +9,78 @@ import {
|
|
|
9
9
|
InformationCircleIcon,
|
|
10
10
|
XMarkIcon,
|
|
11
11
|
} from '@heroicons/vue/24/solid'
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
useDebounceFn,
|
|
14
|
+
useElementSize,
|
|
15
|
+
useEventListener,
|
|
16
|
+
useFullscreen,
|
|
17
|
+
useResizeObserver,
|
|
18
|
+
useWindowSize,
|
|
19
|
+
} from '@vueuse/core'
|
|
13
20
|
import { computed, h, nextTick, onMounted, onUnmounted, reactive, ref, shallowRef, watch } from 'vue'
|
|
14
21
|
import { useEventBus } from '../../composables/event-bus'
|
|
22
|
+
import { ClientOnly } from '../ssr/ClientOnly'
|
|
15
23
|
import DefaultPaging from './DefaultPaging.vue'
|
|
16
24
|
|
|
25
|
+
// Core state
|
|
17
26
|
const isGalleryOpen = ref<boolean>(false)
|
|
18
27
|
const eventBus = useEventBus()
|
|
19
28
|
const sidePanel = ref<boolean>(true)
|
|
20
29
|
const showControls = ref<boolean>(true)
|
|
21
30
|
const isFullscreen = ref<boolean>(false)
|
|
22
31
|
const infoPanel = ref<boolean>(true) // Show info panel by default
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
const direction = ref<'next' | 'prev'>('next')
|
|
33
|
+
|
|
34
|
+
// Refs to track DOM elements and their sizes
|
|
35
|
+
const galleryRef = shallowRef<HTMLElement | null>(null)
|
|
36
|
+
const galleryContentRef = shallowRef<HTMLElement | null>(null)
|
|
37
|
+
const imageContainerRef = shallowRef<HTMLElement | null>(null)
|
|
38
|
+
const infoPanelRef = shallowRef<HTMLElement | null>(null)
|
|
39
|
+
const sidePanelRef = shallowRef<HTMLElement | null>(null)
|
|
40
|
+
const topControlsRef = shallowRef<HTMLElement | null>(null)
|
|
41
|
+
|
|
42
|
+
// Use VueUse's useElementSize for reliable sizing
|
|
43
|
+
const { width: galleryWidth, height: galleryHeight } = useElementSize(galleryRef)
|
|
44
|
+
const { width: windowWidth, height: windowHeight } = useWindowSize()
|
|
45
|
+
const { height: topControlsHeight } = useElementSize(topControlsRef)
|
|
46
|
+
const { height: infoPanelHeight } = useElementSize(infoPanelRef)
|
|
47
|
+
|
|
48
|
+
// Derived measurements
|
|
49
|
+
const availableHeight = computed(() => {
|
|
50
|
+
let height = isFullscreen.value
|
|
51
|
+
? windowHeight.value * 0.95 // 95% of viewport in fullscreen
|
|
52
|
+
: windowHeight.value * 0.85 // 85% of viewport in normal mode
|
|
53
|
+
|
|
54
|
+
// Subtract top controls
|
|
55
|
+
height -= topControlsHeight.value
|
|
56
|
+
|
|
57
|
+
// Subtract info panel if visible
|
|
58
|
+
if (infoPanel.value && infoPanelHeight.value > 0) {
|
|
59
|
+
height -= infoPanelHeight.value
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Convert to rem for consistent sizing
|
|
63
|
+
return `${height / 16}rem`
|
|
64
|
+
})
|
|
26
65
|
|
|
27
66
|
// Use VueUse's useFullscreen for better fullscreen handling
|
|
28
|
-
const { isFullscreen: isElementFullscreen, enter: enterFullscreen, exit: exitFullscreen } = useFullscreen(
|
|
67
|
+
const { isFullscreen: isElementFullscreen, enter: enterFullscreen, exit: exitFullscreen } = useFullscreen(galleryRef)
|
|
29
68
|
|
|
30
69
|
// Track when fullscreen changes externally (like Escape key)
|
|
31
70
|
watch(isElementFullscreen, (newValue) => {
|
|
32
71
|
isFullscreen.value = newValue
|
|
33
|
-
if (newValue) {
|
|
34
|
-
// Force update of image size when entering fullscreen
|
|
35
|
-
nextTick(() => {
|
|
36
|
-
updateImageSizes()
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
72
|
})
|
|
40
73
|
|
|
74
|
+
// Touch handling state
|
|
75
|
+
const touchStartTime = ref<number>(0)
|
|
76
|
+
const start = reactive({ x: 0, y: 0 })
|
|
77
|
+
const isKeyPressed = ref<boolean>(false)
|
|
78
|
+
|
|
79
|
+
// Timers for automatic control hiding
|
|
80
|
+
let controlsTimeout: number | null = null
|
|
81
|
+
let fullscreenResizeTimeout: number | null = null
|
|
82
|
+
|
|
83
|
+
// Props definition with defaults
|
|
41
84
|
const props = withDefaults(
|
|
42
85
|
defineProps<{
|
|
43
86
|
id: string
|
|
@@ -77,7 +120,10 @@ const props = withDefaults(
|
|
|
77
120
|
},
|
|
78
121
|
)
|
|
79
122
|
|
|
123
|
+
// Emits
|
|
80
124
|
const emit = defineEmits(['update:modelValue'])
|
|
125
|
+
|
|
126
|
+
// Two-way binding for model value
|
|
81
127
|
const modelValue = computed({
|
|
82
128
|
get: () => props.modelValue,
|
|
83
129
|
set: (i) => {
|
|
@@ -85,44 +131,61 @@ const modelValue = computed({
|
|
|
85
131
|
},
|
|
86
132
|
})
|
|
87
133
|
|
|
88
|
-
|
|
134
|
+
// Computed values
|
|
135
|
+
const modelValueSrc = computed(() => {
|
|
136
|
+
if (props.images.length === 0) return false
|
|
137
|
+
if (props.images[modelValue.value] === undefined) return false
|
|
138
|
+
return props.getImageUrl(props.images[modelValue.value])
|
|
139
|
+
})
|
|
89
140
|
|
|
90
|
-
|
|
91
|
-
|
|
141
|
+
const currentImage = computed(() => {
|
|
142
|
+
if (props.images.length === 0) return null
|
|
143
|
+
return props.images[modelValue.value]
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const imageCount = computed(() => props.images.length)
|
|
147
|
+
const currentIndex = computed(() => modelValue.value + 1)
|
|
148
|
+
|
|
149
|
+
// Image size and positioning
|
|
150
|
+
const calculateImageSize = useDebounceFn(() => {
|
|
151
|
+
if (!imageContainerRef.value) return
|
|
92
152
|
|
|
93
|
-
// Used to maintain consistent image sizes
|
|
94
|
-
function updateImageSizes() {
|
|
95
153
|
nextTick(() => {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
img.style.maxHeight = `calc(${viewHeight}vh - ${infoHeight}rem - ${topHeight}rem) !important`
|
|
111
|
-
})
|
|
112
|
-
}
|
|
154
|
+
const imageElements = imageContainerRef.value?.querySelectorAll('.image-display img, .image-display .video-component') as NodeListOf<HTMLElement>
|
|
155
|
+
|
|
156
|
+
if (!imageElements || imageElements.length === 0) return
|
|
157
|
+
|
|
158
|
+
imageElements.forEach((img) => {
|
|
159
|
+
// Reset to ensure proper recalculation
|
|
160
|
+
img.style.maxHeight = ''
|
|
161
|
+
// Force browser to recalculate styles
|
|
162
|
+
void img.offsetHeight
|
|
163
|
+
|
|
164
|
+
// Set proper height
|
|
165
|
+
img.style.maxHeight = availableHeight.value
|
|
166
|
+
img.style.maxWidth = sidePanel.value ? 'calc(100vw - 16rem)' : '100vw'
|
|
167
|
+
})
|
|
113
168
|
})
|
|
114
|
-
}
|
|
169
|
+
}, 50)
|
|
170
|
+
|
|
171
|
+
// Update all layout measurements
|
|
172
|
+
const updateLayout = useDebounceFn(() => {
|
|
173
|
+
calculateImageSize()
|
|
174
|
+
}, 50)
|
|
115
175
|
|
|
176
|
+
// Modal controls
|
|
116
177
|
function setModal(value: boolean) {
|
|
117
178
|
if (value === true) {
|
|
118
179
|
if (props.onOpen) props.onOpen()
|
|
119
180
|
document.body.style.overflow = 'hidden' // Prevent scrolling when gallery is open
|
|
181
|
+
|
|
120
182
|
if (!import.meta.env.SSR) {
|
|
121
183
|
useEventListener(document, 'keydown', handleKeyboardInput)
|
|
122
184
|
useEventListener(document, 'keyup', handleKeyboardRelease)
|
|
123
185
|
}
|
|
186
|
+
|
|
124
187
|
// Auto-hide controls after 3 seconds on mobile
|
|
125
|
-
if (
|
|
188
|
+
if (windowWidth.value < 1024) {
|
|
126
189
|
controlsTimeout = window.setTimeout(() => {
|
|
127
190
|
showControls.value = false
|
|
128
191
|
}, 3000)
|
|
@@ -131,11 +194,13 @@ function setModal(value: boolean) {
|
|
|
131
194
|
else {
|
|
132
195
|
if (props.onClose) props.onClose()
|
|
133
196
|
document.body.style.overflow = '' // Restore scrolling
|
|
197
|
+
|
|
134
198
|
// Exit fullscreen if active
|
|
135
199
|
if (isFullscreen.value) {
|
|
136
200
|
exitFullscreen()
|
|
137
201
|
isFullscreen.value = false
|
|
138
202
|
}
|
|
203
|
+
|
|
139
204
|
// Clear timeout if modal is closed
|
|
140
205
|
if (controlsTimeout) {
|
|
141
206
|
clearTimeout(controlsTimeout)
|
|
@@ -147,6 +212,7 @@ function setModal(value: boolean) {
|
|
|
147
212
|
// Don't reset info panel state when opening/closing
|
|
148
213
|
}
|
|
149
214
|
|
|
215
|
+
// Open gallery with debounce to prevent accidental double-clicks
|
|
150
216
|
const openGalleryImage = useDebounceFn((index: number | undefined) => {
|
|
151
217
|
if (index === undefined) {
|
|
152
218
|
modelValue.value = 0
|
|
@@ -155,8 +221,14 @@ const openGalleryImage = useDebounceFn((index: number | undefined) => {
|
|
|
155
221
|
modelValue.value = Number.parseInt(index.toString())
|
|
156
222
|
}
|
|
157
223
|
setModal(true)
|
|
158
|
-
}, 50) // Debounce to prevent accidental double-opens
|
|
159
224
|
|
|
225
|
+
// Update layout after opening
|
|
226
|
+
nextTick(() => {
|
|
227
|
+
updateLayout()
|
|
228
|
+
})
|
|
229
|
+
}, 50)
|
|
230
|
+
|
|
231
|
+
// Navigation functions
|
|
160
232
|
function goNextImage() {
|
|
161
233
|
direction.value = 'next'
|
|
162
234
|
if (modelValue.value < props.images.length - 1) {
|
|
@@ -174,34 +246,18 @@ function goPrevImage() {
|
|
|
174
246
|
modelValue.value--
|
|
175
247
|
}
|
|
176
248
|
else {
|
|
177
|
-
modelValue.value
|
|
178
|
-
= props.images.length - 1 > 0 ? props.images.length - 1 : 0
|
|
249
|
+
modelValue.value = props.images.length - 1 > 0 ? props.images.length - 1 : 0
|
|
179
250
|
}
|
|
180
251
|
resetControlsTimer()
|
|
181
252
|
}
|
|
182
253
|
|
|
183
|
-
|
|
184
|
-
if (props.images.length === 0) return false
|
|
185
|
-
if (props.images[modelValue.value] === undefined) return false
|
|
186
|
-
return props.getImageUrl(props.images[modelValue.value])
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
const currentImage = computed(() => {
|
|
190
|
-
if (props.images.length === 0) return null
|
|
191
|
-
return props.images[modelValue.value]
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
const imageCount = computed(() => props.images.length)
|
|
195
|
-
const currentIndex = computed(() => modelValue.value + 1)
|
|
196
|
-
|
|
197
|
-
const start = reactive({ x: 0, y: 0 })
|
|
198
|
-
|
|
254
|
+
// UI control functions
|
|
199
255
|
function resetControlsTimer() {
|
|
200
256
|
// Show controls when user interacts
|
|
201
257
|
showControls.value = true
|
|
202
258
|
|
|
203
259
|
// Only set timer on mobile
|
|
204
|
-
if (
|
|
260
|
+
if (windowWidth.value < 1024) {
|
|
205
261
|
if (controlsTimeout) {
|
|
206
262
|
clearTimeout(controlsTimeout)
|
|
207
263
|
}
|
|
@@ -213,7 +269,7 @@ function resetControlsTimer() {
|
|
|
213
269
|
|
|
214
270
|
function toggleControls() {
|
|
215
271
|
showControls.value = !showControls.value
|
|
216
|
-
if (showControls.value &&
|
|
272
|
+
if (showControls.value && windowWidth.value < 1024) {
|
|
217
273
|
resetControlsTimer()
|
|
218
274
|
}
|
|
219
275
|
}
|
|
@@ -222,28 +278,32 @@ function toggleInfoPanel() {
|
|
|
222
278
|
infoPanel.value = !infoPanel.value
|
|
223
279
|
resetControlsTimer()
|
|
224
280
|
|
|
225
|
-
// Update
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
281
|
+
// Update layout after panel toggle
|
|
282
|
+
nextTick(() => {
|
|
283
|
+
updateLayout()
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function toggleSidePanel() {
|
|
288
|
+
sidePanel.value = !sidePanel.value
|
|
289
|
+
resetControlsTimer()
|
|
290
|
+
|
|
291
|
+
// Update layout after panel toggle
|
|
292
|
+
nextTick(() => {
|
|
293
|
+
updateLayout()
|
|
294
|
+
})
|
|
235
295
|
}
|
|
236
296
|
|
|
237
297
|
function toggleFullscreen() {
|
|
238
298
|
if (!isFullscreen.value) {
|
|
239
|
-
if (
|
|
299
|
+
if (galleryRef.value) {
|
|
240
300
|
enterFullscreen()
|
|
241
301
|
.then(() => {
|
|
242
302
|
isFullscreen.value = true
|
|
243
303
|
// Give browser time to adjust fullscreen before updating sizing
|
|
244
304
|
if (fullscreenResizeTimeout) clearTimeout(fullscreenResizeTimeout)
|
|
245
305
|
fullscreenResizeTimeout = window.setTimeout(() => {
|
|
246
|
-
|
|
306
|
+
updateLayout()
|
|
247
307
|
}, 50)
|
|
248
308
|
})
|
|
249
309
|
.catch(() => {})
|
|
@@ -253,6 +313,10 @@ function toggleFullscreen() {
|
|
|
253
313
|
exitFullscreen()
|
|
254
314
|
.then(() => {
|
|
255
315
|
isFullscreen.value = false
|
|
316
|
+
if (fullscreenResizeTimeout) clearTimeout(fullscreenResizeTimeout)
|
|
317
|
+
fullscreenResizeTimeout = window.setTimeout(() => {
|
|
318
|
+
updateLayout()
|
|
319
|
+
}, 50)
|
|
256
320
|
})
|
|
257
321
|
.catch(() => {})
|
|
258
322
|
}
|
|
@@ -269,7 +333,7 @@ const touchStart = useDebounceFn((event: TouchEvent) => {
|
|
|
269
333
|
|
|
270
334
|
// Check if the touch started on an interactive element
|
|
271
335
|
if (targetElement.closest('button, a, input, textarea, select')) {
|
|
272
|
-
return // Don't handle swipe if interacting with
|
|
336
|
+
return // Don't handle swipe if interacting with controls
|
|
273
337
|
}
|
|
274
338
|
|
|
275
339
|
start.x = touch.screenX
|
|
@@ -283,7 +347,7 @@ const touchEnd = useDebounceFn((event: TouchEvent) => {
|
|
|
283
347
|
|
|
284
348
|
// Check if the touch ended on an interactive element
|
|
285
349
|
if (targetElement.closest('button, a, input, textarea, select')) {
|
|
286
|
-
return // Don't handle swipe if interacting with
|
|
350
|
+
return // Don't handle swipe if interacting with controls
|
|
287
351
|
}
|
|
288
352
|
|
|
289
353
|
const end = { x: touch.screenX, y: touch.screenY }
|
|
@@ -300,16 +364,15 @@ const touchEnd = useDebounceFn((event: TouchEvent) => {
|
|
|
300
364
|
// Add a threshold to prevent accidental swipes
|
|
301
365
|
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
|
|
302
366
|
if (diffX > 0) {
|
|
303
|
-
direction.value = 'next'
|
|
304
367
|
goNextImage()
|
|
305
368
|
}
|
|
306
369
|
else {
|
|
307
|
-
direction.value = 'prev'
|
|
308
370
|
goPrevImage()
|
|
309
371
|
}
|
|
310
372
|
}
|
|
311
373
|
}, 50)
|
|
312
374
|
|
|
375
|
+
// Border color function
|
|
313
376
|
function getBorderColor(i: any) {
|
|
314
377
|
if (props.borderColor !== undefined) {
|
|
315
378
|
return props.borderColor(i)
|
|
@@ -317,8 +380,7 @@ function getBorderColor(i: any) {
|
|
|
317
380
|
return ''
|
|
318
381
|
}
|
|
319
382
|
|
|
320
|
-
|
|
321
|
-
|
|
383
|
+
// Keyboard handlers
|
|
322
384
|
function handleKeyboardInput(event: KeyboardEvent) {
|
|
323
385
|
if (!isGalleryOpen.value) return
|
|
324
386
|
if (isKeyPressed.value) return
|
|
@@ -330,12 +392,10 @@ function handleKeyboardInput(event: KeyboardEvent) {
|
|
|
330
392
|
break
|
|
331
393
|
case 'ArrowRight':
|
|
332
394
|
isKeyPressed.value = true
|
|
333
|
-
direction.value = 'next'
|
|
334
395
|
goNextImage()
|
|
335
396
|
break
|
|
336
397
|
case 'ArrowLeft':
|
|
337
398
|
isKeyPressed.value = true
|
|
338
|
-
direction.value = 'prev'
|
|
339
399
|
goPrevImage()
|
|
340
400
|
break
|
|
341
401
|
case 'f':
|
|
@@ -366,76 +426,55 @@ const handleBackdropClick = useDebounceFn((event: MouseEvent) => {
|
|
|
366
426
|
}
|
|
367
427
|
}, 200)
|
|
368
428
|
|
|
369
|
-
// Watch for
|
|
370
|
-
watch(
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (infoElement) {
|
|
388
|
-
const height = infoElement.offsetHeight
|
|
389
|
-
infoHeight.value = height
|
|
390
|
-
document.documentElement.style.setProperty('--info-height', `${height}px`)
|
|
391
|
-
}
|
|
392
|
-
})
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Debounced window resize handler to avoid performance issues
|
|
396
|
-
const handleWindowResize = useDebounceFn(() => {
|
|
397
|
-
if (isGalleryOpen.value) {
|
|
398
|
-
updateImageSizes()
|
|
399
|
-
updateInfoHeight()
|
|
400
|
-
}
|
|
401
|
-
}, 100)
|
|
429
|
+
// Watch for image changes, fullscreen, or panel visibility changes
|
|
430
|
+
watch(
|
|
431
|
+
[
|
|
432
|
+
currentImage,
|
|
433
|
+
isFullscreen,
|
|
434
|
+
infoPanel,
|
|
435
|
+
sidePanel,
|
|
436
|
+
windowWidth,
|
|
437
|
+
windowHeight,
|
|
438
|
+
galleryWidth,
|
|
439
|
+
galleryHeight,
|
|
440
|
+
topControlsHeight,
|
|
441
|
+
infoPanelHeight,
|
|
442
|
+
],
|
|
443
|
+
() => {
|
|
444
|
+
updateLayout()
|
|
445
|
+
},
|
|
446
|
+
)
|
|
402
447
|
|
|
448
|
+
// Lifecycle hooks
|
|
403
449
|
onMounted(() => {
|
|
404
450
|
eventBus.on(`${props.id}GalleryImage`, openGalleryImage)
|
|
405
451
|
eventBus.on(`${props.id}Gallery`, openGalleryImage)
|
|
406
452
|
eventBus.on(`${props.id}GalleryClose`, closeGallery)
|
|
407
453
|
|
|
408
|
-
//
|
|
409
|
-
|
|
454
|
+
// Initialize layout
|
|
455
|
+
nextTick(() => {
|
|
456
|
+
updateLayout()
|
|
457
|
+
})
|
|
410
458
|
|
|
411
|
-
//
|
|
412
|
-
if (
|
|
413
|
-
|
|
459
|
+
// Set up observers for dynamic resizing
|
|
460
|
+
if (topControlsRef.value) {
|
|
461
|
+
useResizeObserver(topControlsRef.value, updateLayout)
|
|
414
462
|
}
|
|
415
463
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (infoElement) {
|
|
419
|
-
useResizeObserver(infoElement as HTMLElement, () => {
|
|
420
|
-
if (infoPanel.value) {
|
|
421
|
-
updateInfoHeight()
|
|
422
|
-
}
|
|
423
|
-
})
|
|
464
|
+
if (infoPanelRef.value) {
|
|
465
|
+
useResizeObserver(infoPanelRef.value, updateLayout)
|
|
424
466
|
}
|
|
425
467
|
|
|
426
|
-
|
|
427
|
-
|
|
468
|
+
if (sidePanelRef.value) {
|
|
469
|
+
useResizeObserver(sidePanelRef.value, updateLayout)
|
|
470
|
+
}
|
|
428
471
|
|
|
429
|
-
// Listen for fullscreen changes
|
|
472
|
+
// Listen for fullscreen changes
|
|
430
473
|
useEventListener(document, 'fullscreenchange', () => {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
else {
|
|
437
|
-
isFullscreen.value = false
|
|
438
|
-
}
|
|
474
|
+
isFullscreen.value = !!document.fullscreenElement
|
|
475
|
+
nextTick(() => {
|
|
476
|
+
updateLayout()
|
|
477
|
+
})
|
|
439
478
|
})
|
|
440
479
|
})
|
|
441
480
|
|
|
@@ -476,286 +515,262 @@ onUnmounted(() => {
|
|
|
476
515
|
>
|
|
477
516
|
<div
|
|
478
517
|
v-if="isGalleryOpen"
|
|
479
|
-
|
|
518
|
+
ref="galleryRef"
|
|
519
|
+
class="fixed bg-fv-neutral-900 text-white inset-0 max-w-[100vw] max-h-[100vh] overflow-hidden gallery-container"
|
|
480
520
|
style="z-index: 37"
|
|
481
521
|
role="dialog"
|
|
482
522
|
aria-modal="true"
|
|
483
523
|
@click="handleBackdropClick"
|
|
484
524
|
>
|
|
525
|
+
<!-- Top Controls Bar - Fixed at top -->
|
|
526
|
+
<transition
|
|
527
|
+
enter-active-class="transition-opacity duration-300"
|
|
528
|
+
enter-from-class="opacity-0"
|
|
529
|
+
enter-to-class="opacity-100"
|
|
530
|
+
leave-active-class="transition-opacity duration-300"
|
|
531
|
+
leave-from-class="opacity-100"
|
|
532
|
+
leave-to-class="opacity-0"
|
|
533
|
+
>
|
|
534
|
+
<div
|
|
535
|
+
v-if="showControls"
|
|
536
|
+
ref="topControlsRef"
|
|
537
|
+
class="fixed top-0 left-0 right-0 px-4 py-2 flex justify-between items-center bg-fv-neutral-900/90 backdrop-blur-sm z-50 controls-bar"
|
|
538
|
+
>
|
|
539
|
+
<!-- Title and Counter -->
|
|
540
|
+
<div class="flex items-center space-x-2">
|
|
541
|
+
<span v-if="title" class="font-medium text-lg">{{ title }}</span>
|
|
542
|
+
<span class="text-sm opacity-80">{{ currentIndex }} / {{ imageCount }}</span>
|
|
543
|
+
</div>
|
|
544
|
+
|
|
545
|
+
<!-- Control Buttons -->
|
|
546
|
+
<div class="flex items-center space-x-2">
|
|
547
|
+
<button
|
|
548
|
+
class="btn p-1.5 rounded-full bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
|
|
549
|
+
:class="{ 'bg-fv-primary-500/70': infoPanel }"
|
|
550
|
+
:title="infoPanel ? 'Hide info' : 'Show info'"
|
|
551
|
+
@click="toggleInfoPanel"
|
|
552
|
+
>
|
|
553
|
+
<InformationCircleIcon class="w-5 h-5" />
|
|
554
|
+
</button>
|
|
555
|
+
|
|
556
|
+
<button
|
|
557
|
+
class="btn p-1.5 rounded-full bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
|
|
558
|
+
:title="sidePanel ? 'Hide thumbnails' : 'Show thumbnails'"
|
|
559
|
+
@click="toggleSidePanel"
|
|
560
|
+
>
|
|
561
|
+
<ChevronDoubleRightIcon v-if="sidePanel" class="w-5 h-5" />
|
|
562
|
+
<ChevronDoubleLeftIcon v-else class="w-5 h-5" />
|
|
563
|
+
</button>
|
|
564
|
+
|
|
565
|
+
<button
|
|
566
|
+
class="btn p-1.5 rounded-full bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
|
|
567
|
+
aria-label="Close gallery"
|
|
568
|
+
@click="setModal(false)"
|
|
569
|
+
>
|
|
570
|
+
<component :is="closeIcon" class="w-5 h-5" />
|
|
571
|
+
</button>
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
</transition>
|
|
575
|
+
|
|
576
|
+
<!-- Main Gallery Content - Flexbox layout -->
|
|
485
577
|
<div
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
578
|
+
ref="galleryContentRef"
|
|
579
|
+
class="w-full h-full flex flex-col lg:flex-row"
|
|
580
|
+
style="margin-top: var(--controls-height, 0px)"
|
|
489
581
|
>
|
|
490
|
-
<!-- Main
|
|
491
|
-
<div
|
|
492
|
-
|
|
493
|
-
|
|
582
|
+
<!-- Main Image Area - Fills available space -->
|
|
583
|
+
<div
|
|
584
|
+
class="relative flex-1 h-full flex items-center justify-center"
|
|
585
|
+
:class="{ 'lg:pr-64': sidePanel }"
|
|
586
|
+
>
|
|
587
|
+
<!-- Image Navigation Controls - Left -->
|
|
588
|
+
<transition
|
|
589
|
+
enter-active-class="transition-opacity duration-300"
|
|
590
|
+
enter-from-class="opacity-0"
|
|
591
|
+
enter-to-class="opacity-100"
|
|
592
|
+
leave-active-class="transition-opacity duration-300"
|
|
593
|
+
leave-from-class="opacity-100"
|
|
594
|
+
leave-to-class="opacity-0"
|
|
595
|
+
>
|
|
494
596
|
<div
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
@touchend="touchEnd"
|
|
597
|
+
v-if="showControls && images.length > 1"
|
|
598
|
+
class="absolute left-0 z-40 h-full flex items-center px-2 md:px-4"
|
|
498
599
|
>
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
enter-to-class="opacity-100"
|
|
504
|
-
leave-active-class="transition-opacity duration-300"
|
|
505
|
-
leave-from-class="opacity-100"
|
|
506
|
-
leave-to-class="opacity-0"
|
|
600
|
+
<button
|
|
601
|
+
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"
|
|
602
|
+
aria-label="Previous image"
|
|
603
|
+
@click="goPrevImage()"
|
|
507
604
|
>
|
|
508
|
-
<
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
<button
|
|
513
|
-
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"
|
|
514
|
-
aria-label="Previous image"
|
|
515
|
-
@click="goPrevImage()"
|
|
516
|
-
>
|
|
517
|
-
<ChevronLeftIcon class="w-6 h-6 md:w-8 md:h-8" />
|
|
518
|
-
</button>
|
|
519
|
-
</div>
|
|
520
|
-
</transition>
|
|
605
|
+
<ChevronLeftIcon class="w-6 h-6 md:w-8 md:h-8" />
|
|
606
|
+
</button>
|
|
607
|
+
</div>
|
|
608
|
+
</transition>
|
|
521
609
|
|
|
522
|
-
|
|
610
|
+
<!-- Image Display Container -->
|
|
611
|
+
<div
|
|
612
|
+
ref="imageContainerRef"
|
|
613
|
+
class="image-container flex-grow flex items-center justify-center"
|
|
614
|
+
:class="{ 'has-info': infoPanel }"
|
|
615
|
+
@touchstart="touchStart"
|
|
616
|
+
@touchend="touchEnd"
|
|
617
|
+
>
|
|
618
|
+
<transition
|
|
619
|
+
:name="direction === 'next' ? 'slide-next' : 'slide-prev'"
|
|
620
|
+
mode="out-in"
|
|
621
|
+
>
|
|
523
622
|
<div
|
|
524
|
-
|
|
525
|
-
|
|
623
|
+
:key="`image-display-${modelValue}`"
|
|
624
|
+
class="image-display relative w-full h-full flex flex-col items-center justify-center"
|
|
526
625
|
>
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
626
|
+
<!-- Actual Image/Video Content -->
|
|
627
|
+
<template v-if="videoComponent && isVideo(images[modelValue])">
|
|
628
|
+
<ClientOnly>
|
|
629
|
+
<component
|
|
630
|
+
:is="videoComponent"
|
|
631
|
+
:src="isVideo(images[modelValue])"
|
|
632
|
+
class="shadow max-w-full h-auto object-contain video-component"
|
|
633
|
+
:style="{ maxHeight: availableHeight }"
|
|
634
|
+
/>
|
|
635
|
+
</ClientOnly>
|
|
636
|
+
</template>
|
|
637
|
+
<template v-else>
|
|
638
|
+
<img
|
|
639
|
+
v-if="modelValueSrc && imageComponent === 'img'"
|
|
640
|
+
class="shadow max-w-full h-auto object-contain"
|
|
641
|
+
:style="{ maxHeight: availableHeight }"
|
|
642
|
+
:src="modelValueSrc"
|
|
643
|
+
:alt="`Gallery image ${modelValue + 1}`"
|
|
534
644
|
>
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
? 'calc(90vh - var(--info-height, 0px) - 4rem)'
|
|
550
|
-
: 'calc(90vh - 4rem)'
|
|
551
|
-
: infoPanel
|
|
552
|
-
? 'calc(80vh - var(--info-height, 0px) - 4rem)'
|
|
553
|
-
: 'calc(80vh - 4rem)',
|
|
554
|
-
}"
|
|
555
|
-
/>
|
|
556
|
-
</ClientOnly>
|
|
557
|
-
</template>
|
|
558
|
-
<template v-else>
|
|
559
|
-
<img
|
|
560
|
-
v-if="modelValueSrc && imageComponent === 'img'"
|
|
561
|
-
class="shadow max-w-full h-auto object-contain"
|
|
562
|
-
:style="{
|
|
563
|
-
maxHeight: isFullscreen
|
|
564
|
-
? infoPanel
|
|
565
|
-
? 'calc(90vh - var(--info-height, 0px) - 4rem)'
|
|
566
|
-
: 'calc(90vh - 4rem)'
|
|
567
|
-
: infoPanel
|
|
568
|
-
? 'calc(80vh - var(--info-height, 0px) - 4rem)'
|
|
569
|
-
: 'calc(80vh - 4rem)',
|
|
570
|
-
}"
|
|
571
|
-
:src="modelValueSrc"
|
|
572
|
-
:alt="`Gallery image ${modelValue + 1}`"
|
|
573
|
-
>
|
|
574
|
-
<component
|
|
575
|
-
:is="imageComponent"
|
|
576
|
-
v-else-if="modelValueSrc && imageComponent"
|
|
577
|
-
:image="modelValueSrc.image"
|
|
578
|
-
:variant="modelValueSrc.variant"
|
|
579
|
-
:alt="modelValueSrc.alt"
|
|
580
|
-
class="shadow max-w-full h-auto object-contain"
|
|
581
|
-
:style="{
|
|
582
|
-
maxHeight: isFullscreen
|
|
583
|
-
? infoPanel
|
|
584
|
-
? 'calc(90vh - var(--info-height, 0px) - 4rem)'
|
|
585
|
-
: 'calc(90vh - 4rem)'
|
|
586
|
-
: infoPanel
|
|
587
|
-
? 'calc(80vh - var(--info-height, 0px) - 4rem)'
|
|
588
|
-
: 'calc(80vh - 4rem)',
|
|
589
|
-
}"
|
|
590
|
-
/>
|
|
591
|
-
</template>
|
|
592
|
-
</div>
|
|
593
|
-
|
|
594
|
-
<!-- Image Slot Content -->
|
|
595
|
-
<div
|
|
596
|
-
v-if="infoPanel"
|
|
597
|
-
class="info-panel-slot 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"
|
|
598
|
-
@transitionend="updateInfoHeight"
|
|
599
|
-
>
|
|
600
|
-
<slot :value="images[modelValue]" />
|
|
601
|
-
</div>
|
|
602
|
-
</div>
|
|
603
|
-
</transition>
|
|
645
|
+
<component
|
|
646
|
+
:is="imageComponent"
|
|
647
|
+
v-else-if="modelValueSrc && imageComponent"
|
|
648
|
+
:image="modelValueSrc.image"
|
|
649
|
+
:variant="modelValueSrc.variant"
|
|
650
|
+
:alt="modelValueSrc.alt"
|
|
651
|
+
class="shadow max-w-full h-auto object-contain"
|
|
652
|
+
:style="{ maxHeight: availableHeight }"
|
|
653
|
+
:likes="modelValueSrc.likes"
|
|
654
|
+
:show-likes="modelValueSrc.showLikes"
|
|
655
|
+
:is-author="modelValueSrc.isAuthor"
|
|
656
|
+
:user-uuid="modelValueSrc.userUUID"
|
|
657
|
+
/>
|
|
658
|
+
</template>
|
|
604
659
|
</div>
|
|
660
|
+
</transition>
|
|
661
|
+
</div>
|
|
605
662
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
663
|
+
<!-- Image Navigation Controls - Right -->
|
|
664
|
+
<transition
|
|
665
|
+
enter-active-class="transition-opacity duration-300"
|
|
666
|
+
enter-from-class="opacity-0"
|
|
667
|
+
enter-to-class="opacity-100"
|
|
668
|
+
leave-active-class="transition-opacity duration-300"
|
|
669
|
+
leave-from-class="opacity-100"
|
|
670
|
+
leave-to-class="opacity-0"
|
|
671
|
+
>
|
|
672
|
+
<div
|
|
673
|
+
v-if="showControls && images.length > 1"
|
|
674
|
+
class="absolute right-0 z-40 h-full flex items-center px-2 md:px-4"
|
|
675
|
+
>
|
|
676
|
+
<button
|
|
677
|
+
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"
|
|
678
|
+
aria-label="Next image"
|
|
679
|
+
@click="goNextImage()"
|
|
614
680
|
>
|
|
615
|
-
<
|
|
616
|
-
|
|
617
|
-
class="absolute right-0 z-[40] h-full flex items-center px-2 md:px-4"
|
|
618
|
-
>
|
|
619
|
-
<button
|
|
620
|
-
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"
|
|
621
|
-
aria-label="Next image"
|
|
622
|
-
@click="goNextImage()"
|
|
623
|
-
>
|
|
624
|
-
<ChevronRightIcon class="w-6 h-6 md:w-8 md:h-8" />
|
|
625
|
-
</button>
|
|
626
|
-
</div>
|
|
627
|
-
</transition>
|
|
681
|
+
<ChevronRightIcon class="w-6 h-6 md:w-8 md:h-8" />
|
|
682
|
+
</button>
|
|
628
683
|
</div>
|
|
629
|
-
</
|
|
684
|
+
</transition>
|
|
630
685
|
|
|
631
|
-
<!--
|
|
686
|
+
<!-- Info Panel Below Image -->
|
|
632
687
|
<transition
|
|
633
|
-
enter-active-class="
|
|
634
|
-
enter-from-class="translate-
|
|
635
|
-
enter-to-class="translate-
|
|
636
|
-
leave-active-class="
|
|
637
|
-
leave-from-class="translate-
|
|
638
|
-
leave-to-class="translate-
|
|
688
|
+
enter-active-class="transition-all duration-300 ease-out"
|
|
689
|
+
enter-from-class="opacity-0 transform translate-y-4"
|
|
690
|
+
enter-to-class="opacity-100 transform translate-y-0"
|
|
691
|
+
leave-active-class="transition-all duration-300 ease-in"
|
|
692
|
+
leave-from-class="opacity-100 transform translate-y-0"
|
|
693
|
+
leave-to-class="opacity-0 transform translate-y-4"
|
|
639
694
|
>
|
|
640
695
|
<div
|
|
641
|
-
v-if="
|
|
642
|
-
|
|
643
|
-
|
|
696
|
+
v-if="infoPanel && images[modelValue]"
|
|
697
|
+
ref="infoPanelRef"
|
|
698
|
+
class="info-panel absolute bottom-0 left-0 right-0 px-4 py-3 backdrop-blur-md bg-fv-neutral-900/70 z-45"
|
|
644
699
|
>
|
|
645
|
-
|
|
646
|
-
<div v-if="paging" class="flex items-center justify-center pt-2">
|
|
647
|
-
<DefaultPaging :id="id" :items="paging" />
|
|
648
|
-
</div>
|
|
649
|
-
|
|
650
|
-
<!-- Thumbnail Grid -->
|
|
651
|
-
<div class="grid grid-cols-2 gap-2 p-2">
|
|
652
|
-
<div
|
|
653
|
-
v-for="i in images.length"
|
|
654
|
-
:key="`bg_${id}_${i}`"
|
|
655
|
-
class="group relative"
|
|
656
|
-
>
|
|
657
|
-
<div
|
|
658
|
-
class="absolute inset-0 rounded-lg transition-colors duration-300 group-hover:bg-fv-neutral-700/40"
|
|
659
|
-
:class="{ 'bg-fv-primary-500/40': i - 1 === modelValue }"
|
|
660
|
-
/>
|
|
661
|
-
<img
|
|
662
|
-
v-if="imageComponent === 'img'"
|
|
663
|
-
:class="`h-auto max-w-full rounded-lg cursor-pointer shadow transition-all duration-300 group-hover:brightness-110 ${getBorderColor(
|
|
664
|
-
images[i - 1],
|
|
665
|
-
)}`"
|
|
666
|
-
:style="{
|
|
667
|
-
filter:
|
|
668
|
-
i - 1 === modelValue ? 'brightness(1)' : 'brightness(0.7)',
|
|
669
|
-
}"
|
|
670
|
-
:src="getThumbnailUrl(images[i - 1])"
|
|
671
|
-
:alt="`Thumbnail ${i}`"
|
|
672
|
-
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
673
|
-
>
|
|
674
|
-
<component
|
|
675
|
-
:is="imageComponent"
|
|
676
|
-
v-else
|
|
677
|
-
:image="getThumbnailUrl(images[i - 1]).image"
|
|
678
|
-
:variant="getThumbnailUrl(images[i - 1]).variant"
|
|
679
|
-
:alt="getThumbnailUrl(images[i - 1]).alt"
|
|
680
|
-
:class="`h-auto max-w-full rounded-lg cursor-pointer shadow transition-all duration-300 group-hover:brightness-110 ${getBorderColor(
|
|
681
|
-
images[i - 1],
|
|
682
|
-
)}`"
|
|
683
|
-
:style="{
|
|
684
|
-
filter:
|
|
685
|
-
i - 1 === modelValue ? 'brightness(1)' : 'brightness(0.7)',
|
|
686
|
-
}"
|
|
687
|
-
:likes="getThumbnailUrl(images[i - 1]).likes"
|
|
688
|
-
:show-likes="getThumbnailUrl(images[i - 1]).showLikes"
|
|
689
|
-
:is-author="getThumbnailUrl(images[i - 1]).isAuthor"
|
|
690
|
-
:user-uuid="getThumbnailUrl(images[i - 1]).userUUID"
|
|
691
|
-
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
692
|
-
/>
|
|
693
|
-
</div>
|
|
694
|
-
</div>
|
|
700
|
+
<slot :value="images[modelValue]" />
|
|
695
701
|
</div>
|
|
696
702
|
</transition>
|
|
697
703
|
</div>
|
|
698
704
|
|
|
699
|
-
<!--
|
|
705
|
+
<!-- Side Thumbnails Panel -->
|
|
700
706
|
<transition
|
|
701
|
-
enter-active-class="transition-
|
|
702
|
-
enter-from-class="
|
|
703
|
-
enter-to-class="
|
|
704
|
-
leave-active-class="transition-
|
|
705
|
-
leave-from-class="
|
|
706
|
-
leave-to-class="
|
|
707
|
+
enter-active-class="transform transition ease-in-out duration-300"
|
|
708
|
+
enter-from-class="translate-x-full"
|
|
709
|
+
enter-to-class="translate-x-0"
|
|
710
|
+
leave-active-class="transform transition ease-in-out duration-300"
|
|
711
|
+
leave-from-class="translate-x-0"
|
|
712
|
+
leave-to-class="translate-x-full"
|
|
707
713
|
>
|
|
708
714
|
<div
|
|
709
|
-
v-if="
|
|
710
|
-
|
|
715
|
+
v-if="sidePanel"
|
|
716
|
+
ref="sidePanelRef"
|
|
717
|
+
class="side-panel hidden lg:block absolute right-0 top-0 bottom-0 w-64 bg-fv-neutral-800/90 backdrop-blur-md overflow-y-auto z-40"
|
|
718
|
+
:style="{ 'padding-top': `${topControlsHeight}px` }"
|
|
711
719
|
>
|
|
712
|
-
<!--
|
|
713
|
-
<div class="flex items-center
|
|
714
|
-
<
|
|
715
|
-
<span class="text-sm opacity-80">{{ currentIndex }} / {{ imageCount }}</span>
|
|
720
|
+
<!-- Paging Controls if needed -->
|
|
721
|
+
<div v-if="paging" class="flex items-center justify-center pt-2">
|
|
722
|
+
<DefaultPaging :id="id" :items="paging" />
|
|
716
723
|
</div>
|
|
717
724
|
|
|
718
|
-
<!--
|
|
719
|
-
<div class="
|
|
720
|
-
<
|
|
721
|
-
|
|
722
|
-
:
|
|
723
|
-
|
|
724
|
-
@click="toggleInfoPanel"
|
|
725
|
-
>
|
|
726
|
-
<InformationCircleIcon class="w-5 h-5" />
|
|
727
|
-
</button>
|
|
728
|
-
|
|
729
|
-
<button
|
|
730
|
-
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"
|
|
731
|
-
:title="sidePanel ? 'Hide thumbnails' : 'Show thumbnails'"
|
|
732
|
-
@click="() => (sidePanel = !sidePanel)"
|
|
733
|
-
>
|
|
734
|
-
<ChevronDoubleRightIcon v-if="sidePanel" class="w-5 h-5" />
|
|
735
|
-
<ChevronDoubleLeftIcon v-else class="w-5 h-5" />
|
|
736
|
-
</button>
|
|
737
|
-
|
|
738
|
-
<button
|
|
739
|
-
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"
|
|
740
|
-
:title="sidePanel ? 'Hide thumbnails' : 'Show thumbnails'"
|
|
741
|
-
@click="() => (sidePanel = !sidePanel)"
|
|
742
|
-
>
|
|
743
|
-
<ChevronDoubleRightIcon v-if="sidePanel" class="w-5 h-5" />
|
|
744
|
-
<ChevronDoubleLeftIcon v-else class="w-5 h-5" />
|
|
745
|
-
</button>
|
|
746
|
-
|
|
747
|
-
<button
|
|
748
|
-
class="btn p-1.5 rounded-full bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
|
|
749
|
-
aria-label="Close gallery"
|
|
750
|
-
@click="setModal(false)"
|
|
725
|
+
<!-- Thumbnail Grid -->
|
|
726
|
+
<div class="grid grid-cols-2 gap-2 p-2">
|
|
727
|
+
<div
|
|
728
|
+
v-for="i in images.length"
|
|
729
|
+
:key="`bg_${id}_${i}`"
|
|
730
|
+
class="group relative"
|
|
751
731
|
>
|
|
752
|
-
<
|
|
753
|
-
|
|
732
|
+
<div
|
|
733
|
+
class="absolute inset-0 rounded-lg transition-colors duration-300 group-hover:bg-fv-neutral-700/40"
|
|
734
|
+
:class="{ 'bg-fv-primary-500/40': i - 1 === modelValue }"
|
|
735
|
+
/>
|
|
736
|
+
<img
|
|
737
|
+
v-if="imageComponent === 'img'"
|
|
738
|
+
:class="`h-auto max-w-full rounded-lg cursor-pointer shadow transition-all duration-300 group-hover:brightness-110 ${getBorderColor(
|
|
739
|
+
images[i - 1],
|
|
740
|
+
)}`"
|
|
741
|
+
:style="{
|
|
742
|
+
filter:
|
|
743
|
+
i - 1 === modelValue ? 'brightness(1)' : 'brightness(0.7)',
|
|
744
|
+
}"
|
|
745
|
+
:src="getThumbnailUrl(images[i - 1])"
|
|
746
|
+
:alt="`Thumbnail ${i}`"
|
|
747
|
+
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
748
|
+
>
|
|
749
|
+
<component
|
|
750
|
+
:is="imageComponent"
|
|
751
|
+
v-else
|
|
752
|
+
:image="getThumbnailUrl(images[i - 1]).image"
|
|
753
|
+
:variant="getThumbnailUrl(images[i - 1]).variant"
|
|
754
|
+
:alt="getThumbnailUrl(images[i - 1]).alt"
|
|
755
|
+
:class="`h-auto max-w-full rounded-lg cursor-pointer shadow transition-all duration-300 group-hover:brightness-110 ${getBorderColor(
|
|
756
|
+
images[i - 1],
|
|
757
|
+
)}`"
|
|
758
|
+
:style="{
|
|
759
|
+
filter:
|
|
760
|
+
i - 1 === modelValue ? 'brightness(1)' : 'brightness(0.7)',
|
|
761
|
+
}"
|
|
762
|
+
:likes="getThumbnailUrl(images[i - 1]).likes"
|
|
763
|
+
:show-likes="getThumbnailUrl(images[i - 1]).showLikes"
|
|
764
|
+
:is-author="getThumbnailUrl(images[i - 1]).isAuthor"
|
|
765
|
+
:user-uuid="getThumbnailUrl(images[i - 1]).userUUID"
|
|
766
|
+
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
767
|
+
/>
|
|
768
|
+
</div>
|
|
754
769
|
</div>
|
|
755
770
|
</div>
|
|
756
771
|
</transition>
|
|
757
772
|
|
|
758
|
-
<!-- Mobile Thumbnail Preview -->
|
|
773
|
+
<!-- Mobile Thumbnail Preview (bottom of screen on mobile) -->
|
|
759
774
|
<transition
|
|
760
775
|
enter-active-class="transition-transform duration-300 ease-out"
|
|
761
776
|
enter-from-class="translate-y-full"
|
|
@@ -766,7 +781,8 @@ onUnmounted(() => {
|
|
|
766
781
|
>
|
|
767
782
|
<div
|
|
768
783
|
v-if="showControls && images.length > 1 && !sidePanel"
|
|
769
|
-
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-
|
|
784
|
+
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-45"
|
|
785
|
+
:class="{ 'pb-20': infoPanel }"
|
|
770
786
|
>
|
|
771
787
|
<div class="overflow-x-auto flex space-x-2 pb-1 px-1">
|
|
772
788
|
<div
|
|
@@ -804,7 +820,7 @@ onUnmounted(() => {
|
|
|
804
820
|
</div>
|
|
805
821
|
</transition>
|
|
806
822
|
|
|
807
|
-
<!-- Thumbnail Grid/Mason/Custom Layouts -->
|
|
823
|
+
<!-- Thumbnail Grid/Mason/Custom Layouts for non-opened gallery -->
|
|
808
824
|
<div v-if="mode === 'grid' || mode === 'mason' || mode === 'custom'" class="gallery-grid">
|
|
809
825
|
<div
|
|
810
826
|
:class="{
|
|
@@ -898,13 +914,41 @@ onUnmounted(() => {
|
|
|
898
914
|
</template>
|
|
899
915
|
|
|
900
916
|
<style scoped>
|
|
901
|
-
/*
|
|
902
|
-
.
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
917
|
+
/* Ensure controls stay fixed at top */
|
|
918
|
+
.controls-bar {
|
|
919
|
+
height: auto;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/* Layout container for main image and info panel */
|
|
923
|
+
.image-container {
|
|
924
|
+
position: relative;
|
|
925
|
+
display: flex;
|
|
926
|
+
flex-direction: column;
|
|
927
|
+
justify-content: center;
|
|
928
|
+
align-items: center;
|
|
929
|
+
height: 100%;
|
|
930
|
+
width: 100%;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/* Side panel positioning */
|
|
934
|
+
.side-panel {
|
|
935
|
+
height: 100vh;
|
|
936
|
+
overflow-y: auto;
|
|
937
|
+
overflow-x: hidden;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/* Info panel styling */
|
|
941
|
+
.info-panel {
|
|
942
|
+
width: 100%;
|
|
943
|
+
border-top-left-radius: 0.5rem;
|
|
944
|
+
border-top-right-radius: 0.5rem;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/* Image sizing in different contexts */
|
|
948
|
+
.image-display img,
|
|
949
|
+
.image-display .video-component {
|
|
950
|
+
transition: max-height 0.3s ease-out, max-width 0.3s ease-out;
|
|
951
|
+
object-fit: contain;
|
|
908
952
|
}
|
|
909
953
|
|
|
910
954
|
/* Transition styles for next (right) navigation */
|
|
@@ -918,8 +962,8 @@ onUnmounted(() => {
|
|
|
918
962
|
|
|
919
963
|
.slide-next-enter-from {
|
|
920
964
|
opacity: 0;
|
|
921
|
-
transform: translateX(
|
|
922
|
-
filter: blur(
|
|
965
|
+
transform: translateX(30px);
|
|
966
|
+
filter: blur(8px);
|
|
923
967
|
}
|
|
924
968
|
|
|
925
969
|
.slide-next-enter-to {
|
|
@@ -936,8 +980,8 @@ onUnmounted(() => {
|
|
|
936
980
|
|
|
937
981
|
.slide-next-leave-to {
|
|
938
982
|
opacity: 0;
|
|
939
|
-
transform: translateX(-
|
|
940
|
-
filter: blur(
|
|
983
|
+
transform: translateX(-30px);
|
|
984
|
+
filter: blur(8px);
|
|
941
985
|
}
|
|
942
986
|
|
|
943
987
|
/* Transition styles for prev (left) navigation */
|
|
@@ -951,8 +995,8 @@ onUnmounted(() => {
|
|
|
951
995
|
|
|
952
996
|
.slide-prev-enter-from {
|
|
953
997
|
opacity: 0;
|
|
954
|
-
transform: translateX(-
|
|
955
|
-
filter: blur(
|
|
998
|
+
transform: translateX(-30px);
|
|
999
|
+
filter: blur(8px);
|
|
956
1000
|
}
|
|
957
1001
|
|
|
958
1002
|
.slide-prev-enter-to {
|
|
@@ -969,11 +1013,11 @@ onUnmounted(() => {
|
|
|
969
1013
|
|
|
970
1014
|
.slide-prev-leave-to {
|
|
971
1015
|
opacity: 0;
|
|
972
|
-
transform: translateX(
|
|
973
|
-
filter: blur(
|
|
1016
|
+
transform: translateX(30px);
|
|
1017
|
+
filter: blur(8px);
|
|
974
1018
|
}
|
|
975
1019
|
|
|
976
|
-
/*
|
|
1020
|
+
/* Grid layouts for thumbnails */
|
|
977
1021
|
.gallery-grid {
|
|
978
1022
|
min-height: 200px;
|
|
979
1023
|
}
|