Frontend: Add global clipboard for photo selection #15
This commit is contained in:
parent
1d2e0faf36
commit
65648450a4
|
@ -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, {
|
||||
|
|
105
frontend/src/common/clipboard.js
Normal file
105
frontend/src/common/clipboard.js
Normal 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;
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -36,7 +36,7 @@ class Abstract {
|
|||
}
|
||||
|
||||
getId() {
|
||||
return this.id;
|
||||
return this.ID;
|
||||
}
|
||||
|
||||
hasId() {
|
||||
|
|
|
@ -8,7 +8,7 @@ class User extends Abstract {
|
|||
}
|
||||
|
||||
getId() {
|
||||
return this.userId;
|
||||
return this.ID;
|
||||
}
|
||||
|
||||
getRegisterForm() {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue