@fy-/fws-vue 2.3.22 → 2.3.24
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.
|
@@ -137,8 +137,18 @@ const updateImageSizes = useDebounceFn(() => {
|
|
|
137
137
|
|
|
138
138
|
// Simple, direct calculation of available space
|
|
139
139
|
const topHeight = topControlsRef.value?.offsetHeight || 0
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
|
|
141
|
+
// For desktop, reserve more space at the bottom for the info panel
|
|
142
|
+
// This prevents overlap with the bottom panel
|
|
143
|
+
const bottomPadding = windowWidth.value > 768 ? 100 : 32
|
|
144
|
+
|
|
145
|
+
// Account for info panel only on mobile (where it overlays in the middle)
|
|
146
|
+
// On desktop, we use the bottomPadding to prevent overlap
|
|
147
|
+
const infoHeight = (windowWidth.value <= 768 && infoPanel.value && infoPanelRef.value)
|
|
148
|
+
? infoPanelRef.value.offsetHeight
|
|
149
|
+
: 0
|
|
150
|
+
|
|
151
|
+
const availableHeight = windowHeight.value - topHeight - infoHeight - bottomPadding
|
|
142
152
|
|
|
143
153
|
// Apply size directly to fill available space
|
|
144
154
|
mainImage.style.maxHeight = `${availableHeight}px`
|
|
@@ -162,12 +172,7 @@ function setModal(value: boolean) {
|
|
|
162
172
|
useEventListener(document, 'keyup', handleKeyboardRelease)
|
|
163
173
|
}
|
|
164
174
|
|
|
165
|
-
//
|
|
166
|
-
if (windowWidth.value < 1024) {
|
|
167
|
-
controlsTimeout = window.setTimeout(() => {
|
|
168
|
-
showControls.value = false
|
|
169
|
-
}, 3000)
|
|
170
|
-
}
|
|
175
|
+
// No longer auto-hide controls on mobile
|
|
171
176
|
}
|
|
172
177
|
else {
|
|
173
178
|
if (props.onClose) props.onClose()
|
|
@@ -243,25 +248,13 @@ function goPrevImage() {
|
|
|
243
248
|
|
|
244
249
|
// UI control functions
|
|
245
250
|
function resetControlsTimer() {
|
|
246
|
-
//
|
|
251
|
+
// Always show controls - no auto-hide
|
|
247
252
|
showControls.value = true
|
|
248
|
-
|
|
249
|
-
// Only set timer on mobile
|
|
250
|
-
if (windowWidth.value < 1024) {
|
|
251
|
-
if (controlsTimeout) {
|
|
252
|
-
clearTimeout(controlsTimeout)
|
|
253
|
-
}
|
|
254
|
-
controlsTimeout = window.setTimeout(() => {
|
|
255
|
-
showControls.value = false
|
|
256
|
-
}, 3000)
|
|
257
|
-
}
|
|
258
253
|
}
|
|
259
254
|
|
|
255
|
+
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
260
256
|
function toggleControls() {
|
|
261
257
|
showControls.value = !showControls.value
|
|
262
|
-
if (showControls.value && windowWidth.value < 1024) {
|
|
263
|
-
resetControlsTimer()
|
|
264
|
-
}
|
|
265
258
|
}
|
|
266
259
|
|
|
267
260
|
function toggleInfoPanel() {
|
|
@@ -345,9 +338,8 @@ const touchEnd = useDebounceFn((event: TouchEvent) => {
|
|
|
345
338
|
const diffX = start.x - end.x
|
|
346
339
|
const diffY = start.y - end.y
|
|
347
340
|
|
|
348
|
-
//
|
|
341
|
+
// For taps, we don't toggle controls anymore - they always stay visible
|
|
349
342
|
if (Math.abs(diffX) < 10 && Math.abs(diffY) < 10 && touchDuration < 300) {
|
|
350
|
-
toggleControls()
|
|
351
343
|
return
|
|
352
344
|
}
|
|
353
345
|
|
|
@@ -637,10 +629,6 @@ onUnmounted(() => {
|
|
|
637
629
|
:variant="modelValueSrc.variant"
|
|
638
630
|
:alt="modelValueSrc.alt"
|
|
639
631
|
class="shadow max-w-full h-auto object-contain"
|
|
640
|
-
:likes="modelValueSrc.likes"
|
|
641
|
-
:show-likes="modelValueSrc.showLikes"
|
|
642
|
-
:is-author="modelValueSrc.isAuthor"
|
|
643
|
-
:user-uuid="modelValueSrc.userUUID"
|
|
644
632
|
/>
|
|
645
633
|
</template>
|
|
646
634
|
</div>
|
package/package.json
CHANGED
|
@@ -1,1073 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { Component } from 'vue'
|
|
3
|
-
import type { APIPaging } from '../../composables/rest'
|
|
4
|
-
import {
|
|
5
|
-
ChevronDoubleLeftIcon,
|
|
6
|
-
ChevronDoubleRightIcon,
|
|
7
|
-
ChevronLeftIcon,
|
|
8
|
-
ChevronRightIcon,
|
|
9
|
-
InformationCircleIcon,
|
|
10
|
-
XMarkIcon,
|
|
11
|
-
} from '@heroicons/vue/24/solid'
|
|
12
|
-
import { useDebounceFn, useEventListener, useFullscreen, useResizeObserver } from '@vueuse/core'
|
|
13
|
-
import { computed, h, nextTick, onMounted, onUnmounted, reactive, ref, shallowRef, watch } from 'vue'
|
|
14
|
-
import { useEventBus } from '../../composables/event-bus'
|
|
15
|
-
import DefaultPaging from './DefaultPaging.vue'
|
|
16
|
-
|
|
17
|
-
const isGalleryOpen = ref<boolean>(false)
|
|
18
|
-
const eventBus = useEventBus()
|
|
19
|
-
const sidePanel = ref<boolean>(true)
|
|
20
|
-
const showControls = ref<boolean>(true)
|
|
21
|
-
const isFullscreen = ref<boolean>(false)
|
|
22
|
-
const infoPanel = ref<boolean>(true) // Show info panel by default
|
|
23
|
-
const touchStartTime = ref<number>(0)
|
|
24
|
-
const infoHeight = ref<number>(0)
|
|
25
|
-
const galleryContainerRef = shallowRef<HTMLElement | null>(null)
|
|
26
|
-
|
|
27
|
-
// Use VueUse's useFullscreen for better fullscreen handling
|
|
28
|
-
const { isFullscreen: isElementFullscreen, enter: enterFullscreen, exit: exitFullscreen } = useFullscreen(galleryContainerRef)
|
|
29
|
-
|
|
30
|
-
// Track when fullscreen changes externally (like Escape key)
|
|
31
|
-
watch(isElementFullscreen, (newValue) => {
|
|
32
|
-
isFullscreen.value = newValue
|
|
33
|
-
if (newValue) {
|
|
34
|
-
// Force update of image size when entering fullscreen
|
|
35
|
-
nextTick(() => {
|
|
36
|
-
updateImageSizes()
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
const props = withDefaults(
|
|
42
|
-
defineProps<{
|
|
43
|
-
id: string
|
|
44
|
-
images: Array<any>
|
|
45
|
-
title?: string
|
|
46
|
-
getImageUrl?: Function
|
|
47
|
-
getThumbnailUrl?: Function
|
|
48
|
-
onOpen?: Function
|
|
49
|
-
onClose?: Function
|
|
50
|
-
closeIcon?: object
|
|
51
|
-
gridHeight?: number
|
|
52
|
-
mode: 'mason' | 'grid' | 'button' | 'hidden' | 'custom'
|
|
53
|
-
paging?: APIPaging | undefined
|
|
54
|
-
buttonText?: string
|
|
55
|
-
buttonType?: string
|
|
56
|
-
modelValue: number
|
|
57
|
-
borderColor?: Function
|
|
58
|
-
imageLoader: string
|
|
59
|
-
videoComponent?: Component | string
|
|
60
|
-
imageComponent?: Component | string
|
|
61
|
-
isVideo?: Function
|
|
62
|
-
ranking?: boolean
|
|
63
|
-
}>(),
|
|
64
|
-
{
|
|
65
|
-
modelValue: 0,
|
|
66
|
-
imageComponent: 'img',
|
|
67
|
-
mode: 'grid',
|
|
68
|
-
gridHeight: 4,
|
|
69
|
-
closeIcon: () => h(XMarkIcon),
|
|
70
|
-
images: () => [],
|
|
71
|
-
isVideo: () => false,
|
|
72
|
-
getImageUrl: (image: any) => image.image_url,
|
|
73
|
-
getThumbnailUrl: (image: any) => `${image.image_url}?s=250x250&m=autocrop`,
|
|
74
|
-
paging: undefined,
|
|
75
|
-
borderColor: undefined,
|
|
76
|
-
ranking: false,
|
|
77
|
-
},
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
const emit = defineEmits(['update:modelValue'])
|
|
81
|
-
const modelValue = computed({
|
|
82
|
-
get: () => props.modelValue,
|
|
83
|
-
set: (i) => {
|
|
84
|
-
emit('update:modelValue', i)
|
|
85
|
-
},
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
const direction = ref<'next' | 'prev'>('next')
|
|
89
|
-
|
|
90
|
-
let controlsTimeout: number | null = null
|
|
91
|
-
let fullscreenResizeTimeout: number | null = null
|
|
92
|
-
|
|
93
|
-
// Used to maintain consistent image sizes
|
|
94
|
-
function updateImageSizes() {
|
|
95
|
-
nextTick(() => {
|
|
96
|
-
const imageContainers = document.querySelectorAll('.image-container img, .image-container .video-component') as NodeListOf<HTMLElement>
|
|
97
|
-
if (imageContainers && imageContainers.length > 0) {
|
|
98
|
-
imageContainers.forEach((img) => {
|
|
99
|
-
// Force a reflow to ensure correct sizing
|
|
100
|
-
img.style.maxHeight = ''
|
|
101
|
-
// Force browser to recalculate styles
|
|
102
|
-
void img.offsetHeight
|
|
103
|
-
|
|
104
|
-
// Calculate the correct height based on fullscreen and info panel state
|
|
105
|
-
const topHeight = 4 // rem
|
|
106
|
-
const infoHeight = infoPanel.value ? Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--info-height') || '0px') / 16 : 0
|
|
107
|
-
const viewHeight = isFullscreen.value ? 90 : 80
|
|
108
|
-
|
|
109
|
-
// Set explicit height with important to override any other styles
|
|
110
|
-
img.style.maxHeight = `calc(${viewHeight}vh - ${infoHeight}rem - ${topHeight}rem) !important`
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
})
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function setModal(value: boolean) {
|
|
117
|
-
if (value === true) {
|
|
118
|
-
if (props.onOpen) props.onOpen()
|
|
119
|
-
document.body.style.overflow = 'hidden' // Prevent scrolling when gallery is open
|
|
120
|
-
if (!import.meta.env.SSR) {
|
|
121
|
-
useEventListener(document, 'keydown', handleKeyboardInput)
|
|
122
|
-
useEventListener(document, 'keyup', handleKeyboardRelease)
|
|
123
|
-
}
|
|
124
|
-
// Auto-hide controls after 3 seconds on mobile
|
|
125
|
-
if (window.innerWidth < 1024) {
|
|
126
|
-
controlsTimeout = window.setTimeout(() => {
|
|
127
|
-
showControls.value = false
|
|
128
|
-
}, 3000)
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
if (props.onClose) props.onClose()
|
|
133
|
-
document.body.style.overflow = '' // Restore scrolling
|
|
134
|
-
// Exit fullscreen if active
|
|
135
|
-
if (isFullscreen.value) {
|
|
136
|
-
exitFullscreen()
|
|
137
|
-
isFullscreen.value = false
|
|
138
|
-
}
|
|
139
|
-
// Clear timeout if modal is closed
|
|
140
|
-
if (controlsTimeout) {
|
|
141
|
-
clearTimeout(controlsTimeout)
|
|
142
|
-
controlsTimeout = null
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
isGalleryOpen.value = value
|
|
146
|
-
showControls.value = true
|
|
147
|
-
// Don't reset info panel state when opening/closing
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const openGalleryImage = useDebounceFn((index: number | undefined) => {
|
|
151
|
-
if (index === undefined) {
|
|
152
|
-
modelValue.value = 0
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
modelValue.value = Number.parseInt(index.toString())
|
|
156
|
-
}
|
|
157
|
-
setModal(true)
|
|
158
|
-
}, 50) // Debounce to prevent accidental double-opens
|
|
159
|
-
|
|
160
|
-
function goNextImage() {
|
|
161
|
-
direction.value = 'next'
|
|
162
|
-
if (modelValue.value < props.images.length - 1) {
|
|
163
|
-
modelValue.value++
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
modelValue.value = 0
|
|
167
|
-
}
|
|
168
|
-
resetControlsTimer()
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function goPrevImage() {
|
|
172
|
-
direction.value = 'prev'
|
|
173
|
-
if (modelValue.value > 0) {
|
|
174
|
-
modelValue.value--
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
modelValue.value
|
|
178
|
-
= props.images.length - 1 > 0 ? props.images.length - 1 : 0
|
|
179
|
-
}
|
|
180
|
-
resetControlsTimer()
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const modelValueSrc = computed(() => {
|
|
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
|
-
|
|
199
|
-
function resetControlsTimer() {
|
|
200
|
-
// Show controls when user interacts
|
|
201
|
-
showControls.value = true
|
|
202
|
-
|
|
203
|
-
// Only set timer on mobile
|
|
204
|
-
if (window.innerWidth < 1024) {
|
|
205
|
-
if (controlsTimeout) {
|
|
206
|
-
clearTimeout(controlsTimeout)
|
|
207
|
-
}
|
|
208
|
-
controlsTimeout = window.setTimeout(() => {
|
|
209
|
-
showControls.value = false
|
|
210
|
-
}, 3000)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function toggleControls() {
|
|
215
|
-
showControls.value = !showControls.value
|
|
216
|
-
if (showControls.value && window.innerWidth < 1024) {
|
|
217
|
-
resetControlsTimer()
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function toggleInfoPanel() {
|
|
222
|
-
infoPanel.value = !infoPanel.value
|
|
223
|
-
resetControlsTimer()
|
|
224
|
-
|
|
225
|
-
// Update the info height after panel toggle
|
|
226
|
-
if (infoPanel.value) {
|
|
227
|
-
nextTick(() => {
|
|
228
|
-
updateInfoHeight()
|
|
229
|
-
})
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
// Reset when hiding
|
|
233
|
-
document.documentElement.style.setProperty('--info-height', '0px')
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function toggleFullscreen() {
|
|
238
|
-
if (!isFullscreen.value) {
|
|
239
|
-
if (galleryContainerRef.value) {
|
|
240
|
-
enterFullscreen()
|
|
241
|
-
.then(() => {
|
|
242
|
-
isFullscreen.value = true
|
|
243
|
-
// Give browser time to adjust fullscreen before updating sizing
|
|
244
|
-
if (fullscreenResizeTimeout) clearTimeout(fullscreenResizeTimeout)
|
|
245
|
-
fullscreenResizeTimeout = window.setTimeout(() => {
|
|
246
|
-
updateImageSizes()
|
|
247
|
-
}, 50)
|
|
248
|
-
})
|
|
249
|
-
.catch(() => {})
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
exitFullscreen()
|
|
254
|
-
.then(() => {
|
|
255
|
-
isFullscreen.value = false
|
|
256
|
-
})
|
|
257
|
-
.catch(() => {})
|
|
258
|
-
}
|
|
259
|
-
resetControlsTimer()
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Touch handling with debounce to prevent multiple rapid changes
|
|
263
|
-
const touchStart = useDebounceFn((event: TouchEvent) => {
|
|
264
|
-
const touch = event.touches[0]
|
|
265
|
-
const targetElement = touch.target as HTMLElement
|
|
266
|
-
|
|
267
|
-
// Store start time for tap detection
|
|
268
|
-
touchStartTime.value = Date.now()
|
|
269
|
-
|
|
270
|
-
// Check if the touch started on an interactive element
|
|
271
|
-
if (targetElement.closest('button, a, input, textarea, select')) {
|
|
272
|
-
return // Don't handle swipe if interacting with an interactive element
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
start.x = touch.screenX
|
|
276
|
-
start.y = touch.screenY
|
|
277
|
-
}, 50)
|
|
278
|
-
|
|
279
|
-
const touchEnd = useDebounceFn((event: TouchEvent) => {
|
|
280
|
-
const touch = event.changedTouches[0]
|
|
281
|
-
const targetElement = touch.target as HTMLElement
|
|
282
|
-
const touchDuration = Date.now() - touchStartTime.value
|
|
283
|
-
|
|
284
|
-
// Check if the touch ended on an interactive element
|
|
285
|
-
if (targetElement.closest('button, a, input, textarea, select')) {
|
|
286
|
-
return // Don't handle swipe if interacting with an interactive element
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const end = { x: touch.screenX, y: touch.screenY }
|
|
290
|
-
|
|
291
|
-
const diffX = start.x - end.x
|
|
292
|
-
const diffY = start.y - end.y
|
|
293
|
-
|
|
294
|
-
// Detect tap (quick touch with minimal movement)
|
|
295
|
-
if (Math.abs(diffX) < 10 && Math.abs(diffY) < 10 && touchDuration < 300) {
|
|
296
|
-
toggleControls()
|
|
297
|
-
return
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Add a threshold to prevent accidental swipes
|
|
301
|
-
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
|
|
302
|
-
if (diffX > 0) {
|
|
303
|
-
direction.value = 'next'
|
|
304
|
-
goNextImage()
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
direction.value = 'prev'
|
|
308
|
-
goPrevImage()
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}, 50)
|
|
312
|
-
|
|
313
|
-
function getBorderColor(i: any) {
|
|
314
|
-
if (props.borderColor !== undefined) {
|
|
315
|
-
return props.borderColor(i)
|
|
316
|
-
}
|
|
317
|
-
return ''
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const isKeyPressed = ref<boolean>(false)
|
|
321
|
-
|
|
322
|
-
function handleKeyboardInput(event: KeyboardEvent) {
|
|
323
|
-
if (!isGalleryOpen.value) return
|
|
324
|
-
if (isKeyPressed.value) return
|
|
325
|
-
|
|
326
|
-
switch (event.key) {
|
|
327
|
-
case 'Escape':
|
|
328
|
-
event.preventDefault()
|
|
329
|
-
setModal(false)
|
|
330
|
-
break
|
|
331
|
-
case 'ArrowRight':
|
|
332
|
-
isKeyPressed.value = true
|
|
333
|
-
direction.value = 'next'
|
|
334
|
-
goNextImage()
|
|
335
|
-
break
|
|
336
|
-
case 'ArrowLeft':
|
|
337
|
-
isKeyPressed.value = true
|
|
338
|
-
direction.value = 'prev'
|
|
339
|
-
goPrevImage()
|
|
340
|
-
break
|
|
341
|
-
case 'f':
|
|
342
|
-
toggleFullscreen()
|
|
343
|
-
break
|
|
344
|
-
case 'i':
|
|
345
|
-
toggleInfoPanel()
|
|
346
|
-
break
|
|
347
|
-
default:
|
|
348
|
-
break
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function handleKeyboardRelease(event: KeyboardEvent) {
|
|
353
|
-
if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
|
|
354
|
-
isKeyPressed.value = false
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function closeGallery() {
|
|
359
|
-
setModal(false)
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Click outside gallery content to close - with debounce to prevent accidental closes
|
|
363
|
-
const handleBackdropClick = useDebounceFn((event: MouseEvent) => {
|
|
364
|
-
if (event.target === event.currentTarget) {
|
|
365
|
-
setModal(false)
|
|
366
|
-
}
|
|
367
|
-
}, 200)
|
|
368
|
-
|
|
369
|
-
// Watch for both image changes and fullscreen mode changes
|
|
370
|
-
watch([currentImage, isFullscreen], () => {
|
|
371
|
-
// Update the info height when image changes or fullscreen state changes
|
|
372
|
-
if (infoPanel.value) {
|
|
373
|
-
nextTick(() => {
|
|
374
|
-
updateInfoHeight()
|
|
375
|
-
// Fix image sizing issue when navigating in fullscreen
|
|
376
|
-
if (isFullscreen.value) {
|
|
377
|
-
updateImageSizes()
|
|
378
|
-
}
|
|
379
|
-
})
|
|
380
|
-
}
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
// Update CSS variable with info panel height
|
|
384
|
-
function updateInfoHeight() {
|
|
385
|
-
nextTick(() => {
|
|
386
|
-
const infoElement = document.querySelector('.info-panel-slot') as HTMLElement
|
|
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)
|
|
402
|
-
|
|
403
|
-
onMounted(() => {
|
|
404
|
-
eventBus.on(`${props.id}GalleryImage`, openGalleryImage)
|
|
405
|
-
eventBus.on(`${props.id}Gallery`, openGalleryImage)
|
|
406
|
-
eventBus.on(`${props.id}GalleryClose`, closeGallery)
|
|
407
|
-
|
|
408
|
-
// Store reference to the gallery container
|
|
409
|
-
galleryContainerRef.value = document.querySelector('.gallery-container')
|
|
410
|
-
|
|
411
|
-
// Initialize info height once mounted (only if info panel is shown)
|
|
412
|
-
if (infoPanel.value) {
|
|
413
|
-
updateInfoHeight()
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Use vueUse's useResizeObserver instead of native ResizeObserver
|
|
417
|
-
const infoElement = document.querySelector('.info-panel-slot')
|
|
418
|
-
if (infoElement) {
|
|
419
|
-
useResizeObserver(infoElement as HTMLElement, () => {
|
|
420
|
-
if (infoPanel.value) {
|
|
421
|
-
updateInfoHeight()
|
|
422
|
-
}
|
|
423
|
-
})
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// Listen for window resize events
|
|
427
|
-
useEventListener(window, 'resize', handleWindowResize)
|
|
428
|
-
|
|
429
|
-
// Listen for fullscreen changes to update image sizes
|
|
430
|
-
useEventListener(document, 'fullscreenchange', () => {
|
|
431
|
-
if (document.fullscreenElement) {
|
|
432
|
-
// This handles the case of using F11 or browser fullscreen controls
|
|
433
|
-
isFullscreen.value = true
|
|
434
|
-
updateImageSizes()
|
|
435
|
-
}
|
|
436
|
-
else {
|
|
437
|
-
isFullscreen.value = false
|
|
438
|
-
}
|
|
439
|
-
})
|
|
440
|
-
})
|
|
441
|
-
|
|
442
|
-
onUnmounted(() => {
|
|
443
|
-
eventBus.off(`${props.id}Gallery`, openGalleryImage)
|
|
444
|
-
eventBus.off(`${props.id}GalleryImage`, openGalleryImage)
|
|
445
|
-
eventBus.off(`${props.id}GalleryClose`, closeGallery)
|
|
446
|
-
|
|
447
|
-
if (!import.meta.env.SSR) {
|
|
448
|
-
document.body.style.overflow = '' // Ensure body scrolling is restored
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Clear any remaining timeouts
|
|
452
|
-
if (controlsTimeout) {
|
|
453
|
-
clearTimeout(controlsTimeout)
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (fullscreenResizeTimeout) {
|
|
457
|
-
clearTimeout(fullscreenResizeTimeout)
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Ensure we exit fullscreen mode on unmount
|
|
461
|
-
if (isFullscreen.value) {
|
|
462
|
-
exitFullscreen().catch(() => {})
|
|
463
|
-
}
|
|
464
|
-
})
|
|
465
|
-
</script>
|
|
466
|
-
|
|
467
|
-
<template>
|
|
468
|
-
<div>
|
|
469
|
-
<transition
|
|
470
|
-
enter-active-class="duration-300 ease-out"
|
|
471
|
-
enter-from-class="opacity-0"
|
|
472
|
-
enter-to-class="opacity-100"
|
|
473
|
-
leave-active-class="duration-200 ease-in"
|
|
474
|
-
leave-from-class="opacity-100"
|
|
475
|
-
leave-to-class="opacity-0"
|
|
476
|
-
>
|
|
477
|
-
<div
|
|
478
|
-
v-if="isGalleryOpen"
|
|
479
|
-
class="fixed bg-fv-neutral-900 text-white inset-0 max-w-[100vw] overflow-hidden gallery-container"
|
|
480
|
-
style="z-index: 37"
|
|
481
|
-
role="dialog"
|
|
482
|
-
aria-modal="true"
|
|
483
|
-
@click="handleBackdropClick"
|
|
484
|
-
>
|
|
485
|
-
<div
|
|
486
|
-
class="relative w-full h-full max-w-full flex flex-col justify-center items-center"
|
|
487
|
-
style="z-index: 38"
|
|
488
|
-
@click.stop
|
|
489
|
-
>
|
|
490
|
-
<!-- Main Content Area -->
|
|
491
|
-
<div class="flex flex-grow gap-4 w-full h-full max-w-full">
|
|
492
|
-
<div class="flex-grow h-full flex items-center relative">
|
|
493
|
-
<!-- Image Display Area -->
|
|
494
|
-
<div
|
|
495
|
-
class="flex h-full relative flex-grow items-center justify-center gap-2 z-[1]"
|
|
496
|
-
@touchstart="touchStart"
|
|
497
|
-
@touchend="touchEnd"
|
|
498
|
-
>
|
|
499
|
-
<!-- Image Navigation - Left -->
|
|
500
|
-
<transition
|
|
501
|
-
enter-active-class="transition-opacity duration-300"
|
|
502
|
-
enter-from-class="opacity-0"
|
|
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"
|
|
507
|
-
>
|
|
508
|
-
<div
|
|
509
|
-
v-if="showControls && images.length > 1"
|
|
510
|
-
class="absolute left-0 z-[40] h-full flex items-center px-2 md:px-4"
|
|
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>
|
|
521
|
-
|
|
522
|
-
<!-- Main Image Container -->
|
|
523
|
-
<div
|
|
524
|
-
class="flex-1 flex flex-col z-[2] items-center justify-center max-w-full lg:max-w-[calc(100vw - 256px)] relative"
|
|
525
|
-
style="padding-top: 1rem;"
|
|
526
|
-
>
|
|
527
|
-
<transition
|
|
528
|
-
:name="direction === 'next' ? 'slide-next' : 'slide-prev'"
|
|
529
|
-
mode="out-in"
|
|
530
|
-
>
|
|
531
|
-
<div
|
|
532
|
-
:key="`image-display-${modelValue}`"
|
|
533
|
-
class="flex-1 w-full max-w-full flex flex-col items-center justify-center absolute inset-0 z-[2]"
|
|
534
|
-
>
|
|
535
|
-
<div
|
|
536
|
-
class="flex-1 w-full max-w-full flex items-center justify-center image-container"
|
|
537
|
-
>
|
|
538
|
-
<template
|
|
539
|
-
v-if="videoComponent && isVideo(images[modelValue])"
|
|
540
|
-
>
|
|
541
|
-
<ClientOnly>
|
|
542
|
-
<component
|
|
543
|
-
:is="videoComponent"
|
|
544
|
-
:src="isVideo(images[modelValue])"
|
|
545
|
-
class="shadow max-w-full h-auto object-contain video-component"
|
|
546
|
-
:style="{
|
|
547
|
-
maxHeight: isFullscreen
|
|
548
|
-
? infoPanel
|
|
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>
|
|
604
|
-
</div>
|
|
605
|
-
|
|
606
|
-
<!-- Image Navigation - Right -->
|
|
607
|
-
<transition
|
|
608
|
-
enter-active-class="transition-opacity duration-300"
|
|
609
|
-
enter-from-class="opacity-0"
|
|
610
|
-
enter-to-class="opacity-100"
|
|
611
|
-
leave-active-class="transition-opacity duration-300"
|
|
612
|
-
leave-from-class="opacity-100"
|
|
613
|
-
leave-to-class="opacity-0"
|
|
614
|
-
>
|
|
615
|
-
<div
|
|
616
|
-
v-if="showControls && images.length > 1"
|
|
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>
|
|
628
|
-
</div>
|
|
629
|
-
</div>
|
|
630
|
-
|
|
631
|
-
<!-- Side Panel for Thumbnails -->
|
|
632
|
-
<transition
|
|
633
|
-
enter-active-class="transform transition ease-in-out duration-300"
|
|
634
|
-
enter-from-class="translate-x-full"
|
|
635
|
-
enter-to-class="translate-x-0"
|
|
636
|
-
leave-active-class="transform transition ease-in-out duration-300"
|
|
637
|
-
leave-from-class="translate-x-0"
|
|
638
|
-
leave-to-class="translate-x-full"
|
|
639
|
-
>
|
|
640
|
-
<div
|
|
641
|
-
v-if="sidePanel"
|
|
642
|
-
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"
|
|
643
|
-
style="padding-top: 4rem;"
|
|
644
|
-
>
|
|
645
|
-
<!-- Paging Controls -->
|
|
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>
|
|
695
|
-
</div>
|
|
696
|
-
</transition>
|
|
697
|
-
</div>
|
|
698
|
-
|
|
699
|
-
<!-- Top Controls -->
|
|
700
|
-
<transition
|
|
701
|
-
enter-active-class="transition-opacity duration-300"
|
|
702
|
-
enter-from-class="opacity-0"
|
|
703
|
-
enter-to-class="opacity-100"
|
|
704
|
-
leave-active-class="transition-opacity duration-300"
|
|
705
|
-
leave-from-class="opacity-100"
|
|
706
|
-
leave-to-class="opacity-0"
|
|
707
|
-
>
|
|
708
|
-
<div
|
|
709
|
-
v-if="showControls"
|
|
710
|
-
class="fixed 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 h-16"
|
|
711
|
-
>
|
|
712
|
-
<!-- Title and Counter -->
|
|
713
|
-
<div class="flex items-center space-x-2">
|
|
714
|
-
<span v-if="title" class="font-medium text-lg">{{ title }}</span>
|
|
715
|
-
<span class="text-sm opacity-80">{{ currentIndex }} / {{ imageCount }}</span>
|
|
716
|
-
</div>
|
|
717
|
-
|
|
718
|
-
<!-- Control Buttons -->
|
|
719
|
-
<div class="flex items-center space-x-2">
|
|
720
|
-
<button
|
|
721
|
-
class="btn p-1.5 rounded-full bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
|
|
722
|
-
:class="{ 'bg-fv-primary-500/70': infoPanel }"
|
|
723
|
-
:title="infoPanel ? 'Hide info' : 'Show info'"
|
|
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)"
|
|
751
|
-
>
|
|
752
|
-
<component :is="closeIcon" class="w-5 h-5" />
|
|
753
|
-
</button>
|
|
754
|
-
</div>
|
|
755
|
-
</div>
|
|
756
|
-
</transition>
|
|
757
|
-
|
|
758
|
-
<!-- Mobile Thumbnail Preview -->
|
|
759
|
-
<transition
|
|
760
|
-
enter-active-class="transition-transform duration-300 ease-out"
|
|
761
|
-
enter-from-class="translate-y-full"
|
|
762
|
-
enter-to-class="translate-y-0"
|
|
763
|
-
leave-active-class="transition-transform duration-300 ease-in"
|
|
764
|
-
leave-from-class="translate-y-0"
|
|
765
|
-
leave-to-class="translate-y-full"
|
|
766
|
-
>
|
|
767
|
-
<div
|
|
768
|
-
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-[50]"
|
|
770
|
-
>
|
|
771
|
-
<div class="overflow-x-auto flex space-x-2 pb-1 px-1">
|
|
772
|
-
<div
|
|
773
|
-
v-for="(image, idx) in images"
|
|
774
|
-
:key="`mobile_thumb_${id}_${idx}`"
|
|
775
|
-
class="flex-shrink-0 w-16 h-16 rounded-lg relative cursor-pointer"
|
|
776
|
-
:class="{ 'ring-2 ring-fv-primary-500 ring-offset-1 ring-offset-fv-neutral-900': idx === modelValue }"
|
|
777
|
-
@click="$eventBus.emit(`${id}GalleryImage`, idx)"
|
|
778
|
-
>
|
|
779
|
-
<img
|
|
780
|
-
v-if="imageComponent === 'img'"
|
|
781
|
-
class="w-full h-full object-cover rounded-lg transition duration-200"
|
|
782
|
-
:style="{
|
|
783
|
-
filter: idx === modelValue ? 'brightness(1)' : 'brightness(0.7)',
|
|
784
|
-
}"
|
|
785
|
-
:src="getThumbnailUrl(image)"
|
|
786
|
-
:alt="`Thumbnail ${idx + 1}`"
|
|
787
|
-
>
|
|
788
|
-
<component
|
|
789
|
-
:is="imageComponent"
|
|
790
|
-
v-else
|
|
791
|
-
:image="getThumbnailUrl(image).image"
|
|
792
|
-
:variant="getThumbnailUrl(image).variant"
|
|
793
|
-
:alt="getThumbnailUrl(image).alt"
|
|
794
|
-
class="w-full h-full object-cover rounded-lg transition duration-200"
|
|
795
|
-
:style="{
|
|
796
|
-
filter: idx === modelValue ? 'brightness(1)' : 'brightness(0.7)',
|
|
797
|
-
}"
|
|
798
|
-
/>
|
|
799
|
-
</div>
|
|
800
|
-
</div>
|
|
801
|
-
</div>
|
|
802
|
-
</transition>
|
|
803
|
-
</div>
|
|
804
|
-
</div>
|
|
805
|
-
</transition>
|
|
806
|
-
|
|
807
|
-
<!-- Thumbnail Grid/Mason/Custom Layouts -->
|
|
808
|
-
<div v-if="mode === 'grid' || mode === 'mason' || mode === 'custom'" class="gallery-grid">
|
|
809
|
-
<div
|
|
810
|
-
:class="{
|
|
811
|
-
'masonry-grid': mode === 'mason',
|
|
812
|
-
'standard-grid': mode === 'grid',
|
|
813
|
-
'custom-grid': mode === 'custom',
|
|
814
|
-
}"
|
|
815
|
-
>
|
|
816
|
-
<slot name="thumbnail" />
|
|
817
|
-
<template v-for="i in images.length" :key="`g_${id}_${i}`">
|
|
818
|
-
<template v-if="mode === 'mason'">
|
|
819
|
-
<div
|
|
820
|
-
v-if="i + (1 % gridHeight) === 0"
|
|
821
|
-
class="masonry-column relative"
|
|
822
|
-
>
|
|
823
|
-
<div v-if="ranking" class="img-gallery-ranking">
|
|
824
|
-
{{ i }}
|
|
825
|
-
</div>
|
|
826
|
-
|
|
827
|
-
<template v-for="j in gridHeight" :key="`gi_${id}_${i + j}`">
|
|
828
|
-
<div class="masonry-item">
|
|
829
|
-
<img
|
|
830
|
-
v-if="i + j - 2 < images.length && imageComponent === 'img'"
|
|
831
|
-
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]"
|
|
832
|
-
:src="getThumbnailUrl(images[i + j - 2])"
|
|
833
|
-
:alt="`Gallery image ${i + j - 1}`"
|
|
834
|
-
@click="$eventBus.emit(`${id}GalleryImage`, i + j - 2)"
|
|
835
|
-
>
|
|
836
|
-
<component
|
|
837
|
-
:is="imageComponent"
|
|
838
|
-
v-else-if="i + j - 2 < images.length"
|
|
839
|
-
:image="getThumbnailUrl(images[i + j - 2]).image"
|
|
840
|
-
:variant="getThumbnailUrl(images[i + j - 2]).variant"
|
|
841
|
-
:alt="getThumbnailUrl(images[i + j - 2]).alt"
|
|
842
|
-
: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(
|
|
843
|
-
images[i + j - 2],
|
|
844
|
-
)}`"
|
|
845
|
-
:likes="getThumbnailUrl(images[i + j - 2]).likes"
|
|
846
|
-
:show-likes="getThumbnailUrl(images[i + j - 2]).showLikes"
|
|
847
|
-
:is-author="getThumbnailUrl(images[i + j - 2]).isAuthor"
|
|
848
|
-
:user-uuid="getThumbnailUrl(images[i + j - 2]).userUUID"
|
|
849
|
-
@click="$eventBus.emit(`${id}GalleryImage`, i + j - 2)"
|
|
850
|
-
/>
|
|
851
|
-
</div>
|
|
852
|
-
</template>
|
|
853
|
-
</div>
|
|
854
|
-
</template>
|
|
855
|
-
<div v-else class="grid-item relative group">
|
|
856
|
-
<div v-if="ranking" class="img-gallery-ranking">
|
|
857
|
-
{{ i }}
|
|
858
|
-
</div>
|
|
859
|
-
<div class="overflow-hidden rounded-lg">
|
|
860
|
-
<img
|
|
861
|
-
v-if="imageComponent === 'img'"
|
|
862
|
-
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]"
|
|
863
|
-
:src="getThumbnailUrl(images[i - 1])"
|
|
864
|
-
:alt="`Gallery image ${i}`"
|
|
865
|
-
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
866
|
-
>
|
|
867
|
-
<component
|
|
868
|
-
:is="imageComponent"
|
|
869
|
-
v-else-if="imageComponent"
|
|
870
|
-
:image="getThumbnailUrl(images[i - 1]).image"
|
|
871
|
-
:variant="getThumbnailUrl(images[i - 1]).variant"
|
|
872
|
-
:alt="getThumbnailUrl(images[i - 1]).alt"
|
|
873
|
-
: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(
|
|
874
|
-
images[i - 1],
|
|
875
|
-
)}`"
|
|
876
|
-
:likes="getThumbnailUrl(images[i - 1]).likes"
|
|
877
|
-
:show-likes="getThumbnailUrl(images[i - 1]).showLikes"
|
|
878
|
-
:is-author="getThumbnailUrl(images[i - 1]).isAuthor"
|
|
879
|
-
:user-uuid="getThumbnailUrl(images[i - 1]).userUUID"
|
|
880
|
-
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
881
|
-
/>
|
|
882
|
-
</div>
|
|
883
|
-
</div>
|
|
884
|
-
</template>
|
|
885
|
-
</div>
|
|
886
|
-
</div>
|
|
887
|
-
|
|
888
|
-
<!-- Button Mode -->
|
|
889
|
-
<button
|
|
890
|
-
v-if="mode === 'button'"
|
|
891
|
-
:class="`btn ${buttonType ? buttonType : 'primary'} defaults relative overflow-hidden group`"
|
|
892
|
-
@click="openGalleryImage(0)"
|
|
893
|
-
>
|
|
894
|
-
<span class="relative z-10">{{ buttonText ? buttonText : $t("open_gallery_cta") }}</span>
|
|
895
|
-
<span class="absolute inset-0 bg-white/10 transform -translate-x-full group-hover:translate-x-0 transition-transform duration-300" />
|
|
896
|
-
</button>
|
|
897
|
-
</div>
|
|
898
|
-
</template>
|
|
899
|
-
|
|
900
|
-
<style scoped>
|
|
901
|
-
/* Fix for top controls to ensure they're always visible */
|
|
902
|
-
.fixed.top-0 {
|
|
903
|
-
position: fixed !important;
|
|
904
|
-
top: 0 !important;
|
|
905
|
-
left: 0 !important;
|
|
906
|
-
right: 0 !important;
|
|
907
|
-
z-index: 51 !important;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
/* Transition styles for next (right) navigation */
|
|
911
|
-
.slide-next-enter-active,
|
|
912
|
-
.slide-next-leave-active {
|
|
913
|
-
transition:
|
|
914
|
-
opacity 0.15s,
|
|
915
|
-
transform 0.15s,
|
|
916
|
-
filter 0.15s;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
.slide-next-enter-from {
|
|
920
|
-
opacity: 0;
|
|
921
|
-
transform: translateX(100%);
|
|
922
|
-
filter: blur(10px);
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
.slide-next-enter-to {
|
|
926
|
-
opacity: 1;
|
|
927
|
-
transform: translateX(0);
|
|
928
|
-
filter: blur(0);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
.slide-next-leave-from {
|
|
932
|
-
opacity: 1;
|
|
933
|
-
transform: translateX(0);
|
|
934
|
-
filter: blur(0);
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
.slide-next-leave-to {
|
|
938
|
-
opacity: 0;
|
|
939
|
-
transform: translateX(-100%);
|
|
940
|
-
filter: blur(10px);
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
/* Transition styles for prev (left) navigation */
|
|
944
|
-
.slide-prev-enter-active,
|
|
945
|
-
.slide-prev-leave-active {
|
|
946
|
-
transition:
|
|
947
|
-
opacity 0.15s,
|
|
948
|
-
transform 0.15s,
|
|
949
|
-
filter 0.15s;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
.slide-prev-enter-from {
|
|
953
|
-
opacity: 0;
|
|
954
|
-
transform: translateX(-100%);
|
|
955
|
-
filter: blur(10px);
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
.slide-prev-enter-to {
|
|
959
|
-
opacity: 1;
|
|
960
|
-
transform: translateX(0);
|
|
961
|
-
filter: blur(0);
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
.slide-prev-leave-from {
|
|
965
|
-
opacity: 1;
|
|
966
|
-
transform: translateX(0);
|
|
967
|
-
filter: blur(0);
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
.slide-prev-leave-to {
|
|
971
|
-
opacity: 0;
|
|
972
|
-
transform: translateX(100%);
|
|
973
|
-
filter: blur(10px);
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
/* Modern grids */
|
|
977
|
-
.gallery-grid {
|
|
978
|
-
min-height: 200px;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
.standard-grid {
|
|
982
|
-
display: grid;
|
|
983
|
-
grid-template-columns: repeat(1, 1fr);
|
|
984
|
-
gap: 0.75rem;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
@media (min-width: 480px) {
|
|
988
|
-
.standard-grid {
|
|
989
|
-
grid-template-columns: repeat(2, 1fr);
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
@media (min-width: 768px) {
|
|
994
|
-
.standard-grid {
|
|
995
|
-
grid-template-columns: repeat(3, 1fr);
|
|
996
|
-
gap: 1rem;
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
@media (min-width: 1024px) {
|
|
1001
|
-
.standard-grid {
|
|
1002
|
-
grid-template-columns: repeat(4, 1fr);
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
@media (min-width: 1280px) {
|
|
1007
|
-
.standard-grid {
|
|
1008
|
-
grid-template-columns: repeat(5, 1fr);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
@media (min-width: 1536px) {
|
|
1013
|
-
.standard-grid {
|
|
1014
|
-
grid-template-columns: repeat(6, 1fr);
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
.masonry-grid {
|
|
1019
|
-
display: grid;
|
|
1020
|
-
grid-template-columns: repeat(1, 1fr);
|
|
1021
|
-
gap: 0.75rem;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
@media (min-width: 480px) {
|
|
1025
|
-
.masonry-grid {
|
|
1026
|
-
grid-template-columns: repeat(2, 1fr);
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
@media (min-width: 768px) {
|
|
1031
|
-
.masonry-grid {
|
|
1032
|
-
grid-template-columns: repeat(3, 1fr);
|
|
1033
|
-
gap: 1rem;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
@media (min-width: 1024px) {
|
|
1038
|
-
.masonry-grid {
|
|
1039
|
-
grid-template-columns: repeat(4, 1fr);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
.masonry-column {
|
|
1044
|
-
display: grid;
|
|
1045
|
-
gap: 0.75rem;
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
.masonry-item {
|
|
1049
|
-
break-inside: avoid;
|
|
1050
|
-
margin-bottom: 0.75rem;
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
.grid-item {
|
|
1054
|
-
break-inside: avoid;
|
|
1055
|
-
margin-bottom: 0.75rem;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
.img-gallery-ranking {
|
|
1059
|
-
position: absolute;
|
|
1060
|
-
top: 0.5rem;
|
|
1061
|
-
left: 0.5rem;
|
|
1062
|
-
background-color: rgba(0, 0, 0, 0.6);
|
|
1063
|
-
color: white;
|
|
1064
|
-
width: 1.5rem;
|
|
1065
|
-
height: 1.5rem;
|
|
1066
|
-
display: flex;
|
|
1067
|
-
align-items: center;
|
|
1068
|
-
justify-content: center;
|
|
1069
|
-
border-radius: 9999px;
|
|
1070
|
-
font-size: 0.75rem;
|
|
1071
|
-
z-index: 10;
|
|
1072
|
-
}
|
|
1073
|
-
</style>
|