UX: Remove lag when selecting pictures #477 #500 #862

This commit is contained in:
Michael Mayer 2021-01-10 22:14:47 +01:00
parent 5eea2eac41
commit 8869e5b995
24 changed files with 1097 additions and 924 deletions

View file

@ -31,7 +31,6 @@ https://docs.photoprism.org/developer-guide/
import RestModel from "model/rest";
import Notify from "common/notify";
import { $gettext } from "./vm";
import Event from "pubsub-js";
export const MaxItems = 999;
@ -85,7 +84,11 @@ export class Clipboard {
const id = model.getId();
this.toggleId(id);
const result = this.toggleId(id);
this.updateDom(id, result);
return result;
}
toggleId(id) {
@ -99,15 +102,11 @@ export class Clipboard {
return;
}
Event.publish("photos.updated", { entities: [{ UID: id, Selected: true }] });
this.selection.push(id);
this.selectionMap["id:" + id] = true;
this.lastId = id;
result = true;
} else {
Event.publish("photos.updated", { entities: [{ UID: id, Selected: false }] });
this.selection.splice(index, 1);
delete this.selectionMap["id:" + id];
this.lastId = "";
@ -118,20 +117,18 @@ export class Clipboard {
return result;
}
add(model, publish) {
add(model) {
if (!this.isModel(model)) {
return;
}
const id = model.getId();
this.addId(id, publish);
this.addId(id);
}
addId(id, publish) {
if (publish) {
Event.publish("photos.updated", { entities: [{ UID: id, Selected: true }] });
}
addId(id) {
this.updateDom(id, true);
if (this.hasId(id)) {
return true;
@ -170,15 +167,11 @@ export class Clipboard {
rangeEnd = newEnd;
}
let entities = [];
for (let i = rangeStart; i <= rangeEnd; i++) {
this.add(models[i], false);
entities.push({ UID: models[i].getId(), Selected: true });
this.updateDom(models[i].getId(), true);
}
Event.publish("photos.updated", { entities });
return rangeEnd - rangeStart + 1;
}
@ -202,10 +195,8 @@ export class Clipboard {
this.removeId(model.getId(), publish);
}
removeId(id, publish) {
if (publish) {
Event.publish("photos.updated", { entities: [{ UID: id, Selected: false }] });
}
removeId(id) {
this.updateDom(id, false);
if (!this.hasId(id)) {
return false;
@ -239,17 +230,22 @@ export class Clipboard {
}
clear() {
Event.publish("photos.updated", {
entities: this.selection.map((uid) => {
return { UID: uid, Selected: false };
}),
});
this.selection.forEach((id) => this.updateDom(id, false));
this.lastId = "";
this.selectionMap = {};
this.selection.splice(0, this.selection.length);
this.storage.removeItem(this.storageKey);
}
updateDom(uid, selected) {
document.querySelectorAll(`.uid-${uid}`).forEach((el) => {
if (selected) {
el.classList.add("is-selected");
} else {
el.classList.remove("is-selected");
}
});
}
}
const PhotoClipboard = new Clipboard(window.localStorage, "photo_clipboard");

View file

@ -1,6 +1,6 @@
<template>
<v-container grid-list-xs fluid class="pa-2 p-photos p-photo-cards">
<v-card v-if="photos.length === 0" class="p-photos-empty secondary-light lighten-1 ma-1" flat>
<v-card v-if="photos.length === 0" class="no-results secondary-light lighten-1 ma-1" flat>
<v-card-title primary-title>
<div>
<h3 v-if="filter.order === 'edited'" class="title ma-0 pa-0">
@ -19,98 +19,79 @@
</div>
</v-card-title>
</v-card>
<v-layout row wrap class="p-results">
<v-layout row wrap class="search-results photo-results cards-view">
<v-flex
v-for="(photo, index) in photos"
:key="index"
:data-uid="photo.UID"
class="p-photo"
xs12 sm6 md4 lg3 xlg2 xxxl1 d-flex
:class="{ 'is-selected': photo.Selected, portrait: photo.Portrait }"
>
<v-hover>
<v-card slot-scope="{ hover }" tile
:dark="photo.Selected"
:class="photo.Selected ? 'selected elevation-10 ma-0 accent darken-1 white--text select-transition' : 'elevation-0 ma-1 accent lighten-3 select-transition'"
<v-card tile
:data-id="photo.ID"
:data-uid="photo.UID"
class="result accent lighten-2"
:class="photo.classes()"
@contextmenu="onContextMenu($event, index)">
<v-img :src="photo.thumbnailUrl('tile_500')"
<div class="card-background accent lighten-2"></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"
class="accent lighten-3 clickable"
@mousedown="onMouseDown($event, index)"
@click.stop.prevent="onClick($event, index)"
>
<v-layout
v-if="photo.Type === 'live'"
v-show="hover"
fill-height
align-center
justify-center
ma-0
class="live-player"
style="overflow: hidden;"
>
<video :key="photo.videoUrl()" width="500" height="500" autoplay loop muted playsinline>
<v-layout v-if="photo.Type === 'video' || photo.Type === 'live'" class="live-player">
<video :key="photo.ID" width="500" height="500" autoplay loop muted playsinline>
<source :src="photo.videoUrl()" type="video/mp4">
</video>
</v-layout>
<v-btn v-if="hidePrivate && photo.Private" :ripple="false"
icon flat large absolute
class="p-photo-private opacity-75">
<v-icon color="white">lock</v-icon>
</v-btn>
<v-btn v-if="hover || photo.Selected" :ripple="false"
icon flat large absolute
:class="photo.Selected ? 'p-photo-select' : 'p-photo-select opacity-50'"
@click.stop.prevent="onSelect($event, index)">
<v-icon v-if="photo.Selected" color="white"
class="t-select t-on">check_circle
</v-icon>
<v-icon v-else color="accent lighten-3" class="t-select t-off">radio_button_off</v-icon>
</v-btn>
<v-btn icon flat large absolute :ripple="false"
:class="photo.Favorite ? 'p-photo-like opacity-75' : 'p-photo-like opacity-50'"
@click.stop.prevent="photo.toggleLike()">
<v-icon v-if="photo.Favorite" color="white" class="t-like t-on" :data-uid="photo.UID">
favorite
</v-icon>
<v-icon v-else color="accent lighten-3" class="t-like t-off" :data-uid="photo.UID">
favorite_border
</v-icon>
</v-btn>
<template v-if="photo.isPlayable()">
<v-btn v-if="photo.Type === 'live'" :ripple="false"
icon flat large absolute class="p-photo-live opacity-75"
title="Live Photo" @click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-play">adjust</v-icon>
</v-btn>
<v-btn v-else color="white" :ripple="false"
outline large fab absolute class="p-photo-play opacity-75" :depressed="false"
title="Play" @click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-play">play_arrow</v-icon>
</v-btn>
</template>
<v-btn v-else-if="photo.Type === 'image' && photo.Files.length > 1" :ripple="false"
icon flat large absolute class="p-photo-merged opacity-75"
<v-btn :ripple="false" :depressed="false" class="input-open"
icon flat absolute
@click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-burst">burst_mode</v-icon>
<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')">adjust</v-icon>
<v-icon color="white" class="default-hidden action-stack" :title="$gettext('Stack')">burst_mode</v-icon>
</v-btn>
<v-btn v-else-if="photo.Type === 'image' && selectMode && hover" :ripple="false"
icon flat large absolute class="p-photo-fullscreen opacity-75"
<v-btn :ripple="false" :depressed="false" class="input-view"
icon flat absolute :title="$gettext('View')"
@click.stop.prevent="openPhoto(index, false)">
<v-icon color="white" class="action-open">zoom_in</v-icon>
<v-icon color="white" class="action-fullscreen">zoom_in</v-icon>
</v-btn>
<v-btn v-else-if="photo.Type === 'raw'" :ripple="false"
icon flat large absolute class="p-photo-raw opacity-75"
title="RAW" @click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-burst">photo_camera</v-icon>
<v-btn :ripple="false" :depressed="false" color="white" class="input-play"
outline fab absolute :title="$gettext('Play')"
@click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-play">play_arrow</v-icon>
</v-btn>
<v-btn v-if="hidePrivate" :ripple="false"
icon flat absolute
class="input-private">
<v-icon color="white" class="select-on">lock</v-icon>
</v-btn>
<v-btn :ripple="false"
icon flat absolute
class="input-select"
@click.stop.prevent="onSelect($event, index)">
<v-icon color="white" class="select-on">check_circle</v-icon>
<v-icon color="accent lighten-3" class="select-off">radio_button_off</v-icon>
</v-btn>
<v-btn :ripple="false"
icon flat absolute
class="input-favorite"
@click.stop.prevent="photo.toggleLike()">
<v-icon color="white" class="select-on">favorite</v-icon>
<v-icon color="accent lighten-3" class="select-off">favorite_border</v-icon>
</v-btn>
</v-img>
<v-card-title primary-title class="pa-3 p-photo-desc" style="user-select: none;">
<v-card-title primary-title class="pa-3 card-details" style="user-select: none;">
<div>
<h3 class="body-2 mb-2" :title="photo.Title">
<button class="action-title-edit" :data-uid="photo.UID"
@ -118,7 +99,7 @@
{{ photo.Title | truncate(80) }}
</button>
</h3>
<div v-if="photo.Description" class="caption mb-2" title="Description">
<div v-if="photo.Description" class="caption mb-2" :title="labels.description">
<button @click.exact="editPhoto(index)">
{{ photo.Description }}
</button>
@ -126,17 +107,17 @@
<div class="caption">
<button class="action-date-edit" :data-uid="photo.UID"
@click.exact="editPhoto(index)">
<v-icon size="14" title="Taken">date_range</v-icon>
<v-icon size="14" :title="labels.taken">date_range</v-icon>
{{ photo.getDateString() }}
</button>
<template v-if="!photo.Description">
<br/>
<button v-if="photo.Type === 'video'" title="Video"
<button v-if="photo.Type === 'video'" :title="labels.video"
@click.exact="openPhoto(index, true)">
<v-icon size="14">movie</v-icon>
{{ photo.getVideoInfo() }}
</button>
<button v-else title="Camera" class="action-camera-edit"
<button v-else :title="labels.camera" class="action-camera-edit"
:data-uid="photo.UID" @click.exact="editPhoto(index)">
<v-icon size="14">photo_camera</v-icon>
{{ photo.getPhotoInfo() }}
@ -144,7 +125,7 @@
</template>
<template v-if="filter.order === 'name' && $config.feature('download')">
<br/>
<button title="Name"
<button :title="labels.name"
@click.exact="downloadFile(index)">
<v-icon size="14">insert_drive_file</v-icon>
{{ photo.baseName() }}
@ -152,7 +133,7 @@
</template>
<template v-if="showLocation && photo.Country !== 'zz'">
<br/>
<button title="Location" class="action-location"
<button :title="labels.location" class="action-location"
:data-uid="photo.UID" @click.exact="openLocation(index)">
<v-icon size="14">location_on</v-icon>
{{ photo.locationInfo() }}
@ -161,6 +142,7 @@
</div>
</div>
</v-card-title>
<v-card-actions v-if="photo.Quality < 3 && context === 'review'">
<v-layout row wrap align-center>
<v-flex xs12>
@ -178,7 +160,6 @@
</v-layout>
</v-card-actions>
</v-card>
</v-hover>
</v-flex>
</v-layout>
</v-container>
@ -188,7 +169,6 @@ export default {
name: 'PPhotoCards',
props: {
photos: Array,
selection: Array,
openPhoto: Function,
editPhoto: Function,
openLocation: Function,
@ -203,8 +183,14 @@ export default {
hidePrivate: this.$config.settings().features.private,
debug: this.$config.get('debug'),
labels: {
location: this.$gettext("Location"),
description: this.$gettext("Description"),
taken: this.$gettext("Taken"),
approve: this.$gettext("Approve"),
archive: this.$gettext("Archive"),
camera: this.$gettext("Camera"),
video: this.$gettext("Video"),
name: this.$gettext("Name"),
},
mouseDown: {
index: -1,

View file

@ -1,7 +1,7 @@
<template>
<div>
<div v-if="photos.length === 0" class="pa-2">
<v-card class="p-photos-empty secondary-light lighten-1 ma-1" flat>
<v-card class="no-results secondary-light lighten-1 ma-1" flat>
<v-card-title primary-title>
<div>
<h3 v-if="filter.order === 'edited'" class="title ma-0 pa-0">
@ -26,26 +26,28 @@
:headers="listColumns"
:items="photos"
hide-actions
class="elevation-0 p-photos p-photo-list p-results"
class="search-results photo-results list-view"
disable-initial-sort
item-key="ID"
:no-data-text="notFoundMessage"
>
<template slot="items" slot-scope="props">
<td style="user-select: none;" :data-uid="props.item.UID">
<td style="user-select: none;" :data-uid="props.item.UID" class="result" :class="props.item.classes()">
<v-img class="accent lighten-2 clickable" aspect-ratio="1"
:src="props.item.thumbnailUrl('tile_50')"
@mousedown="onMouseDown($event, props.index)"
@contextmenu="onContextMenu($event, props.index)"
@click.stop.prevent="onClick($event, props.index)"
>
<v-btn v-if="props.item.Selected" :ripple="false"
flat icon large absolute class="p-photo-select">
<v-icon color="white" class="t-select t-on">check_circle</v-icon>
<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="accent lighten-3" class="select-off">radio_button_off</v-icon>
</v-btn>
<v-btn v-else-if="!selectMode && (props.item.Type === 'video' || props.item.Type === 'live')"
<v-btn v-else-if="props.item.Type === 'video' || props.item.Type === 'live'"
:ripple="false"
flat icon large absolute class="p-photo-play opacity-75"
flat icon large absolute class="input-play opacity-75"
@click.stop.prevent="openPhoto(props.index, true)">
<v-icon color="white" class="action-play">play_arrow</v-icon>
</v-btn>
@ -68,7 +70,7 @@
</td>
<td class="p-photo-desc hidden-xs-only">
<button v-if="filter.order === 'name'"
title="Name" @click.exact="downloadFile(props.index)">
:title="$gettext('Name')" @click.exact="downloadFile(props.index)">
{{ props.item.FileName }}
</button>
<button v-else-if="props.item.Country !== 'zz' && showLocation"
@ -146,25 +148,6 @@ export default {
},
};
},
watch: {
photos: function (photos) {
this.selected.splice(0);
for (let i = 0; i < photos.length; i++) {
if (this.$clipboard.has(photos[i])) {
this.selected.push(photos[i]);
}
}
},
selection: function () {
this.refreshSelection();
},
},
mounted: function () {
this.$nextTick(function () {
this.refreshSelection();
});
},
methods: {
downloadFile(index) {
const photo = this.photos[index];
@ -177,7 +160,7 @@ export default {
if (ev.shiftKey) {
this.selectRange(index);
} else {
this.$clipboard.toggle(this.photos[index]);
this.toggle(this.photos[index]);
}
},
onMouseDown(ev, index) {
@ -191,7 +174,7 @@ export default {
if (longClick || ev.shiftKey) {
this.selectRange(index);
} else {
this.$clipboard.toggle(this.photos[index]);
this.toggle(this.photos[index]);
}
} else if (this.photos[index]) {
let photo = this.photos[index];
@ -210,18 +193,12 @@ export default {
this.selectRange(index);
}
},
toggle(photo) {
this.$clipboard.toggle(photo);
},
selectRange(index) {
this.$clipboard.addRange(index, this.photos);
},
refreshSelection() {
this.selected.splice(0);
for (let i = 0; i < this.photos.length; i++) {
if (this.$clipboard.has(this.photos[i])) {
this.selected.push(this.photos[i]);
}
}
},
}
};
</script>

View file

@ -1,6 +1,6 @@
<template>
<v-container grid-list-xs fluid class="pa-2 p-photos p-photo-mosaic">
<v-card v-if="photos.length === 0" class="p-photos-empty secondary-light lighten-1 ma-1" flat>
<v-card v-if="photos.length === 0" class="no-results secondary-light lighten-1 ma-1" flat>
<v-card-title primary-title>
<div>
<h3 v-if="filter.order === 'edited'" class="title ma-0 pa-0">
@ -19,95 +19,77 @@
</div>
</v-card-title>
</v-card>
<v-layout row wrap class="p-results">
<v-layout row wrap class="search-results photo-results mosaic-view">
<v-flex
v-for="(photo, index) in photos"
:key="index"
:data-uid="photo.UID"
:class="{ selected: photo.Selected, portrait: photo.Portrait }"
class="p-photo"
xs4 sm3 md2 lg1 d-flex
>
<v-hover>
<v-card slot-scope="{ hover }" tile
:class="photo.Selected ? 'selected elevation-10 ma-0 select-transition' : 'elevation-0 ma-1 select-transition'"
:title="photo.Title"
@contextmenu="onContextMenu($event, index)">
<v-img :src="photo.thumbnailUrl('tile_224')"
aspect-ratio="1"
class="accent lighten-2 clickable"
@mousedown="onMouseDown($event, index)"
@click.stop.prevent="onClick($event, index)"
>
<v-layout
v-if="photo.Type === 'live'"
v-show="hover"
fill-height
align-center
justify-center
ma-0
class="live-player"
style="overflow: hidden;"
>
<video :key="photo.videoUrl()" width="224" height="224" autoplay loop muted playsinline>
<source :src="photo.videoUrl()" type="video/mp4">
</video>
</v-layout>
<v-card tile
:data-id="photo.ID"
:data-uid="photo.UID"
class="result"
:class="photo.classes()"
@contextmenu="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-3 clickable"
@mousedown="onMouseDown($event, index)"
@click.stop.prevent="onClick($event, index)"
>
<v-layout v-if="photo.Type === 'video' || photo.Type === 'live'" class="live-player">
<video :key="photo.ID" width="224" height="224" autoplay loop muted playsinline>
<source :src="photo.videoUrl()" type="video/mp4">
</video>
</v-layout>
<v-btn v-if="hidePrivate && photo.Private" :ripple="false"
icon flat small absolute
class="p-photo-private opacity-75">
<v-icon color="white">lock</v-icon>
</v-btn>
<v-btn :ripple="false" :depressed="false" class="input-open"
icon flat small absolute
@click.stop.prevent="openPhoto(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')">adjust</v-icon>
<v-icon color="white" class="default-hidden action-stack" :title="$gettext('Stack')">burst_mode</v-icon>
</v-btn>
<v-btn v-if="hover || photo.Selected" :ripple="false"
icon flat small absolute
:class="photo.Selected ? 'p-photo-select' : 'p-photo-select opacity-50'"
@click.stop.prevent="onSelect($event, index)">
<v-icon v-if="photo.Selected" color="white"
class="t-select t-on">check_circle
</v-icon>
<v-icon v-else color="accent lighten-3" class="t-select t-off">radio_button_off</v-icon>
</v-btn>
<v-btn :ripple="false" :depressed="false" class="input-view"
icon flat small absolute :title="$gettext('View')"
@click.stop.prevent="openPhoto(index, false)">
<v-icon color="white" class="action-fullscreen">zoom_in</v-icon>
</v-btn>
<v-btn icon flat small absolute :ripple="false"
:class="photo.Favorite ? 'p-photo-like opacity-75' : 'p-photo-like opacity-50'"
@click.stop.prevent="photo.toggleLike()">
<v-icon v-if="photo.Favorite" color="white" class="t-like t-on" :data-uid="photo.UID">favorite</v-icon>
<v-icon v-else color="accent lighten-3" class="t-like t-off" :data-uid="photo.UID">favorite_border
</v-icon>
</v-btn>
<v-btn :ripple="false" :depressed="false" color="white" class="input-play"
outline fab absolute :title="$gettext('Play')"
@click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-play">play_arrow</v-icon>
</v-btn>
<template v-if="photo.isPlayable()">
<v-btn v-if="photo.Type === 'live'" color="white"
icon flat small absolute class="p-photo-live opacity-75" :depressed="false" :ripple="false"
title="Live Photo" @click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-play">adjust</v-icon>
</v-btn>
<v-btn v-else color="white"
outline fab absolute class="p-photo-play opacity-75" :depressed="false" :ripple="false"
title="Play" @click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-play">play_arrow</v-icon>
</v-btn>
</template>
<v-btn v-else-if="photo.Type === 'image' && photo.Files.length > 1" :ripple="false"
icon flat small absolute class="p-photo-merged opacity-75"
@click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-burst">burst_mode</v-icon>
</v-btn>
<v-btn v-else-if="photo.Type === 'image' && selectMode && hover" :ripple="false"
icon flat small absolute class="p-photo-fullscreen opacity-75"
@click.stop.prevent="openPhoto(index, false)">
<v-icon color="white" class="action-open">zoom_in</v-icon>
</v-btn>
<v-btn v-else-if="photo.Type === 'raw'" :ripple="false"
icon flat small absolute class="p-photo-raw opacity-75"
title="RAW" @click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-burst">photo_camera</v-icon>
</v-btn>
</v-img>
</v-card>
</v-hover>
<v-btn v-if="hidePrivate" :ripple="false"
icon flat small absolute
class="input-private">
<v-icon color="white" class="select-on">lock</v-icon>
</v-btn>
<v-btn :ripple="false"
icon flat small absolute
class="input-select"
@click.stop.prevent="onSelect($event, index)">
<v-icon color="white" class="select-on">check_circle</v-icon>
<v-icon color="accent lighten-3" class="select-off">radio_button_off</v-icon>
</v-btn>
<v-btn :ripple="false"
icon flat small absolute
class="input-favorite"
@click.stop.prevent="photo.toggleLike()">
<v-icon color="white" class="select-on">favorite</v-icon>
<v-icon color="accent lighten-3" class="select-off">favorite_border</v-icon>
</v-btn>
</v-img>
</v-card>
</v-flex>
</v-layout>
</v-container>
@ -138,13 +120,16 @@ export default {
if (ev.shiftKey) {
this.selectRange(index);
} else {
this.$clipboard.toggle(this.photos[index]);
this.toggle(this.photos[index]);
}
},
onMouseDown(ev, index) {
this.mouseDown.index = index;
this.mouseDown.timeStamp = ev.timeStamp;
},
toggle(photo) {
this.$clipboard.toggle(photo);
},
onClick(ev, index) {
let longClick = (this.mouseDown.index === index && ev.timeStamp - this.mouseDown.timeStamp > 400);
@ -152,7 +137,7 @@ export default {
if (longClick || ev.shiftKey) {
this.selectRange(index);
} else {
this.$clipboard.toggle(this.photos[index]);
this.toggle(this.photos[index]);
}
} else {
this.openPhoto(index, false);

View file

@ -39,7 +39,7 @@ https://docs.photoprism.org/developer-guide/
@import url("video.css");
@import url("maps.css");
@import url("viewer.css");
@import url("photos.css");
@import url("search.css");
@import url("labels.css");
@import url("files.css");
@import url("help.css");
@ -134,10 +134,6 @@ main {
left: 10%;
}
#photoprism main .p-results a {
color: #333333;
}
#photoprism .v-badge__badge {
font-size: 12px;
height: 19px;

View file

@ -1,10 +1,5 @@
#photoprism .p-labels-cards .p-label-like {
left: 4px;
bottom: 4px;
}
#photoprism main .p-inline-edit a,
#photoprism main .p-inline-edit a span {
#photoprism .search-results .inline-edit a,
#photoprism .search-results .inline-edit a span {
cursor: text;
color: inherit;
}

View file

@ -1,142 +0,0 @@
#photoprism .p-col-select {
width: 66px;
}
#photoprism .p-col-primary {
width: 44px;
}
#photoprism .p-photo-list tr td:first-child {
padding: 0 0 0 8px;
text-align: center;
}
#photoprism .p-photo-list .p-photo-select,
#photoprism .p-photo-list .p-photo-play {
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
text-align: center;
}
#photoprism .p-photo-mosaic .p-photo-private,
#photoprism .p-photo-cards .p-photo-private {
top: 4px;
right: 4px;
}
#photoprism .p-photo-mosaic .p-photo-edit,
#photoprism .p-photo-cards .p-photo-edit {
top: 4px;
right: 4px;
}
#photoprism .p-photo-mosaic .p-photo-merged,
#photoprism .p-photo-cards .p-photo-merged,
#photoprism .p-photo-mosaic .p-photo-live,
#photoprism .p-photo-cards .p-photo-live {
top: 4px;
left: 4px;
}
#photoprism .p-photo-mosaic .p-photo-play,
#photoprism .p-photo-cards .p-photo-play {
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
text-align: center;
}
#photoprism .p-photo-mosaic .p-photo-like,
#photoprism .p-photo-cards .p-photo-like {
left: 4px;
bottom: 4px;
}
#photoprism .p-photo-cards .action-select,
#photoprism .p-photo-mosaic .action-select,
#photoprism .p-photo-cards .p-photo-select,
#photoprism .p-photo-mosaic .p-photo-select {
right: 4px;
bottom: 4px;
}
#photoprism .p-albums-cards .action-select,
#photoprism .p-albums-cards .p-album-select,
#photoprism .p-labels-cards .p-label-select,
#photoprism .p-folders-cards .p-folder-select,
#photoprism .p-files-cards .p-file-select {
right: 4px;
bottom: 4px;
}
#photoprism .p-albums-cards .action-share {
top: 4px;
left: 4px;
}
#photoprism .p-labels-cards .p-label-count {
position: absolute;
left: 12px;
top: 12px;
}
#photoprism .p-clipboard.--ltr {
right: 8px;
bottom: 12px;
}
#photoprism .p-clipboard.--rtl {
left: 8px;
bottom: 12px;
}
#photoprism .p-clipboard .v-btn.v-btn--disabled:not(.v-btn--icon):not(.v-btn--flat):not(.v-btn--outline) {
background-color: rgba(100, 100, 100, 0.5) !important;
}
#photoprism .p-album-desc button,
#photoprism .p-photo-desc button {
text-align: left;
}
#photoprism .live-player video {
width: auto;
height: 100%;
position: absolute;
overflow: hidden;
}
#photoprism .portrait .live-player video {
width: 100%;
height: auto;
}
#photoprism table.photo-files tbody tr td:first-child {
width: 30%;
padding: 0 16px 0 24px;
}
#photoprism .img-placeholder { opacity: 0.3; }
#photoprism .img-color-0 { background-color: #696969 !important; } /* Black */
#photoprism .img-color-1 { background-color: #DCDCDC !important; } /* Grey */
#photoprism .img-color-2 { background-color: #98817B !important; } /* Brown */
#photoprism .img-color-3 { background-color: #E5E4E2 !important; } /* Gold */
#photoprism .img-color-4 { background-color: #fdfdfd !important; } /* White */
#photoprism .img-color-5 { background-color: #AB47BC !important; } /* Purple */
#photoprism .img-color-6 { background-color: #8A7F8D !important; } /* Blue */
#photoprism .img-color-7 { background-color: #91A3B0 !important; } /* Cyan */
#photoprism .img-color-8 { background-color: #B2BEB5 !important; } /* Teal */
#photoprism .img-color-9 { background-color: #738678 !important; } /* Green */
#photoprism .img-color-10 { background-color: #5E716A !important; } /* Lime */
#photoprism .img-color-11 { background-color: #928E85 !important; } /* Yellow */
#photoprism .img-color-12 { background-color: #CC8899 !important; } /* Magenta */
#photoprism .img-color-13 { background-color: #98817B !important; } /* Orange */
#photoprism .img-color-14 { background-color: #CC8899 !important; } /* Red */
#photoprism .img-color-15 { background-color: #AA98A9 !important; } /* Pink */

