2023-01-04 11:15:24 +03:00
|
|
|
<script setup lang="ts">
|
|
|
|
import { SwipeDirection } from '@vueuse/core'
|
2023-02-04 21:05:55 +03:00
|
|
|
import { useGesture } from '@vueuse/gesture'
|
|
|
|
import type { PermissiveMotionProperties } from '@vueuse/motion'
|
2023-01-04 11:15:24 +03:00
|
|
|
import { useReducedMotion } from '@vueuse/motion'
|
2023-01-08 09:21:09 +03:00
|
|
|
import type { mastodon } from 'masto'
|
2023-01-04 11:15:24 +03:00
|
|
|
|
2023-01-06 18:46:36 +03:00
|
|
|
const { media = [], threshold = 20 } = defineProps<{
|
2023-01-08 09:21:09 +03:00
|
|
|
media?: mastodon.v1.MediaAttachment[]
|
2023-01-06 18:46:36 +03:00
|
|
|
threshold?: number
|
|
|
|
}>()
|
2023-01-04 11:15:24 +03:00
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
(event: 'close'): void
|
|
|
|
}>()
|
|
|
|
|
2023-01-06 18:46:36 +03:00
|
|
|
const { modelValue } = defineModel<{
|
|
|
|
modelValue: number
|
|
|
|
}>()
|
|
|
|
|
2023-01-04 11:15:24 +03:00
|
|
|
const target = ref()
|
|
|
|
|
|
|
|
const animateTimeout = useTimeout(10)
|
2023-02-06 12:34:50 +03:00
|
|
|
const reduceMotion = process.server ? ref(false) : useReducedMotion()
|
2023-01-04 11:15:24 +03:00
|
|
|
|
|
|
|
const canAnimate = computed(() => !reduceMotion.value && animateTimeout.value)
|
|
|
|
|
2023-02-04 21:05:55 +03:00
|
|
|
const { motionProperties } = useMotionProperties(target, {
|
|
|
|
cursor: 'grab',
|
|
|
|
scale: 1,
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
})
|
|
|
|
const { set } = useSpring(motionProperties as Partial<PermissiveMotionProperties>)
|
|
|
|
|
|
|
|
function resetZoom() {
|
|
|
|
set({ scale: 1 })
|
|
|
|
}
|
|
|
|
|
|
|
|
watch(modelValue, resetZoom)
|
|
|
|
|
2023-01-04 11:15:24 +03:00
|
|
|
const { width, height } = useElementSize(target)
|
|
|
|
const { isSwiping, lengthX, lengthY, direction } = useSwipe(target, {
|
|
|
|
threshold: 5,
|
|
|
|
passive: false,
|
|
|
|
onSwipeEnd(e, direction) {
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
2023-02-04 21:05:55 +03:00
|
|
|
if (direction === SwipeDirection.RIGHT && Math.abs(distanceX.value) > threshold) {
|
2023-01-06 18:46:36 +03:00
|
|
|
modelValue.value = Math.max(0, modelValue.value - 1)
|
2023-02-04 21:05:55 +03:00
|
|
|
resetZoom()
|
|
|
|
}
|
2023-01-04 11:15:24 +03:00
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
2023-02-04 21:05:55 +03:00
|
|
|
if (direction === SwipeDirection.LEFT && Math.abs(distanceX.value) > threshold) {
|
2023-01-06 18:46:36 +03:00
|
|
|
modelValue.value = Math.min(media.length - 1, modelValue.value + 1)
|
2023-02-04 21:05:55 +03:00
|
|
|
resetZoom()
|
|
|
|
}
|
2023-01-04 11:15:24 +03:00
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
2023-01-06 18:46:36 +03:00
|
|
|
if (direction === SwipeDirection.UP && Math.abs(distanceY.value) > threshold)
|
2023-01-04 11:15:24 +03:00
|
|
|
emit('close')
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
2023-02-04 21:05:55 +03:00
|
|
|
useGesture({
|
2023-03-19 15:12:20 +03:00
|
|
|
onPinch({ offset: [distance, _angle] }) {
|
2023-02-15 22:20:10 +03:00
|
|
|
set({ scale: Math.max(0.5, 1 + distance / 200) })
|
2023-02-04 21:05:55 +03:00
|
|
|
},
|
|
|
|
onMove({ movement: [x, y], dragging, pinching }) {
|
|
|
|
if (dragging && !pinching)
|
|
|
|
set({ x, y })
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
domTarget: target,
|
|
|
|
eventOptions: {
|
|
|
|
passive: true,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
2023-01-04 11:15:24 +03:00
|
|
|
const distanceX = computed(() => {
|
|
|
|
if (width.value === 0)
|
|
|
|
return 0
|
|
|
|
|
|
|
|
if (!isSwiping.value || (direction.value !== SwipeDirection.LEFT && direction.value !== SwipeDirection.RIGHT))
|
2023-01-06 18:46:36 +03:00
|
|
|
return modelValue.value * 100 * -1
|
2023-01-04 11:15:24 +03:00
|
|
|
|
2023-01-06 18:46:36 +03:00
|
|
|
return (lengthX.value / width.value) * 100 * -1 + (modelValue.value * 100) * -1
|
2023-01-04 11:15:24 +03:00
|
|
|
})
|
|
|
|
|
|
|
|
const distanceY = computed(() => {
|
|
|
|
if (height.value === 0 || !isSwiping.value || direction.value !== SwipeDirection.UP)
|
|
|
|
return 0
|
|
|
|
|
|
|
|
return (lengthY.value / height.value) * 100 * -1
|
|
|
|
})
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
|
|
<div ref="target" flex flex-row max-h-full max-w-full overflow-hidden>
|
|
|
|
<div flex :style="{ transform: `translateX(${distanceX}%) translateY(${distanceY}%)`, transition: isSwiping ? 'none' : canAnimate ? 'all 0.5s ease' : 'none' }">
|
|
|
|
<div v-for="item in media" :key="item.id" p4 select-none w-full flex-shrink-0 flex flex-col place-items-center>
|
|
|
|
<img max-h-full max-w-full :draggable="false" select-none :src="item.url || item.previewUrl" :alt="item.description || ''">
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|