262 lines
7.6 KiB
Vue
262 lines
7.6 KiB
Vue
<script setup>
|
|
import EmbeddedMap from "components/map/EmbeddedMap.vue";
|
|
import {computed, ref, watch} from "vue";
|
|
import {formatDatetime} from "src/composables/datetimeUtils";
|
|
import 'vue-inner-image-zoom/lib/vue-inner-image-zoom.css'
|
|
import {useQuasar} from "quasar";
|
|
import {useRouter} from "vue-router";
|
|
|
|
const props = defineProps({
|
|
photos: {
|
|
type: Array,
|
|
required: false,
|
|
default: () => []
|
|
},
|
|
openedIndex: {
|
|
type: Number,
|
|
required: false,
|
|
},
|
|
modelValue: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
defaultTitle: {
|
|
type: String,
|
|
required: false
|
|
}
|
|
});
|
|
|
|
const $q = useQuasar();
|
|
const $router = useRouter();
|
|
|
|
const imageFullscreen = ref(false);
|
|
const isMaximized = computed(() => $q.screen.width <= $q.screen.sizes.sm);
|
|
|
|
const displayedPhotoIndex = ref(null);
|
|
watch(() => props.openedIndex, (newVal) => {
|
|
displayedPhotoIndex.value = newVal;
|
|
}, {immediate: true});
|
|
const photo = computed(() => props.photos[displayedPhotoIndex.value]);
|
|
|
|
const emit = defineEmits(["update:modelValue"])
|
|
const isDetailOpened = computed({
|
|
get() {
|
|
return props.modelValue;
|
|
},
|
|
set(newVal) {
|
|
emit("update:modelValue", newVal);
|
|
}
|
|
});
|
|
|
|
const nextPhoto = () => {
|
|
if (imageFullscreen.value) return;
|
|
|
|
if (displayedPhotoIndex.value < props.photos.length - 1) {
|
|
displayedPhotoIndex.value++;
|
|
scrollToPhoto();
|
|
}
|
|
}
|
|
|
|
const prevPhoto = () => {
|
|
if (imageFullscreen.value) return;
|
|
if (displayedPhotoIndex.value > 0) {
|
|
displayedPhotoIndex.value--;
|
|
scrollToPhoto();
|
|
}
|
|
}
|
|
|
|
const openDetail = (idx) => {
|
|
displayedPhotoIndex.value = idx;
|
|
scrollToPhoto();
|
|
};
|
|
|
|
watch(() => displayedPhotoIndex.value, (newVal) => {
|
|
const photo = props.photos[newVal];
|
|
$router.replace({query: {photo: photo.id}});
|
|
}, {immediate: true});
|
|
|
|
const removePhotoFromQueries = () => {
|
|
$router.replace({query: {photo: undefined}});
|
|
}
|
|
|
|
const showExtra = ref(false);
|
|
const scrollArea = ref(null);
|
|
const infoWrapper = ref(null);
|
|
const scrollToPhoto = () => {
|
|
scrollArea.value.setScrollPosition('vertical', 0, 500);
|
|
}
|
|
|
|
const showExtraInfo = () => {
|
|
if (showExtra.value) {
|
|
showExtra.value = false;
|
|
scrollToPhoto();
|
|
} else {
|
|
showExtra.value = true;
|
|
const scrollAreaHeight = infoWrapper.value.offsetTop;
|
|
scrollArea.value.setScrollPosition('vertical', scrollAreaHeight, 500);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<q-dialog
|
|
v-model="isDetailOpened"
|
|
full-width
|
|
:maximized="isMaximized"
|
|
no-route-dismiss
|
|
@hide="removePhotoFromQueries"
|
|
>
|
|
<q-card>
|
|
<q-scroll-area ref="scrollArea" style="height: 95vh">
|
|
<q-card-section class="bg-black text-white" v-if="isMaximized">
|
|
<div class="row">
|
|
<div class="col">
|
|
<strong>{{ photo.name || defaultTitle }}</strong>
|
|
</div>
|
|
<div class="col-1 text-right">
|
|
<q-btn round text-color="white" size="sm" icon="close" @click="isDetailOpened = false"/>
|
|
</div>
|
|
</div>
|
|
</q-card-section>
|
|
<div class="fixed-top-right q-mt-md q-mr-md z-max" v-else>
|
|
<q-btn round size="sm" icon="close" @click="isDetailOpened = false"/>
|
|
</div>
|
|
|
|
<q-card-section :class="{'q-pa-none': isMaximized}">
|
|
<div :class="{'relative-position': true, 'image-full-height': imageFullscreen}">
|
|
<div
|
|
class="text-center"
|
|
v-touch-swipe.mouse.right="prevPhoto"
|
|
v-touch-swipe.mouse.left="nextPhoto"
|
|
@keyup.left="prevPhoto"
|
|
@keyup.right="nextPhoto">
|
|
<inner-image-zoom
|
|
:move-type="isMaximized ? 'drag' : 'pan'"
|
|
:zoomScale="0.75"
|
|
:hide-hint="true"
|
|
:after-zoom-in="() => imageFullscreen = true"
|
|
:after-zoom-out="() => imageFullscreen = false"
|
|
:src="photo.url"
|
|
/>
|
|
</div>
|
|
|
|
<template v-if="!imageFullscreen">
|
|
<q-btn
|
|
class="absolute all-pointer-events"
|
|
round
|
|
flat
|
|
icon="info"
|
|
:text-color="showExtra ? 'primary' : 'white'"
|
|
style="background: rgba(0, 0, 0, 0.25); bottom: 16px; left: 8px"
|
|
@click="showExtraInfo"
|
|
/>
|
|
<q-btn
|
|
class="absolute all-pointer-events"
|
|
style="background: rgba(0, 0, 0, 0.25); top: 50%; left: 8px;"
|
|
@click="prevPhoto"
|
|
v-if="displayedPhotoIndex > 0 && !isMaximized"
|
|
icon="chevron_left"
|
|
text-color="white"
|
|
flat
|
|
rounded
|
|
/>
|
|
<q-btn
|
|
class="absolute all-pointer-events"
|
|
style="background: rgba(0, 0, 0, 0.25); top: 50%; right: 8px;"
|
|
@click="nextPhoto"
|
|
v-if="displayedPhotoIndex < photos.length - 1 && !isMaximized"
|
|
icon-right="chevron_right"
|
|
text-color="white"
|
|
flat
|
|
rounded
|
|
/>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="row" v-show="!imageFullscreen">
|
|
<div class="col-3 col-md-1 q-pa-sm" v-for="(thumb, idx) in photos" :key="thumb.thumbnailUrl">
|
|
<q-img
|
|
:src="thumb.thumbnailUrl"
|
|
@click="openDetail(idx)"
|
|
:class='{"shadow-5": true, "active-thumbnail": idx === displayedPhotoIndex}'
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row" ref="infoWrapper">
|
|
<div class="col-12 q-my-md" v-if="showExtra">
|
|
<EmbeddedMap
|
|
v-if="photo.gpsLatitude && photo.gpsLongitude"
|
|
class="q-mx-sm"
|
|
:gps-latitude="photo.gpsLatitude"
|
|
:gps-longitude="photo.gpsLongitude"
|
|
/>
|
|
|
|
<strong v-if="photo.name !== '' && !isMaximized">{{ photo.name }}</strong>
|
|
<p v-if="photo.description !== ''">{{ photo.description }}</p>
|
|
|
|
<q-list dense>
|
|
<q-item v-if="photo.pointOfInterest">
|
|
<q-item-section avatar>
|
|
<q-avatar icon="marker"></q-avatar>
|
|
</q-item-section>
|
|
<q-item-section>
|
|
{{ photo.pointOfInterest.name }}
|
|
</q-item-section>
|
|
</q-item>
|
|
<q-item v-if="photo.exposedAt">
|
|
<q-item-section avatar>
|
|
<q-avatar icon="photo_camera"></q-avatar>
|
|
</q-item-section>
|
|
<q-item-section>
|
|
{{ formatDatetime(photo.exposedAt) }}
|
|
</q-item-section>
|
|
</q-item>
|
|
|
|
<q-item v-if="photo.gpsAltitude">
|
|
<q-item-section avatar>
|
|
<q-avatar icon="height"/>
|
|
</q-item-section>
|
|
<q-item-section>
|
|
Nadmořská výška: {{ photo.gpsAltitude }} m
|
|
<br>
|
|
<template v-if="photo.terrainElevation">
|
|
Výška terénu: {{ photo.terrainElevation }} m
|
|
<br>
|
|
Výška nad zemí: {{ photo.gpsAltitude - photo.terrainElevation }} m
|
|
</template>
|
|
</q-item-section>
|
|
</q-item>
|
|
</q-list>
|
|
</div>
|
|
</div>
|
|
</q-card-section>
|
|
</q-scroll-area>
|
|
</q-card>
|
|
</q-dialog>
|
|
</template>
|
|
|
|
<style>
|
|
.active-thumbnail {
|
|
outline: solid 2px
|
|
}
|
|
|
|
.iiz {
|
|
width: 100%;
|
|
}
|
|
|
|
.image-full-height .iiz {
|
|
height: calc(95vh - 65px) !important;
|
|
}
|
|
|
|
.iiz__img {
|
|
max-height: 90vh !important;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.iiz__img--hidden {
|
|
height: 100vh;
|
|
}
|
|
</style>
|