329
frontend/src/css/search.css Normal file
View file

@ -0,0 +1,329 @@
#photoprism .p-col-select {
width: 66px;
}
#photoprism .p-col-primary {
width: 44px;
}
#photoprism .search-results.list-view tr td:first-child {
padding: 0 0 0 8px;
text-align: center;
}
#photoprism .search-results.list-view .p-photo-select,
#photoprism .search-results.list-view .p-photo-play {
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
text-align: center;
}
#photoprism .album-results .action-share {
top: 4px;
left: 4px;
}
#photoprism .label-results .info-count {
position: absolute;
left: 12px;
top: 12px;
}
#photoprism .p-clipboard.--ltr {
right: 8px;
bottom: 12px;
}
#photoprism .p-clipboard.--rtl {
left: 8px;
bottom: 12px;
}
#photoprism .p-clipboard .v-btn.v-btn--disabled:not(.v-btn--icon):not(.v-btn--flat):not(.v-btn--outline) {
background-color: rgba(100, 100, 100, 0.5) !important;
}
#photoprism .live-player video {
width: auto;
height: 100%;
position: absolute;
overflow: hidden;
}
#photoprism .portrait .live-player video,
#photoprism .is-portrait .live-player video {
width: 100%;
height: auto;
}
#photoprism table.photo-files tbody tr td:first-child {
width: 30%;
padding: 0 16px 0 24px;
}
#photoprism .img-placeholder {
opacity: 0.3;
}
#photoprism .img-color-0 {
background-color: #696969 !important;
}
/* Black */
#photoprism .img-color-1 {
background-color: #DCDCDC !important;
}
/* Grey */
#photoprism .img-color-2 {
background-color: #98817B !important;
}
/* Brown */
#photoprism .img-color-3 {
background-color: #E5E4E2 !important;
}
/* Gold */
#photoprism .img-color-4 {
background-color: #fdfdfd !important;
}
/* White */
#photoprism .img-color-5 {
background-color: #AB47BC !important;
}
/* Purple */
#photoprism .img-color-6 {
background-color: #8A7F8D !important;
}
/* Blue */
#photoprism .img-color-7 {
background-color: #91A3B0 !important;
}
/* Cyan */
#photoprism .img-color-8 {
background-color: #B2BEB5 !important;
}
/* Teal */
#photoprism .img-color-9 {
background-color: #738678 !important;
}
/* Green */
#photoprism .img-color-10 {
background-color: #5E716A !important;
}
/* Lime */
#photoprism .img-color-11 {
background-color: #928E85 !important;
}
/* Yellow */
#photoprism .img-color-12 {
background-color: #CC8899 !important;
}
/* Magenta */
#photoprism .img-color-13 {
background-color: #98817B !important;
}
/* Orange */
#photoprism .img-color-14 {
background-color: #CC8899 !important;
}
/* Red */
#photoprism .img-color-15 {
background-color: #AA98A9 !important;
}
/* Pink */
#photoprism .default-hidden {
display: none;
}
#photoprism .search-results a {
color: #333333;
}
#photoprism .list-view {
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 .cards-view .result,
#photoprism .mosaic-view .result
{
-webkit-transition-duration: 15ms !important;
-moz-transition-duration: 15ms !important;
-o-transition-duration: 15ms !important;
transition-duration: 15ms !important;
margin: 4px !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 .cards-view .result.is-selected,
#photoprism .mosaic-view .result.is-selected {
margin: 0 !important;
box-shadow: 0 6px 6px -3px rgba(0, 0, 0, .2), 0 10px 14px 1px rgba(0, 0, 0, .14), 0 4px 18px 3px rgba(0, 0, 0, .12) !important;
}
#photoprism .cards-view .input-select,
#photoprism .mosaic-view .input-select {
visibility: hidden;
opacity: 0.5;
right: 4px;
bottom: 4px;
}
#photoprism .search-results .result:hover .input-select,
#photoprism .search-results .result.is-selected .input-select {
visibility: visible;
opacity: 1;
}
#photoprism .search-results .result .input-select .select-off,
#photoprism .search-results .result.is-selected .input-select .select-on {
display: inline-flex;
}
#photoprism .search-results .result .input-select .select-on,
#photoprism .search-results .result.is-selected .input-select .select-off {
display: none;
}
#photoprism .cards-view .input-favorite,
#photoprism .mosaic-view .input-favorite {
opacity: 0.5;
left: 4px;
bottom: 4px;
}
#photoprism .search-results .result.is-favorite .input-favorite {
opacity: 0.75;
}
#photoprism .search-results .result .input-favorite .select-off,
#photoprism .search-results .result.is-favorite .input-favorite .select-on {
display: inline-flex;
}
#photoprism .search-results .result .input-favorite .select-on,
#photoprism .search-results .result.is-favorite .input-favorite .select-off {
display: none;
}
#photoprism .cards-view .input-private,
#photoprism .mosaic-view .input-private {
visibility: hidden;
opacity: 0.75;
top: 4px;
right: 4px;
}
#photoprism .search-results .result.is-private .input-private {
visibility: visible;
}
#photoprism .cards-view .input-open,
#photoprism .cards-view .input-view,
#photoprism .mosaic-view .input-open,
#photoprism .mosaic-view .input-view {
display: none;
opacity: 0.75;
top: 4px;
left: 4px;
}
#photoprism .search-results .type-image:hover .input-view,
#photoprism .search-results .type-raw .input-open,
#photoprism .search-results .type-live.is-playable .input-open,
#photoprism .search-results .type-image.is-stack .input-open {
display: inline-flex;
}
#photoprism .search-results .type-image.is-stack .input-view {
display: none;
}
#photoprism .search-results .type-raw .input-open .action-raw,
#photoprism .search-results .type-live.is-playable .input-open .action-live,
#photoprism .search-results .type-image.is-stack .input-open .action-stack {
display: inline-flex;
}
#photoprism .search-results .live-player {
display: none;
}
#photoprism .search-results .type-live.is-playable:hover .live-player {
display: flex;
overflow: hidden !important;
height: 100%;
justify-content: center;
align-items: center;
margin: 0 !important;
}
#photoprism .search-results .result .input-play {
display: none;
}
#photoprism .search-results .type-video.is-playable:hover .input-play {
display: inline-flex;
opacity: 0.75;
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
text-align: center;
}
#photoprism .cards-view .v-card .card-background {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 0;
}
#photoprism .card-details button {
text-align: left;
}
#photoprism .cards-view .v-card .card-details {
z-index: 1;
position: relative;
}
#photoprism .cards-view .v-card.is-selected .card-details,
#photoprism .cards-view .v-card.is-selected .card-background {
filter: invert(1);
}
#photoprism .search-results.list-view .input-select,
#photoprism .search-results.list-view .input-play {
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
text-align: center;
}

