Moznost nahravat fotky copilotem
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {FORMAT_DATE, FORMAT_TIME, formatDatetime, minutesToHumanReadable} from "src/composables/datetimeUtils";
|
import {FORMAT_DATE, FORMAT_TIME, formatDatetime, minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||||
import WeatherInfo from "components/weather/WeatherInfo.vue";
|
import WeatherInfo from "components/weather/WeatherInfo.vue";
|
||||||
|
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
flight: {
|
flight: {
|
||||||
@@ -26,6 +27,11 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
|
},
|
||||||
|
loggedUser: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -144,14 +150,31 @@ const props = defineProps({
|
|||||||
<q-item v-for="copilot in flight.copilots" :key="copilot.id">
|
<q-item v-for="copilot in flight.copilots" :key="copilot.id">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon name="people"/>
|
<q-icon name="people"/>
|
||||||
</q-item-section>
|
</q-item-section>3
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
|
<template v-if="copilot.isPublic">
|
||||||
|
<div>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="copilot.isPublic"
|
|
||||||
:to="{name: copilotDetailRoute.name, params: {...copilotDetailRoute.params, copilotSlug: copilot.urlSlug, id: copilot.id}}"
|
:to="{name: copilotDetailRoute.name, params: {...copilotDetailRoute.params, copilotSlug: copilot.urlSlug, id: copilot.id}}"
|
||||||
>
|
>
|
||||||
{{ copilot.name }}
|
{{ copilot.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
|
<CopyLinkButton
|
||||||
|
:route='{
|
||||||
|
name: "publicCopilotUploadPhoto",
|
||||||
|
params: {flightSlug: flight.urlSlug, username: loggedUser.publicUsername},
|
||||||
|
query: {token: copilot.token}
|
||||||
|
}'
|
||||||
|
icon="cloud_upload"
|
||||||
|
title="Získat odkaz na nahrání fotek od kopilota"
|
||||||
|
label=""
|
||||||
|
round
|
||||||
|
class="q-mx-3"
|
||||||
|
v-if="loggedUser && copilot.token"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template v-else> {{ copilot.name }}</template>
|
<template v-else> {{ copilot.name }}</template>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import FormWrapper from "components/FormWrapper.vue";
|
import FormWrapper from "components/FormWrapper.vue";
|
||||||
import {reactive} from "vue";
|
import {reactive} from "vue";
|
||||||
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
|
||||||
import {roundWithPrecision} from "../../../composables/utils";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
flight: {
|
flight: {
|
||||||
|
|||||||
@@ -7,6 +7,16 @@ const props = defineProps({
|
|||||||
route: {
|
route: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: "link"
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: "Získat odkaz"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -24,9 +34,10 @@ const copyLinkToClipboard = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-btn
|
<q-btn
|
||||||
icon="link"
|
:icon="icon"
|
||||||
@click="copyLinkToClipboard()"
|
:label="label"
|
||||||
|
v-bind="$attrs"
|
||||||
flat
|
flat
|
||||||
label="Získat odkaz"
|
@click="copyLinkToClipboard()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import {computed, onMounted, reactive, ref, watch} from "vue";
|
import {computed, onMounted, reactive, ref, watch} from "vue";
|
||||||
import {useQuasar} from "quasar";
|
import {useQuasar} from "quasar";
|
||||||
import {confirmObject} from "src/composables/dialog";
|
import {confirmObject} from "src/composables/dialog";
|
||||||
|
import {usePhotoBatchManagement} from "src/composables/photos/batch";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
photos: {
|
photos: {
|
||||||
@@ -19,19 +20,12 @@ const props = defineProps({
|
|||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
|
||||||
const batchEdit = ref({});
|
|
||||||
for (const photo of props.photos) {
|
|
||||||
batchEdit.value[photo.id] = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emits = defineEmits(["edit", "delete", "set-title-photo", "batch-edit", "batch-rotate"])
|
const emits = defineEmits(["edit", "delete", "set-title-photo", "batch-edit", "batch-rotate"])
|
||||||
|
|
||||||
const openEdit = (photo) => {
|
const openEdit = (photo) => {
|
||||||
emits("edit", photo);
|
emits("edit", photo);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const confirmPhotoDelete = (id) => {
|
const confirmPhotoDelete = (id) => {
|
||||||
$q.dialog(confirmObject("Opravdu chces smazat tuto fotku?"))
|
$q.dialog(confirmObject("Opravdu chces smazat tuto fotku?"))
|
||||||
.onOk(() => {
|
.onOk(() => {
|
||||||
@@ -39,80 +33,10 @@ const confirmPhotoDelete = (id) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmBatchPhotoDelete = () => {
|
const {
|
||||||
$q.dialog(confirmObject("Opravdu chces smazat vybrané fotky?"))
|
openBatchEdit, batchEdit, handleImageClick, confirmBatchPhotoDelete, batchOptionsProps,
|
||||||
.onOk(() => {
|
isBatchEditAllowed, batchRotate, checkAll
|
||||||
for (const id in batchEdit.value) {
|
} = usePhotoBatchManagement({photos: props.photos, emits});
|
||||||
if (!batchEdit.value[id]) continue;
|
|
||||||
emits("delete", parseInt(id));
|
|
||||||
batchEdit.value[id] = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleImageClick = (photo, idx) => {
|
|
||||||
batchEdit.value[photo.id] = !batchEdit.value[photo.id];
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedPhotos = computed(() => {
|
|
||||||
const photos = [];
|
|
||||||
for (const i in batchEdit.value) {
|
|
||||||
const photo = props.photos.find(p => p.id === parseInt(i));
|
|
||||||
if (photo && batchEdit.value[i]) photos.push(photo);
|
|
||||||
}
|
|
||||||
|
|
||||||
return photos;
|
|
||||||
});
|
|
||||||
|
|
||||||
const openBatchEdit = () => {
|
|
||||||
const photos = selectedPhotos.value;
|
|
||||||
for (const i in batchEdit.value) {
|
|
||||||
batchEdit.value[i] = false;
|
|
||||||
}
|
|
||||||
emits('batch-edit', photos);
|
|
||||||
}
|
|
||||||
|
|
||||||
const batchRotate = (direction) => {
|
|
||||||
const photos = selectedPhotos.value;
|
|
||||||
emits("batch-rotate", {photos, direction});
|
|
||||||
}
|
|
||||||
|
|
||||||
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});
|
|
||||||
|
|
||||||
const batchOptionsProps = computed(() => ({
|
|
||||||
round: true,
|
|
||||||
flat: true,
|
|
||||||
class: "q-ml-lg",
|
|
||||||
disable: !isBatchEditAllowed.value
|
|
||||||
}));
|
|
||||||
|
|
||||||
const setFlightCover = (titlePhoto) => {
|
const setFlightCover = (titlePhoto) => {
|
||||||
emits("set-title-photo", titlePhoto);
|
emits("set-title-photo", titlePhoto);
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<script setup>
|
||||||
|
import {useQuasar} from "quasar";
|
||||||
|
import {confirmObject} from "src/composables/dialog";
|
||||||
|
import {usePhotoBatchManagement} from "src/composables/photos/batch";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
photos: {
|
||||||
|
type: Array
|
||||||
|
},
|
||||||
|
titlePhoto: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
showActions: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
|
||||||
|
const emits = defineEmits(["delete", "batch-edit", "batch-rotate"])
|
||||||
|
|
||||||
|
const confirmPhotoDelete = (id) => {
|
||||||
|
$q.dialog(confirmObject("Opravdu chces smazat tuto fotku?"))
|
||||||
|
.onOk(() => {
|
||||||
|
emits("delete", id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
batchEdit, handleImageClick, confirmBatchPhotoDelete, batchOptionsProps, isBatchEditAllowed, batchRotate, checkAll
|
||||||
|
} = usePhotoBatchManagement({photos: props.photos, emits});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row" v-bind="$attrs">
|
||||||
|
<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)" style="cursor: pointer"/>
|
||||||
|
<q-card-actions align="right" :class="{'bg-primary': batchEdit[photo.id], 'text-white': batchEdit[photo.id]}" v-if="showActions">
|
||||||
|
<div class="col-grow">
|
||||||
|
<q-checkbox v-model="batchEdit[photo.id]"/>
|
||||||
|
</div>
|
||||||
|
<q-btn flat icon="delete" color="red" @click="confirmPhotoDelete(photo.id)"/>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-page-sticky :offset="[50, 50]" position="bottom" v-if="photos.length > 0 && showActions">
|
||||||
|
<q-card>
|
||||||
|
<q-card-actions>
|
||||||
|
<q-checkbox v-model="checkAll" label="Vybrat vše"/>
|
||||||
|
<q-btn v-bind="batchOptionsProps" icon="rotate_90_degrees_ccw" :color="isBatchEditAllowed ? 'secondary' : 'grey'" @click="batchRotate('counterClockwise')"/>
|
||||||
|
<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-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-page-sticky>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import {flightName} from "src/composables/flight";
|
||||||
|
|
||||||
|
const homepageBreadcrumbs = () => {
|
||||||
|
return [{to: {name: 'homepage'}, label: "Polétání.cz"}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: dodelat
|
||||||
|
|
||||||
|
const publicProfileBreadcrumbs = (pilot) => {
|
||||||
|
return [
|
||||||
|
...homepageBreadcrumbs(),
|
||||||
|
{to: {name: "publicProfileDetail", params: {username: pilot.publicUsername}}, label: `@${pilot.publicUsername}`},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicEventBreadcrumbs = (pilot, event) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const flightBreadcrumbs = (pilot, flight, event) => {
|
||||||
|
const breadcrumbs = publicProfileBreadcrumbs(pilot)
|
||||||
|
|
||||||
|
if (event && event.isPublic) {
|
||||||
|
breadcrumbs.push({to: {name: 'publicEventsList', username: pilot.publicUsername}, label: "Události"});
|
||||||
|
breadcrumbs.push({
|
||||||
|
to: {
|
||||||
|
name: 'publicEventDetail',
|
||||||
|
params: {eventSlug: event.urlSlug, username: pilot.publicUsername}
|
||||||
|
}, label: event.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
breadcrumbs.push({label: flightName(flight)})
|
||||||
|
|
||||||
|
return breadcrumbs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const copilotBreadcrumbs = (pilot, flight, event, copilot) => {
|
||||||
|
return [
|
||||||
|
...flightBreadcrumbs(pilot, flight, event),
|
||||||
|
{label: copilot.name}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import {ref} from "vue";
|
||||||
|
import {usePhotosStore} from "stores/Photos";
|
||||||
|
import {errorObject, successObject, warningObject} from "src/composables/dialog";
|
||||||
|
import {useQuasar} from "quasar";
|
||||||
|
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
import {useFlightsStore} from "stores/Flights";
|
||||||
|
|
||||||
|
export const useBasicPhotoOperations = ({copilotUploadToken}) => {
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
const {flight} = storeToRefs(useFlightsStore());
|
||||||
|
const {photos} = storeToRefs(usePhotosStore());
|
||||||
|
|
||||||
|
// TODO: V PRIVATNI CASTI JE TO ZATIM ROZBITE!!!
|
||||||
|
|
||||||
|
|
||||||
|
const uploadInProgress = ref(false);
|
||||||
|
const handlePhotosUpload = (data) => {
|
||||||
|
const promises = [];
|
||||||
|
uploadInProgress.value = true;
|
||||||
|
|
||||||
|
for (const photo of data.photos) {
|
||||||
|
promises.push(usePhotosStore().upload(flight.value.id, {photo, copilotUploadToken}).then((data) => {
|
||||||
|
photos.value.push(data);
|
||||||
|
photo._uploaded = true;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.allSettled(promises).then((values) => {
|
||||||
|
const success = values.filter(v => v.status === 'fulfilled');
|
||||||
|
const failed = values.filter(v => v.status === 'rejected'); // TODO: nebo vse - success?
|
||||||
|
|
||||||
|
if (success.length > 0)
|
||||||
|
$q.notify(successObject(`Úspěšně nahráno ${success.length} fotek`));
|
||||||
|
|
||||||
|
if (failed.length > 0)
|
||||||
|
$q.notify(warningObject(`Nepovedlo se nahrát ${failed.length} fotek. Zde jsou důvody: ${failed.map(f => f.reason)}`));
|
||||||
|
|
||||||
|
data.photos = [];
|
||||||
|
}).finally(() => uploadInProgress.value = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const _findPhotoIndex = (photoId) => photos.value.findIndex((object) => object.id === photoId);
|
||||||
|
const deletePhoto = (id) => {
|
||||||
|
usePhotosStore().destroy(id, copilotUploadToken).then((data) => {
|
||||||
|
photos.value.splice(_findPhotoIndex(data.id), 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchRotatePhotos = (data) => {
|
||||||
|
const selectedPhotos = data.photos;
|
||||||
|
const direction = data.direction;
|
||||||
|
|
||||||
|
const promises = [];
|
||||||
|
for (const photo of selectedPhotos) {
|
||||||
|
promises.push(usePhotosStore().changeOrientation(photo.id, direction, copilotUploadToken).then(data => {
|
||||||
|
const idx = _findPhotoIndex(data.id);
|
||||||
|
data.thumbnailUrl += "?t=" + (new Date()).getTime();
|
||||||
|
photos.value[idx] = data;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let success = 0;
|
||||||
|
Promise.allSettled(promises).then(result => {
|
||||||
|
for (const item of result) {
|
||||||
|
if (item.status === 'fulfilled') success++;
|
||||||
|
else console.error(item.reason)
|
||||||
|
}
|
||||||
|
const error = result.length - success;
|
||||||
|
if (success > 0) $q.notify(successObject("Otočeno " + success + " fotografií"));
|
||||||
|
if (error > 0) $q.notify(errorObject(`Nepovedlo se otočit ${error} fotografi`))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSinglePhoto = (id, formData) => {
|
||||||
|
formData.copilots = formData.copilots.map(c => ({id: c.id, name: c.name}));
|
||||||
|
|
||||||
|
return usePhotosStore().update(id, formData).then((data) => {
|
||||||
|
const idx = _findPhotoIndex(data.id);
|
||||||
|
photos.value[idx] = data;
|
||||||
|
usePointsOfInterestStore().fetch({});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
uploadInProgress, handlePhotosUpload, batchRotatePhotos, deletePhoto, updateSinglePhoto
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import {computed, ref, watch} from "vue";
|
||||||
|
import {confirmObject} from "src/composables/dialog";
|
||||||
|
import {useQuasar} from "quasar";
|
||||||
|
|
||||||
|
|
||||||
|
export const usePhotoBatchManagement = ({photos, emits}) => {
|
||||||
|
const batchEdit = ref({});
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
// console.log(photos);
|
||||||
|
for (const photo of photos) {
|
||||||
|
batchEdit.value[photo.id] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageClick = (photo, idx) => {
|
||||||
|
batchEdit.value[photo.id] = !batchEdit.value[photo.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedPhotos = computed(() => {
|
||||||
|
const selected = [];
|
||||||
|
for (const i in batchEdit.value) {
|
||||||
|
const photo = photos.find(p => p.id === parseInt(i));
|
||||||
|
if (photo && batchEdit.value[i]) selected.push(photo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected;
|
||||||
|
});
|
||||||
|
|
||||||
|
const confirmBatchPhotoDelete = () => {
|
||||||
|
$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 openBatchEdit = () => {
|
||||||
|
const photos = selectedPhotos.value;
|
||||||
|
for (const i in batchEdit.value) {
|
||||||
|
batchEdit.value[i] = false;
|
||||||
|
}
|
||||||
|
emits('batch-edit', photos);
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchRotate = (direction) => {
|
||||||
|
const photos = selectedPhotos.value;
|
||||||
|
emits("batch-rotate", {photos, direction});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 photos) {
|
||||||
|
batchEdit.value[photo.id] = newVal;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const batchOptionsProps = computed(() => ({
|
||||||
|
round: true,
|
||||||
|
flat: true,
|
||||||
|
class: "q-ml-lg",
|
||||||
|
disable: !isBatchEditAllowed.value
|
||||||
|
}));
|
||||||
|
|
||||||
|
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});
|
||||||
|
|
||||||
|
return {
|
||||||
|
batchEdit, handleImageClick, selectedPhotos, confirmBatchPhotoDelete,
|
||||||
|
openBatchEdit, batchRotate, checkAll, isBatchEditAllowed, batchOptionsProps
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -11,18 +11,26 @@ fragment copilotListInfo on Copilot {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FRAGMENT_COPILOT_FULL_INFO = gql`
|
export const FRAGMENT_COPILOT_PUBLIC_INFO = gql`
|
||||||
fragment copilotFullInfo on Copilot {
|
fragment copilotPublicInfo on Copilot {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
isPublic
|
isPublic
|
||||||
urlSlug
|
urlSlug
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const FRAGMENT_COPILOT_FULL_INFO = gql`
|
||||||
|
fragment copilotFullInfo on Copilot {
|
||||||
|
...copilotPublicInfo
|
||||||
|
|
||||||
titlePhoto {
|
titlePhoto {
|
||||||
id
|
id
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_COPILOT_PUBLIC_INFO}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FRAGMENT_COPILOT_FULL_PRIVATE_INFO = gql`
|
export const FRAGMENT_COPILOT_FULL_PRIVATE_INFO = gql`
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {FRAGMENT_AIRCRAFT_FULL_INFO} from "src/graphql/fragments/aircraft";
|
|||||||
import {FRAGMENT_AIRPORT_INFO} from "src/graphql/fragments/airport";
|
import {FRAGMENT_AIRPORT_INFO} from "src/graphql/fragments/airport";
|
||||||
import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
|
import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
|
||||||
import {FRAGMENT_POI_FULL_INFO} from "src/graphql/fragments/poi";
|
import {FRAGMENT_POI_FULL_INFO} from "src/graphql/fragments/poi";
|
||||||
|
import {FRAGMENT_COPILOT_PUBLIC_INFO} from "src/graphql/fragments/copilot";
|
||||||
|
|
||||||
export const FRAGMENT_WEATHER_FULL_INFO = gql`
|
export const FRAGMENT_WEATHER_FULL_INFO = gql`
|
||||||
fragment weatherFullInfo on WeatherInfo {
|
fragment weatherFullInfo on WeatherInfo {
|
||||||
@@ -51,13 +52,6 @@ fragment flightFullInfo on Flight {
|
|||||||
...weatherFullInfo
|
...weatherFullInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
copilots {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
isPublic
|
|
||||||
urlSlug
|
|
||||||
}
|
|
||||||
|
|
||||||
event {
|
event {
|
||||||
id
|
id
|
||||||
isPublic
|
isPublic
|
||||||
@@ -107,6 +101,11 @@ fragment flightFullPrivateInfo on Flight {
|
|||||||
...flightFullInfo
|
...flightFullInfo
|
||||||
debriefingNotes
|
debriefingNotes
|
||||||
|
|
||||||
|
copilots {
|
||||||
|
...copilotPublicInfo
|
||||||
|
token
|
||||||
|
}
|
||||||
|
|
||||||
photos {
|
photos {
|
||||||
...photoFullInfo
|
...photoFullInfo
|
||||||
|
|
||||||
@@ -124,4 +123,5 @@ fragment flightFullPrivateInfo on Flight {
|
|||||||
|
|
||||||
${FRAGMENT_FLIGHT_FULL_INFO}
|
${FRAGMENT_FLIGHT_FULL_INFO}
|
||||||
${FRAGMENT_PHOTO_FULL_INFO}
|
${FRAGMENT_PHOTO_FULL_INFO}
|
||||||
|
${FRAGMENT_COPILOT_PUBLIC_INFO}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ mutation editPhoto($id: Int!, $input: EditPhotoInput!) {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const CHANGE_PHOTO_ORIENTATION = gql`
|
export const CHANGE_PHOTO_ORIENTATION = gql`
|
||||||
mutation changeOrientation($id: Int!, $direction: String!) {
|
mutation changeOrientation($id: Int!, $direction: String!, $copilotUploadToken: String) {
|
||||||
changeOrientation(id: $id, direction: $direction) {
|
changeOrientation(id: $id, direction: $direction, copilotUploadToken: $copilotUploadToken) {
|
||||||
...photoFullInfo
|
...photoFullInfo
|
||||||
|
|
||||||
flight {
|
flight {
|
||||||
@@ -67,8 +67,8 @@ mutation adjustPhoto($id: Int!, $adjustment: AdjustmentInput!) {
|
|||||||
// TODO: ^^ udelat z toho asi jeden fragment a pouzit i v query GET_PHOTO
|
// 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!, $copilotUploadToken: String) {
|
||||||
deletePhoto(id: $id) {
|
deletePhoto(id: $id, copilotUploadToken: $copilotUploadToken) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,14 @@ ${FRAGMENT_FLIGHT_LIST_INFO}
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const COPILOT_PUBLIC_DETAIL = gql`
|
export const COPILOT_PUBLIC_DETAIL = gql`
|
||||||
query copilot($urlSlug: String!, $pilotUsername: String) {
|
query copilot($urlSlug: String, $uploadToken: String, $uploadFlightSlug: String, $pilotUsername: String!) {
|
||||||
copilot(urlSlug: $urlSlug, pilotUsername: $pilotUsername, public: true) {
|
copilot(
|
||||||
|
urlSlug: $urlSlug,
|
||||||
|
uploadToken: $uploadToken,
|
||||||
|
uploadFlightSlug: $uploadFlightSlug,
|
||||||
|
pilotUsername: $pilotUsername,
|
||||||
|
public: true
|
||||||
|
) {
|
||||||
...copilotFullInfo
|
...copilotFullInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import gql from "graphql-tag";
|
|||||||
import {FRAGMENT_FLIGHT_FULL_INFO, FRAGMENT_FLIGHT_FULL_PRIVATE_INFO} from "src/graphql/fragments/flight";
|
import {FRAGMENT_FLIGHT_FULL_INFO, FRAGMENT_FLIGHT_FULL_PRIVATE_INFO} from "src/graphql/fragments/flight";
|
||||||
import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight_list";
|
import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight_list";
|
||||||
import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
|
import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
|
||||||
|
import {FRAGMENT_COPILOT_PUBLIC_INFO} from "src/graphql/fragments/copilot";
|
||||||
|
|
||||||
export const FLIGHTS_LIST = gql`
|
export const FLIGHTS_LIST = gql`
|
||||||
query FlightsList($eventId: Int, $aircraftId: Int, $copilotId: Int, $pointOfInterestId: Int, $limit: Int!, $offset: Int) {
|
query FlightsList($eventId: Int, $aircraftId: Int, $copilotId: Int, $pointOfInterestId: Int, $limit: Int!, $offset: Int) {
|
||||||
@@ -58,12 +59,17 @@ ${FRAGMENT_FLIGHT_FULL_PRIVATE_INFO}
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const FLIGHT_PUBLIC_DETAIL = gql`
|
export const FLIGHT_PUBLIC_DETAIL = gql`
|
||||||
query FlightPublicDetail($urlSlug: String!, $username: String) {
|
query FlightPublicDetail($urlSlug: String, $username: String) {
|
||||||
flight(urlSlug: $urlSlug, username: $username, public: true) {
|
flight(urlSlug: $urlSlug, username: $username, public: true) {
|
||||||
...flightFullInfo
|
...flightFullInfo
|
||||||
|
|
||||||
|
copilots {
|
||||||
|
...copilotPublicInfo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
${FRAGMENT_FLIGHT_FULL_INFO}
|
${FRAGMENT_FLIGHT_FULL_INFO}
|
||||||
|
${FRAGMENT_COPILOT_PUBLIC_INFO}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,21 @@ ${FRAGMENT_FLIGHT_LIST_INFO}
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const PHOTOS_PUBLIC_LIST = gql`
|
export const PHOTOS_PUBLIC_LIST = gql`
|
||||||
query publicPhotos($flightId: Int, $pointOfInterestId: Int, $aircraftId: Int, $copilotId: Int) {
|
query publicPhotos(
|
||||||
photos(public: true, flightId: $flightId, pointOfInterestId: $pointOfInterestId, aircraftId: $aircraftId, copilotId: $copilotId) {
|
$flightId: Int,
|
||||||
|
$pointOfInterestId: Int,
|
||||||
|
$aircraftId: Int,
|
||||||
|
$copilotId: Int,
|
||||||
|
$uploadedByCopilotId: Int
|
||||||
|
) {
|
||||||
|
photos(
|
||||||
|
public: true,
|
||||||
|
flightId: $flightId,
|
||||||
|
pointOfInterestId: $pointOfInterestId,
|
||||||
|
aircraftId: $aircraftId,
|
||||||
|
copilotId: $copilotId,
|
||||||
|
uploadedByCopilotId: $uploadedByCopilotId
|
||||||
|
) {
|
||||||
...photoFullInfo
|
...photoFullInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,39 @@
|
|||||||
|
<script>
|
||||||
|
import {useFlightsStore} from "stores/Flights";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preFetch({currentRoute}) {
|
||||||
|
const flightId = currentRoute.params.id
|
||||||
|
return useFlightsStore().fetchDetail(flightId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import FlightFormSteps from "components/flights/form/FlightFormSteps.vue";
|
import FlightFormSteps from "components/flights/form/FlightFormSteps.vue";
|
||||||
import {computed, onMounted, ref, watch} from "vue";
|
import {onMounted, ref, toRefs, watch} from "vue";
|
||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import {useFlightsStore} from "stores/Flights";
|
|
||||||
import FlightFormBasic from "components/flights/form/FlightFormBasic.vue";
|
import FlightFormBasic from "components/flights/form/FlightFormBasic.vue";
|
||||||
import {useAircraftsStore} from "stores/Aircrafts";
|
import {useAircraftsStore} from "stores/Aircrafts";
|
||||||
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
||||||
import {useAirportsStore} from "stores/Airports";
|
import {useAirportsStore} from "stores/Airports";
|
||||||
import {useQuasar} from "quasar";
|
import {useQuasar} from "quasar";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import {flightName} from "src/composables/flight";
|
|
||||||
import FlightFormCopilot from "components/flights/form/FlightFormCopilot.vue";
|
import FlightFormCopilot from "components/flights/form/FlightFormCopilot.vue";
|
||||||
import {useCopilotsStore} from "stores/Copilots";
|
import {useCopilotsStore} from "stores/Copilots";
|
||||||
import FlightFormNotes from "components/flights/form/FlightFormNotes.vue";
|
import FlightFormNotes from "components/flights/form/FlightFormNotes.vue";
|
||||||
import FlightFormPhotos from "components/flights/form/FlightFormPhotos.vue";
|
import FlightFormPhotos from "components/flights/form/FlightFormPhotos.vue";
|
||||||
import {usePhotosStore} from "stores/Photos";
|
|
||||||
import PhotosEditorList from "components/photos/PhotosEditorList.vue";
|
import PhotosEditorList from "components/photos/PhotosEditorList.vue";
|
||||||
import {errorObject, successObject, warningObject} from "src/composables/dialog";
|
import {errorObject, successObject, warningObject} from "src/composables/dialog";
|
||||||
import PhotoFormDialog from "components/photos/PhotoFormDialog.vue";
|
import PhotoFormDialog from "components/photos/PhotoFormDialog.vue";
|
||||||
import FlightFormTrack from "components/flights/form/FlightFormTrack.vue";
|
import FlightFormTrack from "components/flights/form/FlightFormTrack.vue";
|
||||||
import FlightEditSkeleton from "components/flights/skeletons/FlightEditSkeleton.vue";
|
import FlightEditSkeleton from "components/flights/skeletons/FlightEditSkeleton.vue";
|
||||||
import BatchPhotoFormDialog from "components/photos/BatchPhotoFormDialog.vue";
|
import BatchPhotoFormDialog from "components/photos/BatchPhotoFormDialog.vue";
|
||||||
import {useAppStore} from "stores/App";
|
|
||||||
import {useEventsStore} from "stores/Events";
|
import {useEventsStore} from "stores/Events";
|
||||||
import FlightEditCopilotSkeleton from "components/flights/skeletons/FlightEditCopilotSkeleton.vue";
|
import FlightEditCopilotSkeleton from "components/flights/skeletons/FlightEditCopilotSkeleton.vue";
|
||||||
|
import {useBasicPhotoOperations} from "src/composables/photos/basicOperations";
|
||||||
|
import {usePhotosStore} from "stores/Photos";
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const $route = useRoute();
|
const $route = useRoute();
|
||||||
@@ -34,29 +44,33 @@ const {pointsOfInterest} = storeToRefs(usePointsOfInterestStore());
|
|||||||
const {airports} = storeToRefs(useAirportsStore());
|
const {airports} = storeToRefs(useAirportsStore());
|
||||||
const {copilots} = storeToRefs(useCopilotsStore());
|
const {copilots} = storeToRefs(useCopilotsStore());
|
||||||
const {events} = storeToRefs(useEventsStore());
|
const {events} = storeToRefs(useEventsStore());
|
||||||
const {flight, isQueryRunning, isMutationRunning} = storeToRefs(useFlightsStore());
|
const {photos} = storeToRefs(usePhotosStore());
|
||||||
|
const {flight, isQueryRunning, isRequestPending: isMutationRunning} = storeToRefs(useFlightsStore());
|
||||||
|
|
||||||
const formStep = ref("basic");
|
const formStep = ref("basic");
|
||||||
watch(() => $route.params.step, (val) => formStep.value = val);
|
watch(() => $route.params.step, (val) => formStep.value = val);
|
||||||
|
|
||||||
onMounted(() => {
|
import {flightName} from "src/composables/flight";
|
||||||
formStep.value = $route.params.step || "basic";
|
import {useAppStore} from "stores/App";
|
||||||
|
|
||||||
|
const flightNameFormatted = flightName(flight.value);
|
||||||
|
useAppStore().setPageTitle(`Editace - ${flightNameFormatted}`);
|
||||||
|
useAppStore().setBreadcrumbs([
|
||||||
|
{to: {name: "myFlights"}, label: "Moje lety"},
|
||||||
|
{to: {name: "flightDetail", params: {id: flight.id}}, label: flightNameFormatted},
|
||||||
|
{label: "Editace"}
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
photos.value = flight.value.photos;
|
||||||
|
|
||||||
|
formStep.value = $route.params.step || "basic";
|
||||||
useAircraftsStore().fetch({});
|
useAircraftsStore().fetch({});
|
||||||
usePointsOfInterestStore().fetch({});
|
usePointsOfInterestStore().fetch({});
|
||||||
useAirportsStore().fetch();
|
useAirportsStore().fetch();
|
||||||
useCopilotsStore().fetch();
|
useCopilotsStore().fetch();
|
||||||
useEventsStore().fetch({});
|
useEventsStore().fetch({});
|
||||||
|
|
||||||
useFlightsStore().fetchDetail(flightId).then((f) => {
|
|
||||||
const flightNameFormatted = flightName(f);
|
|
||||||
useAppStore().setPageTitle(`Editace - ${flightNameFormatted}`);
|
|
||||||
useAppStore().setBreadcrumbs([
|
|
||||||
{to: {name: "myFlights"}, label: "Moje lety"},
|
|
||||||
{to: {name: "flightDetail", params: {id: flightId}}, label: flightNameFormatted},
|
|
||||||
{label: "Editace"}
|
|
||||||
]);
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFlightEdit = (data) => {
|
const handleFlightEdit = (data) => {
|
||||||
@@ -64,40 +78,9 @@ const handleFlightEdit = (data) => {
|
|||||||
$q.notify(successObject("Úspěšně upraveno"));
|
$q.notify(successObject("Úspěšně upraveno"));
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const {
|
||||||
const isRequestPending = storeToRefs(useFlightsStore()).isMutationRunning;
|
uploadInProgress, handlePhotosUpload, batchRotatePhotos, deletePhoto, updateSinglePhoto
|
||||||
const uploadInProgress = ref(false);
|
} = useBasicPhotoOperations({});
|
||||||
|
|
||||||
const handlePhotosUpload = (data) => {
|
|
||||||
const promises = [];
|
|
||||||
uploadInProgress.value = true;
|
|
||||||
for (const photo of data.photos) {
|
|
||||||
promises.push(usePhotosStore().upload(flightId, {photo}).then((data) => {
|
|
||||||
flight.value.photos.push(data);
|
|
||||||
photo._uploaded = true;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.allSettled(promises).then((values) => {
|
|
||||||
const success = values.filter(v => v.status === 'fulfilled');
|
|
||||||
const failed = values.filter(v => v.status === 'rejected'); // TODO: nebo vse - success?
|
|
||||||
|
|
||||||
if (success.length > 0)
|
|
||||||
$q.notify(successObject(`Úspěšně nahráno ${success.length} fotek`));
|
|
||||||
|
|
||||||
if (failed.length > 0)
|
|
||||||
$q.notify(warningObject(`Nepovedlo se nahrát ${failed.length} fotek. Zde jsou důvody: ${failed.map(f => f.reason)}`));
|
|
||||||
|
|
||||||
data.photos = [];
|
|
||||||
}).finally(() => uploadInProgress.value = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const _findPhotoIndex = (photoId) => flight.value.photos.findIndex((object) => object.id === photoId);
|
|
||||||
const deletePhoto = (id) => {
|
|
||||||
usePhotosStore().destroy(id).then((data) => {
|
|
||||||
flight.value.photos.splice(_findPhotoIndex(data.id), 1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const setTitlePhoto = (photo) => {
|
const setTitlePhoto = (photo) => {
|
||||||
useFlightsStore().update(flight.value.id, {titlePhotoId: photo.id}).then(f => {
|
useFlightsStore().update(flight.value.id, {titlePhotoId: photo.id}).then(f => {
|
||||||
@@ -105,38 +88,6 @@ const setTitlePhoto = (photo) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const rotatePhotos = ({photos, direction}) => {
|
|
||||||
const promises = [];
|
|
||||||
for (const photo of photos) {
|
|
||||||
promises.push(usePhotosStore().changeOrientation(photo.id, direction).then(data => {
|
|
||||||
const idx = _findPhotoIndex(data.id);
|
|
||||||
data.thumbnailUrl += "?t=" + (new Date()).getTime();
|
|
||||||
flight.value.photos[idx] = data;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
let success = 0;
|
|
||||||
Promise.allSettled(promises).then(result => {
|
|
||||||
for (const item of result) {
|
|
||||||
if (item.status === 'fulfilled') success++;
|
|
||||||
}
|
|
||||||
const error = result.length - success;
|
|
||||||
|
|
||||||
if (success > 0) $q.notify(successObject("Otočeno " + success + " fotografií"));
|
|
||||||
if (error > 0) $q.notify(errorObject(`Nepovedlo se otočit ${error} fotografi`))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateSinglePhoto = (id, formData) => {
|
|
||||||
formData.copilots = formData.copilots.map(c => ({id: c.id, name: c.name}));
|
|
||||||
|
|
||||||
return usePhotosStore().update(id, formData).then((data) => {
|
|
||||||
const idx = _findPhotoIndex(data.id);
|
|
||||||
flight.value.photos[idx] = data;
|
|
||||||
usePointsOfInterestStore().fetch({});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePhotoUpdate = (flightId, formData) => {
|
const handlePhotoUpdate = (flightId, formData) => {
|
||||||
const photoId = formData.id;
|
const photoId = formData.id;
|
||||||
delete formData.id;
|
delete formData.id;
|
||||||
@@ -240,18 +191,18 @@ const handleBatchPhotoEdit = async (flightId, data) => {
|
|||||||
<q-tab-panel name="photos">
|
<q-tab-panel name="photos">
|
||||||
<FlightFormPhotos
|
<FlightFormPhotos
|
||||||
:flight="flight"
|
:flight="flight"
|
||||||
:is-request-pending="isRequestPending || uploadInProgress"
|
:is-request-pending="uploadInProgress"
|
||||||
@save="handlePhotosUpload"
|
@save="handlePhotosUpload"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<template v-if="flight">
|
<template v-if="flight">
|
||||||
<PhotosEditorList
|
<PhotosEditorList
|
||||||
:title-photo="flight.titlePhoto"
|
:title-photo="flight.titlePhoto"
|
||||||
:photos="flight.photos"
|
:photos="photos"
|
||||||
:show-actions="true"
|
:show-actions="true"
|
||||||
@edit="openPhotoEdit"
|
@edit="openPhotoEdit"
|
||||||
@batch-edit="openBatchPhotoForm"
|
@batch-edit="openBatchPhotoForm"
|
||||||
@batch-rotate="rotatePhotos"
|
@batch-rotate="batchRotatePhotos"
|
||||||
@delete="deletePhoto"
|
@delete="deletePhoto"
|
||||||
@set-title-photo="setTitlePhoto"
|
@set-title-photo="setTitlePhoto"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ const togglePublicVisibility = (variables) => toggleVisibility({
|
|||||||
:event-detail-route="flight.event ? {name: 'eventDetail', params: {id: flight.event.id}} : null"
|
:event-detail-route="flight.event ? {name: 'eventDetail', params: {id: flight.event.id}} : null"
|
||||||
:aircraft-detail-route="{name: 'aircraftDetail'}"
|
:aircraft-detail-route="{name: 'aircraftDetail'}"
|
||||||
:copilot-detail-route="{name: 'copilotDetail'}"
|
:copilot-detail-route="{name: 'copilotDetail'}"
|
||||||
|
:logged-user="loggedUser"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ContentCardEditButton
|
<ContentCardEditButton
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {useCopilotsStore} from "stores/Copilots";
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
preFetch({currentRoute}) {
|
preFetch({currentRoute}) {
|
||||||
return useCopilotsStore().fetchPublicDetail(currentRoute.params.copilotSlug, currentRoute.params.username);
|
return useCopilotsStore().fetchPublicDetail({urlSlug: currentRoute.params.copilotSlug, username: currentRoute.params.username});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<script>
|
||||||
|
import {useCopilotsStore} from "stores/Copilots";
|
||||||
|
import {useFlightsStore} from "stores/Flights";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preFetch({currentRoute}) {
|
||||||
|
return Promise.allSettled([
|
||||||
|
useFlightsStore().fetchPublicDetail({
|
||||||
|
username: currentRoute.params.username,
|
||||||
|
urlSlug: currentRoute.params.flightSlug
|
||||||
|
}),
|
||||||
|
useCopilotsStore().fetchPublicDetail({
|
||||||
|
username: currentRoute.params.username,
|
||||||
|
uploadToken: currentRoute.query.token,
|
||||||
|
uploadFlightSlug: currentRoute.params.flightSlug
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script setup>
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
import ContentCard from "components/ContentCard.vue";
|
||||||
|
import {useAppStore} from "stores/App";
|
||||||
|
import {useMeta, useQuasar} from "quasar";
|
||||||
|
import {metaData} from "src/composables/meta";
|
||||||
|
import {flightName} from "src/composables/flight";
|
||||||
|
import {usePhotosStore} from "stores/Photos";
|
||||||
|
import {onMounted, ref} from "vue";
|
||||||
|
import PhotosEditorListSimple from "components/photos/PhotosEditorListSimple.vue";
|
||||||
|
import {copilotBreadcrumbs} from "src/composables/breadcrumbs";
|
||||||
|
import FlightFormPhotos from "components/flights/form/FlightFormPhotos.vue";
|
||||||
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
import {useBasicPhotoOperations} from "src/composables/photos/basicOperations";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
pilot: {
|
||||||
|
type: Object,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {copilot} = storeToRefs(useCopilotsStore());
|
||||||
|
const {flight} = storeToRefs(useFlightsStore());
|
||||||
|
const {photos, isQueryRunning: arePhotosLoading} = storeToRefs(usePhotosStore())
|
||||||
|
|
||||||
|
useAppStore().setPageTitle(`Nahraj fotky - ${props.pilot.name} - ${flightName(flight.value)}`);
|
||||||
|
useAppStore().setBreadcrumbs([
|
||||||
|
...copilotBreadcrumbs(props.pilot, flight.value, flight.value.event, copilot.value),
|
||||||
|
{label: "Nahraj fotky"},
|
||||||
|
]);
|
||||||
|
|
||||||
|
useMeta(() => metaData({
|
||||||
|
title: `Nahraj fotky: ${copilot.value.name}`,
|
||||||
|
index: false,
|
||||||
|
image: copilot.value.titleImage?.url
|
||||||
|
}));
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
usePhotosStore().fetchPublic({
|
||||||
|
flightId: flight.value.id,
|
||||||
|
uploadedByCopilotId: copilot.value.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const $route = useRoute();
|
||||||
|
const copilotUploadToken = $route.query.token;
|
||||||
|
|
||||||
|
const {
|
||||||
|
uploadInProgress, handlePhotosUpload, batchRotatePhotos, deletePhoto
|
||||||
|
} = useBasicPhotoOperations({copilotUploadToken});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ContentCard :title="copilot.name">
|
||||||
|
<template #content>
|
||||||
|
<q-card-section>
|
||||||
|
Nahraj fotky k letu {{ flightName(flight) }}
|
||||||
|
|
||||||
|
<FlightFormPhotos
|
||||||
|
:is-request-pending="uploadInProgress"
|
||||||
|
:flight="flight"
|
||||||
|
@save="handlePhotosUpload"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BasicInfoBanner v-if="photos.length === 0 && !arePhotosLoading">
|
||||||
|
Zatím tu od tebe nejsou nahrané žádné fotky.
|
||||||
|
</BasicInfoBanner>
|
||||||
|
<PhotosEditorListSimple
|
||||||
|
v-else
|
||||||
|
style="margin-bottom: 150px"
|
||||||
|
:photos="photos"
|
||||||
|
@batch-rotate="batchRotatePhotos"
|
||||||
|
@delete="deletePhoto"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
</template>
|
||||||
|
</ContentCard>
|
||||||
|
</template>
|
||||||
@@ -81,7 +81,7 @@ useMeta(() => metaData({
|
|||||||
:flight="flight"
|
:flight="flight"
|
||||||
:poi-detail-route="{name: 'publicPOIDetail'}"
|
:poi-detail-route="{name: 'publicPOIDetail'}"
|
||||||
:aircraft-detail-route="{name: 'publicAircraftDetail'}"
|
:aircraft-detail-route="{name: 'publicAircraftDetail'}"
|
||||||
:event-detail-route="event.isPublic ? {name: 'publicEventDetail', params: {username: pilot.publicUsername, eventSlug: event.urlSlug}} : null"
|
:event-detail-route="event && event.isPublic ? {name: 'publicEventDetail', params: {username: pilot.publicUsername, eventSlug: event.urlSlug}} : null"
|
||||||
:copilot-detail-route="{name: 'publicCopilotDetail', params: {username: pilot.publicUsername}}"
|
:copilot-detail-route="{name: 'publicCopilotDetail', params: {username: pilot.publicUsername}}"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const routes = [
|
|||||||
children: [
|
children: [
|
||||||
{path: '', name: "publicProfileDetail", component: () => import('pages/public/pilotProfile/FlightsListPage.vue')},
|
{path: '', name: "publicProfileDetail", component: () => import('pages/public/pilotProfile/FlightsListPage.vue')},
|
||||||
{path: ':flightSlug', name: "publicFlightDetail", component: () => import('pages/public/pilotProfile/FlightDetailPage.vue')},
|
{path: ':flightSlug', name: "publicFlightDetail", component: () => import('pages/public/pilotProfile/FlightDetailPage.vue')},
|
||||||
|
{path: ':flightSlug/nahraj', name: "publicCopilotUploadPhoto", component: () => import('pages/public/pilotProfile/CopilotUploadPhotoPage.vue')},
|
||||||
{path: 'a/:copilotSlug', name: "publicCopilotDetail", component: () => import('pages/public/pilotProfile/CopilotDetailPage.vue')},
|
{path: 'a/:copilotSlug', name: "publicCopilotDetail", component: () => import('pages/public/pilotProfile/CopilotDetailPage.vue')},
|
||||||
{path: 'udalosti', name: "publicEventsList", component: () => import('pages/public/pilotProfile/EventsListPage.vue')},
|
{path: 'udalosti', name: "publicEventsList", component: () => import('pages/public/pilotProfile/EventsListPage.vue')},
|
||||||
{path: 'udalosti/:eventSlug', name: "publicEventDetail", component: () => import('pages/public/pilotProfile/EventDetailPage.vue')},
|
{path: 'udalosti/:eventSlug', name: "publicEventDetail", component: () => import('pages/public/pilotProfile/EventDetailPage.vue')},
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ export const useCopilotsStore = defineStore('Copilots', () => {
|
|||||||
}).then(item => copilot.value = item);
|
}).then(item => copilot.value = item);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchPublicDetail = (urlSlug, username) => {
|
const fetchPublicDetail = ({urlSlug, username, uploadToken, uploadFlightSlug}) => {
|
||||||
return query({
|
return query({
|
||||||
isQueryRunning,
|
isQueryRunning,
|
||||||
query: COPILOT_PUBLIC_DETAIL,
|
query: COPILOT_PUBLIC_DETAIL,
|
||||||
variables: {urlSlug, pilotUsername: username},
|
variables: {urlSlug, pilotUsername: username, uploadToken, uploadFlightSlug},
|
||||||
}).then(item => {
|
}).then(item => {
|
||||||
copilot.value = item;
|
copilot.value = item;
|
||||||
return item;
|
return item;
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ export const usePhotosStore = defineStore('Photos', () => {
|
|||||||
variables: {id}
|
variables: {id}
|
||||||
}).then(item => photo.value = item);
|
}).then(item => photo.value = item);
|
||||||
|
|
||||||
const fetchPublic = ({flightId, pointOfInterestId, aircraftId, copilotId}) => query({
|
const fetchPublic = ({flightId, pointOfInterestId, aircraftId, copilotId, uploadedByCopilotId}) => query({
|
||||||
isQueryRunning,
|
isQueryRunning,
|
||||||
query: PHOTOS_PUBLIC_LIST,
|
query: PHOTOS_PUBLIC_LIST,
|
||||||
variables: {flightId, pointOfInterestId, aircraftId, copilotId}
|
variables: {flightId, pointOfInterestId, aircraftId, copilotId, uploadedByCopilotId}
|
||||||
}).then(items => photos.value = items);
|
}).then(items => photos.value = items);
|
||||||
|
|
||||||
const upload = (flightId, input) => {
|
const upload = (flightId, input) => {
|
||||||
@@ -61,10 +61,10 @@ export const usePhotosStore = defineStore('Photos', () => {
|
|||||||
return item;
|
return item;
|
||||||
})
|
})
|
||||||
|
|
||||||
const changeOrientation = (id, direction) => mutate({
|
const changeOrientation = (id, direction, copilotUploadToken) => mutate({
|
||||||
isMutationRunning,
|
isMutationRunning,
|
||||||
mutation: CHANGE_PHOTO_ORIENTATION,
|
mutation: CHANGE_PHOTO_ORIENTATION,
|
||||||
variables: {id, direction}
|
variables: {id, direction, copilotUploadToken}
|
||||||
}).then(item => {
|
}).then(item => {
|
||||||
photo.value = item;
|
photo.value = item;
|
||||||
return item;
|
return item;
|
||||||
@@ -79,10 +79,10 @@ export const usePhotosStore = defineStore('Photos', () => {
|
|||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
|
|
||||||
const destroy = (id) => mutate({
|
const destroy = (id, copilotUploadToken) => mutate({
|
||||||
isMutationRunning,
|
isMutationRunning,
|
||||||
mutation: DELETE_PHOTO,
|
mutation: DELETE_PHOTO,
|
||||||
variables: {id}
|
variables: {id, copilotUploadToken}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user