Compare commits

...
Sign in to create a new pull request.

10 commits

Author SHA1 Message Date
Theresa Gresch
6f4e26f547 Frontend: Add acceptance test files 2020-06-01 09:16:11 +02:00
Michael Mayer
f68746768d Import: Show name of new albums #246
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
2020-06-01 02:46:24 +02:00
Michael Mayer
186b2da58a Fix labels count for SQLite
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
2020-06-01 02:30:37 +02:00
Michael Mayer
a6e06844f3 Ignore whitespace when stripping sequence from filename #335
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
2020-06-01 01:35:07 +02:00
Michael Mayer
344c88b84c Photo: Set OriginalName earlier while indexing
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
2020-06-01 01:17:54 +02:00
Michael Mayer
3e4d19c8c7 Photo: Use EstimateCountry() in UpdateLocation()
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
2020-06-01 01:02:28 +02:00
Michael Mayer
bbf6f206f4 Add json tags to PhotoName and PhotoPath
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
2020-06-01 00:50:02 +02:00
Michael Mayer
e0aff43d16 Add OriginalName to photo search result
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
2020-06-01 00:39:59 +02:00
Michael Mayer
d2bf020e23 Import: Implement "add to album" in frontend #246
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
2020-05-31 23:28:42 +02:00
Michael Mayer
c39ec9695f Import: Implement "add to album" in backend #246
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
2020-05-31 21:02:08 +02:00
50 changed files with 549 additions and 370 deletions

View file

@ -6,12 +6,12 @@ import PPhotoViewer from "./p-photo-viewer.vue";
import PPhotoCards from "./p-photo-cards.vue";
import PPhotoMosaic from "./p-photo-mosaic.vue";
import PPhotoList from "./p-photo-list.vue";
import PPhotoSearch from "./p-photo-search.vue";
import PPhotoClipboard from "./p-photo-clipboard.vue";
import PLabelClipboard from "./p-label-clipboard.vue";
import PFileClipboard from "./p-file-clipboard.vue";
import PAlbumClipboard from "./p-album-clipboard.vue";
import PAlbumToolbar from "./p-album-toolbar.vue";
import PPhotoToolbar from "./p-photo-toolbar.vue";
import PScrollTop from "./p-scroll-top.vue";
const components = {};
@ -25,12 +25,12 @@ components.install = (Vue) => {
Vue.component("p-photo-cards", PPhotoCards);
Vue.component("p-photo-mosaic", PPhotoMosaic);
Vue.component("p-photo-list", PPhotoList);
Vue.component("p-photo-search", PPhotoSearch);
Vue.component("p-photo-clipboard", PPhotoClipboard);
Vue.component("p-label-clipboard", PLabelClipboard);
Vue.component("p-file-clipboard", PFileClipboard);
Vue.component("p-album-clipboard", PAlbumClipboard);
Vue.component("p-album-toolbar", PAlbumToolbar);
Vue.component("p-photo-toolbar", PPhotoToolbar);
Vue.component("p-scroll-top", PScrollTop);
};

View file

