UX: Ssearch view render performance improvements (#2433)
* virtualize mosaic-view * start virtualizing parts of the list view * drastically improve mosaic component updating performance by not rendering cards if not necessary * speed up list view virtualization by preventing the rendering of vue-components * start virtualizing cards view * continue virtualizing card view * finish virtualizing cards * start moving common virtualization logic into shared file * try speeding up access to calculated photo details * remove console.log * improve accuracy of cards-placeholder elements * remove console.log * start fixing memoized values not updating on change * fixing memoized values not updating on change * remove console.logs * fix getting location info after memoizing the function * remove obsolete comment * start rendering of only visible icons in mosaic-view * continue rendering of only visible icons in mosaic-view * implement rendering of only visible icons in cards-view * implement rendering of only visible icons in list-view * memoize some photo-model functions that are called when rendering mosaic- or cards-view * fix cards sometimes getting smaller when scrolling * improve fix for cards sometimes getting smaller when scrolling * prevent rendering of no-photos-alert if photos are present * fix selection issues after implementing conditional button-rendering * speedup mosaic rendering by replacing vue components with html components * speedup cards rendering by replacing vue components with html components * speedup list rendering by replacing vue components with html components * fix removed elements leaving behind placeholders in view * speedup photo view rendering by replacing vue buttons and icons with regular html components * fix positioning of card-title in placeholder elements * fix missing icons after multiselect or select via touch * prevent flickering of favourite button on click * prevent flickering of favourite button on click * use div instead of v-flex * replace inline styles with css classes * re-add actually necessary css-classes * add size-containment to mosaic-elements to reduce re-layouting costs when virtualization replaces children * fix typo in comment * use plain html instead of components and css-classes instead of inline styles in cards.vue * improve list scrolling by not using v-data-table for a static table * fix icon-color on card-details in light-theme * fix card-details showing wrong icons with wrong height on pixel 3 * fix animated gif-previews getting out of image-bounds when hovering in firefox * fix closing brackets in mosaic view not matching opening brackets * fix live-photo icon in mosaic- cards and list components * improve render performance by replacing v-hover vue-component with css-hidden button * prevent unnecessary rerenders * prevent unnecessary rerenders * undo "prevent unnecessary rerender" because the real-world-effect is negligable * load next batch earlier to reduce change of scrolling to the end before the load finished * add explanation on why the selection-button isnt removed via v-if * remove console.log * speed up rendering by reducing amount of observed items * fix favourite-buttons in non-search views * prevent unnecessary observeItems-calls by only observing items when photos changed Co-authored-by: Heiko Mathes <heiko@pop-os.localdomain>
This commit is contained in:
parent
d776e9cf83
commit
580de58346
5 changed files with 447 additions and 330 deletions
|
@ -21,34 +21,29 @@
|
||||||
</v-alert>
|
</v-alert>
|
||||||
</template>
|
</template>
|
||||||
<v-layout row wrap class="search-results photo-results cards-view" :class="{'select-results': selectMode}">
|
<v-layout row wrap class="search-results photo-results cards-view" :class="{'select-results': selectMode}">
|
||||||
<v-flex
|
<div
|
||||||
v-for="(photo, index) in photos"
|
v-for="(photo, index) in photos"
|
||||||
ref="items"
|
ref="items"
|
||||||
:key="photo.ID"
|
:key="photo.ID"
|
||||||
:data-index="index"
|
:data-index="index"
|
||||||
style="width: min-content"
|
class="flex xs12 sm6 md4 lg3 xlg2 xxxl1 d-flex"
|
||||||
xs12 sm6 md4 lg3 xlg2 xxxl1 d-flex
|
|
||||||
>
|
>
|
||||||
<div v-if="index < firstVisibleElementIndex || index > lastVisibileElementIndex"
|
<div v-if="index < firstVisibleElementIndex || index > lastVisibileElementIndex" class="accent lighten-3 result placeholder">
|
||||||
style="user-select: none"
|
<div class="accent lighten-2 image"/>
|
||||||
class="accent lighten-3 result"
|
|
||||||
:class="photo.classes()"
|
|
||||||
>
|
|
||||||
<div class="accent lighten-2" style="aspect-ratio: 1" />
|
|
||||||
<div v-if="photo.Quality < 3 && context === 'review'" style="width: 100%; height: 34px"/>
|
<div v-if="photo.Quality < 3 && context === 'review'" style="width: 100%; height: 34px"/>
|
||||||
<div class="v-card__title pa-3 card-details v-card__title--primary">
|
<div class="pa-3 card-details">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="body-2 mb-2" :title="photo.Title">
|
<h3 class="body-2 mb-2" :title="photo.Title">
|
||||||
{{ photo.Title | truncate(80) }}
|
{{ photo.Title | truncate(80) }}
|
||||||
</h3>
|
</h3>
|
||||||
<div v-if="photo.Description" class="caption mb-2" style="hyphens: auto; word-break: break-word">
|
<div v-if="photo.Description" class="caption mb-2">
|
||||||
{{ photo.Description }}
|
{{ photo.Description }}
|
||||||
</div>
|
</div>
|
||||||
<div class="caption" style="hyphens: auto; word-break: break-word">
|
<div class="caption">
|
||||||
<i style="display: inline-block; width: 14px" />
|
<i/>
|
||||||
{{ photo.getDateString(true) }}
|
{{ photo.getDateString(true) }}
|
||||||
<br>
|
<br>
|
||||||
<i style="display: inline-block; width: 14px" />
|
<i/>
|
||||||
<template v-if="photo.Type === 'video' || photo.Type === 'animated'">
|
<template v-if="photo.Type === 'video' || photo.Type === 'animated'">
|
||||||
{{ photo.getVideoInfo() }}
|
{{ photo.getVideoInfo() }}
|
||||||
</template>
|
</template>
|
||||||
|
@ -57,130 +52,125 @@
|
||||||
</template>
|
</template>
|
||||||
<template v-if="filter.order === 'name' && $config.feature('download')">
|
<template v-if="filter.order === 'name' && $config.feature('download')">
|
||||||
<br>
|
<br>
|
||||||
<i style="display: inline-block; width: 14px" />
|
<i/>
|
||||||
{{ photo.baseName() }}
|
{{ photo.baseName() }}
|
||||||
</template>
|
</template>
|
||||||
<template v-if="featPlaces && photo.Country !== 'zz'">
|
<template v-if="featPlaces && photo.Country !== 'zz'">
|
||||||
<br>
|
<br>
|
||||||
<i style="display: inline-block; width: 14px" />
|
<i/>
|
||||||
{{ photo.locationInfo() }}
|
{{ photo.locationInfo() }}
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<v-card v-if="index >= firstVisibleElementIndex && index <= lastVisibileElementIndex"
|
<div v-else
|
||||||
tile
|
tile
|
||||||
:data-id="photo.ID"
|
:data-id="photo.ID"
|
||||||
:data-uid="photo.UID"
|
:data-uid="photo.UID"
|
||||||
style="user-select: none"
|
class="result accent lighten-3"
|
||||||
class="result accent lighten-3"
|
:class="photo.classes()"
|
||||||
:class="photo.classes()"
|
@contextmenu.stop="onContextMenu($event, index)">
|
||||||
@contextmenu.stop="onContextMenu($event, index)">
|
|
||||||
<div class="card-background accent lighten-3"></div>
|
<div class="card-background accent lighten-3"></div>
|
||||||
<v-img :key="photo.Hash"
|
<div :key="photo.Hash"
|
||||||
:src="photo.thumbnailUrl('tile_500')"
|
:alt="photo.Title"
|
||||||
:alt="photo.Title"
|
:title="photo.Title"
|
||||||
:title="photo.Title"
|
class="accent lighten-2 clickable image"
|
||||||
:transition="false"
|
:style="`background-image: url(${photo.thumbnailUrl('tile_500')})`"
|
||||||
aspect-ratio="1"
|
@touchstart.passive="input.touchStart($event, index)"
|
||||||
class="accent lighten-2 clickable"
|
@touchend.stop.prevent="onClick($event, index)"
|
||||||
@touchstart.passive="input.touchStart($event, index)"
|
@mousedown.stop.prevent="input.mouseDown($event, index)"
|
||||||
@touchend.stop.prevent="onClick($event, index)"
|
@click.stop.prevent="onClick($event, index)"
|
||||||
@mousedown.stop.prevent="input.mouseDown($event, index)"
|
@mouseover="playLive(photo)"
|
||||||
@click.stop.prevent="onClick($event, index)"
|
@mouseleave="pauseLive(photo)"
|
||||||
@mouseover="playLive(photo)"
|
|
||||||
@mouseleave="pauseLive(photo)"
|
|
||||||
>
|
>
|
||||||
<v-layout v-if="photo.Type === 'live' || photo.Type === 'animated'" class="live-player">
|
<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"
|
<video :id="'live-player-' + photo.ID" :key="photo.ID" width="500" height="500" preload="none"
|
||||||
loop muted playsinline>
|
loop muted playsinline>
|
||||||
<source :src="photo.videoUrl()">
|
<source :src="photo.videoUrl()">
|
||||||
</video>
|
</video>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
|
|
||||||
<v-btn :ripple="false" :depressed="false" class="input-open"
|
<button v-if="photo.Type !== 'image' || photo.Files.length > 1"
|
||||||
icon flat absolute
|
class="input-open"
|
||||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||||
@touchend.stop.prevent="onOpen($event, index, true)"
|
@touchend.stop.prevent="onOpen($event, index, true)"
|
||||||
@touchmove.stop.prevent
|
@touchmove.stop.prevent
|
||||||
@click.stop.prevent="onOpen($event, index, true)">
|
@click.stop.prevent="onOpen($event, index, true)">
|
||||||
<v-icon color="white" class="default-hidden action-raw" :title="$gettext('RAW')">photo_camera</v-icon>
|
<i v-if="photo.Type === 'raw'" class="action-raw" :title="$gettext('RAW')">photo_camera</i>
|
||||||
<v-icon color="white" class="default-hidden action-live" :title="$gettext('Live')">$vuetify.icons.live_photo</v-icon>
|
<i v-if="photo.Type === 'live'" class="action-live" :title="$gettext('Live')"><icon-live-photo/></i>
|
||||||
<v-icon color="white" class="default-hidden action-animated" :title="$gettext('Animated')">gif</v-icon>
|
<i v-if="photo.Type === 'animated'" class="action-animated" :title="$gettext('Animated')">gif</i>
|
||||||
<v-icon color="white" class="default-hidden action-play" :title="$gettext('Video')">play_arrow</v-icon>
|
<i v-if="photo.Type === 'video'" class="action-play" :title="$gettext('Video')">play_arrow</i>
|
||||||
<v-icon color="white" class="default-hidden action-stack" :title="$gettext('Stack')">burst_mode</v-icon>
|
<i v-if="photo.Type === 'image'" class="action-stack" :title="$gettext('Stack')">burst_mode</i>
|
||||||
</v-btn>
|
</button>
|
||||||
|
|
||||||
<v-btn :ripple="false" :depressed="false" class="input-view"
|
<button v-if="photo.Type === 'image' && selectMode"
|
||||||
icon flat absolute :title="$gettext('View')"
|
class="input-view"
|
||||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
:title="$gettext('View')"
|
||||||
@touchend.stop.prevent="onOpen($event, index, false)"
|
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||||
@touchmove.stop.prevent
|
@touchend.stop.prevent="onOpen($event, index, false)"
|
||||||
@click.stop.prevent="onOpen($event, index, false)">
|
@touchmove.stop.prevent
|
||||||
<v-icon color="white" class="action-fullscreen">zoom_in</v-icon>
|
@click.stop.prevent="onOpen($event, index, false)">
|
||||||
</v-btn>
|
<i class="action-fullscreen">zoom_in</i>
|
||||||
|
</button>
|
||||||
|
|
||||||
<v-btn :ripple="false" :depressed="false" color="white" class="input-play"
|
<button v-if="featPrivate && photo.Private" class="input-private">
|
||||||
outline fab large absolute :title="$gettext('Play')"
|
<i class="select-on">lock</i>
|
||||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
</button>
|
||||||
@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 v-if="featPrivate" :ripple="false"
|
<!--
|
||||||
icon flat absolute
|
We'd usually use v-if here to only render the button if needed.
|
||||||
class="input-private">
|
Because the button is supposed to be visible when the result is
|
||||||
<v-icon color="white" class="select-on">lock</v-icon>
|
being hovered over, implementing the v-if would require the use of
|
||||||
</v-btn>
|
a <v-hover> element around the result.
|
||||||
|
|
||||||
<v-btn :ripple="false"
|
Because rendering the plain HTML-Button is faster than rendering
|
||||||
icon flat absolute
|
the v-hover component we instead hide the button by default and
|
||||||
class="input-select"
|
use css to show it when it is being hovered.
|
||||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
-->
|
||||||
@touchend.stop.prevent="onSelect($event, index)"
|
<button
|
||||||
@touchmove.stop.prevent
|
class="input-select"
|
||||||
@click.stop.prevent="onSelect($event, index)">
|
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||||
<v-icon color="white" class="select-on">check_circle</v-icon>
|
@touchend.stop.prevent="onSelect($event, index)"
|
||||||
<v-icon color="white" class="select-off">radio_button_off</v-icon>
|
@touchmove.stop.prevent
|
||||||
</v-btn>
|
@click.stop.prevent="onSelect($event, index)">
|
||||||
|
<i class="select-on">check_circle</i>
|
||||||
|
<i class="select-off">radio_button_off</i>
|
||||||
|
</button>
|
||||||
|
|
||||||
<v-btn :ripple="false"
|
<button
|
||||||
icon flat absolute
|
class="input-favorite"
|
||||||
class="input-favorite"
|
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
@touchend.stop.prevent="toggleLike($event, index)"
|
||||||
@touchend.stop.prevent="toggleLike($event, index)"
|
@touchmove.stop.prevent
|
||||||
@touchmove.stop.prevent
|
@click.stop.prevent="toggleLike($event, index)">
|
||||||
@click.stop.prevent="toggleLike($event, index)">
|
<i v-if="photo.Favorite">favorite</i>
|
||||||
<v-icon color="white" class="select-on">favorite</v-icon>
|
<i v-else>favorite_border</i>
|
||||||
<v-icon color="white" class="select-off">favorite_border</v-icon>
|
</button>
|
||||||
</v-btn>
|
</div>
|
||||||
</v-img>
|
|
||||||
|
|
||||||
<v-card-actions v-if="photo.Quality < 3 && context === 'review'" class="card-details pa-0">
|
<v-card-actions v-if="photo.Quality < 3 && context === 'review'" class="card-details pa-0">
|
||||||
<v-layout row wrap align-center>
|
<v-layout row wrap align-center>
|
||||||
<v-flex xs6 class="text-xs-center pa-1">
|
<v-flex xs6 class="text-xs-center pa-1">
|
||||||
<v-btn color="accent lighten-2"
|
<v-btn color="accent lighten-2"
|
||||||
small depressed dark block :round="false"
|
small depressed dark block :round="false"
|
||||||
class="action-archive text-xs-center"
|
class="action-archive text-xs-center"
|
||||||
:title="$gettext('Archive')" @click.stop="photo.archive()">
|
:title="$gettext('Archive')" @click.stop="photo.archive()">
|
||||||
<v-icon dark>clear</v-icon>
|
<v-icon dark>clear</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-flex>
|
</v-flex>
|
||||||
<v-flex xs6 class="text-xs-center pa-1">
|
<v-flex xs6 class="text-xs-center pa-1">
|
||||||
<v-btn color="accent lighten-2"
|
<v-btn color="accent lighten-2"
|
||||||
small depressed dark block :round="false"
|
small depressed dark block :round="false"
|
||||||
class="action-approve text-xs-center"
|
class="action-approve text-xs-center"
|
||||||
:title="$gettext('Approve')" @click.stop="photo.approve()">
|
:title="$gettext('Approve')" @click.stop="photo.approve()">
|
||||||
<v-icon dark>check</v-icon>
|
<v-icon dark>check</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-flex>
|
</v-flex>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
|
|
||||||
<v-card-title primary-title class="pa-3 card-details" style="user-select: none;">
|
<div class="pa-3 card-details">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="body-2 mb-2" :title="photo.Title">
|
<h3 class="body-2 mb-2" :title="photo.Title">
|
||||||
<button class="action-title-edit" :data-uid="photo.UID"
|
<button class="action-title-edit" :data-uid="photo.UID"
|
||||||
|
@ -196,30 +186,30 @@
|
||||||
<div class="caption">
|
<div class="caption">
|
||||||
<button class="action-date-edit" :data-uid="photo.UID"
|
<button class="action-date-edit" :data-uid="photo.UID"
|
||||||
@click.exact="editPhoto(index)">
|
@click.exact="editPhoto(index)">
|
||||||
<v-icon size="14" :title="$gettext('Taken')">date_range</v-icon>
|
<i :title="$gettext('Taken')">date_range</i>
|
||||||
{{ photo.getDateString(true) }}
|
{{ photo.getDateString(true) }}
|
||||||
</button>
|
</button>
|
||||||
<br>
|
<br>
|
||||||
<button v-if="photo.Type === 'video'" :title="$gettext('Video')"
|
<button v-if="photo.Type === 'video'" :title="$gettext('Video')"
|
||||||
@click.exact="openPhoto(index, true)">
|
@click.exact="openPhoto(index, true)">
|
||||||
<v-icon size="14">movie</v-icon>
|
<i>movie</i>
|
||||||
{{ photo.getVideoInfo() }}
|
{{ photo.getVideoInfo() }}
|
||||||
</button>
|
</button>
|
||||||
<button v-else-if="photo.Type === 'animated'" :title="$gettext('Animated')+' GIF'"
|
<button v-else-if="photo.Type === 'animated'" :title="$gettext('Animated')+' GIF'"
|
||||||
@click.exact="openPhoto(index, true)">
|
@click.exact="openPhoto(index, true)">
|
||||||
<v-icon size="14">gif_box</v-icon>
|
<i>gif_box</i>
|
||||||
{{ photo.getVideoInfo() }}
|
{{ photo.getVideoInfo() }}
|
||||||
</button>
|
</button>
|
||||||
<button v-else :title="$gettext('Camera')" class="action-camera-edit"
|
<button v-else :title="$gettext('Camera')" class="action-camera-edit"
|
||||||
:data-uid="photo.UID" @click.exact="editPhoto(index)">
|
:data-uid="photo.UID" @click.exact="editPhoto(index)">
|
||||||
<v-icon size="14">photo_camera</v-icon>
|
<i>photo_camera</i>
|
||||||
{{ photo.getPhotoInfo() }}
|
{{ photo.getPhotoInfo() }}
|
||||||
</button>
|
</button>
|
||||||
<template v-if="filter.order === 'name' && $config.feature('download')">
|
<template v-if="filter.order === 'name' && $config.feature('download')">
|
||||||
<br>
|
<br>
|
||||||
<button :title="$gettext('Name')"
|
<button :title="$gettext('Name')"
|
||||||
@click.exact="downloadFile(index)">
|
@click.exact="downloadFile(index)">
|
||||||
<v-icon size="14">insert_drive_file</v-icon>
|
<i>insert_drive_file</i>
|
||||||
{{ photo.baseName() }}
|
{{ photo.baseName() }}
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
@ -227,15 +217,15 @@
|
||||||
<br>
|
<br>
|
||||||
<button :title="$gettext('Location')" class="action-location"
|
<button :title="$gettext('Location')" class="action-location"
|
||||||
:data-uid="photo.UID" @click.exact="openLocation(index)">
|
:data-uid="photo.UID" @click.exact="openLocation(index)">
|
||||||
<v-icon size="14">location_on</v-icon>
|
<i>location_on</i>
|
||||||
{{ photo.locationInfo() }}
|
{{ photo.locationInfo() }}
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</v-card-title>
|
</div>
|
||||||
</v-card>
|
</div>
|
||||||
</v-flex>
|
</div>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
@ -244,9 +234,13 @@ import download from "common/download";
|
||||||
import Notify from "common/notify";
|
import Notify from "common/notify";
|
||||||
import {Input, InputInvalid, ClickShort, ClickLong} from "common/input";
|
import {Input, InputInvalid, ClickShort, ClickLong} from "common/input";
|
||||||
import {virtualizationTools} from 'common/virtualization-tools';
|
import {virtualizationTools} from 'common/virtualization-tools';
|
||||||
|
import IconLivePhoto from "component/icon/live_photo.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PPhotoCards',
|
name: 'PPhotoCards',
|
||||||
|
components: {
|
||||||
|
IconLivePhoto,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
photos: {
|
photos: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
@ -321,8 +315,16 @@ export default {
|
||||||
if (this.$refs.items === undefined) {
|
if (this.$refs.items === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const item of this.$refs.items) {
|
|
||||||
this.intersectionObserver.observe(item);
|
/**
|
||||||
|
* observing only every 5th item reduces the amount of time
|
||||||
|
* spent computing intersection by 80%. me might render up to
|
||||||
|
* 8 items more than required, but the time saved computing
|
||||||
|
* intersections is far greater than the time lost rendering
|
||||||
|
* a couple more items
|
||||||
|
*/
|
||||||
|
for (let i = 0; i < this.$refs.items.length; i += 5) {
|
||||||
|
this.intersectionObserver.observe(this.$refs.items[i]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
elementIndexFromIntersectionObserverEntry(entry) {
|
elementIndexFromIntersectionObserverEntry(entry) {
|
||||||
|
@ -335,8 +337,10 @@ export default {
|
||||||
this.elementIndexFromIntersectionObserverEntry,
|
this.elementIndexFromIntersectionObserverEntry,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.firstVisibleElementIndex = smallestIndex;
|
// we observe only every 5th item, so we increase the rendered
|
||||||
this.lastVisibileElementIndex = largestIndex;
|
// range here by 4 items in every directio just to be safe
|
||||||
|
this.firstVisibleElementIndex = smallestIndex - 4;
|
||||||
|
this.lastVisibileElementIndex = largestIndex + 4;
|
||||||
},
|
},
|
||||||
livePlayer(photo) {
|
livePlayer(photo) {
|
||||||
return document.querySelector("#live-player-" + photo.ID);
|
return document.querySelector("#live-player-" + photo.ID);
|
||||||
|
@ -388,9 +392,19 @@ export default {
|
||||||
if (ev.shiftKey) {
|
if (ev.shiftKey) {
|
||||||
this.selectRange(index);
|
this.selectRange(index);
|
||||||
} else {
|
} else {
|
||||||
this.$clipboard.toggle(this.photos[index]);
|
this.toggle(this.photos[index]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
toggle(photo) {
|
||||||
|
this.$clipboard.toggle(photo);
|
||||||
|
/**
|
||||||
|
* updating the clipboard does not rerender this component. Because of that
|
||||||
|
* there can be scenarios where the select-icon is missing after a change,
|
||||||
|
* for example when using touch and no hover-state changes.We therefore
|
||||||
|
* force an update to fix that.
|
||||||
|
*/
|
||||||
|
this.$forceUpdate();
|
||||||
|
},
|
||||||
onOpen(ev, index, showMerged) {
|
onOpen(ev, index, showMerged) {
|
||||||
const inputType = this.input.eval(ev, index);
|
const inputType = this.input.eval(ev, index);
|
||||||
|
|
||||||
|
@ -412,7 +426,7 @@ export default {
|
||||||
if (longClick || ev.shiftKey) {
|
if (longClick || ev.shiftKey) {
|
||||||
this.selectRange(index);
|
this.selectRange(index);
|
||||||
} else {
|
} else {
|
||||||
this.$clipboard.toggle(this.photos[index]);
|
this.toggle(this.photos[index]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.openPhoto(index, false);
|
this.openPhoto(index, false);
|
||||||
|
@ -427,6 +441,13 @@ export default {
|
||||||
},
|
},
|
||||||
selectRange(index) {
|
selectRange(index) {
|
||||||
this.$clipboard.addRange(index, this.photos);
|
this.$clipboard.addRange(index, this.photos);
|
||||||
|
/**
|
||||||
|
* updating the clipboard does not rerender this component. Because of that
|
||||||
|
* there can be scenarios where the select-icon is missing after a change,
|
||||||
|
* for example when selecting mutliple elements at once. We therefore
|
||||||
|
* force an update to fix that.
|
||||||
|
*/
|
||||||
|
this.$forceUpdate();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,118 +20,127 @@
|
||||||
</p>
|
</p>
|
||||||
</v-alert>
|
</v-alert>
|
||||||
</div>
|
</div>
|
||||||
<v-data-table v-else
|
<div v-else class="search-results photo-results list-view">
|
||||||
ref="dataTable"
|
<div class="v-table__overflow">
|
||||||
v-model="selected"
|
<table class="v-datatable v-table theme--light">
|
||||||
:headers="listColumns"
|
<thead>
|
||||||
:items="photos"
|
<tr>
|
||||||
hide-actions
|
<th class="p-col-select" />
|
||||||
class="search-results photo-results list-view"
|
<th :class="!$rtl ? 'text-xs-left' : 'text-xs-right'">
|
||||||
:class="{'select-results': selectMode}"
|
{{$gettext('Title')}}
|
||||||
disable-initial-sort
|
</th>
|
||||||
item-key="ID"
|
<th :class="!$rtl ? 'text-xs-left' : 'text-xs-right'" class="hidden-xs-only">
|
||||||
:no-data-text="notFoundMessage"
|
{{$gettext('Taken')}}
|
||||||
|
</th>
|
||||||
|
<th :class="!$rtl ? 'text-xs-left' : 'text-xs-right'" class="hidden-sm-and-down">
|
||||||
|
{{$gettext('Camera')}}
|
||||||
|
</th>
|
||||||
|
<th :class="!$rtl ? 'text-xs-left' : 'text-xs-right'" class="hidden-xs-only">
|
||||||
|
{{showName ? $gettext('Name') : $gettext('Location')}}
|
||||||
|
</th>
|
||||||
|
<th class="text-xs-center hidden-xs-only" />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(photo, index) in photos" :key="photo.ID" ref="items" :data-index="index">
|
||||||
|
<td :data-uid="photo.UID" class="result" :class="photo.classes()">
|
||||||
|
<div
|
||||||
|
v-if="index < firstVisibleElementIndex || index > lastVisibileElementIndex"
|
||||||
|
:key="photo.Hash"
|
||||||
|
class="image accent lighten-2"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
:key="photo.Hash"
|
||||||
|
:alt="photo.Title"
|
||||||
|
:style="`background-image: url(${photo.thumbnailUrl('tile_50')})`"
|
||||||
|
class="accent lighten-2 clickable image"
|
||||||
|
@touchstart="onMouseDown($event, index)"
|
||||||
|
@touchend.stop.prevent="onClick($event, index)"
|
||||||
|
@mousedown="onMouseDown($event, index)"
|
||||||
|
@contextmenu.stop="onContextMenu($event, index)"
|
||||||
|
@click.stop.prevent="onClick($event, index)"
|
||||||
|
>
|
||||||
|
<button v-if="selectMode" class="input-select">
|
||||||
|
<i class="select-on">check_circle</i>
|
||||||
|
<i class="select-off">radio_button_off</i>
|
||||||
|
</button>
|
||||||
|
<button v-else-if="photo.Type === 'video' || photo.Type === 'live' || photo.Type === 'animated'"
|
||||||
|
class="input-open"
|
||||||
|
@click.stop.prevent="openPhoto(index, true)">
|
||||||
|
<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>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
>
|
<td class="p-photo-desc clickable" :data-uid="photo.UID"
|
||||||
<template #items="props">
|
@click.exact="editPhoto(index)">
|
||||||
<td style="user-select: none;" :data-uid="props.item.UID" class="result" :class="props.item.classes()">
|
{{ photo.Title }}
|
||||||
<div
|
</td>
|
||||||
v-if="props.index < firstVisibleElementIndex || props.index > lastVisibileElementIndex"
|
<td class="p-photo-desc hidden-xs-only" :title="photo.getDateString()">
|
||||||
class="v-image accent lighten-2"
|
<button @click.stop.prevent="editPhoto(index)">
|
||||||
style="aspect-ratio: 1"
|
{{ photo.shortDateString() }}
|
||||||
/>
|
</button>
|
||||||
<v-img
|
</td>
|
||||||
v-if="props.index >= firstVisibleElementIndex && props.index <= lastVisibileElementIndex"
|
<td class="p-photo-desc hidden-sm-and-down">
|
||||||
:key="props.item.Hash"
|
<button @click.stop.prevent="editPhoto(index)">
|
||||||
:src="props.item.thumbnailUrl('tile_50')"
|
{{ photo.CameraMake }} {{ photo.CameraModel }}
|
||||||
:alt="props.item.Title"
|
</button>
|
||||||
:transition="false"
|
</td>
|
||||||
aspect-ratio="1"
|
<td class="p-photo-desc hidden-xs-only">
|
||||||
style="user-select: none"
|
<button v-if="filter.order === 'name'"
|
||||||
class="accent lighten-2 clickable"
|
:title="$gettext('Name')" @click.exact="downloadFile(index)">
|
||||||
@touchstart="onMouseDown($event, props.index)"
|
{{ photo.FileName }}
|
||||||
@touchend.stop.prevent="onClick($event, props.index)"
|
</button>
|
||||||
@mousedown="onMouseDown($event, props.index)"
|
<button v-else-if="photo.Country !== 'zz' && showLocation"
|
||||||
@contextmenu.stop="onContextMenu($event, props.index)"
|
@click.stop.prevent="openLocation(index)">
|
||||||
@click.stop.prevent="onClick($event, props.index)"
|
{{ photo.locationInfo() }}
|
||||||
>
|
</button>
|
||||||
<v-btn v-if="selectMode" :ripple="false"
|
<span v-else>
|
||||||
flat icon large absolute
|
{{ photo.locationInfo() }}
|
||||||
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.exact="editPhoto(props.index)">
|
|
||||||
{{ 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>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-xs-center">
|
<td class="text-xs-center">
|
||||||
<template v-if="props.index < firstVisibleElementIndex || props.index > lastVisibileElementIndex">
|
<template v-if="index < firstVisibleElementIndex || index > lastVisibileElementIndex">
|
||||||
<div v-if="hidePrivate" class="v-btn v-btn--icon v-btn--small" />
|
<div v-if="hidePrivate" class="v-btn v-btn--icon v-btn--small" />
|
||||||
<div class="v-btn v-btn--icon v-btn--small" />
|
<div class="v-btn v-btn--icon v-btn--small" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<v-btn v-if="hidePrivate" class="input-private" icon small flat :ripple="false"
|
<v-btn v-if="hidePrivate" class="input-private" icon small flat :ripple="false"
|
||||||
:data-uid="props.item.UID" @click.stop.prevent="props.item.togglePrivate()">
|
:data-uid="photo.UID" @click.stop.prevent="photo.togglePrivate()">
|
||||||
<v-icon v-if="props.item.Private" color="secondary-dark" class="select-on">lock</v-icon>
|
<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-icon v-else color="secondary" class="select-off">lock_open</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn class="input-like" icon small flat :ripple="false"
|
<v-btn class="input-like" icon small flat :ripple="false"
|
||||||
:data-uid="props.item.UID" @click.stop.prevent="props.item.toggleLike()">
|
:data-uid="photo.UID" @click.stop.prevent="photo.toggleLike()">
|
||||||
<v-icon v-if="props.item.Favorite" color="pink lighten-3" :data-uid="props.item.UID" class="select-on">
|
<v-icon v-if="photo.Favorite" color="pink lighten-3" :data-uid="photo.UID" class="select-on">
|
||||||
favorite
|
favorite
|
||||||
</v-icon>
|
</v-icon>
|
||||||
<v-icon v-else color="secondary" :data-uid="props.item.UID" class="select-off">favorite_border</v-icon>
|
<v-icon v-else color="secondary" :data-uid="photo.UID" class="select-off">favorite_border</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
</template>
|
</tr>
|
||||||
</v-data-table>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import download from "common/download";
|
import download from "common/download";
|
||||||
import Notify from "common/notify";
|
import Notify from "common/notify";
|
||||||
import {virtualizationTools} from 'common/virtualization-tools';
|
import {virtualizationTools} from 'common/virtualization-tools';
|
||||||
|
import IconLivePhoto from "component/icon/live_photo.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PPhotoList',
|
name: 'PPhotoList',
|
||||||
|
components: {
|
||||||
|
IconLivePhoto,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
photos: {
|
photos: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
@ -172,28 +181,10 @@ export default {
|
||||||
m += " " + this.$gettext("Non-photographic and low-quality images require a review before they appear in search results.");
|
m += " " + this.$gettext("Non-photographic and low-quality images require a review before they appear in search results.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let showName = this.filter.order === 'name';
|
|
||||||
|
|
||||||
const align = !this.$rtl ? 'left' : 'right';
|
|
||||||
return {
|
return {
|
||||||
config: this.$config.values,
|
config: this.$config.values,
|
||||||
notFoundMessage: m,
|
notFoundMessage: m,
|
||||||
'selected': [],
|
showName: this.filter.order === 'name',
|
||||||
'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
|
|
||||||
},
|
|
||||||
{text: '', value: '', align: 'center', sortable: false},
|
|
||||||
],
|
|
||||||
showName: showName,
|
|
||||||
showLocation: this.$config.values.settings.features.places,
|
showLocation: this.$config.values.settings.features.places,
|
||||||
hidePrivate: this.$config.values.settings.features.private,
|
hidePrivate: this.$config.values.settings.features.private,
|
||||||
mouseDown: {
|
mouseDown: {
|
||||||
|
@ -228,16 +219,23 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
observeItems() {
|
observeItems() {
|
||||||
if (this.$refs.dataTable === undefined) {
|
if (this.$refs.items === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rows = this.$refs.dataTable.$el.getElementsByTagName('tbody')[0].children;
|
|
||||||
for (const row of rows) {
|
/**
|
||||||
this.intersectionObserver.observe(row);
|
* observing only every 5th item reduces the amount of time
|
||||||
|
* spent computing intersection by 80%. me might render up to
|
||||||
|
* 8 items more than required, but the time saved computing
|
||||||
|
* intersections is far greater than the time lost rendering
|
||||||
|
* a couple more items
|
||||||
|
*/
|
||||||
|
for (let i = 0; i < this.$refs.items.length; i += 5) {
|
||||||
|
this.intersectionObserver.observe(this.$refs.items[i]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
elementIndexFromIntersectionObserverEntry(entry) {
|
elementIndexFromIntersectionObserverEntry(entry) {
|
||||||
return entry.target.rowIndex - 2;
|
return parseInt(entry.target.getAttribute('data-index'));
|
||||||
},
|
},
|
||||||
visibilitiesChanged(entries) {
|
visibilitiesChanged(entries) {
|
||||||
const [smallestIndex, largestIndex] = virtualizationTools.updateVisibleElementIndices(
|
const [smallestIndex, largestIndex] = virtualizationTools.updateVisibleElementIndices(
|
||||||
|
@ -246,8 +244,10 @@ export default {
|
||||||
this.elementIndexFromIntersectionObserverEntry,
|
this.elementIndexFromIntersectionObserverEntry,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.firstVisibleElementIndex = smallestIndex;
|
// we observe only every 5th item, so we increase the rendered
|
||||||
this.lastVisibileElementIndex = largestIndex;
|
// range here by 4 items in every directio just to be safe
|
||||||
|
this.firstVisibleElementIndex = smallestIndex - 4;
|
||||||
|
this.lastVisibileElementIndex = largestIndex + 4;
|
||||||
},
|
},
|
||||||
downloadFile(index) {
|
downloadFile(index) {
|
||||||
Notify.success(this.$gettext("Downloading…"));
|
Notify.success(this.$gettext("Downloading…"));
|
||||||
|
|
|
@ -21,118 +21,116 @@
|
||||||
</v-alert>
|
</v-alert>
|
||||||
</template>
|
</template>
|
||||||
<v-layout row wrap class="search-results photo-results mosaic-view" :class="{'select-results': selectMode}">
|
<v-layout row wrap class="search-results photo-results mosaic-view" :class="{'select-results': selectMode}">
|
||||||
<v-flex
|
<div
|
||||||
v-for="(photo, index) in photos"
|
v-for="(photo, index) in photos"
|
||||||
ref="items"
|
ref="items"
|
||||||
:key="photo.ID"
|
:key="photo.ID"
|
||||||
|
class="flex xs4 sm3 md2 lg1"
|
||||||
:data-index="index"
|
:data-index="index"
|
||||||
xs4 sm3 md2 lg1 d-flex
|
|
||||||
>
|
>
|
||||||
|
<!--
|
||||||
<div v-if="index < firstVisibleElementIndex || index > lastVisibileElementIndex"
|
The following div is the layout + size container. It makes the browser not
|
||||||
style="user-select: none; aspect-ratio: 1"
|
re-layout all elements in the list when the children of one of them changes
|
||||||
class="accent lighten-2 result"
|
-->
|
||||||
:class="photo.classes()"/>
|
<div class="image-container">
|
||||||
<v-card v-if="index >= firstVisibleElementIndex && index <= lastVisibileElementIndex"
|
<div v-if="index < firstVisibleElementIndex || index > lastVisibileElementIndex" class="accent lighten-2 result image" />
|
||||||
|
<div v-else
|
||||||
|
:key="photo.Hash"
|
||||||
tile
|
tile
|
||||||
:data-id="photo.ID"
|
:data-id="photo.ID"
|
||||||
:data-uid="photo.UID"
|
:data-uid="photo.UID"
|
||||||
style="user-select: none; aspect-ratio: 1"
|
:style="`background-image: url(${photo.thumbnailUrl('tile_224')})`"
|
||||||
class="accent lighten-2 result"
|
:class="photo.classes().join(' ') + ' accent lighten-2 result clickable image'"
|
||||||
:class="photo.classes()"
|
:alt="photo.Title"
|
||||||
@contextmenu.stop="onContextMenu($event, index)">
|
:title="photo.Title"
|
||||||
<v-img
|
@contextmenu.stop="onContextMenu($event, index)"
|
||||||
:key="photo.Hash"
|
@touchstart.passive="input.touchStart($event, index)"
|
||||||
:src="photo.thumbnailUrl('tile_224')"
|
@touchend.stop.prevent="onClick($event, index)"
|
||||||
:alt="photo.Title"
|
@mousedown.stop.prevent="input.mouseDown($event, index)"
|
||||||
:title="photo.Title"
|
@click.stop.prevent="onClick($event, index)"
|
||||||
:transition="false"
|
@mouseover="playLive(photo)"
|
||||||
aspect-ratio="1"
|
@mouseleave="pauseLive(photo)">
|
||||||
class="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">
|
<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"
|
<video :id="'live-player-' + photo.ID" :key="photo.ID" width="224" height="224" preload="none"
|
||||||
loop muted playsinline>
|
loop muted playsinline>
|
||||||
<source :src="photo.videoUrl()">
|
<source :src="photo.videoUrl()">
|
||||||
</video>
|
</video>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
|
|
||||||
<v-btn :ripple="false" :depressed="false" class="input-open"
|
<button v-if="photo.Type !== 'image' || photo.Files.length > 1"
|
||||||
icon flat small absolute
|
class="input-open"
|
||||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||||
@touchend.stop.prevent="onOpen($event, index, true)"
|
@touchend.stop.prevent="onOpen($event, index, true)"
|
||||||
@touchmove.stop.prevent
|
@touchmove.stop.prevent
|
||||||
@click.stop.prevent="onOpen($event, index, true)">
|
@click.stop.prevent="onOpen($event, index, true)">
|
||||||
<v-icon color="white" class="default-hidden action-raw" :title="$gettext('RAW')">photo_camera</v-icon>
|
<i v-if="photo.Type === 'raw'" color="white" class="action-raw" :title="$gettext('RAW')">photo_camera</i>
|
||||||
<v-icon color="white" class="default-hidden action-live" :title="$gettext('Live')">$vuetify.icons.live_photo</v-icon>
|
<i v-if="photo.Type === 'live'" color="white" class="action-live" :title="$gettext('Live')"><icon-live-photo/></i>
|
||||||
<v-icon color="white" class="default-hidden action-animated" :title="$gettext('Animated')">gif</v-icon>
|
<i v-if="photo.Type === 'animated'" color="white" class="action-animated" :title="$gettext('Animated')">gif</i>
|
||||||
<v-icon color="white" class="default-hidden action-play" :title="$gettext('Video')">play_arrow</v-icon>
|
<i v-if="photo.Type === 'video'" color="white" class="action-play" :title="$gettext('Video')">play_arrow</i>
|
||||||
<v-icon color="white" class="default-hidden action-stack" :title="$gettext('Stack')">burst_mode</v-icon>
|
<i v-if="photo.Type === 'image'" color="white" class="action-stack" :title="$gettext('Stack')">burst_mode</i>
|
||||||
</v-btn>
|
</button>
|
||||||
|
|
||||||
<v-btn :ripple="false" :depressed="false" class="input-view"
|
<button v-if="photo.Type === 'image' && selectMode"
|
||||||
icon flat small absolute :title="$gettext('View')"
|
class="input-view"
|
||||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
:title="$gettext('View')"
|
||||||
@touchend.stop.prevent="onOpen($event, index, false)"
|
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||||
@touchmove.stop.prevent
|
@touchend.stop.prevent="onOpen($event, index, false)"
|
||||||
@click.stop.prevent="onOpen($event, index, false)">
|
@touchmove.stop.prevent
|
||||||
<v-icon color="white" class="action-fullscreen">zoom_in</v-icon>
|
@click.stop.prevent="onOpen($event, index, false)">
|
||||||
</v-btn>
|
<i color="white" class="action-fullscreen">zoom_in</i>
|
||||||
|
</button>
|
||||||
|
|
||||||
<v-btn :ripple="false" :depressed="false" color="white" class="input-play"
|
<button v-if="hidePrivate && photo.Private" class="input-private">
|
||||||
icon flat small absolute :title="$gettext('Play')"
|
<i color="white" class="select-on">lock</i>
|
||||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
</button>
|
||||||
@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 v-if="hidePrivate" :ripple="false"
|
<!--
|
||||||
icon flat small absolute
|
We'd usually use v-if here to only render the button if needed.
|
||||||
class="input-private">
|
Because the button is supposed to be visible when the result is
|
||||||
<v-icon color="white" class="select-on">lock</v-icon>
|
being hovered over, implementing the v-if would require the use of
|
||||||
</v-btn>
|
a <v-hover> element around the result.
|
||||||
|
|
||||||
<v-btn :ripple="false"
|
Because rendering the plain HTML-Button is faster than rendering
|
||||||
icon flat small absolute
|
the v-hover component we instead hide the button by default and
|
||||||
class="input-select"
|
use css to show it when it is being hovered.
|
||||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
-->
|
||||||
@touchend.stop.prevent="onSelect($event, index)"
|
<button
|
||||||
@touchmove.stop.prevent
|
class="input-select"
|
||||||
@click.stop.prevent="onSelect($event, index)">
|
@mousedown.stop.prevent="input.mouseDown($event, index)"
|
||||||
<v-icon color="white" class="select-on">check_circle</v-icon>
|
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||||
<v-icon color="white" class="select-off">radio_button_off</v-icon>
|
@touchend.stop.prevent="onSelect($event, index)"
|
||||||
</v-btn>
|
@touchmove.stop.prevent
|
||||||
|
@click.stop.prevent="onSelect($event, index)">
|
||||||
|
<i color="white" class="select-on">check_circle</i>
|
||||||
|
<i color="white" class="select-off">radio_button_off</i>
|
||||||
|
</button>
|
||||||
|
|
||||||
<v-btn :ripple="false"
|
<button
|
||||||
icon flat small absolute
|
class="input-favorite"
|
||||||
class="input-favorite"
|
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
@touchend.stop.prevent="toggleLike($event, index)"
|
||||||
@touchend.stop.prevent="toggleLike($event, index)"
|
@touchmove.stop.prevent
|
||||||
@touchmove.stop.prevent
|
@click.stop.prevent="toggleLike($event, index)"
|
||||||
@click.stop.prevent="toggleLike($event, index)">
|
>
|
||||||
<v-icon color="white" class="select-on">favorite</v-icon>
|
<i v-if="photo.Favorite">favorite</i>
|
||||||
<v-icon color="white" class="select-off">favorite_border</v-icon>
|
<i v-else>favorite_border</i>
|
||||||
</v-btn>
|
</button>
|
||||||
</v-img>
|
</div>
|
||||||
</v-card>
|
</div>
|
||||||
</v-flex>
|
</div>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import {Input, InputInvalid, ClickShort, ClickLong} from "common/input";
|
import {Input, InputInvalid, ClickShort, ClickLong} from "common/input";
|
||||||
import {virtualizationTools} from 'common/virtualization-tools';
|
import {virtualizationTools} from 'common/virtualization-tools';
|
||||||
|
import IconLivePhoto from "component/icon/live_photo.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PPhotoMosaic',
|
name: 'PPhotoMosaic',
|
||||||
|
components: {
|
||||||
|
IconLivePhoto,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
photos: {
|
photos: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
@ -194,8 +192,16 @@ export default {
|
||||||
if (this.$refs.items === undefined) {
|
if (this.$refs.items === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const item of this.$refs.items) {
|
|
||||||
this.intersectionObserver.observe(item);
|
/**
|
||||||
|
* observing only every 5th item reduces the amount of time
|
||||||
|
* spent computing intersection by 80%. me might render up to
|
||||||
|
* 8 items more than required, but the time saved computing
|
||||||
|
* intersections is far greater than the time lost rendering
|
||||||
|
* a couple more items
|
||||||
|
*/
|
||||||
|
for (let i = 0; i < this.$refs.items.length; i += 5) {
|
||||||
|
this.intersectionObserver.observe(this.$refs.items[i]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
elementIndexFromIntersectionObserverEntry(entry) {
|
elementIndexFromIntersectionObserverEntry(entry) {
|
||||||
|
@ -208,8 +214,10 @@ export default {
|
||||||
this.elementIndexFromIntersectionObserverEntry,
|
this.elementIndexFromIntersectionObserverEntry,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.firstVisibleElementIndex = smallestIndex;
|
// we observe only every 5th item, so we increase the rendered
|
||||||
this.lastVisibileElementIndex = largestIndex;
|
// range here by 4 items in every directio just to be safe
|
||||||
|
this.firstVisibleElementIndex = smallestIndex - 4;
|
||||||
|
this.lastVisibileElementIndex = largestIndex + 4;
|
||||||
},
|
},
|
||||||
livePlayer(photo) {
|
livePlayer(photo) {
|
||||||
return document.querySelector("#live-player-" + photo.ID);
|
return document.querySelector("#live-player-" + photo.ID);
|
||||||
|
@ -258,6 +266,7 @@ export default {
|
||||||
},
|
},
|
||||||
toggle(photo) {
|
toggle(photo) {
|
||||||
this.$clipboard.toggle(photo);
|
this.$clipboard.toggle(photo);
|
||||||
|
this.$forceUpdate();
|
||||||
},
|
},
|
||||||
onOpen(ev, index, showMerged) {
|
onOpen(ev, index, showMerged) {
|
||||||
const inputType = this.input.eval(ev, index);
|
const inputType = this.input.eval(ev, index);
|
||||||
|
@ -295,6 +304,7 @@ export default {
|
||||||
},
|
},
|
||||||
selectRange(index) {
|
selectRange(index) {
|
||||||
this.$clipboard.addRange(index, this.photos);
|
this.$clipboard.addRange(index, this.photos);
|
||||||
|
this.$forceUpdate();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -198,6 +198,92 @@ body.chrome #photoprism .search-results .result {
|
||||||
box-shadow: 0 0 0 0 rgba(0, 0, 0, .2), 0 0 0 0 rgba(0, 0, 0, .14), 0 0 0 0 rgba(0, 0, 0, .12) !important;
|
box-shadow: 0 0 0 0 rgba(0, 0, 0, .2), 0 0 0 0 rgba(0, 0, 0, .14), 0 0 0 0 rgba(0, 0, 0, .12) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#photoprism .search-results .image-container {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
contain: layout paint style size;
|
||||||
|
}
|
||||||
|
|
||||||
|
#photoprism .cards-view .result .image,
|
||||||
|
#photoprism .list-view .result .image,
|
||||||
|
#photoprism .mosaic-view .result.image
|
||||||
|
{
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background-position: center center;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#photoprism .cards-view .result .image button,
|
||||||
|
#photoprism .list-view .result .image button,
|
||||||
|
#photoprism .mosaic-view .result.image button {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
transition: background-color .3s cubic-bezier(.25,.8,.5,1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#photoprism .cards-view .result .image button {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#photoprism .list-view .result .image button {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
#photoprism .cards-view .result .image button:hover,
|
||||||
|
#photoprism .list-view .result .image button:hover,
|
||||||
|
#photoprism .mosaic-view .result.image button:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#photoprism .mosaic-view .result.image i,
|
||||||
|
#photoprism .list-view .result .image i,
|
||||||
|
#photoprism .cards-view .result .image i {
|
||||||
|
color: white;
|
||||||
|
font-size: 24px;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-family: Material Icons;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
text-transform: none;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#photoprism .cards-view .result .caption {
|
||||||
|
hyphens: auto;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
#photoprism .cards-view .result .card-details i {
|
||||||
|
font-size: 14px;
|
||||||
|
display: inline-block;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-family: Material Icons;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
text-transform: none;
|
||||||
|
line-height: 1;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#photoprism .cards-view .result.placeholder .card-details i {
|
||||||
|
width: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#photoprism .cards-view .result.is-selected,
|
#photoprism .cards-view .result.is-selected,
|
||||||
#photoprism .mosaic-view .result.is-selected {
|
#photoprism .mosaic-view .result.is-selected {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
|
@ -392,4 +478,4 @@ body.chrome #photoprism .search-results .result {
|
||||||
#photoprism .face-results .is-face.is-hidden,
|
#photoprism .face-results .is-face.is-hidden,
|
||||||
#photoprism .face-results .is-marker.is-invalid {
|
#photoprism .face-results .is-marker.is-invalid {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@ export default {
|
||||||
complete: false,
|
complete: false,
|
||||||
results: [],
|
results: [],
|
||||||
scrollDisabled: true,
|
scrollDisabled: true,
|
||||||
scrollDistance: window.innerHeight * 2,
|
scrollDistance: window.innerHeight * 6,
|
||||||
batchSize: batchSize,
|
batchSize: batchSize,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
page: 0,
|
page: 0,
|
||||||
|
|
Loading…
Add table
Reference in a new issue