Refaktoring
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
<script setup>
|
||||
import PhotoDetail from "components/photos/PhotoDetail.vue";
|
||||
import { ref, watch} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
photos: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
coverPhotoUrl: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
defaultTitle: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
})
|
||||
|
||||
const displayedPhoto = ref(null);
|
||||
const openPhotoDetail = ref(false);
|
||||
const photoData = ref(null);
|
||||
const openDetail = ({photo, idx, allPhotos}) => {
|
||||
openPhotoDetail.value = true;
|
||||
photoData.value = {photo, idx, allPhotos}
|
||||
}
|
||||
|
||||
watch(() => props.photos, (newVal) => {
|
||||
if (newVal.length === 0) return;
|
||||
displayedPhoto.value = props.coverPhotoUrl || newVal[0].url;
|
||||
}, {immediate: true});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-carousel
|
||||
v-if="photos.length > 0"
|
||||
animated
|
||||
v-model="displayedPhoto"
|
||||
arrows
|
||||
swipeable
|
||||
height="auto"
|
||||
infinite
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<q-carousel-slide style="padding: 0" v-for="(photo, idx) in photos" :key="photo.url" :name="photo.url">
|
||||
<q-img :src="photo.url" @click="openDetail({photo, idx, allPhotos: photos})"/>
|
||||
</q-carousel-slide>
|
||||
</q-carousel>
|
||||
|
||||
<PhotoDetail
|
||||
v-if="photoData"
|
||||
v-model="openPhotoDetail"
|
||||
:default-title="defaultTitle"
|
||||
:photo="photoData.photo"
|
||||
:photos="photoData.allPhotos"
|
||||
:opened-index="photoData.idx"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import EmbeddedMap from "components/map/EmbeddedMap.vue";
|
||||
import {computed, onMounted, ref} from "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";
|
||||
@@ -26,8 +26,17 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"])
|
||||
const $q = useQuasar();
|
||||
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;
|
||||
@@ -35,36 +44,47 @@ const isDetailOpened = computed({
|
||||
set(newVal) {
|
||||
emit("update:modelValue", newVal);
|
||||
}
|
||||
})
|
||||
|
||||
const photoIndex = ref(null);
|
||||
onMounted(() => {
|
||||
photoIndex.value = props.openedIndex
|
||||
});
|
||||
|
||||
const photo = computed(() => props.photos[photoIndex.value]);
|
||||
|
||||
const nextPhoto = () => {
|
||||
if (imageFullscreen.value) return;
|
||||
|
||||
if (photoIndex.value < props.photos.length - 1)
|
||||
photoIndex.value++
|
||||
if (displayedPhotoIndex.value < props.photos.length - 1) {
|
||||
displayedPhotoIndex.value++;
|
||||
scrollToPhoto();
|
||||
}
|
||||
}
|
||||
|
||||
const prevPhoto = () => {
|
||||
if (imageFullscreen.value) return;
|
||||
if (photoIndex.value > 0) photoIndex.value--;
|
||||
if (displayedPhotoIndex.value > 0) {
|
||||
displayedPhotoIndex.value--;
|
||||
scrollToPhoto();
|
||||
}
|
||||
}
|
||||
|
||||
const showDetail = (idx) => {
|
||||
photoIndex.value = idx;
|
||||
}
|
||||
const openDetail = (idx) => {
|
||||
displayedPhotoIndex.value = idx;
|
||||
scrollToPhoto();
|
||||
};
|
||||
|
||||
const showExtra = ref(false);
|
||||
const scrollArea = ref(null);
|
||||
const infoWrapper = ref(null);
|
||||
const scrollToPhoto = () => {
|
||||
scrollArea.value.setScrollPosition('vertical', 0, 500);
|
||||
}
|
||||
|
||||
const $q = useQuasar();
|
||||
const isMaximized = computed(() => $q.screen.width <= $q.screen.sizes.sm);
|
||||
const imageFullscreen = ref(false);
|
||||
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>
|
||||
@@ -74,127 +94,129 @@ const imageFullscreen = ref(false);
|
||||
:maximized="isMaximized"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section class="bg-black text-white" v-if="isMaximized">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<strong>{{ photo.name || defaultTitle }}</strong>
|
||||
<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>
|
||||
<div class="col-1 text-right">
|
||||
<q-btn round text-color="white" size="sm" icon="close" @click="isDetailOpened = false"/>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card-section>
|
||||
|
||||
<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"
|
||||
/>
|
||||
<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>
|
||||
|
||||
<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="showExtra = !showExtra"
|
||||
/>
|
||||
<q-btn
|
||||
class="absolute all-pointer-events"
|
||||
style="background: rgba(0, 0, 0, 0.25); top: 50%; left: 8px;"
|
||||
@click="prevPhoto"
|
||||
v-if="photoIndex > 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="photoIndex < 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 class="row" v-show="!imageFullscreen">
|
||||
<div class="col-4 col-md-1 q-pa-sm" v-for="(thumb, idx) in photos" :key="thumb.thumbnailUrl">
|
||||
<q-img
|
||||
:src="thumb.thumbnailUrl"
|
||||
@click="showDetail(idx)"
|
||||
:class='{"shadow-5": true, "active-thumbnail": idx === photoIndex}'
|
||||
/>
|
||||
</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"
|
||||
/>
|
||||
|
||||
<div class="row">
|
||||
<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>
|
||||
|
||||
<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-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
|
||||
<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>
|
||||
Výška nad zemí: {{ photo.gpsAltitude - photo.terrainElevation }} m
|
||||
</template>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<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>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card-section>
|
||||
</q-scroll-area>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
@@ -220,8 +242,4 @@ const imageFullscreen = ref(false);
|
||||
.iiz__img--hidden {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.iiz__zoom-img--visible {
|
||||
//height: 125vh !important
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {computed, onMounted, ref, watch} from "vue";
|
||||
import {useQuasar} from "quasar";
|
||||
import {confirmObject} from "src/composables/dialog";
|
||||
|
||||
@@ -33,20 +33,20 @@ const $q = useQuasar();
|
||||
|
||||
const confirmPhotoDelete = (id) => {
|
||||
$q.dialog(confirmObject("Opravdu chces smazat tuto fotku?"))
|
||||
.onOk(() => {
|
||||
emits("delete", id);
|
||||
});
|
||||
.onOk(() => {
|
||||
emits("delete", id);
|
||||
});
|
||||
}
|
||||
|
||||
const confirmBatchPhotoDelete = () => {
|
||||
$q.dialog(confirmObject("Opravdu chces smazat vybrané fotky?" + JSON.stringify(batchEdit.value)))
|
||||
.onOk(() => {
|
||||
for (const id in batchEdit.value) {
|
||||
if (!batchEdit.value[id]) continue;
|
||||
emits("delete", parseInt(id));
|
||||
batchEdit.value[id] = false;
|
||||
}
|
||||
});
|
||||
$q.dialog(confirmObject("Opravdu chces smazat vybrané fotky?"))
|
||||
.onOk(() => {
|
||||
for (const id in batchEdit.value) {
|
||||
if (!batchEdit.value[id]) continue;
|
||||
emits("delete", parseInt(id));
|
||||
batchEdit.value[id] = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const handleImageClick = (photo, idx) => {
|
||||
@@ -67,18 +67,40 @@ const openBatchEdit = () => {
|
||||
}
|
||||
|
||||
const isBatchEditAllowed = computed(() => Object.values(batchEdit.value).filter(v => v).length > 0);
|
||||
|
||||
const checkAll = ref(false);
|
||||
watch(() => checkAll.value, (newVal) => {
|
||||
if (newVal === null) return;
|
||||
|
||||
for (const photo of props.photos) {
|
||||
batchEdit.value[photo.id] = newVal;
|
||||
}
|
||||
});
|
||||
watch(() => batchEdit.value, () => {
|
||||
let checkedAll = true;
|
||||
let uncheckedAll = true;
|
||||
|
||||
for (const i of Object.keys(batchEdit.value)) {
|
||||
if (typeof batchEdit.value[i] === 'undefined') continue;
|
||||
|
||||
if (batchEdit.value[i]) uncheckedAll = false;
|
||||
if (!batchEdit.value[i]) checkedAll = false;
|
||||
}
|
||||
|
||||
if (checkedAll) {
|
||||
checkAll.value = true;
|
||||
} else if (uncheckedAll) {
|
||||
checkAll.value = false;
|
||||
} else {
|
||||
checkAll.value = null;
|
||||
}
|
||||
}, {deep: true});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row q-my-md">
|
||||
<div class="col text-right">
|
||||
<q-btn round flat icon="edit" :color="isBatchEditAllowed ? 'primary' : 'grey'" :disable="!isBatchEditAllowed" @click="openBatchEdit" v-if="showActions"/>
|
||||
<q-btn round flat icon="delete" :color="isBatchEditAllowed ? 'red' : 'grey'" :disable="!isBatchEditAllowed" @click="confirmBatchPhotoDelete" v-if="showActions"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 col-sm-4 col-md-4 col-lg-3" v-for="(photo, idx) in photos" :key="photo.id">
|
||||
<div class="col-6 col-sm-4 col-md-4 col-lg-3" v-for="(photo, idx) in photos" :key="photo.id">
|
||||
<q-card :class='{"q-ma-sm": true, "shadow-1": !batchEdit[photo.id], "shadow-10": batchEdit[photo.id]}'>
|
||||
<q-img :src="photo.thumbnailUrl" @click="handleImageClick(photo, idx)">
|
||||
<div class="absolute-bottom" v-if="photo.name !== '' || photo.description !== ''">
|
||||
@@ -106,4 +128,14 @@ const isBatchEditAllowed = computed(() => Object.values(batchEdit.value).filter(
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-page-sticky :offset="[50, 50]" position="bottom" v-if="showActions && photos.length > 0">
|
||||
<q-card>
|
||||
<q-card-actions>
|
||||
<q-checkbox v-model="checkAll" label="Vybrat vše"/>
|
||||
<q-btn round class="q-ml-lg" flat icon="edit" :color="isBatchEditAllowed ? 'primary' : 'grey'" :disable="!isBatchEditAllowed" @click="openBatchEdit" v-if="showActions"/>
|
||||
<q-btn round flat icon="delete" :color="isBatchEditAllowed ? 'red' : 'grey'" :disable="!isBatchEditAllowed" @click="confirmBatchPhotoDelete" v-if="showActions"/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-page-sticky>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user