UI: Deduplication of photo cards, mosaic and list view components (#2434)
This commit is contained in:
parent
e13eb8b3e0
commit
2080459579
10 changed files with 151 additions and 781 deletions
|
@ -3,7 +3,10 @@
|
|||
<template v-if="photos.length === 0">
|
||||
<v-alert
|
||||
:value="true"
|
||||
color="secondary-dark" icon="lightbulb_outline" class="no-results ma-2 opacity-70" outline
|
||||
color="secondary-dark"
|
||||
:icon="isSharedView ? 'image_not_supported' : 'lightbulb_outline'"
|
||||
class="no-results ma-2 opacity-70"
|
||||
outline
|
||||
>
|
||||
<h3 v-if="filter.order === 'edited'" class="body-2 ma-0 pa-0">
|
||||
<translate>No recently edited pictures</translate>
|
||||
|
@ -13,9 +16,11 @@
|
|||
</h3>
|
||||
<p class="body-1 mt-2 mb-0 pa-0">
|
||||
<translate>Try again using other filters or keywords.</translate>
|
||||
<translate>In case pictures you expect are missing, please rescan your library and wait until indexing has been completed.</translate>
|
||||
<template v-if="$config.feature('review')">
|
||||
<translate>Non-photographic and low-quality images require a review before they appear in search results.</translate>
|
||||
<template v-if="!isSharedView">
|
||||
<translate>In case pictures you expect are missing, please rescan your library and wait until indexing has been completed.</translate>
|
||||
<template v-if="$config.feature('review')">
|
||||
<translate>Non-photographic and low-quality images require a review before they appear in search results.</translate>
|
||||
</template>
|
||||
</template>
|
||||
</p>
|
||||
</v-alert>
|
||||
|
@ -94,9 +99,9 @@
|
|||
<button v-if="photo.Type !== 'image' || photo.Files.length > 1"
|
||||
class="input-open"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onOpen($event, index, true)"
|
||||
@touchend.stop.prevent="onOpen($event, index, !isSharedView, photo.Type === 'live')"
|
||||
@touchmove.stop.prevent
|
||||
@click.stop.prevent="onOpen($event, index, true)">
|
||||
@click.stop.prevent="onOpen($event, index, !isSharedView, photo.Type === 'live')">
|
||||
<i v-if="photo.Type === 'raw'" class="action-raw" :title="$gettext('RAW')">photo_camera</i>
|
||||
<i v-if="photo.Type === 'live'" class="action-live" :title="$gettext('Live')"><icon-live-photo/></i>
|
||||
<i v-if="photo.Type === 'animated'" class="action-animated" :title="$gettext('Animated')">gif</i>
|
||||
|
@ -108,13 +113,13 @@
|
|||
class="input-view"
|
||||
:title="$gettext('View')"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onOpen($event, index, false)"
|
||||
@touchend.stop.prevent="onOpen($event, index)"
|
||||
@touchmove.stop.prevent
|
||||
@click.stop.prevent="onOpen($event, index, false)">
|
||||
@click.stop.prevent="onOpen($event, index)">
|
||||
<i class="action-fullscreen">zoom_in</i>
|
||||
</button>
|
||||
|
||||
<button v-if="featPrivate && photo.Private" class="input-private">
|
||||
<button v-if="!isSharedView && featPrivate && photo.Private" class="input-private">
|
||||
<i class="select-on">lock</i>
|
||||
</button>
|
||||
|
||||
|
@ -139,6 +144,7 @@
|
|||
</button>
|
||||
|
||||
<button
|
||||
v-if="!isSharedView"
|
||||
class="input-favorite"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="toggleLike($event, index)"
|
||||
|
@ -149,7 +155,7 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<v-card-actions v-if="photo.Quality < 3 && context === 'review'" class="card-details pa-0">
|
||||
<v-card-actions v-if="!isSharedView && photo.Quality < 3 && context === 'review'" class="card-details pa-0">
|
||||
<v-layout row wrap align-center>
|
||||
<v-flex xs6 class="text-xs-center pa-1">
|
||||
<v-btn color="accent lighten-2"
|
||||
|
@ -174,41 +180,41 @@
|
|||
<div>
|
||||
<h3 class="body-2 mb-2" :title="photo.Title">
|
||||
<button class="action-title-edit" :data-uid="photo.UID"
|
||||
@click.exact="editPhoto(index)">
|
||||
@click.exact="isSharedView ? openPhoto(index) : editPhoto(index)">
|
||||
{{ photo.Title | truncate(80) }}
|
||||
</button>
|
||||
</h3>
|
||||
<div v-if="photo.Description" class="caption mb-2" :title="$gettext('Description')">
|
||||
<button @click.exact="editPhoto(index)">
|
||||
<button @[!isSharedView&&`click`].exact="editPhoto(index)">
|
||||
{{ photo.Description }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="caption">
|
||||
<button class="action-date-edit" :data-uid="photo.UID"
|
||||
@click.exact="editPhoto(index)">
|
||||
@[!isSharedView&&`click`].exact="editPhoto(index)">
|
||||
<i :title="$gettext('Taken')">date_range</i>
|
||||
{{ photo.getDateString(true) }}
|
||||
</button>
|
||||
<br>
|
||||
<button v-if="photo.Type === 'video'" :title="$gettext('Video')"
|
||||
@click.exact="openPhoto(index, true)">
|
||||
@[!isSharedView&&`click`].exact="openPhoto(index)">
|
||||
<i>movie</i>
|
||||
{{ photo.getVideoInfo() }}
|
||||
</button>
|
||||
<button v-else-if="photo.Type === 'animated'" :title="$gettext('Animated')+' GIF'"
|
||||
@click.exact="openPhoto(index, true)">
|
||||
@[!isSharedView&&`click`].exact="openPhoto(index)">
|
||||
<i>gif_box</i>
|
||||
{{ photo.getVideoInfo() }}
|
||||
</button>
|
||||
<button v-else :title="$gettext('Camera')" class="action-camera-edit"
|
||||
:data-uid="photo.UID" @click.exact="editPhoto(index)">
|
||||
:data-uid="photo.UID" @[!isSharedView&&`click`].exact="editPhoto(index)">
|
||||
<i>photo_camera</i>
|
||||
{{ photo.getPhotoInfo() }}
|
||||
</button>
|
||||
<template v-if="filter.order === 'name' && $config.feature('download')">
|
||||
<br>
|
||||
<button :title="$gettext('Name')"
|
||||
@click.exact="downloadFile(index)">
|
||||
@[!isSharedView&&`click`].exact="downloadFile(index)">
|
||||
<i>insert_drive_file</i>
|
||||
{{ photo.baseName() }}
|
||||
</button>
|
||||
|
@ -216,7 +222,7 @@
|
|||
<template v-if="featPlaces && photo.Country !== 'zz'">
|
||||
<br>
|
||||
<button :title="$gettext('Location')" class="action-location"
|
||||
:data-uid="photo.UID" @click.exact="openLocation(index)">
|
||||
:data-uid="photo.UID" @[!isSharedView&&`click`].exact="openLocation(index)">
|
||||
<i>location_on</i>
|
||||
{{ photo.locationInfo() }}
|
||||
</button>
|
||||
|
@ -273,6 +279,10 @@ export default {
|
|||
default: "",
|
||||
},
|
||||
selectMode: Boolean,
|
||||
isSharedView: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const featPlaces = this.$config.settings().features.places;
|
||||
|
@ -403,14 +413,14 @@ export default {
|
|||
*/
|
||||
this.$forceUpdate();
|
||||
},
|
||||
onOpen(ev, index, showMerged) {
|
||||
onOpen(ev, index, showMerged, preferVideo) {
|
||||
const inputType = this.input.eval(ev, index);
|
||||
|
||||
if (inputType !== ClickShort) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.openPhoto(index, showMerged);
|
||||
this.openPhoto(index, showMerged, preferVideo);
|
||||
},
|
||||
onClick(ev, index) {
|
||||
const inputType = this.input.eval(ev, index);
|
||||
|
@ -427,7 +437,7 @@ export default {
|
|||
this.toggle(this.photos[index]);
|
||||
}
|
||||
} else {
|
||||
this.openPhoto(index, false);
|
||||
this.openPhoto(index);
|
||||
}
|
||||
},
|
||||
onContextMenu(ev, index) {
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
<div v-if="photos.length === 0" class="pa-2">
|
||||
<v-alert
|
||||
:value="true"
|
||||
color="secondary-dark" icon="lightbulb_outline" class="no-results ma-2 opacity-70" outline
|
||||
color="secondary-dark"
|
||||
:icon="isSharedView ? 'image_not_supported' : 'lightbulb_outline'"
|
||||
class="no-results ma-2 opacity-70"
|
||||
outline
|
||||
>
|
||||
<h3 v-if="filter.order === 'edited'" class="body-2 ma-0 pa-0">
|
||||
<translate>No recently edited pictures</translate>
|
||||
|
@ -13,9 +16,11 @@
|
|||
</h3>
|
||||
<p class="body-1 mt-2 mb-0 pa-0">
|
||||
<translate>Try again using other filters or keywords.</translate>
|
||||
<translate>In case pictures you expect are missing, please rescan your library and wait until indexing has been completed.</translate>
|
||||
<template v-if="config.settings.features.review">
|
||||
<translate>Non-photographic and low-quality images require a review before they appear in search results.</translate>
|
||||
<template v-if="!isSharedView">
|
||||
<translate>In case pictures you expect are missing, please rescan your library and wait until indexing has been completed.</translate>
|
||||
<template v-if="config.settings.features.review">
|
||||
<translate>Non-photographic and low-quality images require a review before they appear in search results.</translate>
|
||||
</template>
|
||||
</template>
|
||||
</p>
|
||||
</v-alert>
|
||||
|
@ -67,7 +72,7 @@
|
|||
</button>
|
||||
<button v-else-if="photo.Type === 'video' || photo.Type === 'live' || photo.Type === 'animated'"
|
||||
class="input-open"
|
||||
@click.stop.prevent="openPhoto(index, true)">
|
||||
@click.stop.prevent="openPhoto(index, false, photo.Type === 'live')">
|
||||
<i v-if="photo.Type === 'live'" class="action-live" :title="$gettext('Live')"><icon-live-photo/></i>
|
||||
<i v-if="photo.Type === 'animated'" class="action-animated" :title="$gettext('Animated')">gif</i>
|
||||
<i v-if="photo.Type === 'video'" class="action-play" :title="$gettext('Video')">play_arrow</i>
|
||||
|
@ -76,7 +81,7 @@
|
|||
</td>
|
||||
|
||||
<td class="p-photo-desc clickable" :data-uid="photo.UID"
|
||||
@click.exact="editPhoto(index)">
|
||||
@click.exact="isSharedView ? openPhoto(index) : editPhoto(index)">
|
||||
{{ photo.Title }}
|
||||
</td>
|
||||
<td class="p-photo-desc hidden-xs-only" :title="photo.getDateString()">
|
||||
|
@ -102,27 +107,29 @@
|
|||
{{ photo.locationInfo() }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-xs-center">
|
||||
<template v-if="index < firstVisibleElementIndex || index > lastVisibileElementIndex">
|
||||
<div v-if="hidePrivate" class="v-btn v-btn--icon v-btn--small" />
|
||||
<div class="v-btn v-btn--icon v-btn--small" />
|
||||
</template>
|
||||
<template v-if="!isSharedView">
|
||||
<td class="text-xs-center">
|
||||
<template v-if="index < firstVisibleElementIndex || index > lastVisibileElementIndex">
|
||||
<div v-if="hidePrivate" class="v-btn v-btn--icon v-btn--small" />
|
||||
<div class="v-btn v-btn--icon v-btn--small" />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<v-btn v-if="hidePrivate" class="input-private" icon small flat :ripple="false"
|
||||
:data-uid="photo.UID" @click.stop.prevent="photo.togglePrivate()">
|
||||
<v-icon v-if="photo.Private" color="secondary-dark" class="select-on">lock</v-icon>
|
||||
<v-icon v-else color="secondary" class="select-off">lock_open</v-icon>
|
||||
</v-btn>
|
||||
<v-btn class="input-like" icon small flat :ripple="false"
|
||||
:data-uid="photo.UID" @click.stop.prevent="photo.toggleLike()">
|
||||
<v-icon v-if="photo.Favorite" color="pink lighten-3" :data-uid="photo.UID" class="select-on">
|
||||
favorite
|
||||
</v-icon>
|
||||
<v-icon v-else color="secondary" :data-uid="photo.UID" class="select-off">favorite_border</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</td>
|
||||
<template v-else>
|
||||
<v-btn v-if="hidePrivate" class="input-private" icon small flat :ripple="false"
|
||||
:data-uid="photo.UID" @click.stop.prevent="photo.togglePrivate()">
|
||||
<v-icon v-if="photo.Private" color="secondary-dark" class="select-on">lock</v-icon>
|
||||
<v-icon v-else color="secondary" class="select-off">lock_open</v-icon>
|
||||
</v-btn>
|
||||
<v-btn class="input-like" icon small flat :ripple="false"
|
||||
:data-uid="photo.UID" @click.stop.prevent="photo.toggleLike()">
|
||||
<v-icon v-if="photo.Favorite" color="pink lighten-3" :data-uid="photo.UID" class="select-on">
|
||||
favorite
|
||||
</v-icon>
|
||||
<v-icon v-else color="secondary" :data-uid="photo.UID" class="select-off">favorite_border</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -171,13 +178,17 @@ export default {
|
|||
default: "",
|
||||
},
|
||||
selectMode: Boolean,
|
||||
isSharedView: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
let m = this.$gettext("Couldn't find anything.");
|
||||
|
||||
m += " " + this.$gettext("Try again using other filters or keywords.");
|
||||
|
||||
if (this.$config.feature("review")) {
|
||||
if (!this.isSharedView && this.$config.feature("review")) {
|
||||
m += " " + this.$gettext("Non-photographic and low-quality images require a review before they appear in search results.");
|
||||
}
|
||||
|
||||
|
@ -282,13 +293,7 @@ export default {
|
|||
this.toggle(this.photos[index]);
|
||||
}
|
||||
} else if (this.photos[index]) {
|
||||
let photo = this.photos[index];
|
||||
|
||||
if ((photo.Type === 'video' || photo.Type === 'animated') && photo.isPlayable()) {
|
||||
this.openPhoto(index, true);
|
||||
} else {
|
||||
this.openPhoto(index, false);
|
||||
}
|
||||
this.openPhoto(index);
|
||||
}
|
||||
},
|
||||
onContextMenu(ev, index) {
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
<template v-if="photos.length === 0">
|
||||
<v-alert
|
||||
:value="true"
|
||||
color="secondary-dark" icon="lightbulb_outline" class="no-results ma-2 opacity-70" outline
|
||||
color="secondary-dark"
|
||||
:icon="isSharedView ? 'image_not_supported' : 'lightbulb_outline'"
|
||||
class="no-results ma-2 opacity-70"
|
||||
outline
|
||||
>
|
||||
<h3 v-if="filter.order === 'edited'" class="body-2 ma-0 pa-0">
|
||||
<translate>No recently edited pictures</translate>
|
||||
|
@ -13,9 +16,11 @@
|
|||
</h3>
|
||||
<p class="body-1 mt-2 mb-0 pa-0">
|
||||
<translate>Try again using other filters or keywords.</translate>
|
||||
<translate>In case pictures you expect are missing, please rescan your library and wait until indexing has been completed.</translate>
|
||||
<template v-if="$config.feature('review')">
|
||||
<translate>Non-photographic and low-quality images require a review before they appear in search results.</translate>
|
||||
<template v-if="!isSharedView">
|
||||
<translate>In case pictures you expect are missing, please rescan your library and wait until indexing has been completed.</translate>
|
||||
<template v-if="$config.feature('review')">
|
||||
<translate>Non-photographic and low-quality images require a review before they appear in search results.</translate>
|
||||
</template>
|
||||
</template>
|
||||
</p>
|
||||
</v-alert>
|
||||
|
@ -60,9 +65,9 @@
|
|||
<button v-if="photo.Type !== 'image' || photo.Files.length > 1"
|
||||
class="input-open"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onOpen($event, index, true)"
|
||||
@touchend.stop.prevent="onOpen($event, index, !isSharedView, photo.Type === 'live')"
|
||||
@touchmove.stop.prevent
|
||||
@click.stop.prevent="onOpen($event, index, true)">
|
||||
@click.stop.prevent="onOpen($event, index, !isSharedView, photo.Type === 'live')">
|
||||
<i v-if="photo.Type === 'raw'" color="white" class="action-raw" :title="$gettext('RAW')">photo_camera</i>
|
||||
<i v-if="photo.Type === 'live'" color="white" class="action-live" :title="$gettext('Live')"><icon-live-photo/></i>
|
||||
<i v-if="photo.Type === 'animated'" color="white" class="action-animated" :title="$gettext('Animated')">gif</i>
|
||||
|
@ -74,13 +79,13 @@
|
|||
class="input-view"
|
||||
:title="$gettext('View')"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onOpen($event, index, false)"
|
||||
@touchend.stop.prevent="onOpen($event, index)"
|
||||
@touchmove.stop.prevent
|
||||
@click.stop.prevent="onOpen($event, index, false)">
|
||||
@click.stop.prevent="onOpen($event, index)">
|
||||
<i color="white" class="action-fullscreen">zoom_in</i>
|
||||
</button>
|
||||
|
||||
<button v-if="hidePrivate && photo.Private" class="input-private">
|
||||
<button v-if="!isSharedView && hidePrivate && photo.Private" class="input-private">
|
||||
<i color="white" class="select-on">lock</i>
|
||||
</button>
|
||||
|
||||
|
@ -105,7 +110,7 @@
|
|||
<i color="white" class="select-off">radio_button_off</i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
<button v-if="!isSharedView"
|
||||
class="input-favorite"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="toggleLike($event, index)"
|
||||
|
@ -157,6 +162,10 @@ export default {
|
|||
default: "",
|
||||
},
|
||||
selectMode: Boolean,
|
||||
isSharedView: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -268,14 +277,14 @@ export default {
|
|||
this.$clipboard.toggle(photo);
|
||||
this.$forceUpdate();
|
||||
},
|
||||
onOpen(ev, index, showMerged) {
|
||||
onOpen(ev, index, showMerged, preferVideo) {
|
||||
const inputType = this.input.eval(ev, index);
|
||||
|
||||
if (inputType !== ClickShort) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.openPhoto(index, showMerged);
|
||||
this.openPhoto(index, showMerged, preferVideo);
|
||||
},
|
||||
onClick(ev, index) {
|
||||
const inputType = this.input.eval(ev, index);
|
||||
|
@ -292,7 +301,7 @@ export default {
|
|||
this.toggle(this.photos[index]);
|
||||
}
|
||||
} else {
|
||||
this.openPhoto(index, false);
|
||||
this.openPhoto(index);
|
||||
}
|
||||
},
|
||||
onContextMenu(ev, index) {
|
||||
|
|
|
@ -82,7 +82,7 @@ export default {
|
|||
uid: uid,
|
||||
results: [],
|
||||
scrollDisabled: true,
|
||||
scrollDistance: window.innerHeight * 2,
|
||||
scrollDistance: window.innerHeight * 6,
|
||||
batchSize: batchSize,
|
||||
offset: 0,
|
||||
page: 0,
|
||||
|
@ -177,7 +177,7 @@ export default {
|
|||
// Open Edit Dialog
|
||||
Event.publish("dialog.edit", {selection: selection, album: this.album, index: index});
|
||||
},
|
||||
openPhoto(index, showMerged) {
|
||||
openPhoto(index, showMerged = false, preferVideo = false) {
|
||||
if (this.loading || !this.listen || this.viewer.loading || !this.results[index]) {
|
||||
return false;
|
||||
}
|
||||
|
@ -189,7 +189,21 @@ export default {
|
|||
showMerged = false;
|
||||
}
|
||||
|
||||
if (showMerged && selected.Type === MediaLive || selected.Type === MediaVideo || selected.Type === MediaAnimated) {
|
||||
/**
|
||||
* If the file is an video or an animation (like gif), then we always play
|
||||
* it in the video-player.
|
||||
* If the file is a live-image (an image with an embedded video), then we only
|
||||
* play it in the video-player if specifically requested.
|
||||
* This is because:
|
||||
* 1. the lower-resolution video in these files is already
|
||||
* played when hovering the element (which does not happen for regular
|
||||
* video files)
|
||||
* 2. The video in live-images is an addon. The main focus is usually still
|
||||
* the high resolution image inside
|
||||
*
|
||||
* preferVideo is true, when the user explicitly clicks the live-image-icon.
|
||||
*/
|
||||
if (preferVideo && selected.Type === MediaLive || selected.Type === MediaVideo || selected.Type === MediaAnimated) {
|
||||
if (selected.isPlayable()) {
|
||||
this.$viewer.play({video: selected, album: this.album});
|
||||
} else {
|
||||
|
|
|
@ -224,7 +224,7 @@ export default {
|
|||
// Open Edit Dialog
|
||||
Event.publish("dialog.edit", {selection: selection, album: null, index: index});
|
||||
},
|
||||
openPhoto(index, showMerged) {
|
||||
openPhoto(index, showMerged, preferVideo = false) {
|
||||
if (this.loading || !this.listen || this.viewer.loading || !this.results[index]) {
|
||||
return false;
|
||||
}
|
||||
|
@ -236,7 +236,21 @@ export default {
|
|||
showMerged = false;
|
||||
}
|
||||
|
||||
if (showMerged && selected.Type === MediaLive || selected.Type === MediaVideo || selected.Type === MediaAnimated) {
|
||||
/**
|
||||
* If the file is an video or an animation (like gif), then we always play
|
||||
* it in the video-player.
|
||||
* If the file is a live-image (an image with an embedded video), then we only
|
||||
* play it in the video-player if specifically requested.
|
||||
* This is because:
|
||||
* 1. the lower-resolution video in these files is already
|
||||
* played when hovering the element (which does not happen for regular
|
||||
* video files)
|
||||
* 2. The video in live-images is an addon. The main focus is usually still
|
||||
* the high resolution image inside
|
||||
*
|
||||
* preferVideo is true, when the user explicitly clicks the live-image-icon.
|
||||
*/
|
||||
if (preferVideo && selected.Type === MediaLive || selected.Type === MediaVideo || selected.Type === MediaAnimated) {
|
||||
if (selected.isPlayable()) {
|
||||
this.$viewer.play({video: selected});
|
||||
} else {
|
||||
|
|
|
@ -29,9 +29,9 @@ import PScrollTop from "component/scroll-top.vue";
|
|||
import PLoadingBar from "component/loading-bar.vue";
|
||||
import PPhotoViewer from "component/photo-viewer.vue";
|
||||
import PVideoPlayer from "component/video/player.vue";
|
||||
import PPhotoCards from "photo/cards.vue";
|
||||
import PPhotoMosaic from "photo/mosaic.vue";
|
||||
import PPhotoList from "photo/list.vue";
|
||||
import PPhotoCards from "component/photo/cards.vue";
|
||||
import PPhotoMosaic from "component/photo/mosaic.vue";
|
||||
import PPhotoList from "component/photo/list.vue";
|
||||
import PPhotoClipboard from "photo/clipboard.vue";
|
||||
import PAlbumClipboard from "album/clipboard.vue";
|
||||
|
||||
|
|
|
@ -1,275 +0,0 @@
|
|||
<template>
|
||||
<v-container grid-list-xs fluid class="pa-2 p-photos p-photo-cards">
|
||||
<v-alert
|
||||
:value="photos.length === 0"
|
||||
color="secondary-dark" icon="image_not_supported" class="no-results ma-2 opacity-70" outline
|
||||
>
|
||||
<h3 v-if="filter.order === 'edited'" class="body-2 ma-0 pa-0">
|
||||
<translate>No recently edited pictures</translate>
|
||||
</h3>
|
||||
<h3 v-else class="body-2 ma-0 pa-0">
|
||||
<translate>No pictures found</translate>
|
||||
</h3>
|
||||
<p class="body-1 mt-2 mb-0 pa-0">
|
||||
<translate>Try again using other filters or keywords.</translate>
|
||||
</p>
|
||||
</v-alert>
|
||||
<v-layout row wrap class="search-results photo-results cards-view" :class="{'select-results': selectMode}">
|
||||
<v-flex
|
||||
v-for="(photo, index) in photos"
|
||||
:key="photo.ID"
|
||||
xs12 sm6 md4 lg3 xlg2 xxxl1 d-flex
|
||||
>
|
||||
<v-card tile
|
||||
:data-id="photo.ID"
|
||||
:data-uid="photo.UID"
|
||||
style="user-select: none"
|
||||
class="result accent lighten-3"
|
||||
:class="photo.classes()"
|
||||
@contextmenu.stop="onContextMenu($event, index)">
|
||||
<div class="card-background accent lighten-3"></div>
|
||||
<v-img :key="photo.Hash"
|
||||
:src="photo.thumbnailUrl('tile_500')"
|
||||
:alt="photo.Title"
|
||||
:title="photo.Title"
|
||||
:transition="false"
|
||||
aspect-ratio="1"
|
||||
class="accent lighten-2 clickable"
|
||||
@touchstart.passive="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onClick($event, index)"
|
||||
@mousedown.stop.prevent="input.mouseDown($event, index)"
|
||||
@click.stop.prevent="onClick($event, index)"
|
||||
@mouseover="playLive(photo)"
|
||||
@mouseleave="pauseLive(photo)"
|
||||
>
|
||||
<v-layout v-if="photo.Type === 'live' || photo.Type === 'animated'" class="live-player">
|
||||
<video :id="'live-player-' + photo.ID" :key="photo.ID" width="500" height="500" preload="none"
|
||||
loop muted playsinline>
|
||||
<source :src="photo.videoUrl()">
|
||||
</video>
|
||||
</v-layout>
|
||||
|
||||
<v-btn :ripple="false" :depressed="false" class="input-open"
|
||||
icon flat absolute
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onOpen($event, index, true)"
|
||||
@touchmove.stop.prevent
|
||||
@click.stop.prevent="onOpen($event, index, true)">
|
||||
<v-icon color="white" class="default-hidden action-raw" :title="$gettext('RAW')">photo_camera</v-icon>
|
||||
<v-icon color="white" class="default-hidden action-live" :title="$gettext('Live')">$vuetify.icons.live_photo</v-icon>
|
||||
<v-icon color="white" class="default-hidden action-animated" :title="$gettext('Animated')">gif</v-icon>
|
||||
<v-icon color="white" class="default-hidden action-play" :title="$gettext('Video')">play_arrow</v-icon>
|
||||
<v-icon color="white" class="default-hidden action-stack" :title="$gettext('Stack')">burst_mode</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn :ripple="false" :depressed="false" class="input-view"
|
||||
icon flat absolute :title="$gettext('View')"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onOpen($event, index, false)"
|
||||
@touchmove.stop.prevent
|
||||
@click.stop.prevent="onOpen($event, index, true)">
|
||||
<v-icon color="white" class="action-fullscreen">zoom_in</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn :ripple="false" :depressed="false" color="white" class="input-play"
|
||||
outline fab large absolute :title="$gettext('Play')"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onOpen($event, index, true)"
|
||||
@touchmove.stop.prevent
|
||||
@click.stop.prevent="onOpen($event, index, true)">
|
||||
<v-icon color="white" class="action-play">play_arrow</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn :ripple="false"
|
||||
icon flat absolute
|
||||
class="input-select"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onSelect($event, index)"
|
||||
@touchmove.stop.prevent
|
||||
@click.stop.prevent="onSelect($event, index)">
|
||||
<v-icon color="white" class="select-on">check_circle</v-icon>
|
||||
<v-icon color="white" class="select-off">radio_button_off</v-icon>
|
||||
</v-btn>
|
||||
</v-img>
|
||||
|
||||
<v-card-title primary-title class="pa-3 card-details" style="user-select: none;">
|
||||
<div>
|
||||
<h3 class="body-2 mb-2" :title="photo.Title">
|
||||
<div @click.stop.prevent="openPhoto(index, false)">
|
||||
{{ photo.Title | truncate(80) }}
|
||||
</div>
|
||||
</h3>
|
||||
<div v-if="photo.Description" class="caption mb-2" :title="$gettext('Description')">
|
||||
<div>
|
||||
{{ photo.Description }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption">
|
||||
<div>
|
||||
<v-icon size="14" :title="$gettext('Taken')">date_range</v-icon>
|
||||
{{ photo.getDateString(true) }}
|
||||
</div>
|
||||
<template v-if="!photo.Description">
|
||||
<div v-if="photo.Type === 'video'" :title="$gettext('Video')">
|
||||
<v-icon size="14">movie</v-icon>
|
||||
{{ photo.getVideoInfo() }}
|
||||
</div>
|
||||
<div v-else-if="photo.Type === 'animated'" :title="$gettext('Animated')+' GIF'">
|
||||
<v-icon size="14">gif_box</v-icon>
|
||||
{{ photo.getVideoInfo() }}
|
||||
</div>
|
||||
<div v-else :title="$gettext('Camera')">
|
||||
<v-icon size="14">photo_camera</v-icon>
|
||||
{{ photo.getPhotoInfo() }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="filter.order === 'name' && $config.feature('download')">
|
||||
<div :title="$gettext('Name')">
|
||||
<v-icon size="14">insert_drive_file</v-icon>
|
||||
{{ photo.baseName() }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="featPlaces && photo.Country !== 'zz'">
|
||||
<div :title="$gettext('Location')">
|
||||
<v-icon size="14">location_on</v-icon>
|
||||
{{ photo.locationInfo() }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import download from "common/download";
|
||||
import Notify from "common/notify";
|
||||
import {Input, InputInvalid, ClickShort, ClickLong} from "common/input";
|
||||
|
||||
export default {
|
||||
name: 'PPhotoCards',
|
||||
props: {
|
||||
photos: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
openPhoto: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
editPhoto: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
openLocation: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
album: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
},
|
||||
},
|
||||
context: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
selectMode: Boolean,
|
||||
},
|
||||
data() {
|
||||
const featPlaces = this.$config.settings().features.places;
|
||||
const input = new Input();
|
||||
const debug = this.$config.get('debug');
|
||||
|
||||
return {
|
||||
featPlaces,
|
||||
debug,
|
||||
input,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
livePlayer(photo) {
|
||||
return document.querySelector("#live-player-" + photo.ID);
|
||||
},
|
||||
playLive(photo) {
|
||||
const player = this.livePlayer(photo);
|
||||
try {
|
||||
if (player) player.play();
|
||||
} catch (e) {
|
||||
// Ignore.
|
||||
}
|
||||
},
|
||||
pauseLive(photo) {
|
||||
const player = this.livePlayer(photo);
|
||||
try {
|
||||
if (player) player.pause();
|
||||
} catch (e) {
|
||||
// Ignore.
|
||||
}
|
||||
},
|
||||
downloadFile(index) {
|
||||
Notify.success(this.$gettext("Downloading…"));
|
||||
|
||||
const photo = this.photos[index];
|
||||
download(`${this.$config.apiUri}/dl/${photo.Hash}?t=${this.$config.downloadToken()}`, photo.FileName);
|
||||
},
|
||||
onSelect(ev, index) {
|
||||
const inputType = this.input.eval(ev, index);
|
||||
|
||||
if (inputType !== ClickShort) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.shiftKey) {
|
||||
this.selectRange(index);
|
||||
} else {
|
||||
this.$clipboard.toggle(this.photos[index]);
|
||||
}
|
||||
},
|
||||
onOpen(ev, index, showMerged) {
|
||||
const inputType = this.input.eval(ev, index);
|
||||
|
||||
if (inputType !== ClickShort) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.openPhoto(index, showMerged);
|
||||
},
|
||||
onClick(ev, index) {
|
||||
const inputType = this.input.eval(ev, index);
|
||||
const longClick = inputType === ClickLong;
|
||||
|
||||
if (inputType === InputInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (longClick || this.selectMode) {
|
||||
if (longClick || ev.shiftKey) {
|
||||
this.selectRange(index);
|
||||
} else {
|
||||
this.$clipboard.toggle(this.photos[index]);
|
||||
}
|
||||
} else {
|
||||
this.openPhoto(index, false);
|
||||
}
|
||||
},
|
||||
onContextMenu(ev, index) {
|
||||
if (this.$isMobile) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.selectRange(index);
|
||||
}
|
||||
},
|
||||
selectRange(index) {
|
||||
this.$clipboard.addRange(index, this.photos);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -1,224 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="photos.length === 0" class="pa-2">
|
||||
<v-alert
|
||||
:value="true"
|
||||
color="secondary-dark" icon="image_not_supported" class="no-results ma-2 opacity-70" outline
|
||||
>
|
||||
<h3 v-if="filter.order === 'edited'" class="body-2 ma-0 pa-0">
|
||||
<translate>No recently edited pictures</translate>
|
||||
</h3>
|
||||
<h3 v-else class="body-2 ma-0 pa-0">
|
||||
<translate>No pictures found</translate>
|
||||
</h3>
|
||||
<p class="body-1 mt-2 mb-0 pa-0">
|
||||
<translate>Try again using other filters or keywords.</translate>
|
||||
</p>
|
||||
</v-alert>
|
||||
</div>
|
||||
<v-data-table v-else
|
||||
v-model="selected"
|
||||
:headers="listColumns"
|
||||
:items="photos"
|
||||
hide-actions
|
||||
class="search-results photo-results list-view"
|
||||
:class="{'select-results': selectMode}"
|
||||
disable-initial-sort
|
||||
item-key="ID"
|
||||
:no-data-text="notFoundMessage"
|
||||
>
|
||||
<template #items="props">
|
||||
<td style="user-select: none;" :data-uid="props.item.UID" class="result" :class="props.item.classes()">
|
||||
<v-img :key="props.item.Hash"
|
||||
:src="props.item.thumbnailUrl('tile_50')"
|
||||
:alt="props.item.Title"
|
||||
:transition="false"
|
||||
aspect-ratio="1"
|
||||
style="user-select: none"
|
||||
class="accent lighten-2 clickable"
|
||||
@touchstart="onMouseDown($event, props.index)"
|
||||
@touchend.stop.prevent="onClick($event, props.index)"
|
||||
@mousedown="onMouseDown($event, props.index)"
|
||||
@contextmenu.stop="onContextMenu($event, props.index)"
|
||||
@click.stop.prevent="onClick($event, props.index)"
|
||||
>
|
||||
<v-btn v-if="selectMode" :ripple="false"
|
||||
flat icon large absolute
|
||||
class="input-select">
|
||||
<v-icon color="white" class="select-on">check_circle</v-icon>
|
||||
<v-icon color="white" class="select-off">radio_button_off</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-else-if="props.item.Type === 'video' || props.item.Type === 'live' || props.item.Type === 'animated'"
|
||||
:ripple="false"
|
||||
flat icon large absolute class="input-open"
|
||||
@click.stop.prevent="openPhoto(props.index, true)">
|
||||
<v-icon color="white" class="default-hidden action-live" :title="$gettext('Live')">$vuetify.icons.live_photo</v-icon>
|
||||
<v-icon color="white" class="default-hidden action-animated" :title="$gettext('Animated')">gif</v-icon>
|
||||
<v-icon color="white" class="default-hidden action-play" :title="$gettext('Video')">play_arrow</v-icon>
|
||||
</v-btn>
|
||||
</v-img>
|
||||
</td>
|
||||
|
||||
<td class="p-photo-desc clickable" :data-uid="props.item.UID" style="user-select: none;"
|
||||
@click.stop.prevent="openPhoto(props.index, false)">
|
||||
{{ props.item.Title }}
|
||||
</td>
|
||||
<td class="p-photo-desc hidden-xs-only" :title="props.item.getDateString()">
|
||||
<button style="user-select: none;" @click.stop.prevent="editPhoto(props.index)">
|
||||
{{ props.item.shortDateString() }}
|
||||
</button>
|
||||
</td>
|
||||
<td class="p-photo-desc hidden-sm-and-down" style="user-select: none;">
|
||||
<button @click.stop.prevent="editPhoto(props.index)">
|
||||
{{ props.item.CameraMake }} {{ props.item.CameraModel }}
|
||||
</button>
|
||||
</td>
|
||||
<td class="p-photo-desc hidden-xs-only">
|
||||
<button v-if="filter.order === 'name'"
|
||||
:title="$gettext('Name')" @click.exact="downloadFile(props.index)">
|
||||
{{ props.item.FileName }}
|
||||
</button>
|
||||
<button v-else-if="props.item.Country !== 'zz' && showLocation"
|
||||
style="user-select: none;"
|
||||
@click.stop.prevent="openLocation(props.index)">
|
||||
{{ props.item.locationInfo() }}
|
||||
</button>
|
||||
<span v-else>
|
||||
{{ props.item.locationInfo() }}
|
||||
</span>
|
||||
</td>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import download from "common/download";
|
||||
import Notify from "common/notify";
|
||||
|
||||
export default {
|
||||
name: 'PPhotoList',
|
||||
props: {
|
||||
photos: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
openPhoto: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
editPhoto: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
openLocation: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
album: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
filter: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
context: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
selectMode: Boolean,
|
||||
},
|
||||
data() {
|
||||
let m = this.$gettext("Couldn't find anything.");
|
||||
|
||||
m += " " + this.$gettext("Try again using other filters or keywords.");
|
||||
|
||||
let showName = this.filter.order === 'name';
|
||||
|
||||
const align = !this.$rtl ? 'left' : 'right';
|
||||
return {
|
||||
config: this.$config.values,
|
||||
notFoundMessage: m,
|
||||
'selected': [],
|
||||
'listColumns': [
|
||||
{text: '', value: '', align: 'center', class: 'p-col-select', sortable: false},
|
||||
{text: this.$gettext('Title'), align, value: 'Title', sortable: false},
|
||||
{text: this.$gettext('Taken'), align, class: 'hidden-xs-only', value: 'TakenAt', sortable: false},
|
||||
{text: this.$gettext('Camera'), align, class: 'hidden-sm-and-down', value: 'CameraModel', sortable: false},
|
||||
{
|
||||
text: showName ? this.$gettext('Name') : this.$gettext('Location'),
|
||||
align,
|
||||
class: 'hidden-xs-only',
|
||||
value: showName ? 'FileName' : 'PlaceLabel',
|
||||
sortable: false
|
||||
},
|
||||
],
|
||||
showName: showName,
|
||||
showLocation: this.$config.values.settings.features.places,
|
||||
hidePrivate: this.$config.values.settings.features.private,
|
||||
mouseDown: {
|
||||
index: -1,
|
||||
scrollY: window.scrollY,
|
||||
timeStamp: -1,
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
downloadFile(index) {
|
||||
Notify.success(this.$gettext("Downloading…"));
|
||||
|
||||
const photo = this.photos[index];
|
||||
download(`${this.$config.apiUri}/dl/${photo.Hash}?t=${this.$config.downloadToken()}`, photo.FileName);
|
||||
},
|
||||
onSelect(ev, index) {
|
||||
if (ev.shiftKey) {
|
||||
this.selectRange(index);
|
||||
} else {
|
||||
this.toggle(this.photos[index]);
|
||||
}
|
||||
},
|
||||
onMouseDown(ev, index) {
|
||||
this.mouseDown.index = index;
|
||||
this.mouseDown.scrollY = window.scrollY;
|
||||
this.mouseDown.timeStamp = ev.timeStamp;
|
||||
},
|
||||
onClick(ev, index) {
|
||||
const longClick = (this.mouseDown.index === index && (ev.timeStamp - this.mouseDown.timeStamp) > 400);
|
||||
const scrolled = (this.mouseDown.scrollY - window.scrollY) !== 0;
|
||||
|
||||
if (scrolled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (longClick || this.selectMode) {
|
||||
if (longClick || ev.shiftKey) {
|
||||
this.selectRange(index);
|
||||
} else {
|
||||
this.toggle(this.photos[index]);
|
||||
}
|
||||
} else if (this.photos[index]) {
|
||||
let photo = this.photos[index];
|
||||
|
||||
if ((photo.Type === 'video' || photo.Type === 'animated') && photo.isPlayable()) {
|
||||
this.openPhoto(index, true);
|
||||
} else {
|
||||
this.openPhoto(index, false);
|
||||
}
|
||||
}
|
||||
},
|
||||
onContextMenu(ev, index) {
|
||||
if (this.$isMobile) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.selectRange(index);
|
||||
}
|
||||
},
|
||||
toggle(photo) {
|
||||
this.$clipboard.toggle(photo);
|
||||
},
|
||||
selectRange(index) {
|
||||
this.$clipboard.addRange(index, this.photos);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -1,200 +0,0 @@
|
|||
<template>
|
||||
<v-container grid-list-xs fluid class="pa-2 p-photos p-photo-mosaic">
|
||||
<v-alert
|
||||
:value="photos.length === 0"
|
||||
color="secondary-dark" icon="image_not_supported" class="no-results ma-2 opacity-70" outline
|
||||
>
|
||||
<h3 v-if="filter.order === 'edited'" class="body-2 ma-0 pa-0">
|
||||
<translate>No recently edited pictures</translate>
|
||||
</h3>
|
||||
<h3 v-else class="body-2 ma-0 pa-0">
|
||||
<translate>No pictures found</translate>
|
||||
</h3>
|
||||
<p class="body-1 mt-2 mb-0 pa-0">
|
||||
<translate>Try again using other filters or keywords.</translate>
|
||||
</p>
|
||||
</v-alert>
|
||||
<v-layout row wrap class="search-results photo-results mosaic-view" :class="{'select-results': selectMode}">
|
||||
<v-flex
|
||||
v-for="(photo, index) in photos"
|
||||
:key="photo.ID"
|
||||
xs4 sm3 md2 lg1 d-flex
|
||||
>
|
||||
<v-card tile
|
||||
:data-id="photo.ID"
|
||||
:data-uid="photo.UID"
|
||||
style="user-select: none"
|
||||
class="result"
|
||||
:class="photo.classes()"
|
||||
@contextmenu.stop="onContextMenu($event, index)">
|
||||
<v-img :key="photo.Hash"
|
||||
:src="photo.thumbnailUrl('tile_224')"
|
||||
:alt="photo.Title"
|
||||
:title="photo.Title"
|
||||
:transition="false"
|
||||
aspect-ratio="1"
|
||||
class="accent lighten-2 clickable"
|
||||
@touchstart.passive="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onClick($event, index)"
|
||||
@mousedown.stop.prevent="input.mouseDown($event, index)"
|
||||
@click.stop.prevent="onClick($event, index)"
|
||||
@mouseover="playLive(photo)"
|
||||
@mouseleave="pauseLive(photo)"
|
||||
>
|
||||
<v-layout v-if="photo.Type === 'live' || photo.Type === 'animated'" class="live-player">
|
||||
<video :id="'live-player-' + photo.ID" :key="photo.ID" width="224" height="224" preload="none"
|
||||
loop muted playsinline>
|
||||
<source :src="photo.videoUrl()">
|
||||
</video>
|
||||
</v-layout>
|
||||
|
||||
<v-btn :ripple="false" :depressed="false" class="input-open"
|
||||
icon flat small absolute
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onOpen($event, index, true)"
|
||||
@touchmove.stop.prevent
|
||||
@click.stop.prevent="onOpen($event, index, true)">
|
||||
<v-icon color="white" class="default-hidden action-raw" :title="$gettext('RAW')">photo_camera</v-icon>
|
||||
<v-icon color="white" class="default-hidden action-live" :title="$gettext('Live')">$vuetify.icons.live_photo</v-icon>
|
||||
<v-icon color="white" class="default-hidden action-animated" :title="$gettext('Animated')">gif</v-icon>
|
||||
<v-icon color="white" class="default-hidden action-play" :title="$gettext('Video')">play_arrow</v-icon>
|
||||
<v-icon color="white" class="default-hidden action-stack" :title="$gettext('Stack')">burst_mode</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn :ripple="false" :depressed="false" class="input-view"
|
||||
icon flat small absolute :title="$gettext('View')"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onOpen($event, index, false)"
|
||||
@touchmove.stop.prevent
|
||||
@click.stop.prevent="onOpen($event, index, false)">
|
||||
<v-icon color="white" class="action-fullscreen">zoom_in</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn :ripple="false" :depressed="false" color="white" class="input-play"
|
||||
icon flat small absolute :title="$gettext('Play')"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onOpen($event, index, true)"
|
||||
@touchmove.stop.prevent
|
||||
@click.stop.prevent="onOpen($event, index, true)">
|
||||
<v-icon color="white" class="action-play">play_arrow</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn :ripple="false"
|
||||
icon flat small absolute
|
||||
class="input-select"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onSelect($event, index)"
|
||||
@touchmove.stop.prevent
|
||||
@click.stop.prevent="onSelect($event, index)">
|
||||
<v-icon color="white" class="select-on">check_circle</v-icon>
|
||||
<v-icon color="white" class="select-off">radio_button_off</v-icon>
|
||||
</v-btn>
|
||||
</v-img>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import {Input, InputInvalid, ClickShort, ClickLong} from "common/input";
|
||||
|
||||
export default {
|
||||
name: 'PPhotoMosaic',
|
||||
props: {
|
||||
photos: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
openPhoto: Function,
|
||||
editPhoto: Function,
|
||||
album: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
filter: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
context: String,
|
||||
selectMode: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hidePrivate: this.$config.settings().features.private,
|
||||
input: new Input(),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
livePlayer(photo) {
|
||||
return document.querySelector("#live-player-" + photo.ID);
|
||||
},
|
||||
playLive(photo) {
|
||||
const player = this.livePlayer(photo);
|
||||
try { if (player) player.play(); }
|
||||
catch (e) {
|
||||
// Ignore.
|
||||
}
|
||||
},
|
||||
pauseLive(photo) {
|
||||
const player = this.livePlayer(photo);
|
||||
try { if (player) player.pause(); }
|
||||
catch (e) {
|
||||
// Ignore.
|
||||
}
|
||||
},
|
||||
onSelect(ev, index) {
|
||||
const inputType = this.input.eval(ev, index);
|
||||
|
||||
if (inputType !== ClickShort) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.shiftKey) {
|
||||
this.selectRange(index);
|
||||
} else {
|
||||
this.toggle(this.photos[index]);
|
||||
}
|
||||
},
|
||||
toggle(photo) {
|
||||
this.$clipboard.toggle(photo);
|
||||
},
|
||||
onOpen(ev, index, showMerged) {
|
||||
const inputType = this.input.eval(ev, index);
|
||||
|
||||
if (inputType !== ClickShort) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.openPhoto(index, showMerged);
|
||||
},
|
||||
onClick(ev, index) {
|
||||
const inputType = this.input.eval(ev, index);
|
||||
const longClick = inputType === ClickLong;
|
||||
|
||||
if (inputType === InputInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (longClick || this.selectMode) {
|
||||
if (longClick || ev.shiftKey) {
|
||||
this.selectRange(index);
|
||||
} else {
|
||||
this.toggle(this.photos[index]);
|
||||
}
|
||||
} else {
|
||||
this.openPhoto(index, false);
|
||||
}
|
||||
},
|
||||
onContextMenu(ev, index) {
|
||||
if (this.$isMobile) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.selectRange(index);
|
||||
}
|
||||
},
|
||||
selectRange(index) {
|
||||
this.$clipboard.addRange(index, this.photos);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -66,7 +66,8 @@
|
|||
:filter="filter"
|
||||
:album="model"
|
||||
:edit-photo="editPhoto"
|
||||
:open-photo="openPhoto"></p-photo-mosaic>
|
||||
:open-photo="openPhoto"
|
||||
:is-shared-view="true"></p-photo-mosaic>
|
||||
<p-photo-list v-else-if="settings.view === 'list'"
|
||||
:photos="results"
|
||||
:select-mode="selectMode"
|
||||
|
@ -74,7 +75,8 @@
|
|||
:album="model"
|
||||
:open-photo="openPhoto"
|
||||
:edit-photo="editPhoto"
|
||||
:open-location="openLocation"></p-photo-list>
|
||||
:open-location="openLocation"
|
||||
:is-shared-view="true"></p-photo-list>
|
||||
<p-photo-cards v-else
|
||||
:photos="results"
|
||||
:select-mode="selectMode"
|
||||
|
@ -82,7 +84,8 @@
|
|||
:album="model"
|
||||
:open-photo="openPhoto"
|
||||
:edit-photo="editPhoto"
|
||||
:open-location="openLocation"></p-photo-cards>
|
||||
:open-location="openLocation"
|
||||
:is-shared-view="true"></p-photo-cards>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -126,7 +129,7 @@ export default {
|
|||
uid: uid,
|
||||
results: [],
|
||||
scrollDisabled: true,
|
||||
scrollDistance: window.innerHeight*2,
|
||||
scrollDistance: window.innerHeight * 6,
|
||||
batchSize: batchSize,
|
||||
offset: 0,
|
||||
page: 0,
|
||||
|
@ -234,7 +237,7 @@ export default {
|
|||
// Open Edit Dialog
|
||||
Event.publish("dialog.edit", {selection: selection, album: this.album, index: index});
|
||||
},
|
||||
openPhoto(index, showMerged) {
|
||||
openPhoto(index, showMerged = false, preferVideo = false) {
|
||||
if (this.loading || !this.listen || this.viewer.loading || !this.results[index]) {
|
||||
return false;
|
||||
}
|
||||
|
@ -246,7 +249,21 @@ export default {
|
|||
showMerged = false;
|
||||
}
|
||||
|
||||
if (showMerged && selected.Type === MediaLive || selected.Type === MediaVideo|| selected.Type === MediaAnimated) {
|
||||
/**
|
||||
* If the file is an video or an animation (like gif), then we always play
|
||||
* it in the video-player.
|
||||
* If the file is a live-image (an image with an embedded video), then we only
|
||||
* play it in the video-player if specifically requested.
|
||||
* This is because:
|
||||
* 1. the lower-resolution video in these files is already
|
||||
* played when hovering the element (which does not happen for regular
|
||||
* video files)
|
||||
* 2. The video in live-images is an addon. The main focus is usually still
|
||||
* the high resolution image inside
|
||||
*
|
||||
* preferVideo is true, when the user explicitly clicks the live-image-icon.
|
||||
*/
|
||||
if (preferVideo && selected.Type === MediaLive || selected.Type === MediaVideo|| selected.Type === MediaAnimated) {
|
||||
if (selected.isPlayable()) {
|
||||
this.$viewer.play({video: selected, album: this.album});
|
||||
} else {
|
||||
|
|
Loading…
Reference in a new issue