@fy-/fws-vue 2.3.13 → 2.3.15
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 +447 -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,69 @@ 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
|
-
|
|
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 and width
|
|
165
|
+
img.style.maxHeight = availableHeight.value
|
|
166
|
+
|
|
167
|
+
// Adjust image size based on screen size
|
|
168
|
+
if (windowWidth.value <= 768) {
|
|
169
|
+
img.style.maxWidth = '95vw'
|
|
170
|
+
img.style.maxHeight = `calc(${windowHeight.value * 0.7 / 16}rem)`
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
img.style.maxWidth = sidePanel.value ? 'calc(100vw - 17rem)' : '94vw'
|
|
174
|
+
}
|
|
175
|
+
})
|
|
113
176
|
})
|
|
114
|
-
}
|
|
177
|
+
}, 50)
|
|
115
178
|
|
|
179
|
+
// Update all layout measurements
|
|
180
|
+
const updateLayout = useDebounceFn(() => {
|
|
181
|
+
calculateImageSize()
|
|
182
|
+
}, 50)
|
|
183
|
+
|
|
184
|
+
// Modal controls
|
|
116
185
|
function setModal(value: boolean) {
|
|
117
186
|
if (value === true) {
|
|
118
187
|
if (props.onOpen) props.onOpen()
|
|
119
188
|
document.body.style.overflow = 'hidden' // Prevent scrolling when gallery is open
|
|
189
|
+
|
|
120
190
|
if (!import.meta.env.SSR) {
|
|
121
191
|
useEventListener(document, 'keydown', handleKeyboardInput)
|
|
122
192
|
useEventListener(document, 'keyup', handleKeyboardRelease)
|
|
123
193
|
}
|
|
194
|
+
|
|
124
195
|
// Auto-hide controls after 3 seconds on mobile
|
|
125
|
-
if (
|
|
196
|
+
if (windowWidth.value < 1024) {
|
|
126
197
|
controlsTimeout = window.setTimeout(() => {
|
|
127
198
|
showControls.value = false
|
|
128
199
|
}, 3000)
|
|
@@ -131,11 +202,13 @@ function setModal(value: boolean) {
|
|
|
131
202
|
else {
|
|
132
203
|
if (props.onClose) props.onClose()
|
|
133
204
|
document.body.style.overflow = '' // Restore scrolling
|
|
205
|
+
|
|
134
206
|
// Exit fullscreen if active
|
|
135
207
|
if (isFullscreen.value) {
|
|
136
208
|
exitFullscreen()
|
|
137
209
|
isFullscreen.value = false
|
|
138
210
|
}
|
|
211
|
+
|
|
139
212
|
// Clear timeout if modal is closed
|
|
140
213
|
if (controlsTimeout) {
|
|
141
214
|
clearTimeout(controlsTimeout)
|
|
@@ -147,6 +220,7 @@ function setModal(value: boolean) {
|
|
|
147
220
|
// Don't reset info panel state when opening/closing
|
|
148
221
|
}
|
|
149
222
|
|
|
223
|
+
// Open gallery with debounce to prevent accidental double-clicks
|
|
150
224
|
const openGalleryImage = useDebounceFn((index: number | undefined) => {
|
|
151
225
|
if (index === undefined) {
|
|
152
226
|
modelValue.value = 0
|
|
@@ -155,8 +229,14 @@ const openGalleryImage = useDebounceFn((index: number | undefined) => {
|
|
|
155
229
|
modelValue.value = Number.parseInt(index.toString())
|
|
156
230
|
}
|
|
157
231
|
setModal(true)
|
|
158
|
-
}, 50) // Debounce to prevent accidental double-opens
|
|
159
232
|
|
|
233
|
+
// Update layout after opening
|
|
234
|
+
nextTick(() => {
|
|
235
|
+
updateLayout()
|
|
236
|
+
})
|
|
237
|
+
}, 50)
|
|
238
|
+
|
|
239
|
+
// Navigation functions
|
|
160
240
|
function goNextImage() {
|
|
161
241
|
direction.value = 'next'
|
|
162
242
|
if (modelValue.value < props.images.length - 1) {
|
|
@@ -174,34 +254,18 @@ function goPrevImage() {
|
|
|
174
254
|
modelValue.value--
|
|
175
255
|
}
|
|
176
256
|
else {
|
|
177
|
-
modelValue.value
|
|
178
|
-
= props.images.length - 1 > 0 ? props.images.length - 1 : 0
|
|
257
|
+
modelValue.value = props.images.length - 1 > 0 ? props.images.length - 1 : 0
|
|
179
258
|
}
|
|
180
259
|
resetControlsTimer()
|
|
181
260
|
}
|
|
182
261
|
|
|
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
|
-
|
|
262
|
+
// UI control functions
|
|
199
263
|
function resetControlsTimer() {
|
|
200
264
|
// Show controls when user interacts
|
|
201
265
|
showControls.value = true
|
|
202
266
|
|
|
203
267
|
// Only set timer on mobile
|
|
204
|
-
if (
|
|
268
|
+
if (windowWidth.value < 1024) {
|
|
205
269
|
if (controlsTimeout) {
|
|
206
270
|
clearTimeout(controlsTimeout)
|
|
207
271
|
}
|
|
@@ -213,7 +277,7 @@ function resetControlsTimer() {
|
|
|
213
277
|
|
|
214
278
|
function toggleControls() {
|
|
215
279
|
showControls.value = !showControls.value
|
|
216
|
-
if (showControls.value &&
|
|
280
|
+
if (showControls.value && windowWidth.value < 1024) {
|
|
217
281
|
resetControlsTimer()
|
|
218
282
|
}
|
|
219
283
|
}
|
|
@@ -222,28 +286,32 @@ function toggleInfoPanel() {
|
|
|
222
286
|
infoPanel.value = !infoPanel.value
|
|
223
287
|
resetControlsTimer()
|
|
224
288
|
|
|
225
|
-
// Update
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
289
|
+
// Update layout after panel toggle
|
|
290
|
+
nextTick(() => {
|
|
291
|
+
updateLayout()
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function toggleSidePanel() {
|
|
296
|
+
sidePanel.value = !sidePanel.value
|
|
297
|
+
resetControlsTimer()
|
|
298
|
+
|
|
299
|
+
// Update layout after panel toggle
|
|
300
|
+
nextTick(() => {
|
|
301
|
+
updateLayout()
|
|
302
|
+
})
|
|
235
303
|
}
|
|
236
304
|
|
|
237
305
|
function toggleFullscreen() {
|
|
238
306
|
if (!isFullscreen.value) {
|
|
239
|
-
if (
|
|
307
|
+
if (galleryRef.value) {
|
|
240
308
|
enterFullscreen()
|
|
241
309
|
.then(() => {
|
|
242
310
|
isFullscreen.value = true
|
|
243
311
|
// Give browser time to adjust fullscreen before updating sizing
|
|
244
312
|
if (fullscreenResizeTimeout) clearTimeout(fullscreenResizeTimeout)
|
|
245
313
|
fullscreenResizeTimeout = window.setTimeout(() => {
|
|
246
|
-
|
|
314
|
+
updateLayout()
|
|
247
315
|
}, 50)
|
|
248
316
|
})
|
|
249
317
|
.catch(() => {})
|
|
@@ -253,6 +321,10 @@ function toggleFullscreen() {
|
|
|
253
321
|
exitFullscreen()
|
|
254
322
|
.then(() => {
|
|
255
323
|
isFullscreen.value = false
|
|
324
|
+
if (fullscreenResizeTimeout) clearTimeout(fullscreenResizeTimeout)
|
|
325
|
+
fullscreenResizeTimeout = window.setTimeout(() => {
|
|
326
|
+
updateLayout()
|
|
327
|
+
}, 50)
|
|
256
328
|
})
|
|
257
329
|
.catch(() => {})
|
|
258
330
|
}
|
|
@@ -269,7 +341,7 @@ const touchStart = useDebounceFn((event: TouchEvent) => {
|
|
|
269
341
|
|
|
270
342
|
// Check if the touch started on an interactive element
|
|
271
343
|
if (targetElement.closest('button, a, input, textarea, select')) {
|
|
272
|
-
return // Don't handle swipe if interacting with
|
|
344
|
+
return // Don't handle swipe if interacting with controls
|
|
273
345
|
}
|
|
274
346
|
|
|
275
347
|
start.x = touch.screenX
|
|
@@ -283,7 +355,7 @@ const touchEnd = useDebounceFn((event: TouchEvent) => {
|
|
|
283
355
|
|
|
284
356
|
// Check if the touch ended on an interactive element
|
|
285
357
|
if (targetElement.closest('button, a, input, textarea, select')) {
|
|
286
|
-
return // Don't handle swipe if interacting with
|
|
358
|
+
return // Don't handle swipe if interacting with controls
|
|
287
359
|
}
|
|
288
360
|
|
|
289
361
|
const end = { x: touch.screenX, y: touch.screenY }
|
|
@@ -300,16 +372,15 @@ const touchEnd = useDebounceFn((event: TouchEvent) => {
|
|
|
300
372
|
// Add a threshold to prevent accidental swipes
|
|
301
373
|
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
|
|
302
374
|
if (diffX > 0) {
|
|
303
|
-
direction.value = 'next'
|
|
304
375
|
goNextImage()
|
|
305
376
|
}
|
|
306
377
|
else {
|
|
307
|
-
direction.value = 'prev'
|
|
308
378
|
goPrevImage()
|
|
309
379
|
}
|
|
310
380
|
}
|
|
311
381
|
}, 50)
|
|
312
382
|
|
|
383
|
+
// Border color function
|
|
313
384
|
function getBorderColor(i: any) {
|
|
314
385
|
if (props.borderColor !== undefined) {
|
|
315
386
|
return props.borderColor(i)
|
|
@@ -317,8 +388,7 @@ function getBorderColor(i: any) {
|
|
|
317
388
|
return ''
|
|
318
389
|
}
|
|
319
390
|
|
|
320
|
-
|
|
321
|
-
|
|
391
|
+
// Keyboard handlers
|
|
322
392
|
function handleKeyboardInput(event: KeyboardEvent) {
|
|
323
393
|
if (!isGalleryOpen.value) return
|
|
324
394
|
if (isKeyPressed.value) return
|
|
@@ -330,12 +400,10 @@ function handleKeyboardInput(event: KeyboardEvent) {
|
|
|
330
400
|
break
|
|
331
401
|
case 'ArrowRight':
|
|
332
402
|
isKeyPressed.value = true
|
|
333
|
-
direction.value = 'next'
|
|
334
403
|
goNextImage()
|
|
335
404
|
break
|
|
336
405
|
case 'ArrowLeft':
|
|
337
406
|
isKeyPressed.value = true
|
|
338
|
-
direction.value = 'prev'
|
|
339
407
|
goPrevImage()
|
|
340
408
|
break
|
|
341
409
|
case 'f':
|
|
@@ -366,76 +434,55 @@ const handleBackdropClick = useDebounceFn((event: MouseEvent) => {
|
|
|
366
434
|
}
|
|
367
435
|
}, 200)
|
|
368
436
|
|
|
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)
|
|
437
|
+
// Watch for image changes, fullscreen, or panel visibility changes
|
|
438
|
+
watch(
|
|
439
|
+
[
|
|
440
|
+
currentImage,
|
|
441
|
+
isFullscreen,
|
|
442
|
+
infoPanel,
|
|
443
|
+
sidePanel,
|
|
444
|
+
windowWidth,
|
|
445
|
+
windowHeight,
|
|
446
|
+
galleryWidth,
|
|
447
|
+
galleryHeight,
|
|
448
|
+
topControlsHeight,
|
|
449
|
+
infoPanelHeight,
|
|
450
|
+
],
|
|
451
|
+
() => {
|
|
452
|
+
updateLayout()
|
|
453
|
+
},
|
|
454
|
+
)
|
|
402
455
|
|
|
456
|
+
// Lifecycle hooks
|
|
403
457
|
onMounted(() => {
|
|
404
458
|
eventBus.on(`${props.id}GalleryImage`, openGalleryImage)
|
|
405
459
|
eventBus.on(`${props.id}Gallery`, openGalleryImage)
|
|
406
460
|
eventBus.on(`${props.id}GalleryClose`, closeGallery)
|
|
407
461
|
|
|
408
|
-
//
|
|
409
|
-
|
|
462
|
+
// Initialize layout
|
|
463
|
+
nextTick(() => {
|
|
464
|
+
updateLayout()
|
|
465
|
+
})
|
|
410
466
|
|
|
411
|
-
//
|
|
412
|
-
if (
|
|
413
|
-
|
|
467
|
+
// Set up observers for dynamic resizing
|
|
468
|
+
if (topControlsRef.value) {
|
|
469
|
+
useResizeObserver(topControlsRef.value, updateLayout)
|
|
414
470
|
}
|
|
415
471
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (infoElement) {
|
|
419
|
-
useResizeObserver(infoElement as HTMLElement, () => {
|
|
420
|
-
if (infoPanel.value) {
|
|
421
|
-
updateInfoHeight()
|
|
422
|
-
}
|
|
423
|
-
})
|
|
472
|
+
if (infoPanelRef.value) {
|
|
473
|
+
useResizeObserver(infoPanelRef.value, updateLayout)
|
|
424
474
|
}
|
|
425
475
|
|
|
426
|
-
|
|
427
|
-
|
|
476
|
+
if (sidePanelRef.value) {
|
|
477
|
+
useResizeObserver(sidePanelRef.value, updateLayout)
|
|
478
|
+
}
|
|
428
479
|
|
|
429
|
-
// Listen for fullscreen changes
|
|
480
|
+
// Listen for fullscreen changes
|
|
430
481
|
useEventListener(document, 'fullscreenchange', () => {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
else {
|
|
437
|
-
isFullscreen.value = false
|
|
438
|
-
}
|
|
482
|
+
isFullscreen.value = !!document.fullscreenElement
|
|
483
|
+
nextTick(() => {
|
|
484
|
+
updateLayout()
|
|
485
|
+
})
|
|
439
486
|
})
|
|
440
487
|
})
|
|
441
488
|
|
|
@@ -476,286 +523,264 @@ onUnmounted(() => {
|
|
|
476
523
|
>
|
|
477
524
|
<div
|
|
478
525
|
v-if="isGalleryOpen"
|
|
479
|
-
|
|
526
|
+
ref="galleryRef"
|
|
527
|
+
class="fixed bg-fv-neutral-900 text-white inset-0 max-w-[100vw] max-h-[100vh] overflow-hidden gallery-container"
|
|
480
528
|
style="z-index: 37"
|
|
481
529
|
role="dialog"
|
|
482
530
|
aria-modal="true"
|
|
483
531
|
@click="handleBackdropClick"
|
|
484
532
|
>
|
|
533
|
+
<!-- Top Controls Bar - Fixed at top -->
|
|
534
|
+
<transition
|
|
535
|
+
enter-active-class="transition-opacity duration-300"
|
|
536
|
+
enter-from-class="opacity-0"
|
|
537
|
+
enter-to-class="opacity-100"
|
|
538
|
+
leave-active-class="transition-opacity duration-300"
|
|
539
|
+
leave-from-class="opacity-100"
|
|
540
|
+
leave-to-class="opacity-0"
|
|
541
|
+
>
|
|
542
|
+
<div
|
|
543
|
+
v-if="showControls"
|
|
544
|
+
ref="topControlsRef"
|
|
545
|
+
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"
|
|
546
|
+
>
|
|
547
|
+
<!-- Title and Counter -->
|
|
548
|
+
<div class="flex items-center space-x-2">
|
|
549
|
+
<span v-if="title" class="font-medium text-lg">{{ title }}</span>
|
|
550
|
+
<span class="text-sm opacity-80">{{ currentIndex }} / {{ imageCount }}</span>
|
|
551
|
+
</div>
|
|
552
|
+
|
|
553
|
+
<!-- Control Buttons -->
|
|
554
|
+
<div class="flex items-center space-x-2">
|
|
555
|
+
<button
|
|
556
|
+
class="btn p-1.5 rounded-full bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
|
|
557
|
+
:class="{ 'bg-fv-primary-500/70': infoPanel }"
|
|
558
|
+
:title="infoPanel ? 'Hide info' : 'Show info'"
|
|
559
|
+
@click="toggleInfoPanel"
|
|
560
|
+
>
|
|
561
|
+
<InformationCircleIcon class="w-5 h-5" />
|
|
562
|
+
</button>
|
|
563
|
+
|
|
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="sidePanel ? 'Hide thumbnails' : 'Show thumbnails'"
|
|
567
|
+
@click="toggleSidePanel"
|
|
568
|
+
>
|
|
569
|
+
<ChevronDoubleRightIcon v-if="sidePanel" class="w-5 h-5" />
|
|
570
|
+
<ChevronDoubleLeftIcon v-else class="w-5 h-5" />
|
|
571
|
+
</button>
|
|
572
|
+
|
|
573
|
+
<button
|
|
574
|
+
class="btn p-1.5 rounded-full bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
|
|
575
|
+
aria-label="Close gallery"
|
|
576
|
+
@click="setModal(false)"
|
|
577
|
+
>
|
|
578
|
+
<component :is="closeIcon" class="w-5 h-5" />
|
|
579
|
+
</button>
|
|
580
|
+
</div>
|
|
581
|
+
</div>
|
|
582
|
+
</transition>
|
|
583
|
+
|
|
584
|
+
<!-- Main Gallery Content - Flexbox layout -->
|
|
485
585
|
<div
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
586
|
+
ref="galleryContentRef"
|
|
587
|
+
class="w-full h-full flex flex-col lg:flex-row"
|
|
588
|
+
style="margin-top: var(--controls-height, 0px)"
|
|
489
589
|
>
|
|
490
|
-
<!-- Main
|
|
491
|
-
<div
|
|
492
|
-
|
|
493
|
-
|
|
590
|
+
<!-- Main Image Area - Fills available space -->
|
|
591
|
+
<div
|
|
592
|
+
class="relative flex-1 h-full flex items-center justify-center"
|
|
593
|
+
:class="{ 'lg:pr-64': sidePanel, 'lg:max-w-[calc(100%-16rem)]': sidePanel }"
|
|
594
|
+
style="max-width: 100%;"
|
|
595
|
+
>
|
|
596
|
+
<!-- Image Navigation Controls - Left -->
|
|
597
|
+
<transition
|
|
598
|
+
enter-active-class="transition-opacity duration-300"
|
|
599
|
+
enter-from-class="opacity-0"
|
|
600
|
+
enter-to-class="opacity-100"
|
|
601
|
+
leave-active-class="transition-opacity duration-300"
|
|
602
|
+
leave-from-class="opacity-100"
|
|
603
|
+
leave-to-class="opacity-0"
|
|
604
|
+
>
|
|
494
605
|
<div
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
@touchend="touchEnd"
|
|
606
|
+
v-if="showControls && images.length > 1"
|
|
607
|
+
class="absolute left-0 z-40 h-full flex items-center px-2 md:px-4"
|
|
498
608
|
>
|
|
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"
|
|
609
|
+
<button
|
|
610
|
+
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"
|
|
611
|
+
aria-label="Previous image"
|
|
612
|
+
@click="goPrevImage()"
|
|
507
613
|
>
|
|
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>
|
|
614
|
+
<ChevronLeftIcon class="w-6 h-6 md:w-8 md:h-8" />
|
|
615
|
+
</button>
|
|
616
|
+
</div>
|
|
617
|
+
</transition>
|
|
521
618
|
|
|
522
|
-
|
|
619
|
+
<!-- Image Display Container -->
|
|
620
|
+
<div
|
|
621
|
+
ref="imageContainerRef"
|
|
622
|
+
class="image-container flex-grow flex items-center justify-center"
|
|
623
|
+
:class="{ 'has-info': infoPanel }"
|
|
624
|
+
@touchstart="touchStart"
|
|
625
|
+
@touchend="touchEnd"
|
|
626
|
+
>
|
|
627
|
+
<transition
|
|
628
|
+
:name="direction === 'next' ? 'slide-next' : 'slide-prev'"
|
|
629
|
+
mode="out-in"
|
|
630
|
+
>
|
|
523
631
|
<div
|
|
524
|
-
|
|
525
|
-
|
|
632
|
+
:key="`image-display-${modelValue}`"
|
|
633
|
+
class="image-display relative w-full h-full flex flex-col items-center justify-center"
|
|
526
634
|
>
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
635
|
+
<!-- Actual Image/Video Content -->
|
|
636
|
+
<template v-if="videoComponent && isVideo(images[modelValue])">
|
|
637
|
+
<ClientOnly>
|
|
638
|
+
<component
|
|
639
|
+
:is="videoComponent"
|
|
640
|
+
:src="isVideo(images[modelValue])"
|
|
641
|
+
class="shadow max-w-full h-auto object-contain video-component"
|
|
642
|
+
:style="{ maxHeight: availableHeight }"
|
|
643
|
+
/>
|
|
644
|
+
</ClientOnly>
|
|
645
|
+
</template>
|
|
646
|
+
<template v-else>
|
|
647
|
+
<img
|
|
648
|
+
v-if="modelValueSrc && imageComponent === 'img'"
|
|
649
|
+
class="shadow max-w-full h-auto object-contain"
|
|
650
|
+
:style="{ maxHeight: availableHeight }"
|
|
651
|
+
:src="modelValueSrc"
|
|
652
|
+
:alt="`Gallery image ${modelValue + 1}`"
|
|
534
653
|
>
|
|
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>
|
|
654
|
+
<component
|
|
655
|
+
:is="imageComponent"
|
|
656
|
+
v-else-if="modelValueSrc && imageComponent"
|
|
657
|
+
:image="modelValueSrc.image"
|
|
658
|
+
:variant="modelValueSrc.variant"
|
|
659
|
+
:alt="modelValueSrc.alt"
|
|
660
|
+
class="shadow max-w-full h-auto object-contain"
|
|
661
|
+
:style="{ maxHeight: availableHeight }"
|
|
662
|
+
:likes="modelValueSrc.likes"
|
|
663
|
+
:show-likes="modelValueSrc.showLikes"
|
|
664
|
+
:is-author="modelValueSrc.isAuthor"
|
|
665
|
+
:user-uuid="modelValueSrc.userUUID"
|
|
666
|
+
/>
|
|
667
|
+
</template>
|
|
604
668
|
</div>
|
|
669
|
+
</transition>
|
|
670
|
+
</div>
|
|
605
671
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
672
|
+
<!-- Image Navigation Controls - Right -->
|
|
673
|
+
<transition
|
|
674
|
+
enter-active-class="transition-opacity duration-300"
|
|
675
|
+
enter-from-class="opacity-0"
|
|
676
|
+
enter-to-class="opacity-100"
|
|
677
|
+
leave-active-class="transition-opacity duration-300"
|
|
678
|
+
leave-from-class="opacity-100"
|
|
679
|
+
leave-to-class="opacity-0"
|
|
680
|
+
>
|
|
681
|
+
<div
|
|
682
|
+
v-if="showControls && images.length > 1"
|
|
683
|
+
class="absolute right-0 z-40 h-full flex items-center px-2 md:px-4"
|
|
684
|
+
:class="{ 'lg:mr-64': sidePanel }"
|
|
685
|
+
>
|
|
686
|
+
<button
|
|
687
|
+
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"
|
|
688
|
+
aria-label="Next image"
|
|
689
|
+
@click="goNextImage()"
|
|
614
690
|
>
|
|
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>
|
|
691
|
+
<ChevronRightIcon class="w-6 h-6 md:w-8 md:h-8" />
|
|
692
|
+
</button>
|
|
628
693
|
</div>
|
|
629
|
-
</
|
|
694
|
+
</transition>
|
|
630
695
|
|
|
631
|
-
<!--
|
|
696
|
+
<!-- Info Panel Below Image -->
|
|
632
697
|
<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-
|
|
698
|
+
enter-active-class="transition-all duration-300 ease-out"
|
|
699
|
+
enter-from-class="opacity-0 transform translate-y-4"
|
|
700
|
+
enter-to-class="opacity-100 transform translate-y-0"
|
|
701
|
+
leave-active-class="transition-all duration-300 ease-in"
|
|
702
|
+
leave-from-class="opacity-100 transform translate-y-0"
|
|
703
|
+
leave-to-class="opacity-0 transform translate-y-4"
|
|
639
704
|
>
|
|
640
705
|
<div
|
|
641
|
-
v-if="
|
|
642
|
-
|
|
643
|
-
|
|
706
|
+
v-if="infoPanel && images[modelValue]"
|
|
707
|
+
ref="infoPanelRef"
|
|
708
|
+
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
709
|
>
|
|
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>
|
|
710
|
+
<slot :value="images[modelValue]" />
|
|
695
711
|
</div>
|
|
696
712
|
</transition>
|
|
697
713
|
</div>
|
|
698
714
|
|
|
699
|
-
<!--
|
|
715
|
+
<!-- Side Thumbnails Panel -->
|
|
700
716
|
<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="
|
|
717
|
+
enter-active-class="transform transition ease-in-out duration-300"
|
|
718
|
+
enter-from-class="translate-x-full"
|
|
719
|
+
enter-to-class="translate-x-0"
|
|
720
|
+
leave-active-class="transform transition ease-in-out duration-300"
|
|
721
|
+
leave-from-class="translate-x-0"
|
|
722
|
+
leave-to-class="translate-x-full"
|
|
707
723
|
>
|
|
708
724
|
<div
|
|
709
|
-
v-if="
|
|
710
|
-
|
|
725
|
+
v-if="sidePanel"
|
|
726
|
+
ref="sidePanelRef"
|
|
727
|
+
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 cool-scroll"
|
|
728
|
+
:style="{ 'padding-top': `${topControlsHeight}px` }"
|
|
711
729
|
>
|
|
712
|
-
<!--
|
|
713
|
-
<div class="flex items-center
|
|
714
|
-
<
|
|
715
|
-
<span class="text-sm opacity-80">{{ currentIndex }} / {{ imageCount }}</span>
|
|
730
|
+
<!-- Paging Controls if needed -->
|
|
731
|
+
<div v-if="paging" class="flex items-center justify-center pt-2">
|
|
732
|
+
<DefaultPaging :id="id" :items="paging" />
|
|
716
733
|
</div>
|
|
717
734
|
|
|
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)"
|
|
735
|
+
<!-- Thumbnail Grid -->
|
|
736
|
+
<div class="grid grid-cols-2 gap-2 p-2">
|
|
737
|
+
<div
|
|
738
|
+
v-for="i in images.length"
|
|
739
|
+
:key="`bg_${id}_${i}`"
|
|
740
|
+
class="group relative"
|
|
751
741
|
>
|
|
752
|
-
<
|
|
753
|
-
|
|
742
|
+
<div
|
|
743
|
+
class="absolute inset-0 rounded-lg transition-colors duration-300 group-hover:bg-fv-neutral-700/40"
|
|
744
|
+
:class="{ 'bg-fv-primary-500/40': i - 1 === modelValue }"
|
|
745
|
+
/>
|
|
746
|
+
<img
|
|
747
|
+
v-if="imageComponent === 'img'"
|
|
748
|
+
:class="`h-auto max-w-full rounded-lg cursor-pointer shadow transition-all duration-300 group-hover:brightness-110 ${getBorderColor(
|
|
749
|
+
images[i - 1],
|
|
750
|
+
)}`"
|
|
751
|
+
:style="{
|
|
752
|
+
filter:
|
|
753
|
+
i - 1 === modelValue ? 'brightness(1)' : 'brightness(0.7)',
|
|
754
|
+
}"
|
|
755
|
+
:src="getThumbnailUrl(images[i - 1])"
|
|
756
|
+
:alt="`Thumbnail ${i}`"
|
|
757
|
+
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
758
|
+
>
|
|
759
|
+
<component
|
|
760
|
+
:is="imageComponent"
|
|
761
|
+
v-else
|
|
762
|
+
:image="getThumbnailUrl(images[i - 1]).image"
|
|
763
|
+
:variant="getThumbnailUrl(images[i - 1]).variant"
|
|
764
|
+
:alt="getThumbnailUrl(images[i - 1]).alt"
|
|
765
|
+
:class="`h-auto max-w-full rounded-lg cursor-pointer shadow transition-all duration-300 group-hover:brightness-110 ${getBorderColor(
|
|
766
|
+
images[i - 1],
|
|
767
|
+
)}`"
|
|
768
|
+
:style="{
|
|
769
|
+
filter:
|
|
770
|
+
i - 1 === modelValue ? 'brightness(1)' : 'brightness(0.7)',
|
|
771
|
+
}"
|
|
772
|
+
:likes="getThumbnailUrl(images[i - 1]).likes"
|
|
773
|
+
:show-likes="getThumbnailUrl(images[i - 1]).showLikes"
|
|
774
|
+
:is-author="getThumbnailUrl(images[i - 1]).isAuthor"
|
|
775
|
+
:user-uuid="getThumbnailUrl(images[i - 1]).userUUID"
|
|
776
|
+
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
777
|
+
/>
|
|
778
|
+
</div>
|
|
754
779
|
</div>
|
|
755
780
|
</div>
|
|
756
781
|
</transition>
|
|
757
782
|
|
|
758
|
-
<!-- Mobile Thumbnail Preview -->
|
|
783
|
+
<!-- Mobile Thumbnail Preview (bottom of screen on mobile) -->
|
|
759
784
|
<transition
|
|
760
785
|
enter-active-class="transition-transform duration-300 ease-out"
|
|
761
786
|
enter-from-class="translate-y-full"
|
|
@@ -766,7 +791,8 @@ onUnmounted(() => {
|
|
|
766
791
|
>
|
|
767
792
|
<div
|
|
768
793
|
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-
|
|
794
|
+
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"
|
|
795
|
+
:class="{ 'pb-20': infoPanel }"
|
|
770
796
|
>
|
|
771
797
|
<div class="overflow-x-auto flex space-x-2 pb-1 px-1">
|
|
772
798
|
<div
|
|
@@ -804,7 +830,7 @@ onUnmounted(() => {
|
|
|
804
830
|
</div>
|
|
805
831
|
</transition>
|
|
806
832
|
|
|
807
|
-
<!-- Thumbnail Grid/Mason/Custom Layouts -->
|
|
833
|
+
<!-- Thumbnail Grid/Mason/Custom Layouts for non-opened gallery -->
|
|
808
834
|
<div v-if="mode === 'grid' || mode === 'mason' || mode === 'custom'" class="gallery-grid">
|
|
809
835
|
<div
|
|
810
836
|
:class="{
|
|
@@ -898,13 +924,41 @@ onUnmounted(() => {
|
|
|
898
924
|
</template>
|
|
899
925
|
|
|
900
926
|
<style scoped>
|
|
901
|
-
/*
|
|
902
|
-
.
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
927
|
+
/* Ensure controls stay fixed at top */
|
|
928
|
+
.controls-bar {
|
|
929
|
+
height: auto;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/* Layout container for main image and info panel */
|
|
933
|
+
.image-container {
|
|
934
|
+
position: relative;
|
|
935
|
+
display: flex;
|
|
936
|
+
flex-direction: column;
|
|
937
|
+
justify-content: center;
|
|
938
|
+
align-items: center;
|
|
939
|
+
height: 100%;
|
|
940
|
+
width: 100%;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/* Side panel positioning */
|
|
944
|
+
.side-panel {
|
|
945
|
+
height: 100vh;
|
|
946
|
+
overflow-y: auto;
|
|
947
|
+
overflow-x: hidden;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/* Info panel styling */
|
|
951
|
+
.info-panel {
|
|
952
|
+
width: 100%;
|
|
953
|
+
border-top-left-radius: 0.5rem;
|
|
954
|
+
border-top-right-radius: 0.5rem;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/* Image sizing in different contexts */
|
|
958
|
+
.image-display img,
|
|
959
|
+
.image-display .video-component {
|
|
960
|
+
transition: max-height 0.3s ease-out, max-width 0.3s ease-out;
|
|
961
|
+
object-fit: contain;
|
|
908
962
|
}
|
|
909
963
|
|
|
910
964
|
/* Transition styles for next (right) navigation */
|
|
@@ -918,8 +972,8 @@ onUnmounted(() => {
|
|
|
918
972
|
|
|
919
973
|
.slide-next-enter-from {
|
|
920
974
|
opacity: 0;
|
|
921
|
-
transform: translateX(
|
|
922
|
-
filter: blur(
|
|
975
|
+
transform: translateX(30px);
|
|
976
|
+
filter: blur(8px);
|
|
923
977
|
}
|
|
924
978
|
|
|
925
979
|
.slide-next-enter-to {
|
|
@@ -936,8 +990,8 @@ onUnmounted(() => {
|
|
|
936
990
|
|
|
937
991
|
.slide-next-leave-to {
|
|
938
992
|
opacity: 0;
|
|
939
|
-
transform: translateX(-
|
|
940
|
-
filter: blur(
|
|
993
|
+
transform: translateX(-30px);
|
|
994
|
+
filter: blur(8px);
|
|
941
995
|
}
|
|
942
996
|
|
|
943
997
|
/* Transition styles for prev (left) navigation */
|
|
@@ -951,8 +1005,8 @@ onUnmounted(() => {
|
|
|
951
1005
|
|
|
952
1006
|
.slide-prev-enter-from {
|
|
953
1007
|
opacity: 0;
|
|
954
|
-
transform: translateX(-
|
|
955
|
-
filter: blur(
|
|
1008
|
+
transform: translateX(-30px);
|
|
1009
|
+
filter: blur(8px);
|
|
956
1010
|
}
|
|
957
1011
|
|
|
958
1012
|
.slide-prev-enter-to {
|
|
@@ -969,11 +1023,11 @@ onUnmounted(() => {
|
|
|
969
1023
|
|
|
970
1024
|
.slide-prev-leave-to {
|
|
971
1025
|
opacity: 0;
|
|
972
|
-
transform: translateX(
|
|
973
|
-
filter: blur(
|
|
1026
|
+
transform: translateX(30px);
|
|
1027
|
+
filter: blur(8px);
|
|
974
1028
|
}
|
|
975
1029
|
|
|
976
|
-
/*
|
|
1030
|
+
/* Grid layouts for thumbnails */
|
|
977
1031
|
.gallery-grid {
|
|
978
1032
|
min-height: 200px;
|
|
979
1033
|
}
|