Frontend: Add global clipboard for photo selection #15

This commit is contained in:
Michael Mayer 2019-05-21 11:54:39 +02:00
parent 1d2e0faf36
commit 65648450a4
9 changed files with 132 additions and 29 deletions

View file

@ -5,6 +5,7 @@ import PhotoPrism from "photoprism.vue";
import Routes from "routes";
import Api from "common/api";
import Config from "common/config";
import Clipboard from "common/clipboard";
import Components from "component/register";
import Maps from "maps/register";
import Alert from "common/alert";
@ -16,17 +17,20 @@ import InfiniteScroll from "vue-infinite-scroll";
import VueTruncate from "vue-truncate-filter";
import VueFullscreen from "vue-fullscreen";
// Initialize client-side session
// Initialize helpers
const session = new Session(window.localStorage);
const config = new Config(window.localStorage, window.appConfig);
const viewer = new Viewer();
const clipboard = new Clipboard(window.localStorage, "photo_clipboard");
// Set global helpers
// Assign helpers to VueJS prototype
Vue.prototype.$event = Event;
Vue.prototype.$alert = Alert;
Vue.prototype.$viewer = new Viewer;
Vue.prototype.$viewer = viewer;
Vue.prototype.$session = session;
Vue.prototype.$api = Api;
Vue.prototype.$config = config;
Vue.prototype.$clipboard = clipboard;
// Register Vuetify
Vue.use(Vuetify, {

View file

@ -0,0 +1,105 @@
class Clipboard {
/**
* @param {Storage} storage
* @param {string} key
*/
constructor(storage, key) {
this.storageKey = key ? key : "clipboard";
this.storage = storage;
this.selectionMap = {};
this.selection = [];
this.loadFromStorage();
}
loadFromStorage() {
const photosJson = this.storage.getItem(this.storageKey);
if (photosJson !== null && typeof photosJson !== "undefined") {
this.setIds(JSON.parse(photosJson));
}
}
saveToStorage() {
this.storage.setItem(this.storageKey, JSON.stringify(this.selection));
}
toggle(model) {
const id = model.getId();
this.toggleId(id);
}
toggleId(id) {
const index = this.selection.indexOf(id);
if (index === -1) {
this.selection.push(id);
this.selectionMap["id:" + id] = true;
} else {
this.selection.splice(index, 1);
delete this.selectionMap["id:" + id];
}
this.saveToStorage();
}
add(model) {
const id = model.getId();
this.addId(id);
}
addId(id) {
if (this.hasId(id)) return;
this.selection.push(id);
this.selectionMap["id:" + id] = true;
this.saveToStorage();
}
has(model) {
return this.hasId(model.getId())
}
hasId(id) {
return typeof this.selectionMap["id:" + id] !== "undefined";
}
remove(model) {
const id = model.getId();
if (!this.hasId(id)) return;
const index = this.selection.indexOf(id);
this.selection.splice(index, 1);
delete this.selectionMap["id:" + id];
this.saveToStorage();
}
getIds() {
return this.selection;
}
setIds(ids) {
if (!Array.isArray(ids)) return;
this.selection = ids;
this.selectionMap = {};
for (let i = 0; i < this.selection.length; i++) {
this.selectionMap["id:" + this.selection[i]] = true;
}
}
clear() {
this.selectionMap = {};
this.selection.splice(0, this.selection.length);
this.storage.removeItem(this.storageKey);
}
}
export default Clipboard;

View file

@ -17,12 +17,12 @@
>
<v-hover>
<v-card tile slot-scope="{ hover }"
:dark="selection.includes(photo.ID)"
:class="selection.includes(photo.ID) ? 'elevation-15 ma-1' : 'elevation-2 ma-2'">
:dark="$clipboard.has(photo)"
:class="$clipboard.has(photo) ? 'elevation-15 ma-1' : 'elevation-2 ma-2'">
<v-img
:src="photo.getThumbnailUrl('tile_500')"
aspect-ratio="1"
v-bind:class="{ selected: selection.includes(photo.ID) }"
v-bind:class="{ selected: $clipboard.has(photo) }"
style="cursor: pointer"
class="grey lighten-2"
@click="open(index)"
@ -38,11 +38,11 @@
<v-progress-circular indeterminate color="grey lighten-5"></v-progress-circular>
</v-layout>
<v-btn v-if="hover || selection.includes(photo.ID)" :flat="!hover" :ripple="false"
<v-btn v-if="hover || $clipboard.has(photo)" :flat="!hover" :ripple="false"
icon large absolute
class="p-photo-select"
@click.stop.prevent="select(photo)">
<v-icon v-if="selection.length && selection.includes(photo.ID)" color="white">check_circle</v-icon>
<v-icon v-if="selection.length && $clipboard.has(photo)" color="white">check_circle</v-icon>
<v-icon v-else color="grey lighten-3">radio_button_off</v-icon>
</v-btn>

View file

@ -14,8 +14,8 @@
<v-btn icon small :ripple="false"
class="p-photo-select"
@click.stop.prevent="select(props.item)">
<v-icon v-if="selection.length && selection.includes(props.item.ID)" color="grey darken-2">check_circle</v-icon>
<v-icon v-else-if="!selection.includes(props.item.ID)" color="grey lighten-4">radio_button_off</v-icon>
<v-icon v-if="selection.length && $clipboard.has(props.item)" color="grey darken-2">check_circle</v-icon>
<v-icon v-else-if="!$clipboard.has(props.item)" color="grey lighten-4">radio_button_off</v-icon>
</v-btn>
</td>
<td @click="open(props.index)" class="p-pointer">{{ props.item.PhotoTitle }}</td>

View file

@ -12,13 +12,13 @@
<v-flex
v-for="(photo, index) in photos"
:key="index"
v-bind:class="{ selected: selection.includes(photo.ID) }"
v-bind:class="{ selected: $clipboard.has(photo) }"
class="p-photo"
xs4 sm3 md2 lg1 d-flex
>
<v-hover>
<v-card tile slot-scope="{ hover }"
:class="selection.includes(photo.ID) ? 'elevation-15 ma-1' : hover ? 'elevation-6 ma-2' : 'elevation-2 ma-2'">
:class="$clipboard.has(photo) ? 'elevation-15 ma-1' : hover ? 'elevation-6 ma-2' : 'elevation-2 ma-2'">
<v-img :src="photo.getThumbnailUrl('tile_224')"
aspect-ratio="1"
class="grey lighten-2"
@ -36,11 +36,11 @@
color="grey lighten-5"></v-progress-circular>
</v-layout>
<v-btn v-if="hover || selection.includes(photo.ID)" :flat="!hover" :ripple="false"
<v-btn v-if="hover || $clipboard.has(photo)" :flat="!hover" :ripple="false"
icon small absolute
class="p-photo-select"
@click.stop.prevent="select(photo)">
<v-icon v-if="selection.length && selection.includes(photo.ID)" color="white">check_circle</v-icon>
<v-icon v-if="selection.length && $clipboard.has(photo)" color="white">check_circle</v-icon>
<v-icon v-else color="grey lighten-3">radio_button_off</v-icon>
</v-btn>

View file

@ -12,13 +12,13 @@
<v-flex
v-for="(photo, index) in photos"
:key="index"
v-bind:class="{ selected: selection.includes(photo.ID) }"
v-bind:class="{ selected: $clipboard.has(photo) }"
class="p-photo"
xs12 sm6 md3 lg2 d-flex
>
<v-hover>
<v-card tile slot-scope="{ hover }"
:class="selection.includes(photo.ID) ? 'elevation-15 ma-1' : hover ? 'elevation-6 ma-2' : 'elevation-2 ma-2'">
:class="$clipboard.has(photo) ? 'elevation-15 ma-1' : hover ? 'elevation-6 ma-2' : 'elevation-2 ma-2'">
<v-img :src="photo.getThumbnailUrl('tile_500')"
aspect-ratio="1"
class="grey lighten-2"
@ -36,12 +36,12 @@
color="grey lighten-5"></v-progress-circular>
</v-layout>
<v-btn v-if="hover || selection.includes(photo.ID)" :flat="!hover" :ripple="false"
<v-btn v-if="hover || $clipboard.has(photo)" :flat="!hover" :ripple="false"
icon large absolute
class="p-photo-select"
@click.stop.prevent="select(photo)">
<v-icon v-if="selection.length && selection.includes(photo.ID)" color="white">check_circle</v-icon>
<v-icon v-else-if="!selection.includes(photo.ID)" color="grey lighten-3">radio_button_off</v-icon>
<v-icon v-if="selection.length && $clipboard.has(photo)" color="white">check_circle</v-icon>
<v-icon v-else-if="!$clipboard.has(photo)" color="grey lighten-3">radio_button_off</v-icon>
</v-btn>
<v-btn v-if="hover || photo.PhotoFavorite" :flat="!hover" :ripple="false"

View file

@ -36,7 +36,7 @@ class Abstract {
}
getId() {
return this.id;
return this.ID;
}
hasId() {

View file

@ -8,7 +8,7 @@ class User extends Abstract {
}
getId() {
return this.userId;
return this.ID;
}
getRegisterForm() {

View file

@ -189,7 +189,7 @@
'loadMoreDisabled': true,
'menuVisible': false,
'results': [],
'selected': [],
'selected': this.$clipboard.selection,
'view': view,
'pageSize': 60,
'offset': 0,
@ -252,13 +252,7 @@
this.menuVisible = false;
},
selectPhoto(photo) {
const index = this.selected.indexOf(photo.ID);
if (index === -1) {
this.selected.push(photo.ID);
} else {
this.selected.splice(index, 1);
}
this.$clipboard.toggle(photo);
},
likePhoto(photo) {
photo.PhotoFavorite = !photo.PhotoFavorite;