Pridan fotoeditor
This commit is contained in:
@@ -24,6 +24,7 @@
|
|||||||
"vue": "^3.2.47",
|
"vue": "^3.2.47",
|
||||||
"vue-chartjs": "^5.2.0",
|
"vue-chartjs": "^5.2.0",
|
||||||
"vue-inner-image-zoom": "^2.0.0",
|
"vue-inner-image-zoom": "^2.0.0",
|
||||||
|
"vue-picture-cropper": "^0.7.0",
|
||||||
"vue-router": "^4.1.6",
|
"vue-router": "^4.1.6",
|
||||||
"vue3-google-map": "^0.15.0"
|
"vue3-google-map": "^0.15.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const poisWithGPS = computed(() => flight.value.track.map(poi => poi.pointOfInte
|
|||||||
|
|
||||||
const mapMarkers = [flight.value.takeoffAirport].concat(poisWithGPS.value).concat([flight.value.landingAirport]);
|
const mapMarkers = [flight.value.takeoffAirport].concat(poisWithGPS.value).concat([flight.value.landingAirport]);
|
||||||
const pointsOfInterests = computed(() => mapMarkers
|
const pointsOfInterests = computed(() => mapMarkers
|
||||||
.filter(marker => marker.gpsLatitude && marker.gpsLongiture)
|
.filter(marker => marker.gpsLatitude && marker.gpsLongitude)
|
||||||
.map(
|
.map(
|
||||||
marker => ({label: marker.name, position: {lat: marker.gpsLatitude, lng: marker.gpsLongitude}})
|
marker => ({label: marker.name, position: {lat: marker.gpsLatitude, lng: marker.gpsLongitude}})
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const track = ref([]);
|
const track = ref([]);
|
||||||
const speed = ref([]);
|
|
||||||
const elevation = ref([]);
|
|
||||||
|
|
||||||
const bounds = computed(() => {
|
const bounds = computed(() => {
|
||||||
const photosTransformed = props.photos.map(p => ({position: {lat: p.gpsLatitude, lng: p.gpsLongitude}}));
|
const photosTransformed = props.photos.map(p => ({position: {lat: p.gpsLatitude, lng: p.gpsLongitude}}));
|
||||||
const allData = [...props.markers, ...track.value, ...photosTransformed];
|
const allData = [...props.markers, ...track.value, ...photosTransformed];
|
||||||
|
|||||||
@@ -73,9 +73,9 @@ const openBatchEdit = () => {
|
|||||||
emits('batch-edit', photos);
|
emits('batch-edit', photos);
|
||||||
}
|
}
|
||||||
|
|
||||||
const batchRotate = (angle) => {
|
const batchRotate = (direction) => {
|
||||||
const photos = selectedPhotos.value;
|
const photos = selectedPhotos.value;
|
||||||
emits("batch-rotate", {photos, angle});
|
emits("batch-rotate", {photos, direction});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isBatchEditAllowed = computed(() => Object.values(batchEdit.value).filter(v => v).length > 0);
|
const isBatchEditAllowed = computed(() => Object.values(batchEdit.value).filter(v => v).length > 0);
|
||||||
@@ -139,9 +139,9 @@ const batchOptionsProps = computed(() => ({
|
|||||||
<q-icon name="gps_fixed" v-if="photo.gpsLatitude && photo.gpsLongitude"/>
|
<q-icon name="gps_fixed" v-if="photo.gpsLatitude && photo.gpsLongitude"/>
|
||||||
<q-icon name="gps_off" v-else/>
|
<q-icon name="gps_off" v-else/>
|
||||||
</div>
|
</div>
|
||||||
|
<q-btn flat icon="edit" color="black" :to="{name: 'photoEditor', params: {id: photo.id}}" />
|
||||||
<q-btn flat icon="edit" color="blue" @click="openEdit(photo)"></q-btn>
|
<q-btn flat icon="info" color="blue" @click="openEdit(photo)" />
|
||||||
<q-btn flat icon="delete" color="red" @click="confirmPhotoDelete(photo.id)"></q-btn>
|
<q-btn flat icon="delete" color="red" @click="confirmPhotoDelete(photo.id)" />
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,9 +151,9 @@ const batchOptionsProps = computed(() => ({
|
|||||||
<q-card>
|
<q-card>
|
||||||
<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="edit" :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(90)"/>
|
<q-btn v-bind="batchOptionsProps" icon="rotate_left" :color="isBatchEditAllowed ? 'secondary' : 'grey'" @click="batchRotate('counterClockwise')"/>
|
||||||
<q-btn v-bind="batchOptionsProps" icon="rotate_right" :color="isBatchEditAllowed ? 'secondary' : 'grey'" @click="batchRotate(-90)"/>
|
<q-btn v-bind="batchOptionsProps" icon="rotate_right" :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>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {useFlightsStore} from "stores/Flights";
|
|||||||
import {ref} from "vue";
|
import {ref} from "vue";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import {confirmObject} from "src/composables/dialog";
|
import {confirmObject} from "src/composables/dialog";
|
||||||
import {useAppStore} from "stores/App";
|
|
||||||
|
|
||||||
export const flightName = (flight) => {
|
export const flightName = (flight) => {
|
||||||
const date = formatDatetime(flight.takeoffDatetime, FORMAT_DATE);
|
const date = formatDatetime(flight.takeoffDatetime, FORMAT_DATE);
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import {computed, ref, watch} from "vue";
|
||||||
|
import {cropper} from "vue-picture-cropper";
|
||||||
|
import {roundWithPrecision} from "src/composables/utils";
|
||||||
|
import {photoAspectRatio} from "src/composables/photos/editor/photo";
|
||||||
|
|
||||||
|
export const cropperImgSizes = computed(() => [cropper.getImageData().width, cropper.getImageData().height]);
|
||||||
|
export const croppedArea = ref(null);
|
||||||
|
export const selectedAspectRatio = ref(null);
|
||||||
|
|
||||||
|
watch(() => selectedAspectRatio.value, (newVal) => {
|
||||||
|
cropper.setAspectRatio(newVal);
|
||||||
|
})
|
||||||
|
|
||||||
|
export const setCropData = () => {
|
||||||
|
const [w, h] = cropperImgSizes.value;
|
||||||
|
|
||||||
|
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(() =>
|
||||||
|
[
|
||||||
|
{label: '16:9', value: roundWithPrecision(16 / 9, 5)},
|
||||||
|
{label: '4:3', value: roundWithPrecision(4 / 3, 5)},
|
||||||
|
{label: '9:16', value: roundWithPrecision(9 / 16, 5)},
|
||||||
|
{label: '3:4', value: roundWithPrecision(3 / 4, 5)},
|
||||||
|
{label: '1:1', value: 1},
|
||||||
|
{label: 'Volný', value: null},
|
||||||
|
{label: 'Původní', value: roundWithPrecision(photoAspectRatio.value, 5)},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const cropperReady = (e) => {
|
||||||
|
if (croppedArea.value) {
|
||||||
|
const [w, h] = cropperImgSizes.value;
|
||||||
|
selectedAspectRatio.value = roundWithPrecision((croppedArea.value.width * w) / (croppedArea.value.height * h), 5);
|
||||||
|
|
||||||
|
cropper.setCropBoxData({
|
||||||
|
left: croppedArea.value.left * w,
|
||||||
|
top: croppedArea.value.top * h,
|
||||||
|
width: croppedArea.value.width * w,
|
||||||
|
height: croppedArea.value.height * h
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
selectedAspectRatio.value = roundWithPrecision(photoAspectRatio.value, 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import {computed, ref} from "vue";
|
||||||
|
import {usePhotosStore} from "stores/Photo";
|
||||||
|
|
||||||
|
export const loading = ref(true);
|
||||||
|
export const photo = ref(null);
|
||||||
|
export const fetchPhoto = (id) => {
|
||||||
|
loading.value = true;
|
||||||
|
return usePhotosStore().fetchDetail(id)
|
||||||
|
.then(resp => {
|
||||||
|
photo.value = resp;
|
||||||
|
return resp;
|
||||||
|
})
|
||||||
|
.finally(() => loading.value = false);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const photoAspectRatio = computed(() => photo.value.width / photo.value.height);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import {computed} from "vue";
|
||||||
|
import {croppedArea} from "src/composables/photos/editor/crop";
|
||||||
|
|
||||||
|
export const getPreviewUrl = (photoId, adjustments) => {
|
||||||
|
const url = new URL(`${process.env.API_URL}/photo/editor-preview/${photoId}`);
|
||||||
|
url.search = new URLSearchParams(adjustments).toString();
|
||||||
|
return url.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const croppedAreaForPreview = computed(() => {
|
||||||
|
if (!croppedArea.value) return {};
|
||||||
|
return {
|
||||||
|
crop_left: croppedArea.value.left || 0,
|
||||||
|
crop_top: croppedArea.value.top || 0,
|
||||||
|
crop_width: croppedArea.value.width || 1,
|
||||||
|
crop_height: croppedArea.value.height || 1
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import {computed, ref} from "vue";
|
||||||
|
import {photoAspectRatio} from "src/composables/photos/editor/photo";
|
||||||
|
|
||||||
|
export const compare = ref(false);
|
||||||
|
|
||||||
|
export const splitterWidth = ref(750); // TODO: doresit onresize a zmenu velikosti
|
||||||
|
export const splitter = ref(50);
|
||||||
|
export const onResize = (info) => {
|
||||||
|
splitterWidth.value = info.width < 750 ? info.width : 750;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export const roundWithPrecision = (number, precision) => {
|
||||||
|
const factor = Math.pow(10, precision);
|
||||||
|
return Math.round(number * factor) / factor;
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ export const FRAGMENT_PHOTO_FULL_INFO = gql`
|
|||||||
id
|
id
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
width
|
||||||
|
height
|
||||||
isFlightCover
|
isFlightCover
|
||||||
gpsLatitude
|
gpsLatitude
|
||||||
gpsLongitude
|
gpsLongitude
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
|
import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
|
||||||
|
import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight_list";
|
||||||
|
|
||||||
export const UPLOAD_PHOTO = gql`
|
export const UPLOAD_PHOTO = gql`
|
||||||
mutation uploadPhoto($input: UploadPhotoInput!) {
|
mutation uploadPhoto($input: UploadPhotoInput!) {
|
||||||
@@ -21,16 +22,49 @@ mutation editPhoto($id: Int!, $input: EditPhotoInput!) {
|
|||||||
${FRAGMENT_PHOTO_FULL_INFO}
|
${FRAGMENT_PHOTO_FULL_INFO}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ROTATE_PHOTO = gql`
|
export const CHANGE_PHOTO_ORIENTATION = gql`
|
||||||
mutation rotatePhoto($id: Int!, $angle: Int!) {
|
mutation changeOrientation($id: Int!, $direction: String!) {
|
||||||
rotatePhoto(id: $id, angle: $angle) {
|
changeOrientation(id: $id, direction: $direction) {
|
||||||
...photoFullInfo
|
...photoFullInfo
|
||||||
|
|
||||||
|
flight {
|
||||||
|
...flightListInfo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
${FRAGMENT_PHOTO_FULL_INFO}
|
${FRAGMENT_PHOTO_FULL_INFO}
|
||||||
|
${FRAGMENT_FLIGHT_LIST_INFO}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const ADJUST_PHOTO = gql`
|
||||||
|
mutation adjustPhoto($id: Int!, $adjustment: AdjustmentInput!) {
|
||||||
|
adjustPhoto(id: $id, adjustment: $adjustment) {
|
||||||
|
...photoFullInfo
|
||||||
|
|
||||||
|
adjustment {
|
||||||
|
contrast
|
||||||
|
saturation
|
||||||
|
brightness
|
||||||
|
rotate
|
||||||
|
sharpness
|
||||||
|
cropLeft
|
||||||
|
cropTop
|
||||||
|
cropWidth
|
||||||
|
cropHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
flight {
|
||||||
|
...flightListInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_PHOTO_FULL_INFO}
|
||||||
|
${FRAGMENT_FLIGHT_LIST_INFO}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// TODO: ^^ udelat z toho asi jeden fragment a pouzit i v query GET_PHOTO
|
||||||
|
|
||||||
export const DELETE_PHOTO = gql`
|
export const DELETE_PHOTO = gql`
|
||||||
mutation deletePhoto($id: Int!) {
|
mutation deletePhoto($id: Int!) {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import gql from "graphql-tag";
|
||||||
|
import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
|
||||||
|
import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight_list";
|
||||||
|
|
||||||
|
export const PHOTO_DETAIL = gql`
|
||||||
|
query photo($id: Int!) {
|
||||||
|
photo(id: $id) {
|
||||||
|
...photoFullInfo
|
||||||
|
|
||||||
|
adjustment {
|
||||||
|
contrast
|
||||||
|
saturation
|
||||||
|
brightness
|
||||||
|
rotate
|
||||||
|
sharpness
|
||||||
|
cropLeft
|
||||||
|
cropTop
|
||||||
|
cropWidth
|
||||||
|
cropHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
flight {
|
||||||
|
...flightListInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_PHOTO_FULL_INFO}
|
||||||
|
${FRAGMENT_FLIGHT_LIST_INFO}
|
||||||
|
`;
|
||||||
@@ -101,10 +101,10 @@ const deletePhoto = (id) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const rotatePhotos = ({photos, angle}) => {
|
const rotatePhotos = ({photos, direction}) => {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for (const photo of photos) {
|
for (const photo of photos) {
|
||||||
promises.push(usePhotosStore().rotate(photo.id, angle));
|
promises.push(usePhotosStore().changeOrientation(photo.id, direction));
|
||||||
}
|
}
|
||||||
|
|
||||||
let success = 0;
|
let success = 0;
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
<script setup>
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
import {computed, onMounted, ref, watch} from "vue";
|
||||||
|
import {usePhotosStore} from "stores/Photo";
|
||||||
|
import {useAppStore} from "stores/App";
|
||||||
|
import {flightName} from "src/composables/objects/flight";
|
||||||
|
import VuePictureCropper, {cropper} from 'vue-picture-cropper';
|
||||||
|
import {croppedAreaForPreview, getPreviewUrl} from "src/composables/photos/editor/preview";
|
||||||
|
import {compare, onResize, splitter, splitterWidth} from "src/composables/photos/editor/splitter";
|
||||||
|
import {aspectRatioOptions, croppedArea, cropperReady, selectedAspectRatio, setCropData} from "src/composables/photos/editor/crop";
|
||||||
|
import {fetchPhoto, loading, photo, photoAspectRatio} from "src/composables/photos/editor/photo";
|
||||||
|
import {debounce, useQuasar} from "quasar";
|
||||||
|
import {successObject} from "src/composables/dialog";
|
||||||
|
|
||||||
|
const $route = useRoute();
|
||||||
|
const $q = useQuasar();
|
||||||
|
const photoId = parseInt($route.params.id);
|
||||||
|
const noCacheTimestamp = ref((new Date()).getTime());
|
||||||
|
const editorTab = ref("colors");
|
||||||
|
|
||||||
|
// adjustments
|
||||||
|
const defaultAdjustments = {
|
||||||
|
saturation: 1,
|
||||||
|
brightness: 1,
|
||||||
|
contrast: 1,
|
||||||
|
sharpness: 1,
|
||||||
|
rotate: 0,
|
||||||
|
};
|
||||||
|
const adjustment = ref({...defaultAdjustments});
|
||||||
|
const croppedAdjusted = ref("");
|
||||||
|
const uncroppedAdjusted = ref("");
|
||||||
|
const croppedUnadjusted = ref("");
|
||||||
|
|
||||||
|
watch(() => [adjustment.value, croppedAreaForPreview.value], debounce(() => {
|
||||||
|
croppedAdjusted.value = getPreviewUrl(photoId, {...adjustment.value, ...croppedAreaForPreview.value});
|
||||||
|
uncroppedAdjusted.value = getPreviewUrl(photoId, {...adjustment.value});
|
||||||
|
croppedUnadjusted.value = getPreviewUrl(photoId, {rotate: adjustment.value.rotate, ...croppedAreaForPreview.value});
|
||||||
|
}, 100), {deep: true, immediate: true});
|
||||||
|
|
||||||
|
watch(() => adjustment.value.rotate, (newVal) => {
|
||||||
|
if (!cropper) return;
|
||||||
|
cropper.rotateTo(newVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
const savePhotoAdjustment = (photoId) => {
|
||||||
|
const adjustmentInput = {...adjustment.value, crop: croppedArea.value};
|
||||||
|
usePhotosStore().adjust(photoId, adjustmentInput).then((data) => {
|
||||||
|
noCacheTimestamp.value = (new Date()).getTime();
|
||||||
|
photo.value = data;
|
||||||
|
|
||||||
|
$q.notify(successObject("Fotka úspěšně upravena"));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchPhoto(photoId).then(() => {
|
||||||
|
const flightNameFormatted = flightName(photo.value.flight);
|
||||||
|
|
||||||
|
if (photo.value.adjustment) {
|
||||||
|
const photoAdjustment = photo.value.adjustment;
|
||||||
|
const src = photoAdjustment || defaultAdjustments;
|
||||||
|
for (const i in defaultAdjustments) {
|
||||||
|
adjustment.value[i] = src[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (photoAdjustment) {
|
||||||
|
croppedArea.value = {
|
||||||
|
left: photoAdjustment.cropLeft,
|
||||||
|
top: photoAdjustment.cropTop,
|
||||||
|
width: photoAdjustment.cropWidth,
|
||||||
|
height: photoAdjustment.cropHeight
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
useAppStore().setBreadcrumbs([
|
||||||
|
{to: {name: "myFlights"}, label: "Moje lety"},
|
||||||
|
{to: {name: "flightDetail", params: {id: photo.value.flightId}}, label: flightNameFormatted},
|
||||||
|
{to: {name: "flightEdit", params: {id: photo.value.flightId, step: 'photos'}}, label: "Fotografie"},
|
||||||
|
{label: "Foto editor"}
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
|
||||||
|
useAppStore().setPageTitle("Foto editor");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-resize-observer @resize="onResize" :debounce="100"/>
|
||||||
|
|
||||||
|
<template v-if="loading">Nacitani</template>
|
||||||
|
|
||||||
|
<template v-if="!loading">
|
||||||
|
<q-card :style="`width: ${splitterWidth}px; margin: 0 auto`">
|
||||||
|
<VuePictureCropper
|
||||||
|
v-if="editorTab === 'rotate_crop'"
|
||||||
|
:img="uncroppedAdjusted"
|
||||||
|
:boxStyle="{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
backgroundColor: '#f8f8f8',
|
||||||
|
margin: 'auto',
|
||||||
|
}"
|
||||||
|
:options="{
|
||||||
|
viewMode: 1,
|
||||||
|
autoCropArea: 1,
|
||||||
|
dragMode: 'none',
|
||||||
|
zoomable: false,
|
||||||
|
draggable: false,
|
||||||
|
}"
|
||||||
|
@cropend="setCropData"
|
||||||
|
@ready="cropperReady"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="overflow-hidden" v-else>
|
||||||
|
<q-img fit="contain" :src="croppedAdjusted" v-show="!compare"/>
|
||||||
|
<q-splitter
|
||||||
|
v-show="compare"
|
||||||
|
v-model="splitter"
|
||||||
|
:limits="[0, 100]"
|
||||||
|
:style="{
|
||||||
|
height: Math.round(splitterWidth / photoAspectRatio) + 'px',
|
||||||
|
width: splitterWidth.value + 'px'
|
||||||
|
}"
|
||||||
|
before-class="overflow-hidden"
|
||||||
|
after-class="overflow-hidden">
|
||||||
|
<template #before>
|
||||||
|
<img
|
||||||
|
:width="splitterWidth"
|
||||||
|
class="absolute-top-left"
|
||||||
|
:src="croppedUnadjusted"
|
||||||
|
alt="before"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #after>
|
||||||
|
<img
|
||||||
|
:width="splitterWidth"
|
||||||
|
class="absolute-top-right"
|
||||||
|
:src="croppedAdjusted"
|
||||||
|
alt="after"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-splitter>
|
||||||
|
|
||||||
|
<div class="text-right q-pt-md q-pr-md">
|
||||||
|
|
||||||
|
<q-btn flat icon="compare" @click="compare = !compare" :text-color="compare ? 'primary' : 'grey-5'">
|
||||||
|
<q-tooltip>Klikni pro porovnání</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-card-section>
|
||||||
|
<q-separator/>
|
||||||
|
<q-tabs v-model="editorTab">
|
||||||
|
<q-tab name="colors" icon="palette" label="Barvy"/>
|
||||||
|
<q-tab name="details" icon="deblur" label="Detialy"/>
|
||||||
|
<q-tab name="rotate_crop" icon="crop_rotate" label="Oříznout a otočit"/>
|
||||||
|
</q-tabs>
|
||||||
|
<q-separator/>
|
||||||
|
|
||||||
|
<q-tab-panels v-model="editorTab" animated>
|
||||||
|
<q-tab-panel name="colors">
|
||||||
|
<q-list>
|
||||||
|
<q-item>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="contrast"/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-slider
|
||||||
|
v-model="adjustment.contrast"
|
||||||
|
:min="0.5"
|
||||||
|
:max="1.5"
|
||||||
|
:step="0.01"
|
||||||
|
@dblclick="adjustment.contrast=1"
|
||||||
|
:label="true"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="brightness_6"/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-slider
|
||||||
|
v-model="adjustment.brightness"
|
||||||
|
:min="0.5"
|
||||||
|
:max="1.5"
|
||||||
|
:step="0.01"
|
||||||
|
:label="true"
|
||||||
|
@dblclick="adjustment.brightness = 1"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="palette"/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-slider
|
||||||
|
v-model="adjustment.saturation"
|
||||||
|
:min="0"
|
||||||
|
:max="2"
|
||||||
|
:step="0.05"
|
||||||
|
:label="true"
|
||||||
|
@dblclick="adjustment.saturation = 1"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-tab-panel>
|
||||||
|
<q-tab-panel name="details">
|
||||||
|
<q-list>
|
||||||
|
<q-item>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="deblur"/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-slider
|
||||||
|
v-model="adjustment.sharpness"
|
||||||
|
:min="-3"
|
||||||
|
:max="8"
|
||||||
|
:step="1"
|
||||||
|
:label="true"
|
||||||
|
@dblclick="adjustment.sharpness=1"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-tab-panel>
|
||||||
|
<q-tab-panel name="rotate_crop">
|
||||||
|
<q-btn-toggle
|
||||||
|
no-caps
|
||||||
|
spread
|
||||||
|
v-model="selectedAspectRatio"
|
||||||
|
toggle-color="primary"
|
||||||
|
:options="aspectRatioOptions"
|
||||||
|
class="q-ma-lg"
|
||||||
|
/>
|
||||||
|
<q-list>
|
||||||
|
<q-item>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="flip_camera_android"/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-slider
|
||||||
|
v-model="adjustment.rotate"
|
||||||
|
:min="-90"
|
||||||
|
:max="90"
|
||||||
|
:step="0.1"
|
||||||
|
:label="true"
|
||||||
|
@dblclick="adjustment.rotate=0"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions>
|
||||||
|
<q-space/>
|
||||||
|
<q-btn color="primary" icon="save" @click="savePhotoAdjustment(photoId)">Uložit</q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
@@ -50,6 +50,8 @@ const routes = [
|
|||||||
{path: 'letadla/:id', name: "aircraftDetail", component: () => import('pages/profile/aircraft/AircraftDetailPage.vue')},
|
{path: 'letadla/:id', name: "aircraftDetail", component: () => import('pages/profile/aircraft/AircraftDetailPage.vue')},
|
||||||
{path: 'letadla/:id/edit', name: "aircraftEdit", component: () => import('pages/profile/aircraft/EditAircraftPage.vue')},
|
{path: 'letadla/:id/edit', name: "aircraftEdit", component: () => import('pages/profile/aircraft/EditAircraftPage.vue')},
|
||||||
|
|
||||||
|
{path: 'foto-editor/:id', name: "photoEditor", component: () => import("pages/profile/flight/PhotoEditorPage.vue")},
|
||||||
|
|
||||||
{path: 'udalosti', name: "events", component: () => import('pages/profile/event/EventsPage.vue')},
|
{path: 'udalosti', name: "events", component: () => import('pages/profile/event/EventsPage.vue')},
|
||||||
{path: 'udalosti/novy', name: "addEvent", component: () => import('pages/profile/event/NewEventPage.vue')},
|
{path: 'udalosti/novy', name: "addEvent", component: () => import('pages/profile/event/NewEventPage.vue')},
|
||||||
{path: 'udalosti/:id', name: "eventDetail", component: () => import('pages/profile/event/EventDetailPage.vue')},
|
{path: 'udalosti/:id', name: "eventDetail", component: () => import('pages/profile/event/EventDetailPage.vue')},
|
||||||
|
|||||||
+20
-5
@@ -1,6 +1,7 @@
|
|||||||
import {defineStore} from "pinia";
|
import {defineStore} from "pinia";
|
||||||
import {apolloWrapper} from "src/graphql/apollo";
|
import {apolloWrapper} from "src/graphql/apollo";
|
||||||
import {DELETE_PHOTO, EDIT_PHOTO, ROTATE_PHOTO, UPLOAD_PHOTO} from "src/graphql/mutations/photo";
|
import {ADJUST_PHOTO, CHANGE_PHOTO_ORIENTATION, DELETE_PHOTO, EDIT_PHOTO, UPLOAD_PHOTO} from "src/graphql/mutations/photo";
|
||||||
|
import {PHOTO_DETAIL} from "src/graphql/queries/photo";
|
||||||
|
|
||||||
export const usePhotosStore = defineStore('Photos', {
|
export const usePhotosStore = defineStore('Photos', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@@ -8,6 +9,11 @@ export const usePhotosStore = defineStore('Photos', {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
fetchDetail(id) {
|
||||||
|
return apolloWrapper.query(PHOTO_DETAIL, {id}).then(({data}) => {
|
||||||
|
return data.photo;
|
||||||
|
});
|
||||||
|
},
|
||||||
upload(flightId, formData) {
|
upload(flightId, formData) {
|
||||||
if (!flightId) {
|
if (!flightId) {
|
||||||
console.error("No flightId specified for photo upload! Aborting");
|
console.error("No flightId specified for photo upload! Aborting");
|
||||||
@@ -35,16 +41,25 @@ export const usePhotosStore = defineStore('Photos', {
|
|||||||
return data.editPhoto;
|
return data.editPhoto;
|
||||||
}).finally(() => this.isMutationRunning = false);
|
}).finally(() => this.isMutationRunning = false);
|
||||||
},
|
},
|
||||||
rotate(id, angle) {
|
|
||||||
|
changeOrientation(id, direction) {
|
||||||
this.isMutationRunning = true;
|
this.isMutationRunning = true;
|
||||||
return apolloWrapper.mutate(ROTATE_PHOTO, {id, angle}).then(({data}) => {
|
return apolloWrapper.mutate(CHANGE_PHOTO_ORIENTATION, {id, direction}).then(({data}) => {
|
||||||
return data.rotatePhoto;
|
return data.changeOrientation;
|
||||||
|
}).finally(() => this.isMutationRunning = false);
|
||||||
|
},
|
||||||
|
|
||||||
|
adjust(id, adjustment) {
|
||||||
|
this.isMutationRunning = true;
|
||||||
|
return apolloWrapper.mutate(ADJUST_PHOTO, {id, adjustment}).then(({data}) => {
|
||||||
|
return data.adjustPhoto;
|
||||||
}).finally(() => this.isMutationRunning = false);
|
}).finally(() => this.isMutationRunning = false);
|
||||||
},
|
},
|
||||||
delete(photoId) {
|
delete(photoId) {
|
||||||
|
this.isMutationRunning = true;
|
||||||
return apolloWrapper.mutate(DELETE_PHOTO, {id: photoId}).then(({data}) => {
|
return apolloWrapper.mutate(DELETE_PHOTO, {id: photoId}).then(({data}) => {
|
||||||
return data.deletePhoto;
|
return data.deletePhoto;
|
||||||
});
|
}).finally(() => this.isMutationRunning = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,6 +26,13 @@
|
|||||||
resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.21.3.tgz"
|
resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.21.3.tgz"
|
||||||
integrity sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==
|
integrity sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==
|
||||||
|
|
||||||
|
"@bassist/utils@^0.4.0":
|
||||||
|
version "0.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@bassist/utils/-/utils-0.4.0.tgz#58eb247b37afc3c405543884731a134f522fc64b"
|
||||||
|
integrity sha512-aoFTl0jUjm8/tDZodP41wnEkvB+C5O9NFCuYN/ztL6jSUSsuBkXq90/1ifBm1XhV/zySHgLYlU1+tgo3XtQ+nA==
|
||||||
|
dependencies:
|
||||||
|
"@withtypes/mime" "^0.1.2"
|
||||||
|
|
||||||
"@esbuild/linux-loong64@0.14.54":
|
"@esbuild/linux-loong64@0.14.54":
|
||||||
version "0.14.54"
|
version "0.14.54"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028"
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028"
|
||||||
@@ -414,6 +421,13 @@
|
|||||||
resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.2.47.tgz"
|
resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.2.47.tgz"
|
||||||
integrity sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==
|
integrity sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==
|
||||||
|
|
||||||
|
"@withtypes/mime@^0.1.2":
|
||||||
|
version "0.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@withtypes/mime/-/mime-0.1.2.tgz#956883251b0786f2c898ed76a23594ea6d9939e0"
|
||||||
|
integrity sha512-PB9BfZGzwblUONJY0LiOwsHCA6uV3DIPj/w9ReekdHxPOl0VdUFgI5s4avKycuuq9Gf5Nz2ZPA2O36GAUzlMPA==
|
||||||
|
dependencies:
|
||||||
|
mime "^3.0.0"
|
||||||
|
|
||||||
"@wry/context@^0.7.0":
|
"@wry/context@^0.7.0":
|
||||||
version "0.7.2"
|
version "0.7.2"
|
||||||
resolved "https://registry.npmjs.org/@wry/context/-/context-0.7.2.tgz"
|
resolved "https://registry.npmjs.org/@wry/context/-/context-0.7.2.tgz"
|
||||||
@@ -901,6 +915,11 @@ crc32-stream@^4.0.2:
|
|||||||
crc-32 "^1.2.0"
|
crc-32 "^1.2.0"
|
||||||
readable-stream "^3.4.0"
|
readable-stream "^3.4.0"
|
||||||
|
|
||||||
|
cropperjs@^1.6.1:
|
||||||
|
version "1.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/cropperjs/-/cropperjs-1.6.1.tgz#fd132021d93b824b1b0f2c2c3b763419fb792d89"
|
||||||
|
integrity sha512-F4wsi+XkDHCOMrHMYjrTEE4QBOrsHHN5/2VsVAaRq8P7E5z7xQpT75S+f/9WikmBEailas3+yo+6zPIomW+NOA==
|
||||||
|
|
||||||
cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz"
|
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz"
|
||||||
@@ -2099,6 +2118,11 @@ mime@1.6.0:
|
|||||||
resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz"
|
resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz"
|
||||||
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||||
|
|
||||||
|
mime@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7"
|
||||||
|
integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==
|
||||||
|
|
||||||
mimic-fn@^2.1.0:
|
mimic-fn@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz"
|
resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz"
|
||||||
@@ -2988,6 +3012,14 @@ vue-inner-image-zoom@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/vue-inner-image-zoom/-/vue-inner-image-zoom-2.0.0.tgz#d84a8d5f4b76ba8d9675bfb67927aef06c4cf62c"
|
resolved "https://registry.yarnpkg.com/vue-inner-image-zoom/-/vue-inner-image-zoom-2.0.0.tgz#d84a8d5f4b76ba8d9675bfb67927aef06c4cf62c"
|
||||||
integrity sha512-PrCppBbgffCLCpvetEWzt+8qzYs984HrhEj/9649/4nPV9leCeditl2MY5IgKWElWpDgtR3j0O2sLDVcpgGbYA==
|
integrity sha512-PrCppBbgffCLCpvetEWzt+8qzYs984HrhEj/9649/4nPV9leCeditl2MY5IgKWElWpDgtR3j0O2sLDVcpgGbYA==
|
||||||
|
|
||||||
|
vue-picture-cropper@^0.7.0:
|
||||||
|
version "0.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue-picture-cropper/-/vue-picture-cropper-0.7.0.tgz#e9dec1b17542ba6c681a7ba6240282193e101d68"
|
||||||
|
integrity sha512-NF7+Dgso6d0GB16E5d/BbrcTIHm1VWz8dS3IjLhoBl+ZeC+yDA46CyJphQuO32SisaPmrKHN8VbiE2LgAfhnkQ==
|
||||||
|
dependencies:
|
||||||
|
"@bassist/utils" "^0.4.0"
|
||||||
|
cropperjs "^1.6.1"
|
||||||
|
|
||||||
vue-router@^4.1.6:
|
vue-router@^4.1.6:
|
||||||
version "4.1.6"
|
version "4.1.6"
|
||||||
resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.1.6.tgz"
|
resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.1.6.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user