From ec71858477ac9640901ae9c578e40eba6a47a497 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Sat, 9 Jan 2021 01:33:20 +0100 Subject: [PATCH] Frontend: Refactor clipboard #477 --- frontend/src/app.js | 3 +- frontend/src/common/clipboard.js | 44 +++++++++++++++++++++---- frontend/src/component/photo/cards.vue | 27 +++++++-------- frontend/src/component/photo/list.vue | 15 +++++---- frontend/src/component/photo/mosaic.vue | 22 +++++++------ frontend/src/css/animate.css | 7 ++++ frontend/src/model/photo.js | 7 ++++ frontend/src/pages/photos.vue | 9 +++-- frontend/src/share.js | 3 +- frontend/src/share/photo/cards.vue | 20 +++++------ frontend/src/share/photo/list.vue | 8 ++--- frontend/src/share/photo/mosaic.vue | 16 ++++----- frontend/src/share/photos.vue | 11 +++++-- 13 files changed, 123 insertions(+), 69 deletions(-) diff --git a/frontend/src/app.js b/frontend/src/app.js index dcc868c2a..1b446a4f0 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -59,7 +59,6 @@ import offline from "offline-plugin/runtime"; // Initialize helpers const viewer = new Viewer(); -const clipboard = new Clipboard(window.localStorage, "photo_clipboard"); const isPublic = config.get("public"); const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( navigator.userAgent @@ -83,7 +82,7 @@ Vue.prototype.$api = Api; Vue.prototype.$log = Log; Vue.prototype.$socket = Socket; Vue.prototype.$config = config; -Vue.prototype.$clipboard = clipboard; +Vue.prototype.$clipboard = Clipboard; Vue.prototype.$isMobile = isMobile; Vue.prototype.$rtl = rtl; diff --git a/frontend/src/common/clipboard.js b/frontend/src/common/clipboard.js index 6cf36b62a..52dd48ecb 100644 --- a/frontend/src/common/clipboard.js +++ b/frontend/src/common/clipboard.js @@ -31,10 +31,11 @@ https://docs.photoprism.org/developer-guide/ import RestModel from "model/rest"; import Notify from "common/notify"; import { $gettext } from "./vm"; +import Event from "pubsub-js"; export const MaxItems = 999; -export default class Clipboard { +export class Clipboard { /** * @param {Storage} storage * @param {string} key @@ -83,28 +84,37 @@ export default class Clipboard { } const id = model.getId(); - this.toggleId(id); + model.Selected = this.toggleId(id); } toggleId(id) { const index = this.selection.indexOf(id); + let result = false; + if (index === -1) { if (this.selection.length >= this.maxItems) { Notify.warn($gettext("Can't select more items")); return; } + Event.publish("photos.updated", { entities: [{ UID: id, Selected: true }] }); + this.selection.push(id); this.selectionMap["id:" + id] = true; this.lastId = id; + result = true; } else { + Event.publish("photos.updated", { entities: [{ UID: id, Selected: false }] }); + this.selection.splice(index, 1); delete this.selectionMap["id:" + id]; this.lastId = ""; } this.saveToStorage(); + + return result; } add(model) { @@ -114,17 +124,19 @@ export default class Clipboard { const id = model.getId(); - this.addId(id); + model.Selected = this.addId(id); } addId(id) { + Event.publish("photos.updated", { entities: [{ UID: id, Selected: true }] }); + if (this.hasId(id)) { - return; + return true; } if (this.selection.length >= this.maxItems) { Notify.warn($gettext("Can't select more items")); - return; + return false; } this.selection.push(id); @@ -132,6 +144,8 @@ export default class Clipboard { this.lastId = id; this.saveToStorage(); + + return true; } addRange(rangeEnd, models) { @@ -177,11 +191,15 @@ export default class Clipboard { return; } - this.removeId(model.getId()); + model.Selected = this.removeId(model.getId()); } removeId(id) { - if (!this.hasId(id)) return; + Event.publish("photos.updated", { entities: [{ UID: id, Selected: false }] }); + + if (!this.hasId(id)) { + return false; + } const index = this.selection.indexOf(id); @@ -190,6 +208,8 @@ export default class Clipboard { delete this.selectionMap["id:" + id]; this.saveToStorage(); + + return false; } getIds() { @@ -209,9 +229,19 @@ export default class Clipboard { } clear() { + Event.publish("photos.updated", { + entities: this.selection.map((uid) => { + return { UID: uid, Selected: false }; + }), + }); + this.lastId = ""; this.selectionMap = {}; this.selection.splice(0, this.selection.length); this.storage.removeItem(this.storageKey); } } + +const PhotoClipboard = new Clipboard(window.localStorage, "photo_clipboard"); + +export default PhotoClipboard; diff --git a/frontend/src/component/photo/cards.vue b/frontend/src/component/photo/cards.vue index eb6b5c9d5..8b54beaa9 100644 --- a/frontend/src/component/photo/cards.vue +++ b/frontend/src/component/photo/cards.vue @@ -26,30 +26,30 @@ :data-uid="photo.UID" class="p-photo" xs12 sm6 md4 lg3 xlg2 xxxl1 d-flex - :class="{ 'is-selected': clipboard.has(photo), portrait: photo.Portrait }" + :class="{ 'is-selected': photo.Selected, portrait: photo.Portrait }" > - - + lock - - check_circle radio_button_off @@ -110,7 +110,7 @@ @click.stop.prevent="openPhoto(index, true)"> burst_mode - zoom_in @@ -207,10 +207,11 @@ export default { album: Object, filter: Object, context: String, + selectMode: Boolean, }, data() { return { - clipboard: this.$clipboard, + spinners: false, showLocation: this.$config.settings().features.places, hidePrivate: this.$config.settings().features.private, debug: this.$config.get('debug'), @@ -246,7 +247,7 @@ export default { onClick(ev, index) { let longClick = (this.mouseDown.index === index && ev.timeStamp - this.mouseDown.timeStamp > 400); - if (longClick || this.selection.length > 0) { + if (longClick || this.selectMode) { if (longClick || ev.shiftKey) { this.selectRange(index); } else { diff --git a/frontend/src/component/photo/list.vue b/frontend/src/component/photo/list.vue index 5a1e7b7d9..a69175f7f 100644 --- a/frontend/src/component/photo/list.vue +++ b/frontend/src/component/photo/list.vue @@ -39,7 +39,8 @@ @contextmenu="onContextMenu($event, props.index)" @click.stop.prevent="onClick($event, props.index)" > - - + - check_circle - @@ -112,13 +113,13 @@ export default { name: 'PPhotoList', props: { photos: Array, - selection: Array, openPhoto: Function, editPhoto: Function, openLocation: Function, album: Object, filter: Object, context: String, + selectMode: Boolean, }, data() { let m = this.$gettext("Couldn't find anything."); @@ -132,7 +133,7 @@ export default { let showName = this.filter.order === 'name'; return { - clipboard: this.$clipboard, + spinners: false, config: this.$config.values, notFoundMessage: m, 'selected': [], @@ -199,7 +200,7 @@ export default { onClick(ev, index) { let longClick = (this.mouseDown.index === index && ev.timeStamp - this.mouseDown.timeStamp > 400); - if (longClick || this.selection.length > 0) { + if (longClick || this.selectMode) { if (longClick || ev.shiftKey) { this.selectRange(index); } else { diff --git a/frontend/src/component/photo/mosaic.vue b/frontend/src/component/photo/mosaic.vue index aa89050c3..6f93b7bea 100644 --- a/frontend/src/component/photo/mosaic.vue +++ b/frontend/src/component/photo/mosaic.vue @@ -24,13 +24,13 @@ v-for="(photo, index) in photos" :key="index" :data-uid="photo.UID" - :class="{ selected: $clipboard.has(photo), portrait: photo.Portrait }" + :class="{ selected: photo.Selected, portrait: photo.Portrait }" class="p-photo" xs4 sm3 md2 lg1 d-flex > - - + lock - - check_circle radio_button_off @@ -106,7 +107,7 @@ @click.stop.prevent="openPhoto(index, true)"> burst_mode - zoom_in @@ -128,15 +129,16 @@ export default { name: 'PPhotoMosaic', props: { photos: Array, - selection: Array, openPhoto: Function, editPhoto: Function, album: Object, filter: Object, context: String, + selectMode: Boolean, }, data() { return { + spinners: false, hidePrivate: this.$config.settings().features.private, mouseDown: { index: -1, @@ -159,7 +161,7 @@ export default { onClick(ev, index) { let longClick = (this.mouseDown.index === index && ev.timeStamp - this.mouseDown.timeStamp > 400); - if (longClick || this.selection.length > 0) { + if (longClick || this.selectMode) { if (longClick || ev.shiftKey) { this.selectRange(index); } else { diff --git a/frontend/src/css/animate.css b/frontend/src/css/animate.css index 4675df142..7dc5e4118 100644 --- a/frontend/src/css/animate.css +++ b/frontend/src/css/animate.css @@ -92,3 +92,10 @@ #photoprism .animate-stretch { animation: stretch 1.5s ease-out 0s alternate infinite none running; } + +#photoprism .select-transition { + -webkit-transition-duration: 15ms !important; + -moz-transition-duration: 15ms !important; + -o-transition-duration: 15ms !important; + transition-duration: 15ms !important; +} diff --git a/frontend/src/model/photo.js b/frontend/src/model/photo.js index 35d2367fe..dbaa3c8c5 100644 --- a/frontend/src/model/photo.js +++ b/frontend/src/model/photo.js @@ -36,6 +36,7 @@ import Util from "common/util"; import { config } from "../session"; import countries from "options/countries.json"; import { $gettext } from "common/vm"; +import Clipboard from "common/clipboard"; export const SrcManual = "manual"; export const CodecAvc1 = "avc1"; @@ -51,8 +52,14 @@ export const MonthUnknown = -1; export const DayUnknown = -1; export class Photo extends RestModel { + constructor(values) { + super(values); + this.Selected = Clipboard.has(this); + } + getDefaults() { return { + Selected: false, UID: "", DocumentID: "", Type: TypeImage, diff --git a/frontend/src/pages/photos.vue b/frontend/src/pages/photos.vue index 199bb5f69..aa75a3bd5 100644 --- a/frontend/src/pages/photos.vue +++ b/frontend/src/pages/photos.vue @@ -16,14 +16,14 @@ 0; + }, context: function () { if (!this.staticFilter) { return "photos"; diff --git a/frontend/src/share.js b/frontend/src/share.js index 8dd76be66..3caf0895f 100644 --- a/frontend/src/share.js +++ b/frontend/src/share.js @@ -58,7 +58,6 @@ import * as options from "./options/options"; // Initialize helpers const viewer = new Viewer(); -const clipboard = new Clipboard(window.localStorage, "photo_clipboard"); const isPublic = config.get("public"); const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( navigator.userAgent @@ -82,7 +81,7 @@ Vue.prototype.$api = Api; Vue.prototype.$log = Log; Vue.prototype.$socket = Socket; Vue.prototype.$config = config; -Vue.prototype.$clipboard = clipboard; +Vue.prototype.$clipboard = Clipboard; Vue.prototype.$isMobile = isMobile; Vue.prototype.$rtl = rtl; diff --git a/frontend/src/share/photo/cards.vue b/frontend/src/share/photo/cards.vue index 0c7d47f33..4c801a442 100644 --- a/frontend/src/share/photo/cards.vue +++ b/frontend/src/share/photo/cards.vue @@ -22,16 +22,16 @@ :data-uid="photo.UID" class="p-photo" xs12 sm6 md4 lg3 xlg2 xxxl1 d-flex - :class="{ 'is-selected': $clipboard.has(photo), portrait: photo.Portrait }" + :class="{ 'is-selected': photo.Selected, portrait: photo.Portrait }" > lock - - check_circle radio_button_off @@ -106,7 +106,7 @@ @click.stop.prevent="openPhoto(index, true)"> burst_mode - zoom_in @@ -162,12 +162,12 @@ export default { name: 'PPhotoCards', props: { photos: Array, - selection: Array, openPhoto: Function, editPhoto: Function, openLocation: Function, album: Object, filter: Object, + selectMode: Boolean, }, data() { return { @@ -202,7 +202,7 @@ export default { onClick(ev, index) { let longClick = (this.mouseDown.index === index && ev.timeStamp - this.mouseDown.timeStamp > 400); - if (longClick || this.selection.length > 0) { + if (longClick || this.selectMode) { if (longClick || ev.shiftKey) { this.selectRange(index); } else { diff --git a/frontend/src/share/photo/list.vue b/frontend/src/share/photo/list.vue index 1feef40fd..e17e3fd4e 100644 --- a/frontend/src/share/photo/list.vue +++ b/frontend/src/share/photo/list.vue @@ -46,11 +46,11 @@ color="accent lighten-5"> - check_circle - @@ -93,12 +93,12 @@ export default { name: 'PPhotoList', props: { photos: Array, - selection: Array, openPhoto: Function, editPhoto: Function, openLocation: Function, album: Object, filter: Object, + selectMode: Boolean, }, data() { let m = this.$gettext("Couldn't find anything."); @@ -172,7 +172,7 @@ export default { onClick(ev, index) { let longClick = (this.mouseDown.index === index && ev.timeStamp - this.mouseDown.timeStamp > 400); - if (longClick || this.selection.length > 0) { + if (longClick || this.selectMode) { if (longClick || ev.shiftKey) { this.selectRange(index); } else { diff --git a/frontend/src/share/photo/mosaic.vue b/frontend/src/share/photo/mosaic.vue index c456eebcb..6438fe1ba 100644 --- a/frontend/src/share/photo/mosaic.vue +++ b/frontend/src/share/photo/mosaic.vue @@ -20,13 +20,13 @@ v-for="(photo, index) in photos" :key="index" :data-uid="photo.UID" - :class="{ selected: $clipboard.has(photo), portrait: photo.Portrait }" + :class="{ selected: photo.Selected, portrait: photo.Portrait }" class="p-photo" xs4 sm3 md2 lg1 d-flex > lock - - check_circle radio_button_off @@ -101,7 +101,7 @@ @click.stop.prevent="openPhoto(index, true)"> burst_mode - zoom_in @@ -123,11 +123,11 @@ export default { name: 'PPhotoMosaic', props: { photos: Array, - selection: Array, openPhoto: Function, editPhoto: Function, album: Object, filter: Object, + selectMode: Boolean, }, data() { return { @@ -153,7 +153,7 @@ export default { onClick(ev, index) { let longClick = (this.mouseDown.index === index && ev.timeStamp - this.mouseDown.timeStamp > 400); - if (longClick || this.selection.length > 0) { + if (longClick || this.selectMode) { if (longClick || ev.shiftKey) { this.selectRange(index); } else { diff --git a/frontend/src/share/photos.vue b/frontend/src/share/photos.vue index 204f11900..1491be2db 100644 --- a/frontend/src/share/photos.vue +++ b/frontend/src/share/photos.vue @@ -60,14 +60,14 @@ 0; + }, + }, watch: { '$route'() { const query = this.$route.query;