@ -1,6 +1,6 @@
<template>
<v-form lazy-validation dense
ref="form" autocomplete="off" class="p-photo-search p-album-toolbar" accept-charset="UTF-8"
ref="form" autocomplete="off" class="p-photo-toolbar p-album-toolbar" accept-charset="UTF-8"
@submit.prevent="filterChange">
<v-toolbar flat color="secondary">
<v-edit-dialog
@ -39,6 +39,11 @@
<v-icon>view_column</v-icon>
</v-btn>
<v-btn icon @click.stop="showUpload()" v-if="!$config.values.readonly && $config.feature('upload')"
class="hidden-sm-and-down">
<v-icon>cloud_upload</v-icon>
</v-btn>
<v-btn icon @click.stop="expand" class="p-expand-search">
<v-icon>{{ searchExpanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down' }}</v-icon>
</v-btn>
@ -108,7 +113,7 @@
</v-form>
</template>
<script>
import Album from "../model/album";
import Event from "pubsub-js";
export default {
name: 'p-album-toolbar',
@ -164,12 +169,14 @@
};
},
methods: {
showUpload() {
Event.publish("dialog.upload");
},
expand() {
this.searchExpanded = !this.searchExpanded;
this.growDesc = !this.growDesc;
},
updateAlbum() {
console.log("UPDATE ALBUM", this.album.getValues(true));
if(this.album.wasChanged()) {
this.album.update();
}

View file

@ -1,6 +1,6 @@
<template>
<v-form lazy-validation dense
ref="form" autocomplete="off" class="p-photo-search" accept-charset="UTF-8"
ref="form" autocomplete="off" class="p-photo-toolbar" accept-charset="UTF-8"
@submit.prevent="filterChange">
<v-toolbar flat color="secondary">
<v-text-field class="pt-3 pr-3 p-search-field"
@ -153,7 +153,7 @@
import { Info } from "luxon";
export default {
name: 'p-photo-search',
name: 'p-photo-toolbar',
props: {
dirty: Boolean,
filter: Object,

View file

@ -6,7 +6,9 @@
<v-btn icon dark @click.stop="cancel">
<v-icon>close</v-icon>
</v-btn>
<v-toolbar-title><translate>Upload</translate></v-toolbar-title>
<v-toolbar-title>
<translate>Upload</translate>
</v-toolbar-title>
</v-toolbar>
<v-container grid-list-xs text-xs-left fluid>
<v-form ref="form" class="p-photo-upload" lazy-validation @submit.prevent="submit" dense>
@ -14,7 +16,7 @@
<v-container fluid>
<p class="subheading">
<span v-if="total === 0">Select photos to start upload...</span>
<span v-if="total === 0">Select files to start upload...</span>
<span v-else-if="failed">Upload failed</span>
<span v-else-if="total > 0 && completed < 100">
Uploading {{current}} of {{total}}...
@ -23,6 +25,39 @@
<span v-else-if="completed === 100">Done.</span>
</p>
<v-combobox flat solo hide-details chips deletable-chips
multiple color="secondary-dark" class="my-3"
v-model="selectedAlbums"
:items="albums"
item-text="Title"
item-value="UID"
:allow-overflow="false"
label="Add to existing albums or create a new one."
return-object
>
<template v-slot:no-data>
<v-list-tile>
<v-list-tile-content>
<v-list-tile-title>
Press <kbd>enter</kbd> to create a new album.
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</template>
<template v-slot:selection="data">
<v-chip
:key="JSON.stringify(data.item)"
:selected="data.selected"
:disabled="data.disabled"
class="v-chip--select-multi"
@input="data.parent.selectItem(data.item)"
>
<v-icon class="pr-1">folder</v-icon>
{{ data.item.Title ? data.item.Title : data.item | truncate(40) }}
</v-chip>
</template>
</v-combobox>
<v-progress-linear color="secondary-dark" v-model="completed"
:indeterminate="indexing"></v-progress-linear>
@ -56,6 +91,7 @@
<script>
import Api from "common/api";
import Notify from "common/notify";
import Album from "../model/album";
export default {
name: 'p-tab-upload',
@ -64,7 +100,10 @@
},
data() {
return {
albums: [],
selectedAlbums: [],
selected: [],
loading: false,
uploads: [],
busy: false,
indexing: false,
@ -78,8 +117,27 @@
}
},
methods: {
findAlbums(q) {
if (this.loading) {
return;
}
this.loading = true;
const params = {
q: q,
count: 1000,
offset: 0,
type: "album"
};
Album.search(params).then(response => {
this.loading = false;
this.albums = response.models;
}).catch(() => this.loading = false);
},
cancel() {
if(this.busy) {
if (this.busy) {
Notify.info(this.$gettext("Uploading photos..."));
return;
}
@ -87,7 +145,7 @@
this.$emit('cancel');
},
confirm() {
if(this.busy) {
if (this.busy) {
Notify.info(this.$gettext("Uploading photos..."));
return;
}
@ -117,6 +175,18 @@
Notify.info(this.$gettext("Uploading photos..."));
let addToAlbums = [];
if (this.selectedAlbums && this.selectedAlbums.length > 0) {
this.selectedAlbums.forEach((a) => {
if (typeof a === "string") {
addToAlbums.push(a)
} else if (a instanceof Album && a.UID) {
addToAlbums.push(a.UID)
}
});
}
async function performUpload(ctx) {
for (let i = 0; i < ctx.selected.length; i++) {
let file = ctx.selected[i];
@ -152,6 +222,7 @@
Api.post('import/upload/' + this.started, {
move: true,
albums: addToAlbums,
}).then(() => {
Notify.success(ctx.$gettext("Upload complete"));
ctx.busy = false;
@ -178,6 +249,7 @@
this.started = 0;
this.review = this.$config.feature("review");
this.safe = !this.$config.get("uploadNSFW");
this.findAlbums("");
}
},
};

View file

@ -31,6 +31,11 @@
<v-icon>refresh</v-icon>
</v-btn>
<v-btn icon @click.stop="showUpload()" v-if="!$config.values.readonly && $config.feature('upload')"
class="hidden-sm-and-down">
<v-icon>cloud_upload</v-icon>
</v-btn>
<v-btn icon @click.prevent="create">
<v-icon>add</v-icon>
</v-btn>
@ -222,6 +227,9 @@
};
},
methods: {
showUpload() {
Event.publish("dialog.upload");
},
selectRange(rangeEnd, models) {
if (!models || !models[rangeEnd] || !(models[rangeEnd] instanceof RestModel)) {
console.warn("selectRange() - invalid arguments:", rangeEnd, models);

View file

@ -2,8 +2,8 @@
<div class="p-page p-page-photos" v-infinite-scroll="loadMore" :infinite-scroll-disabled="scrollDisabled"
:infinite-scroll-distance="10" :infinite-scroll-listen-for-event="'scrollRefresh'">
<p-photo-search :settings="settings" :filter="filter" :filter-change="updateQuery" :dirty="dirty"
:refresh="refresh"></p-photo-search>
<p-photo-toolbar :settings="settings" :filter="filter" :filter-change="updateQuery" :dirty="dirty"
:refresh="refresh"></p-photo-toolbar>
<v-container fluid class="pa-4" v-if="loading">
<v-progress-linear color="secondary-dark" :indeterminate="true"></v-progress-linear>

View file

@ -89,27 +89,27 @@ msgstr "Alben gelöscht"
msgid "All "
msgstr "Alle"
#: src/component/p-album-toolbar.vue:136 src/component/p-photo-search.vue:157
#: src/component/p-album-toolbar.vue:136 src/component/p-photo-toolbar.vue:157
msgid "All Cameras"
msgstr "Alle Kameras"
#: src/component/p-photo-search.vue:160
#: src/component/p-photo-toolbar.vue:160
msgid "All Categories"
msgstr "Alle Kategorien"
#: src/component/p-photo-search.vue:159
#: src/component/p-photo-toolbar.vue:159
msgid "All Colors"
msgstr "Alle Farben"
#: src/component/p-album-toolbar.vue:140 src/component/p-photo-search.vue:156
#: src/component/p-album-toolbar.vue:140 src/component/p-photo-toolbar.vue:156
msgid "All Countries"
msgstr "Alle Länder"
#: src/component/p-photo-search.vue:158
#: src/component/p-photo-toolbar.vue:158
msgid "All Lenses"
msgstr "Alle Linsen"
#: src/component/p-photo-search.vue:208
#: src/component/p-photo-toolbar.vue:208
msgid "All Years"
msgstr "Alle Jahre"
@ -155,7 +155,7 @@ msgid "Browse and edit image classification labels."
msgstr "Labels können angesehen und bearbeitet werden."
#: src/component/p-album-toolbar.vue:166 src/component/p-photo-list.vue:92
#: src/component/p-photo-search.vue:180 src/dialog/photo/details.vue:409
#: src/component/p-photo-toolbar.vue:180 src/dialog/photo/details.vue:409
msgid "Camera"
msgstr "Kamera"
@ -171,11 +171,11 @@ msgstr "Kamera"
msgid "Cancel"
msgstr "Abbrechen"
#: src/component/p-album-toolbar.vue:148 src/component/p-photo-search.vue:165
#: src/component/p-album-toolbar.vue:148 src/component/p-photo-toolbar.vue:165
msgid "Cards"
msgstr "Karten"
#: src/component/p-photo-search.vue:184 src/dialog/photo/details.vue:413
#: src/component/p-photo-toolbar.vue:184 src/dialog/photo/details.vue:413
msgid "Category"
msgstr "Kategorie"
@ -191,7 +191,7 @@ msgstr "Private Flag ändern"
msgid "Close"
msgstr "Schließen"
#: src/component/p-photo-search.vue:183 src/dialog/photo/details.vue:412
#: src/component/p-photo-toolbar.vue:183 src/dialog/photo/details.vue:412
msgid "Color"
msgstr "Farbe"
@ -215,7 +215,7 @@ msgstr "Verbinden"
msgid "Convert RAW files"
msgstr "RAW Dateien konvertieren"
#: src/component/p-album-toolbar.vue:165 src/component/p-photo-search.vue:179
#: src/component/p-album-toolbar.vue:165 src/component/p-photo-toolbar.vue:179
#: src/dialog/photo/details.vue:408
msgid "Country"
msgstr "Land"
@ -352,7 +352,7 @@ msgstr ""
msgid "General"
msgstr "Allgemein"
#: src/component/p-album-toolbar.vue:157 src/component/p-photo-search.vue:172
#: src/component/p-album-toolbar.vue:157 src/component/p-photo-toolbar.vue:172
msgid "Group by similarity"
msgstr "Ähnlichkeit"
@ -438,7 +438,7 @@ msgstr "Labels gelöscht"
msgid "Language"
msgstr "Sprache"
#: src/component/p-photo-search.vue:181 src/dialog/photo/details.vue:410
#: src/component/p-photo-toolbar.vue:181 src/dialog/photo/details.vue:410
msgid "Lens"
msgstr "Linse"
@ -446,7 +446,7 @@ msgstr "Linse"
msgid "Library"
msgstr "Bibliothek"
#: src/component/p-album-toolbar.vue:149 src/component/p-photo-search.vue:166
#: src/component/p-album-toolbar.vue:149 src/component/p-photo-toolbar.vue:166
msgid "List"
msgstr "Liste"
@ -499,11 +499,11 @@ msgstr "Über 20 Labels gefunden"
msgid "More than 50 photos found"
msgstr "Über 50 Fotos gefunden"
#: src/component/p-album-toolbar.vue:147 src/component/p-photo-search.vue:164
#: src/component/p-album-toolbar.vue:147 src/component/p-photo-toolbar.vue:164
msgid "Mosaic"
msgstr "Mosaik"
#: src/component/p-album-toolbar.vue:158 src/component/p-photo-search.vue:173
#: src/component/p-album-toolbar.vue:158 src/component/p-photo-toolbar.vue:173
msgid "Most relevant"
msgstr "Relevanz"
@ -529,7 +529,7 @@ msgstr "Name zu lang"
msgid "Never"
msgstr "Nie"
#: src/component/p-album-toolbar.vue:155 src/component/p-photo-search.vue:170
#: src/component/p-album-toolbar.vue:155 src/component/p-photo-toolbar.vue:170
msgid "Newest first"
msgstr "Neueste zuerst"
@ -584,7 +584,7 @@ msgstr ""
msgid "Nothing to see here yet. Be patient."
msgstr "Noch nichts zu sehen. Gedulde dich noch ein bisschen."
#: src/component/p-album-toolbar.vue:156 src/component/p-photo-search.vue:171
#: src/component/p-album-toolbar.vue:156 src/component/p-photo-toolbar.vue:171
msgid "Oldest first"
msgstr "Älteste zuerst"
@ -687,7 +687,7 @@ msgid "Re-index all originals, including already indexed and unchanged files."
msgstr ""
"Indexiere alle Dateien, auch bereits indexierte und unveränderte Dateien."
#: src/component/p-album-toolbar.vue:154 src/component/p-photo-search.vue:169
#: src/component/p-album-toolbar.vue:154 src/component/p-photo-toolbar.vue:169
msgid "Recently imported"
msgstr "Zuletzt importiert"
@ -729,7 +729,7 @@ msgstr "Review"
msgid "Save"
msgstr "Speichern"
#: src/component/p-album-toolbar.vue:163 src/component/p-photo-search.vue:177
#: src/component/p-album-toolbar.vue:163 src/component/p-photo-toolbar.vue:177
#: src/dialog/photo/details.vue:406 src/dialog/photo/labels.vue:111
#: src/pages/albums.vue:183 src/pages/labels.vue:184 src/pages/places.vue:66
msgid "Search"
@ -781,7 +781,7 @@ msgstr "Ähnlich"
msgid "Size"
msgstr "Größe"
#: src/component/p-album-toolbar.vue:167 src/component/p-photo-search.vue:185
#: src/component/p-album-toolbar.vue:167 src/component/p-photo-toolbar.vue:185
#: src/dialog/photo/details.vue:414
msgid "Sort By"
msgstr "Sortiere nach"
@ -822,11 +822,11 @@ msgstr "Synchronisiert"
msgid "Taken"
msgstr "Erstellt am"
#: src/component/p-photo-search.vue:187 src/dialog/photo/details.vue:416
#: src/component/p-photo-toolbar.vue:187 src/dialog/photo/details.vue:416
msgid "Taken after"
msgstr "Erstell nach"
#: src/component/p-photo-search.vue:186 src/dialog/photo/details.vue:415
#: src/component/p-photo-toolbar.vue:186 src/dialog/photo/details.vue:415
msgid "Taken before"
msgstr "Erstellt vor"
@ -910,7 +910,7 @@ msgstr "Nutzername"
msgid "Video"
msgstr "Video"
#: src/component/p-album-toolbar.vue:164 src/component/p-photo-search.vue:178
#: src/component/p-album-toolbar.vue:164 src/component/p-photo-toolbar.vue:178
#: src/dialog/photo/details.vue:407
msgid "View"
msgstr "Ansicht"
@ -919,7 +919,7 @@ msgstr "Ansicht"
msgid "Week"
msgstr "Woche"
#: src/component/p-photo-search.vue:182 src/dialog/photo/details.vue:411
#: src/component/p-photo-toolbar.vue:182 src/dialog/photo/details.vue:411
msgid "Year"
msgstr "Jahr"

View file

@ -64,11 +64,11 @@ msgstr "Albums deleted"
msgid "All "
msgstr "All "
#: src/component/p-album-search.vue:107 src/component/p-photo-search.vue:157
#: src/component/p-album-search.vue:107 src/component/p-photo-toolbar.vue:157
msgid "All Cameras"
msgstr "All Cameras"
#: src/component/p-album-search.vue:110 src/component/p-photo-search.vue:160
#: src/component/p-album-search.vue:110 src/component/p-photo-toolbar.vue:160
msgid "All Countries"
msgstr "All Countries"
@ -81,7 +81,7 @@ msgid "Are you sure you want to delete these photos?"
msgstr "Are you sure you want to delete these photos?"
#: src/component/p-album-search.vue:134 src/component/p-photo-list.vue:53
#: src/component/p-photo-search.vue:186
#: src/component/p-photo-toolbar.vue:186
msgid "Camera"
msgstr "Camera"
@ -93,7 +93,7 @@ msgid "Cancel"
msgstr "Cancel"
#: src/component/p-album-search.vue:133 src/component/p-photo-list.vue:52
#: src/component/p-photo-search.vue:185
#: src/component/p-photo-toolbar.vue:185
msgid "Country"
msgstr "Country"
@ -121,7 +121,7 @@ msgstr "Day"
msgid "Delete"
msgstr "Delete"
#: src/component/p-album-search.vue:119 src/component/p-photo-search.vue:169
#: src/component/p-album-search.vue:119 src/component/p-photo-toolbar.vue:169
msgid "Details"
msgstr "Details"
@ -204,7 +204,7 @@ msgstr "Language"
msgid "Library"
msgstr "Library"
#: src/component/p-album-search.vue:120 src/component/p-photo-search.vue:170
#: src/component/p-album-search.vue:120 src/component/p-photo-toolbar.vue:170
msgid "List"
msgstr "List"
@ -248,7 +248,7 @@ msgstr "More than 20 labels found"
msgid "More than 50 photos found"
msgstr "More than 50 photos found"
#: src/component/p-album-search.vue:118 src/component/p-photo-search.vue:168
#: src/component/p-album-search.vue:118 src/component/p-photo-toolbar.vue:168
msgid "Mosaic"
msgstr "Mosaic"
@ -256,7 +256,7 @@ msgstr "Mosaic"
msgid "New Album"
msgstr "New Album"
#: src/component/p-album-search.vue:125 src/component/p-photo-search.vue:175
#: src/component/p-album-search.vue:125 src/component/p-photo-toolbar.vue:175
msgid "Newest first"
msgstr "Newest first"
@ -303,7 +303,7 @@ msgstr "Nothing to see here yet. Be patient."
msgid "of"
msgstr "of"
#: src/component/p-album-search.vue:126 src/component/p-photo-search.vue:176
#: src/component/p-album-search.vue:126 src/component/p-photo-toolbar.vue:176
msgid "Oldest first"
msgstr "Oldest first"
@ -351,7 +351,7 @@ msgstr "Prev"
msgid "Private"
msgstr "Private"
#: src/component/p-album-search.vue:127 src/component/p-photo-search.vue:177
#: src/component/p-album-search.vue:127 src/component/p-photo-toolbar.vue:177
msgid "Recently imported"
msgstr "Recently imported"
@ -367,7 +367,7 @@ msgstr "Remove from album"
msgid "Save"
msgstr "Save"
#: src/component/p-album-search.vue:131 src/component/p-photo-search.vue:183
#: src/component/p-album-search.vue:131 src/component/p-photo-toolbar.vue:183
#: src/pages/albums.vue:162 src/pages/labels.vue:116 src/pages/places.vue:73
msgid "Search"
msgstr "Search"
@ -396,7 +396,7 @@ msgstr "Sign in"
msgid "Skip existing photos and sidecar files"
msgstr "Skip existing photos and sidecar files"
#: src/component/p-album-search.vue:135 src/component/p-photo-search.vue:187
#: src/component/p-album-search.vue:135 src/component/p-photo-toolbar.vue:187
msgid "Sort By"
msgstr "Sort By"
@ -408,7 +408,7 @@ msgstr "Start"
msgid "Story"
msgstr "Story"
#: src/component/p-photo-search.vue:189
#: src/component/p-photo-toolbar.vue:189
msgid "Taken after"
msgstr "Taken after"
@ -416,7 +416,7 @@ msgstr "Taken after"
msgid "Taken At"
msgstr "Taken At"
#: src/component/p-photo-search.vue:188
#: src/component/p-photo-toolbar.vue:188
msgid "Taken before"
msgstr "Taken before"
@ -424,7 +424,7 @@ msgstr "Taken before"
msgid "Theme"
msgstr "Theme"
#: src/component/p-album-search.vue:117 src/component/p-photo-search.vue:167
#: src/component/p-album-search.vue:117 src/component/p-photo-toolbar.vue:167
msgid "Tiles"
msgstr "Tiles"
@ -493,7 +493,7 @@ msgstr "Uploading photos…"
msgid "Vibrant"
msgstr "Vibrant"
#: src/component/p-album-search.vue:132 src/component/p-photo-search.vue:184
#: src/component/p-album-search.vue:132 src/component/p-photo-toolbar.vue:184
msgid "View"
msgstr "View"

View file

@ -64,11 +64,11 @@ msgstr "Albums verwijderd"
msgid "All "
msgstr "Alle "
#: src/component/p-album-search.vue:107 src/component/p-photo-search.vue:157
#: src/component/p-album-search.vue:107 src/component/p-photo-toolbar.vue:157
msgid "All Cameras"
msgstr "Alle cameras"
#: src/component/p-album-search.vue:110 src/component/p-photo-search.vue:160
#: src/component/p-album-search.vue:110 src/component/p-photo-toolbar.vue:160
msgid "All Countries"
msgstr "Alle landen"
@ -81,7 +81,7 @@ msgid "Are you sure you want to delete these photos?"
msgstr "Weet je zeker dat je deze fotos wil verwijderen?"
#: src/component/p-album-search.vue:134 src/component/p-photo-list.vue:53
#: src/component/p-photo-search.vue:186
#: src/component/p-photo-toolbar.vue:186
msgid "Camera"
msgstr "Camera"
@ -93,7 +93,7 @@ msgid "Cancel"
msgstr "Annuleren"
#: src/component/p-album-search.vue:133 src/component/p-photo-list.vue:52
#: src/component/p-photo-search.vue:185
#: src/component/p-photo-toolbar.vue:185
msgid "Country"
msgstr "Land"
@ -121,7 +121,7 @@ msgstr "Dag"
msgid "Delete"
msgstr "Verwijder"
#: src/component/p-album-search.vue:119 src/component/p-photo-search.vue:169
#: src/component/p-album-search.vue:119 src/component/p-photo-toolbar.vue:169
msgid "Details"
msgstr "Details"
@ -204,7 +204,7 @@ msgstr "Taal"
msgid "Library"
msgstr "Bibliotheek"
#: src/component/p-album-search.vue:120 src/component/p-photo-search.vue:170
#: src/component/p-album-search.vue:120 src/component/p-photo-toolbar.vue:170
msgid "List"
msgstr "Lijst"
@ -248,7 +248,7 @@ msgstr "Meer dan 20 labels gevonden"
msgid "More than 50 photos found"
msgstr "Meer dan 50 fotos gevonden"
#: src/component/p-album-search.vue:118 src/component/p-photo-search.vue:168
#: src/component/p-album-search.vue:118 src/component/p-photo-toolbar.vue:168
msgid "Mosaic"
msgstr "Mozaiek"
@ -256,7 +256,7 @@ msgstr "Mozaiek"
msgid "New Album"
msgstr "Nieuw album"
#: src/component/p-album-search.vue:125 src/component/p-photo-search.vue:175
#: src/component/p-album-search.vue:125 src/component/p-photo-toolbar.vue:175
msgid "Newest first"
msgstr "Nieuwste eerst"
@ -303,7 +303,7 @@ msgstr "Niets te zien hier. Weer geduldig."
msgid "of"
msgstr "van"
#: src/component/p-album-search.vue:126 src/component/p-photo-search.vue:176
#: src/component/p-album-search.vue:126 src/component/p-photo-toolbar.vue:176
msgid "Oldest first"
msgstr "Oudste eerst"
@ -351,7 +351,7 @@ msgstr "Vorige"
msgid "Private"
msgstr "Privé"
#: src/component/p-album-search.vue:127 src/component/p-photo-search.vue:177
#: src/component/p-album-search.vue:127 src/component/p-photo-toolbar.vue:177
msgid "Recently imported"
msgstr "Recent geïmporteerd"
@ -367,7 +367,7 @@ msgstr "Verwijder uit album"
msgid "Save"
msgstr "Opslaan"
#: src/component/p-album-search.vue:131 src/component/p-photo-search.vue:183
#: src/component/p-album-search.vue:131 src/component/p-photo-toolbar.vue:183
#: src/pages/albums.vue:162 src/pages/labels.vue:116 src/pages/places.vue:73
msgid "Search"
msgstr "Zoeken"
@ -396,7 +396,7 @@ msgstr "Inloggen"
msgid "Skip existing photos and sidecar files"
msgstr "Sla bestaande fotos en sidecar-bestanden over"
#: src/component/p-album-search.vue:135 src/component/p-photo-search.vue:187
#: src/component/p-album-search.vue:135 src/component/p-photo-toolbar.vue:187
msgid "Sort By"
msgstr "Sorteer op"
@ -408,7 +408,7 @@ msgstr "Start"
msgid "Story"
msgstr "Verhaal"
#: src/component/p-photo-search.vue:189
#: src/component/p-photo-toolbar.vue:189
msgid "Taken after"
msgstr "Genomen na"
@ -416,7 +416,7 @@ msgstr "Genomen na"
msgid "Taken At"
msgstr "Genomen op"
#: src/component/p-photo-search.vue:188
#: src/component/p-photo-toolbar.vue:188
msgid "Taken before"
msgstr "Genomen voor"
@ -424,7 +424,7 @@ msgstr "Genomen voor"
msgid "Theme"
msgstr "Thema"
#: src/component/p-album-search.vue:117 src/component/p-photo-search.vue:167
#: src/component/p-album-search.vue:117 src/component/p-photo-toolbar.vue:167
msgid "Tiles"
msgstr "Tegels"
@ -493,7 +493,7 @@ msgstr "Fotos uploaden…"
msgid "Vibrant"
msgstr "Levendig"
#: src/component/p-album-search.vue:132 src/component/p-photo-search.vue:184
#: src/component/p-album-search.vue:132 src/component/p-photo-toolbar.vue:184
msgid "View"
msgstr "Weergave"

View file

@ -68,27 +68,27 @@ msgstr "Альбом удален"
msgid "All "
msgstr "Все "
#: src/component/p-album-toolbar.vue:124 src/component/p-photo-search.vue:150
#: src/component/p-album-toolbar.vue:124 src/component/p-photo-toolbar.vue:150
msgid "All Cameras"
msgstr "Все Камеры"
#: src/component/p-photo-search.vue:153
#: src/component/p-photo-toolbar.vue:153
msgid "All Categories"
msgstr "Все Категории"
#: src/component/p-photo-search.vue:152
#: src/component/p-photo-toolbar.vue:152
msgid "All Colors"
msgstr "Все Цвета"
#: src/component/p-album-toolbar.vue:128 src/component/p-photo-search.vue:149
#: src/component/p-album-toolbar.vue:128 src/component/p-photo-toolbar.vue:149
msgid "All Countries"
msgstr "Все Страны"
#: src/component/p-photo-search.vue:151
#: src/component/p-photo-toolbar.vue:151
msgid "All Lenses"
msgstr "Все Объективы"
#: src/component/p-photo-search.vue:199
#: src/component/p-photo-toolbar.vue:199
msgid "All Years"
msgstr "Все Года"
@ -111,7 +111,7 @@ msgid "Authentication required"
msgstr "Требуется авторизация"
#: src/component/p-album-toolbar.vue:151 src/component/p-photo-list.vue:53
#: src/component/p-photo-search.vue:171 src/dialog/photo-edit/meta.vue:311
#: src/component/p-photo-toolbar.vue:171 src/dialog/photo-edit/meta.vue:311
#: src/dialog/photo-edit/todo.vue:34
msgid "Camera"
msgstr "Камера"
@ -123,7 +123,7 @@ msgstr "Камера"
msgid "Cancel"
msgstr "Отменить"
#: src/component/p-photo-search.vue:175 src/dialog/photo-edit/meta.vue:315
#: src/component/p-photo-toolbar.vue:175 src/dialog/photo-edit/meta.vue:315
#: src/dialog/photo-edit/todo.vue:38
msgid "Category"
msgstr "Категория"
@ -132,7 +132,7 @@ msgstr "Категория"
msgid "Close"
msgstr "Закрыть"
#: src/component/p-photo-search.vue:174 src/dialog/photo-edit/meta.vue:314
#: src/component/p-photo-toolbar.vue:174 src/dialog/photo-edit/meta.vue:314
#: src/dialog/photo-edit/todo.vue:37
msgid "Color"
msgstr "Цвет"
@ -145,7 +145,7 @@ msgstr "Цвета"
msgid "Convert RAW to JPEG"
msgstr "Конвертировать RAW в JPEG"
#: src/component/p-album-toolbar.vue:150 src/component/p-photo-search.vue:170
#: src/component/p-album-toolbar.vue:150 src/component/p-photo-toolbar.vue:170
#: src/dialog/photo-edit/meta.vue:310 src/dialog/photo-edit/todo.vue:33
msgid "Country"
msgstr "Страна"
@ -171,7 +171,7 @@ msgstr "День"
msgid "Delete"
msgstr "Удалить"
#: src/component/p-album-toolbar.vue:136 src/component/p-photo-search.vue:158
#: src/component/p-album-toolbar.vue:136 src/component/p-photo-toolbar.vue:158
#: src/dialog/p-photo-edit-dialog.vue:32
msgid "Details"
msgstr "Подробно"
@ -255,7 +255,7 @@ msgstr "Метки"
msgid "Language"
msgstr "Язык"
#: src/component/p-photo-search.vue:172 src/dialog/photo-edit/meta.vue:312
#: src/component/p-photo-toolbar.vue:172 src/dialog/photo-edit/meta.vue:312
#: src/dialog/photo-edit/todo.vue:35
msgid "Lens"
msgstr "Объектив"
@ -264,7 +264,7 @@ msgstr "Объектив"
msgid "Library"
msgstr "Библиотека"
#: src/component/p-album-toolbar.vue:137 src/component/p-photo-search.vue:159
#: src/component/p-album-toolbar.vue:137 src/component/p-photo-toolbar.vue:159
msgid "List"
msgstr "Список"
@ -308,7 +308,7 @@ msgstr "Более 20 меток найдено"
msgid "More than 50 photos found"
msgstr "Более 50 фотографий найдено"
#: src/component/p-album-toolbar.vue:135 src/component/p-photo-search.vue:157
#: src/component/p-album-toolbar.vue:135 src/component/p-photo-toolbar.vue:157
msgid "Mosaic"
msgstr "Мозайка"
@ -316,7 +316,7 @@ msgstr "Мозайка"
msgid "Name"
msgstr "Название"
#: src/component/p-album-toolbar.vue:143 src/component/p-photo-search.vue:163
#: src/component/p-album-toolbar.vue:143 src/component/p-photo-toolbar.vue:163
msgid "Newest first"
msgstr "Сначала новые"
@ -354,7 +354,7 @@ msgstr "Еще не реализованно"
msgid "Nothing to see here yet. Be patient."
msgstr "Пока что тут нечего смотреть. Будьте терпеливы."
#: src/component/p-album-toolbar.vue:144 src/component/p-photo-search.vue:164
#: src/component/p-album-toolbar.vue:144 src/component/p-photo-toolbar.vue:164
msgid "Oldest first"
msgstr "Сначала старые"
@ -414,7 +414,7 @@ msgstr "Приоритет"
msgid "Random"
msgstr "Случайно"
#: src/component/p-album-toolbar.vue:142 src/component/p-photo-search.vue:162
#: src/component/p-album-toolbar.vue:142 src/component/p-photo-toolbar.vue:162
msgid "Recently imported"
msgstr "Недавно добавленные"
@ -438,7 +438,7 @@ msgstr "Восстановить"
msgid "Save"
msgstr "Сохранить"
#: src/component/p-album-toolbar.vue:148 src/component/p-photo-search.vue:168
#: src/component/p-album-toolbar.vue:148 src/component/p-photo-toolbar.vue:168
#: src/dialog/photo-edit/meta.vue:308 src/dialog/photo-edit/todo.vue:31
#: src/pages/albums.vue:175 src/pages/labels.vue:134 src/pages/places.vue:63
msgid "Search"
@ -480,7 +480,7 @@ msgstr "Похожие"
msgid "Skip unchanged files"
msgstr "Пропустить не измененные файлы"
#: src/component/p-album-toolbar.vue:152 src/component/p-photo-search.vue:176
#: src/component/p-album-toolbar.vue:152 src/component/p-photo-toolbar.vue:176
#: src/dialog/photo-edit/meta.vue:316 src/dialog/photo-edit/todo.vue:39
msgid "Sort By"
msgstr "Сортировать по"
@ -489,7 +489,7 @@ msgstr "Сортировать по"
msgid "Story"
msgstr "История"
#: src/component/p-photo-search.vue:178 src/dialog/photo-edit/meta.vue:318
#: src/component/p-photo-toolbar.vue:178 src/dialog/photo-edit/meta.vue:318
#: src/dialog/photo-edit/todo.vue:41
msgid "Taken after"
msgstr "Снято после"
@ -498,7 +498,7 @@ msgstr "Снято после"
msgid "Taken At"
msgstr "Дата съемки"
#: src/component/p-photo-search.vue:177 src/dialog/photo-edit/meta.vue:317
#: src/component/p-photo-toolbar.vue:177 src/dialog/photo-edit/meta.vue:317
#: src/dialog/photo-edit/todo.vue:40
msgid "Taken before"
msgstr "Снято до"
@ -569,7 +569,7 @@ msgstr "Загрузка фотографий..."
msgid "Vibrant"
msgstr "Вибрант"
#: src/component/p-album-toolbar.vue:149 src/component/p-photo-search.vue:169
#: src/component/p-album-toolbar.vue:149 src/component/p-photo-toolbar.vue:169
#: src/dialog/photo-edit/meta.vue:309 src/dialog/photo-edit/todo.vue:32
msgid "View"
msgstr "Обзор"
@ -582,7 +582,7 @@ msgstr "Неделя"
msgid "Width"
msgstr "Ширина"
#: src/component/p-photo-search.vue:173 src/dialog/photo-edit/meta.vue:313
#: src/component/p-photo-toolbar.vue:173 src/dialog/photo-edit/meta.vue:313
#: src/dialog/photo-edit/todo.vue:36
msgid "Year"
msgstr "Год"

View file

@ -88,28 +88,28 @@ msgid "All "
msgstr ""
#: src/component/p-album-toolbar.vue:136
#: src/component/p-photo-search.vue:157
#: src/component/p-photo-toolbar.vue:157
msgid "All Cameras"
msgstr ""
#: src/component/p-photo-search.vue:160
#: src/component/p-photo-toolbar.vue:160
msgid "All Categories"
msgstr ""
#: src/component/p-photo-search.vue:159
#: src/component/p-photo-toolbar.vue:159
msgid "All Colors"
msgstr ""
#: src/component/p-album-toolbar.vue:140
#: src/component/p-photo-search.vue:156
#: src/component/p-photo-toolbar.vue:156
msgid "All Countries"
msgstr ""
#: src/component/p-photo-search.vue:158
#: src/component/p-photo-toolbar.vue:158
msgid "All Lenses"
msgstr ""
#: src/component/p-photo-search.vue:208
#: src/component/p-photo-toolbar.vue:208
msgid "All Years"
msgstr ""
@ -158,7 +158,7 @@ msgstr ""
#: src/component/p-album-toolbar.vue:166
#: src/component/p-photo-list.vue:92
#: src/component/p-photo-search.vue:180
#: src/component/p-photo-toolbar.vue:180
#: src/dialog/photo/details.vue:409
msgid "Camera"
msgstr ""
@ -177,11 +177,11 @@ msgid "Cancel"
msgstr ""
#: src/component/p-album-toolbar.vue:148
#: src/component/p-photo-search.vue:165
#: src/component/p-photo-toolbar.vue:165
msgid "Cards"
msgstr ""
#: src/component/p-photo-search.vue:184
#: src/component/p-photo-toolbar.vue:184
#: src/dialog/photo/details.vue:413
msgid "Category"
msgstr ""
@ -198,7 +198,7 @@ msgstr ""
msgid "Close"
msgstr ""
#: src/component/p-photo-search.vue:183
#: src/component/p-photo-toolbar.vue:183
#: src/dialog/photo/details.vue:412
msgid "Color"
msgstr ""
@ -226,7 +226,7 @@ msgid "Convert RAW files"
msgstr ""
#: src/component/p-album-toolbar.vue:165
#: src/component/p-photo-search.vue:179
#: src/component/p-photo-toolbar.vue:179
#: src/dialog/photo/details.vue:408
msgid "Country"
msgstr ""
@ -365,7 +365,7 @@ msgid "General"
msgstr ""
#: src/component/p-album-toolbar.vue:157
#: src/component/p-photo-search.vue:172
#: src/component/p-photo-toolbar.vue:172
msgid "Group by similarity"
msgstr ""
@ -453,7 +453,7 @@ msgstr ""
msgid "Language"
msgstr ""
#: src/component/p-photo-search.vue:181
#: src/component/p-photo-toolbar.vue:181
#: src/dialog/photo/details.vue:410
msgid "Lens"
msgstr ""
@ -464,7 +464,7 @@ msgid "Library"
msgstr ""
#: src/component/p-album-toolbar.vue:149
#: src/component/p-photo-search.vue:166
#: src/component/p-photo-toolbar.vue:166
msgid "List"
msgstr ""
@ -516,12 +516,12 @@ msgid "More than 50 photos found"
msgstr ""
#: src/component/p-album-toolbar.vue:147
#: src/component/p-photo-search.vue:164
#: src/component/p-photo-toolbar.vue:164
msgid "Mosaic"
msgstr ""
#: src/component/p-album-toolbar.vue:158
#: src/component/p-photo-search.vue:173
#: src/component/p-photo-toolbar.vue:173
msgid "Most relevant"
msgstr ""
@ -546,7 +546,7 @@ msgid "Never"
msgstr ""
#: src/component/p-album-toolbar.vue:155
#: src/component/p-photo-search.vue:170
#: src/component/p-photo-toolbar.vue:170
msgid "Newest first"
msgstr ""
@ -602,7 +602,7 @@ msgid "Nothing to see here yet. Be patient."
msgstr ""
#: src/component/p-album-toolbar.vue:156
#: src/component/p-photo-search.vue:171
#: src/component/p-photo-toolbar.vue:171
msgid "Oldest first"
msgstr ""
@ -701,7 +701,7 @@ msgid "Re-index all originals, including already indexed and unchanged files."
msgstr ""
#: src/component/p-album-toolbar.vue:154
#: src/component/p-photo-search.vue:169
#: src/component/p-photo-toolbar.vue:169
msgid "Recently imported"
msgstr ""
@ -745,7 +745,7 @@ msgid "Save"
msgstr ""
#: src/component/p-album-toolbar.vue:163
#: src/component/p-photo-search.vue:177
#: src/component/p-photo-toolbar.vue:177
#: src/dialog/photo/details.vue:406
#: src/dialog/photo/labels.vue:111
#: src/pages/albums.vue:183
@ -802,7 +802,7 @@ msgid "Size"
msgstr ""
#: src/component/p-album-toolbar.vue:167
#: src/component/p-photo-search.vue:185
#: src/component/p-photo-toolbar.vue:185
#: src/dialog/photo/details.vue:414
msgid "Sort By"
msgstr ""
@ -843,12 +843,12 @@ msgstr ""
msgid "Taken"
msgstr ""
#: src/component/p-photo-search.vue:187
#: src/component/p-photo-toolbar.vue:187
#: src/dialog/photo/details.vue:416
msgid "Taken after"
msgstr ""
#: src/component/p-photo-search.vue:186
#: src/component/p-photo-toolbar.vue:186
#: src/dialog/photo/details.vue:415
msgid "Taken before"
msgstr ""
@ -935,7 +935,7 @@ msgid "Video"
msgstr ""
#: src/component/p-album-toolbar.vue:164
#: src/component/p-photo-search.vue:178
#: src/component/p-photo-toolbar.vue:178
#: src/dialog/photo/details.vue:407
msgid "View"
msgstr ""
@ -944,7 +944,7 @@ msgstr ""
msgid "Week"
msgstr ""
#: src/component/p-photo-search.vue:182
#: src/component/p-photo-toolbar.vue:182
#: src/dialog/photo/details.vue:411
msgid "Year"
msgstr ""

View file

@ -1,9 +1,8 @@
//search + filters?
//clipboard?
//views ?
//clipboard yes!
//create + delete album
//Add description to album (Update)

View file

@ -2,4 +2,4 @@
//download files
//clipboard
//clipboard yes

View file

@ -2,7 +2,7 @@
//views?
//clipboard?
//clipboard yes
//show only high prio labels + check labels exist

View file

@ -1,2 +1,2 @@
//upload file + import
//upload file + import from library and from navigation + delete later

View file

@ -1,34 +0,0 @@
import { Selector } from 'testcafe';
import testcafeconfig from './testcafeconfig';
import Page from "./page-model";
import { RequestLogger } from 'testcafe';
const logger = RequestLogger( /http:\/\/localhost:2342\/api\/v1\/photos*/ , {
logResponseHeaders: true,
logResponseBody: true
});
fixture`Test favorites page`
.page `localhost:2342/favorites`
.requestHooks(logger);
const page = new Page();
test('Like photo', async t => {
}),
test('Dislike photo', async t => {
const FavoritesCount = await Selector('.t-like.t-on').count;
await t
.click(Selector('.t-like.t-on'));
logger.clear();
await t.navigateTo("../favorites");
const request3 = await logger.requests[0].responseBody;
const FavoritesCountAfterDislike = await Selector('.t-like.t-on').count;
await t
.expect(FavoritesCountAfterDislike).eql(FavoritesCount - 1);
});

View file

@ -1,22 +0,0 @@
import { Selector } from 'testcafe';
import testcafeconfig from '../testcafeconfig';
import Page from "../page-model";
import { RequestLogger } from 'testcafe';
const logger = RequestLogger( /http:\/\/localhost:2342\/api\/v1\*/ , {
logResponseHeaders: true,
logResponseBody: true
});
fixture`Library page`
.page`localhost:2342/library`
.requestHooks(logger);
const page = new Page();
test('Upload image', async t => {
await t
//.click(Selector(button).withText('Upload'));
});

View file

@ -1,23 +0,0 @@
import { Selector } from 'testcafe';
import testcafeconfig from '../testcafeconfig';
import Page from "../page-model";
fixture`Use navigation`
.page`${testcafeconfig.url}`;
const page = new Page();
test('Navigate', async t => {
await page.openNav();
await t
.click('a[href="/albums"]')
.expect(Selector('div.p-page-albums').exists, {timeout: 5000}).ok();
await page.openNav();
await t
.click('a[href="/places"]')
.expect(Selector('#map').exists).ok();
await page.openNav();
await t
.click('a[href="/labels"]')
.expect(Selector('div.p-page-labels').exists, {timeout: 5000}).ok();
});

View file

@ -1,35 +0,0 @@
import { Selector } from 'testcafe';
import testcafeconfig from '../testcafeconfig';
import Page from "../page-model";
fixture`Test clipboard`
.page`${testcafeconfig.url}`;
const page = new Page();
test('Test selecting photos and clear clipboard', async t => {
const clipboardCount = await Selector('span.t-clipboard-count');
await page.selectNthPhoto(0);
await page.selectNthPhoto(2);
await t
.expect(clipboardCount.textContent).eql("2");
await page.unselectPhoto(0);
await t
.expect(clipboardCount.textContent).eql("1")
await page.openNav();
await t
.click('a[href="/labels"]')
.expect(Selector('main .p-page-labels').exists, {timeout: 5000}).ok();
await page.openNav();
await t
.click('a[href="/photos"]')
.expect(clipboardCount.textContent).eql("1")
.click(Selector('div.p-photo-clipboard'))
.click(Selector('.p-photo-clipboard-clear'), {timeout: 15000});
await t.expect(Selector('#t-clipboard').exists).eql(false);
});

View file

@ -1,19 +0,0 @@
import { Selector } from 'testcafe';
import { ClientFunction } from 'testcafe';
const getLocation = ClientFunction(() => document.location.href);
fixture`Test places page`
.page `localhost:2342/places`;
test('Test places', async t => {
await t
.expect(Selector('#map').exists, {timeout: 15000}).ok()
.expect(Selector('div.p-map-control').visible).ok();
await t
.typeText(Selector('input[aria-label="Search"]'), 'Berlin')
.pressKey('enter');
await t
.expect(Selector('div.p-map-control').visible).ok()
.expect(getLocation()).contains('Berlin');
});

View file

@ -1,26 +0,0 @@
import { Selector } from 'testcafe';
import testcafeconfig from '../testcafeconfig';
import Page from "../page-model";
import { ClientFunction } from 'testcafe';
fixture`Scroll to top`
.page`${testcafeconfig.url}`;
const page = new Page();
const scroll = ClientFunction((x, y) => window.scrollTo(x, y));
const getcurrentPosition = ClientFunction(() => window.pageYOffset);
test('Test scroll to top functionality', async t => {
await t
.expect(Selector('button.p-photo-scroll-top').exists).notOk()
.expect(getcurrentPosition()).eql(0)
.expect(Selector('div[class="v-image__image v-image__image--cover"]').nth(0).visible).ok();
await scroll(0, 1200);
await t
.expect(getcurrentPosition()).eql(1200);
await scroll(0, 900);
await t
.expect(getcurrentPosition()).eql(900)
.click(Selector('button.p-photo-scroll-top'))
.expect(getcurrentPosition()).eql(0);
});

View file

@ -272,27 +272,19 @@ func AddPhotosToAlbum(router *gin.RouterGroup, conf *config.Config) {
return
}
var added []entity.PhotoAlbum
added := a.AddPhotos(photos.UIDs())
for _, p := range photos {
pa := entity.PhotoAlbum{AlbumUID: a.AlbumUID, PhotoUID: p.PhotoUID, Hidden: false}
if err := pa.Save(); err != nil {
log.Errorf("album: %s", err.Error())
if len(added) > 0 {
if len(added) == 1 {
event.Success(fmt.Sprintf("one entry added to %s", txt.Quote(a.Title())))
} else {
added = append(added, pa)
event.Success(fmt.Sprintf("%d entries added to %s", len(added), txt.Quote(a.Title())))
}
PublishAlbumEvent(EntityUpdated, a.AlbumUID, c)
}
if len(added) == 1 {
event.Success(fmt.Sprintf("one photo added to %s", txt.Quote(a.AlbumTitle)))
} else {
event.Success(fmt.Sprintf("%d photos added to %s", len(added), txt.Quote(a.AlbumTitle)))
}
PublishAlbumEvent(EntityUpdated, a.AlbumUID, c)
c.JSON(http.StatusOK, gin.H{"message": "photos added to album", "album": a, "added": added})
c.JSON(http.StatusOK, gin.H{"message": "photos added to album", "album": a, "photos": photos.UIDs(), "added": added})
})
}
@ -324,18 +316,19 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup, conf *config.Config) {
return
}
for _, photoUID := range f.Photos {
pa := entity.PhotoAlbum{AlbumUID: a.AlbumUID, PhotoUID: photoUID, Hidden: true}
logError("album", pa.Save())
removed := a.RemovePhotos(f.Photos)
if len(removed) > 0 {
if len(removed) == 1 {
event.Success(fmt.Sprintf("one entry removed from %s", txt.Quote(a.Title())))
} else {
event.Success(fmt.Sprintf("%d entries removed from %s", len(removed), txt.Quote(txt.Quote(a.Title()))))
}
PublishAlbumEvent(EntityUpdated, a.AlbumUID, c)
}
// affected := entity.Db().Model(entity.PhotoAlbum{}).Where("album_uid = ? AND photo_uid IN (?)", a.AlbumUID, f.Photos).UpdateColumn("Hidden", true).RowsAffected
event.Success(fmt.Sprintf("entries removed from %s", a.AlbumTitle))
PublishAlbumEvent(EntityUpdated, a.AlbumUID, c)
c.JSON(http.StatusOK, gin.H{"message": "entries removed from album", "album": a, "photos": f.Photos})
c.JSON(http.StatusOK, gin.H{"message": "entries removed from album", "album": a, "photos": f.Photos, "removed": removed})
})
}

View file

@ -65,6 +65,11 @@ func StartImport(router *gin.RouterGroup, conf *config.Config) {
opt = photoprism.ImportOptionsCopy(path)
}
if len(f.Albums) > 0 {
log.Debugf("import: files will be added to album %s", strings.Join(f.Albums, " and "))
opt.Albums = f.Albums
}
imp.Start(opt)
if subPath != "" && path != conf.ImportPath() && fs.IsEmpty(path) {
@ -81,6 +86,10 @@ func StartImport(router *gin.RouterGroup, conf *config.Config) {
event.Publish("import.completed", event.Data{"path": path, "seconds": elapsed})
event.Publish("index.completed", event.Data{"path": path, "seconds": elapsed})
for _, uid := range f.Albums {
PublishAlbumEvent(EntityUpdated, uid, c)
}
UpdateClientConfig(conf)
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("import completed in %d s", elapsed)})

View file

@ -201,7 +201,7 @@ func (c *Config) ClientConfig() ClientConfig {
Select("MAX(photo_count) as label_max_photos, COUNT(*) AS labels").
Where("photo_count > 0").
Where("deleted_at IS NULL").
Where("(label_priority >= 0 || label_favorite = 1)").
Where("(label_priority >= 0 OR label_favorite = 1)").
Take(&result.Count)
c.Db().Table("albums").

View file

@ -19,6 +19,8 @@ const (
AccountSyncStatusSynced = "synced"
)
type Accounts []Account
// Account represents a remote service account for uploading, downloading or syncing media files.
type Account struct {
ID uint `gorm:"primary_key"`

View file

@ -1,6 +1,7 @@
package entity
import (
"fmt"
"strings"
"time"
@ -13,6 +14,8 @@ import (
"github.com/ulule/deepcopier"
)
type Albums []Album
// Album represents a photo album
type Album struct {
ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
@ -40,13 +43,49 @@ type Album struct {
DeletedAt *time.Time `sql:"index" json:"-" yaml:"-"`
}
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func (m *Album) BeforeCreate(scope *gorm.Scope) error {
if rnd.IsUID(m.AlbumUID, 'a') {
// AddPhotoToAlbums adds a photo UID to multiple albums and automatically creates them if needed.
func AddPhotoToAlbums(photo string, albums []string) (err error) {
if photo == "" || len(albums) == 0 {
// Do nothing.
return nil
}
return scope.SetColumn("AlbumUID", rnd.PPID('a'))
if !rnd.IsPPID(photo, 'p') {
return fmt.Errorf("album: invalid photo uid %s", photo)
}
for _, album := range albums {
var aUID string
if album == "" {
log.Debugf("album: empty album identifier while adding photo %s", photo)
continue
}
if rnd.IsPPID(album, 'a') {
aUID = album
} else {
a := NewAlbum(album, TypeAlbum)
if err = a.Find(); err == nil {
aUID = a.AlbumUID
} else if err = a.Create(); err == nil {
aUID = a.AlbumUID
} else {
log.Errorf("album: %s (add photo %s to albums)", err.Error(), photo)
}
}
if aUID != "" {
entry := PhotoAlbum{AlbumUID: aUID, PhotoUID: photo, Hidden: false}
if err = entry.Save(); err != nil {
log.Errorf("album: %s (add photo %s to albums)", err.Error(), photo)
}
}
}
return err
}
// NewAlbum creates a new album; default name is current month and year
@ -58,7 +97,6 @@ func NewAlbum(albumTitle, albumType string) *Album {
}
result := &Album{
AlbumUID: rnd.PPID('a'),
AlbumOrder: SortOrderOldest,
AlbumType: albumType,
CreatedAt: now,
@ -79,7 +117,6 @@ func NewFolderAlbum(albumTitle, albumSlug, albumFilter string) *Album {
now := time.Now().UTC()
result := &Album{
AlbumUID: rnd.PPID('a'),
AlbumOrder: SortOrderOldest,
AlbumType: TypeFolder,
AlbumTitle: albumTitle,
@ -101,7 +138,6 @@ func NewMomentsAlbum(albumTitle, albumSlug, albumFilter string) *Album {
now := time.Now().UTC()
result := &Album{
AlbumUID: rnd.PPID('a'),
AlbumOrder: SortOrderOldest,
AlbumType: TypeMoment,
AlbumTitle: albumTitle,
@ -128,7 +164,6 @@ func NewMonthAlbum(albumTitle, albumSlug string, year, month int) *Album {
now := time.Now().UTC()
result := &Album{
AlbumUID: rnd.PPID('a'),
AlbumOrder: SortOrderOldest,
AlbumType: TypeMonth,
AlbumTitle: albumTitle,
@ -143,6 +178,59 @@ func NewMonthAlbum(albumTitle, albumSlug string, year, month int) *Album {
return result
}
// FindAlbumBySlug finds a matching album or returns nil.
func FindAlbumBySlug(slug, albumType string) *Album {
result := Album{}
if err := UnscopedDb().Where("album_slug = ? AND album_type = ?", slug, albumType).First(&result).Error; err != nil {
return nil
}
return &result
}
// Find updates the entity with values from the database.
func (m *Album) Find() error {
if rnd.IsPPID(m.AlbumUID, 'a') {
log.Debugf("IS PPID: %s", m.AlbumUID)
if err := UnscopedDb().First(m, "album_uid = ?", m.AlbumUID).Error; err != nil {
return err
}
}
if err := UnscopedDb().First(m, "album_slug = ? AND album_type = ?", m.AlbumSlug, m.AlbumType).Error; err != nil {
return err
}
return nil
}
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func (m *Album) BeforeCreate(scope *gorm.Scope) error {
if rnd.IsUID(m.AlbumUID, 'a') {
return nil
}
return scope.SetColumn("AlbumUID", rnd.PPID('a'))
}
// String returns the id or name as string.
func (m *Album) String() string {
if m.AlbumSlug != "" {
return m.AlbumSlug
}
if m.AlbumTitle != "" {
return txt.Quote(m.AlbumTitle)
}
if m.AlbumUID != "" {
return m.AlbumUID
}
return "[unknown album]"
}
// Checks if the album is of type moment.
func (m *Album) IsMoment() bool {
return m.AlbumType == TypeMoment
@ -213,13 +301,37 @@ func (m *Album) Create() error {
return nil
}
// FindAlbum finds a matching album or returns nil.
func FindAlbum(slug, albumType string) *Album {
result := Album{}
// Returns the album title.
func (m *Album) Title() string {
return m.AlbumTitle
}
if err := UnscopedDb().Where("album_slug = ? AND album_type = ?", slug, albumType).First(&result).Error; err != nil {
return nil
// AddPhotos adds photos to an existing album.
func (m *Album) AddPhotos(UIDs []string) (added []PhotoAlbum) {
for _, uid := range UIDs {
entry := PhotoAlbum{AlbumUID: m.AlbumUID, PhotoUID: uid, Hidden: false}
if err := entry.Save(); err != nil {
log.Errorf("album: %s (add to album %s)", err.Error(), m)
} else {
added = append(added, entry)
}
}
return &result
return added
}
// RemovePhotos removes photos from an album.
func (m *Album) RemovePhotos(UIDs []string) (removed []PhotoAlbum) {
for _, uid := range UIDs {
entry := PhotoAlbum{AlbumUID: m.AlbumUID, PhotoUID: uid, Hidden: true}
if err := entry.Save(); err != nil {
log.Errorf("album: %s (remove from album %s)", err.Error(), m)
} else {
removed = append(removed, entry)
}
}
return removed
}

View file

@ -92,6 +92,24 @@ var AlbumFixtures = AlbumMap{
UpdatedAt: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
DeletedAt: nil,
},
"import": {
ID: 1000004,
CoverUID: "",
AlbumUID: "at6axuzitogaaiax",
AlbumSlug: "import",
AlbumType: TypeAlbum,
AlbumTitle: "Import Album",
AlbumDescription: "",
AlbumNotes: "",
AlbumOrder: "name",
AlbumTemplate: "",
AlbumFilter: "",
AlbumFavorite: false,
Links: []Link{},
CreatedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
UpdatedAt: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
DeletedAt: nil,
},
}
// CreateAlbumFixtures inserts known entities into the database for testing.

View file

@ -89,3 +89,31 @@ func TestAlbum_Save(t *testing.T) {
})
}
func TestAddPhotoToAlbums(t *testing.T) {
t.Run("success", func(t *testing.T) {
err := AddPhotoToAlbums("pt9jtxrexxvl0yh0", []string{"at6axuzitogaaiax"})
if err != nil {
t.Fatal(err)
}
a := Album{AlbumUID: "at6axuzitogaaiax"}
if err := a.Find(); err != nil {
t.Fatal(err)
}
var entries []PhotoAlbum
if err := Db().Where("album_uid = ? AND photo_uid = ?", "at6axuzitogaaiax", "pt9jtxrexxvl0yh0").Find(&entries).Error; err != nil {
t.Fatal(err)
}
if len(entries) < 1 {
t.Error("at least one album entry expected")
}
// t.Logf("photo album entries: %+v", entries)
})
}

View file

@ -41,4 +41,8 @@ const (
RootDefault = ""
RootImport = "import"
RootPath = "/"
Updated = "updated"
Created = "created"
Deleted = "deleted"
)

View file

@ -12,6 +12,8 @@ import (
"github.com/ulule/deepcopier"
)
type Files []File
// File represents an image or sidecar file that belongs to a photo.
type File struct {
ID uint `gorm:"primary_key" json:"-" yaml:"-"`

View file

@ -15,6 +15,8 @@ import (
"github.com/ulule/deepcopier"
)
type Folders []Folder
// Folder represents a file system directory.
type Folder struct {
Path string `gorm:"type:varbinary(255);unique_index:idx_folders_path_root;" json:"Path" yaml:"Path"`

View file

@ -17,6 +17,20 @@ import (
"github.com/ulule/deepcopier"
)
// Default photo result slice for simple use cases.
type Photos []Photo
// UIDs returns a slice of unique photo IDs.
func (m Photos) UIDs() []string {
result := make([]string, len(m))
for i, el := range m {
result[i] = el.PhotoUID
}
return result
}
// Photo represents a photo, all its properties, and link to all its images and sidecar files.
type Photo struct {
ID uint `gorm:"primary_key" yaml:"-"`
@ -31,8 +45,8 @@ type Photo struct {
PhotoDescription string `gorm:"type:text;" json:"Description" yaml:"Description,omitempty"`
DescriptionSrc string `gorm:"type:varbinary(8);" json:"DescriptionSrc" yaml:"DescriptionSrc,omitempty"`
Details Details `json:"Details" yaml:"Details"`
PhotoPath string `gorm:"type:varbinary(768);index;" yaml:"-"`
PhotoName string `gorm:"type:varbinary(255);" json:"PhotoName" yaml:"-"`
PhotoPath string `gorm:"type:varbinary(768);index;" json:"Path" yaml:"-"`
PhotoName string `gorm:"type:varbinary(255);" json:"Name" yaml:"-"`
OriginalName string `gorm:"type:varbinary(768);" json:"OriginalName" yaml:"OriginalName,omitempty"`
PhotoFavorite bool `json:"Favorite" yaml:"Favorite,omitempty"`
PhotoPrivate bool `json:"Private" yaml:"Private,omitempty"`
@ -132,7 +146,7 @@ func SavePhotoForm(model Photo, form form.Photo, geoApi string) error {
return nil
}
// String returns an entity identifier as string for use in logs.
// String returns the id or name as string.
func (m *Photo) String() string {
if m.PhotoUID == "" {
if m.PhotoName != "" {

View file

@ -1,12 +1,10 @@
package entity
import (
"path"
"time"
"github.com/photoprism/photoprism/internal/classify"
"github.com/photoprism/photoprism/internal/maps"
"github.com/photoprism/photoprism/pkg/txt"
"gopkg.in/ugjka/go-tz.v2/tz"
)
@ -117,7 +115,7 @@ func (m *Photo) UpdateLocation(geoApi string) (keywords []string, labels classif
}
if m.UnknownCountry() {
m.PhotoCountry = txt.CountryCode(path.Join(m.PhotoPath, m.PhotoName))
m.EstimateCountry()
}
if m.HasCountry() {

View file

@ -33,4 +33,15 @@ func TestPhoto_EstimateCountry(t *testing.T) {
assert.Equal(t, "de", m.CountryCode())
assert.Equal(t, "Germany", m.CountryName())
})
t.Run("ca", func(t *testing.T) {
m := Photo{PhotoTitle: "Port Lands / Gardiner Expressway / Toronto", PhotoPath: "2012/09", PhotoName: "20120910_231851_CA06E1AD", OriginalName: "demo/Toronto/port-lands--gardiner-expressway--toronto_7999515645_o.jpg"}
assert.Equal(t, UnknownCountry.ID, m.CountryCode())
assert.Equal(t, UnknownCountry.CountryName, m.CountryName())
m.EstimateCountry()
assert.Equal(t, "ca", m.CountryCode())
assert.Equal(t, "Canada", m.CountryName())
})
//OriginalName: "demo/Toronto/port-lands--gardiner-expressway--toronto_7999515645_o.jpg"
}

View file

@ -1,6 +1,7 @@
package form
type ImportOptions struct {
Path string `json:"path"`
Move bool `json:"move"`
Albums []string `json:"albums"`
Path string `json:"path"`
Move bool `json:"move"`
}

View file

@ -1,6 +1,7 @@
package photoprism
type ImportOptions struct {
Albums []string
Path string
Move bool
RemoveDotFiles bool

View file

@ -5,6 +5,7 @@ import (
"path"
"path/filepath"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
@ -129,6 +130,13 @@ func ImportWorker(jobs <-chan ImportJob) {
}
res := ind.MediaFile(related.Main, indexOpt, originalName)
if res.Success() {
if err := entity.AddPhotoToAlbums(res.PhotoUID, opt.Albums); err != nil {
log.Warn(err)
}
}
log.Infof("import: %s main %s file %s", res, related.Main.FileType(), txt.Quote(related.Main.RelativeName(ind.originalsPath())))
done[related.Main.FileName()] = true
} else {
@ -149,6 +157,8 @@ func ImportWorker(jobs <-chan ImportJob) {
log.Infof("import: %s related %s file %s", res, f.FileType(), txt.Quote(f.RelativeName(ind.originalsPath())))
}
}
}
}

View file

@ -157,6 +157,14 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
}
}
if originalName != "" {
file.OriginalName = originalName
if file.FilePrimary && photo.OriginalName == "" {
photo.OriginalName = originalName
}
}
if photo.PhotoQuality == -1 && file.FilePrimary {
// restore photos that have been purged automatically
photo.DeletedAt = nil
@ -358,14 +366,6 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
photo.UpdateYearMonth()
if originalName != "" {
file.OriginalName = originalName
if photo.OriginalName == "" {
photo.OriginalName = originalName
}
}
file.FileSidecar = m.IsSidecar()
file.FileVideo = m.IsVideo()
file.FileRoot = fileRoot

View file

@ -75,7 +75,7 @@ func (m *Moments) Start() (err error) {
Path: mom.Path,
}
if a := entity.FindAlbum(mom.Slug(), entity.TypeFolder); a != nil {
if a := entity.FindAlbumBySlug(mom.Slug(), entity.TypeFolder); a != nil {
if a.DeletedAt != nil {
// Nothing to do.
log.Debugf("moments: %s was deleted (%s)", txt.Quote(a.AlbumTitle), a.AlbumFilter)
@ -101,7 +101,7 @@ func (m *Moments) Start() (err error) {
log.Errorf("moments: %s", err.Error())
} else {
for _, mom := range results {
if a := entity.FindAlbum(mom.Slug(), entity.TypeMonth); a != nil {
if a := entity.FindAlbumBySlug(mom.Slug(), entity.TypeMonth); a != nil {
if a.DeletedAt != nil {
// Nothing to do.
log.Debugf("moments: %s was deleted (%s)", txt.Quote(a.AlbumTitle), a.AlbumFilter)
@ -128,7 +128,7 @@ func (m *Moments) Start() (err error) {
Year: mom.Year,
}
if a := entity.FindAlbum(mom.Slug(), entity.TypeMoment); a != nil {
if a := entity.FindAlbumBySlug(mom.Slug(), entity.TypeMoment); a != nil {
if a.DeletedAt != nil {
// Nothing to do.
log.Debugf("moments: %s was deleted (%s)", txt.Quote(a.AlbumTitle), a.AlbumFilter)
@ -158,7 +158,7 @@ func (m *Moments) Start() (err error) {
State: mom.State,
}
if a := entity.FindAlbum(mom.Slug(), entity.TypeMoment); a != nil {
if a := entity.FindAlbumBySlug(mom.Slug(), entity.TypeMoment); a != nil {
if a.DeletedAt != nil {
// Nothing to do.
log.Debugf("moments: %s was deleted (%s)", txt.Quote(a.AlbumTitle), a.AlbumFilter)
@ -186,7 +186,7 @@ func (m *Moments) Start() (err error) {
Label: mom.Label,
}
if a := entity.FindAlbum(mom.Slug(), entity.TypeMoment); a != nil {
if a := entity.FindAlbumBySlug(mom.Slug(), entity.TypeMoment); a != nil {
log.Debugf("moments: %s already exists (%s)", txt.Quote(mom.Title()), f.Serialize())
if f.Serialize() == a.AlbumFilter || a.DeletedAt != nil {

View file

@ -5,10 +5,8 @@ import (
"github.com/photoprism/photoprism/internal/form"
)
type Accounts []entity.Account
// AccountSearch returns a list of accounts.
func AccountSearch(f form.AccountSearch) (result Accounts, err error) {
func AccountSearch(f form.AccountSearch) (result entity.Accounts, err error) {
s := Db().Where(&entity.Account{})
if f.Share {

View file

@ -6,7 +6,7 @@ import (
)
// AccountUploads a list of files for uploading to a remote account.
func AccountUploads(a entity.Account, limit int) (results []entity.File, err error) {
func AccountUploads(a entity.Account, limit int) (results entity.Files, err error) {
s := Db().Where("files.file_missing = 0").
Where("files.id NOT IN (SELECT file_id FROM files_sync WHERE file_id > 0 AND account_id = ?)", a.ID)

View file

@ -6,10 +6,8 @@ import (
"github.com/photoprism/photoprism/internal/entity"
)
type Files []entity.File
// FilesByPath returns a slice of files in a given originals folder.
func FilesByPath(rootName, pathName string) (files Files, err error) {
func FilesByPath(rootName, pathName string) (files entity.Files, err error) {
if strings.HasPrefix(pathName, "/") {
pathName = pathName[1:]
}
@ -26,7 +24,7 @@ func FilesByPath(rootName, pathName string) (files Files, err error) {
}
// ExistingFiles returns not-missing and not-deleted file entities in the range of limit and offset sorted by id.
func ExistingFiles(limit int, offset int, pathName string) (files Files, err error) {
func ExistingFiles(limit int, offset int, pathName string) (files entity.Files, err error) {
if strings.HasPrefix(pathName, "/") {
pathName = pathName[1:]
}
@ -43,7 +41,7 @@ func ExistingFiles(limit int, offset int, pathName string) (files Files, err err
}
// FilesByUID
func FilesByUID(u []string, limit int, offset int) (files Files, err error) {
func FilesByUID(u []string, limit int, offset int) (files entity.Files, err error) {
if err := Db().Where("(photo_uid IN (?) AND file_primary = 1) OR file_uid IN (?)", u, u).Preload("Photo").Limit(limit).Offset(offset).Find(&files).Error; err != nil {
return files, err
}

View file

@ -7,17 +7,15 @@ import (
"github.com/photoprism/photoprism/pkg/fs"
)
type Folders []entity.Folder
// FoldersByPath returns a slice of folders in a given directory incl sub directories in recursive mode.
func FoldersByPath(rootName, rootPath, path string, recursive bool) (folders Folders, err error) {
func FoldersByPath(rootName, rootPath, path string, recursive bool) (folders entity.Folders, err error) {
dirs, err := fs.Dirs(filepath.Join(rootPath, path), recursive)
if err != nil {
return folders, err
}
folders = make(Folders, len(dirs))
folders = make(entity.Folders, len(dirs))
for i, dir := range dirs {
folder := entity.FindFolder(rootName, filepath.Join(path, dir))
@ -39,7 +37,7 @@ func FoldersByPath(rootName, rootPath, path string, recursive bool) (folders Fol
}
// AlbumFolders returns folders that should be added as album.
func AlbumFolders(threshold int) (folders Folders, err error) {
func AlbumFolders(threshold int) (folders entity.Folders, err error) {
db := UnscopedDb().Table("folders").
Select("folders.*, COUNT(photos.id) AS photo_count").
Joins("JOIN photos ON photos.photo_path = folders.path AND photos.deleted_at IS NULL AND photos.photo_quality >= 3").

View file

@ -73,7 +73,7 @@ func PhotoPreloadByUID(photoUID string) (photo entity.Photo, err error) {
}
// PhotosMissing returns photo entities without existing files.
func PhotosMissing(limit int, offset int) (entities Photos, err error) {
func PhotosMissing(limit int, offset int) (entities entity.Photos, err error) {
err = Db().
Select("photos.*").
Joins("JOIN files a ON photos.id = a.photo_id ").
@ -94,7 +94,7 @@ func ResetPhotoQuality() error {
}
// PhotosMaintenance returns photos selected for maintenance.
func PhotosMaintenance(limit int, offset int) (entities Photos, err error) {
func PhotosMaintenance(limit int, offset int) (entities entity.Photos, err error) {
err = Db().
Preload("Labels", func(db *gorm.DB) *gorm.DB {
return db.Order("photos_labels.uncertainty ASC, photos_labels.label_id DESC")

View file

@ -11,8 +11,6 @@ import (
"github.com/ulule/deepcopier"
)
// Default photo result slice for simple use cases.
type Photos []entity.Photo
// PhotoResult contains found photos and their main file plus other meta data.
type PhotoResult struct {
@ -26,6 +24,7 @@ type PhotoResult struct {
TimeZone string `json:"TimeZone"`
PhotoPath string `json:"Path"`
PhotoName string `json:"Name"`
OriginalName string `json:"OriginalName"`
PhotoTitle string `json:"Title"`
PhotoDescription string `json:"Description"`
PhotoYear int `json:"Year"`

View file

@ -4,11 +4,12 @@ import (
"errors"
"fmt"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
)
// PhotoSelection queries all selected photos.
func PhotoSelection(f form.Selection) (results Photos, err error) {
func PhotoSelection(f form.Selection) (results entity.Photos, err error) {
if f.Empty() {
return results, errors.New("no items selected")
}
@ -47,7 +48,7 @@ func PhotoSelection(f form.Selection) (results Photos, err error) {
}
// FileSelection queries all selected files e.g. for downloading.
func FileSelection(f form.Selection) (results Files, err error) {
func FileSelection(f form.Selection) (results entity.Files, err error) {
if f.Empty() {
return results, errors.New("no items selected")
}

View file

@ -3,6 +3,7 @@ package query
import (
"testing"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/stretchr/testify/assert"
)
@ -30,7 +31,7 @@ func TestPhotoSelection(t *testing.T) {
}
assert.Equal(t, 2, len(r))
assert.IsType(t, Photos{}, r)
assert.IsType(t, entity.Photos{}, r)
})
}
@ -57,6 +58,6 @@ func TestFileSelection(t *testing.T) {
}
assert.Equal(t, 3, len(r))
assert.IsType(t, Files{}, r)
assert.IsType(t, entity.Files{}, r)
})
}

View file

@ -29,7 +29,7 @@ func Base(fileName string, stripSequence bool) string {
}
// other common sequential naming schemes
if end := strings.Index(basename, " ("); end != -1 {
if end := strings.Index(basename, "("); end != -1 {
// copies created by Chrome & Windows, example: IMG_1234 (2)
basename = basename[:end]
} else if end := strings.Index(basename, " copy"); end != -1 {
@ -37,6 +37,8 @@ func Base(fileName string, stripSequence bool) string {
basename = basename[:end]
}
basename = strings.TrimSpace(basename)
return basename
}

View file

@ -67,6 +67,26 @@ func TestBase(t *testing.T) {
result := Base("/testdata/Test (3).jpg", false)
assert.Equal(t, "Test (3)", result)
})
t.Run("20180506_091537_DSC02122.JPG", func(t *testing.T) {
result := Base("20180506_091537_DSC02122.JPG", true)
assert.Equal(t, "20180506_091537_DSC02122", result)
})
t.Run("20180506_091537_DSC02122 (+3.3).JPG", func(t *testing.T) {
result := Base("20180506_091537_DSC02122 (+3.3).JPG", true)
assert.Equal(t, "20180506_091537_DSC02122", result)
})
t.Run("20180506_091537_DSC02122 (-2.7).JPG", func(t *testing.T) {
result := Base("20180506_091537_DSC02122 (-2.7).JPG", true)
assert.Equal(t, "20180506_091537_DSC02122", result)
})
t.Run("20180506_091537_DSC02122(+3.3).JPG", func(t *testing.T) {
result := Base("20180506_091537_DSC02122(+3.3).JPG", true)
assert.Equal(t, "20180506_091537_DSC02122", result)
})
t.Run("20180506_091537_DSC02122(-2.7).JPG", func(t *testing.T) {
result := Base("20180506_091537_DSC02122(-2.7).JPG", true)
assert.Equal(t, "20180506_091537_DSC02122", result)
})
}
func TestRelativeBase(t *testing.T) {

View file

@ -21,13 +21,43 @@ func IsPPID(s string, prefix byte) bool {
return false
}
return s[0] == prefix
return s[0] == prefix && IsLowerAlnum(s)
}
// IsHex returns true if the string only contains hex numbers, dashes and letters without whitespace.
func IsHex(s string) bool {
if s == "" {
return false
}
for _, r := range s {
if (r < 48 || r > 57) && (r < 97 || r > 102) && (r < 65 || r > 90) && r != 45{
return false
}
}
return true
}
// IsLowerAlnum returns true if the string only contains alphanumeric ascii chars without whitespace.
func IsLowerAlnum(s string) bool {
if s == "" {
return false
}
for _, r := range s {
if (r < 48 || r > 57) && (r < 97 || r > 122) {
return false
}
}
return true
}
// IsUID returns true if string is a seemingly unique id.
func IsUID(s string, prefix byte) bool {
// Regular UUID.
if len(s) == 36 {
if len(s) == 36 && IsHex(s) {
return true
}