Moznost nahravat fotky copilotem

This commit is contained in:
Michal Kváček
2024-03-07 06:45:22 +01:00
parent f0a4c439dc
commit cd9c7351e9
22 changed files with 550 additions and 216 deletions
+30 -7
View File
@@ -1,6 +1,7 @@
<script setup>
import {FORMAT_DATE, FORMAT_TIME, formatDatetime, minutesToHumanReadable} from "src/composables/datetimeUtils";
import WeatherInfo from "components/weather/WeatherInfo.vue";
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
const props = defineProps({
flight: {
@@ -26,6 +27,11 @@ const props = defineProps({
type: Object,
required: false,
default: null
},
loggedUser: {
type: Object,
required: false,
default: null
}
})
</script>
@@ -144,14 +150,31 @@ const props = defineProps({
<q-item v-for="copilot in flight.copilots" :key="copilot.id">
<q-item-section avatar>
<q-icon name="people"/>
</q-item-section>
</q-item-section>3
<q-item-section>
<router-link
v-if="copilot.isPublic"
:to="{name: copilotDetailRoute.name, params: {...copilotDetailRoute.params, copilotSlug: copilot.urlSlug, id: copilot.id}}"
>
{{ copilot.name }}
</router-link>
<template v-if="copilot.isPublic">
<div>
<router-link
:to="{name: copilotDetailRoute.name, params: {...copilotDetailRoute.params, copilotSlug: copilot.urlSlug, id: copilot.id}}"
>
{{ copilot.name }}
</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>
</q-item-section>
</q-item>
@@ -1,8 +1,6 @@
<script setup>
import FormWrapper from "components/FormWrapper.vue";
import {reactive} from "vue";
import ComboboxInput from "components/inputs/ComboboxInput.vue";
import {roundWithPrecision} from "../../../composables/utils";
const props = defineProps({
flight: {
+14 -3
View File
@@ -7,6 +7,16 @@ const props = defineProps({
route: {
type: Object,
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>
<q-btn
icon="link"
@click="copyLinkToClipboard()"
:icon="icon"
:label="label"
v-bind="$attrs"
flat
label="Získat odkaz"
@click="copyLinkToClipboard()"
/>
</template>
+13 -89
View File
@@ -2,6 +2,7 @@
import {computed, onMounted, reactive, ref, watch} from "vue";
import {useQuasar} from "quasar";
import {confirmObject} from "src/composables/dialog";
import {usePhotoBatchManagement} from "src/composables/photos/batch";
const props = defineProps({
photos: {
@@ -19,100 +20,23 @@ const props = defineProps({
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 openEdit = (photo) => {
emits("edit", photo);
};
const confirmPhotoDelete = (id) => {
$q.dialog(confirmObject("Opravdu chces smazat tuto fotku?"))
.onOk(() => {
emits("delete", id);
});
.onOk(() => {
emits("delete", id);
});
}
const confirmBatchPhotoDelete = () => {
$q.dialog(confirmObject("Opravdu chces smazat vybrané fotky?"))
.onOk(() => {
for (const id in batchEdit.value) {
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 {
openBatchEdit, batchEdit, handleImageClick, confirmBatchPhotoDelete, batchOptionsProps,
isBatchEditAllowed, batchRotate, checkAll
} = usePhotoBatchManagement({photos: props.photos, emits});
const setFlightCover = (titlePhoto) => {
emits("set-title-photo", titlePhoto);
@@ -136,10 +60,10 @@ const showLarge = (displayedPhoto) => {
<q-btn icon="fullscreen" color="grey-6" round @click.stop.prevent="showLarge(photo)"/>
</div>
<q-btn
icon="star"
:color="titlePhoto?.id === photo.id ? 'amber-9' : 'grey-6'"
round
@click.stop.prevent="setFlightCover(photo)"
icon="star"
:color="titlePhoto?.id === photo.id ? 'amber-9' : 'grey-6'"
round
@click.stop.prevent="setFlightCover(photo)"
/>
</div>
@@ -183,7 +107,7 @@ const showLarge = (displayedPhoto) => {
<q-dialog v-model="openPhoto" full-width>
<q-img :src="photo.url">
<div class="absolute-top bg-transparent text-right">
<q-btn icon="close" round color="primary" @click="openPhoto = false" />
<q-btn icon="close" round color="primary" @click="openPhoto = false"/>
</div>
</q-img>
</q-dialog>
@@ -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>
+44
View File
@@ -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}
]
}
+89
View File
@@ -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
}
}
+96
View File
@@ -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
}
};
+10 -2
View File
@@ -11,18 +11,26 @@ fragment copilotListInfo on Copilot {
}
`;
export const FRAGMENT_COPILOT_FULL_INFO = gql`
fragment copilotFullInfo on Copilot {
export const FRAGMENT_COPILOT_PUBLIC_INFO = gql`
fragment copilotPublicInfo on Copilot {
id
name
isPublic
urlSlug
}
`
export const FRAGMENT_COPILOT_FULL_INFO = gql`
fragment copilotFullInfo on Copilot {
...copilotPublicInfo
titlePhoto {
id
url
}
}
${FRAGMENT_COPILOT_PUBLIC_INFO}
`;
export const FRAGMENT_COPILOT_FULL_PRIVATE_INFO = gql`
+7 -7
View File
@@ -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_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
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`
fragment weatherFullInfo on WeatherInfo {
@@ -51,13 +52,6 @@ fragment flightFullInfo on Flight {
...weatherFullInfo
}
copilots {
id
name
isPublic
urlSlug
}
event {
id
isPublic
@@ -107,6 +101,11 @@ fragment flightFullPrivateInfo on Flight {
...flightFullInfo
debriefingNotes
copilots {
...copilotPublicInfo
token
}
photos {
...photoFullInfo
@@ -124,4 +123,5 @@ fragment flightFullPrivateInfo on Flight {
${FRAGMENT_FLIGHT_FULL_INFO}
${FRAGMENT_PHOTO_FULL_INFO}
${FRAGMENT_COPILOT_PUBLIC_INFO}
`;
+4 -4
View File
@@ -23,8 +23,8 @@ mutation editPhoto($id: Int!, $input: EditPhotoInput!) {
`;
export const CHANGE_PHOTO_ORIENTATION = gql`
mutation changeOrientation($id: Int!, $direction: String!) {
changeOrientation(id: $id, direction: $direction) {
mutation changeOrientation($id: Int!, $direction: String!, $copilotUploadToken: String) {
changeOrientation(id: $id, direction: $direction, copilotUploadToken: $copilotUploadToken) {
...photoFullInfo
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
export const DELETE_PHOTO = gql`
mutation deletePhoto($id: Int!) {
deletePhoto(id: $id) {
mutation deletePhoto($id: Int!, $copilotUploadToken: String) {
deletePhoto(id: $id, copilotUploadToken: $copilotUploadToken) {
id
}
}
+8 -2
View File
@@ -34,8 +34,14 @@ ${FRAGMENT_FLIGHT_LIST_INFO}
`;
export const COPILOT_PUBLIC_DETAIL = gql`
query copilot($urlSlug: String!, $pilotUsername: String) {
copilot(urlSlug: $urlSlug, pilotUsername: $pilotUsername, public: true) {
query copilot($urlSlug: String, $uploadToken: String, $uploadFlightSlug: String, $pilotUsername: String!) {
copilot(
urlSlug: $urlSlug,
uploadToken: $uploadToken,
uploadFlightSlug: $uploadFlightSlug,
pilotUsername: $pilotUsername,
public: true
) {
...copilotFullInfo
}
}
+7 -1
View File
@@ -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_LIST_INFO} from "src/graphql/fragments/flight_list";
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`
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`
query FlightPublicDetail($urlSlug: String!, $username: String) {
query FlightPublicDetail($urlSlug: String, $username: String) {
flight(urlSlug: $urlSlug, username: $username, public: true) {
...flightFullInfo
copilots {
...copilotPublicInfo
}
}
}
${FRAGMENT_FLIGHT_FULL_INFO}
${FRAGMENT_COPILOT_PUBLIC_INFO}
`;
+15 -2
View File
@@ -39,8 +39,21 @@ ${FRAGMENT_FLIGHT_LIST_INFO}
`;
export const PHOTOS_PUBLIC_LIST = gql`
query publicPhotos($flightId: Int, $pointOfInterestId: Int, $aircraftId: Int, $copilotId: Int) {
photos(public: true, flightId: $flightId, pointOfInterestId: $pointOfInterestId, aircraftId: $aircraftId, copilotId: $copilotId) {
query publicPhotos(
$flightId: Int,
$pointOfInterestId: Int,
$aircraftId: Int,
$copilotId: Int,
$uploadedByCopilotId: Int
) {
photos(
public: true,
flightId: $flightId,
pointOfInterestId: $pointOfInterestId,
aircraftId: $aircraftId,
copilotId: $copilotId,
uploadedByCopilotId: $uploadedByCopilotId
) {
...photoFullInfo
}
}
+38 -87
View File
@@ -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>
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 {useFlightsStore} from "stores/Flights";
import FlightFormBasic from "components/flights/form/FlightFormBasic.vue";
import {useAircraftsStore} from "stores/Aircrafts";
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
import {useAirportsStore} from "stores/Airports";
import {useQuasar} from "quasar";
import {storeToRefs} from "pinia";
import {flightName} from "src/composables/flight";
import FlightFormCopilot from "components/flights/form/FlightFormCopilot.vue";
import {useCopilotsStore} from "stores/Copilots";
import FlightFormNotes from "components/flights/form/FlightFormNotes.vue";
import FlightFormPhotos from "components/flights/form/FlightFormPhotos.vue";
import {usePhotosStore} from "stores/Photos";
import PhotosEditorList from "components/photos/PhotosEditorList.vue";
import {errorObject, successObject, warningObject} from "src/composables/dialog";
import PhotoFormDialog from "components/photos/PhotoFormDialog.vue";
import FlightFormTrack from "components/flights/form/FlightFormTrack.vue";
import FlightEditSkeleton from "components/flights/skeletons/FlightEditSkeleton.vue";
import BatchPhotoFormDialog from "components/photos/BatchPhotoFormDialog.vue";
import {useAppStore} from "stores/App";
import {useEventsStore} from "stores/Events";
import FlightEditCopilotSkeleton from "components/flights/skeletons/FlightEditCopilotSkeleton.vue";
import {useBasicPhotoOperations} from "src/composables/photos/basicOperations";
import {usePhotosStore} from "stores/Photos";
const $q = useQuasar();
const $route = useRoute();
@@ -34,29 +44,33 @@ const {pointsOfInterest} = storeToRefs(usePointsOfInterestStore());
const {airports} = storeToRefs(useAirportsStore());
const {copilots} = storeToRefs(useCopilotsStore());
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");
watch(() => $route.params.step, (val) => formStep.value = val);
onMounted(() => {
formStep.value = $route.params.step || "basic";
import {flightName} from "src/composables/flight";
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({});
usePointsOfInterestStore().fetch({});
useAirportsStore().fetch();
useCopilotsStore().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) => {
@@ -64,40 +78,9 @@ const handleFlightEdit = (data) => {
$q.notify(successObject("Úspěšně upraveno"));
})
}
const isRequestPending = storeToRefs(useFlightsStore()).isMutationRunning;
const uploadInProgress = ref(false);
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 {
uploadInProgress, handlePhotosUpload, batchRotatePhotos, deletePhoto, updateSinglePhoto
} = useBasicPhotoOperations({});
const setTitlePhoto = (photo) => {
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 photoId = formData.id;
delete formData.id;
@@ -240,18 +191,18 @@ const handleBatchPhotoEdit = async (flightId, data) => {
<q-tab-panel name="photos">
<FlightFormPhotos
:flight="flight"
:is-request-pending="isRequestPending || uploadInProgress"
:is-request-pending="uploadInProgress"
@save="handlePhotosUpload"
/>
<template v-if="flight">
<PhotosEditorList
:title-photo="flight.titlePhoto"
:photos="flight.photos"
:photos="photos"
:show-actions="true"
@edit="openPhotoEdit"
@batch-edit="openBatchPhotoForm"
@batch-rotate="rotatePhotos"
@batch-rotate="batchRotatePhotos"
@delete="deletePhoto"
@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"
:aircraft-detail-route="{name: 'aircraftDetail'}"
:copilot-detail-route="{name: 'copilotDetail'}"
:logged-user="loggedUser"
/>
<ContentCardEditButton
@@ -3,7 +3,7 @@ import {useCopilotsStore} from "stores/Copilots";
export default {
preFetch({currentRoute}) {
return useCopilotsStore().fetchPublicDetail(currentRoute.params.copilotSlug, currentRoute.params.username);
return useCopilotsStore().fetchPublicDetail({urlSlug: currentRoute.params.copilotSlug, username: currentRoute.params.username});
}
}
</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"
:poi-detail-route="{name: 'publicPOIDetail'}"
: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}}"
/>
+1
View File
@@ -27,6 +27,7 @@ const routes = [
children: [
{path: '', name: "publicProfileDetail", component: () => import('pages/public/pilotProfile/FlightsListPage.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: 'udalosti', name: "publicEventsList", component: () => import('pages/public/pilotProfile/EventsListPage.vue')},
{path: 'udalosti/:eventSlug', name: "publicEventDetail", component: () => import('pages/public/pilotProfile/EventDetailPage.vue')},
+2 -2
View File
@@ -18,11 +18,11 @@ export const useCopilotsStore = defineStore('Copilots', () => {
}).then(item => copilot.value = item);
};
const fetchPublicDetail = (urlSlug, username) => {
const fetchPublicDetail = ({urlSlug, username, uploadToken, uploadFlightSlug}) => {
return query({
isQueryRunning,
query: COPILOT_PUBLIC_DETAIL,
variables: {urlSlug, pilotUsername: username},
variables: {urlSlug, pilotUsername: username, uploadToken, uploadFlightSlug},
}).then(item => {
copilot.value = item;
return item;
+6 -6
View File
@@ -21,10 +21,10 @@ export const usePhotosStore = defineStore('Photos', () => {
variables: {id}
}).then(item => photo.value = item);
const fetchPublic = ({flightId, pointOfInterestId, aircraftId, copilotId}) => query({
const fetchPublic = ({flightId, pointOfInterestId, aircraftId, copilotId, uploadedByCopilotId}) => query({
isQueryRunning,
query: PHOTOS_PUBLIC_LIST,
variables: {flightId, pointOfInterestId, aircraftId, copilotId}
variables: {flightId, pointOfInterestId, aircraftId, copilotId, uploadedByCopilotId}
}).then(items => photos.value = items);
const upload = (flightId, input) => {
@@ -61,10 +61,10 @@ export const usePhotosStore = defineStore('Photos', () => {
return item;
})
const changeOrientation = (id, direction) => mutate({
const changeOrientation = (id, direction, copilotUploadToken) => mutate({
isMutationRunning,
mutation: CHANGE_PHOTO_ORIENTATION,
variables: {id, direction}
variables: {id, direction, copilotUploadToken}
}).then(item => {
photo.value = item;
return item;
@@ -79,10 +79,10 @@ export const usePhotosStore = defineStore('Photos', () => {
return item;
});
const destroy = (id) => mutate({
const destroy = (id, copilotUploadToken) => mutate({
isMutationRunning,
mutation: DELETE_PHOTO,
variables: {id}
variables: {id, copilotUploadToken}
});
return {