View file

@ -33,6 +33,7 @@ import Api from "common/api";
import { DateTime } from "luxon";
import { config } from "../session";
import { $gettext } from "common/vm";
import Clipboard from "../common/clipboard";
export class Album extends RestModel {
getDefaults() {
@ -65,6 +66,16 @@ export class Album extends RestModel {
};
}
classes(selected) {
let classes = ["is-album", "uid-" + this.UID, "type-" + this.Type];
if (this.Favorite) classes.push("is-favorite");
if (this.Private) classes.push("is-private");
if (selected) classes.push("is-selected");
return classes;
}
getEntityName() {
return this.Slug;
}
@ -153,6 +164,10 @@ export class Album extends RestModel {
return Api.delete(this.getEntityResource() + "/like");
}
static pageSize() {
return 24;
}
static getCollectionResource() {
return "albums";
}

View file

@ -76,6 +76,17 @@ export class File extends RestModel {
};
}
classes(selected) {
let classes = ["is-file", "uid-" + this.UID];
if (this.Primary) classes.push("is-primary");
if (this.Sidecar) classes.push("is-sidecar");
if (this.Video) classes.push("is-video");
if (selected) classes.push("is-selected");
return classes;
}
baseName(truncate) {
let result = this.Name;
const slash = result.lastIndexOf("/");

View file

@ -63,6 +63,16 @@ export class Folder extends RestModel {
};
}
classes(selected) {
let classes = ["is-folder", "uid-" + this.UID];
if (this.Favorite) classes.push("is-favorite");
if (this.Private) classes.push("is-private");
if (selected) classes.push("is-selected");
return classes;
}
baseName(truncate) {
let result = this.Path;
const slash = result.lastIndexOf("/");

View file

@ -53,6 +53,15 @@ export class Label extends RestModel {
};
}
classes(selected) {
let classes = ["is-label", "uid-" + this.UID];
if (this.Favorite) classes.push("is-favorite");
if (selected) classes.push("is-selected");
return classes;
}
getEntityName() {
return this.Slug;
}
@ -89,6 +98,10 @@ export class Label extends RestModel {
return Api.delete(this.getEntityResource() + "/like");
}
static pageSize() {
return 24;
}
static getCollectionResource() {
return "labels";
}

