Uprava editoru
This commit is contained in:
@@ -1,80 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import PhotoDetail from "components/photos/PhotoDetail.vue";
|
|
||||||
import {onMounted, ref, watch} from "vue";
|
|
||||||
import {useRoute, useRouter} from "vue-router";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
photos: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
coverPhotoUrl: {
|
|
||||||
type: String,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
carouselTitle: {
|
|
||||||
type: String,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
defaultTitle: {
|
|
||||||
type: String,
|
|
||||||
required: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const $route = useRoute();
|
|
||||||
const photoId = parseInt($route.query.photo);
|
|
||||||
|
|
||||||
const displayedPhoto = ref(null);
|
|
||||||
|
|
||||||
const emits = defineEmits(["open-detail"]);
|
|
||||||
|
|
||||||
watch(() => props.photos, (newVal) => {
|
|
||||||
if (newVal.length === 0) return;
|
|
||||||
displayedPhoto.value = props.coverPhotoUrl || newVal[0].url;
|
|
||||||
}, {immediate: true});
|
|
||||||
|
|
||||||
|
|
||||||
const openDetail = ({photo, idx, allPhotos}) => {
|
|
||||||
emits("open-detail", {photo, idx, allPhotos});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const idx = props.photos.findIndex(p => p.id === photoId);
|
|
||||||
if (idx >= 0) {
|
|
||||||
const photo = props.photos[idx];
|
|
||||||
openDetail({photo, allPhotos: props.photos, idx});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</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">
|
|
||||||
<span class="absolute-top text-right">
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
icon="zoom_in"
|
|
||||||
class="bg-blue-grey-9 text-white q-ma-md all-pointer-events"
|
|
||||||
@click="openDetail({photo, idx, allPhotos: photos})" />
|
|
||||||
</span>
|
|
||||||
<div class="absolute-bottom custom-caption">
|
|
||||||
<div class="text-h5" v-if="carouselTitle"> {{ carouselTitle }} </div>
|
|
||||||
<div class="text-subtitle1" v-if="photos.length > 0">
|
|
||||||
<q-btn rounded @click.prevent="openDetail({photo: photos[0], idx: 0, allPhotos: photos})">{{photos.length}} fotografií</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-img>
|
|
||||||
</q-carousel-slide>
|
|
||||||
</q-carousel>
|
|
||||||
</template>
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -111,7 +111,7 @@ const batchOptionsProps = computed(() => ({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="row q-mb-lg">
|
<div class="row" style="margin-bottom: 150px">
|
||||||
<div class="col-6 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-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)" style="cursor: pointer">
|
<q-img :src="photo.thumbnailUrl" @click="handleImageClick(photo, idx)" style="cursor: pointer">
|
||||||
@@ -143,8 +143,8 @@ const batchOptionsProps = computed(() => ({
|
|||||||
<q-card-actions>
|
<q-card-actions>
|
||||||
<q-checkbox v-model="checkAll" label="Vybrat vše"/>
|
<q-checkbox v-model="checkAll" label="Vybrat vše"/>
|
||||||
<q-btn v-bind="batchOptionsProps" icon="info" :color="isBatchEditAllowed ? 'primary' : 'grey'" @click="openBatchEdit"/>
|
<q-btn v-bind="batchOptionsProps" icon="info" :color="isBatchEditAllowed ? 'primary' : 'grey'" @click="openBatchEdit"/>
|
||||||
<q-btn v-bind="batchOptionsProps" icon="rotate_left" :color="isBatchEditAllowed ? 'secondary' : 'grey'" @click="batchRotate('counterClockwise')"/>
|
<q-btn v-bind="batchOptionsProps" icon="rotate_90_degrees_ccw" :color="isBatchEditAllowed ? 'secondary' : 'grey'" @click="batchRotate('counterClockwise')"/>
|
||||||
<q-btn v-bind="batchOptionsProps" icon="rotate_right" :color="isBatchEditAllowed ? 'secondary' : 'grey'" @click="batchRotate('clockwise')"/>
|
<q-btn v-bind="batchOptionsProps" icon="rotate_90_degrees_cw" :color="isBatchEditAllowed ? 'secondary' : 'grey'" @click="batchRotate('clockwise')"/>
|
||||||
<q-btn v-bind="batchOptionsProps" icon="delete" :color="isBatchEditAllowed ? 'red' : 'grey'" @click="confirmBatchPhotoDelete"/>
|
<q-btn v-bind="batchOptionsProps" icon="delete" :color="isBatchEditAllowed ? 'red' : 'grey'" @click="confirmBatchPhotoDelete"/>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|||||||
@@ -8,45 +8,48 @@ export const croppedArea = ref(null);
|
|||||||
export const selectedAspectRatio = ref(null);
|
export const selectedAspectRatio = ref(null);
|
||||||
|
|
||||||
watch(() => selectedAspectRatio.value, (newVal) => {
|
watch(() => selectedAspectRatio.value, (newVal) => {
|
||||||
cropper.setAspectRatio(newVal);
|
cropper.setAspectRatio(newVal);
|
||||||
})
|
})
|
||||||
|
|
||||||
export const setCropData = () => {
|
|
||||||
const [w, h] = cropperImgSizes.value;
|
|
||||||
|
|
||||||
const cropData = cropper.getCropBoxData();
|
export const setCropData = () => {
|
||||||
croppedArea.value = {
|
const [w, h] = cropperImgSizes.value;
|
||||||
left: roundWithPrecision(cropData.left / w, 5),
|
|
||||||
top: roundWithPrecision(cropData.top / h, 5),
|
console.log("crop end");
|
||||||
width: roundWithPrecision(cropData.width / w, 5),
|
|
||||||
height: roundWithPrecision(cropData.height / h, 5)
|
const cropData = cropper.getCropBoxData();
|
||||||
};
|
croppedArea.value = {
|
||||||
|
left: roundWithPrecision(cropData.left / w, 5),
|
||||||
|
top: roundWithPrecision(cropData.top / h, 5),
|
||||||
|
width: roundWithPrecision(cropData.width / w, 5),
|
||||||
|
height: roundWithPrecision(cropData.height / h, 5)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const aspectRatioOptions = computed(() =>
|
export const aspectRatioOptions = computed(() =>
|
||||||
[
|
[
|
||||||
{label: '16:9', value: roundWithPrecision(16 / 9, 5)},
|
{label: '16:9', value: roundWithPrecision(16 / 9, 5)},
|
||||||
{label: '4:3', value: roundWithPrecision(4 / 3, 5)},
|
{label: '4:3', value: roundWithPrecision(4 / 3, 5)},
|
||||||
{label: '9:16', value: roundWithPrecision(9 / 16, 5)},
|
{label: '9:16', value: roundWithPrecision(9 / 16, 5)},
|
||||||
{label: '3:4', value: roundWithPrecision(3 / 4, 5)},
|
{label: '3:4', value: roundWithPrecision(3 / 4, 5)},
|
||||||
{label: '1:1', value: 1},
|
{label: '1:1', value: 1},
|
||||||
{label: 'Volný', value: null},
|
{label: 'Volný', value: null},
|
||||||
{label: 'Původní', value: roundWithPrecision(photoAspectRatio.value, 5)},
|
{label: 'Původní', value: roundWithPrecision(photoAspectRatio.value, 5)},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const cropperReady = (e) => {
|
export const cropperReady = (e) => {
|
||||||
if (croppedArea.value) {
|
if (croppedArea.value) {
|
||||||
const [w, h] = cropperImgSizes.value;
|
const [w, h] = cropperImgSizes.value;
|
||||||
selectedAspectRatio.value = roundWithPrecision((croppedArea.value.width * w) / (croppedArea.value.height * h), 5);
|
selectedAspectRatio.value = roundWithPrecision((croppedArea.value.width * w) / (croppedArea.value.height * h), 5);
|
||||||
|
|
||||||
cropper.setCropBoxData({
|
cropper.setCropBoxData({
|
||||||
left: croppedArea.value.left * w,
|
left: croppedArea.value.left * w,
|
||||||
top: croppedArea.value.top * h,
|
top: croppedArea.value.top * h,
|
||||||
width: croppedArea.value.width * w,
|
width: croppedArea.value.width * w,
|
||||||
height: croppedArea.value.height * h
|
height: croppedArea.value.height * h
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
selectedAspectRatio.value = roundWithPrecision(photoAspectRatio.value, 5);
|
selectedAspectRatio.value = roundWithPrecision(photoAspectRatio.value, 5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import {ref} from "vue";
|
|
||||||
import {flight} from "src/composables/objects/flight";
|
|
||||||
|
|
||||||
export const openPhotoDetail = ref(false);
|
|
||||||
export const photoData = ref(null);
|
|
||||||
export const openDetail = ({photo, idx, allPhotos}) => {
|
|
||||||
openPhotoDetail.value = true;
|
|
||||||
photoData.value = {photo, idx, allPhotos};
|
|
||||||
}
|
|
||||||
export const openPhotoFromList = ({photo, idx}) => {
|
|
||||||
openDetail({photo, idx, allPhotos: flight.value.photos});
|
|
||||||
}
|
|
||||||
@@ -69,11 +69,6 @@ const copyLinkToClipboard = () => {
|
|||||||
<FlightDetailSkeleton v-if="loading || !flight"/>
|
<FlightDetailSkeleton v-if="loading || !flight"/>
|
||||||
|
|
||||||
<q-card class="flight-detail-card" v-else>
|
<q-card class="flight-detail-card" v-else>
|
||||||
<!-- <PhotoCarousel-->
|
|
||||||
<!-- :cover-photo-url="flight.coverPhoto?.url"-->
|
|
||||||
<!-- :carousel-title="flightName(flight)"-->
|
|
||||||
<!-- :photos="flight.photos"-->
|
|
||||||
<!-- />-->
|
|
||||||
<q-img :src="flight.coverPhoto.url" v-if="flight.coverPhoto">
|
<q-img :src="flight.coverPhoto.url" v-if="flight.coverPhoto">
|
||||||
<div class="absolute-bottom custom-caption">
|
<div class="absolute-bottom custom-caption">
|
||||||
<div class="text-h5"> {{ flightName(flight) }} </div>
|
<div class="text-h5"> {{ flightName(flight) }} </div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import {computed, onMounted, ref, watch} from "vue";
|
import {onMounted, ref, watch} from "vue";
|
||||||
import {usePhotosStore} from "stores/Photo";
|
import {usePhotosStore} from "stores/Photo";
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import {flightName} from "src/composables/objects/flight";
|
import {flightName} from "src/composables/objects/flight";
|
||||||
@@ -9,14 +9,14 @@ import {croppedAreaForPreview, getPreviewUrl} from "src/composables/photos/edito
|
|||||||
import {compare, onResize, splitter, splitterWidth} from "src/composables/photos/editor/splitter";
|
import {compare, onResize, splitter, splitterWidth} from "src/composables/photos/editor/splitter";
|
||||||
import {aspectRatioOptions, croppedArea, cropperReady, selectedAspectRatio, setCropData} from "src/composables/photos/editor/crop";
|
import {aspectRatioOptions, croppedArea, cropperReady, selectedAspectRatio, setCropData} from "src/composables/photos/editor/crop";
|
||||||
import {fetchPhoto, loading, photo, photoAspectRatio} from "src/composables/photos/editor/photo";
|
import {fetchPhoto, loading, photo, photoAspectRatio} from "src/composables/photos/editor/photo";
|
||||||
import {debounce, useQuasar} from "quasar";
|
import {debounce, throttle, useQuasar} from "quasar";
|
||||||
import {successObject} from "src/composables/dialog";
|
import {successObject} from "src/composables/dialog";
|
||||||
|
|
||||||
const $route = useRoute();
|
const $route = useRoute();
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const photoId = parseInt($route.params.id);
|
const photoId = parseInt($route.params.id);
|
||||||
const noCacheTimestamp = ref((new Date()).getTime());
|
const noCacheTimestamp = ref((new Date()).getTime());
|
||||||
const editorTab = ref("colors");
|
const editorTab = ref("rotate");
|
||||||
|
|
||||||
// adjustments
|
// adjustments
|
||||||
const defaultAdjustments = {
|
const defaultAdjustments = {
|
||||||
@@ -28,19 +28,16 @@ const defaultAdjustments = {
|
|||||||
};
|
};
|
||||||
const adjustment = ref({...defaultAdjustments});
|
const adjustment = ref({...defaultAdjustments});
|
||||||
const croppedAdjusted = ref("");
|
const croppedAdjusted = ref("");
|
||||||
|
const croppedAdjustedUnrotated = ref("");
|
||||||
const uncroppedAdjusted = ref("");
|
const uncroppedAdjusted = ref("");
|
||||||
const croppedUnadjusted = ref("");
|
const croppedUnadjusted = ref("");
|
||||||
|
|
||||||
watch(() => [adjustment.value, croppedAreaForPreview.value], debounce(() => {
|
watch(() => [adjustment.value, croppedAreaForPreview.value], debounce(() => {
|
||||||
croppedAdjusted.value = getPreviewUrl(photoId, {...adjustment.value, ...croppedAreaForPreview.value});
|
croppedAdjusted.value = getPreviewUrl(photoId, {...adjustment.value, ...croppedAreaForPreview.value});
|
||||||
|
croppedAdjustedUnrotated.value = getPreviewUrl(photoId, {...adjustment.value, rotate: 0, ...croppedAreaForPreview.value});
|
||||||
uncroppedAdjusted.value = getPreviewUrl(photoId, {...adjustment.value});
|
uncroppedAdjusted.value = getPreviewUrl(photoId, {...adjustment.value});
|
||||||
croppedUnadjusted.value = getPreviewUrl(photoId, {rotate: adjustment.value.rotate, ...croppedAreaForPreview.value});
|
croppedUnadjusted.value = getPreviewUrl(photoId, {rotate: adjustment.value.rotate, ...croppedAreaForPreview.value});
|
||||||
}, 100), {deep: true, immediate: true});
|
}, 200), {deep: true, immediate: true});
|
||||||
|
|
||||||
watch(() => adjustment.value.rotate, (newVal) => {
|
|
||||||
if (!cropper) return;
|
|
||||||
cropper.rotateTo(newVal);
|
|
||||||
});
|
|
||||||
|
|
||||||
const savePhotoAdjustment = (photoId) => {
|
const savePhotoAdjustment = (photoId) => {
|
||||||
const adjustmentInput = {...adjustment.value, crop: croppedArea.value};
|
const adjustmentInput = {...adjustment.value, crop: croppedArea.value};
|
||||||
@@ -64,6 +61,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (photoAdjustment) {
|
if (photoAdjustment) {
|
||||||
|
rotate.value = photoAdjustment.rotate;
|
||||||
croppedArea.value = {
|
croppedArea.value = {
|
||||||
left: photoAdjustment.cropLeft,
|
left: photoAdjustment.cropLeft,
|
||||||
top: photoAdjustment.cropTop,
|
top: photoAdjustment.cropTop,
|
||||||
@@ -92,51 +90,57 @@ onMounted(() => {
|
|||||||
<template v-if="!loading">
|
<template v-if="!loading">
|
||||||
<q-card :style="`width: ${splitterWidth}px; margin: 0 auto`">
|
<q-card :style="`width: ${splitterWidth}px; margin: 0 auto`">
|
||||||
<VuePictureCropper
|
<VuePictureCropper
|
||||||
v-if="editorTab === 'rotate_crop'"
|
v-if="editorTab === 'crop'"
|
||||||
:img="uncroppedAdjusted"
|
:img="uncroppedAdjusted"
|
||||||
:boxStyle="{
|
:boxStyle="{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
backgroundColor: '#f8f8f8',
|
backgroundColor: '#f8f8f8',
|
||||||
margin: 'auto',
|
margin: 'auto',
|
||||||
}"
|
}"
|
||||||
:options="{
|
:options="{
|
||||||
viewMode: 1,
|
viewMode: 1,
|
||||||
autoCropArea: 1,
|
autoCropArea: 1,
|
||||||
dragMode: 'none',
|
dragMode: 'none',
|
||||||
zoomable: false,
|
zoomable: false,
|
||||||
draggable: false,
|
draggable: false,
|
||||||
}"
|
rotatable: false,
|
||||||
@cropend="setCropData"
|
}"
|
||||||
@ready="cropperReady"
|
@cropend="setCropData"
|
||||||
|
@ready="cropperReady"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="overflow-hidden relative-position" v-else-if="editorTab === 'rotate'">
|
||||||
|
<div class="rotate-overlay"></div>
|
||||||
|
<q-img :src="croppedAdjustedUnrotated" fit="contain" :style="`transform: rotate(${adjustment.rotate}deg)`"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="overflow-hidden" v-else>
|
<div class="overflow-hidden" v-else>
|
||||||
<q-img fit="contain" :src="croppedAdjusted" v-show="!compare"/>
|
<q-img fit="contain" :src="croppedAdjusted" v-show="!compare"/>
|
||||||
<q-splitter
|
<q-splitter
|
||||||
v-show="compare"
|
v-show="compare"
|
||||||
v-model="splitter"
|
v-model="splitter"
|
||||||
:limits="[0, 100]"
|
:limits="[0, 100]"
|
||||||
:style="{
|
:style="{
|
||||||
height: Math.round(splitterWidth / photoAspectRatio) + 'px',
|
height: Math.round(splitterWidth / photoAspectRatio) + 'px',
|
||||||
width: splitterWidth.value + 'px'
|
width: splitterWidth.value + 'px'
|
||||||
}"
|
}"
|
||||||
before-class="overflow-hidden"
|
before-class="overflow-hidden"
|
||||||
after-class="overflow-hidden">
|
after-class="overflow-hidden">
|
||||||
<template #before>
|
<template #before>
|
||||||
<img
|
<img
|
||||||
:width="splitterWidth"
|
:width="splitterWidth"
|
||||||
class="absolute-top-left"
|
class="absolute-top-left"
|
||||||
:src="croppedUnadjusted"
|
:src="croppedUnadjusted"
|
||||||
alt="before"
|
alt="before"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #after>
|
<template #after>
|
||||||
<img
|
<img
|
||||||
:width="splitterWidth"
|
:width="splitterWidth"
|
||||||
class="absolute-top-right"
|
class="absolute-top-right"
|
||||||
:src="croppedAdjusted"
|
:src="croppedAdjusted"
|
||||||
alt="after"
|
alt="after"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</q-splitter>
|
</q-splitter>
|
||||||
@@ -154,7 +158,8 @@ onMounted(() => {
|
|||||||
<q-tabs v-model="editorTab">
|
<q-tabs v-model="editorTab">
|
||||||
<q-tab name="colors" icon="palette" label="Barvy"/>
|
<q-tab name="colors" icon="palette" label="Barvy"/>
|
||||||
<q-tab name="details" icon="deblur" label="Detialy"/>
|
<q-tab name="details" icon="deblur" label="Detialy"/>
|
||||||
<q-tab name="rotate_crop" icon="crop_rotate" label="Oříznout a otočit"/>
|
<q-tab name="rotate" icon="rotate" label="Otočit"/>
|
||||||
|
<q-tab name="crop" icon="crop" label="Oříznout"/>
|
||||||
</q-tabs>
|
</q-tabs>
|
||||||
<q-separator/>
|
<q-separator/>
|
||||||
|
|
||||||
@@ -167,12 +172,13 @@ onMounted(() => {
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-slider
|
<q-slider
|
||||||
v-model="adjustment.contrast"
|
v-model="adjustment.contrast"
|
||||||
:min="0.5"
|
:min="0.5"
|
||||||
:max="1.5"
|
:max="1.5"
|
||||||
:step="0.01"
|
:step="0.01"
|
||||||
@dblclick="adjustment.contrast=1"
|
label-always
|
||||||
:label="true"
|
@dblclick="adjustment.contrast=1"
|
||||||
|
:label="true"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
@@ -183,12 +189,13 @@ onMounted(() => {
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-slider
|
<q-slider
|
||||||
v-model="adjustment.brightness"
|
v-model="adjustment.brightness"
|
||||||
:min="0.5"
|
:min="0.5"
|
||||||
:max="1.5"
|
:max="1.5"
|
||||||
:step="0.01"
|
:step="0.01"
|
||||||
:label="true"
|
:label="true"
|
||||||
@dblclick="adjustment.brightness = 1"
|
label-always
|
||||||
|
@dblclick="adjustment.brightness = 1"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
@@ -199,12 +206,13 @@ onMounted(() => {
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-slider
|
<q-slider
|
||||||
v-model="adjustment.saturation"
|
v-model="adjustment.saturation"
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="2"
|
:max="2"
|
||||||
:step="0.05"
|
:step="0.05"
|
||||||
:label="true"
|
:label="true"
|
||||||
@dblclick="adjustment.saturation = 1"
|
label-always
|
||||||
|
@dblclick="adjustment.saturation = 1"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
@@ -218,44 +226,52 @@ onMounted(() => {
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-slider
|
<q-slider
|
||||||
v-model="adjustment.sharpness"
|
v-model="adjustment.sharpness"
|
||||||
:min="-3"
|
:min="-3"
|
||||||
:max="8"
|
:max="8"
|
||||||
:step="1"
|
:step="1"
|
||||||
:label="true"
|
:label="true"
|
||||||
@dblclick="adjustment.sharpness=1"
|
label-always
|
||||||
|
snap
|
||||||
|
@dblclick="adjustment.sharpness=1"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
<q-tab-panel name="rotate_crop">
|
<q-tab-panel name="rotate">
|
||||||
<q-btn-toggle
|
|
||||||
no-caps
|
|
||||||
spread
|
|
||||||
v-model="selectedAspectRatio"
|
|
||||||
toggle-color="primary"
|
|
||||||
:options="aspectRatioOptions"
|
|
||||||
class="q-ma-lg"
|
|
||||||
/>
|
|
||||||
<q-list>
|
<q-list>
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon name="flip_camera_android"/>
|
<q-btn round flat icon="rotate_left" @click="adjustment.rotate -= 0.1"/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-slider
|
<q-slider
|
||||||
v-model="adjustment.rotate"
|
v-model="adjustment.rotate"
|
||||||
:min="-90"
|
:min="-45"
|
||||||
:max="90"
|
:max="45"
|
||||||
:step="0.1"
|
:step="0.1"
|
||||||
:label="true"
|
:label="true"
|
||||||
|
label-always
|
||||||
@dblclick="adjustment.rotate=0"
|
@dblclick="adjustment.rotate=0"
|
||||||
/>
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-btn round flat icon="rotate_right" @click="adjustment.rotate += 0.1"/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
<q-tab-panel name="crop">
|
||||||
|
<q-btn-toggle
|
||||||
|
no-caps
|
||||||
|
spread
|
||||||
|
v-model="selectedAspectRatio"
|
||||||
|
toggle-color="primary"
|
||||||
|
:options="aspectRatioOptions"
|
||||||
|
class="q-ma-lg"
|
||||||
|
/>
|
||||||
|
</q-tab-panel>
|
||||||
</q-tab-panels>
|
</q-tab-panels>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-actions>
|
<q-card-actions>
|
||||||
@@ -265,3 +281,17 @@ onMounted(() => {
|
|||||||
</q-card>
|
</q-card>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.rotate-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 888;
|
||||||
|
background-size: 60px 60px;
|
||||||
|
background-image: linear-gradient(to right, rgba(61, 4, 4, 0.5) 1px, transparent 1px),
|
||||||
|
linear-gradient(to bottom, rgba(61, 4, 4, 0.5) 1px, transparent 1px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user