Search: Improve handling of search query updates in UI components #1995

This commit is contained in:
Michael Mayer 2022-04-16 15:59:47 +02:00
parent ac9fc4108b
commit adb40433a5
18 changed files with 436 additions and 243 deletions

View file

@ -1,7 +1,7 @@
<template> <template>
<v-form ref="form" lazy-validation <v-form ref="form" lazy-validation
dense autocomplete="off" class="p-photo-toolbar p-album-toolbar" accept-charset="UTF-8" dense autocomplete="off" class="p-photo-toolbar p-album-toolbar" accept-charset="UTF-8"
@submit.prevent="updateQuery"> @submit.prevent="updateQuery()">
<v-toolbar flat :dense="$vuetify.breakpoint.smAndDown" class="page-toolbar" color="secondary"> <v-toolbar flat :dense="$vuetify.breakpoint.smAndDown" class="page-toolbar" color="secondary">
<v-toolbar-title :title="album.Title"> <v-toolbar-title :title="album.Title">
{{ album.Title }} {{ album.Title }}
@ -9,7 +9,7 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn icon class="hidden-xs-only action-reload" :title="$gettext('Reload')" @click.stop="refresh"> <v-btn icon class="hidden-xs-only action-reload" :title="$gettext('Reload')" @click.stop="refresh()">
<v-icon>refresh</v-icon> <v-icon>refresh</v-icon>
</v-btn> </v-btn>
@ -23,7 +23,7 @@
</v-btn> </v-btn>
<v-btn v-if="$config.feature('download')" icon class="hidden-xs-only action-download" :title="$gettext('Download')" <v-btn v-if="$config.feature('download')" icon class="hidden-xs-only action-download" :title="$gettext('Download')"
@click.stop="download"> @click.stop="download()">
<v-icon>get_app</v-icon> <v-icon>get_app</v-icon>
</v-btn> </v-btn>
@ -83,12 +83,22 @@ export default {
type: Object, type: Object,
default: () => {}, default: () => {},
}, },
updateFilter: {
type: Function,
default: () => {},
},
updateQuery: {
type: Function,
default: () => {},
},
settings: { settings: {
type: Object, type: Object,
default: () => {}, default: () => {},
}, },
refresh: Function, refresh: {
filterChange: Function, type: Function,
default: () => {},
},
}, },
data() { data() {
const cameras = [{ const cameras = [{
@ -149,24 +159,10 @@ export default {
this.album.update(); this.album.update();
} }
}, },
dropdownChange() {
this.updateQuery();
if (window.innerWidth < 600) {
this.searchExpanded = false;
}
if (this.filter.order !== this.album.Order) {
this.album.Order = this.filter.order;
this.updateAlbum();
}
},
setView(name) { setView(name) {
this.settings.view = name; if (name) {
this.updateQuery(); this.refresh({'view': name});
}, }
updateQuery() {
this.filterChange();
}, },
download() { download() {
this.onDownload(`${this.$config.apiUri}/albums/${this.album.UID}/dl?t=${this.$config.downloadToken()}`); this.onDownload(`${this.$config.apiUri}/albums/${this.album.UID}/dl?t=${this.$config.downloadToken()}`);

View file

@ -207,21 +207,15 @@ export default {
}, },
openPhoto: { openPhoto: {
type: Function, type: Function,
default: () => () => { default: () => {},
console.warn('cards view: openPhoto is undefined');
},
}, },
editPhoto: { editPhoto: {
type: Function, type: Function,
default: () => () => { default: () => {},
console.warn('cards view: editPhoto is undefined');
},
}, },
openLocation: { openLocation: {
type: Function, type: Function,
default: () => () => { default: () => {},
console.warn('cards view: openLocation is undefined');
},
}, },
album: { album: {
type: Object, type: Object,

View file

@ -167,12 +167,18 @@ export default {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
refresh: Function, refresh: {
type: Function,
default: () => {},
},
album: { album: {
type: Object, type: Object,
default: () => {}, default: () => {},
}, },
context: String, context: {
type: String,
default: '',
},
}, },
data() { data() {
return { return {

View file

@ -122,21 +122,15 @@ export default {
}, },
openPhoto: { openPhoto: {
type: Function, type: Function,
default: () => () => { default: () => {},
console.warn('list view: openPhoto is undefined');
},
}, },
editPhoto: { editPhoto: {
type: Function, type: Function,
default: () => () => { default: () => {},
console.warn('list view: editPhoto is undefined');
},
}, },
openLocation: { openLocation: {
type: Function, type: Function,
default: () => () => { default:() => {},
console.warn('list view: openLocation is undefined');
},
}, },
album: { album: {
type: Object, type: Object,

View file

@ -128,15 +128,11 @@ export default {
}, },
openPhoto: { openPhoto: {
type: Function, type: Function,
default: () => () => { default:() => {},
console.warn('mosaic view: openPhoto is undefined');
},
}, },
editPhoto: { editPhoto: {
type: Function, type: Function,
default: () => () => { default: () => {},
console.warn('mosaic view: editPhoto is undefined');
},
}, },
album: { album: {
type: Object, type: Object,

View file

@ -1,23 +1,24 @@
<template> <template>
<v-form ref="form" lazy-validation <v-form ref="form" lazy-validation
dense autocomplete="off" class="p-photo-toolbar" accept-charset="UTF-8" dense autocomplete="off" class="p-photo-toolbar" accept-charset="UTF-8"
@submit.prevent="updateQuery"> @submit.prevent="updateQuery()">
<v-toolbar flat :dense="$vuetify.breakpoint.smAndDown" class="page-toolbar" color="secondary"> <v-toolbar flat :dense="$vuetify.breakpoint.smAndDown" class="page-toolbar" color="secondary">
<v-text-field :value="filter.q" <v-text-field :value="filter.q"
class="input-search background-inherit elevation-0" class="input-search background-inherit elevation-0"
solo hide-details clearable overflow single-line validate-on-blur solo hide-details clearable overflow single-line
validate-on-blur
autocorrect="off" autocorrect="off"
autocapitalize="none" autocapitalize="none"
browser-autocomplete="off" browser-autocomplete="off"
:label="$gettext('Search')" :label="$gettext('Search')"
prepend-inner-icon="search" prepend-inner-icon="search"
color="secondary-dark" color="secondary-dark"
@input="onChangeQuery" @change="(v) => {updateFilter({'q': v})}"
@keyup.enter.native="updateQuery" @keyup.enter.native="refresh()"
@click:clear="clearQuery" @click:clear="() => {updateQuery({'q': ''})}"
></v-text-field> ></v-text-field>
<v-btn icon class="hidden-xs-only action-reload" :title="$gettext('Reload')" @click.stop="refresh"> <v-btn icon class="hidden-xs-only action-reload" :title="$gettext('Reload')" @click.stop="refresh()">
<v-icon>refresh</v-icon> <v-icon>refresh</v-icon>
</v-btn> </v-btn>
@ -49,7 +50,7 @@
<v-card-text> <v-card-text>
<v-layout row wrap> <v-layout row wrap>
<v-flex xs12 sm6 md3 pa-2 class="p-countries-select"> <v-flex xs12 sm6 md3 pa-2 class="p-countries-select">
<v-select v-model="filter.country" <v-select :value="filter.country"
:label="$gettext('Country')" :label="$gettext('Country')"
flat solo hide-details flat solo hide-details
color="secondary-dark" color="secondary-dark"
@ -57,60 +58,60 @@
item-text="Name" item-text="Name"
:items="countryOptions" :items="countryOptions"
class="input-countries" class="input-countries"
@change="dropdownChange" @change="(v) => {updateQuery({'country': v})}"
> >
</v-select> </v-select>
</v-flex> </v-flex>
<v-flex xs12 sm6 md3 pa-2 class="p-camera-select"> <v-flex xs12 sm6 md3 pa-2 class="p-camera-select">
<v-select v-model="filter.camera" <v-select :value="filter.camera"
:label="$gettext('Camera')" :label="$gettext('Camera')"
flat solo hide-details flat solo hide-details
color="secondary-dark" color="secondary-dark"
item-value="ID" item-value="ID"
item-text="Name" item-text="Name"
:items="cameraOptions" :items="cameraOptions"
@change="dropdownChange"> @change="(v) => {updateQuery({'camera': v})}">
</v-select> </v-select>
</v-flex> </v-flex>
<v-flex xs12 sm6 md3 pa-2 class="p-view-select"> <v-flex xs12 sm6 md3 pa-2 class="p-view-select">
<v-select id="viewSelect" <v-select id="viewSelect"
v-model="settings.view" :value="settings.view"
:label="$gettext('View')" flat solo :label="$gettext('View')" flat solo
hide-details hide-details
color="secondary-dark" color="secondary-dark"
:items="options.views" :items="options.views"
@change="dropdownChange"> @change="(v) => {setView(v)}">
</v-select> </v-select>
</v-flex> </v-flex>
<v-flex xs12 sm6 md3 pa-2 class="p-time-select"> <v-flex xs12 sm6 md3 pa-2 class="p-time-select">
<v-select v-model="filter.order" <v-select :value="filter.order"
:label="$gettext('Sort Order')" :label="$gettext('Sort Order')"
flat solo hide-details flat solo hide-details
color="secondary-dark" color="secondary-dark"
:items="options.sorting" :items="options.sorting"
@change="dropdownChange"> @change="(v) => {updateQuery({'order': v})}">
</v-select> </v-select>
</v-flex> </v-flex>
<v-flex xs12 sm6 md3 pa-2 class="p-year-select"> <v-flex xs12 sm6 md3 pa-2 class="p-year-select">
<v-select v-model="filter.year" <v-select :value="filter.year"
:label="$gettext('Year')" :label="$gettext('Year')"
flat solo hide-details flat solo hide-details
color="secondary-dark" color="secondary-dark"
item-value="value" item-value="value"
item-text="text" item-text="text"
:items="yearOptions()" :items="yearOptions()"
@change="dropdownChange"> @change="(v) => {updateQuery({'year': v})}">
</v-select> </v-select>
</v-flex> </v-flex>
<v-flex xs12 sm6 md3 pa-2 class="p-month-select"> <v-flex xs12 sm6 md3 pa-2 class="p-month-select">
<v-select v-model="filter.month" <v-select :value="filter.month"
:label="$gettext('Month')" :label="$gettext('Month')"
flat solo hide-details flat solo hide-details
color="secondary-dark" color="secondary-dark"
item-value="value" item-value="value"
item-text="text" item-text="text"
:items="monthOptions()" :items="monthOptions()"
@change="dropdownChange"> @change="(v) => {updateQuery({'month': v})}">
</v-select> </v-select>
</v-flex> </v-flex>
<!-- v-flex xs12 sm6 md3 pa-2 class="p-lens-select"> <!-- v-flex xs12 sm6 md3 pa-2 class="p-lens-select">
@ -125,25 +126,25 @@
</v-select> </v-select>
</v-flex --> </v-flex -->
<v-flex xs12 sm6 md3 pa-2 class="p-color-select"> <v-flex xs12 sm6 md3 pa-2 class="p-color-select">
<v-select v-model="filter.color" <v-select :value="filter.color"
:label="$gettext('Color')" :label="$gettext('Color')"
flat solo hide-details flat solo hide-details
color="secondary-dark" color="secondary-dark"
item-value="Slug" item-value="Slug"
item-text="Name" item-text="Name"
:items="colorOptions()" :items="colorOptions()"
@change="dropdownChange"> @change="(v) => {updateQuery({'color': v})}">
</v-select> </v-select>
</v-flex> </v-flex>
<v-flex xs12 sm6 md3 pa-2 class="p-category-select"> <v-flex xs12 sm6 md3 pa-2 class="p-category-select">
<v-select v-model="filter.label" <v-select :value="filter.label"
:label="$gettext('Category')" :label="$gettext('Category')"
flat solo hide-details flat solo hide-details
color="secondary-dark" color="secondary-dark"
item-value="Slug" item-value="Slug"
item-text="Name" item-text="Name"
:items="categoryOptions" :items="categoryOptions"
@change="dropdownChange"> @change="(v) => {updateQuery({'label': v})}">
</v-select> </v-select>
</v-flex> </v-flex>
</v-layout> </v-layout>
@ -158,17 +159,26 @@ import * as options from "options/options";
export default { export default {
name: 'PPhotoToolbar', name: 'PPhotoToolbar',
props: { props: {
dirty: Boolean,
filter: { filter: {
type: Object, type: Object,
default: () => {}, default: () => {},
}, },
updateFilter: {
type: Function,
default: () => {},
},
updateQuery: {
type: Function,
default: () => {},
},
settings: { settings: {
type: Object, type: Object,
default: () => {}, default: () => {},
}, },
refresh: Function, refresh: {
filterChange: Function, type: Function,
default: () => {},
},
}, },
data() { data() {
return { return {
@ -176,7 +186,6 @@ export default {
isFullScreen: !!document.fullscreenElement, isFullScreen: !!document.fullscreenElement,
config: this.$config.values, config: this.$config.values,
searchExpanded: false, searchExpanded: false,
q: this.filter.q ? this.filter.q : '',
all: { all: {
countries: [{ID: "", Name: this.$gettext("All Countries")}], countries: [{ID: "", Name: this.$gettext("All Countries")}],
cameras: [{ID: 0, Name: this.$gettext("All Cameras")}], cameras: [{ID: 0, Name: this.$gettext("All Cameras")}],
@ -228,27 +237,10 @@ export default {
yearOptions() { yearOptions() {
return this.all.years.concat(options.IndexedYears()); return this.all.years.concat(options.IndexedYears());
}, },
dropdownChange() {
this.updateQuery();
if (window.innerWidth < 600) {
this.searchExpanded = false;
}
},
setView(name) { setView(name) {
this.settings.view = name; if (name) {
this.updateQuery(); this.refresh({'view': name});
}, }
onChangeQuery(val) {
this.q = val ? String(val) : '';
},
clearQuery() {
this.q = '';
this.updateQuery();
},
updateQuery() {
this.filter.q = this.q.trim();
this.filterChange();
}, },
showUpload() { showUpload() {
Event.publish("dialog.upload"); Event.publish("dialog.upload");

View file

@ -54,15 +54,11 @@ export default {
}, },
success: { success: {
type: Function, type: Function,
default() { default: () => {},
return false;
}
}, },
error: { error: {
type: Function, type: Function,
default() { default: () => {},
return false;
}
} }
}, },
data: () => ({ data: () => ({

View file

@ -2,8 +2,8 @@
<div v-infinite-scroll="loadMore" class="p-page p-page-album-photos" :infinite-scroll-disabled="scrollDisabled" <div v-infinite-scroll="loadMore" class="p-page p-page-album-photos" :infinite-scroll-disabled="scrollDisabled"
:infinite-scroll-distance="scrollDistance" :infinite-scroll-listen-for-event="'scrollRefresh'"> :infinite-scroll-distance="scrollDistance" :infinite-scroll-listen-for-event="'scrollRefresh'">
<p-album-toolbar :album="model" :settings="settings" :filter="filter" :filter-change="updateQuery" <p-album-toolbar :filter="filter" :album="model" :settings="settings" :refresh="refresh"
:refresh="refresh"></p-album-toolbar> :update-filter="updateFilter" :update-query="updateQuery"></p-album-toolbar>
<v-container v-if="loading" fluid class="pa-4"> <v-container v-if="loading" fluid class="pa-4">
<v-progress-linear color="secondary-dark" :indeterminate="true"></v-progress-linear> <v-progress-linear color="secondary-dark" :indeterminate="true"></v-progress-linear>
@ -55,7 +55,10 @@ import Viewer from "common/viewer";
export default { export default {
name: 'PPageAlbumPhotos', name: 'PPageAlbumPhotos',
props: { props: {
staticFilter: Object staticFilter: {
type: Object,
default: () => {},
},
}, },
data() { data() {
const uid = this.$route.params.uid; const uid = this.$route.params.uid;
@ -261,8 +264,51 @@ export default {
this.listen = true; this.listen = true;
}); });
}, },
updateQuery() { updateSettings(props) {
this.filter.q = this.filter.q.trim(); if (!props || typeof props !== "object" || props.target) {
return;
}
for (const [key, value] of Object.entries(props)) {
if (!this.settings.hasOwnProperty(key)) {
continue;
}
switch (typeof value) {
case "string":
this.settings[key] = value.trim();
break;
default:
this.settings[key] = value;
}
}
},
updateFilter(props) {
if (!props || typeof props !== "object" || props.target) {
return;
}
for (const [key, value] of Object.entries(props)) {
if (!this.filter.hasOwnProperty(key)) {
continue;
}
switch (typeof value) {
case "string":
this.filter[key] = value.trim();
break;
default:
this.filter[key] = value;
}
}
},
updateQuery(props) {
this.updateFilter(props);
if (this.model.Order !== this.filter.order) {
this.model.Order = this.filter.order;
this.updateAlbum();
}
if (this.loading) return;
const query = { const query = {
view: this.settings.view view: this.settings.view
@ -299,10 +345,10 @@ export default {
return params; return params;
}, },
refresh() { refresh(props) {
if (this.loading) { this.updateSettings(props);
return;
} if (this.loading) return;
this.loading = true; this.loading = true;
this.page = 0; this.page = 0;

View file

@ -3,7 +3,7 @@
:infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="scrollDistance" :infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="scrollDistance"
:infinite-scroll-listen-for-event="'scrollRefresh'"> :infinite-scroll-listen-for-event="'scrollRefresh'">
<v-form ref="form" class="p-albums-search" lazy-validation dense @submit.prevent="updateQuery"> <v-form ref="form" class="p-albums-search" lazy-validation dense @submit.prevent="updateQuery()">
<v-toolbar flat :dense="$vuetify.breakpoint.smAndDown" class="page-toolbar" color="secondary"> <v-toolbar flat :dense="$vuetify.breakpoint.smAndDown" class="page-toolbar" color="secondary">
<v-text-field :value="filter.q" <v-text-field :value="filter.q"
solo hide-details clearable overflow single-line validate-on-blur solo hide-details clearable overflow single-line validate-on-blur
@ -14,12 +14,12 @@
autocapitalize="none" autocapitalize="none"
prepend-inner-icon="search" prepend-inner-icon="search"
color="secondary-dark" color="secondary-dark"
@input="onChangeQuery" @change="(v) => {updateFilter({'q': v})}"
@keyup.enter.native="updateQuery" @keyup.enter.native="refresh()"
@click:clear="clearQuery" @click:clear="() => {updateQuery({'q': ''})}"
></v-text-field> ></v-text-field>
<v-overflow-btn v-model="filter.category" <v-overflow-btn :value="filter.category"
solo hide-details single-line solo hide-details single-line
:label="$gettext('Category')" :label="$gettext('Category')"
color="secondary-dark" color="secondary-dark"
@ -28,11 +28,11 @@
append-icon="" append-icon=""
:items="categories" :items="categories"
class="hidden-xs-only input-category background-inherit elevation-0" class="hidden-xs-only input-category background-inherit elevation-0"
@change="updateQuery" @change="(v) => {updateFilter({'category': v})}"
> >
</v-overflow-btn> </v-overflow-btn>
<v-btn icon class="action-reload" :title="$gettext('Reload')" @click.stop="refresh"> <v-btn icon class="action-reload" :title="$gettext('Reload')" @click.stop="refresh()">
<v-icon>refresh</v-icon> <v-icon>refresh</v-icon>
</v-btn> </v-btn>
@ -42,7 +42,7 @@
</v-btn> </v-btn>
<v-btn v-if="staticFilter.type === 'album'" icon class="action-add" :title="$gettext('Add Album')" <v-btn v-if="staticFilter.type === 'album'" icon class="action-add" :title="$gettext('Add Album')"
@click.prevent="create"> @click.prevent="create()">
<v-icon>add</v-icon> <v-icon>add</v-icon>
</v-btn> </v-btn>
</v-toolbar> </v-toolbar>
@ -218,8 +218,14 @@ import {Input, InputInvalid, ClickShort, ClickLong} from "common/input";
export default { export default {
name: 'PPageAlbums', name: 'PPageAlbums',
props: { props: {
staticFilter: Object, staticFilter: {
view: String, type: Object,
default: () => {},
},
view: {
type: String,
default: "",
},
}, },
data() { data() {
const query = this.$route.query; const query = this.$route.query;
@ -483,15 +489,44 @@ export default {
this.listen = true; this.listen = true;
}); });
}, },
onChangeQuery(val) { updateSettings(props) {
this.q = val ? String(val) : ''; if (!props || typeof props !== "object" || props.target) {
return;
}
for (const [key, value] of Object.entries(props)) {
if (!this.settings.hasOwnProperty(key)) {
continue;
}
switch (typeof value) {
case "string":
this.settings[key] = value.trim();
break;
default:
this.settings[key] = value;
}
}
}, },
clearQuery() { updateFilter(props) {
this.q = ''; if (!props || typeof props !== "object" || props.target) {
this.updateQuery(); return;
}
for (const [key, value] of Object.entries(props)) {
if (!this.filter.hasOwnProperty(key)) {
continue;
}
switch (typeof value) {
case "string":
this.filter[key] = value.trim();
break;
default:
this.filter[key] = value;
}
}
}, },
updateQuery() { updateQuery(props) {
this.filter.q = this.q.trim(); this.updateFilter(props);
if (this.loading) return; if (this.loading) return;
@ -574,8 +609,11 @@ export default {
this.listen = true; this.listen = true;
}); });
}, },
refresh() { refresh(props) {
this.updateSettings(props);
if (this.loading) return; if (this.loading) return;
this.loading = true; this.loading = true;
this.page = 0; this.page = 0;
this.dirty = true; this.dirty = true;

View file

@ -3,7 +3,7 @@
:infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="scrollDistance" :infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="scrollDistance"
:infinite-scroll-listen-for-event="'scrollRefresh'"> :infinite-scroll-listen-for-event="'scrollRefresh'">
<v-form ref="form" class="p-labels-search" lazy-validation dense @submit.stop.prevent> <v-form ref="form" class="p-labels-search" lazy-validation dense @submit.stop.prevent="updateQuery()">
<v-toolbar flat :dense="$vuetify.breakpoint.smAndDown" class="page-toolbar" color="secondary"> <v-toolbar flat :dense="$vuetify.breakpoint.smAndDown" class="page-toolbar" color="secondary">
<v-text-field :value="filter.q" <v-text-field :value="filter.q"
solo hide-details clearable overflow single-line validate-on-blur solo hide-details clearable overflow single-line validate-on-blur
@ -14,19 +14,19 @@
autocorrect="off" autocorrect="off"
autocapitalize="none" autocapitalize="none"
color="secondary-dark" color="secondary-dark"
@input="onChangeQuery" @change="(v) => {updateFilter({'q': v})}"
@keyup.enter.native="updateQuery" @keyup.enter.native="refresh()"
@click:clear="clearQuery" @click:clear="() => {updateQuery({'q': ''})}"
></v-text-field> ></v-text-field>
<v-btn icon class="action-reload" :title="$gettext('Reload')" @click.stop="refresh"> <v-btn icon class="action-reload" :title="$gettext('Reload')" @click.stop="refresh()">
<v-icon>refresh</v-icon> <v-icon>refresh</v-icon>
</v-btn> </v-btn>
<v-btn v-if="!filter.all" icon class="action-show-all" :title="$gettext('Show more')" @click.stop="showAll"> <v-btn v-if="!filter.all" icon class="action-show-all" :title="$gettext('Show more')" @click.stop="showAll()">
<v-icon>unfold_more</v-icon> <v-icon>unfold_more</v-icon>
</v-btn> </v-btn>
<v-btn v-else icon class="action-show-important" :title="$gettext('Show less')" @click.stop="showImportant"> <v-btn v-else icon class="action-show-important" :title="$gettext('Show less')" @click.stop="showImportant()">
<v-icon>unfold_less</v-icon> <v-icon>unfold_less</v-icon>
</v-btn> </v-btn>
</v-toolbar> </v-toolbar>
@ -167,8 +167,6 @@ export default {
const routeName = this.$route.name; const routeName = this.$route.name;
const q = query['q'] ? query['q'] : ''; const q = query['q'] ? query['q'] : '';
const all = query['all'] ? query['all'] : ''; const all = query['all'] ? query['all'] : '';
const filter = {"q": String(q), all: String(all)};
const settings = {};
return { return {
view: 'all', view: 'all',
@ -184,9 +182,8 @@ export default {
offset: 0, offset: 0,
page: 0, page: 0,
selection: [], selection: [],
settings: settings, settings: {},
q: q, filter: {q, all},
filter: filter,
lastFilter: {}, lastFilter: {},
routeName: routeName, routeName: routeName,
titleRule: v => v.length <= this.$config.get('clip') || this.$gettext("Name too long"), titleRule: v => v.length <= this.$config.get('clip') || this.$gettext("Name too long"),
@ -200,8 +197,7 @@ export default {
this.routeName = this.$route.name; this.routeName = this.$route.name;
this.lastFilter = {}; this.lastFilter = {};
this.q = query['q'] ? query['q'] : ''; this.filter.q = query['q'] ? query['q'] : '';
this.filter.q = this.q;
this.filter.all = query['all'] ? query['all'] : ''; this.filter.all = query['all'] ? query['all'] : '';
this.search(); this.search();
@ -368,7 +364,7 @@ export default {
this.lastId = ""; this.lastId = "";
}, },
loadMore() { loadMore() {
if (this.scrollDisabled) return; if (this.scrollDisabled || this.$scrollbar.disabled()) return;
this.scrollDisabled = true; this.scrollDisabled = true;
this.listen = false; this.listen = false;
@ -387,8 +383,12 @@ export default {
Object.assign(params, this.staticFilter); Object.assign(params, this.staticFilter);
} }
if (offset === 0) {
this.results = [];
}
Label.search(params).then(resp => { Label.search(params).then(resp => {
this.results = this.dirty ? resp.models : this.results.concat(resp.models); this.results = (offset === 0) ? resp.models : this.results.concat(resp.models);
this.scrollDisabled = (resp.count < resp.limit); this.scrollDisabled = (resp.count < resp.limit);
@ -415,15 +415,44 @@ export default {
this.listen = true; this.listen = true;
}); });
}, },
onChangeQuery(val) { updateSettings(props) {
this.q = val ? String(val) : ''; if (!props || typeof props !== "object" || props.target) {
return;
}
for (const [key, value] of Object.entries(props)) {
if (!this.settings.hasOwnProperty(key)) {
continue;
}
switch (typeof value) {
case "string":
this.settings[key] = value.trim();
break;
default:
this.settings[key] = value;
}
}
}, },
clearQuery() { updateFilter(props) {
this.q = ''; if (!props || typeof props !== "object" || props.target) {
this.updateQuery(); return;
}
for (const [key, value] of Object.entries(props)) {
if (!this.filter.hasOwnProperty(key)) {
continue;
}
switch (typeof value) {
case "string":
this.filter[key] = value.trim();
break;
default:
this.filter[key] = value;
}
}
}, },
updateQuery() { updateQuery(props) {
this.filter.q = this.q.trim(); this.updateFilter(props);
if (this.loading) return; if (this.loading) return;
@ -459,12 +488,16 @@ export default {
return params; return params;
}, },
refresh() { refresh(props) {
this.updateSettings(props);
if (this.loading) return; if (this.loading) return;
this.loading = true; this.loading = true;
this.page = 0; this.page = 0;
this.dirty = true; this.dirty = true;
this.scrollDisabled = false; this.scrollDisabled = false;
this.loadMore(); this.loadMore();
}, },
search() { search() {

View file

@ -11,15 +11,15 @@
:label="$gettext('Search')" :label="$gettext('Search')"
prepend-inner-icon="search" prepend-inner-icon="search"
color="secondary-dark" color="secondary-dark"
@input="onChangeQuery" @change="(v) => {updateFilter({'q': v})}"
@keyup.enter.native="updateQuery" @keyup.enter.native="updateQuery()"
@click:clear="clearQuery" @click:clear="() => {updateQuery({'q': ''})}"
></v-text-field> ></v-text-field>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn icon class="action-reload" :title="$gettext('Reload')" @click.stop="onReload"> <v-btn icon class="action-reload" :title="$gettext('Reload')" @click.stop="onReload()">
<v-icon>refresh</v-icon> <v-icon>refresh</v-icon>
</v-btn> </v-btn>
<v-btn v-if="!isPublic" icon class="action-delete" :title="$gettext('Delete')" @click.stop="onDelete"> <v-btn v-if="!isPublic" icon class="action-delete" :title="$gettext('Delete')" @click.stop="onDelete()">
<v-icon>delete</v-icon> <v-icon>delete</v-icon>
</v-btn> </v-btn>
<v-btn icon href="https://docs.photoprism.app/getting-started/troubleshooting/" target="_blank" class="action-bug-report" <v-btn icon href="https://docs.photoprism.app/getting-started/troubleshooting/" target="_blank" class="action-bug-report"
@ -108,7 +108,6 @@ export default {
loading: false, loading: false,
scrollDisabled: false, scrollDisabled: false,
scrollDistance: window.innerHeight*2, scrollDistance: window.innerHeight*2,
q: q,
filter: {q}, filter: {q},
isPublic: this.$config.get("public"), isPublic: this.$config.get("public"),
batchSize: 100, batchSize: 100,
@ -127,10 +126,7 @@ export default {
watch: { watch: {
'$route'() { '$route'() {
const query = this.$route.query; const query = this.$route.query;
this.filter.q = query['q'] ? query['q'] : '';
this.q = query['q'] ? query['q'] : '';
this.filter.q = this.q;
this.onReload(); this.onReload();
} }
}, },
@ -138,15 +134,26 @@ export default {
this.loadMore(); this.loadMore();
}, },
methods: { methods: {
onChangeQuery(val) { updateFilter(props) {
this.q = val ? String(val) : ''; if (!props || typeof props !== "object" || props.target) {
return;
}
for (const [key, value] of Object.entries(props)) {
if (!this.filter.hasOwnProperty(key)) {
continue;
}
switch (typeof value) {
case "string":
this.filter[key] = value.trim();
break;
default:
this.filter[key] = value;
}
}
}, },
clearQuery() { updateQuery(props) {
this.q = ''; this.updateFilter(props);
this.updateQuery();
},
updateQuery() {
this.filter.q = this.q.trim();
if (this.loading) return; if (this.loading) return;

View file

@ -3,7 +3,7 @@
:infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="scrollDistance" :infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="scrollDistance"
:infinite-scroll-listen-for-event="'scrollRefresh'"> :infinite-scroll-listen-for-event="'scrollRefresh'">
<v-form ref="form" class="p-people-search" lazy-validation dense @submit.prevent="updateQuery"> <v-form ref="form" class="p-people-search" lazy-validation dense @submit.prevent="updateQuery()">
<v-toolbar dense flat class="page-toolbar" color="secondary-light pa-0"> <v-toolbar dense flat class="page-toolbar" color="secondary-light pa-0">
<v-text-field :value="filter.q" <v-text-field :value="filter.q"
solo hide-details clearable overflow single-line validate-on-blur solo hide-details clearable overflow single-line validate-on-blur
@ -14,21 +14,21 @@
autocorrect="off" autocorrect="off"
autocapitalize="none" autocapitalize="none"
color="secondary-dark" color="secondary-dark"
@input="onChangeQuery" @change="(v) => {updateFilter({'q': v})}"
@keyup.enter.native="updateQuery" @keyup.enter.native="refresh()"
@click:clear="clearQuery" @click:clear="() => {updateQuery({'q': ''})}"
></v-text-field> ></v-text-field>
<v-divider vertical></v-divider> <v-divider vertical></v-divider>
<v-btn icon overflow flat depressed color="secondary-dark" class="action-reload" :title="$gettext('Reload')" @click.stop="refresh"> <v-btn icon overflow flat depressed color="secondary-dark" class="action-reload" :title="$gettext('Reload')" @click.stop="refresh()">
<v-icon>refresh</v-icon> <v-icon>refresh</v-icon>
</v-btn> </v-btn>
<v-btn v-if="!filter.hidden" icon class="action-show-hidden" :title="$gettext('Show hidden')" @click.stop="onShowHidden"> <v-btn v-if="!filter.hidden" icon class="action-show-hidden" :title="$gettext('Show hidden')" @click.stop="onShowHidden()">
<v-icon>visibility</v-icon> <v-icon>visibility</v-icon>
</v-btn> </v-btn>
<v-btn v-else icon class="action-exclude-hidden" :title="$gettext('Exclude hidden')" @click.stop="onExcludeHidden"> <v-btn v-else icon class="action-exclude-hidden" :title="$gettext('Exclude hidden')" @click.stop="onExcludeHidden()">
<v-icon>visibility_off</v-icon> <v-icon>visibility_off</v-icon>
</v-btn> </v-btn>
</v-toolbar> </v-toolbar>
@ -193,8 +193,6 @@ export default {
const q = query['q'] ? query['q'] : ''; const q = query['q'] ? query['q'] : '';
const hidden = query['hidden'] ? query['hidden'] : ''; const hidden = query['hidden'] ? query['hidden'] : '';
const order = this.sortOrder(); const order = this.sortOrder();
const filter = {q, hidden, order};
const settings = {};
return { return {
view: 'all', view: 'all',
@ -210,9 +208,8 @@ export default {
offset: 0, offset: 0,
page: 0, page: 0,
selection: [], selection: [],
settings: settings, settings: {},
q: q, filter: {q, hidden, order},
filter: filter,
lastFilter: {}, lastFilter: {},
routeName: routeName, routeName: routeName,
titleRule: v => v.length <= this.$config.get("clip") || this.$gettext("Name too long"), titleRule: v => v.length <= this.$config.get("clip") || this.$gettext("Name too long"),
@ -244,8 +241,7 @@ export default {
const query = this.$route.query; const query = this.$route.query;
this.routeName = this.$route.name; this.routeName = this.$route.name;
this.q = query["q"] ? query["q"] : ""; this.filter.q = query["q"] ? query["q"] : "";
this.filter.q = this.q;
this.filter.hidden = query["hidden"] ? query["hidden"] : ""; this.filter.hidden = query["hidden"] ? query["hidden"] : "";
this.filter.order = this.sortOrder(); this.filter.order = this.sortOrder();
@ -527,15 +523,44 @@ export default {
this.listen = true; this.listen = true;
}); });
}, },
onChangeQuery(val) { updateSettings(props) {
this.q = val ? String(val) : ''; if (!props || typeof props !== "object" || props.target) {
return;
}
for (const [key, value] of Object.entries(props)) {
if (!this.settings.hasOwnProperty(key)) {
continue;
}
switch (typeof value) {
case "string":
this.settings[key] = value.trim();
break;
default:
this.settings[key] = value;
}
}
}, },
clearQuery() { updateFilter(props) {
this.q = ''; if (!props || typeof props !== "object" || props.target) {
this.updateQuery(); return;
}
for (const [key, value] of Object.entries(props)) {
if (!this.filter.hasOwnProperty(key)) {
continue;
}
switch (typeof value) {
case "string":
this.filter[key] = value.trim();
break;
default:
this.filter[key] = value;
}
}
}, },
updateQuery() { updateQuery(props) {
this.filter.q = this.q.trim(); this.updateFilter(props);
if (this.loading || !this.active) { if (this.loading || !this.active) {
return; return;
@ -573,10 +598,10 @@ export default {
return params; return params;
}, },
refresh() { refresh(props) {
if (this.loading || !this.active) { this.updateSettings(props);
return;
} if (this.loading || !this.active) return;
this.loading = true; this.loading = true;
this.page = 0; this.page = 0;

View file

@ -3,8 +3,8 @@
:infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="scrollDistance" :infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="scrollDistance"
:infinite-scroll-listen-for-event="'scrollRefresh'"> :infinite-scroll-listen-for-event="'scrollRefresh'">
<p-photo-toolbar :settings="settings" :filter="filter" :filter-change="updateQuery" :dirty="dirty" <p-photo-toolbar :filter="filter" :settings="settings" :refresh="refresh"
:refresh="refresh"></p-photo-toolbar> :update-filter="updateFilter" :update-query="updateQuery"></p-photo-toolbar>
<v-container v-if="loading" fluid class="pa-4"> <v-container v-if="loading" fluid class="pa-4">
<v-progress-linear color="secondary-dark" :indeterminate="true"></v-progress-linear> <v-progress-linear color="secondary-dark" :indeterminate="true"></v-progress-linear>
@ -42,7 +42,7 @@
</template> </template>
<script> <script>
import {Photo, MediaLive, MediaRaw, MediaVideo, MediaAnimated} from "model/photo"; import {MediaAnimated, MediaLive, MediaRaw, MediaVideo, Photo} from "model/photo";
import Thumb from "model/thumb"; import Thumb from "model/thumb";
import Viewer from "common/viewer"; import Viewer from "common/viewer";
import Event from "pubsub-js"; import Event from "pubsub-js";
@ -50,7 +50,11 @@ import Event from "pubsub-js";
export default { export default {
name: 'PPagePhotos', name: 'PPagePhotos',
props: { props: {
staticFilter: Object staticFilter: {
type: Object,
default: () => {
},
},
}, },
data() { data() {
const query = this.$route.query; const query = this.$route.query;
@ -98,7 +102,7 @@ export default {
complete: false, complete: false,
results: [], results: [],
scrollDisabled: true, scrollDisabled: true,
scrollDistance: window.innerHeight*2, scrollDistance: window.innerHeight * 2,
batchSize: batchSize, batchSize: batchSize,
offset: 0, offset: 0,
page: 0, page: 0,
@ -232,7 +236,7 @@ export default {
showMerged = false; showMerged = false;
} }
if (showMerged && selected.Type === MediaLive || selected.Type === MediaVideo|| selected.Type === MediaAnimated) { if (showMerged && selected.Type === MediaLive || selected.Type === MediaVideo || selected.Type === MediaAnimated) {
if (selected.isPlayable()) { if (selected.isPlayable()) {
this.$viewer.play({video: selected}); this.$viewer.play({video: selected});
} else { } else {
@ -303,10 +307,46 @@ export default {
this.listen = true; this.listen = true;
}); });
}, },
updateQuery() { updateSettings(props) {
if (this.loading) return; if (!props || typeof props !== "object" || props.target) {
return;
}
this.filter.q = this.filter.q.trim(); for (const [key, value] of Object.entries(props)) {
if (!this.settings.hasOwnProperty(key)) {
continue;
}
switch (typeof value) {
case "string":
this.settings[key] = value.trim();
break;
default:
this.settings[key] = value;
}
}
},
updateFilter(props) {
if (!props || typeof props !== "object" || props.target) {
return;
}
for (const [key, value] of Object.entries(props)) {
if (!this.filter.hasOwnProperty(key)) {
continue;
}
switch (typeof value) {
case "string":
this.filter[key] = value.trim();
break;
default:
this.filter[key] = value;
}
}
},
updateQuery(props) {
this.updateFilter(props);
if (this.loading) return;
const query = { const query = {
view: this.settings.view view: this.settings.view
@ -341,10 +381,10 @@ export default {
return params; return params;
}, },
refresh() { refresh(props) {
if (this.loading) { this.updateSettings(props);
return;
} if (this.loading) return;
this.loading = true; this.loading = true;
this.page = 0; this.page = 0;

View file

@ -231,11 +231,11 @@ export default {
this.search(); this.search();
}, },
clearQuery() { clearQuery() {
this.filter.q = ""; this.filter.q = '';
this.search(); this.search();
}, },
updateQuery() { updateQuery() {
this.filter.q = this.filter.q.trim(); if (this.loading) return;
if (this.query() !== this.filter.q) { if (this.query() !== this.filter.q) {
if (this.filter.q) { if (this.filter.q) {
@ -247,6 +247,7 @@ export default {
}, },
search() { search() {
if (this.loading) return; if (this.loading) return;
// Don't query the same data more than once // Don't query the same data more than once
if (JSON.stringify(this.lastFilter) === JSON.stringify(this.filter)) return; if (JSON.stringify(this.lastFilter) === JSON.stringify(this.filter)) return;
this.loading = true; this.loading = true;

View file

@ -122,15 +122,16 @@ export default {
type: Object, type: Object,
default: () => {}, default: () => {},
}, },
view: String, view: {
type: String,
default: "",
},
}, },
data() { data() {
const query = this.$route.query; const query = this.$route.query;
const routeName = this.$route.name; const routeName = this.$route.name;
const q = query["q"] ? query["q"] : ""; const q = query["q"] ? query["q"] : "";
const category = query["category"] ? query["category"] : ""; const category = query["category"] ? query["category"] : "";
const filter = {q, category};
const settings = {};
let categories = [{"value": "", "text": this.$gettext("All Categories")}]; let categories = [{"value": "", "text": this.$gettext("All Categories")}];
@ -154,8 +155,8 @@ export default {
offset: 0, offset: 0,
page: 0, page: 0,
selection: [], selection: [],
settings: settings, settings: {},
filter: filter, filter: {q, category},
lastFilter: {}, lastFilter: {},
routeName: routeName, routeName: routeName,
titleRule: v => v.length <= this.$config.get('clip') || this.$gettext("Title too long"), titleRule: v => v.length <= this.$config.get('clip') || this.$gettext("Title too long"),

View file

@ -157,21 +157,15 @@ export default {
}, },
openPhoto: { openPhoto: {
type: Function, type: Function,
default: () => () => { default: () => {},
console.warn('cards view: openPhoto is undefined');
},
}, },
editPhoto: { editPhoto: {
type: Function, type: Function,
default: () => () => { default: () => {},
console.warn('cards view: editPhoto is undefined');
},
}, },
openLocation: { openLocation: {
type: Function, type: Function,
default: () => () => { default: () => {},
console.warn('cards view: openLocation is undefined');
},
}, },
album: { album: {
type: Object, type: Object,

View file

@ -104,21 +104,15 @@ export default {
}, },
openPhoto: { openPhoto: {
type: Function, type: Function,
default: () => () => { default: () => {},
console.warn('list view: openPhoto is undefined');
},
}, },
editPhoto: { editPhoto: {
type: Function, type: Function,
default: () => () => { default: () => {},
console.warn('list view: editPhoto is undefined');
},
}, },
openLocation: { openLocation: {
type: Function, type: Function,
default: () => () => { default: () => {},
console.warn('list view: openLocation is undefined');
},
}, },
album: { album: {
type: Object, type: Object,

View file

@ -3,8 +3,9 @@
:infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="scrollDistance" :infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="scrollDistance"
:infinite-scroll-listen-for-event="'scrollRefresh'"> :infinite-scroll-listen-for-event="'scrollRefresh'">
<v-form ref="form" lazy-validation <v-form ref="form" lazy-validation dense
dense autocomplete="off" class="p-photo-toolbar p-album-toolbar" accept-charset="UTF-8"> autocomplete="off" class="p-photo-toolbar p-album-toolbar"
accept-charset="UTF-8" @submit.prevent="updateQuery()">
<v-toolbar flat color="secondary" :dense="$vuetify.breakpoint.smAndDown"> <v-toolbar flat color="secondary" :dense="$vuetify.breakpoint.smAndDown">
<v-toolbar-title> <v-toolbar-title>
{{ model.Title }} {{ model.Title }}
@ -12,12 +13,12 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn icon class="hidden-xs-only action-reload" @click.stop="refresh"> <v-btn icon class="hidden-xs-only action-reload" @click.stop="refresh()">
<v-icon>refresh</v-icon> <v-icon>refresh</v-icon>
</v-btn> </v-btn>
<v-btn v-if="$config.feature('download')" icon class="hidden-xs-only action-download" :title="$gettext('Download')" <v-btn v-if="$config.feature('download')" icon class="hidden-xs-only action-download" :title="$gettext('Download')"
@click.stop="download"> @click.stop="download()">
<v-icon>get_app</v-icon> <v-icon>get_app</v-icon>
</v-btn> </v-btn>
@ -98,7 +99,10 @@ import Viewer from "common/viewer";
export default { export default {
name: 'PPageAlbumPhotos', name: 'PPageAlbumPhotos',
props: { props: {
staticFilter: Object staticFilter: {
type: Object,
default: () => {},
},
}, },
data() { data() {
const uid = this.$route.params.uid; const uid = this.$route.params.uid;
@ -317,8 +321,44 @@ export default {
this.listen = true; this.listen = true;
}); });
}, },
updateQuery() { updateSettings(props) {
this.filter.q = this.filter.q.trim(); if (!props || typeof props !== "object" || props.target) {
return;
}
for (const [key, value] of Object.entries(props)) {
if (!this.settings.hasOwnProperty(key)) {
continue;
}
switch (typeof value) {
case "string":
this.settings[key] = value.trim();
break;
default:
this.settings[key] = value;
}
}
},
updateFilter(props) {
if (!props || typeof props !== "object" || props.target) {
return;
}
for (const [key, value] of Object.entries(props)) {
if (!this.filter.hasOwnProperty(key)) {
continue;
}
switch (typeof value) {
case "string":
this.filter[key] = value.trim();
break;
default:
this.filter[key] = value;
}
}
},
updateQuery(props) {
this.updateFilter(props);
const query = { const query = {
view: this.settings.view view: this.settings.view
@ -355,10 +395,10 @@ export default {
return params; return params;
}, },
refresh() { refresh(props) {
if (this.loading) { this.updateSettings(props);
return;
} if (this.loading) return;
this.loading = true; this.loading = true;
this.page = 0; this.page = 0;