generated from Nodarx/template
4e302ea923
Signed-off-by: Sebastian <krupinski01@gmail.com>
167 lines
4.2 KiB
Vue
167 lines
4.2 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
import type { MediaLeafProps } from '@/types'
|
|
|
|
const props = defineProps<MediaLeafProps>()
|
|
|
|
// ── Pan / zoom state ────────────────────────────────────────────────────────
|
|
|
|
const scale = ref(1)
|
|
const translateX = ref(0)
|
|
const translateY = ref(0)
|
|
const isDragging = ref(false)
|
|
|
|
const MIN_SCALE = 0.1
|
|
const MAX_SCALE = 10
|
|
const ZOOM_FACTOR = 0.12
|
|
|
|
let dragStartX = 0
|
|
let dragStartY = 0
|
|
let dragStartTX = 0
|
|
let dragStartTY = 0
|
|
|
|
const containerRef = ref<HTMLElement | null>(null)
|
|
|
|
// ── Zoom ───────────────────────────────────────────────────────────────────
|
|
|
|
function clamp(val: number, min: number, max: number) {
|
|
return Math.min(max, Math.max(min, val))
|
|
}
|
|
|
|
function handleWheel(e: WheelEvent) {
|
|
e.preventDefault()
|
|
const delta = e.deltaY > 0 ? -(ZOOM_FACTOR) : ZOOM_FACTOR
|
|
scale.value = clamp(scale.value * (1 + delta), MIN_SCALE, MAX_SCALE)
|
|
}
|
|
|
|
// ── Drag (pan) ─────────────────────────────────────────────────────────────
|
|
|
|
function handleMousedown(e: MouseEvent) {
|
|
if (e.button !== 0) return
|
|
isDragging.value = true
|
|
dragStartX = e.clientX
|
|
dragStartY = e.clientY
|
|
dragStartTX = translateX.value
|
|
dragStartTY = translateY.value
|
|
}
|
|
|
|
function handleMousemove(e: MouseEvent) {
|
|
if (!isDragging.value) return
|
|
translateX.value = dragStartTX + (e.clientX - dragStartX)
|
|
translateY.value = dragStartTY + (e.clientY - dragStartY)
|
|
}
|
|
|
|
function handleMouseup() {
|
|
isDragging.value = false
|
|
}
|
|
|
|
function handleDblclick() {
|
|
// Double-click to reset
|
|
scale.value = 1
|
|
translateX.value = 0
|
|
translateY.value = 0
|
|
}
|
|
|
|
// Pan/zoom is for the full viewer only; the hover popover shows a static fit.
|
|
const interactive = computed(() => props.presentation !== 'popover')
|
|
|
|
onMounted(() => {
|
|
if (!interactive.value) return
|
|
const el = containerRef.value
|
|
if (el) {
|
|
el.addEventListener('wheel', handleWheel, { passive: false })
|
|
}
|
|
window.addEventListener('mousemove', handleMousemove)
|
|
window.addEventListener('mouseup', handleMouseup)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (!interactive.value) return
|
|
const el = containerRef.value
|
|
if (el) {
|
|
el.removeEventListener('wheel', handleWheel)
|
|
}
|
|
window.removeEventListener('mousemove', handleMousemove)
|
|
window.removeEventListener('mouseup', handleMouseup)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
ref="containerRef"
|
|
class="image-viewer"
|
|
:class="{ 'image-viewer--dragging': isDragging, 'image-viewer--static': !interactive }"
|
|
@mousedown="interactive && handleMousedown($event)"
|
|
@dblclick="interactive && handleDblclick()"
|
|
>
|
|
<img
|
|
:src="url"
|
|
:alt="title ?? item?.title ?? 'Preview image'"
|
|
class="image-viewer-img"
|
|
:style="{
|
|
transform: `translate(${translateX}px, ${translateY}px) scale(${scale})`,
|
|
transformOrigin: 'center center',
|
|
}"
|
|
draggable="false"
|
|
/>
|
|
|
|
<!-- Zoom hint -->
|
|
<div v-if="interactive" class="image-viewer-hint text-caption text-medium-emphasis">
|
|
Scroll to zoom · Drag to pan · Double-click to reset
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.image-viewer {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
cursor: grab;
|
|
user-select: none;
|
|
}
|
|
|
|
.image-viewer--dragging {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.image-viewer--static {
|
|
cursor: default;
|
|
}
|
|
|
|
.image-viewer-img {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
object-fit: contain;
|
|
transition: transform 0.05s linear;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.image-viewer--dragging .image-viewer-img {
|
|
transition: none;
|
|
}
|
|
|
|
.image-viewer-hint {
|
|
position: absolute;
|
|
bottom: 12px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(0, 0, 0, 0.35);
|
|
color: #fff;
|
|
padding: 2px 10px;
|
|
border-radius: 12px;
|
|
pointer-events: none;
|
|
white-space: nowrap;
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
}
|
|
|
|
.image-viewer:hover .image-viewer-hint {
|
|
opacity: 1;
|
|
}
|
|
</style>
|