UI improvements for gallery, maps and alerts
This commit is contained in:
parent
827e671c84
commit
0f0a1b5f12
|
@ -23,7 +23,7 @@
|
|||
body {
|
||||
background: rgb(250, 250, 250);
|
||||
color: #333333;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-family: Roboto, sans-serif;;
|
||||
}
|
||||
|
||||
footer {
|
||||
|
@ -52,3 +52,14 @@ main {
|
|||
#app-navigation {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Photo Gallery */
|
||||
#app .pswp__caption__center {
|
||||
text-align: center;
|
||||
max-width: 80%;
|
||||
margin: 0 auto;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
line-height: 20px;
|
||||
color: #E0E0E0;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import Api from 'common/api';
|
|||
import Config from 'common/config';
|
||||
import Components from 'component/components';
|
||||
import Alert from 'common/alert';
|
||||
import Gallery from 'common/gallery';
|
||||
import Session from 'common/session';
|
||||
import Event from 'pubsub-js';
|
||||
import Moment from 'vue-moment';
|
||||
|
@ -22,6 +23,7 @@ const config = new Config(window.localStorage, window.appConfig);
|
|||
// Set global helpers
|
||||
Vue.prototype.$event = Event;
|
||||
Vue.prototype.$alert = Alert;
|
||||
Vue.prototype.$gallery = new Gallery;
|
||||
Vue.prototype.$session = session;
|
||||
Vue.prototype.$api = Api;
|
||||
Vue.prototype.$config = config;
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
<router-view></router-view>
|
||||
</v-content>
|
||||
</v-app>
|
||||
|
||||
<app-gallery></app-gallery>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -20,7 +22,7 @@
|
|||
computed: {},
|
||||
methods: {
|
||||
login() {
|
||||
this.$refs.loginDialog.open();
|
||||
// this.$refs.loginDialog.open();
|
||||
},
|
||||
|
||||
logout() {
|
||||
|
@ -29,7 +31,3 @@
|
|||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
|
||||
</style>
|
||||
|
|
|
@ -74,497 +74,496 @@
|
|||
</v-form>
|
||||
<v-container fluid>
|
||||
<p class="md-subheading">
|
||||
A user-friendly tool for importing, filtering and archiving large amounts of JPEG and RAW files
|
||||
</p>
|
||||
<v-btn
|
||||
color="success"
|
||||
dark
|
||||
@click.stop="dialog = true"
|
||||
>Create album
|
||||
</v-btn>
|
||||
</v-container>
|
||||
A user-friendly tool for importing, filtering and archiving large amounts of JPEG and RAW files
|
||||
</p>
|
||||
<v-btn
|
||||
color="success"
|
||||
dark
|
||||
@click.stop="dialog = true"
|
||||
>Create album
|
||||
</v-btn>
|
||||
</v-container>
|
||||
<v-dialog v-model="dialog" dark persistent max-width="600px">
|
||||
<v-card dark>
|
||||
<v-card-title>
|
||||
<span class="headline">Create album</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-container grid-list-md>
|
||||
<v-layout wrap>
|
||||
<v-flex xs12>
|
||||
<v-text-field label="Album name*" required></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs12>
|
||||
<v-textarea label="Description"></v-textarea>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="success" flat @click="dialog = false">Close</v-btn>
|
||||
<v-btn color="success" flat @click="dialog = false">Save</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-card dark>
|
||||
<v-card-title>
|
||||
<span class="headline">Create album</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-container grid-list-md>
|
||||
<v-layout wrap>
|
||||
<v-flex xs12>
|
||||
<v-text-field label="Album name*" required></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs12>
|
||||
<v-textarea label="Description"></v-textarea>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="success" flat @click="dialog = false">Close</v-btn>
|
||||
<v-btn color="success" flat @click="dialog = false">Save</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-container fluid>
|
||||
<v-speed-dial
|
||||
fixed
|
||||
bottom
|
||||
right
|
||||
direction="top"
|
||||
open-on-hover
|
||||
transition="slide-y-reverse-transition"
|
||||
style="right: 8px; bottom: 8px;"
|
||||
>
|
||||
<v-btn
|
||||
slot="activator"
|
||||
color="grey darken-2"
|
||||
dark
|
||||
fab
|
||||
>
|
||||
<v-icon>menu</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
fab
|
||||
dark
|
||||
small
|
||||
color="deep-purple lighten-2"
|
||||
>
|
||||
<v-icon>favorite</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
fab
|
||||
dark
|
||||
small
|
||||
color="cyan accent-4"
|
||||
>
|
||||
<v-icon>youtube_searched_for</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
fab
|
||||
dark
|
||||
small
|
||||
color="teal accent-4"
|
||||
>
|
||||
<v-icon>save</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
fab
|
||||
dark
|
||||
small
|
||||
color="yellow accent-4"
|
||||
>
|
||||
<v-icon>create_new_folder</v-icon>
|
||||
</v-btn>
|
||||
<v-container fluid>
|
||||
<v-speed-dial
|
||||
fixed
|
||||
bottom
|
||||
right
|
||||
direction="top"
|
||||
open-on-hover
|
||||
transition="slide-y-reverse-transition"
|
||||
style="right: 8px; bottom: 8px;"
|
||||
>
|
||||
<v-btn
|
||||
slot="activator"
|
||||
color="grey darken-2"
|
||||
dark
|
||||
fab
|
||||
>
|
||||
<v-icon>menu</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
fab
|
||||
dark
|
||||
small
|
||||
color="deep-purple lighten-2"
|
||||
>
|
||||
<v-icon>favorite</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
fab
|
||||
dark
|
||||
small
|
||||
color="cyan accent-4"
|
||||
>
|
||||
<v-icon>youtube_searched_for</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
fab
|
||||
dark
|
||||
small
|
||||
color="teal accent-4"
|
||||
>
|
||||
<v-icon>save</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
fab
|
||||
dark
|
||||
small
|
||||
color="yellow accent-4"
|
||||
>
|
||||
<v-icon>create_new_folder</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
fab
|
||||
dark
|
||||
small
|
||||
color="delete"
|
||||
>
|
||||
<v-icon>delete</v-icon>
|
||||
</v-btn>
|
||||
</v-speed-dial>
|
||||
<v-data-table
|
||||
:headers="listColumns"
|
||||
:items="results"
|
||||
hide-actions
|
||||
class="elevation-1"
|
||||
v-if="query.view === 'list'"
|
||||
select-all
|
||||
disable-initial-sort
|
||||
item-key="ID"
|
||||
v-model="selected"
|
||||
:no-data-text="'No photos matched your search'"
|
||||
<v-btn
|
||||
fab
|
||||
dark
|
||||
small
|
||||
color="delete"
|
||||
>
|
||||
<v-icon>delete</v-icon>
|
||||
</v-btn>
|
||||
</v-speed-dial>
|
||||
<v-data-table
|
||||
:headers="listColumns"
|
||||
:items="results"
|
||||
hide-actions
|
||||
class="elevation-1"
|
||||
v-if="query.view === 'list'"
|
||||
select-all
|
||||
disable-initial-sort
|
||||
item-key="ID"
|
||||
v-model="selected"
|
||||
:no-data-text="'No photos matched your search'"
|
||||
>
|
||||
<template slot="items" slot-scope="props">
|
||||
<td>
|
||||
<v-checkbox
|
||||
v-model="props.selected"
|
||||
primary
|
||||
hide-details
|
||||
></v-checkbox>
|
||||
</td>
|
||||
<td>Album Title</td>
|
||||
<td>Some album description</td>
|
||||
<td>11/01/2018 - 01/02/2018</td>
|
||||
<td>London, Durban, Berlin</td>
|
||||
<td>Germany, South Africa</td>
|
||||
<td>Iphone SE, Canon</td>
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
<v-container grid-list-xs fluid class="pa-0" v-if="query.view === 'details'">
|
||||
<v-card v-if="results.length === 0">
|
||||
<v-card-title primary-title>
|
||||
<div>
|
||||
<h3 class="headline mb-3">No photos matched your search</h3>
|
||||
<div>Try using other terms and search options such as category, country and camera.</div>
|
||||
</div>
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
<v-layout row wrap>
|
||||
<v-flex
|
||||
v-for="(photo, index) in results"
|
||||
:key="photo.ID"
|
||||
xs12 sm6 md4 lg3 d-flex
|
||||
>
|
||||
<v-hover>
|
||||
<v-card tile slot-scope="{ hover }"
|
||||
:dark="photo.selected"
|
||||
:class="photo.selected ? 'elevation-14 ma-1' : 'elevation-2 ma-2'">
|
||||
<v-img
|
||||
:src="photo.getThumbnailUrl('square', 500)"
|
||||
aspect-ratio="1"
|
||||
v-bind:class="{ selected: photo.selected }"
|
||||
style="cursor: pointer"
|
||||
class="grey lighten-2"
|
||||
@click="openPhoto(index)"
|
||||
|
||||
>
|
||||
<v-layout
|
||||
slot="placeholder"
|
||||
fill-height
|
||||
align-center
|
||||
justify-center
|
||||
ma-0
|
||||
>
|
||||
<template slot="items" slot-scope="props">
|
||||
<td>
|
||||
<v-checkbox
|
||||
v-model="props.selected"
|
||||
primary
|
||||
hide-details
|
||||
></v-checkbox>
|
||||
</td>
|
||||
<td>Album Title</td>
|
||||
<td>Some album description</td>
|
||||
<td>11/01/2018 - 01/02/2018</td>
|
||||
<td>London, Durban, Berlin</td>
|
||||
<td>Germany, South Africa</td>
|
||||
<td>Iphone SE, Canon</td>
|
||||
</template>
|
||||
</v-data-table>
|
||||
<v-progress-circular indeterminate color="grey lighten-5"></v-progress-circular>
|
||||
</v-layout>
|
||||
|
||||
<v-container grid-list-xs fluid class="pa-0" v-if="query.view === 'details'">
|
||||
<v-card v-if="results.length === 0">
|
||||
<v-card-title primary-title>
|
||||
<div>
|
||||
<h3 class="headline mb-3">No photos matched your search</h3>
|
||||
<div>Try using other terms and search options such as category, country and camera.</div>
|
||||
</div>
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
<v-layout row wrap>
|
||||
<v-flex
|
||||
v-for="(photo, index) in results"
|
||||
:key="photo.ID"
|
||||
xs12 sm6 md4 lg3 d-flex
|
||||
>
|
||||
<v-hover>
|
||||
<v-card tile slot-scope="{ hover }"
|
||||
:dark="photo.selected"
|
||||
:class="photo.selected ? 'elevation-14 ma-1' : 'elevation-2 ma-2'">
|
||||
<v-img
|
||||
:src="photo.getThumbnailUrl('square', 500)"
|
||||
aspect-ratio="1"
|
||||
v-bind:class="{ selected: photo.selected }"
|
||||
style="cursor: pointer"
|
||||
class="grey lighten-2"
|
||||
@click="openPhoto(index)"
|
||||
<v-btn v-if="hover || photo.selected" :flat="!hover" icon large absolute
|
||||
:ripple="false" style="right: 4px; bottom: 4px;"
|
||||
@click.stop.prevent="selectPhoto(photo)">
|
||||
<v-icon v-if="photo.selected" color="white">check_box</v-icon>
|
||||
<v-icon v-else color="white">check_box_outline_blank</v-icon>
|
||||
</v-btn>
|
||||
|
||||
>
|
||||
<v-layout
|
||||
slot="placeholder"
|
||||
fill-height
|
||||
align-center
|
||||
justify-center
|
||||
ma-0
|
||||
>
|
||||
<v-progress-circular indeterminate color="grey lighten-5"></v-progress-circular>
|
||||
</v-layout>
|
||||
|
||||
<v-btn v-if="hover || photo.selected" :flat="!hover" icon large absolute
|
||||
:ripple="false" style="right: 4px; bottom: 4px;"
|
||||
@click.stop.prevent="selectPhoto(photo)">
|
||||
<v-icon v-if="photo.selected" color="white">check_box</v-icon>
|
||||
<v-icon v-else color="white">check_box_outline_blank</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn v-if="hover || photo.PhotoFavorite" :flat="!hover" icon large absolute
|
||||
:ripple="false" style="bottom: 4px; left: 4px"
|
||||
@click.stop.prevent="likePhoto(photo)">
|
||||
<v-icon v-if="photo.PhotoFavorite" color="white">favorite
|
||||
</v-icon>
|
||||
<v-icon v-else color="white">favorite_border</v-icon>
|
||||
</v-btn>
|
||||
</v-img>
|
||||
<v-btn v-if="hover || photo.PhotoFavorite" :flat="!hover" icon large absolute
|
||||
:ripple="false" style="bottom: 4px; left: 4px"
|
||||
@click.stop.prevent="likePhoto(photo)">
|
||||
<v-icon v-if="photo.PhotoFavorite" color="white">favorite
|
||||
</v-icon>
|
||||
<v-icon v-else color="white">favorite_border</v-icon>
|
||||
</v-btn>
|
||||
</v-img>
|
||||
|
||||
|
||||
<v-card-title primary-title class="pa-3">
|
||||
<div>
|
||||
<h3 class="subheading mb-2" :title="photo.PhotoTitle">Album Title</h3>
|
||||
<div class="caption">
|
||||
Some description
|
||||
<br/>
|
||||
<v-icon size="14">date_range</v-icon>
|
||||
11/01/2018 - 01/02/2018
|
||||
<br/>
|
||||
<v-icon size="14">photo_camera</v-icon>
|
||||
iPhone SE, Canon
|
||||
<br/>
|
||||
<v-icon size="14">location_on</v-icon>
|
||||
South africa, Germany (Most occuring locations)
|
||||
</div>
|
||||
</div>
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
<v-card-title primary-title class="pa-3">
|
||||
<div>
|
||||
<h3 class="subheading mb-2" :title="photo.PhotoTitle">Album Title</h3>
|
||||
<div class="caption">
|
||||
Some description
|
||||
<br/>
|
||||
<v-icon size="14">date_range</v-icon>
|
||||
11/01/2018 - 01/02/2018
|
||||
<br/>
|
||||
<v-icon size="14">photo_camera</v-icon>
|
||||
iPhone SE, Canon
|
||||
<br/>
|
||||
<v-icon size="14">location_on</v-icon>
|
||||
South africa, Germany (Most occuring locations)
|
||||
</div>
|
||||
</div>
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
|
||||
<v-container grid-list-xs fluid class="pa-0" v-if="query.view === 'tiles'">
|
||||
<v-card v-if="results.length === 0">
|
||||
<v-card-title primary-title>
|
||||
<div>
|
||||
<h3 class="headline mb-3">No photos matched your search</h3>
|
||||
<div>Try using other terms and search options such as category, country and camera.</div>
|
||||
</div>
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
<v-layout row wrap>
|
||||
<v-flex
|
||||
v-for="(photo, index) in results"
|
||||
:key="photo.ID"
|
||||
xs12 sm6 md3 lg2 d-flex
|
||||
v-bind:class="{ selected: photo.selected }"
|
||||
>
|
||||
<v-hover>
|
||||
<v-card tile slot-scope="{ hover }"
|
||||
:dark="photo.selected"
|
||||
:class="photo.selected ? 'elevation-14 ma-1' : hover ? 'elevation-6 ma-2' : 'elevation-2 ma-2'">
|
||||
<v-img :src="photo.getThumbnailUrl('square', 500)"
|
||||
aspect-ratio="1"
|
||||
class="grey lighten-2"
|
||||
style="cursor: pointer"
|
||||
@click="openPhoto(index)"
|
||||
>
|
||||
<v-layout
|
||||
slot="placeholder"
|
||||
fill-height
|
||||
align-center
|
||||
justify-center
|
||||
ma-0
|
||||
>
|
||||
<v-progress-circular indeterminate
|
||||
color="grey lighten-5"></v-progress-circular>
|
||||
</v-layout>
|
||||
|
||||
<v-btn v-if="hover || photo.selected" :flat="!hover" icon large absolute
|
||||
:ripple="false" style="right: 4px; bottom: 4px;"
|
||||
@click.stop.prevent="selectPhoto(photo)">
|
||||
<v-icon v-if="photo.selected" color="white">check_box</v-icon>
|
||||
<v-icon v-else color="white">check_box_outline_blank</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn v-if="hover || photo.PhotoFavorite" :flat="!hover" icon large absolute
|
||||
:ripple="false" style="bottom: 4px; left: 4px"
|
||||
@click.stop.prevent="likePhoto(photo)">
|
||||
<v-icon v-if="photo.PhotoFavorite" color="white">favorite</v-icon>
|
||||
<v-icon v-else color="white">favorite_border</v-icon>
|
||||
</v-btn>
|
||||
</v-img>
|
||||
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
<v-snackbar
|
||||
v-model="snackbarVisible"
|
||||
bottom
|
||||
:timeout="0"
|
||||
<v-container grid-list-xs fluid class="pa-0" v-if="query.view === 'tiles'">
|
||||
<v-card v-if="results.length === 0">
|
||||
<v-card-title primary-title>
|
||||
<div>
|
||||
<h3 class="headline mb-3">No photos matched your search</h3>
|
||||
<div>Try using other terms and search options such as category, country and camera.</div>
|
||||
</div>
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
<v-layout row wrap>
|
||||
<v-flex
|
||||
v-for="(photo, index) in results"
|
||||
:key="photo.ID"
|
||||
xs12 sm6 md3 lg2 d-flex
|
||||
v-bind:class="{ selected: photo.selected }"
|
||||
>
|
||||
<v-hover>
|
||||
<v-card tile slot-scope="{ hover }"
|
||||
:dark="photo.selected"
|
||||
:class="photo.selected ? 'elevation-14 ma-1' : hover ? 'elevation-6 ma-2' : 'elevation-2 ma-2'">
|
||||
<v-img :src="photo.getThumbnailUrl('square', 500)"
|
||||
aspect-ratio="1"
|
||||
class="grey lighten-2"
|
||||
style="cursor: pointer"
|
||||
@click="openPhoto(index)"
|
||||
>
|
||||
<v-layout
|
||||
slot="placeholder"
|
||||
fill-height
|
||||
align-center
|
||||
justify-center
|
||||
ma-0
|
||||
>
|
||||
{{ snackbarText }}
|
||||
<v-btn
|
||||
class="pr-0"
|
||||
color="primary"
|
||||
icon
|
||||
flat
|
||||
@click="clearSelection()"
|
||||
>
|
||||
<v-icon>close</v-icon>
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
<photoswipe :images="results" ref="gallery"></photoswipe>
|
||||
</div>
|
||||
<v-progress-circular indeterminate
|
||||
color="grey lighten-5"></v-progress-circular>
|
||||
</v-layout>
|
||||
|
||||
<v-btn v-if="hover || photo.selected" :flat="!hover" icon large absolute
|
||||
:ripple="false" style="right: 4px; bottom: 4px;"
|
||||
@click.stop.prevent="selectPhoto(photo)">
|
||||
<v-icon v-if="photo.selected" color="white">check_box</v-icon>
|
||||
<v-icon v-else color="white">check_box_outline_blank</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn v-if="hover || photo.PhotoFavorite" :flat="!hover" icon large absolute
|
||||
:ripple="false" style="bottom: 4px; left: 4px"
|
||||
@click.stop.prevent="likePhoto(photo)">
|
||||
<v-icon v-if="photo.PhotoFavorite" color="white">favorite</v-icon>
|
||||
<v-icon v-else color="white">favorite_border</v-icon>
|
||||
</v-btn>
|
||||
</v-img>
|
||||
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
<v-snackbar
|
||||
v-model="snackbarVisible"
|
||||
bottom
|
||||
:timeout="0"
|
||||
>
|
||||
{{ snackbarText }}
|
||||
<v-btn
|
||||
class="pr-0"
|
||||
color="primary"
|
||||
icon
|
||||
flat
|
||||
@click="clearSelection()"
|
||||
>
|
||||
<v-icon>close</v-icon>
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Photo from 'model/photo';
|
||||
|
||||
export default {
|
||||
name: 'browse',
|
||||
props: {},
|
||||
data() {
|
||||
const query = this.$route.query;
|
||||
const order = query['order'] ? query['order'] : 'newest';
|
||||
const camera = query['camera'] ? parseInt(query['camera']) : 0;
|
||||
const q = query['q'] ? query['q'] : '';
|
||||
const country = query['country'] ? query['country'] : '';
|
||||
const view = query['view'] ? query['view'] : 'details';
|
||||
const cameras = [{ID: 0, CameraModel: 'All Cameras'}].concat(this.$config.getValue('cameras'));
|
||||
const countries = [{
|
||||
LocCountryCode: '',
|
||||
LocCountry: 'All Countries'
|
||||
}].concat(this.$config.getValue('countries'));
|
||||
export default {
|
||||
name: 'browse',
|
||||
props: {},
|
||||
data() {
|
||||
const query = this.$route.query;
|
||||
const order = query['order'] ? query['order'] : 'newest';
|
||||
const camera = query['camera'] ? parseInt(query['camera']) : 0;
|
||||
const q = query['q'] ? query['q'] : '';
|
||||
const country = query['country'] ? query['country'] : '';
|
||||
const view = query['view'] ? query['view'] : 'details';
|
||||
const cameras = [{ID: 0, CameraModel: 'All Cameras'}].concat(this.$config.getValue('cameras'));
|
||||
const countries = [{
|
||||
LocCountryCode: '',
|
||||
LocCountry: 'All Countries'
|
||||
}].concat(this.$config.getValue('countries'));
|
||||
|
||||
return {
|
||||
'snackbarVisible': false,
|
||||
'snackbarText': '',
|
||||
'advandedSearch': false,
|
||||
'window': {
|
||||
width: 0,
|
||||
height: 0
|
||||
},
|
||||
'results': [],
|
||||
'query': {
|
||||
view: view,
|
||||
country: country,
|
||||
camera: camera,
|
||||
order: order,
|
||||
q: q,
|
||||
},
|
||||
'options': {
|
||||
'categories': [
|
||||
{value: '', text: 'All Categories'},
|
||||
{value: 'airport', text: 'Airport'},
|
||||
{value: 'amenity', text: 'Amenity'},
|
||||
{value: 'building', text: 'Building'},
|
||||
{value: 'historic', text: 'Historic'},
|
||||
{value: 'shop', text: 'Shop'},
|
||||
{value: 'tourism', text: 'Tourism'},
|
||||
],
|
||||
'views': [
|
||||
{value: 'details', text: 'Details'},
|
||||
{value: 'list', text: 'List'},
|
||||
{value: 'tiles', text: 'Tiles'},
|
||||
],
|
||||
'countries': countries,
|
||||
'cameras': cameras,
|
||||
'sorting': [
|
||||
{value: 'newest', text: 'Newest first'},
|
||||
{value: 'oldest', text: 'Oldest first'},
|
||||
{value: 'imported', text: 'Recently imported'},
|
||||
],
|
||||
},
|
||||
'listColumns': [
|
||||
{text: 'Title', value: 'PhotoTitle'},
|
||||
{text: 'Description', value: 'PhotoFavorite'},
|
||||
{text: 'Taken At', value: 'TakenAt'},
|
||||
{text: 'City', value: 'LocCity'},
|
||||
{text: 'Country', value: 'LocCountry'},
|
||||
{text: 'Camera', value: 'CameraModel'},
|
||||
return {
|
||||
'snackbarVisible': false,
|
||||
'snackbarText': '',
|
||||
'advandedSearch': false,
|
||||
'window': {
|
||||
width: 0,
|
||||
height: 0
|
||||
},
|
||||
'results': [],
|
||||
'query': {
|
||||
view: view,
|
||||
country: country,
|
||||
camera: camera,
|
||||
order: order,
|
||||
q: q,
|
||||
},
|
||||
'options': {
|
||||
'categories': [
|
||||
{value: '', text: 'All Categories'},
|
||||
{value: 'airport', text: 'Airport'},
|
||||
{value: 'amenity', text: 'Amenity'},
|
||||
{value: 'building', text: 'Building'},
|
||||
{value: 'historic', text: 'Historic'},
|
||||
{value: 'shop', text: 'Shop'},
|
||||
{value: 'tourism', text: 'Tourism'},
|
||||
],
|
||||
'views': [
|
||||
{value: 'details', text: 'Details'},
|
||||
{value: 'list', text: 'List'},
|
||||
{value: 'tiles', text: 'Tiles'},
|
||||
],
|
||||
'countries': countries,
|
||||
'cameras': cameras,
|
||||
'sorting': [
|
||||
{value: 'newest', text: 'Newest first'},
|
||||
{value: 'oldest', text: 'Oldest first'},
|
||||
{value: 'imported', text: 'Recently imported'},
|
||||
],
|
||||
'view': view,
|
||||
'loadMoreDisabled': true,
|
||||
'pageSize': 60,
|
||||
'offset': 0,
|
||||
'lastQuery': {},
|
||||
'submitTimeout': false,
|
||||
'selected': [],
|
||||
'dialog': false,
|
||||
};
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
},
|
||||
methods: {
|
||||
handleResize() {
|
||||
this.window.width = window.innerWidth;
|
||||
this.window.height = window.innerHeight;
|
||||
},
|
||||
clearSelection() {
|
||||
'listColumns': [
|
||||
{text: 'Title', value: 'PhotoTitle'},
|
||||
{text: 'Description', value: 'PhotoFavorite'},
|
||||
{text: 'Taken At', value: 'TakenAt'},
|
||||
{text: 'City', value: 'LocCity'},
|
||||
{text: 'Country', value: 'LocCountry'},
|
||||
{text: 'Camera', value: 'CameraModel'},
|
||||
],
|
||||
'view': view,
|
||||
'loadMoreDisabled': true,
|
||||
'pageSize': 60,
|
||||
'offset': 0,
|
||||
'lastQuery': {},
|
||||
'submitTimeout': false,
|
||||
'selected': [],
|
||||
'dialog': false,
|
||||
};
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
},
|
||||
methods: {
|
||||
handleResize() {
|
||||
this.window.width = window.innerWidth;
|
||||
this.window.height = window.innerHeight;
|
||||
},
|
||||
clearSelection() {
|
||||
for (let i = 0; i < this.selected.length; i++) {
|
||||
this.selected[i].selected = false;
|
||||
}
|
||||
this.selected = [];
|
||||
this.updateSnackbar();
|
||||
},
|
||||
updateSnackbar(text) {
|
||||
if (!text) text = "";
|
||||
|
||||
this.snackbarText = text;
|
||||
|
||||
this.snackbarVisible = this.snackbarText !== "";
|
||||
},
|
||||
showSnackbar() {
|
||||
this.snackbarVisible = this.snackbarText !== "";
|
||||
},
|
||||
hideSnackbar() {
|
||||
this.snackbarVisible = false;
|
||||
},
|
||||
selectPhoto(photo, ev) {
|
||||
if (photo.selected) {
|
||||
for (let i = 0; i < this.selected.length; i++) {
|
||||
this.selected[i].selected = false;
|
||||
if (this.selected[i].id === photo.id) {
|
||||
this.selected.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.selected = [];
|
||||
this.updateSnackbar();
|
||||
},
|
||||
updateSnackbar(text) {
|
||||
if (!text) text = "";
|
||||
|
||||
this.snackbarText = text;
|
||||
photo.selected = false;
|
||||
} else {
|
||||
this.selected.push(photo);
|
||||
photo.selected = true;
|
||||
}
|
||||
|
||||
this.snackbarVisible = this.snackbarText !== "";
|
||||
},
|
||||
showSnackbar() {
|
||||
this.snackbarVisible = this.snackbarText !== "";
|
||||
},
|
||||
hideSnackbar() {
|
||||
if (this.selected.length > 0) {
|
||||
if (this.selected.length === 1) {
|
||||
this.snackbarText = 'One photo selected';
|
||||
} else {
|
||||
this.snackbarText = this.selected.length + ' photos selected';
|
||||
}
|
||||
this.snackbarVisible = true;
|
||||
} else {
|
||||
this.snackbarText = '';
|
||||
this.snackbarVisible = false;
|
||||
},
|
||||
selectPhoto(photo, ev) {
|
||||
if (photo.selected) {
|
||||
for (let i = 0; i < this.selected.length; i++) {
|
||||
if (this.selected[i].id === photo.id) {
|
||||
this.selected.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
photo.selected = false;
|
||||
} else {
|
||||
this.selected.push(photo);
|
||||
photo.selected = true;
|
||||
}
|
||||
|
||||
if (this.selected.length > 0) {
|
||||
if (this.selected.length === 1) {
|
||||
this.snackbarText = 'One photo selected';
|
||||
} else {
|
||||
this.snackbarText = this.selected.length + ' photos selected';
|
||||
}
|
||||
this.snackbarVisible = true;
|
||||
} else {
|
||||
this.snackbarText = '';
|
||||
this.snackbarVisible = false;
|
||||
}
|
||||
},
|
||||
likePhoto(photo) {
|
||||
photo.PhotoFavorite = !photo.PhotoFavorite;
|
||||
photo.like(photo.PhotoFavorite);
|
||||
},
|
||||
deletePhoto(photo) {
|
||||
this.$alert.success('Photo deleted');
|
||||
},
|
||||
formChange(event) {
|
||||
this.refreshList();
|
||||
},
|
||||
clearQuery() {
|
||||
this.query.q = '';
|
||||
this.refreshList();
|
||||
},
|
||||
openPhoto(index) {
|
||||
this.$refs.gallery.openPhoto(index)
|
||||
},
|
||||
loadMore() {
|
||||
if (this.loadMoreDisabled) return;
|
||||
|
||||
this.loadMoreDisabled = true;
|
||||
|
||||
this.offset += this.pageSize;
|
||||
|
||||
const params = {
|
||||
count: this.pageSize,
|
||||
offset: this.offset,
|
||||
};
|
||||
|
||||
Object.assign(params, this.lastQuery);
|
||||
|
||||
Photo.search(params).then(response => {
|
||||
console.log(response);
|
||||
this.results = this.results.concat(response.models);
|
||||
|
||||
this.loadMoreDisabled = (response.models.length < this.pageSize);
|
||||
|
||||
if (this.loadMoreDisabled) {
|
||||
this.$alert.info('All ' + this.results.length + ' photos loaded');
|
||||
}
|
||||
});
|
||||
},
|
||||
refreshList() {
|
||||
this.loadMoreDisabled = true;
|
||||
|
||||
// Don't query the same data more than once:197
|
||||
if (JSON.stringify(this.lastQuery) === JSON.stringify(this.query)) return;
|
||||
|
||||
Object.assign(this.lastQuery, this.query);
|
||||
|
||||
this.offset = 0;
|
||||
|
||||
this.$router.replace({query: this.query});
|
||||
|
||||
const params = {
|
||||
count: this.pageSize,
|
||||
offset: this.offset,
|
||||
};
|
||||
|
||||
Object.assign(params, this.query);
|
||||
|
||||
Photo.search(params).then(response => {
|
||||
this.results = response.models;
|
||||
|
||||
this.loadMoreDisabled = (response.models.length < this.pageSize);
|
||||
|
||||
if (this.loadMoreDisabled) {
|
||||
this.$alert.info(this.results.length + ' photos found');
|
||||
} else {
|
||||
this.$alert.info('More than 50 photos found');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
next()
|
||||
likePhoto(photo) {
|
||||
photo.PhotoFavorite = !photo.PhotoFavorite;
|
||||
photo.like(photo.PhotoFavorite);
|
||||
},
|
||||
created() {
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
this.handleResize();
|
||||
deletePhoto(photo) {
|
||||
this.$alert.success('Photo deleted');
|
||||
},
|
||||
formChange(event) {
|
||||
this.refreshList();
|
||||
},
|
||||
};
|
||||
clearQuery() {
|
||||
this.query.q = '';
|
||||
this.refreshList();
|
||||
},
|
||||
openPhoto(index) {
|
||||
this.$gallery.show(this.results, index)
|
||||
},
|
||||
loadMore() {
|
||||
if (this.loadMoreDisabled) return;
|
||||
|
||||
this.loadMoreDisabled = true;
|
||||
|
||||
this.offset += this.pageSize;
|
||||
|
||||
const params = {
|
||||
count: this.pageSize,
|
||||
offset: this.offset,
|
||||
};
|
||||
|
||||
Object.assign(params, this.lastQuery);
|
||||
|
||||
Photo.search(params).then(response => {
|
||||
console.log(response);
|
||||
this.results = this.results.concat(response.models);
|
||||
|
||||
this.loadMoreDisabled = (response.models.length < this.pageSize);
|
||||
|
||||
if (this.loadMoreDisabled) {
|
||||
this.$alert.info('All ' + this.results.length + ' photos loaded');
|
||||
}
|
||||
});
|
||||
},
|
||||
refreshList() {
|
||||
this.loadMoreDisabled = true;
|
||||
|
||||
// Don't query the same data more than once:197
|
||||
if (JSON.stringify(this.lastQuery) === JSON.stringify(this.query)) return;
|
||||
|
||||
Object.assign(this.lastQuery, this.query);
|
||||
|
||||
this.offset = 0;
|
||||
|
||||
this.$router.replace({query: this.query});
|
||||
|
||||
const params = {
|
||||
count: this.pageSize,
|
||||
offset: this.offset,
|
||||
};
|
||||
|
||||
Object.assign(params, this.query);
|
||||
|
||||
Photo.search(params).then(response => {
|
||||
this.results = response.models;
|
||||
|
||||
this.loadMoreDisabled = (response.models.length < this.pageSize);
|
||||
|
||||
if (this.loadMoreDisabled) {
|
||||
this.$alert.info(this.results.length + ' photos found');
|
||||
} else {
|
||||
this.$alert.info('More than 50 photos found');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
next()
|
||||
},
|
||||
created() {
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
this.handleResize();
|
||||
this.refreshList();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -312,7 +312,7 @@
|
|||
</v-btn>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
<photoswipe :images="results" ref="gallery"></photoswipe>
|
||||
|
||||
<v-dialog v-model="dialog" dark persistent max-width="600px">
|
||||
<v-card dark>
|
||||
<v-card-title>
|
||||
|
@ -663,7 +663,7 @@
|
|||
this.refreshList();
|
||||
},
|
||||
openPhoto(index) {
|
||||
this.$refs.gallery.openPhoto(index)
|
||||
this.$gallery.show(this.results, index)
|
||||
},
|
||||
loadMore() {
|
||||
if (this.loadMoreDisabled) return;
|
||||
|
@ -693,7 +693,7 @@
|
|||
refreshList() {
|
||||
this.loadMoreDisabled = true;
|
||||
|
||||
// Don't query the same data more than once:197
|
||||
// Don't query the same data more than once
|
||||
if (JSON.stringify(this.lastQuery) === JSON.stringify(this.query)) return;
|
||||
|
||||
Object.assign(this.lastQuery, this.query);
|
||||
|
|
|
@ -329,7 +329,7 @@
|
|||
this.refreshList();
|
||||
},
|
||||
openPhoto(index) {
|
||||
this.$refs.gallery.openPhoto(index)
|
||||
this.$gallery.show(this.results, index)
|
||||
},
|
||||
loadMore() {
|
||||
if (this.loadMoreDisabled) return;
|
||||
|
|
|
@ -1,24 +1,190 @@
|
|||
<template>
|
||||
<v-container fluid fill-height class="pa-0 map">
|
||||
<l-map :zoom="zoom" :center="center">
|
||||
<l-map :zoom="zoom" :center="center" :bounds="bounds" :options="options">
|
||||
<l-control position="bottomright">
|
||||
<v-toolbar dense floating color="grey lighten-4" v-on:dblclick.stop v-on:click.stop>
|
||||
<v-btn icon v-on:click="currentPosition()">
|
||||
<v-icon>my_location</v-icon>
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-text-field class="pt-3 pr-3"
|
||||
single-line
|
||||
label="Search"
|
||||
prepend-inner-icon="search"
|
||||
clearable
|
||||
color="blue-grey"
|
||||
@click:clear="clearQuery"
|
||||
v-model="query.q"
|
||||
@keyup.enter.native="formChange"
|
||||
></v-text-field>
|
||||
</v-toolbar>
|
||||
</l-control>
|
||||
<l-tile-layer :url="url" :attribution="attribution"></l-tile-layer>
|
||||
<l-marker :lat-lng="marker"></l-marker>
|
||||
<l-marker v-for="photo in photos" v-bind:data="photo"
|
||||
v-bind:key="photo.index" :lat-lng="photo.location" :icon="photo.icon"
|
||||
:options="photo.options" @click="openPhoto(photo.index)"></l-marker>
|
||||
<l-marker v-if="position" :lat-lng="position" z-index-offset="1"></l-marker>
|
||||
</l-map>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as L from "leaflet";
|
||||
import Photo from "model/photo";
|
||||
|
||||
export default {
|
||||
name: 'places',
|
||||
data() {
|
||||
const query = this.$route.query;
|
||||
const order = query['order'] ? query['order'] : 'newest';
|
||||
const q = query['q'] ? query['q'] : '';
|
||||
|
||||
return {
|
||||
zoom: 13,
|
||||
center: L.latLng(52.5259279,13.414496),
|
||||
zoom: 15,
|
||||
position: null,
|
||||
center: L.latLng(52.5259279, 13.414496),
|
||||
url: 'https://{s}.tile.osm.org/{z}/{x}/{y}.png',
|
||||
attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
|
||||
marker: L.latLng(52.5259279,13.414496),
|
||||
options: {
|
||||
icon: {
|
||||
iconSize: [40, 40]
|
||||
}
|
||||
},
|
||||
photos: [],
|
||||
results: [],
|
||||
query: {
|
||||
order: order,
|
||||
q: q,
|
||||
},
|
||||
offset: 0,
|
||||
pageSize: 100,
|
||||
lastQuery: {},
|
||||
bounds: null,
|
||||
minLat: null,
|
||||
maxLat: null,
|
||||
minLong: null,
|
||||
maxLong: null,
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
methods: {
|
||||
openPhoto(index) {
|
||||
this.$gallery.show(this.results, index)
|
||||
},
|
||||
currentPosition() {
|
||||
if ("geolocation" in navigator) {
|
||||
const self = this;
|
||||
this.$alert.success('Finding your position...');
|
||||
navigator.geolocation.getCurrentPosition(function(position) {
|
||||
self.center = L.latLng(position.coords.latitude, position.coords.longitude);
|
||||
self.position = L.latLng(position.coords.latitude, position.coords.longitude);
|
||||
});
|
||||
} else {
|
||||
this.$alert.warning('Geolocation is not available');
|
||||
}
|
||||
},
|
||||
formChange() {
|
||||
this.refreshList();
|
||||
},
|
||||
clearQuery() {
|
||||
this.query.q = '';
|
||||
this.refreshList();
|
||||
},
|
||||
resetBoundingBox() {
|
||||
this.minLat = null;
|
||||
this.maxLat = null;
|
||||
this.minLong = null;
|
||||
this.maxLong = null;
|
||||
},
|
||||
fitBoundingBox(lat, long) {
|
||||
if(this.maxLat === null || lat > this.maxLat) {
|
||||
this.maxLat = lat;
|
||||
}
|
||||
|
||||
if(this.minLat === null || lat < this.minLat) {
|
||||
this.minLat = lat;
|
||||
}
|
||||
|
||||
if(this.maxLong === null || long > this.maxLong) {
|
||||
this.maxLong = long;
|
||||
}
|
||||
|
||||
if(this.minLong === null || long < this.minLong) {
|
||||
this.minLong = long;
|
||||
}
|
||||
},
|
||||
updateMap() {
|
||||
const photos = [];
|
||||
|
||||
this.resetBoundingBox();
|
||||
|
||||
for (let i = 0, len = this.results.length; i < len; i++) {
|
||||
let result = this.results[i];
|
||||
|
||||
if (!result.hasLocation()) continue;
|
||||
|
||||
this.fitBoundingBox(result.PhotoLat, result.PhotoLong);
|
||||
|
||||
photos.push({
|
||||
id: result.getId(),
|
||||
index: i,
|
||||
options: {
|
||||
title: result.getTitle(),
|
||||
clickable: true,
|
||||
},
|
||||
icon: L.icon({
|
||||
iconUrl: result.getThumbnailUrl('square', 50),
|
||||
iconRetinaUrl: result.getThumbnailUrl('square', 100),
|
||||
iconSize: [50, 50],
|
||||
}),
|
||||
location: L.latLng(result.PhotoLat, result.PhotoLong),
|
||||
});
|
||||
}
|
||||
|
||||
this.center = photos[photos.length - 1].location;
|
||||
|
||||
this.bounds = [[this.maxLat, this.minLong], [this.minLat, this.maxLong]];
|
||||
|
||||
this.$alert.info(photos.length + ' photos found');
|
||||
|
||||
this.photos = photos;
|
||||
},
|
||||
|
||||
refreshList() {
|
||||
// Don't query the same data more than once
|
||||
if (JSON.stringify(this.lastQuery) === JSON.stringify(this.query)) return;
|
||||
|
||||
Object.assign(this.lastQuery, this.query);
|
||||
|
||||
this.offset = 0;
|
||||
|
||||
this.$router.replace({query: this.query});
|
||||
|
||||
const params = {
|
||||
count: this.pageSize,
|
||||
offset: this.offset,
|
||||
};
|
||||
|
||||
Object.assign(params, this.query);
|
||||
|
||||
Photo.search(params).then(response => {
|
||||
if (!response.models.length) {
|
||||
this.$alert.warning('No photos found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.results = response.models;
|
||||
|
||||
this.updateMap();
|
||||
});
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.refreshList();
|
||||
},
|
||||
};
|
||||
|
||||
/* L.icon({
|
||||
html: '<div style="background-image: url(/api/v1/thumbnails/square/40/cc1a022c30fff3d5603f1c3f722ec1960e3fa95e);"></div>',
|
||||
className: 'leaflet-marker-photo' }), */
|
||||
</script>
|
||||
|
||||
|
|
|
@ -145,16 +145,18 @@
|
|||
<td>28/11/2019</td>
|
||||
<td>#4</td>
|
||||
<td>55</td>
|
||||
<td> <v-btn color="success"
|
||||
@click.stop="dialog = true">
|
||||
Edit
|
||||
</v-btn></td>
|
||||
<td>
|
||||
<v-btn color="success"
|
||||
@click.stop="dialog = true">
|
||||
Edit
|
||||
</v-btn>
|
||||
</td>
|
||||
</template>
|
||||
</v-data-table>
|
||||
<v-container fluid v-if="query.view === 'cloud'">
|
||||
<v-layout justify-space-around>
|
||||
<v-flex>
|
||||
<v-img src="/assets/img/tagcloud.jpg" aspect-ratio="1.7" @click.stop="dialog = true"></v-img>
|
||||
<v-img src="/assets/img/tagcloud.jpg" aspect-ratio="1.7" @click.stop="dialog = true"></v-img>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
|
@ -176,7 +178,7 @@
|
|||
</v-btn>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
<photoswipe :images="results" ref="gallery"></photoswipe>
|
||||
|
||||
<v-dialog v-model="dialog" dark persistent max-width="600px">
|
||||
<v-card dark>
|
||||
<v-card-title>
|
||||
|
@ -231,7 +233,7 @@
|
|||
<template>
|
||||
<form>
|
||||
13 tags selected <br>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer></v-spacer>
|
||||
<v-select
|
||||
v-model="select"
|
||||
:items="items2"
|
||||
|
@ -295,10 +297,10 @@
|
|||
{value: 'cloud', text: 'Cloud'},
|
||||
{value: 'list', text: 'List'},
|
||||
],
|
||||
'groups': [
|
||||
'groups': [
|
||||
{value: 'a', text: 'Animals'},
|
||||
{value: 'b', text: 'People'},
|
||||
],
|
||||
],
|
||||
'sorting': [
|
||||
{value: 'newest', text: 'Mostly used'},
|
||||
{value: 'oldest', text: 'Rarely used'},
|
||||
|
@ -404,7 +406,7 @@
|
|||
this.refreshList();
|
||||
},
|
||||
openPhoto(index) {
|
||||
this.$refs.gallery.openPhoto(index)
|
||||
this.$gallery.show(this.results, index)
|
||||
},
|
||||
loadMore() {
|
||||
if (this.loadMoreDisabled) return;
|
||||
|
|
127
frontend/src/common/gallery.js
Normal file
127
frontend/src/common/gallery.js
Normal file
|
@ -0,0 +1,127 @@
|
|||
import PhotoSwipe from 'photoswipe'
|
||||
import PhotoSwipeUI_Default from 'photoswipe/dist/photoswipe-ui-default.js'
|
||||
|
||||
class Gallery {
|
||||
constructor() {
|
||||
this.photos = [];
|
||||
this.el = null;
|
||||
}
|
||||
|
||||
photosWithSizes() {
|
||||
return this.photos.map(this.createPhotoSizes);
|
||||
}
|
||||
|
||||
createPhotoSizes(photo) {
|
||||
const createPhotoSize = height => ({
|
||||
src: photo.getThumbnailUrl('fit', height),
|
||||
w: photo.calculateWidth(height),
|
||||
h: height,
|
||||
title: photo.PhotoTitle
|
||||
});
|
||||
|
||||
return {
|
||||
xxs: createPhotoSize(320),
|
||||
xs: createPhotoSize(500),
|
||||
s: createPhotoSize(720),
|
||||
m: createPhotoSize(1280),
|
||||
l: createPhotoSize(1920),
|
||||
xl: createPhotoSize(2560),
|
||||
xxl: createPhotoSize(3840)
|
||||
}
|
||||
}
|
||||
|
||||
getEl() {
|
||||
if(!this.el) {
|
||||
const elements = document.querySelectorAll('.pswp');
|
||||
|
||||
if(elements.length !== 1) {
|
||||
let err = "There should be only one PhotoSwipe element";
|
||||
console.log(err, elements);
|
||||
throw err;
|
||||
}
|
||||
|
||||
this.el = elements[0];
|
||||
}
|
||||
|
||||
return this.el;
|
||||
}
|
||||
|
||||
show(photos, index = 0) {
|
||||
if (!Array.isArray(photos) || photos.length === 0 || index >= photos.length) {
|
||||
console.log("Array passed to gallery was empty:", photos);
|
||||
return
|
||||
}
|
||||
|
||||
this.photos = photos;
|
||||
|
||||
const options = {
|
||||
index: index,
|
||||
history: false,
|
||||
preload: true,
|
||||
focus: true,
|
||||
modal: true,
|
||||
closeEl: true,
|
||||
captionEl: true,
|
||||
fullscreenEl: true,
|
||||
zoomEl: true,
|
||||
shareEl: false,
|
||||
counterEl: false,
|
||||
arrowEl: true,
|
||||
preloaderEl: true,
|
||||
};
|
||||
|
||||
let photosWithSizes = this.photosWithSizes();
|
||||
let gallery = new PhotoSwipe(this.getEl(), PhotoSwipeUI_Default, photosWithSizes, options);
|
||||
let realViewportWidth;
|
||||
let realViewportHeight;
|
||||
let previousSize;
|
||||
let nextSize;
|
||||
let firstResize = true;
|
||||
let photoSrcWillChange;
|
||||
|
||||
gallery.listen('beforeResize', () => {
|
||||
realViewportWidth = gallery.viewportSize.x * window.devicePixelRatio;
|
||||
realViewportHeight = gallery.viewportSize.y * window.devicePixelRatio;
|
||||
|
||||
if (!previousSize) {
|
||||
previousSize = 'm'
|
||||
}
|
||||
|
||||
nextSize = this.constructor.mapViewportToImageSize(realViewportWidth, realViewportHeight, photosWithSizes[index])
|
||||
if (nextSize !== previousSize) {
|
||||
photoSrcWillChange = true
|
||||
}
|
||||
|
||||
if (photoSrcWillChange && !firstResize) {
|
||||
gallery.invalidateCurrItems();
|
||||
}
|
||||
|
||||
if (firstResize) {
|
||||
firstResize = false;
|
||||
}
|
||||
|
||||
photoSrcWillChange = false;
|
||||
});
|
||||
|
||||
|
||||
gallery.listen('gettingData', function (index, item) {
|
||||
item.src = item[nextSize].src;
|
||||
item.w = item[nextSize].w;
|
||||
item.h = item[nextSize].h;
|
||||
item.title = item[nextSize].title;
|
||||
previousSize = nextSize;
|
||||
});
|
||||
|
||||
gallery.init();
|
||||
}
|
||||
|
||||
static mapViewportToImageSize(viewportWidth, viewportHeight, item) {
|
||||
for (const [sizeKey, photo] of Object.entries(item)) {
|
||||
if (photo.w > viewportWidth || photo.h > viewportHeight) {
|
||||
return sizeKey
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Gallery;
|
|
@ -3,13 +3,13 @@
|
|||
v-model="visible"
|
||||
:color="color"
|
||||
:timeout="0"
|
||||
:class="textColor"
|
||||
top
|
||||
right
|
||||
>
|
||||
{{ text }}
|
||||
<v-btn
|
||||
class="pr-0"
|
||||
|
||||
:class="textColor + ' pr-0'"
|
||||
icon
|
||||
flat
|
||||
@click="close"
|
||||
|
@ -27,6 +27,7 @@
|
|||
return {
|
||||
text: '',
|
||||
color: 'primary',
|
||||
textColor: '',
|
||||
visible: false,
|
||||
messages: [],
|
||||
lastMessageId: 1,
|
||||
|
@ -63,28 +64,28 @@
|
|||
},
|
||||
|
||||
addWarningMessage: function (message) {
|
||||
this.addMessage('warning', message, 4000);
|
||||
this.addMessage('warning', 'black--text', message, 4000);
|
||||
},
|
||||
|
||||
addErrorMessage: function (message) {
|
||||
this.addMessage('error', message, 8000);
|
||||
this.addMessage('error', 'white--text', message, 8000);
|
||||
},
|
||||
|
||||
addSuccessMessage: function (message) {
|
||||
this.addMessage('success', message, 3000);
|
||||
this.addMessage('success', 'white--text', message, 3000);
|
||||
},
|
||||
|
||||
addInfoMessage: function (message) {
|
||||
this.addMessage('info', message, 3000);
|
||||
this.addMessage('info', 'white--text', message, 3000);
|
||||
},
|
||||
|
||||
addMessage: function (color, message, delay) {
|
||||
addMessage: function (color, textColor, message, delay) {
|
||||
if (message === this.lastMessage) return;
|
||||
|
||||
this.lastMessageId++;
|
||||
this.lastMessage = message;
|
||||
|
||||
const alert = {'id': this.lastMessageId, 'color': color, 'delay': delay, 'msg': message};
|
||||
const alert = {'id': this.lastMessageId, 'color': color, 'textColor': textColor, 'delay': delay, 'msg': message};
|
||||
|
||||
this.messages.push(alert);
|
||||
|
||||
|
@ -103,6 +104,7 @@
|
|||
if(message) {
|
||||
this.text = message.msg;
|
||||
this.color = message.color;
|
||||
this.textColor = message.textColor;
|
||||
this.visible = true;
|
||||
|
||||
setTimeout(() => {
|
||||
|
|
83
frontend/src/component/app-gallery.vue
Normal file
83
frontend/src/component/app-gallery.vue
Normal file
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<div>
|
||||
<slot v-bind:openGallery="openGallery"></slot>
|
||||
<!-- Root element of PhotoSwipe. Must have class pswp. -->
|
||||
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
|
||||
<!-- Background of PhotoSwipe.
|
||||
It's a separate element as animating opacity is faster than rgba(). -->
|
||||
<div class="pswp__bg"></div>
|
||||
|
||||
<!-- Slides wrapper with overflow:hidden. -->
|
||||
<div class="pswp__scroll-wrap">
|
||||
|
||||
<!-- Container that holds slides.
|
||||
PhotoSwipe keeps only 3 of them in the DOM to save memory.
|
||||
Don't modify these 3 pswp__item elements, data is added later on. -->
|
||||
<div class="pswp__container">
|
||||
<div class="pswp__item"></div>
|
||||
<div class="pswp__item"></div>
|
||||
<div class="pswp__item"></div>
|
||||
</div>
|
||||
|
||||
<!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
|
||||
<div class="pswp__ui pswp__ui--hidden">
|
||||
|
||||
<div class="pswp__top-bar">
|
||||
|
||||
<!-- Controls are self-explanatory. Order can be changed. -->
|
||||
|
||||
<div class="pswp__counter"></div>
|
||||
|
||||
<button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
|
||||
|
||||
<button class="pswp__button pswp__button--share" title="Share"></button>
|
||||
|
||||
<button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
|
||||
|
||||
<button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
|
||||
|
||||
<!-- Preloader demo https://codepen.io/dimsemenov/pen/yyBWoR -->
|
||||
<!-- element will get class pswp__preloader--active when preloader is running -->
|
||||
<div class="pswp__preloader">
|
||||
<div class="pswp__preloader__icn">
|
||||
<div class="pswp__preloader__cut">
|
||||
<div class="pswp__preloader__donut"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
|
||||
<div class="pswp__share-tooltip"></div>
|
||||
</div>
|
||||
|
||||
<button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
|
||||
</button>
|
||||
|
||||
<button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
|
||||
</button>
|
||||
|
||||
<div class="pswp__caption">
|
||||
<div class="pswp__caption__center"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import 'photoswipe/dist/photoswipe.css'
|
||||
import 'photoswipe/dist/default-skin/default-skin.css'
|
||||
|
||||
export default {
|
||||
name: "photoswipe",
|
||||
methods: {
|
||||
openGallery: function () {
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,21 +1,22 @@
|
|||
import AppAlert from './app-alert.vue';
|
||||
import AppNavigation from './app-navigation.vue';
|
||||
import AppLoadingBar from './app-loading-bar.vue';
|
||||
import PhotoSwipe from './photoswipe.vue';
|
||||
import {LMap, LMarker, LTileLayer} from 'vue2-leaflet';
|
||||
import AppGallery from './app-gallery.vue';
|
||||
import {LMap, LTileLayer, LMarker, LControl} from 'vue2-leaflet';
|
||||
import {Icon} from 'leaflet';
|
||||
|
||||
const components = {};
|
||||
|
||||
components.install = (Vue) => {
|
||||
Vue.component('app-alert', AppAlert);
|
||||
Vue.component('photoswipe', PhotoSwipe);
|
||||
Vue.component('app-gallery', AppGallery);
|
||||
Vue.component('app-navigation', AppNavigation);
|
||||
Vue.component('app-loading-bar', AppLoadingBar);
|
||||
|
||||
Vue.component('l-map', LMap);
|
||||
Vue.component('l-tile-layer', LTileLayer);
|
||||
Vue.component('l-marker', LMarker);
|
||||
Vue.component('l-control', LControl);
|
||||
|
||||
delete Icon.Default.prototype._getIconUrl;
|
||||
|
||||
|
|
|
@ -1,175 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<slot v-bind:openGallery="openGallery"></slot>
|
||||
<!-- Root element of PhotoSwipe. Must have class pswp. -->
|
||||
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
|
||||
<!-- Background of PhotoSwipe.
|
||||
It's a separate element as animating opacity is faster than rgba(). -->
|
||||
<div class="pswp__bg"></div>
|
||||
|
||||
<!-- Slides wrapper with overflow:hidden. -->
|
||||
<div class="pswp__scroll-wrap">
|
||||
|
||||
<!-- Container that holds slides.
|
||||
PhotoSwipe keeps only 3 of them in the DOM to save memory.
|
||||
Don't modify these 3 pswp__item elements, data is added later on. -->
|
||||
<div class="pswp__container">
|
||||
<div class="pswp__item"></div>
|
||||
<div class="pswp__item"></div>
|
||||
<div class="pswp__item"></div>
|
||||
</div>
|
||||
|
||||
<!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
|
||||
<div class="pswp__ui pswp__ui--hidden">
|
||||
|
||||
<div class="pswp__top-bar">
|
||||
|
||||
<!-- Controls are self-explanatory. Order can be changed. -->
|
||||
|
||||
<div class="pswp__counter"></div>
|
||||
|
||||
<button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
|
||||
|
||||
<button class="pswp__button pswp__button--share" title="Share"></button>
|
||||
|
||||
<button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
|
||||
|
||||
<button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
|
||||
|
||||
<!-- Preloader demo https://codepen.io/dimsemenov/pen/yyBWoR -->
|
||||
<!-- element will get class pswp__preloader--active when preloader is running -->
|
||||
<div class="pswp__preloader">
|
||||
<div class="pswp__preloader__icn">
|
||||
<div class="pswp__preloader__cut">
|
||||
<div class="pswp__preloader__donut"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
|
||||
<div class="pswp__share-tooltip"></div>
|
||||
</div>
|
||||
|
||||
<button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
|
||||
</button>
|
||||
|
||||
<button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
|
||||
</button>
|
||||
|
||||
<div class="pswp__caption">
|
||||
<div class="pswp__caption__center"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PhotoSwipe from 'photoswipe'
|
||||
import PhotoSwipeUI_Default from 'photoswipe/dist/photoswipe-ui-default.js'
|
||||
import 'photoswipe/dist/photoswipe.css'
|
||||
import 'photoswipe/dist/default-skin/default-skin.css'
|
||||
|
||||
export default {
|
||||
name: "photoswipe",
|
||||
props: {
|
||||
images: Array
|
||||
},
|
||||
computed: {
|
||||
imagesWithSizes: function() {
|
||||
return this.images.map(this.createPhotoSizes);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createPhotoSizes(photo) {
|
||||
const createPhotoSize = height => ({
|
||||
src: photo.getThumbnailUrl('fit', height),
|
||||
w: photo.calculateWidth(height),
|
||||
h: height,
|
||||
title: photo.PhotoTitle
|
||||
});
|
||||
|
||||
return {
|
||||
xxs: createPhotoSize(320),
|
||||
xs: createPhotoSize(500),
|
||||
s: createPhotoSize(720),
|
||||
m: createPhotoSize(1280),
|
||||
l: createPhotoSize(1920),
|
||||
xl: createPhotoSize(2560),
|
||||
xxl: createPhotoSize(3840)
|
||||
}
|
||||
},
|
||||
|
||||
mapViewportToImageSize(viewportWidth, viewportHeight, item) {
|
||||
for (const [sizeKey, photo] of Object.entries(item)) {
|
||||
if (photo.w > viewportWidth || photo.h > viewportHeight) {
|
||||
return sizeKey
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
openGallery: function () {
|
||||
},
|
||||
|
||||
openPhoto: function (index = 0) {
|
||||
if (this.$props.images.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const pswpElement = document.querySelectorAll('.pswp')[0];
|
||||
|
||||
const options = {
|
||||
index
|
||||
};
|
||||
|
||||
let gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, this.imagesWithSizes, options);
|
||||
let realViewportWidth;
|
||||
let realViewportHeight;
|
||||
let previousSize;
|
||||
let nextSize;
|
||||
let firstResize = true;
|
||||
let imageSrcWillChange;
|
||||
|
||||
gallery.listen('beforeResize', () => {
|
||||
realViewportWidth = gallery.viewportSize.x * window.devicePixelRatio;
|
||||
realViewportHeight = gallery.viewportSize.y * window.devicePixelRatio;
|
||||
|
||||
if (!previousSize) {
|
||||
previousSize = 'm'
|
||||
}
|
||||
|
||||
nextSize = this.mapViewportToImageSize(realViewportWidth, realViewportHeight, this.imagesWithSizes[index])
|
||||
if (nextSize !== previousSize) {
|
||||
imageSrcWillChange = true
|
||||
}
|
||||
|
||||
if (imageSrcWillChange && !firstResize) {
|
||||
gallery.invalidateCurrItems();
|
||||
}
|
||||
|
||||
if (firstResize) {
|
||||
firstResize = false;
|
||||
}
|
||||
|
||||
imageSrcWillChange = false;
|
||||
});
|
||||
|
||||
|
||||
gallery.listen('gettingData', function (index, item) {
|
||||
item.src = item[nextSize].src;
|
||||
item.w = item[nextSize].w;
|
||||
item.h = item[nextSize].h;
|
||||
item.title = item[nextSize].title;
|
||||
previousSize = nextSize;
|
||||
});
|
||||
|
||||
gallery.init();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -10,6 +10,10 @@ class Photo extends Abstract {
|
|||
return this.ID;
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return this.PhotoTitle;
|
||||
}
|
||||
|
||||
getColor() {
|
||||
switch (this.PhotoColor) {
|
||||
case 'brown':
|
||||
|
@ -66,6 +70,10 @@ class Photo extends Abstract {
|
|||
return result.join(', ');
|
||||
}
|
||||
|
||||
hasLocation() {
|
||||
return this.PhotoLat !== 0 || this.PhotoLong !== 0;
|
||||
}
|
||||
|
||||
getLocation() {
|
||||
const location = [];
|
||||
|
||||
|
|
Loading…
Reference in a new issue