View file

@ -54,12 +54,11 @@ export const DayUnknown = -1;
export class Photo extends RestModel {
constructor(values) {
super(values);
this.Selected = Clipboard.has(this);
}
getDefaults() {
return {
Selected: false,
ID: "",
UID: "",
DocumentID: "",
Type: TypeImage,
@ -146,6 +145,19 @@ export class Photo extends RestModel {
};
}
classes() {
let classes = ["is-photo", "uid-" + this.UID, "type-" + this.Type];
if (this.isPlayable()) classes.push("is-playable");
if (Clipboard.has(this)) classes.push("is-selected");
if (this.Portrait) classes.push("is-portrait");
if (this.Favorite) classes.push("is-favorite");
if (this.Private) classes.push("is-private");
if (this.Files.length > 1) classes.push("is-stack");
return classes;
}
localDayString() {
if (!this.TakenAtLocal) {
return new Date().getDate().toString().padStart(2, "0");
@ -570,11 +582,14 @@ export class Photo extends RestModel {
}
toggleLike() {
this.Favorite = !this.Favorite;
const favorite = !this.Favorite;
const elements = document.querySelectorAll(`.uid-${this.UID}`);
if (this.Favorite) {
if (favorite) {
elements.forEach((el) => el.classList.add("is-favorite"));
return Api.post(this.getEntityResource() + "/like");
} else {
elements.forEach((el) => el.classList.remove("is-favorite"));
return Api.delete(this.getEntityResource() + "/like");
}
}
@ -707,6 +722,10 @@ export class Photo extends RestModel {
});
}
static pageSize() {
return 120;
}
static getCollectionResource() {
return "photos";
}

View file

@ -77,7 +77,7 @@ export default {
uid: uid,
results: [],
scrollDisabled: true,
pageSize: 60,
pageSize: Photo.pageSize(),
offset: 0,
page: 0,
selection: this.$clipboard.selection,
@ -444,16 +444,22 @@ export default {
}
}
},
updateResult(results, values) {
const model = results.find((m) => m.UID === values.UID);
if (model) {
for (let key in values) {
if (values.hasOwnProperty(key) && values[key] != null && typeof values[key] !== "object") {
model[key] = values[key];
updateResults(entity) {
this.results.filter((m) => m.UID === entity.UID).forEach((m) => {
for (let key in entity) {
if (key !== "UID" && entity.hasOwnProperty(key) && entity[key] != null && typeof entity[key] !== "object") {
m[key] = entity[key];
}
}
}
});
this.viewer.results.filter((m) => m.UID === entity.UID).forEach((m) => {
for (let key in entity) {
if (key !== "UID" && entity.hasOwnProperty(key) && entity[key] != null && typeof entity[key] !== "object") {
m[key] = entity[key];
}
}
});
},
removeResult(results, uid) {
const index = results.findIndex((m) => m.UID === uid);
@ -474,9 +480,7 @@ export default {
switch (type) {
case 'updated':
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
this.updateResult(this.results, values);
this.updateResult(this.viewer.results, values);
this.updateResults(data.entities[i]);
}
break;
case 'restored':
@ -502,6 +506,9 @@ export default {
break;
}
// TODO: Needed?
this.$forceUpdate();
},
},
};

View file

@ -54,8 +54,8 @@
<p-album-clipboard :refresh="refresh" :selection="selection" :share="share" :edit="edit"
:clear-selection="clearSelection" :context="context"></p-album-clipboard>
<v-container grid-list-xs fluid class="pa-2 p-albums p-albums-cards">
<v-card v-if="results.length === 0" class="p-albums-empty secondary-light lighten-1 ma-1" flat>
<v-container grid-list-xs fluid class="pa-2">
<v-card v-if="results.length === 0" class="no-results secondary-light lighten-1 ma-1" flat>
<v-card-title primary-title>
<div v-if="staticFilter.type === 'album'">
<h3 class="title ma-0 pa-0">
@ -77,107 +77,101 @@
</div>
</v-card-title>
</v-card>
<v-layout row wrap class="p-album-results">
<v-layout row wrap class="search-results album-results cards-view">
<v-flex
v-for="(album, index) in results"
:key="index"
:data-uid="album.UID"
class="p-album"
xs6 sm4 md3 lg2 xxl1 d-flex
>
<v-hover>
<v-card slot-scope="{ hover }" tile
class="accent lighten-3"
:dark="selection.includes(album.UID)"
:class="selection.includes(album.UID) ? 'elevation-10 ma-0 accent darken-1 white--text' : 'elevation-0 ma-1 accent lighten-3'"
:to="{name: view, params: {uid: album.UID, slug: album.Slug, year: album.Year, month: album.Month}}"
@contextmenu="onContextMenu($event, index)"
<v-card tile
:data-uid="album.UID"
class="result accent lighten-2"
:class="album.classes(selection.includes(album.UID))"
:to="{name: view, params: {uid: album.UID, slug: album.Slug, year: album.Year, month: album.Month}}"
@contextmenu="onContextMenu($event, index)"
>
<div class="card-background accent lighten-2"></div>
<v-img
:src="album.thumbnailUrl('tile_500')"
:alt="album.Title"
:transition="false"
aspect-ratio="1"
class="accent lighten-3 clickable"
@mousedown="onMouseDown($event, index)"
@click="onClick($event, index)"
>
<v-img
:src="album.thumbnailUrl('tile_500')"
aspect-ratio="1"
class="accent lighten-2"
@mousedown="onMouseDown($event, index)"
@click="onClick($event, index)"
>
<v-btn v-if="featureShare && album.LinkCount > 0" :ripple="false"
icon large absolute
class="action-share"
@click.stop.prevent="share(album)">
<v-icon color="white">share</v-icon>
</v-btn>
<v-btn v-if="featureShare && album.LinkCount > 0" :ripple="false"
icon flat absolute
class="action-share"
@click.stop.prevent="share(album)">
<v-icon color="white">share</v-icon>
</v-btn>
<v-btn v-if="hover || selection.includes(album.UID)" :flat="!hover" :ripple="false"
icon large absolute
:class="selection.includes(album.UID) ? 'action-select' : 'action-select opacity-50'"
@click.stop.prevent="onSelect($event, index)">
<v-icon v-if="selection.includes(album.UID)" color="white"
class="t-select t-on">check_circle
</v-icon>
<v-icon v-else color="accent lighten-3" class="t-select t-off">
radio_button_off
</v-icon>
</v-btn>
</v-img>
<v-btn :ripple="false"
icon flat absolute
class="input-select"
@click.stop.prevent="onSelect($event, index)">
<v-icon color="white" class="select-on">check_circle</v-icon>
<v-icon color="accent lighten-3" class="select-off">radio_button_off</v-icon>
</v-btn>
<v-card-actions primary-title class="pl-3 pr-2 pb-0 mb-0" style="user-select: none;">
<h3 v-if="album.Type !== 'month'"
class="body-2 ma-0 action-title-edit"
:data-uid="album.UID"
@click.stop.prevent="edit(album)">
{{ album.Title }}
<v-btn :ripple="false"
icon flat absolute
class="input-favorite"
@click.stop.prevent="album.toggleLike()">
<v-icon color="#FFD600" class="select-on">star</v-icon>
<v-icon color="white" class="select-off">star_border</v-icon>
</v-btn>
</v-img>
<v-card-title primary-title class="pl-3 pt-3 pr-3 pb-2 card-details" style="user-select: none;">
<div>
<h3 class="body-2 mb-0">
<button v-if="album.Type !== 'month'" class="action-title-edit" :data-uid="album.UID"
@click.stop.prevent="edit(album)">
{{ album.Title | truncate(80) }}
</button>
<button v-else class="action-title-edit" :data-uid="album.UID"
@click.stop.prevent="edit(album)">
{{ album.getDateString() | capitalize }}
</button>
</h3>
</div>
</v-card-title>
<h3 v-else
class="body-2 ma-0 action-title-edit"
:data-uid="album.UID"
@click.stop.prevent="edit(album)">
{{ album.getDateString() | capitalize }}
</h3>
<v-card-text primary-title class="pb-2 pt-0 card-details" style="user-select: none;"
@click.stop.prevent="">
<div v-if="album.Description" class="caption mb-2" :title="$gettext('Description')">
<button @click.exact="edit(album)">
{{ album.Description | truncate(100) }}
</button>
</div>
<v-spacer></v-spacer>
<div v-else-if="album.Type === 'album'" class="caption mb-2">
<button v-if="album.PhotoCount === 1" @click.exact="edit(album)">
<translate>Contains one entry.</translate>
</button>
<button v-else-if="album.PhotoCount > 0">
<translate :translate-params="{n: album.PhotoCount}">Contains %{n} entries.</translate>
</button>
<button v-else @click.stop.prevent="$router.push({name: 'photos'})">
<translate>Add photos or videos from search results by selecting them.</translate>
</button>
</div>
<div v-else-if="album.Type === 'folder'" class="caption mb-2">
<button @click.exact="edit(album)">
/{{ album.Path | truncate(100) }}
</button>
</div>
<v-btn icon @click.stop.prevent="album.toggleLike()">
<v-icon v-if="album.Favorite" color="#FFD600">star
</v-icon>
<v-icon v-else color="accent lighten-2">star</v-icon>
</v-btn>
</v-card-actions>
<v-card-text primary-title class="pb-2 pt-0 p-photo-desc" style="user-select: none;"
@click.stop.prevent="">
<div v-if="album.Description" class="caption mb-2">
<button @click.exact="edit(album)">
{{ album.Description | truncate(100) }}
</button>
</div>
<div v-else-if="album.Type === 'album'" class="caption mb-2">
<button v-if="album.PhotoCount === 1" @click.exact="edit(album)">
<translate>Contains one entry.</translate>
</button>
<button v-else-if="album.PhotoCount > 0">
<translate :translate-params="{n: album.PhotoCount}">Contains %{n} entries.</translate>
</button>
<button v-else @click.stop.prevent="$router.push({name: 'photos'})">
<translate>Add photos or videos from search results by selecting them.</translate>
</button>
</div>
<div v-else-if="album.Type === 'folder'" class="caption mb-2">
<button @click.exact="edit(album)">
/{{ album.Path | truncate(100) }}
</button>
</div>
<div v-if="album.Location" class="caption mb-2 d-block">
<button @click.exact="edit(album)">
<v-icon size="14">location_on</v-icon>
{{ album.Location }}
</button>
</div>
</v-card-text>
</v-card>
</v-hover>
<div v-if="album.Location" class="caption mb-2 d-block">
<button @click.exact="edit(album)">
<v-icon size="14">location_on</v-icon>
{{ album.Location }}
</button>
</div>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
@ -229,7 +223,7 @@ export default {
results: [],
loading: true,
scrollDisabled: true,
pageSize: 24,
pageSize: Album.pageSize(),
offset: 0,
page: 0,
selection: [],

View file

@ -41,8 +41,8 @@
<p-scroll-top></p-scroll-top>
<v-container grid-list-xs fluid class="pa-2 p-labels p-labels-cards">
<v-card v-if="results.length === 0" class="p-labels-empty secondary-light lighten-1 ma-1" flat>
<v-container grid-list-xs fluid class="pa-2">
<v-card v-if="results.length === 0" class="no-results secondary-light lighten-1 ma-1" flat>
<v-card-title primary-title>
<div>
<h3 class="title ma-0 pa-0">
@ -54,71 +54,72 @@
</div>
</v-card-title>
</v-card>
<v-layout row wrap class="p-label-results">
<v-layout row wrap class="search-results label-results cards-view">
<v-flex
v-for="(label, index) in results"
:key="index"
class="p-label"
:data-uid="label.UID"
xs6 sm4 md3 lg2 xxl1 d-flex
>
<v-hover>
<v-card slot-scope="{ hover }" tile
class="accent lighten-3"
:dark="selection.includes(label.UID)"
:class="selection.includes(label.UID) ? 'elevation-10 ma-0 accent darken-1 white--text' : 'elevation-0 ma-1 accent lighten-3'"
:to="{name: 'browse', query: {q: 'label:' + (label.CustomSlug ? label.CustomSlug : label.Slug)}}"
@contextmenu="onContextMenu($event, index)">
<v-img
:src="label.thumbnailUrl('tile_500')"
aspect-ratio="1"
class="accent lighten-2"
@mousedown="onMouseDown($event, index)"
@click="onClick($event, index)"
>
<v-btn v-if="hover || selection.includes(label.UID)" :flat="!hover" :ripple="false"
icon large absolute
:class="selection.includes(label.UID) ? 'p-label-select' : 'p-label-select opacity-50'"
@click.stop.prevent="onSelect($event, index)">
<v-icon v-if="selection.includes(label.UID)" color="white" class="t-select t-on">check_circle
</v-icon>
<v-icon v-else color="accent lighten-3" class="t-select t-off">radio_button_off</v-icon>
</v-btn>
</v-img>
<v-card tile
:data-uid="label.UID"
class="result accent lighten-2"
:class="label.classes(selection.includes(label.UID))"
:to="{name: 'browse', query: {q: 'label:' + (label.CustomSlug ? label.CustomSlug : label.Slug)}}"
@contextmenu="onContextMenu($event, index)"
>
<div class="card-background accent lighten-2"></div>
<v-img
:src="label.thumbnailUrl('tile_500')"
:alt="label.Name"
:transition="false"
aspect-ratio="1"
class="accent lighten-3 clickable"
@mousedown="onMouseDown($event, index)"
@click="onClick($event, index)"
>
<v-btn :ripple="false"
icon flat absolute
class="input-select"
@click.stop.prevent="onSelect($event, index)">
<v-icon color="white" class="select-on">check_circle</v-icon>
<v-icon color="accent lighten-3" class="select-off">radio_button_off</v-icon>
</v-btn>
<v-card-actions @click.stop.prevent="">
<v-edit-dialog
:return-value.sync="label.Name"
lazy
class="p-inline-edit"
@save="onSave(label)"
>
<span v-if="label.Name" class="body-2 ma-0">
{{ label.Name | capitalize }}
</span>
<span v-else>
<v-icon>edit</v-icon>
</span>
<template #input>
<v-text-field
v-model="label.Name"
:rules="[titleRule]"
:label="$gettext('Label Name')"
color="secondary-dark"
single-line
autofocus
></v-text-field>
</template>
</v-edit-dialog>
<v-spacer></v-spacer>
<v-btn icon @click.stop.prevent="label.toggleLike()">
<v-icon v-if="label.Favorite" color="#FFD600">star
</v-icon>
<v-icon v-else color="accent lighten-2">star</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</v-hover>
<v-btn :ripple="false"
icon flat absolute
class="input-favorite"
@click.stop.prevent="label.toggleLike()">
<v-icon color="#FFD600" class="select-on">star</v-icon>
<v-icon color="white" class="select-off">star_border</v-icon>
</v-btn>
</v-img>
<v-card-title primary-title class="pa-3 card-details" style="user-select: none;" @click.stop.prevent="">
<v-edit-dialog
:return-value.sync="label.Name"
lazy
class="inline-edit"
@save="onSave(label)"
>
<span v-if="label.Name" class="body-2 ma-0">
{{ label.Name | capitalize }}
</span>
<span v-else>
<v-icon>edit</v-icon>
</span>
<template #input>
<v-text-field
v-model="label.Name"
:rules="[titleRule]"
:label="$gettext('Label Name')"
color="secondary-dark"
single-line
autofocus
></v-text-field>
</template>
</v-edit-dialog>
</v-card-title>
</v-card>
</v-flex>
</v-layout>
</v-container>
@ -154,7 +155,7 @@ export default {
results: [],
scrollDisabled: true,
loading: true,
pageSize: 24,
pageSize: Label.pageSize(),
offset: 0,
page: 0,
selection: [],
@ -459,9 +460,11 @@ export default {
const values = data.entities[i];
const model = this.results.find((m) => m.UID === values.UID);
for (let key in values) {
if (values.hasOwnProperty(key)) {
model[key] = values[key];
if (model) {
for (let key in values) {
if (values.hasOwnProperty(key) && values[key] != null && typeof values[key] !== "object") {
model[key] = values[key];
}
}
}
}

View file

@ -31,7 +31,7 @@
<p-scroll-top></p-scroll-top>
<v-container grid-list-xs fluid class="pa-2 p-files p-files-cards">
<v-card v-if="results.length === 0" class="p-files-empty secondary-light lighten-1 ma-1" flat>
<v-card v-if="results.length === 0" class="no-results secondary-light lighten-1 ma-1" flat>
<v-card-title primary-title>
<div>
<h3 class="title ma-0 pa-0">
@ -44,64 +44,63 @@
</div>
</v-card-title>
</v-card>
<v-layout row wrap class="p-files-results">
<v-layout row wrap class="search-results file-results cards-view">
<v-flex
v-for="(model, index) in results"
:key="index"
:data-uid="model.UID"
class="p-file"
xs6 sm4 md3 lg2 xxl1 d-flex
>
<v-hover>
<v-card slot-scope="{ hover }" tile
class="accent lighten-3 clickable"
:dark="selection.includes(model.UID)"
:class="selection.includes(model.UID) ? 'elevation-10 ma-0 darken-1 white--text' : 'elevation-0 ma-1 lighten-3'"
@contextmenu="onContextMenu($event, index)">
<v-img
:src="model.thumbnailUrl('tile_500')"
aspect-ratio="1"
class="accent lighten-2"
@mousedown="onMouseDown($event, index)"
@click="onClick($event, index)"
>
<v-btn v-if="hover || selection.includes(model.UID)" :flat="!hover" :ripple="false"
icon large absolute
:class="selection.includes(model.UID) ? 'p-file-select' : 'p-file-select opacity-50'"
@click.stop.prevent="onSelect($event, index)">
<v-icon v-if="selection.includes(model.UID)" color="white" class="t-select t-on">check_circle
</v-icon>
<v-icon v-else color="accent lighten-3" class="t-select t-off">radio_button_off</v-icon>
</v-btn>
</v-img>
<v-card tile
:data-uid="model.UID"
class="result accent lighten-2"
:class="model.classes(selection.includes(model.UID))"
@contextmenu="onContextMenu($event, index)"
>
<div class="card-background accent lighten-2"></div>
<v-img
:src="model.thumbnailUrl('tile_500')"
:alt="model.Name"
:transition="false"
aspect-ratio="1"
class="accent lighten-3 clickable"
@mousedown="onMouseDown($event, index)"
@click="onClick($event, index)"
>
<v-btn :ripple="false"
icon flat absolute
class="input-select"
@click.stop.prevent="onSelect($event, index)">
<v-icon color="white" class="select-on">check_circle</v-icon>
<v-icon color="accent lighten-3" class="select-off">radio_button_off</v-icon>
</v-btn>
</v-img>
<v-card-title v-if="model.isFile()" primary-title class="pa-3 p-photo-desc"
style="user-select: none;">
<div>
<h3 class="body-2 mb-2" :title="model.Name">
<button @click.exact="openFile(index)">
{{ model.baseName() }}
</button>
</h3>
<div class="caption" title="Info">
{{ model.getInfo() }}
</div>
<v-card-title v-if="model.isFile()" primary-title class="pa-3 card-details"
style="user-select: none;">
<div>
<h3 class="body-2 mb-2" :title="model.Name">
<button @click.exact="openFile(index)">
{{ model.baseName() }}
</button>
</h3>
<div class="caption" title="Info">
{{ model.getInfo() }}
</div>
</v-card-title>
<v-card-title v-else primary-title class="pa-3 p-photo-desc">
<div>
<h3 class="body-2 mb-2" :title="model.Title">
<button @click.exact="openFile(index)">
{{ model.baseName() }}
</button>
</h3>
<div class="caption" title="Path">
<translate key="Folder">Folder</translate>
</div>
</div>
</v-card-title>
<v-card-title v-else primary-title class="pa-3 card-details">
<div>
<h3 class="body-2 mb-2" :title="model.Title">
<button @click.exact="openFile(index)">
{{ model.baseName() }}
</button>
</h3>
<div class="caption" title="Path">
<translate key="Folder">Folder</translate>
</div>
</v-card-title>
</v-card>
</v-hover>
</div>
</v-card-title>
</v-card>
</v-flex>
</v-layout>
</v-container>

View file

@ -92,7 +92,7 @@ export default {
complete: false,
results: [],
scrollDisabled: true,
pageSize: 60,
pageSize: Photo.pageSize(),
offset: 0,
page: 0,
selection: this.$clipboard.selection,
@ -438,16 +438,22 @@ export default {
this.loadMore();
},
updateResult(results, values) {
const model = results.find((m) => m.UID === values.UID);
if (model) {
for (let key in values) {
if (values.hasOwnProperty(key) && values[key] != null && typeof values[key] !== "object") {
model[key] = values[key];
updateResults(entity) {
this.results.filter((m) => m.UID === entity.UID).forEach((m) => {
for (let key in entity) {
if (key !== "UID" && entity.hasOwnProperty(key) && entity[key] != null && typeof entity[key] !== "object") {
m[key] = entity[key];
}
}
}
});
this.viewer.results.filter((m) => m.UID === entity.UID).forEach((m) => {
for (let key in entity) {
if (key !== "UID" && entity.hasOwnProperty(key) && entity[key] != null && typeof entity[key] !== "object") {
m[key] = entity[key];
}
}
});
},
removeResult(results, uid) {
const index = results.findIndex((m) => m.UID === uid);
@ -475,8 +481,7 @@ export default {
this.removeResult(this.viewer.results, values.UID);
this.$clipboard.removeId(values.UID);
} else {
this.updateResult(this.results, values);
this.updateResult(this.viewer.results, values);
this.updateResults(values);
}
}
break;
@ -531,6 +536,9 @@ export default {
default:
console.warn("unexpected event type", ev);
}
// TODO: Needed?
this.$forceUpdate();
},
},
};

View file

@ -15,10 +15,10 @@
<p-album-clipboard :refresh="refresh" :selection="selection"
:clear-selection="clearSelection" :context="context"></p-album-clipboard>
<v-container grid-list-xs fluid class="pa-2 p-albums p-albums-cards">
<v-card v-if="results.length === 0" class="p-albums-empty secondary-light lighten-1 ma-1" flat>
<v-container grid-list-xs fluid class="pa-2">
<v-card v-if="results.length === 0" class="no-results secondary-light lighten-1 ma-1" flat>
<v-card-title primary-title>
<div>
<div v-if="staticFilter.type === 'album'">
<h3 class="title ma-0 pa-0">
<translate>Couldn't find anything</translate>
</h3>
@ -28,57 +28,64 @@
</div>
</v-card-title>
</v-card>
<v-layout row wrap class="p-album-results">
<v-layout row wrap class="search-results album-results cards-view">
<v-flex
v-for="(album, index) in results"
:key="index"
:data-uid="album.UID"
class="p-album"
xs6 sm4 md3 lg2 xxl1 d-flex
>
<v-hover>
<v-card slot-scope="{ hover }" tile
class="accent lighten-3"
:dark="selection.includes(album.UID)"
:class="selection.includes(album.UID) ? 'elevation-10 ma-0 accent darken-1 white--text' : 'elevation-0 ma-1 accent lighten-3'"
:to="{name: view, params: {uid: album.UID, slug: album.Slug, year: album.Year, month: album.Month}}"
@contextmenu="onContextMenu($event, index)"
<v-card tile
:data-uid="album.UID"
class="result accent lighten-2"
:class="album.classes(selection.includes(album.UID))"
:to="{name: view, params: {uid: album.UID, slug: album.Slug, year: album.Year, month: album.Month}}"
@contextmenu="onContextMenu($event, index)"
>
<div class="card-background accent lighten-2"></div>
<v-img
:src="album.thumbnailUrl('tile_500')"
:alt="album.Title"
:transition="false"
aspect-ratio="1"
class="accent lighten-3 clickable"
@mousedown="onMouseDown($event, index)"
@click="onClick($event, index)"
>
<v-img
:src="album.thumbnailUrl('tile_500')"
aspect-ratio="1"
class="accent lighten-2"
@mousedown="onMouseDown($event, index)"
@click="onClick($event, index)"
>
<v-btn v-if="hover || selection.includes(album.UID)" :flat="!hover" :ripple="false"
icon large absolute
:class="selection.includes(album.UID) ? 'action-select' : 'action-select opacity-50'"
@click.stop.prevent="onSelect($event, index)">
<v-icon v-if="selection.includes(album.UID)" color="white"
class="t-select t-on">check_circle
</v-icon>
<v-icon v-else color="accent lighten-3" class="t-select t-off">
radio_button_off
</v-icon>
</v-btn>
</v-img>
<v-btn :ripple="false"
icon flat absolute
class="input-select"
@click.stop.prevent="onSelect($event, index)">
<v-icon color="white" class="select-on">check_circle</v-icon>
<v-icon color="accent lighten-3" class="select-off">radio_button_off</v-icon>
</v-btn>
</v-img>
<v-card-title primary-title class="pa-3 p-album-desc" style="user-select: none;">
<h3 class="body-2 ma-0">
{{ album.Title }}
<v-card-title primary-title class="pl-3 pt-3 pr-3 pb-2 card-details" style="user-select: none;">
<div>
<h3 v-if="album.Type !== 'month'" class="body-2 mb-0" :title="album.Title">
{{ album.Title | truncate(80) }}
</h3>
</v-card-title>
<v-card-text class="pl-3 pr-3 pt-0 pb-3 p-album-desc">
<div v-if="album.Description" class="caption" title="Description">
{{ album.Description | truncate(100) }}
</div>
<div v-else class="caption" title="Description">
<translate>Shared with you.</translate>
</div>
</v-card-text>
</v-card>
</v-hover>
<h3 v-else class="body-2 mb-0">
{{ album.getDateString() | capitalize }}
</h3>
</div>
</v-card-title>
<v-card-text class="pb-2 pt-0 card-details">
<div v-if="album.Description" class="caption mb-2" :title="$gettext('Description')">
{{ album.Description }}
</div>
<div v-else class="caption mb-2">
<translate>Shared with you.</translate>
</div>
<div v-if="album.Location" class="caption mb-2 d-block">
<button @click.stop="">
<v-icon size="14">location_on</v-icon>
{{ album.Location }}
</button>
</div>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
@ -110,8 +117,8 @@ export default {
let categories = [{"value": "", "text": this.$gettext("All Categories")}];
if (this.$config.values.albumCategories) {
categories = categories.concat(this.$config.values.albumCategories.map(cat => {
if (this.$config.albumCategories().length > 0) {
categories = categories.concat(this.$config.albumCategories().map(cat => {
return {"value": cat, "text": cat};
}));
}
@ -125,7 +132,7 @@ export default {
results: [],
loading: true,
scrollDisabled: true,
pageSize: 24,
pageSize: Album.pageSize(),
offset: 0,
page: 0,
selection: [],
@ -139,6 +146,7 @@ export default {
timeStamp: -1,
},
lastId: "",
model: new Album(),
};
},
computed: {
@ -402,7 +410,7 @@ export default {
title = `${title} (${existing.length + 1})`;
}
const album = new Album({"Title": title, "Favorite": true});
const album = new Album({"Title": title, "Favorite": false});
album.save();
},
@ -473,6 +481,17 @@ export default {
}
}
}
let categories = [{"value": "", "text": this.$gettext("All Categories")}];
if (this.$config.albumCategories().length > 0) {
categories = categories.concat(this.$config.albumCategories().map(cat => {
return {"value": cat, "text": cat};
}));
}
this.categories = categories;
break;
case 'deleted':
this.dirty = true;

View file

@ -1,6 +1,6 @@
<template>
<v-container grid-list-xs fluid class="pa-2 p-photos p-photo-cards">
<v-card v-if="photos.length === 0" class="p-photos-empty secondary-light lighten-1 ma-1" flat>
<v-card v-if="photos.length === 0" class="no-results secondary-light lighten-1 ma-1" flat>
<v-card-title primary-title>
<div>
<h3 v-if="filter.order === 'edited'" class="title ma-0 pa-0">
@ -15,132 +15,110 @@
</div>
</v-card-title>
</v-card>
<v-layout row wrap class="p-results">
<v-layout row wrap class="search-results photo-results cards-view">
<v-flex
v-for="(photo, index) in photos"
:key="index"
:data-uid="photo.UID"
class="p-photo"
xs12 sm6 md4 lg3 xlg2 xxxl1 d-flex
:class="{ 'is-selected': photo.Selected, portrait: photo.Portrait }"
>
<v-hover>
<v-card slot-scope="{ hover }" tile
:dark="photo.Selected"
:class="photo.Selected ? 'selected elevation-10 ma-0 accent darken-1 white--text select-transition' : 'elevation-0 ma-1 accent lighten-3 select-transition'"
@contextmenu="onContextMenu($event, index)">
<v-img :src="photo.thumbnailUrl('tile_500')"
aspect-ratio="1"
class="accent lighten-2 clickable"
@mousedown="onMouseDown($event, index)"
@click.stop.prevent="onClick($event, index)"
>
<v-layout
v-if="photo.Type === 'live'"
v-show="hover"
fill-height
align-center
justify-center
ma-0
class="live-player"
style="overflow: hidden;"
>
<video :key="photo.videoUrl()" width="500" height="500" autoplay loop muted playsinline>
<source :src="photo.videoUrl()" type="video/mp4">
</video>
</v-layout>
<v-card tile
:data-id="photo.ID"
:data-uid="photo.UID"
class="result accent lighten-2"
:class="photo.classes()"
@contextmenu="onContextMenu($event, index)">
<div class="card-background accent lighten-2"></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-3 clickable"
@mousedown="onMouseDown($event, index)"
@click.stop.prevent="onClick($event, index)"
>
<v-layout v-if="photo.Type === 'video' || photo.Type === 'live'" class="live-player">
<video :key="photo.ID" width="500" height="500" autoplay loop muted playsinline>
<source :src="photo.videoUrl()" type="video/mp4">
</video>
</v-layout>
<v-btn v-if="hidePrivate && photo.Private" :ripple="false"
icon flat large absolute
class="p-photo-private opacity-75">
<v-icon color="white">lock</v-icon>
</v-btn>
<v-btn :ripple="false" :depressed="false" class="input-open"
icon flat absolute
@click.stop.prevent="openPhoto(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')">adjust</v-icon>
<v-icon color="white" class="default-hidden action-stack" :title="$gettext('Stack')">burst_mode</v-icon>
</v-btn>
<v-btn v-if="hover || photo.Selected" :ripple="false"
icon flat large absolute
:class="photo.Selected ? 'p-photo-select' : 'p-photo-select opacity-50'"
@click.stop.prevent="onSelect($event, index)">
<v-icon v-if="photo.Selected" color="white"
class="t-select t-on">check_circle
</v-icon>
<v-icon v-else color="accent lighten-3" class="t-select t-off">radio_button_off</v-icon>
</v-btn>
<v-btn :ripple="false" :depressed="false" class="input-view"
icon flat absolute :title="$gettext('View')"
@click.stop.prevent="openPhoto(index, false)">
<v-icon color="white" class="action-fullscreen">zoom_in</v-icon>
</v-btn>
<!-- v-btn icon flat large absolute :ripple="false"
:class="photo.Favorite ? 'p-photo-like opacity-75' : 'p-photo-like opacity-50'"
@click.stop.prevent="photo.toggleLike()">
<v-icon v-if="photo.Favorite" color="white" class="t-like t-on" :data-uid="photo.UID">
favorite
</v-icon>
<v-icon v-else color="accent lighten-3" class="t-like t-off" :data-uid="photo.UID">
favorite_border
</v-icon>
</v-btn -->
<v-btn :ripple="false" :depressed="false" color="white" class="input-play"
outline fab absolute :title="$gettext('Play')"
@click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-play">play_arrow</v-icon>
</v-btn>
<template v-if="photo.isPlayable()">
<v-btn v-if="photo.Type === 'live'" :ripple="false"
icon flat large absolute class="p-photo-live opacity-75"
title="Live Photo" @click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-play">adjust</v-icon>
</v-btn>
<v-btn v-else color="white" :ripple="false"
outline large fab absolute class="p-photo-play opacity-75" :depressed="false"
title="Play" @click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-play">play_arrow</v-icon>
</v-btn>
</template>
<v-btn v-else-if="photo.Type === 'image' && photo.Files.length > 1" :ripple="false"
icon flat large absolute class="p-photo-merged opacity-75"
@click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-burst">burst_mode</v-icon>
</v-btn>
<v-btn v-else-if="photo.Type === 'image' && selectMode && hover" :ripple="false"
icon flat large absolute class="p-photo-merged opacity-75"
@click.stop.prevent="openPhoto(index, false)">
<v-icon color="white" class="action-open">zoom_in</v-icon>
</v-btn>
<v-btn v-else-if="photo.Type === 'raw'" :ripple="false"
icon flat large absolute class="p-photo-merged opacity-75"
title="RAW" @click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-burst">photo_camera</v-icon>
</v-btn>
</v-img>
<v-btn :ripple="false"
icon flat absolute
class="input-select"
@click.stop.prevent="onSelect($event, index)">
<v-icon color="white" class="select-on">check_circle</v-icon>
<v-icon color="accent lighten-3" class="select-off">radio_button_off</v-icon>
</v-btn>
</v-img>
<v-card-title primary-title class="pa-3 p-photo-desc" style="user-select: none;"
@click.stop.prevent="openPhoto(index, false)">
<div>
<h3 class="body-2 mb-2" :title="photo.Title">
<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) }}
</h3>
<div v-if="photo.Description" class="caption mb-2" title="Description">
</div>
</h3>
<div v-if="photo.Description" class="caption mb-2" :title="labels.description">
<div>
{{ photo.Description }}
</div>
<div class="caption">
<v-icon size="14" title="Taken">date_range</v-icon>
{{ photo.getDateString() }}
<template v-if="!photo.Description">
<br/>
<div v-if="photo.Type === 'video'" title="Video"
@click.exact="openPhoto(index, true)">
<v-icon size="14">movie</v-icon>
{{ photo.getVideoInfo() }}
</div>
<div v-else title="Camera">
<v-icon size="14">photo_camera</v-icon>
{{ photo.getPhotoInfo() }}
</div>
</template>
<template v-if="showLocation && photo.Country !== 'zz'">
<div title="Location">
<v-icon size="14">location_on</v-icon>
{{ photo.locationInfo() }}
</div>
</template>
</div>
</div>
</v-card-title>
</v-card>
</v-hover>
<div class="caption">
<div>
<v-icon size="14" :title="labels.taken">date_range</v-icon>
{{ photo.getDateString() }}
</div>
<template v-if="!photo.Description">
<br/>
<div v-if="photo.Type === 'video'" :title="labels.video">
<v-icon size="14">movie</v-icon>
{{ photo.getVideoInfo() }}
</div>
<div v-else :title="labels.camera">
<v-icon size="14">photo_camera</v-icon>
{{ photo.getPhotoInfo() }}
</div>
</template>
<template v-if="filter.order === 'name' && $config.feature('download')">
<br/>
<div :title="labels.name">
<v-icon size="14">insert_drive_file</v-icon>
{{ photo.baseName() }}
</div>
</template>
<template v-if="showLocation && photo.Country !== 'zz'">
<br/>
<div :title="labels.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>
@ -155,6 +133,7 @@ export default {
openLocation: Function,
album: Object,
filter: Object,
context: String,
selectMode: Boolean,
},
data() {
@ -162,6 +141,16 @@ export default {
showLocation: this.$config.settings().features.places,
hidePrivate: this.$config.settings().features.private,
debug: this.$config.get('debug'),
labels: {
location: this.$gettext("Location"),
description: this.$gettext("Description"),
taken: this.$gettext("Taken"),
approve: this.$gettext("Approve"),
archive: this.$gettext("Archive"),
camera: this.$gettext("Camera"),
video: this.$gettext("Video"),
name: this.$gettext("Name"),
},
mouseDown: {
index: -1,
timeStamp: -1,

View file

@ -1,7 +1,7 @@
<template>
<div>
<div v-if="photos.length === 0" class="pa-2">
<v-card class="p-photos-empty secondary-light lighten-1 ma-1" flat>
<v-card class="no-results secondary-light lighten-1 ma-1" flat>
<v-card-title primary-title>
<div>
<h3 v-if="filter.order === 'edited'" class="title ma-0 pa-0">
@ -22,51 +22,56 @@
:headers="listColumns"
:items="photos"
hide-actions
class="elevation-0 p-photos p-photo-list p-results"
class="search-results photo-results list-view"
disable-initial-sort
item-key="ID"
:no-data-text="notFoundMessage"
>
<template slot="items" slot-scope="props">
<td style="user-select: none;" :data-uid="props.item.UID">
<td style="user-select: none;" :data-uid="props.item.UID" class="result" :class="props.item.classes()">
<v-img class="accent lighten-2 clickable" aspect-ratio="1"
:src="props.item.thumbnailUrl('tile_50')"
@mousedown="onMouseDown($event, props.index)"
@contextmenu="onContextMenu($event, props.index)"
@click.stop.prevent="onClick($event, props.index)"
>
<v-btn v-if="props.item.Selected" :ripple="false"
flat icon large absolute class="p-photo-select">
<v-icon color="white" class="t-select t-on">check_circle</v-icon>
<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="accent lighten-3" class="select-off">radio_button_off</v-icon>
</v-btn>
<v-btn v-else-if="!selectMode && (props.item.Type === 'video' || props.item.Type === 'live')"
<v-btn v-else-if="props.item.Type === 'video' || props.item.Type === 'live'"
:ripple="false"
flat icon large absolute class="p-photo-play opacity-75"
flat icon large absolute class="input-play opacity-75"
@click.stop.prevent="openPhoto(props.index, true)">
<v-icon color="white" class="action-play">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)">
<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()"
style="user-select: none;" @click.stop.prevent="openPhoto(props.index, false)">
{{ props.item.shortDateString() }}
<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;">
{{ props.item.CameraMake }} {{ props.item.CameraModel }}
<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="Name" @click.exact="downloadFile(props.index)">
: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="openPhoto(props.index, false)">
@click.stop.prevent="openLocation(props.index)">
{{ props.item.locationInfo() }}
</button>
<span v-else>
@ -87,6 +92,7 @@ export default {
openLocation: Function,
album: Object,
filter: Object,
context: String,
selectMode: Boolean,
},
data() {
@ -97,6 +103,7 @@ export default {
let showName = this.filter.order === 'name';
return {
config: this.$config.values,
notFoundMessage: m,
'selected': [],
'listColumns': [
@ -112,33 +119,14 @@ export default {
},
],
showName: showName,
showLocation: this.$config.settings().features.places,
hidePrivate: this.$config.settings().features.private,
showLocation: this.$config.values.settings.features.places,
hidePrivate: this.$config.values.settings.features.private,
mouseDown: {
index: -1,
timeStamp: -1,
},
};
},
watch: {
photos: function (photos) {
this.selected.splice(0);
for (let i = 0; i < photos.length; i++) {
if (this.$clipboard.has(photos[i])) {
this.selected.push(photos[i]);
}
}
},
selection: function () {
this.refreshSelection();
},
},
mounted: function () {
this.$nextTick(function () {
this.refreshSelection();
});
},
methods: {
downloadFile(index) {
const photo = this.photos[index];
@ -151,7 +139,7 @@ export default {
if (ev.shiftKey) {
this.selectRange(index);
} else {
this.$clipboard.toggle(this.photos[index]);
this.toggle(this.photos[index]);
}
},
onMouseDown(ev, index) {
@ -165,12 +153,12 @@ export default {
if (longClick || ev.shiftKey) {
this.selectRange(index);
} else {
this.$clipboard.toggle(this.photos[index]);
this.toggle(this.photos[index]);
}
} else if (this.photos[index]) {
let photo = this.photos[index];
if (photo.Type === 'video' || photo.Type === 'live') {
if (photo.Type === 'video' && photo.isPlayable()) {
this.openPhoto(index, true);
} else {
this.openPhoto(index, false);
@ -184,18 +172,12 @@ export default {
this.selectRange(index);
}
},
toggle(photo) {
this.$clipboard.toggle(photo);
},
selectRange(index) {
this.$clipboard.addRange(index, this.photos);
},
refreshSelection() {
this.selected.splice(0);
for (let i = 0; i < this.photos.length; i++) {
if (this.$clipboard.has(this.photos[i])) {
this.selected.push(this.photos[i]);
}
}
},
}
};
</script>

View file

@ -1,6 +1,6 @@
<template>
<v-container grid-list-xs fluid class="pa-2 p-photos p-photo-mosaic">
<v-card v-if="photos.length === 0" class="p-photos-empty secondary-light lighten-1 ma-1" flat>
<v-card v-if="photos.length === 0" class="no-results secondary-light lighten-1 ma-1" flat>
<v-card-title primary-title>
<div>
<h3 v-if="filter.order === 'edited'" class="title ma-0 pa-0">
@ -15,94 +15,63 @@
</div>
</v-card-title>
</v-card>
<v-layout row wrap class="p-results">
<v-layout row wrap class="search-results photo-results mosaic-view">
<v-flex
v-for="(photo, index) in photos"
:key="index"
:data-uid="photo.UID"
:class="{ selected: photo.Selected, portrait: photo.Portrait }"
class="p-photo"
xs4 sm3 md2 lg1 d-flex
>
<v-hover>
<v-card slot-scope="{ hover }" tile
:class="photo.Selected ? 'selected elevation-10 ma-0 select-transition' : 'elevation-0 ma-1 select-transition'"
:title="photo.Title"
@contextmenu="onContextMenu($event, index)">
<v-img :src="photo.thumbnailUrl('tile_224')"
aspect-ratio="1"
class="accent lighten-2 clickable"
@mousedown="onMouseDown($event, index)"
@click.stop.prevent="onClick($event, index)"
>
<v-layout
v-if="photo.Type === 'live'"
v-show="hover"
fill-height
align-center
justify-center
ma-0
class="live-player"
style="overflow: hidden;"
>
<video :key="photo.videoUrl()" width="224" height="224" autoplay loop muted playsinline>
<source :src="photo.videoUrl()" type="video/mp4">
</video>
</v-layout>
<v-card tile
:data-id="photo.ID"
:data-uid="photo.UID"
class="result"
:class="photo.classes()"
@contextmenu="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-3 clickable"
@mousedown="onMouseDown($event, index)"
@click.stop.prevent="onClick($event, index)"
>
<v-layout v-if="photo.Type === 'video' || photo.Type === 'live'" class="live-player">
<video :key="photo.ID" width="224" height="224" autoplay loop muted playsinline>
<source :src="photo.videoUrl()" type="video/mp4">
</video>
</v-layout>
<v-btn v-if="hidePrivate && photo.Private" :ripple="false"
icon flat small absolute
class="p-photo-private opacity-75">
<v-icon color="white">lock</v-icon>
</v-btn>
<v-btn :ripple="false" :depressed="false" class="input-open"
icon flat small absolute
@click.stop.prevent="openPhoto(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')">adjust</v-icon>
<v-icon color="white" class="default-hidden action-stack" :title="$gettext('Stack')">burst_mode</v-icon>
</v-btn>
<v-btn v-if="hover || photo.Selected" :ripple="false"
icon flat small absolute
:class="photo.Selected ? 'p-photo-select' : 'p-photo-select opacity-50'"
@click.stop.prevent="onSelect($event, index)">
<v-icon v-if="photo.Selected" color="white"
class="t-select t-on">check_circle
</v-icon>
<v-icon v-else color="accent lighten-3" class="t-select t-off">radio_button_off</v-icon>
</v-btn>
<v-btn :ripple="false" :depressed="false" class="input-view"
icon flat small absolute :title="$gettext('View')"
@click.stop.prevent="openPhoto(index, false)">
<v-icon color="white" class="action-fullscreen">zoom_in</v-icon>
</v-btn>
<!-- v-btn icon flat small absolute :ripple="false"
:class="photo.Favorite ? 'p-photo-like opacity-75' : 'p-photo-like opacity-50'"
@click.stop.prevent="photo.toggleLike()">
<v-icon v-if="photo.Favorite" color="white" class="t-like t-on" :data-uid="photo.UID">favorite</v-icon>
<v-icon v-else color="accent lighten-3" class="t-like t-off" :data-uid="photo.UID">favorite_border</v-icon>
</v-btn -->
<v-btn :ripple="false" :depressed="false" color="white" class="input-play"
outline fab absolute :title="$gettext('Play')"
@click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-play">play_arrow</v-icon>
</v-btn>
<template v-if="photo.isPlayable()">
<v-btn v-if="photo.Type === 'live'" color="white"
icon flat small absolute class="p-photo-live opacity-75" :depressed="false" :ripple="false"
title="Live Photo" @click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-play">adjust</v-icon>
</v-btn>
<v-btn v-else color="white"
outline fab absolute class="p-photo-play opacity-75" :depressed="false" :ripple="false"
title="Play" @click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-play">play_arrow</v-icon>
</v-btn>
</template>
<v-btn v-else-if="photo.Type === 'image' && photo.Files.length > 1" :ripple="false"
icon flat small absolute class="p-photo-merged opacity-75"
@click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-burst">burst_mode</v-icon>
</v-btn>
<v-btn v-else-if="photo.Type === 'image' && selectMode && hover" :ripple="false"
icon flat small absolute class="p-photo-merged opacity-75"
@click.stop.prevent="openPhoto(index, false)">
<v-icon color="white" class="action-open">zoom_in</v-icon>
</v-btn>
<v-btn v-else-if="photo.Type === 'raw'" :ripple="false"
icon flat small absolute class="p-photo-merged opacity-75"
title="RAW" @click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-burst">photo_camera</v-icon>
</v-btn>
</v-img>
</v-card>
</v-hover>
<v-btn :ripple="false"
icon flat small absolute
class="input-select"
@click.stop.prevent="onSelect($event, index)">
<v-icon color="white" class="select-on">check_circle</v-icon>
<v-icon color="accent lighten-3" class="select-off">radio_button_off</v-icon>
</v-btn>
</v-img>
</v-card>
</v-flex>
</v-layout>
</v-container>
@ -116,6 +85,7 @@ export default {
editPhoto: Function,
album: Object,
filter: Object,
context: String,
selectMode: Boolean,
},
data() {
@ -132,13 +102,16 @@ export default {
if (ev.shiftKey) {
this.selectRange(index);
} else {
this.$clipboard.toggle(this.photos[index]);
this.toggle(this.photos[index]);
}
},
onMouseDown(ev, index) {
this.mouseDown.index = index;
this.mouseDown.timeStamp = ev.timeStamp;
},
toggle(photo) {
this.$clipboard.toggle(photo);
},
onClick(ev, index) {
let longClick = (this.mouseDown.index === index && ev.timeStamp - this.mouseDown.timeStamp > 400);
@ -146,7 +119,7 @@ export default {
if (longClick || ev.shiftKey) {
this.selectRange(index);
} else {
this.$clipboard.toggle(this.photos[index]);
this.toggle(this.photos[index]);
}
} else {
this.openPhoto(index, false);

View file

@ -118,7 +118,7 @@ export default {
uid: uid,
results: [],
scrollDisabled: true,
pageSize: 60,
pageSize: Photo.pageSize(),
offset: 0,
page: 0,
selection: this.$clipboard.selection,
@ -498,16 +498,22 @@ export default {
}
}
},
updateResult(results, values) {
const model = results.find((m) => m.UID === values.UID);
if (model) {
for (let key in values) {
if (values.hasOwnProperty(key) && values[key] != null && typeof values[key] !== "object") {
model[key] = values[key];
updateResults(entity) {
this.results.filter((m) => m.UID === entity.UID).forEach((m) => {
for (let key in entity) {
if (key !== "UID" && entity.hasOwnProperty(key) && entity[key] != null && typeof entity[key] !== "object") {
m[key] = entity[key];
}
}
}
});
this.viewer.results.filter((m) => m.UID === entity.UID).forEach((m) => {
for (let key in entity) {
if (key !== "UID" && entity.hasOwnProperty(key) && entity[key] != null && typeof entity[key] !== "object") {
m[key] = entity[key];
}
}
});
},
removeResult(results, uid) {
const index = results.findIndex((m) => m.UID === uid);
@ -528,9 +534,7 @@ export default {
switch (type) {
case 'updated':
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
this.updateResult(this.results, values);
this.updateResult(this.viewer.results, values);
this.updateResults(data.entities[i]);
}
break;
case 'restored':
@ -556,6 +560,9 @@ export default {
break;
}
// TODO: Needed?
this.$forceUpdate();
},
download() {
this.onDownload(`/api/v1/albums/${this.uid}/dl?t=${this.$config.downloadToken()}`);

View file

@ -313,6 +313,7 @@ describe("model/photo", () => {
assert.equal(photo.Favorite, false);
});
/* TODO
it("should toggle like", () => {
const values = {ID: 5, Title: "Crazy Cat", CountryName: "Africa", Favorite: true};
const photo = new Photo(values);
@ -322,6 +323,7 @@ describe("model/photo", () => {
photo.toggleLike();
assert.equal(photo.Favorite, true);
});
*/
it("should get photo defaults", () => {
const values = {ID: 5, UID: "ABC123"};