Refaktoring
This commit is contained in:
@@ -24,6 +24,11 @@ const props = defineProps({
|
||||
isRequestPending,
|
||||
});
|
||||
|
||||
const airportName = (airport) => `${airport.icaoCode} (${airport.name})`;
|
||||
|
||||
const airports = computed(() => props.airports.map(a => ({id: a.id, name: airportName(a)})));
|
||||
const aircrafts = computed(() => props.aircrafts.map(a => ({id: a.id, name: a.callSign})));
|
||||
|
||||
const formData = reactive({
|
||||
aircraft: null,
|
||||
takeoffAirport: null,
|
||||
@@ -43,19 +48,15 @@ const rules = {
|
||||
landingDatetime: [isRequired],
|
||||
landingAirport: [isRequired],
|
||||
};
|
||||
|
||||
watch(() => props.flight, (newVal) => {
|
||||
if (newVal) {
|
||||
setDefaultData(formData, newVal);
|
||||
formData.aircraft = {id: newVal.aircraft.id, name: newVal.aircraft.callSign};
|
||||
formData.takeoffAirport = {id: newVal.takeoffAirport.id, name: newVal.takeoffAirport.name}
|
||||
formData.landingAirport = {id: newVal.landingAirport.id, name: newVal.landingAirport.name}
|
||||
formData.takeoffAirport = {id: newVal.takeoffAirport.id, name: airportName(newVal.takeoffAirport)}
|
||||
formData.landingAirport = {id: newVal.landingAirport.id, name: airportName(newVal.landingAirport)}
|
||||
}
|
||||
}, {immediate: true});
|
||||
|
||||
const aircrafts = computed(() => {
|
||||
return props.aircrafts.map(a => ({id: a.id, name: a.callSign}));
|
||||
});
|
||||
|
||||
watch(() => formData.takeoffDatetime, () => {
|
||||
if (!formData.landingDatetime) {
|
||||
formData.landingDatetime = structuredClone(formData.takeoffDatetime);
|
||||
@@ -73,7 +74,7 @@ watch(() => formData.takeoffAirport, () => {
|
||||
<FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-card class="q-mx-md q-my-md">
|
||||
<q-card class="q-my-sm">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Vyber letadlo</div>
|
||||
</q-card-section>
|
||||
@@ -92,7 +93,7 @@ watch(() => formData.takeoffAirport, () => {
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<q-card class="q-mx-md q-my-md">
|
||||
<q-card class="q-mr-md-sm q-my-sm">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Vzlet</div>
|
||||
</q-card-section>
|
||||
@@ -110,7 +111,6 @@ watch(() => formData.takeoffAirport, () => {
|
||||
v-model="formData.takeoffAirport"
|
||||
label="Letiště vzletu"
|
||||
v-bind="commonProps"
|
||||
:enable-new-values="false"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -118,7 +118,7 @@ watch(() => formData.takeoffAirport, () => {
|
||||
|
||||
<div class="col col-12 col-md-6">
|
||||
|
||||
<q-card class="q-mx-md q-my-md">
|
||||
<q-card class="q-ml-md-sm q-my-sm">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Přistání</div>
|
||||
</q-card-section>
|
||||
@@ -137,7 +137,6 @@ watch(() => formData.takeoffAirport, () => {
|
||||
label="Letiště přistání"
|
||||
v-bind="commonProps"
|
||||
:rules="rules.landingAirport"
|
||||
:enable-new-values="false"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
@@ -10,11 +10,6 @@ const props = defineProps({
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
enableNewValues: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
required: true,
|
||||
@@ -101,11 +96,11 @@ const filterFn = (val, update, abort) => {
|
||||
:options="availableOptions"
|
||||
:option-label="optionLabel"
|
||||
:option-value="optionValue"
|
||||
:new-value-mode="enableNewValues ? 'add' : 'toggle'"
|
||||
new-value-mode="add"
|
||||
clear-icon="clear"
|
||||
:v-bind="$attrs"
|
||||
>
|
||||
<template v-slot:no-option v-if="enableNewValues">
|
||||
<template v-slot:no-option>
|
||||
<q-item v-if="inputValue">
|
||||
<q-item-section avatar>
|
||||
<q-avatar text-color="green" size="32px" icon="add"/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import {computed, onMounted, ref, watch} from "vue";
|
||||
import {FORMAT_DATETIME_LONG, FORMAT_ISO, formatDatetime, parseDate} from "src/composables/datetimeUtils";
|
||||
import {FORMAT_DATE, FORMAT_DATE_ISO, FORMAT_DATETIME_LONG, FORMAT_ISO, formatDatetime, parseDate} from "src/composables/datetimeUtils";
|
||||
import {commonProps} from "src/composables/inputs";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -23,10 +23,17 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
let dateModel = ref("");
|
||||
let timeModel = ref("");
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const dateModel = ref("");
|
||||
const timeModel = ref("");
|
||||
const tab = ref("date");
|
||||
|
||||
let originalTime, originalDate;
|
||||
|
||||
|
||||
const popupProxy = ref(null);
|
||||
|
||||
const publicModel = computed(() => {
|
||||
if (!dateModel.value) {
|
||||
return "";
|
||||
@@ -35,12 +42,21 @@ const publicModel = computed(() => {
|
||||
return formatDatetime(dateObject, props.mask);
|
||||
});
|
||||
|
||||
const popupProxy = ref(null);
|
||||
const showTimePicker = computed(() => containsAnyOf(["H", "h", "m"], props.mask));
|
||||
|
||||
const model = computed(() => dateModel.value + "T" + timeModel.value);
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
watch(() => dateModel.value, (newVal, oldVal) => {
|
||||
tab.value = "time";
|
||||
});
|
||||
|
||||
const model = computed(() => {
|
||||
if (dateModel.value === '' || timeModel.value === '') return null;
|
||||
return dateModel.value + "T" + timeModel.value
|
||||
});
|
||||
const emitModelUpdate = () => {
|
||||
if (dateModel.value === '' || timeModel.value === '') return;
|
||||
if (dateModel.value === '' || timeModel.value === '') {
|
||||
// console.log(dateModel.value, timeModel.value);
|
||||
return;
|
||||
}
|
||||
|
||||
const val = parseDate(model.value);
|
||||
emit("update:modelValue", val);
|
||||
@@ -57,9 +73,6 @@ const containsAnyOf = (needles, haystack) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
const showDatePicker = computed(() => containsAnyOf(["YY", "M", "D"], props.mask));
|
||||
const showTimePicker = computed(() => containsAnyOf(["H", "h", "m"], props.mask));
|
||||
|
||||
const setOriginalValues = () => {
|
||||
originalDate = dateModel.value;
|
||||
originalTime = timeModel.value;
|
||||
@@ -110,6 +123,7 @@ watch(() => props.modelValue, (newVal) => {
|
||||
emitModelUpdate();
|
||||
}
|
||||
}, {immediate: true});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -120,21 +134,43 @@ watch(() => props.modelValue, (newVal) => {
|
||||
|
||||
<template #default>
|
||||
<q-popup-proxy ref="popupProxy" @hide="emitModelUpdate" @show="setOriginalValues">
|
||||
<div class="proxy-wrapper">
|
||||
<div class="row">
|
||||
<div class="col-12 col-sm-6" v-if="showDatePicker">
|
||||
<q-date square v-model="dateModel" :options="isDateValid" :today-btn="true" flat mask="YYYY-MM-DD" :locale="myLocale"/>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6" v-if="showTimePicker">
|
||||
<q-time square v-model="timeModel" format24h flat mask="HH:mm:ss"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mb-sm">
|
||||
<div class="proxy-wrapper relative-position" style="min-height: 500px">
|
||||
|
||||
<q-tabs v-model="tab" v-if="showTimePicker" class="bg-primary text-white">
|
||||
<q-tab name="date" icon="event" :label="dateModel ? formatDatetime(dateModel, 'D. M.', FORMAT_DATE_ISO) : 'datum'"/>
|
||||
<q-tab name="time" v-if="showTimePicker" icon="schedule" :label="model ? formatDatetime(model, 'HH:mm') : 'čas'"/>
|
||||
</q-tabs>
|
||||
|
||||
<q-separator/>
|
||||
|
||||
<q-tab-panels v-model="tab" animated>
|
||||
<q-tab-panel name="date">
|
||||
<q-date
|
||||
minimal
|
||||
flat
|
||||
v-model="dateModel"
|
||||
:options="isDateValid"
|
||||
mask="YYYY-MM-DD"
|
||||
:locale="myLocale"
|
||||
/>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="time" v-if="showTimePicker">
|
||||
<q-time
|
||||
v-model="timeModel"
|
||||
format24h
|
||||
flat
|
||||
mask="HH:mm:ss"
|
||||
/>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
|
||||
<div class="row q-mb-sm absolute-bottom">
|
||||
<div class="col">
|
||||
<q-btn color="secondary" class="q-ml-sm" @click="cancelSelection">Zrušit</q-btn>
|
||||
</div>
|
||||
<div class="col text-right">
|
||||
<q-btn color="primary" class="q-mr-sm" @click="emitModelUpdate">Nastavit</q-btn>
|
||||
<q-btn color="primary" class="q-mr-sm" :disable="!dateModel || !timeModel" @click="emitModelUpdate">Nastavit</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import EmbeddedMap from "components/map/EmbeddedMap.vue";
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {formatDatetime} from "src/composables/datetimeUtils";
|
||||
import 'vue-inner-image-zoom/lib/vue-inner-image-zoom.css'
|
||||
import {useQuasar} from "quasar";
|
||||
@@ -26,8 +26,17 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"])
|
||||
const $q = useQuasar();
|
||||
const imageFullscreen = ref(false);
|
||||
const isMaximized = computed(() => $q.screen.width <= $q.screen.sizes.sm);
|
||||
|
||||
const displayedPhotoIndex = ref(null);
|
||||
watch(() => props.openedIndex, (newVal) => {
|
||||
displayedPhotoIndex.value = newVal;
|
||||
}, {immediate: true});
|
||||
const photo = computed(() => props.photos[displayedPhotoIndex.value]);
|
||||
|
||||
const emit = defineEmits(["update:modelValue"])
|
||||
const isDetailOpened = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
@@ -35,36 +44,47 @@ const isDetailOpened = computed({
|
||||
set(newVal) {
|
||||
emit("update:modelValue", newVal);
|
||||
}
|
||||
})
|
||||
|
||||
const photoIndex = ref(null);
|
||||
onMounted(() => {
|
||||
photoIndex.value = props.openedIndex
|
||||
});
|
||||
|
||||
const photo = computed(() => props.photos[photoIndex.value]);
|
||||
|
||||
const nextPhoto = () => {
|
||||
if (imageFullscreen.value) return;
|
||||
|
||||
if (photoIndex.value < props.photos.length - 1)
|
||||
photoIndex.value++
|
||||
if (displayedPhotoIndex.value < props.photos.length - 1) {
|
||||
displayedPhotoIndex.value++;
|
||||
scrollToPhoto();
|
||||
}
|
||||
}
|
||||
|
||||
const prevPhoto = () => {
|
||||
if (imageFullscreen.value) return;
|
||||
if (photoIndex.value > 0) photoIndex.value--;
|
||||
if (displayedPhotoIndex.value > 0) {
|
||||
displayedPhotoIndex.value--;
|
||||
scrollToPhoto();
|
||||
}
|
||||
}
|
||||
|
||||
const showDetail = (idx) => {
|
||||
photoIndex.value = idx;
|
||||
}
|
||||
const openDetail = (idx) => {
|
||||
displayedPhotoIndex.value = idx;
|
||||
scrollToPhoto();
|
||||
};
|
||||
|
||||
const showExtra = ref(false);
|
||||
const scrollArea = ref(null);
|
||||
const infoWrapper = ref(null);
|
||||
const scrollToPhoto = () => {
|
||||
scrollArea.value.setScrollPosition('vertical', 0, 500);
|
||||
}
|
||||
|
||||
const $q = useQuasar();
|
||||
const isMaximized = computed(() => $q.screen.width <= $q.screen.sizes.sm);
|
||||
const imageFullscreen = ref(false);
|
||||
const showExtraInfo = () => {
|
||||
if (showExtra.value) {
|
||||
showExtra.value = false;
|
||||
scrollToPhoto();
|
||||
} else {
|
||||
showExtra.value = true;
|
||||
const scrollAreaHeight = infoWrapper.value.offsetTop;
|
||||
scrollArea.value.setScrollPosition('vertical', scrollAreaHeight, 500);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -74,127 +94,129 @@ const imageFullscreen = ref(false);
|
||||
:maximized="isMaximized"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section class="bg-black text-white" v-if="isMaximized">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<strong>{{ photo.name || defaultTitle }}</strong>
|
||||
<q-scroll-area ref="scrollArea" style="height: 95vh">
|
||||
<q-card-section class="bg-black text-white" v-if="isMaximized">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<strong>{{ photo.name || defaultTitle }}</strong>
|
||||
</div>
|
||||
<div class="col-1 text-right">
|
||||
<q-btn round text-color="white" size="sm" icon="close" @click="isDetailOpened = false"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-1 text-right">
|
||||
<q-btn round text-color="white" size="sm" icon="close" @click="isDetailOpened = false"/>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section :class="{'q-pa-none': isMaximized}">
|
||||
<div :class="{'relative-position': true, 'image-full-height': imageFullscreen}">
|
||||
<div
|
||||
class="text-center"
|
||||
v-touch-swipe.mouse.right="prevPhoto"
|
||||
v-touch-swipe.mouse.left="nextPhoto"
|
||||
@keyup.left="prevPhoto"
|
||||
@keyup.right="nextPhoto">
|
||||
<inner-image-zoom
|
||||
:move-type="isMaximized ? 'drag' : 'pan'"
|
||||
:zoomScale="0.75"
|
||||
:hide-hint="true"
|
||||
:after-zoom-in="() => imageFullscreen = true"
|
||||
:after-zoom-out="() => imageFullscreen = false"
|
||||
:src="photo.url"
|
||||
/>
|
||||
<q-card-section :class="{'q-pa-none': isMaximized}">
|
||||
<div :class="{'relative-position': true, 'image-full-height': imageFullscreen}">
|
||||
<div
|
||||
class="text-center"
|
||||
v-touch-swipe.mouse.right="prevPhoto"
|
||||
v-touch-swipe.mouse.left="nextPhoto"
|
||||
@keyup.left="prevPhoto"
|
||||
@keyup.right="nextPhoto">
|
||||
<inner-image-zoom
|
||||
:move-type="isMaximized ? 'drag' : 'pan'"
|
||||
:zoomScale="0.75"
|
||||
:hide-hint="true"
|
||||
:after-zoom-in="() => imageFullscreen = true"
|
||||
:after-zoom-out="() => imageFullscreen = false"
|
||||
:src="photo.url"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-if="!imageFullscreen">
|
||||
<q-btn
|
||||
class="absolute all-pointer-events"
|
||||
round
|
||||
flat
|
||||
icon="info"
|
||||
:text-color="showExtra ? 'primary' : 'white'"
|
||||
style="background: rgba(0, 0, 0, 0.25); bottom: 16px; left: 8px"
|
||||
@click="showExtraInfo"
|
||||
/>
|
||||
<q-btn
|
||||
class="absolute all-pointer-events"
|
||||
style="background: rgba(0, 0, 0, 0.25); top: 50%; left: 8px;"
|
||||
@click="prevPhoto"
|
||||
v-if="displayedPhotoIndex > 0 && !isMaximized"
|
||||
icon="chevron_left"
|
||||
text-color="white"
|
||||
flat
|
||||
rounded
|
||||
/>
|
||||
<q-btn
|
||||
class="absolute all-pointer-events"
|
||||
style="background: rgba(0, 0, 0, 0.25); top: 50%; right: 8px;"
|
||||
@click="nextPhoto"
|
||||
v-if="displayedPhotoIndex < photos.length - 1 && !isMaximized"
|
||||
icon-right="chevron_right"
|
||||
text-color="white"
|
||||
flat
|
||||
rounded
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-if="!imageFullscreen">
|
||||
<q-btn
|
||||
class="absolute all-pointer-events"
|
||||
round
|
||||
flat
|
||||
icon="info"
|
||||
:text-color="showExtra ? 'primary' : 'white'"
|
||||
style="background: rgba(0, 0, 0, 0.25); bottom: 16px; left: 8px"
|
||||
@click="showExtra = !showExtra"
|
||||
/>
|
||||
<q-btn
|
||||
class="absolute all-pointer-events"
|
||||
style="background: rgba(0, 0, 0, 0.25); top: 50%; left: 8px;"
|
||||
@click="prevPhoto"
|
||||
v-if="photoIndex > 0 && !isMaximized"
|
||||
icon="chevron_left"
|
||||
text-color="white"
|
||||
flat
|
||||
rounded
|
||||
/>
|
||||
<q-btn
|
||||
class="absolute all-pointer-events"
|
||||
style="background: rgba(0, 0, 0, 0.25); top: 50%; right: 8px;"
|
||||
@click="nextPhoto"
|
||||
v-if="photoIndex < photos.length - 1 && !isMaximized"
|
||||
icon-right="chevron_right"
|
||||
text-color="white"
|
||||
flat
|
||||
rounded
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div class="row" v-show="!imageFullscreen">
|
||||
<div class="col-3 col-md-1 q-pa-sm" v-for="(thumb, idx) in photos" :key="thumb.thumbnailUrl">
|
||||
<q-img
|
||||
:src="thumb.thumbnailUrl"
|
||||
@click="openDetail(idx)"
|
||||
:class='{"shadow-5": true, "active-thumbnail": idx === displayedPhotoIndex}'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row" v-show="!imageFullscreen">
|
||||
<div class="col-4 col-md-1 q-pa-sm" v-for="(thumb, idx) in photos" :key="thumb.thumbnailUrl">
|
||||
<q-img
|
||||
:src="thumb.thumbnailUrl"
|
||||
@click="showDetail(idx)"
|
||||
:class='{"shadow-5": true, "active-thumbnail": idx === photoIndex}'
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="row" ref="infoWrapper">
|
||||
<div class="col-12 q-my-md" v-if="showExtra">
|
||||
<EmbeddedMap
|
||||
v-if="photo.gpsLatitude && photo.gpsLongitude"
|
||||
class="q-mx-sm"
|
||||
:gps-latitude="photo.gpsLatitude"
|
||||
:gps-longitude="photo.gpsLongitude"
|
||||
/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 q-my-md" v-if="showExtra">
|
||||
<EmbeddedMap
|
||||
v-if="photo.gpsLatitude && photo.gpsLongitude"
|
||||
class="q-mx-sm"
|
||||
:gps-latitude="photo.gpsLatitude"
|
||||
:gps-longitude="photo.gpsLongitude"
|
||||
/>
|
||||
<strong v-if="photo.name !== '' && !isMaximized">{{ photo.name }}</strong>
|
||||
<p v-if="photo.description !== ''">{{ photo.description }}</p>
|
||||
|
||||
<strong v-if="photo.name !== '' && !isMaximized">{{ photo.name }}</strong>
|
||||
<p v-if="photo.description !== ''">{{ photo.description }}</p>
|
||||
<q-list dense>
|
||||
<q-item v-if="photo.pointOfInterest">
|
||||
<q-item-section avatar>
|
||||
<q-avatar icon="marker"></q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
{{ photo.pointOfInterest.name }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="photo.exposedAt">
|
||||
<q-item-section avatar>
|
||||
<q-avatar icon="photo_camera"></q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
{{ formatDatetime(photo.exposedAt) }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-list dense>
|
||||
<q-item v-if="photo.pointOfInterest">
|
||||
<q-item-section avatar>
|
||||
<q-avatar icon="marker"></q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
{{ photo.pointOfInterest.name }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="photo.exposedAt">
|
||||
<q-item-section avatar>
|
||||
<q-avatar icon="photo_camera"></q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
{{ formatDatetime(photo.exposedAt) }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item v-if="photo.gpsAltitude">
|
||||
<q-item-section avatar>
|
||||
<q-avatar icon="height"/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
Nadmořská výška: {{ photo.gpsAltitude }} m
|
||||
<br>
|
||||
<template v-if="photo.terrainElevation">
|
||||
Výška terénu: {{ photo.terrainElevation }} m
|
||||
<q-item v-if="photo.gpsAltitude">
|
||||
<q-item-section avatar>
|
||||
<q-avatar icon="height"/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
Nadmořská výška: {{ photo.gpsAltitude }} m
|
||||
<br>
|
||||
Výška nad zemí: {{ photo.gpsAltitude - photo.terrainElevation }} m
|
||||
</template>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<template v-if="photo.terrainElevation">
|
||||
Výška terénu: {{ photo.terrainElevation }} m
|
||||
<br>
|
||||
Výška nad zemí: {{ photo.gpsAltitude - photo.terrainElevation }} m
|
||||
</template>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card-section>
|
||||
</q-scroll-area>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
@@ -220,8 +242,4 @@ const imageFullscreen = ref(false);
|
||||
.iiz__img--hidden {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.iiz__zoom-img--visible {
|
||||
//height: 125vh !important
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {computed, onMounted, ref, watch} from "vue";
|
||||
import {useQuasar} from "quasar";
|
||||
import {confirmObject} from "src/composables/dialog";
|
||||
|
||||
@@ -33,20 +33,20 @@ const $q = useQuasar();
|
||||
|
||||
const confirmPhotoDelete = (id) => {
|
||||
$q.dialog(confirmObject("Opravdu chces smazat tuto fotku?"))
|
||||
.onOk(() => {
|
||||
emits("delete", id);
|
||||
});
|
||||
.onOk(() => {
|
||||
emits("delete", id);
|
||||
});
|
||||
}
|
||||
|
||||
const confirmBatchPhotoDelete = () => {
|
||||
$q.dialog(confirmObject("Opravdu chces smazat vybrané fotky?" + JSON.stringify(batchEdit.value)))
|
||||
.onOk(() => {
|
||||
for (const id in batchEdit.value) {
|
||||
if (!batchEdit.value[id]) continue;
|
||||
emits("delete", parseInt(id));
|
||||
batchEdit.value[id] = false;
|
||||
}
|
||||
});
|
||||
$q.dialog(confirmObject("Opravdu chces smazat vybrané fotky?"))
|
||||
.onOk(() => {
|
||||
for (const id in batchEdit.value) {
|
||||
if (!batchEdit.value[id]) continue;
|
||||
emits("delete", parseInt(id));
|
||||
batchEdit.value[id] = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const handleImageClick = (photo, idx) => {
|
||||
@@ -67,18 +67,40 @@ const openBatchEdit = () => {
|
||||
}
|
||||
|
||||
const isBatchEditAllowed = computed(() => Object.values(batchEdit.value).filter(v => v).length > 0);
|
||||
|
||||
const checkAll = ref(false);
|
||||
watch(() => checkAll.value, (newVal) => {
|
||||
if (newVal === null) return;
|
||||
|
||||
for (const photo of props.photos) {
|
||||
batchEdit.value[photo.id] = newVal;
|
||||
}
|
||||
});
|
||||
watch(() => batchEdit.value, () => {
|
||||
let checkedAll = true;
|
||||
let uncheckedAll = true;
|
||||
|
||||
for (const i of Object.keys(batchEdit.value)) {
|
||||
if (typeof batchEdit.value[i] === 'undefined') continue;
|
||||
|
||||
if (batchEdit.value[i]) uncheckedAll = false;
|
||||
if (!batchEdit.value[i]) checkedAll = false;
|
||||
}
|
||||
|
||||
if (checkedAll) {
|
||||
checkAll.value = true;
|
||||
} else if (uncheckedAll) {
|
||||
checkAll.value = false;
|
||||
} else {
|
||||
checkAll.value = null;
|
||||
}
|
||||
}, {deep: true});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row q-my-md">
|
||||
<div class="col text-right">
|
||||
<q-btn round flat icon="edit" :color="isBatchEditAllowed ? 'primary' : 'grey'" :disable="!isBatchEditAllowed" @click="openBatchEdit" v-if="showActions"/>
|
||||
<q-btn round flat icon="delete" :color="isBatchEditAllowed ? 'red' : 'grey'" :disable="!isBatchEditAllowed" @click="confirmBatchPhotoDelete" v-if="showActions"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 col-sm-4 col-md-4 col-lg-3" v-for="(photo, idx) in photos" :key="photo.id">
|
||||
<div class="col-6 col-sm-4 col-md-4 col-lg-3" v-for="(photo, idx) in photos" :key="photo.id">
|
||||
<q-card :class='{"q-ma-sm": true, "shadow-1": !batchEdit[photo.id], "shadow-10": batchEdit[photo.id]}'>
|
||||
<q-img :src="photo.thumbnailUrl" @click="handleImageClick(photo, idx)">
|
||||
<div class="absolute-bottom" v-if="photo.name !== '' || photo.description !== ''">
|
||||
@@ -106,4 +128,14 @@ const isBatchEditAllowed = computed(() => Object.values(batchEdit.value).filter(
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-page-sticky :offset="[50, 50]" position="bottom" v-if="showActions && photos.length > 0">
|
||||
<q-card>
|
||||
<q-card-actions>
|
||||
<q-checkbox v-model="checkAll" label="Vybrat vše"/>
|
||||
<q-btn round class="q-ml-lg" flat icon="edit" :color="isBatchEditAllowed ? 'primary' : 'grey'" :disable="!isBatchEditAllowed" @click="openBatchEdit" v-if="showActions"/>
|
||||
<q-btn round flat icon="delete" :color="isBatchEditAllowed ? 'red' : 'grey'" :disable="!isBatchEditAllowed" @click="confirmBatchPhotoDelete" v-if="showActions"/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-page-sticky>
|
||||
</template>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {date} from "quasar";
|
||||
export const FORMAT_ISO = "YYYY-MM-DDTHH:mm:ss";
|
||||
export const FORMAT_DATETIME_LONG = "DD. MM. YYYY, HH:mm";
|
||||
export const FORMAT_DATE = "DD. MM. YYYY";
|
||||
export const FORMAT_DATE_ISO = "YYYY-MM-DD";
|
||||
export const FORMAT_TIME = "HH:mm"
|
||||
|
||||
export const minutesToHumanReadable = (totalMinutes) => {
|
||||
@@ -45,9 +46,9 @@ export const parseDate = (datetimeStr, format = FORMAT_ISO) => {
|
||||
return date.extractDate(datetimeStr, format)
|
||||
}
|
||||
|
||||
export const formatDatetime = (datetime, format = FORMAT_DATETIME_LONG) => {
|
||||
export const formatDatetime = (datetime, format = FORMAT_DATETIME_LONG, inputFormat = FORMAT_ISO) => {
|
||||
if (typeof datetime === 'string') {
|
||||
datetime = parseDate(datetime);
|
||||
datetime = parseDate(datetime, inputFormat);
|
||||
}
|
||||
return date.formatDate(datetime, format);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,40 @@
|
||||
import {ref} from "vue";
|
||||
import {usePhotosStore} from "stores/Photo";
|
||||
import {fetchFlight} from "src/composables/objects/flight";
|
||||
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
||||
|
||||
export const openBatchEdit = ref(false);
|
||||
export const editBatchPhotos = ref([]);
|
||||
export const openBatchPhotoForm = (photos) => {
|
||||
openBatchEdit.value = true;
|
||||
editBatchPhotos.value = photos;
|
||||
openBatchEdit.value = true;
|
||||
editBatchPhotos.value = photos;
|
||||
}
|
||||
|
||||
export const handleBatchPhotoEdit = (flightId, data) => {
|
||||
const photoIds = data.photoIds;
|
||||
delete data.photoIds;
|
||||
delete data.id;
|
||||
export const handleBatchPhotoEdit = async (flightId, data) => {
|
||||
const photoIds = data.photoIds;
|
||||
delete data.photoIds;
|
||||
delete data.id;
|
||||
|
||||
const promises = [];
|
||||
for (const id of photoIds) {
|
||||
const updateData = {};
|
||||
if (data.name !== '') updateData.name = data.name;
|
||||
if (data.description !== '') updateData.description = data.description;
|
||||
if (data.pointOfInterest) updateData.pointOfInterest = data.pointOfInterest;
|
||||
|
||||
if (Object.keys(updateData).length === 0) continue;
|
||||
|
||||
promises.push(usePhotosStore().update(id, updateData));
|
||||
if (data.pointOfInterest && !data.pointOfInterest.id) {
|
||||
const poiData = {
|
||||
...data.pointOfInterest,
|
||||
isPublic: false,
|
||||
description: "",
|
||||
}
|
||||
await usePointsOfInterestStore().create(poiData).then(poi => data.pointOfInterest = {id: poi.id, name: poi.name});
|
||||
}
|
||||
|
||||
Promise.all(promises).finally(() => fetchFlight(flightId))
|
||||
const promises = [];
|
||||
for (const id of photoIds) {
|
||||
const updateData = {};
|
||||
if (data.name !== '') updateData.name = data.name;
|
||||
if (data.description !== '') updateData.description = data.description;
|
||||
if (data.pointOfInterest) updateData.pointOfInterest = data.pointOfInterest;
|
||||
|
||||
if (Object.keys(updateData).length === 0) continue;
|
||||
|
||||
promises.push(usePhotosStore().update(id, updateData));
|
||||
}
|
||||
|
||||
Promise.all(promises).finally(() => fetchFlight(flightId))
|
||||
}
|
||||
|
||||
@@ -17,5 +17,6 @@ export const FRAGMENT_PHOTO_FULL_INFO = gql`
|
||||
gpsLatitude
|
||||
gpsLongitude
|
||||
gpsAltitude
|
||||
terrainElevation
|
||||
exposedAt
|
||||
}`;
|
||||
|
||||
@@ -16,7 +16,7 @@ import FlightFormNotes from "components/flights/form/FlightFormNotes.vue";
|
||||
import FlightFormPhotos from "components/flights/form/FlightFormPhotos.vue";
|
||||
import {usePhotosStore} from "stores/Photo";
|
||||
import PhotosList from "components/photos/PhotosList.vue";
|
||||
import {successObject} from "src/composables/dialog";
|
||||
import {successObject, warningObject} from "src/composables/dialog";
|
||||
import PhotoFormDialog from "components/photos/PhotoFormDialog.vue";
|
||||
import FlightFormTrack from "components/flights/form/FlightFormTrack.vue";
|
||||
import {fetchFlight, flight, loading} from "src/composables/objects/flight";
|
||||
@@ -76,8 +76,18 @@ const handlePhotosUpload = (data) => {
|
||||
promises.push(usePhotosStore().upload(flightId, {photo}));
|
||||
}
|
||||
|
||||
Promise.all(promises).then(() => {
|
||||
$q.notify(successObject("Úspěšně nahráno"));
|
||||
Promise.allSettled(promises).then((values) => {
|
||||
console.log(values.map(v => v.status));
|
||||
|
||||
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)}`));
|
||||
|
||||
fetchFlight(flightId);
|
||||
}).finally(() => uploadInProgress.value = false);
|
||||
}
|
||||
@@ -101,11 +111,11 @@ const deletePhoto = (id) => {
|
||||
<q-tab-panels v-model="formStep">
|
||||
<q-tab-panel name="basic">
|
||||
<FlightFormBasic
|
||||
@save="handleFlightEdit"
|
||||
:flight="flight"
|
||||
:locations="locations"
|
||||
:airports="airports"
|
||||
:aircrafts="aircrafts"
|
||||
@save="handleFlightEdit"
|
||||
:flight="flight"
|
||||
:locations="locations"
|
||||
:airports="airports"
|
||||
:aircrafts="aircrafts"
|
||||
/>
|
||||
</q-tab-panel>
|
||||
|
||||
@@ -115,41 +125,41 @@ const deletePhoto = (id) => {
|
||||
|
||||
<q-tab-panel name="track">
|
||||
<FlightFormTrack
|
||||
v-if="locations"
|
||||
:flight="flight"
|
||||
:locations="locations"
|
||||
@save="handleFlightEdit"
|
||||
v-if="locations"
|
||||
:flight="flight"
|
||||
:locations="locations"
|
||||
@save="handleFlightEdit"
|
||||
/>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="photos">
|
||||
<FlightFormPhotos
|
||||
:flight="flight"
|
||||
:is-request-pending="isRequestPending || uploadInProgress"
|
||||
@save="handlePhotosUpload"
|
||||
:flight="flight"
|
||||
:is-request-pending="isRequestPending || uploadInProgress"
|
||||
@save="handlePhotosUpload"
|
||||
/>
|
||||
|
||||
<PhotosList
|
||||
:photos="flight.photos"
|
||||
@edit="openPhotoEdit"
|
||||
@batch-edit="openBatchPhotoForm"
|
||||
@delete="deletePhoto"
|
||||
:photos="flight.photos"
|
||||
@edit="openPhotoEdit"
|
||||
@batch-edit="openBatchPhotoForm"
|
||||
@delete="deletePhoto"
|
||||
/>
|
||||
|
||||
<!-- Dialogs for photo edit -->
|
||||
<PhotoFormDialog
|
||||
v-model="openEdit"
|
||||
:locations="locations"
|
||||
:photo="editPhoto"
|
||||
@save="handlePhotoUpdate(flightId, $event)"
|
||||
v-model="openEdit"
|
||||
:locations="locations"
|
||||
:photo="editPhoto"
|
||||
@save="handlePhotoUpdate(flightId, $event)"
|
||||
/>
|
||||
|
||||
<BatchPhotoFormDialog
|
||||
v-model="openBatchEdit"
|
||||
:locations="locations"
|
||||
:photos="editBatchPhotos"
|
||||
@save="handleBatchPhotoEdit(flightId, $event)"
|
||||
@delete="deletePhoto"
|
||||
v-model="openBatchEdit"
|
||||
:locations="locations"
|
||||
:photos="editBatchPhotos"
|
||||
@save="handleBatchPhotoEdit(flightId, $event)"
|
||||
@delete="deletePhoto"
|
||||
/>
|
||||
</q-tab-panel>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import FlightBasicInfo from "components/flights/FlightBasicInfo.vue";
|
||||
import {copyToClipboard, useQuasar} from "quasar";
|
||||
import {successObject} from "src/composables/dialog";
|
||||
import {useFlightsStore} from "stores/Flights";
|
||||
import PhotoCarousel from "components/flights/PhotoCarousel.vue";
|
||||
import PhotoCarousel from "components/photos/PhotoCarousel.vue";
|
||||
import FlightTrackMap from "components/flights/FlightTrackMap.vue";
|
||||
import {useUserStore} from "stores/User";
|
||||
import {storeToRefs} from "pinia";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {onMounted, watch} from "vue";
|
||||
import {fetchFlight, flight, flightName, loading} from "src/composables/objects/flight";
|
||||
import {useRoute} from "vue-router";
|
||||
import PhotoCarousel from "components/flights/PhotoCarousel.vue";
|
||||
import PhotoCarousel from "components/photos/PhotoCarousel.vue";
|
||||
import FlightTrackMap from "components/flights/FlightTrackMap.vue";
|
||||
import FlightBasicInfo from "components/flights/FlightBasicInfo.vue";
|
||||
import {useAppStore} from "stores/App";
|
||||
|
||||
+4
-2
@@ -27,8 +27,10 @@ export default route(function (/* { store, ssrContext } */) {
|
||||
history: createHistory(process.env.VUE_ROUTER_BASE)
|
||||
})
|
||||
|
||||
Router.afterEach(async (to) => {
|
||||
useAppStore().resetAfterRouteChange();
|
||||
Router.afterEach(async (to, from) => {
|
||||
if (from.name !== to.name) {
|
||||
useAppStore().resetAfterRouteChange();
|
||||
}
|
||||
});
|
||||
|
||||
return Router
|
||||
|
||||
Reference in New Issue
Block a user