<template>
<q-dialog class="q-pa-none q-ma-none fullscreen" v-model="$store.popups.imageViewer.show" @hide="close">
<div class="fullscreen" @wheel="handleZoom" @touchstart.prevent="null">
<help-button class="help-button" name="ImageViewer" titleToShow="Image viewer" />
<q-btn size="md" class="close" dense flat round icon="close" @click="close" @touchstart="close" />
<q-circular-progress v-if="$store.working" size="48px" indeterminate color="primary" class="nomy center"
:thickness="0.3" style="z-index: 100;" />
<q-btn size="xl" v-if="i > 0" class="left" dense flat round icon="arrow_back" @click="backward"
@touchstart="backward" />
<q-btn size="xl" v-if="i < files.length - 1" class="right" dense flat round icon="arrow_forward"
@click="forward" @touchstart="forward" />
<img ref="img" class="img" :src="src" @mousedown="handleDragStart" @mousemove="handleDragMove"
@mouseup="handleDragEnd" @selectstart="handleSelectStart" @mouseleave="handleDragEnd"
@dragstart="handleDefaultDragStart" @touchstart.prevent="onTouchStart" @touchend.prevent="onTouchEnd"
@touchmove.prevent="onTouchMove" @load="$store.working = false" />
<compass v-model="compass" :id="file_id" class="compass" v-if="files.length > 0 && files[i].compass != null"
@update:model-value="compassChanged" />
</div>
</q-dialog>
</template>
<script>
import Compass from "./compass.vue";
import HelpButton from "./help-button.vue";
/**
* Image viewer component
* @component
* @name ImageViewer
* @example
* <ImageViewer />
*/
export default {
name: "ImageViewer",
components: {
Compass,
HelpButton
},
// props: {
// files: { type: Array, default: [] },
// index: { type: Number, default: 0 },
// needsAuthentication: { type: Boolean, default: false }
// },
data() {
return {
files: [],
needsAuthentication: false,
src: "",
i: 0,
img: null,
scale: 1,
prevScale: 1,
prevX: 0,
prevY: 0,
dX: 0,
dY: 0,
totaldX: 0,
totaldY: 0,
prevDist: 0,
isDragging: false,
compass: null,
file_id: null
};
},
methods: {
/**
* Displays the image viewer with the specified files and index.
*
* @param {Array} files - The array of files to display in the image viewer.
* @param {number} index - The index of the file to display initially.
* @param {boolean} needsAuthentication - Indicates whether authentication is required to view the images.
*/
async show(files, index, needsAuthentication) {
this.$store.popups.imageViewer.show = true;
await this.$nextTick();
this.needsAuthentication = needsAuthentication;
this.files = files;
this.i = index;
this.load();
},
/**
* Moves the image viewer forward to the next image.
*/
forward() {
if (this.i < this.files.length - 1) {
this.i++;
this.load();
}
},
/**
* Moves the image viewer backward to the previous image.
*/
backward() {
if (this.i > 0) {
this.i--;
this.load();
}
},
/**
* Loads the image from the server.
*/
async load() {
this.scale = 1;
this.dX = 0;
this.dY = 0;
this.prevX = 0;
this.prevY = 0;
this.totaldX = 0;
this.totaldY = 0;
this.isDragging = false;
this.compass = this.files[this.i].compass;
this.file_id = this.files[this.i].file_id;
this.transform();
this.$store.working = true;
if (this.needsAuthentication) {
this.src = await this.getImage(this.files[this.i].url);
} else {
this.src = this.files[this.i].url;
}
},
/**
* Handles the drag start event for the default drag behavior.
*
* @param {Event} event - The drag start event.
*/
handleDefaultDragStart(event) {
event.preventDefault();
},
/**
* Handles the select start event.
*
* @param {Event} event - The select start event.
*/
handleSelectStart(event) {
event.preventDefault();
return false;
},
reset() {
this.scale = 1;
this.dX = 0;
this.dY = 0;
this.prevX = 0;
this.prevY = 0;
this.totaldX = 0;
this.totaldY = 0;
this.isDragging = false;
this.$refs.img.style.cursor = "default";
this.transform();
},
/**
* Handles the zoom event.
*
* @param {Event} event - The zoom event.
*/
handleZoom(event) {
event.preventDefault();
const delta = event.deltaY ? event.deltaY / 1000 : event.scale - 1;
this.scale += delta;
this.scale = Math.min(Math.max(1, this.scale), 10); // Limit scale between 1 and 4
if (this.scale <= 1) {
this.reset();
} else {
this.$refs.img.style.cursor = "grab";
}
this.transform();
},
shouldChangeImage(dX) {
if (this.scale > 1) return;
if (dX < - 100) {
this.forward();
} else if (dX > 100) {
this.backward();
};
},
/**
* Handles the drag start event.
*
* @param {Event} event - The drag start event.
*/
handleDragStart(event) {
this.prevX = event.clientX;
this.prevY = event.clientY;
//if (this.scale <= 1) return;
this.$refs.img.style.cursor = "grabbing";
this.isDragging = true;
},
/**
* Handles the drag move event.
*
* @param {Event} event - The drag move event.
*/
handleDragMove(event) {
if (!this.isDragging || this.scale <= 1) return;
this.dX = event.clientX - this.prevX;
this.dY = event.clientY - this.prevY;
this.prevX = event.clientX;
this.prevY = event.clientY;
this.totaldX += this.dX;
this.totaldY += this.dY;
this.transform();
},
/**
* Handles the drag end event.
*
* @param {Event} event - The drag end event.
*/
handleDragEnd(event) {
if (this.scale <= 1 && this.isDragging) {
this.shouldChangeImage(event.clientX - this.prevX);
}
this.$refs.img.style.cursor = "default";
this.isDragging = false;
},
/**
* Sets CSS image transformation depending on scale and drag.
*/
transform() {
this.$refs.img.style.transform = `scale(${this.scale}) translate(${this.totaldX}px, ${this.totaldY}px)`;
},
/**
* Closes the image viewer.
*/
close() {
this.$store.popups.imageViewer.show = false;
},
onTouchStart(ev) {
if (ev.touches.length === 1) {
this.prevX = ev.touches[0].clientX
this.prevY = ev.touches[0].clientY
this.isDragging = true;
} else if (ev.touches.length === 2) {
let distX = ev.touches[0].clientX - ev.touches[1].clientX
let distY = ev.touches[0].clientY - ev.touches[1].clientY
this.prevDist = Math.sqrt(distX * distX + distY * distY)
}
},
onTouchMove(ev) {
if (ev.touches.length === 1 && this.isDragging) {
this.dX = ev.touches[0].clientX - this.prevX;
this.dY = ev.touches[0].clientY - this.prevY;
if (this.scale <= 1) return;
this.prevX = ev.touches[0].clientX;
this.prevY = ev.touches[0].clientY;
this.totaldX += this.dX;
this.totaldY += this.dY;
this.transform();
// this.onPointerMove(ev.touches[0].clientX, ev.touches[0].clientY)
} else if (ev.touches.length === 2) {
let distX = ev.touches[0].clientX - ev.touches[1].clientX;
let distY = ev.touches[0].clientY - ev.touches[1].clientY;
let dist = Math.sqrt(distX * distX + distY * distY);
this.scale *= dist / this.prevDist;
this.scale = Math.min(Math.max(1, this.scale), 10);
if (this.scale <= 1) {
this.reset();
}
this.prevDist = dist;
this.transform();
}
},
onTouchEnd(ev) {
this.isDragging = false;
if (this.scale <= 1) {
this.shouldChangeImage(this.dX);
}
},
compassChanged(val) {
console.log("compassChanged", val);
this.compass = val;
this.files[this.i].compass = val;
this.$emit("compassChanged", this.compass, this.files[this.i].file_id);
}
}
}
</script>
<style scoped>
.help-button {
position: absolute;
top: 5px;
right: 35px;
z-index: 200;
cursor: pointer;
}
.close {
position: absolute;
top: 2px;
right: 2px;
z-index: 200;
cursor: pointer;
color: black;
}
.left {
position: absolute;
left: 3px;
z-index: 100;
color: black
}
.right {
position: absolute;
right: 3px;
z-index: 100;
color: black
}
.center {
position: absolute;
z-index: 100;
color: black
}
.compass {
position: absolute;
left: 3px;
top: 3px;
z-index: 100;
color: black
}
.fullscreen {
max-width: none !important;
max-height: none !important;
width: 100vw !important;
height: 100vh !important;
background-color: white !important;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.img {
max-width: 100% !important;
max-height: 100% !important;
width: auto !important;
height: auto !important;
z-index: 0
}
</style>