Rename "thumbnails" to "thumbs" and group api functions in one file

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-07-13 17:25:27 +02:00
parent dac846ba01
commit 4d42222caf
31 changed files with 662 additions and 654 deletions

View file

@ -33,7 +33,7 @@ import PhotoSwipeUI_Default from "photoswipe/dist/photoswipe-ui-default.js";
import Event from "pubsub-js"; import Event from "pubsub-js";
import stripHtml from "string-strip-html"; import stripHtml from "string-strip-html";
const thumbs = window.__CONFIG__.thumbnails; const thumbs = window.__CONFIG__.thumbs;
class Viewer { class Viewer {
constructor() { constructor() {

View file

@ -278,7 +278,7 @@
model: Object, model: Object,
}, },
data() { data() {
const thumbs = this.$config.values.thumbnails; const thumbs = this.$config.values.thumbs;
return { return {
options: options, options: options,

View file

@ -33,7 +33,7 @@ import Api from "common/api";
import {config} from "../session"; import {config} from "../session";
import {$gettext} from "common/vm"; import {$gettext} from "common/vm";
const thumbs = window.__CONFIG__.thumbnails; const thumbs = window.__CONFIG__.thumbs;
export class Thumb extends Model { export class Thumb extends Model {
getDefaults() { getDefaults() {

Binary file not shown.

View file

@ -58,7 +58,7 @@ msgstr "Auf den Link klicken, um ihn zu kopieren."
msgid "Account" msgid "Account"
msgstr "Zugang" msgstr "Zugang"
#: src/dialog/photo/info.vue:122 #: src/dialog/photo/info.vue:128
msgid "Accuracy" msgid "Accuracy"
msgstr "Genauigkeit" msgstr "Genauigkeit"
@ -78,7 +78,7 @@ msgstr "Link hinzufügen"
msgid "Add photos or videos from search results by selecting them." msgid "Add photos or videos from search results by selecting them."
msgstr "Fotos und Videos können über das Kontextmenü hinzugefügt werden." msgstr "Fotos und Videos können über das Kontextmenü hinzugefügt werden."
#: src/dialog/account/add.vue:5 src/pages/settings/sync.vue:40 #: src/dialog/account/add.vue:5 src/pages/settings/sync.vue:46
msgid "Add Server" msgid "Add Server"
msgstr "Server hinzufügen" msgstr "Server hinzufügen"
@ -204,7 +204,7 @@ msgstr ""
"Alternativ können Dateien auch direkt auf kompatible WebDAV-Server " "Alternativ können Dateien auch direkt auf kompatible WebDAV-Server "
"hochgeladen z.B. Nextcloud." "hochgeladen z.B. Nextcloud."
#: src/dialog/photo/info.vue:114 #: src/dialog/photo/info.vue:120
msgid "Altitude" msgid "Altitude"
msgstr "Höhe" msgstr "Höhe"
@ -244,7 +244,7 @@ msgstr "Übernehmen"
msgid "Archive" msgid "Archive"
msgstr "Archiv" msgstr "Archiv"
#: src/dialog/photo/info.vue:162 #: src/dialog/photo/info.vue:168
msgid "Archived" msgid "Archived"
msgstr "Archiviert" msgstr "Archiviert"
@ -310,7 +310,7 @@ msgstr "Kalender"
msgid "Camera" msgid "Camera"
msgstr "Kamera" msgstr "Kamera"
#: src/dialog/photo/info.vue:67 #: src/dialog/photo/info.vue:73
msgid "Camera Serial" msgid "Camera Serial"
msgstr "Kamera-Seriennummer" msgstr "Kamera-Seriennummer"
@ -354,7 +354,7 @@ msgstr "Als privat markieren"
msgid "Change Status" msgid "Change Status"
msgstr "Status ändern" msgstr "Status ändern"
#: src/dialog/photo/info.vue:154 #: src/dialog/photo/info.vue:160
msgid "Checked" msgid "Checked"
msgstr "Geprüft" msgstr "Geprüft"
@ -387,7 +387,7 @@ msgstr "Wahrscheinlichkeit"
msgid "Connect" msgid "Connect"
msgstr "Verbinden" msgstr "Verbinden"
#: src/dialog/webdav.vue:4 src/pages/settings/sync.vue:45 #: src/dialog/webdav.vue:4 src/pages/settings/sync.vue:42
msgid "Connect via WebDAV" msgid "Connect via WebDAV"
msgstr "Mit WebDAV verbinden" msgstr "Mit WebDAV verbinden"
@ -439,7 +439,7 @@ msgstr "Land"
msgid "Create album" msgid "Create album"
msgstr "Album erstellen" msgstr "Album erstellen"
#: src/dialog/photo/info.vue:130 #: src/dialog/photo/info.vue:136
msgid "Created" msgid "Created"
msgstr "Hinzugefügt" msgstr "Hinzugefügt"
@ -501,7 +501,7 @@ msgstr "Auflösung"
#: src/dialog/account/edit.vue:61 src/dialog/account/edit.vue:120 #: src/dialog/account/edit.vue:61 src/dialog/account/edit.vue:120
msgid "Disabled" msgid "Disabled"
msgstr "Aus" msgstr "Deaktiviert"
#: src/routes.js:290 src/routes.js:297 src/routes.js:304 src/routes.js:311 #: src/routes.js:290 src/routes.js:297 src/routes.js:304 src/routes.js:311
msgid "Discover" msgid "Discover"
@ -568,7 +568,7 @@ msgstr "Account bearbeiten"
msgid "Edit Photo" msgid "Edit Photo"
msgstr "Foto bearbeiten" msgstr "Foto bearbeiten"
#: src/dialog/photo/info.vue:146 #: src/dialog/photo/info.vue:152
msgid "Edited" msgid "Edited"
msgstr "Bearbeitet" msgstr "Bearbeitet"
@ -632,7 +632,7 @@ msgstr "Fehler beim Importieren der hochgeladenen Dateien"
msgid "Fast" msgid "Fast"
msgstr "Schnell" msgstr "Schnell"
#: src/dialog/photo/info.vue:74 #: src/dialog/photo/info.vue:80
msgid "Favorite" msgid "Favorite"
msgstr "Favorit" msgstr "Favorit"
@ -666,8 +666,8 @@ msgid ""
"Files with sequential names like 'IMG_1234 (2)' or 'IMG_1234 copy 2' belong " "Files with sequential names like 'IMG_1234 (2)' or 'IMG_1234 copy 2' belong "
"to the same photo." "to the same photo."
msgstr "" msgstr ""
"Dateien mit Namen wie IMG_1234 (2) oder IMG_1234 copy 2 werden " "Dateien mit Namen wie IMG_1234 (2) oder IMG_1234 copy 2 werden als "
"zusammengefasst." "Stapel zusammengefasst."
#: src/dialog/photo/details.vue:448 #: src/dialog/photo/details.vue:448
msgid "Focal Length" msgid "Focal Length"
@ -856,11 +856,11 @@ msgstr "Kategorien gelöscht"
msgid "Language" msgid "Language"
msgstr "Sprache" msgstr "Sprache"
#: src/pages/settings/sync.vue:111 #: src/pages/settings/sync.vue:113
msgid "Last Backup" msgid "Last Backup"
msgstr "Letztes Backup" msgstr "Letztes Backup"
#: src/dialog/photo/details.vue:443 src/dialog/photo/info.vue:98 #: src/dialog/photo/details.vue:443 src/dialog/photo/info.vue:104
msgid "Latitude" msgid "Latitude"
msgstr "Breitengrad" msgstr "Breitengrad"
@ -938,7 +938,7 @@ msgstr "Abmelden"
msgid "Logs" msgid "Logs"
msgstr "Logs" msgstr "Logs"
#: src/dialog/photo/details.vue:444 src/dialog/photo/info.vue:106 #: src/dialog/photo/details.vue:444 src/dialog/photo/info.vue:112
msgid "Longitude" msgid "Longitude"
msgstr "Längengrad" msgstr "Längengrad"
@ -1032,7 +1032,7 @@ msgstr "Name"
msgid "Name too long" msgid "Name too long"
msgstr "Name zu lang" msgstr "Name zu lang"
#: src/pages/settings/sync.vue:122 src/resources/options.js:178 #: src/pages/settings/sync.vue:129 src/resources/options.js:178
#: src/resources/options.js:188 #: src/resources/options.js:188
msgid "Never" msgid "Never"
msgstr "Nie" msgstr "Nie"
@ -1046,8 +1046,8 @@ msgstr "Neues Passwort"
msgid "Newest first" msgid "Newest first"
msgstr "Neueste zuerst" msgstr "Neueste zuerst"
#: src/dialog/photo/archive.vue:13 src/dialog/photo/info.vue:187 #: src/dialog/photo/archive.vue:13 src/dialog/photo/info.vue:160
#: src/dialog/photo/info.vue:218 src/dialog/photo/info.vue:245 #: src/dialog/photo/info.vue:187 src/dialog/photo/info.vue:214
msgid "No" msgid "No"
msgstr "Nein" msgstr "Nein"
@ -1113,7 +1113,7 @@ msgstr ""
"sollen, ist kein Import notwendig. Die Ordner müssen in diesem Fall manuell " "sollen, ist kein Import notwendig. Die Ordner müssen in diesem Fall manuell "
"verwaltet und indiziert werden." "verwaltet und indiziert werden."
#: src/pages/settings/sync.vue:32 #: src/pages/settings/sync.vue:34
msgid "Note:" msgid "Note:"
msgstr "Hinweis:" msgstr "Hinweis:"
@ -1299,7 +1299,7 @@ msgstr "Voransicht"
msgid "Primary" msgid "Primary"
msgstr "Hauptdatei" msgstr "Hauptdatei"
#: src/component/navigation.vue:129 src/dialog/photo/info.vue:82 #: src/component/navigation.vue:129 src/dialog/photo/info.vue:88
#: src/routes.js:171 #: src/routes.js:171
msgid "Private" msgid "Private"
msgstr "Privat" msgstr "Privat"
@ -1312,7 +1312,7 @@ msgstr "Purpur"
msgid "Quality Filter" msgid "Quality Filter"
msgstr "Qualitätsfilter" msgstr "Qualitätsfilter"
#: src/dialog/photo/info.vue:53 #: src/dialog/photo/info.vue:59
msgid "Quality Score" msgid "Quality Score"
msgstr "Qualität" msgstr "Qualität"
@ -1370,7 +1370,7 @@ msgstr ""
msgid "Request failed - invalid response" msgid "Request failed - invalid response"
msgstr "Anfrage fehlgeschlagen - ungültige Antwort" msgstr "Anfrage fehlgeschlagen - ungültige Antwort"
#: src/dialog/photo/info.vue:61 #: src/dialog/photo/info.vue:67
msgid "Resolution" msgid "Resolution"
msgstr "Auflösung" msgstr "Auflösung"
@ -1395,7 +1395,7 @@ msgstr "Russisch"
msgid "Save" msgid "Save"
msgstr "Speichern" msgstr "Speichern"
#: src/dialog/photo/info.vue:90 #: src/dialog/photo/info.vue:96
msgid "Scan" msgid "Scan"
msgstr "Scan" msgstr "Scan"
@ -1440,7 +1440,7 @@ msgstr "Auswahl archiviert"
msgid "Selection restored" msgid "Selection restored"
msgstr "Auswahl wiederhergestellt" msgstr "Auswahl wiederhergestellt"
#: src/pages/settings/sync.vue:108 #: src/pages/settings/sync.vue:109
msgid "Server" msgid "Server"
msgstr "Server" msgstr "Server"
@ -1561,7 +1561,7 @@ msgid ""
"Support for additional services, like Google Drive, will be added over time." "Support for additional services, like Google Drive, will be added over time."
msgstr "Die Unterstützung weiterer Dienste, wie Google Drive, ist geplant." msgstr "Die Unterstützung weiterer Dienste, wie Google Drive, ist geplant."
#: src/pages/settings/sync.vue:110 #: src/pages/settings/sync.vue:111
msgid "Sync" msgid "Sync"
msgstr "Sync" msgstr "Sync"
@ -1569,7 +1569,8 @@ msgstr "Sync"
msgid "Sync raw images" msgid "Sync raw images"
msgstr "RAW-Dateien sichern" msgstr "RAW-Dateien sichern"
#: src/component/photo/list.vue:114 src/share/photo/list.vue:93 #: src/component/photo/list.vue:114 src/dialog/photo/info.vue:53
#: src/share/photo/list.vue:93
msgid "Taken" msgid "Taken"
msgstr "Aufgenommen" msgstr "Aufgenommen"
@ -1605,7 +1606,7 @@ msgstr ""
msgid "Theme" msgid "Theme"
msgstr "Theme" msgstr "Theme"
#: src/dialog/webdav.vue:17 src/pages/settings/sync.vue:34 #: src/dialog/webdav.vue:17 src/pages/settings/sync.vue:36
msgid "" msgid ""
"This mounts the originals folder as a network drive and allows you to open, " "This mounts the originals folder as a network drive and allows you to open, "
"edit, and delete files from your computer or smartphone as if they were " "edit, and delete files from your computer or smartphone as if they were "
@ -1669,7 +1670,7 @@ msgstr "Typ"
msgid "UID" msgid "UID"
msgstr "UID" msgstr "UID"
#: src/dialog/photo/details.vue:423 src/dialog/photo/info.vue:221 #: src/dialog/photo/details.vue:423 src/dialog/photo/info.vue:227
#: src/model/album.js:122 src/model/photo.js:399 src/model/photo.js:413 #: src/model/album.js:122 src/model/photo.js:399 src/model/photo.js:413
#: src/model/photo.js:436 src/model/photo.js:448 src/model/photo.js:525 #: src/model/photo.js:436 src/model/photo.js:448 src/model/photo.js:525
#: src/model/photo.js:538 src/pages/library/errors.vue:196 #: src/model/photo.js:538 src/pages/library/errors.vue:196
@ -1688,13 +1689,13 @@ msgstr "Unsortiert"
msgid "Unstack" msgid "Unstack"
msgstr "Aus Stapel entfernen" msgstr "Aus Stapel entfernen"
#: src/dialog/photo/files.vue:120 src/dialog/photo/info.vue:138 #: src/dialog/photo/files.vue:120 src/dialog/photo/info.vue:144
msgid "Updated" msgid "Updated"
msgstr "Geändert" msgstr "Geändert"
#: src/dialog/share/upload.vue:35 src/dialog/upload.vue:8 #: src/dialog/share/upload.vue:35 src/dialog/upload.vue:8
#: src/dialog/upload.vue:54 src/pages/library/import.vue:38 #: src/dialog/upload.vue:54 src/pages/library/import.vue:38
#: src/pages/settings/general.vue:319 src/pages/settings/sync.vue:109 #: src/pages/settings/general.vue:319 src/pages/settings/sync.vue:110
msgid "Upload" msgid "Upload"
msgstr "Upload" msgstr "Upload"
@ -1766,7 +1767,7 @@ msgid "WebDAV clients can connect to PhotoPrism using the following URL:"
msgstr "" msgstr ""
"WebDAV-Clients können sich über die folgende URL mit PhotoPrism verbinden:" "WebDAV-Clients können sich über die folgende URL mit PhotoPrism verbinden:"
#: src/pages/settings/sync.vue:33 #: src/pages/settings/sync.vue:35
msgid "" msgid ""
"WebDAV clients, like Microsofts Windows Explorer or Apple's Finder, can " "WebDAV clients, like Microsofts Windows Explorer or Apple's Finder, can "
"connect directly to PhotoPrism." "connect directly to PhotoPrism."
@ -1801,8 +1802,8 @@ msgstr "Gelb"
#: src/dialog/photo/archive.vue:15 src/dialog/photo/files.vue:80 #: src/dialog/photo/archive.vue:15 src/dialog/photo/files.vue:80
#: src/dialog/photo/files.vue:86 src/dialog/photo/files.vue:104 #: src/dialog/photo/files.vue:86 src/dialog/photo/files.vue:104
#: src/dialog/photo/files.vue:110 src/dialog/photo/info.vue:186 #: src/dialog/photo/files.vue:110 src/dialog/photo/info.vue:159
#: src/dialog/photo/info.vue:217 src/dialog/photo/info.vue:244 #: src/dialog/photo/info.vue:186 src/dialog/photo/info.vue:213
msgid "Yes" msgid "Yes"
msgstr "Ja" msgstr "Ja"

File diff suppressed because one or more lines are too long

View file

@ -53,7 +53,7 @@ msgstr ""
msgid "Account" msgid "Account"
msgstr "" msgstr ""
#: src/dialog/photo/info.vue:122 #: src/dialog/photo/info.vue:128
msgid "Accuracy" msgid "Accuracy"
msgstr "" msgstr ""
@ -74,7 +74,7 @@ msgid "Add photos or videos from search results by selecting them."
msgstr "" msgstr ""
#: src/dialog/account/add.vue:5 #: src/dialog/account/add.vue:5
#: src/pages/settings/sync.vue:40 #: src/pages/settings/sync.vue:46
msgid "Add Server" msgid "Add Server"
msgstr "" msgstr ""
@ -206,7 +206,7 @@ msgstr ""
msgid "Alternatively, you can upload files directly to WebDAV servers like Nextcloud." msgid "Alternatively, you can upload files directly to WebDAV servers like Nextcloud."
msgstr "" msgstr ""
#: src/dialog/photo/info.vue:114 #: src/dialog/photo/info.vue:120
msgid "Altitude" msgid "Altitude"
msgstr "" msgstr ""
@ -248,7 +248,7 @@ msgstr ""
msgid "Archive" msgid "Archive"
msgstr "" msgstr ""
#: src/dialog/photo/info.vue:162 #: src/dialog/photo/info.vue:168
msgid "Archived" msgid "Archived"
msgstr "" msgstr ""
@ -319,7 +319,7 @@ msgstr ""
msgid "Camera" msgid "Camera"
msgstr "" msgstr ""
#: src/dialog/photo/info.vue:67 #: src/dialog/photo/info.vue:73
msgid "Camera Serial" msgid "Camera Serial"
msgstr "" msgstr ""
@ -375,7 +375,7 @@ msgstr ""
msgid "Change Status" msgid "Change Status"
msgstr "" msgstr ""
#: src/dialog/photo/info.vue:154 #: src/dialog/photo/info.vue:160
msgid "Checked" msgid "Checked"
msgstr "" msgstr ""
@ -411,7 +411,7 @@ msgid "Connect"
msgstr "" msgstr ""
#: src/dialog/webdav.vue:4 #: src/dialog/webdav.vue:4
#: src/pages/settings/sync.vue:45 #: src/pages/settings/sync.vue:42
msgid "Connect via WebDAV" msgid "Connect via WebDAV"
msgstr "" msgstr ""
@ -474,7 +474,7 @@ msgstr ""
msgid "Create album" msgid "Create album"
msgstr "" msgstr ""
#: src/dialog/photo/info.vue:130 #: src/dialog/photo/info.vue:136
msgid "Created" msgid "Created"
msgstr "" msgstr ""
@ -619,7 +619,7 @@ msgstr ""
msgid "Edit Photo" msgid "Edit Photo"
msgstr "" msgstr ""
#: src/dialog/photo/info.vue:146 #: src/dialog/photo/info.vue:152
msgid "Edited" msgid "Edited"
msgstr "" msgstr ""
@ -682,7 +682,7 @@ msgstr ""
msgid "Fast" msgid "Fast"
msgstr "" msgstr ""
#: src/dialog/photo/info.vue:74 #: src/dialog/photo/info.vue:80
msgid "Favorite" msgid "Favorite"
msgstr "" msgstr ""
@ -903,12 +903,12 @@ msgstr ""
msgid "Language" msgid "Language"
msgstr "" msgstr ""
#: src/pages/settings/sync.vue:111 #: src/pages/settings/sync.vue:113
msgid "Last Backup" msgid "Last Backup"
msgstr "" msgstr ""
#: src/dialog/photo/details.vue:443 #: src/dialog/photo/details.vue:443
#: src/dialog/photo/info.vue:98 #: src/dialog/photo/info.vue:104
msgid "Latitude" msgid "Latitude"
msgstr "" msgstr ""
@ -991,7 +991,7 @@ msgid "Logs"
msgstr "" msgstr ""
#: src/dialog/photo/details.vue:444 #: src/dialog/photo/details.vue:444
#: src/dialog/photo/info.vue:106 #: src/dialog/photo/info.vue:112
msgid "Longitude" msgid "Longitude"
msgstr "" msgstr ""
@ -1102,7 +1102,7 @@ msgstr ""
msgid "Name too long" msgid "Name too long"
msgstr "" msgstr ""
#: src/pages/settings/sync.vue:122 #: src/pages/settings/sync.vue:129
#: src/resources/options.js:178 #: src/resources/options.js:178
#: src/resources/options.js:188 #: src/resources/options.js:188
msgid "Never" msgid "Never"
@ -1119,9 +1119,9 @@ msgid "Newest first"
msgstr "" msgstr ""
#: src/dialog/photo/archive.vue:13 #: src/dialog/photo/archive.vue:13
#: src/dialog/photo/info.vue:160
#: src/dialog/photo/info.vue:187 #: src/dialog/photo/info.vue:187
#: src/dialog/photo/info.vue:218 #: src/dialog/photo/info.vue:214
#: src/dialog/photo/info.vue:245
msgid "No" msgid "No"
msgstr "" msgstr ""
@ -1182,7 +1182,7 @@ msgstr ""
msgid "Note that you can as well manage and re-index your originals manually." msgid "Note that you can as well manage and re-index your originals manually."
msgstr "" msgstr ""
#: src/pages/settings/sync.vue:32 #: src/pages/settings/sync.vue:34
msgid "Note:" msgid "Note:"
msgstr "" msgstr ""
@ -1373,7 +1373,7 @@ msgid "Primary"
msgstr "" msgstr ""
#: src/component/navigation.vue:129 #: src/component/navigation.vue:129
#: src/dialog/photo/info.vue:82 #: src/dialog/photo/info.vue:88
#: src/routes.js:171 #: src/routes.js:171
msgid "Private" msgid "Private"
msgstr "" msgstr ""
@ -1386,7 +1386,7 @@ msgstr ""
msgid "Quality Filter" msgid "Quality Filter"
msgstr "" msgstr ""
#: src/dialog/photo/info.vue:53 #: src/dialog/photo/info.vue:59
msgid "Quality Score" msgid "Quality Score"
msgstr "" msgstr ""
@ -1444,7 +1444,7 @@ msgstr ""
msgid "Request failed - invalid response" msgid "Request failed - invalid response"
msgstr "" msgstr ""
#: src/dialog/photo/info.vue:61 #: src/dialog/photo/info.vue:67
msgid "Resolution" msgid "Resolution"
msgstr "" msgstr ""
@ -1472,7 +1472,7 @@ msgstr ""
msgid "Save" msgid "Save"
msgstr "" msgstr ""
#: src/dialog/photo/info.vue:90 #: src/dialog/photo/info.vue:96
msgid "Scan" msgid "Scan"
msgstr "" msgstr ""
@ -1524,7 +1524,7 @@ msgstr ""
msgid "Selection restored" msgid "Selection restored"
msgstr "" msgstr ""
#: src/pages/settings/sync.vue:108 #: src/pages/settings/sync.vue:109
msgid "Server" msgid "Server"
msgstr "" msgstr ""
@ -1655,7 +1655,7 @@ msgstr ""
msgid "Support for additional services, like Google Drive, will be added over time." msgid "Support for additional services, like Google Drive, will be added over time."
msgstr "" msgstr ""
#: src/pages/settings/sync.vue:110 #: src/pages/settings/sync.vue:111
msgid "Sync" msgid "Sync"
msgstr "" msgstr ""
@ -1664,6 +1664,7 @@ msgid "Sync raw images"
msgstr "" msgstr ""
#: src/component/photo/list.vue:114 #: src/component/photo/list.vue:114
#: src/dialog/photo/info.vue:53
#: src/share/photo/list.vue:93 #: src/share/photo/list.vue:93
msgid "Taken" msgid "Taken"
msgstr "" msgstr ""
@ -1699,7 +1700,7 @@ msgid "Theme"
msgstr "" msgstr ""
#: src/dialog/webdav.vue:17 #: src/dialog/webdav.vue:17
#: src/pages/settings/sync.vue:34 #: src/pages/settings/sync.vue:36
msgid "This mounts the originals folder as a network drive and allows you to open, edit, and delete files from your computer or smartphone as if they were local." msgid "This mounts the originals folder as a network drive and allows you to open, edit, and delete files from your computer or smartphone as if they were local."
msgstr "" msgstr ""
@ -1769,7 +1770,7 @@ msgid "UID"
msgstr "" msgstr ""
#: src/dialog/photo/details.vue:423 #: src/dialog/photo/details.vue:423
#: src/dialog/photo/info.vue:221 #: src/dialog/photo/info.vue:227
#: src/model/album.js:122 #: src/model/album.js:122
#: src/model/photo.js:399 #: src/model/photo.js:399
#: src/model/photo.js:413 #: src/model/photo.js:413
@ -1798,7 +1799,7 @@ msgid "Unstack"
msgstr "" msgstr ""
#: src/dialog/photo/files.vue:120 #: src/dialog/photo/files.vue:120
#: src/dialog/photo/info.vue:138 #: src/dialog/photo/info.vue:144
msgid "Updated" msgid "Updated"
msgstr "" msgstr ""
@ -1807,7 +1808,7 @@ msgstr ""
#: src/dialog/upload.vue:54 #: src/dialog/upload.vue:54
#: src/pages/library/import.vue:38 #: src/pages/library/import.vue:38
#: src/pages/settings/general.vue:319 #: src/pages/settings/general.vue:319
#: src/pages/settings/sync.vue:109 #: src/pages/settings/sync.vue:110
msgid "Upload" msgid "Upload"
msgstr "" msgstr ""
@ -1885,7 +1886,7 @@ msgstr ""
msgid "WebDAV clients can connect to PhotoPrism using the following URL:" msgid "WebDAV clients can connect to PhotoPrism using the following URL:"
msgstr "" msgstr ""
#: src/pages/settings/sync.vue:33 #: src/pages/settings/sync.vue:35
msgid "WebDAV clients, like Microsofts Windows Explorer or Apple's Finder, can connect directly to PhotoPrism." msgid "WebDAV clients, like Microsofts Windows Explorer or Apple's Finder, can connect directly to PhotoPrism."
msgstr "" msgstr ""
@ -1921,9 +1922,9 @@ msgstr ""
#: src/dialog/photo/files.vue:86 #: src/dialog/photo/files.vue:86
#: src/dialog/photo/files.vue:104 #: src/dialog/photo/files.vue:104
#: src/dialog/photo/files.vue:110 #: src/dialog/photo/files.vue:110
#: src/dialog/photo/info.vue:159
#: src/dialog/photo/info.vue:186 #: src/dialog/photo/info.vue:186
#: src/dialog/photo/info.vue:217 #: src/dialog/photo/info.vue:213
#: src/dialog/photo/info.vue:244
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""

View file

@ -1,7 +1,7 @@
import Api from "common/api"; import Api from "common/api";
import MockAdapter from "axios-mock-adapter"; import MockAdapter from "axios-mock-adapter";
window.__CONFIG__ = {"name":"PhotoPrism","version":"200531-4684f66-Linux-x86_64-DEBUG","copyright":"(c) 2018-2020 PhotoPrism.org \u003chello@photoprism.org\u003e","flags":"public debug experimental settings","siteUrl":"http://localhost:2342/","siteTitle":"PhotoPrism","siteCaption":"Browse your life","siteDescription":"Personal Photo Management powered by Go and Google TensorFlow. Free and open-source.","siteAuthor":"Anonymous","debug":true,"readonly":false,"uploadNSFW":false,"public":true,"experimental":true,"disableSettings":false,"albumCategories":null,"albums":[],"cameras":[{"ID":2,"Slug":"olympus-c2500l","Name":"Olympus C2500L","Make":"Olympus","Model":"C2500L"},{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown"}],"lenses":[{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown","Type":""}],"countries":[{"ID":"de","Slug":"germany","Name":"Germany"},{"ID":"is","Slug":"iceland","Name":"Iceland"},{"ID":"zz","Slug":"zz","Name":"Unknown"}],"thumbnails":[{"Name":"fit_720","Width":720,"Height":720},{"Name":"fit_2048","Width":2048,"Height":2048},{"Name":"fit_1280","Width":1280,"Height":1024},{"Name":"fit_1920","Width":1920,"Height":1200},{"Name":"fit_2560","Width":2560,"Height":1600},{"Name":"fit_3840","Width":3840,"Height":2400}],"downloadToken":"1uhovi0e","previewToken":"static","jsHash":"0fd34136","cssHash":"2b327230","settings":{"theme":"default","language":"en","templates":{"default":"index.tmpl"},"maps":{"animate":0,"style":"streets"},"features":{"archive":true,"private":true,"review":true,"upload":true,"import":true,"files":true,"moments":true,"labels":true,"places":true,"download":true,"edit":true,"share":true,"logs":true},"import":{"path":"/","move":false},"index":{"path":"/","convert":true,"rescan":false,"group":true}},"count":{"cameras":1,"lenses":0,"countries":2,"photos":126,"videos":0,"hidden":3,"favorites":1,"private":0,"review":0,"stories":0,"albums":0,"moments":0,"months":0,"folders":0,"files":255,"places":0,"labels":13,"labelMaxPhotos":1},"pos":{"uid":"","loc":"","utc":"0001-01-01T00:00:00Z","lat":0,"lng":0},"years":[2003,2002],"colors":[{"Example":"#AB47BC","Name":"Purple","Slug":"purple"},{"Example":"#FF00FF","Name":"Magenta","Slug":"magenta"},{"Example":"#EC407A","Name":"Pink","Slug":"pink"},{"Example":"#EF5350","Name":"Red","Slug":"red"},{"Example":"#FFA726","Name":"Orange","Slug":"orange"},{"Example":"#D4AF37","Name":"Gold","Slug":"gold"},{"Example":"#FDD835","Name":"Yellow","Slug":"yellow"},{"Example":"#CDDC39","Name":"Lime","Slug":"lime"},{"Example":"#66BB6A","Name":"Green","Slug":"green"},{"Example":"#009688","Name":"Teal","Slug":"teal"},{"Example":"#00BCD4","Name":"Cyan","Slug":"cyan"},{"Example":"#2196F3","Name":"Blue","Slug":"blue"},{"Example":"#A1887F","Name":"Brown","Slug":"brown"},{"Example":"#F5F5F5","Name":"White","Slug":"white"},{"Example":"#9E9E9E","Name":"Grey","Slug":"grey"},{"Example":"#212121","Name":"Black","Slug":"black"}],"categories":[{"UID":"lqb6y631re96cper","Slug":"animal","Name":"Animal"},{"UID":"lqb6y5gvo9avdfx5","Slug":"architecture","Name":"Architecture"},{"UID":"lqb6y633nhfj1uzt","Slug":"bird","Name":"Bird"},{"UID":"lqb6y633g3hxg1aq","Slug":"farm","Name":"Farm"},{"UID":"lqb6y4i1ez9cw5bi","Slug":"nature","Name":"Nature"},{"UID":"lqb6y4f2v7dw8irs","Slug":"plant","Name":"Plant"},{"UID":"lqb6y6s2ohhmu0fn","Slug":"reptile","Name":"Reptile"},{"UID":"lqb6y6ctgsq2g2np","Slug":"water","Name":"Water"}],"clip":160,"server":{"cores":2,"routines":23,"memory":{"used":1224531272,"reserved":1416904088,"info":"Used 1.2 GB / Reserved 1.4 GB"}}}; window.__CONFIG__ = {"name":"PhotoPrism","version":"200531-4684f66-Linux-x86_64-DEBUG","copyright":"(c) 2018-2020 PhotoPrism.org \u003chello@photoprism.org\u003e","flags":"public debug experimental settings","siteUrl":"http://localhost:2342/","siteTitle":"PhotoPrism","siteCaption":"Browse your life","siteDescription":"Personal Photo Management powered by Go and Google TensorFlow. Free and open-source.","siteAuthor":"Anonymous","debug":true,"readonly":false,"uploadNSFW":false,"public":true,"experimental":true,"disableSettings":false,"albumCategories":null,"albums":[],"cameras":[{"ID":2,"Slug":"olympus-c2500l","Name":"Olympus C2500L","Make":"Olympus","Model":"C2500L"},{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown"}],"lenses":[{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown","Type":""}],"countries":[{"ID":"de","Slug":"germany","Name":"Germany"},{"ID":"is","Slug":"iceland","Name":"Iceland"},{"ID":"zz","Slug":"zz","Name":"Unknown"}],"thumbs":[{"Name":"fit_720","Width":720,"Height":720},{"Name":"fit_2048","Width":2048,"Height":2048},{"Name":"fit_1280","Width":1280,"Height":1024},{"Name":"fit_1920","Width":1920,"Height":1200},{"Name":"fit_2560","Width":2560,"Height":1600},{"Name":"fit_3840","Width":3840,"Height":2400}],"downloadToken":"1uhovi0e","previewToken":"static","jsHash":"0fd34136","cssHash":"2b327230","settings":{"theme":"default","language":"en","templates":{"default":"index.tmpl"},"maps":{"animate":0,"style":"streets"},"features":{"archive":true,"private":true,"review":true,"upload":true,"import":true,"files":true,"moments":true,"labels":true,"places":true,"download":true,"edit":true,"share":true,"logs":true},"import":{"path":"/","move":false},"index":{"path":"/","convert":true,"rescan":false,"group":true}},"count":{"cameras":1,"lenses":0,"countries":2,"photos":126,"videos":0,"hidden":3,"favorites":1,"private":0,"review":0,"stories":0,"albums":0,"moments":0,"months":0,"folders":0,"files":255,"places":0,"labels":13,"labelMaxPhotos":1},"pos":{"uid":"","loc":"","utc":"0001-01-01T00:00:00Z","lat":0,"lng":0},"years":[2003,2002],"colors":[{"Example":"#AB47BC","Name":"Purple","Slug":"purple"},{"Example":"#FF00FF","Name":"Magenta","Slug":"magenta"},{"Example":"#EC407A","Name":"Pink","Slug":"pink"},{"Example":"#EF5350","Name":"Red","Slug":"red"},{"Example":"#FFA726","Name":"Orange","Slug":"orange"},{"Example":"#D4AF37","Name":"Gold","Slug":"gold"},{"Example":"#FDD835","Name":"Yellow","Slug":"yellow"},{"Example":"#CDDC39","Name":"Lime","Slug":"lime"},{"Example":"#66BB6A","Name":"Green","Slug":"green"},{"Example":"#009688","Name":"Teal","Slug":"teal"},{"Example":"#00BCD4","Name":"Cyan","Slug":"cyan"},{"Example":"#2196F3","Name":"Blue","Slug":"blue"},{"Example":"#A1887F","Name":"Brown","Slug":"brown"},{"Example":"#F5F5F5","Name":"White","Slug":"white"},{"Example":"#9E9E9E","Name":"Grey","Slug":"grey"},{"Example":"#212121","Name":"Black","Slug":"black"}],"categories":[{"UID":"lqb6y631re96cper","Slug":"animal","Name":"Animal"},{"UID":"lqb6y5gvo9avdfx5","Slug":"architecture","Name":"Architecture"},{"UID":"lqb6y633nhfj1uzt","Slug":"bird","Name":"Bird"},{"UID":"lqb6y633g3hxg1aq","Slug":"farm","Name":"Farm"},{"UID":"lqb6y4i1ez9cw5bi","Slug":"nature","Name":"Nature"},{"UID":"lqb6y4f2v7dw8irs","Slug":"plant","Name":"Plant"},{"UID":"lqb6y6s2ohhmu0fn","Slug":"reptile","Name":"Reptile"},{"UID":"lqb6y6ctgsq2g2np","Slug":"water","Name":"Water"}],"clip":160,"server":{"cores":2,"routines":23,"memory":{"used":1224531272,"reserved":1416904088,"info":"Used 1.2 GB / Reserved 1.4 GB"}}};
let chai = require("chai/chai"); let chai = require("chai/chai");
let assert = chai.assert; let assert = chai.assert;

View file

@ -2,7 +2,7 @@ import Clipboard from "common/clipboard";
import Photo from "model/photo"; import Photo from "model/photo";
import Album from "model/album"; import Album from "model/album";
window.__CONFIG__ = {"name":"PhotoPrism","version":"200531-4684f66-Linux-x86_64-DEBUG","copyright":"(c) 2018-2020 PhotoPrism.org \u003chello@photoprism.org\u003e","flags":"public debug experimental settings","siteUrl":"http://localhost:2342/","siteTitle":"PhotoPrism","siteCaption":"Browse your life","siteDescription":"Personal Photo Management powered by Go and Google TensorFlow. Free and open-source.","siteAuthor":"Anonymous","debug":true,"readonly":false,"uploadNSFW":false,"public":true,"experimental":true,"disableSettings":false,"albumCategories":null,"albums":[],"cameras":[{"ID":2,"Slug":"olympus-c2500l","Name":"Olympus C2500L","Make":"Olympus","Model":"C2500L"},{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown"}],"lenses":[{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown","Type":""}],"countries":[{"ID":"de","Slug":"germany","Name":"Germany"},{"ID":"is","Slug":"iceland","Name":"Iceland"},{"ID":"zz","Slug":"zz","Name":"Unknown"}],"thumbnails":[{"Name":"fit_720","Width":720,"Height":720},{"Name":"fit_2048","Width":2048,"Height":2048},{"Name":"fit_1280","Width":1280,"Height":1024},{"Name":"fit_1920","Width":1920,"Height":1200},{"Name":"fit_2560","Width":2560,"Height":1600},{"Name":"fit_3840","Width":3840,"Height":2400}],"downloadToken":"1uhovi0e","previewToken":"static","jsHash":"0fd34136","cssHash":"2b327230","settings":{"theme":"default","language":"en","templates":{"default":"index.tmpl"},"maps":{"animate":0,"style":"streets"},"features":{"archive":true,"private":true,"review":true,"upload":true,"import":true,"files":true,"moments":true,"labels":true,"places":true,"download":true,"edit":true,"share":true,"logs":true},"import":{"path":"/","move":false},"index":{"path":"/","convert":true,"rescan":false,"group":true}},"count":{"cameras":1,"lenses":0,"countries":2,"photos":126,"videos":0,"hidden":3,"favorites":1,"private":0,"review":0,"stories":0,"albums":0,"moments":0,"months":0,"folders":0,"files":255,"places":0,"labels":13,"labelMaxPhotos":1},"pos":{"uid":"","loc":"","utc":"0001-01-01T00:00:00Z","lat":0,"lng":0},"years":[2003,2002],"colors":[{"Example":"#AB47BC","Name":"Purple","Slug":"purple"},{"Example":"#FF00FF","Name":"Magenta","Slug":"magenta"},{"Example":"#EC407A","Name":"Pink","Slug":"pink"},{"Example":"#EF5350","Name":"Red","Slug":"red"},{"Example":"#FFA726","Name":"Orange","Slug":"orange"},{"Example":"#D4AF37","Name":"Gold","Slug":"gold"},{"Example":"#FDD835","Name":"Yellow","Slug":"yellow"},{"Example":"#CDDC39","Name":"Lime","Slug":"lime"},{"Example":"#66BB6A","Name":"Green","Slug":"green"},{"Example":"#009688","Name":"Teal","Slug":"teal"},{"Example":"#00BCD4","Name":"Cyan","Slug":"cyan"},{"Example":"#2196F3","Name":"Blue","Slug":"blue"},{"Example":"#A1887F","Name":"Brown","Slug":"brown"},{"Example":"#F5F5F5","Name":"White","Slug":"white"},{"Example":"#9E9E9E","Name":"Grey","Slug":"grey"},{"Example":"#212121","Name":"Black","Slug":"black"}],"categories":[{"UID":"lqb6y631re96cper","Slug":"animal","Name":"Animal"},{"UID":"lqb6y5gvo9avdfx5","Slug":"architecture","Name":"Architecture"},{"UID":"lqb6y633nhfj1uzt","Slug":"bird","Name":"Bird"},{"UID":"lqb6y633g3hxg1aq","Slug":"farm","Name":"Farm"},{"UID":"lqb6y4i1ez9cw5bi","Slug":"nature","Name":"Nature"},{"UID":"lqb6y4f2v7dw8irs","Slug":"plant","Name":"Plant"},{"UID":"lqb6y6s2ohhmu0fn","Slug":"reptile","Name":"Reptile"},{"UID":"lqb6y6ctgsq2g2np","Slug":"water","Name":"Water"}],"clip":160,"server":{"cores":2,"routines":23,"memory":{"used":1224531272,"reserved":1416904088,"info":"Used 1.2 GB / Reserved 1.4 GB"}}}; window.__CONFIG__ = {"name":"PhotoPrism","version":"200531-4684f66-Linux-x86_64-DEBUG","copyright":"(c) 2018-2020 PhotoPrism.org \u003chello@photoprism.org\u003e","flags":"public debug experimental settings","siteUrl":"http://localhost:2342/","siteTitle":"PhotoPrism","siteCaption":"Browse your life","siteDescription":"Personal Photo Management powered by Go and Google TensorFlow. Free and open-source.","siteAuthor":"Anonymous","debug":true,"readonly":false,"uploadNSFW":false,"public":true,"experimental":true,"disableSettings":false,"albumCategories":null,"albums":[],"cameras":[{"ID":2,"Slug":"olympus-c2500l","Name":"Olympus C2500L","Make":"Olympus","Model":"C2500L"},{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown"}],"lenses":[{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown","Type":""}],"countries":[{"ID":"de","Slug":"germany","Name":"Germany"},{"ID":"is","Slug":"iceland","Name":"Iceland"},{"ID":"zz","Slug":"zz","Name":"Unknown"}],"thumbs":[{"Name":"fit_720","Width":720,"Height":720},{"Name":"fit_2048","Width":2048,"Height":2048},{"Name":"fit_1280","Width":1280,"Height":1024},{"Name":"fit_1920","Width":1920,"Height":1200},{"Name":"fit_2560","Width":2560,"Height":1600},{"Name":"fit_3840","Width":3840,"Height":2400}],"downloadToken":"1uhovi0e","previewToken":"static","jsHash":"0fd34136","cssHash":"2b327230","settings":{"theme":"default","language":"en","templates":{"default":"index.tmpl"},"maps":{"animate":0,"style":"streets"},"features":{"archive":true,"private":true,"review":true,"upload":true,"import":true,"files":true,"moments":true,"labels":true,"places":true,"download":true,"edit":true,"share":true,"logs":true},"import":{"path":"/","move":false},"index":{"path":"/","convert":true,"rescan":false,"group":true}},"count":{"cameras":1,"lenses":0,"countries":2,"photos":126,"videos":0,"hidden":3,"favorites":1,"private":0,"review":0,"stories":0,"albums":0,"moments":0,"months":0,"folders":0,"files":255,"places":0,"labels":13,"labelMaxPhotos":1},"pos":{"uid":"","loc":"","utc":"0001-01-01T00:00:00Z","lat":0,"lng":0},"years":[2003,2002],"colors":[{"Example":"#AB47BC","Name":"Purple","Slug":"purple"},{"Example":"#FF00FF","Name":"Magenta","Slug":"magenta"},{"Example":"#EC407A","Name":"Pink","Slug":"pink"},{"Example":"#EF5350","Name":"Red","Slug":"red"},{"Example":"#FFA726","Name":"Orange","Slug":"orange"},{"Example":"#D4AF37","Name":"Gold","Slug":"gold"},{"Example":"#FDD835","Name":"Yellow","Slug":"yellow"},{"Example":"#CDDC39","Name":"Lime","Slug":"lime"},{"Example":"#66BB6A","Name":"Green","Slug":"green"},{"Example":"#009688","Name":"Teal","Slug":"teal"},{"Example":"#00BCD4","Name":"Cyan","Slug":"cyan"},{"Example":"#2196F3","Name":"Blue","Slug":"blue"},{"Example":"#A1887F","Name":"Brown","Slug":"brown"},{"Example":"#F5F5F5","Name":"White","Slug":"white"},{"Example":"#9E9E9E","Name":"Grey","Slug":"grey"},{"Example":"#212121","Name":"Black","Slug":"black"}],"categories":[{"UID":"lqb6y631re96cper","Slug":"animal","Name":"Animal"},{"UID":"lqb6y5gvo9avdfx5","Slug":"architecture","Name":"Architecture"},{"UID":"lqb6y633nhfj1uzt","Slug":"bird","Name":"Bird"},{"UID":"lqb6y633g3hxg1aq","Slug":"farm","Name":"Farm"},{"UID":"lqb6y4i1ez9cw5bi","Slug":"nature","Name":"Nature"},{"UID":"lqb6y4f2v7dw8irs","Slug":"plant","Name":"Plant"},{"UID":"lqb6y6s2ohhmu0fn","Slug":"reptile","Name":"Reptile"},{"UID":"lqb6y6ctgsq2g2np","Slug":"water","Name":"Water"}],"clip":160,"server":{"cores":2,"routines":23,"memory":{"used":1224531272,"reserved":1416904088,"info":"Used 1.2 GB / Reserved 1.4 GB"}}};
let chai = require("chai/chai"); let chai = require("chai/chai");
let assert = chai.assert; let assert = chai.assert;

View file

@ -33,7 +33,7 @@ window.__CONFIG__ = {
"Slug": "iceland", "Slug": "iceland",
"Name": "Iceland" "Name": "Iceland"
}, {"ID": "zz", "Slug": "zz", "Name": "Unknown"}], }, {"ID": "zz", "Slug": "zz", "Name": "Unknown"}],
"thumbnails": [{"Name": "fit_720", "Width": 720, "Height": 720}, { "thumbs": [{"Name": "fit_720", "Width": 720, "Height": 720}, {
"Name": "fit_2048", "Name": "fit_2048",
"Width": 2048, "Width": 2048,
"Height": 2048 "Height": 2048

View file

@ -30,7 +30,7 @@ window.__CONFIG__ = {
"Slug": "iceland", "Slug": "iceland",
"Name": "Iceland" "Name": "Iceland"
}, {"ID": "zz", "Slug": "zz", "Name": "Unknown"}], }, {"ID": "zz", "Slug": "zz", "Name": "Unknown"}],
"thumbnails": [{"Name": "fit_720", "Width": 720, "Height": 720}, { "thumbs": [{"Name": "fit_720", "Width": 720, "Height": 720}, {
"Name": "fit_2048", "Name": "fit_2048",
"Width": 2048, "Width": 2048,
"Height": 2048 "Height": 2048

View file

@ -1,7 +1,7 @@
import Util from "common/util"; import Util from "common/util";
import MockAdapter from "axios-mock-adapter"; import MockAdapter from "axios-mock-adapter";
window.__CONFIG__ = {"name":"PhotoPrism","version":"200531-4684f66-Linux-x86_64-DEBUG","copyright":"(c) 2018-2020 PhotoPrism.org \u003chello@photoprism.org\u003e","flags":"public debug experimental settings","siteUrl":"http://localhost:2342/","siteTitle":"PhotoPrism","siteCaption":"Browse your life","siteDescription":"Personal Photo Management powered by Go and Google TensorFlow. Free and open-source.","siteAuthor":"Anonymous","debug":true,"readonly":false,"uploadNSFW":false,"public":true,"experimental":true,"disableSettings":false,"albumCategories":null,"albums":[],"cameras":[{"ID":2,"Slug":"olympus-c2500l","Name":"Olympus C2500L","Make":"Olympus","Model":"C2500L"},{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown"}],"lenses":[{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown","Type":""}],"countries":[{"ID":"de","Slug":"germany","Name":"Germany"},{"ID":"is","Slug":"iceland","Name":"Iceland"},{"ID":"zz","Slug":"zz","Name":"Unknown"}],"thumbnails":[{"Name":"fit_720","Width":720,"Height":720},{"Name":"fit_2048","Width":2048,"Height":2048},{"Name":"fit_1280","Width":1280,"Height":1024},{"Name":"fit_1920","Width":1920,"Height":1200},{"Name":"fit_2560","Width":2560,"Height":1600},{"Name":"fit_3840","Width":3840,"Height":2400}],"downloadToken":"1uhovi0e","previewToken":"static","jsHash":"0fd34136","cssHash":"2b327230","settings":{"theme":"default","language":"en","templates":{"default":"index.tmpl"},"maps":{"animate":0,"style":"streets"},"features":{"archive":true,"private":true,"review":true,"upload":true,"import":true,"files":true,"moments":true,"labels":true,"places":true,"download":true,"edit":true,"share":true,"logs":true},"import":{"path":"/","move":false},"index":{"path":"/","convert":true,"rescan":false,"group":true}},"count":{"cameras":1,"lenses":0,"countries":2,"photos":126,"videos":0,"hidden":3,"favorites":1,"private":0,"review":0,"stories":0,"albums":0,"moments":0,"months":0,"folders":0,"files":255,"places":0,"labels":13,"labelMaxPhotos":1},"pos":{"uid":"","loc":"","utc":"0001-01-01T00:00:00Z","lat":0,"lng":0},"years":[2003,2002],"colors":[{"Example":"#AB47BC","Name":"Purple","Slug":"purple"},{"Example":"#FF00FF","Name":"Magenta","Slug":"magenta"},{"Example":"#EC407A","Name":"Pink","Slug":"pink"},{"Example":"#EF5350","Name":"Red","Slug":"red"},{"Example":"#FFA726","Name":"Orange","Slug":"orange"},{"Example":"#D4AF37","Name":"Gold","Slug":"gold"},{"Example":"#FDD835","Name":"Yellow","Slug":"yellow"},{"Example":"#CDDC39","Name":"Lime","Slug":"lime"},{"Example":"#66BB6A","Name":"Green","Slug":"green"},{"Example":"#009688","Name":"Teal","Slug":"teal"},{"Example":"#00BCD4","Name":"Cyan","Slug":"cyan"},{"Example":"#2196F3","Name":"Blue","Slug":"blue"},{"Example":"#A1887F","Name":"Brown","Slug":"brown"},{"Example":"#F5F5F5","Name":"White","Slug":"white"},{"Example":"#9E9E9E","Name":"Grey","Slug":"grey"},{"Example":"#212121","Name":"Black","Slug":"black"}],"categories":[{"UID":"lqb6y631re96cper","Slug":"animal","Name":"Animal"},{"UID":"lqb6y5gvo9avdfx5","Slug":"architecture","Name":"Architecture"},{"UID":"lqb6y633nhfj1uzt","Slug":"bird","Name":"Bird"},{"UID":"lqb6y633g3hxg1aq","Slug":"farm","Name":"Farm"},{"UID":"lqb6y4i1ez9cw5bi","Slug":"nature","Name":"Nature"},{"UID":"lqb6y4f2v7dw8irs","Slug":"plant","Name":"Plant"},{"UID":"lqb6y6s2ohhmu0fn","Slug":"reptile","Name":"Reptile"},{"UID":"lqb6y6ctgsq2g2np","Slug":"water","Name":"Water"}],"clip":160,"server":{"cores":2,"routines":23,"memory":{"used":1224531272,"reserved":1416904088,"info":"Used 1.2 GB / Reserved 1.4 GB"}}}; window.__CONFIG__ = {"name":"PhotoPrism","version":"200531-4684f66-Linux-x86_64-DEBUG","copyright":"(c) 2018-2020 PhotoPrism.org \u003chello@photoprism.org\u003e","flags":"public debug experimental settings","siteUrl":"http://localhost:2342/","siteTitle":"PhotoPrism","siteCaption":"Browse your life","siteDescription":"Personal Photo Management powered by Go and Google TensorFlow. Free and open-source.","siteAuthor":"Anonymous","debug":true,"readonly":false,"uploadNSFW":false,"public":true,"experimental":true,"disableSettings":false,"albumCategories":null,"albums":[],"cameras":[{"ID":2,"Slug":"olympus-c2500l","Name":"Olympus C2500L","Make":"Olympus","Model":"C2500L"},{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown"}],"lenses":[{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown","Type":""}],"countries":[{"ID":"de","Slug":"germany","Name":"Germany"},{"ID":"is","Slug":"iceland","Name":"Iceland"},{"ID":"zz","Slug":"zz","Name":"Unknown"}],"thumbs":[{"Name":"fit_720","Width":720,"Height":720},{"Name":"fit_2048","Width":2048,"Height":2048},{"Name":"fit_1280","Width":1280,"Height":1024},{"Name":"fit_1920","Width":1920,"Height":1200},{"Name":"fit_2560","Width":2560,"Height":1600},{"Name":"fit_3840","Width":3840,"Height":2400}],"downloadToken":"1uhovi0e","previewToken":"static","jsHash":"0fd34136","cssHash":"2b327230","settings":{"theme":"default","language":"en","templates":{"default":"index.tmpl"},"maps":{"animate":0,"style":"streets"},"features":{"archive":true,"private":true,"review":true,"upload":true,"import":true,"files":true,"moments":true,"labels":true,"places":true,"download":true,"edit":true,"share":true,"logs":true},"import":{"path":"/","move":false},"index":{"path":"/","convert":true,"rescan":false,"group":true}},"count":{"cameras":1,"lenses":0,"countries":2,"photos":126,"videos":0,"hidden":3,"favorites":1,"private":0,"review":0,"stories":0,"albums":0,"moments":0,"months":0,"folders":0,"files":255,"places":0,"labels":13,"labelMaxPhotos":1},"pos":{"uid":"","loc":"","utc":"0001-01-01T00:00:00Z","lat":0,"lng":0},"years":[2003,2002],"colors":[{"Example":"#AB47BC","Name":"Purple","Slug":"purple"},{"Example":"#FF00FF","Name":"Magenta","Slug":"magenta"},{"Example":"#EC407A","Name":"Pink","Slug":"pink"},{"Example":"#EF5350","Name":"Red","Slug":"red"},{"Example":"#FFA726","Name":"Orange","Slug":"orange"},{"Example":"#D4AF37","Name":"Gold","Slug":"gold"},{"Example":"#FDD835","Name":"Yellow","Slug":"yellow"},{"Example":"#CDDC39","Name":"Lime","Slug":"lime"},{"Example":"#66BB6A","Name":"Green","Slug":"green"},{"Example":"#009688","Name":"Teal","Slug":"teal"},{"Example":"#00BCD4","Name":"Cyan","Slug":"cyan"},{"Example":"#2196F3","Name":"Blue","Slug":"blue"},{"Example":"#A1887F","Name":"Brown","Slug":"brown"},{"Example":"#F5F5F5","Name":"White","Slug":"white"},{"Example":"#9E9E9E","Name":"Grey","Slug":"grey"},{"Example":"#212121","Name":"Black","Slug":"black"}],"categories":[{"UID":"lqb6y631re96cper","Slug":"animal","Name":"Animal"},{"UID":"lqb6y5gvo9avdfx5","Slug":"architecture","Name":"Architecture"},{"UID":"lqb6y633nhfj1uzt","Slug":"bird","Name":"Bird"},{"UID":"lqb6y633g3hxg1aq","Slug":"farm","Name":"Farm"},{"UID":"lqb6y4i1ez9cw5bi","Slug":"nature","Name":"Nature"},{"UID":"lqb6y4f2v7dw8irs","Slug":"plant","Name":"Plant"},{"UID":"lqb6y6s2ohhmu0fn","Slug":"reptile","Name":"Reptile"},{"UID":"lqb6y6ctgsq2g2np","Slug":"water","Name":"Water"}],"clip":160,"server":{"cores":2,"routines":23,"memory":{"used":1224531272,"reserved":1416904088,"info":"Used 1.2 GB / Reserved 1.4 GB"}}};
let chai = require("chai/chai"); let chai = require("chai/chai");
let assert = chai.assert; let assert = chai.assert;
@ -55,4 +55,4 @@ describe("common/util", () => {
const result = Util.truncate("teststring for mocha", 5, "ng"); const result = Util.truncate("teststring for mocha", 5, "ng");
assert.equal(result, "tesng"); assert.equal(result, "tesng");
}); });
}); });

View file

@ -2,7 +2,7 @@ import Label from "model/label";
import MockAdapter from "axios-mock-adapter"; import MockAdapter from "axios-mock-adapter";
import Api from "common/api"; import Api from "common/api";
window.__CONFIG__ = {"name":"PhotoPrism","version":"200531-4684f66-Linux-x86_64-DEBUG","copyright":"(c) 2018-2020 PhotoPrism.org \u003chello@photoprism.org\u003e","flags":"public debug experimental settings","siteUrl":"http://localhost:2342/","siteTitle":"PhotoPrism","siteCaption":"Browse your life","siteDescription":"Personal Photo Management powered by Go and Google TensorFlow. Free and open-source.","siteAuthor":"Anonymous","debug":true,"readonly":false,"uploadNSFW":false,"public":true,"experimental":true,"disableSettings":false,"albumCategories":null,"albums":[],"cameras":[{"ID":2,"Slug":"olympus-c2500l","Name":"Olympus C2500L","Make":"Olympus","Model":"C2500L"},{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown"}],"lenses":[{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown","Type":""}],"countries":[{"ID":"de","Slug":"germany","Name":"Germany"},{"ID":"is","Slug":"iceland","Name":"Iceland"},{"ID":"zz","Slug":"zz","Name":"Unknown"}],"thumbnails":[{"Name":"fit_720","Width":720,"Height":720},{"Name":"fit_2048","Width":2048,"Height":2048},{"Name":"fit_1280","Width":1280,"Height":1024},{"Name":"fit_1920","Width":1920,"Height":1200},{"Name":"fit_2560","Width":2560,"Height":1600},{"Name":"fit_3840","Width":3840,"Height":2400}],"downloadToken":"1uhovi0e","previewToken":"static","jsHash":"0fd34136","cssHash":"2b327230","settings":{"theme":"default","language":"en","templates":{"default":"index.tmpl"},"maps":{"animate":0,"style":"streets"},"features":{"archive":true,"private":true,"review":true,"upload":true,"import":true,"files":true,"moments":true,"labels":true,"places":true,"download":true,"edit":true,"share":true,"logs":true},"import":{"path":"/","move":false},"index":{"path":"/","convert":true,"rescan":false,"group":true}},"count":{"cameras":1,"lenses":0,"countries":2,"photos":126,"videos":0,"hidden":3,"favorites":1,"private":0,"review":0,"stories":0,"albums":0,"moments":0,"months":0,"folders":0,"files":255,"places":0,"labels":13,"labelMaxPhotos":1},"pos":{"uid":"","loc":"","utc":"0001-01-01T00:00:00Z","lat":0,"lng":0},"years":[2003,2002],"colors":[{"Example":"#AB47BC","Name":"Purple","Slug":"purple"},{"Example":"#FF00FF","Name":"Magenta","Slug":"magenta"},{"Example":"#EC407A","Name":"Pink","Slug":"pink"},{"Example":"#EF5350","Name":"Red","Slug":"red"},{"Example":"#FFA726","Name":"Orange","Slug":"orange"},{"Example":"#D4AF37","Name":"Gold","Slug":"gold"},{"Example":"#FDD835","Name":"Yellow","Slug":"yellow"},{"Example":"#CDDC39","Name":"Lime","Slug":"lime"},{"Example":"#66BB6A","Name":"Green","Slug":"green"},{"Example":"#009688","Name":"Teal","Slug":"teal"},{"Example":"#00BCD4","Name":"Cyan","Slug":"cyan"},{"Example":"#2196F3","Name":"Blue","Slug":"blue"},{"Example":"#A1887F","Name":"Brown","Slug":"brown"},{"Example":"#F5F5F5","Name":"White","Slug":"white"},{"Example":"#9E9E9E","Name":"Grey","Slug":"grey"},{"Example":"#212121","Name":"Black","Slug":"black"}],"categories":[{"UID":"lqb6y631re96cper","Slug":"animal","Name":"Animal"},{"UID":"lqb6y5gvo9avdfx5","Slug":"architecture","Name":"Architecture"},{"UID":"lqb6y633nhfj1uzt","Slug":"bird","Name":"Bird"},{"UID":"lqb6y633g3hxg1aq","Slug":"farm","Name":"Farm"},{"UID":"lqb6y4i1ez9cw5bi","Slug":"nature","Name":"Nature"},{"UID":"lqb6y4f2v7dw8irs","Slug":"plant","Name":"Plant"},{"UID":"lqb6y6s2ohhmu0fn","Slug":"reptile","Name":"Reptile"},{"UID":"lqb6y6ctgsq2g2np","Slug":"water","Name":"Water"}],"clip":160,"server":{"cores":2,"routines":23,"memory":{"used":1224531272,"reserved":1416904088,"info":"Used 1.2 GB / Reserved 1.4 GB"}}}; window.__CONFIG__ = {"name":"PhotoPrism","version":"200531-4684f66-Linux-x86_64-DEBUG","copyright":"(c) 2018-2020 PhotoPrism.org \u003chello@photoprism.org\u003e","flags":"public debug experimental settings","siteUrl":"http://localhost:2342/","siteTitle":"PhotoPrism","siteCaption":"Browse your life","siteDescription":"Personal Photo Management powered by Go and Google TensorFlow. Free and open-source.","siteAuthor":"Anonymous","debug":true,"readonly":false,"uploadNSFW":false,"public":true,"experimental":true,"disableSettings":false,"albumCategories":null,"albums":[],"cameras":[{"ID":2,"Slug":"olympus-c2500l","Name":"Olympus C2500L","Make":"Olympus","Model":"C2500L"},{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown"}],"lenses":[{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown","Type":""}],"countries":[{"ID":"de","Slug":"germany","Name":"Germany"},{"ID":"is","Slug":"iceland","Name":"Iceland"},{"ID":"zz","Slug":"zz","Name":"Unknown"}],"thumbs":[{"Name":"fit_720","Width":720,"Height":720},{"Name":"fit_2048","Width":2048,"Height":2048},{"Name":"fit_1280","Width":1280,"Height":1024},{"Name":"fit_1920","Width":1920,"Height":1200},{"Name":"fit_2560","Width":2560,"Height":1600},{"Name":"fit_3840","Width":3840,"Height":2400}],"downloadToken":"1uhovi0e","previewToken":"static","jsHash":"0fd34136","cssHash":"2b327230","settings":{"theme":"default","language":"en","templates":{"default":"index.tmpl"},"maps":{"animate":0,"style":"streets"},"features":{"archive":true,"private":true,"review":true,"upload":true,"import":true,"files":true,"moments":true,"labels":true,"places":true,"download":true,"edit":true,"share":true,"logs":true},"import":{"path":"/","move":false},"index":{"path":"/","convert":true,"rescan":false,"group":true}},"count":{"cameras":1,"lenses":0,"countries":2,"photos":126,"videos":0,"hidden":3,"favorites":1,"private":0,"review":0,"stories":0,"albums":0,"moments":0,"months":0,"folders":0,"files":255,"places":0,"labels":13,"labelMaxPhotos":1},"pos":{"uid":"","loc":"","utc":"0001-01-01T00:00:00Z","lat":0,"lng":0},"years":[2003,2002],"colors":[{"Example":"#AB47BC","Name":"Purple","Slug":"purple"},{"Example":"#FF00FF","Name":"Magenta","Slug":"magenta"},{"Example":"#EC407A","Name":"Pink","Slug":"pink"},{"Example":"#EF5350","Name":"Red","Slug":"red"},{"Example":"#FFA726","Name":"Orange","Slug":"orange"},{"Example":"#D4AF37","Name":"Gold","Slug":"gold"},{"Example":"#FDD835","Name":"Yellow","Slug":"yellow"},{"Example":"#CDDC39","Name":"Lime","Slug":"lime"},{"Example":"#66BB6A","Name":"Green","Slug":"green"},{"Example":"#009688","Name":"Teal","Slug":"teal"},{"Example":"#00BCD4","Name":"Cyan","Slug":"cyan"},{"Example":"#2196F3","Name":"Blue","Slug":"blue"},{"Example":"#A1887F","Name":"Brown","Slug":"brown"},{"Example":"#F5F5F5","Name":"White","Slug":"white"},{"Example":"#9E9E9E","Name":"Grey","Slug":"grey"},{"Example":"#212121","Name":"Black","Slug":"black"}],"categories":[{"UID":"lqb6y631re96cper","Slug":"animal","Name":"Animal"},{"UID":"lqb6y5gvo9avdfx5","Slug":"architecture","Name":"Architecture"},{"UID":"lqb6y633nhfj1uzt","Slug":"bird","Name":"Bird"},{"UID":"lqb6y633g3hxg1aq","Slug":"farm","Name":"Farm"},{"UID":"lqb6y4i1ez9cw5bi","Slug":"nature","Name":"Nature"},{"UID":"lqb6y4f2v7dw8irs","Slug":"plant","Name":"Plant"},{"UID":"lqb6y6s2ohhmu0fn","Slug":"reptile","Name":"Reptile"},{"UID":"lqb6y6ctgsq2g2np","Slug":"water","Name":"Water"}],"clip":160,"server":{"cores":2,"routines":23,"memory":{"used":1224531272,"reserved":1416904088,"info":"Used 1.2 GB / Reserved 1.4 GB"}}};
let chai = require("chai/chai"); let chai = require("chai/chai");
let assert = chai.assert; let assert = chai.assert;

View file

@ -2,7 +2,7 @@ import Photo from "model/photo";
import MockAdapter from "axios-mock-adapter"; import MockAdapter from "axios-mock-adapter";
import Api from "common/api"; import Api from "common/api";
window.__CONFIG__ = {"name":"PhotoPrism","version":"200531-4684f66-Linux-x86_64-DEBUG","copyright":"(c) 2018-2020 PhotoPrism.org \u003chello@photoprism.org\u003e","flags":"public debug experimental settings","siteUrl":"http://localhost:2342/","siteTitle":"PhotoPrism","siteCaption":"Browse your life","siteDescription":"Personal Photo Management powered by Go and Google TensorFlow. Free and open-source.","siteAuthor":"Anonymous","debug":true,"readonly":false,"uploadNSFW":false,"public":true,"experimental":true,"disableSettings":false,"albumCategories":null,"albums":[],"cameras":[{"ID":2,"Slug":"olympus-c2500l","Name":"Olympus C2500L","Make":"Olympus","Model":"C2500L"},{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown"}],"lenses":[{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown","Type":""}],"countries":[{"ID":"de","Slug":"germany","Name":"Germany"},{"ID":"is","Slug":"iceland","Name":"Iceland"},{"ID":"zz","Slug":"zz","Name":"Unknown"}],"thumbnails":[{"Name":"fit_720","Width":720,"Height":720},{"Name":"fit_2048","Width":2048,"Height":2048},{"Name":"fit_1280","Width":1280,"Height":1024},{"Name":"fit_1920","Width":1920,"Height":1200},{"Name":"fit_2560","Width":2560,"Height":1600},{"Name":"fit_3840","Width":3840,"Height":2400}],"downloadToken":"1uhovi0e","previewToken":"static","jsHash":"0fd34136","cssHash":"2b327230","settings":{"theme":"default","language":"en","templates":{"default":"index.tmpl"},"maps":{"animate":0,"style":"streets"},"features":{"archive":true,"private":true,"review":true,"upload":true,"import":true,"files":true,"moments":true,"labels":true,"places":true,"download":true,"edit":true,"share":true,"logs":true},"import":{"path":"/","move":false},"index":{"path":"/","convert":true,"rescan":false,"group":true}},"count":{"cameras":1,"lenses":0,"countries":2,"photos":126,"videos":0,"hidden":3,"favorites":1,"private":0,"review":0,"stories":0,"albums":0,"moments":0,"months":0,"folders":0,"files":255,"places":0,"labels":13,"labelMaxPhotos":1},"pos":{"uid":"","loc":"","utc":"0001-01-01T00:00:00Z","lat":0,"lng":0},"years":[2003,2002],"colors":[{"Example":"#AB47BC","Name":"Purple","Slug":"purple"},{"Example":"#FF00FF","Name":"Magenta","Slug":"magenta"},{"Example":"#EC407A","Name":"Pink","Slug":"pink"},{"Example":"#EF5350","Name":"Red","Slug":"red"},{"Example":"#FFA726","Name":"Orange","Slug":"orange"},{"Example":"#D4AF37","Name":"Gold","Slug":"gold"},{"Example":"#FDD835","Name":"Yellow","Slug":"yellow"},{"Example":"#CDDC39","Name":"Lime","Slug":"lime"},{"Example":"#66BB6A","Name":"Green","Slug":"green"},{"Example":"#009688","Name":"Teal","Slug":"teal"},{"Example":"#00BCD4","Name":"Cyan","Slug":"cyan"},{"Example":"#2196F3","Name":"Blue","Slug":"blue"},{"Example":"#A1887F","Name":"Brown","Slug":"brown"},{"Example":"#F5F5F5","Name":"White","Slug":"white"},{"Example":"#9E9E9E","Name":"Grey","Slug":"grey"},{"Example":"#212121","Name":"Black","Slug":"black"}],"categories":[{"UID":"lqb6y631re96cper","Slug":"animal","Name":"Animal"},{"UID":"lqb6y5gvo9avdfx5","Slug":"architecture","Name":"Architecture"},{"UID":"lqb6y633nhfj1uzt","Slug":"bird","Name":"Bird"},{"UID":"lqb6y633g3hxg1aq","Slug":"farm","Name":"Farm"},{"UID":"lqb6y4i1ez9cw5bi","Slug":"nature","Name":"Nature"},{"UID":"lqb6y4f2v7dw8irs","Slug":"plant","Name":"Plant"},{"UID":"lqb6y6s2ohhmu0fn","Slug":"reptile","Name":"Reptile"},{"UID":"lqb6y6ctgsq2g2np","Slug":"water","Name":"Water"}],"clip":160,"server":{"cores":2,"routines":23,"memory":{"used":1224531272,"reserved":1416904088,"info":"Used 1.2 GB / Reserved 1.4 GB"}}}; window.__CONFIG__ = {"name":"PhotoPrism","version":"200531-4684f66-Linux-x86_64-DEBUG","copyright":"(c) 2018-2020 PhotoPrism.org \u003chello@photoprism.org\u003e","flags":"public debug experimental settings","siteUrl":"http://localhost:2342/","siteTitle":"PhotoPrism","siteCaption":"Browse your life","siteDescription":"Personal Photo Management powered by Go and Google TensorFlow. Free and open-source.","siteAuthor":"Anonymous","debug":true,"readonly":false,"uploadNSFW":false,"public":true,"experimental":true,"disableSettings":false,"albumCategories":null,"albums":[],"cameras":[{"ID":2,"Slug":"olympus-c2500l","Name":"Olympus C2500L","Make":"Olympus","Model":"C2500L"},{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown"}],"lenses":[{"ID":1,"Slug":"zz","Name":"Unknown","Make":"","Model":"Unknown","Type":""}],"countries":[{"ID":"de","Slug":"germany","Name":"Germany"},{"ID":"is","Slug":"iceland","Name":"Iceland"},{"ID":"zz","Slug":"zz","Name":"Unknown"}],"thumbs":[{"Name":"fit_720","Width":720,"Height":720},{"Name":"fit_2048","Width":2048,"Height":2048},{"Name":"fit_1280","Width":1280,"Height":1024},{"Name":"fit_1920","Width":1920,"Height":1200},{"Name":"fit_2560","Width":2560,"Height":1600},{"Name":"fit_3840","Width":3840,"Height":2400}],"downloadToken":"1uhovi0e","previewToken":"static","jsHash":"0fd34136","cssHash":"2b327230","settings":{"theme":"default","language":"en","templates":{"default":"index.tmpl"},"maps":{"animate":0,"style":"streets"},"features":{"archive":true,"private":true,"review":true,"upload":true,"import":true,"files":true,"moments":true,"labels":true,"places":true,"download":true,"edit":true,"share":true,"logs":true},"import":{"path":"/","move":false},"index":{"path":"/","convert":true,"rescan":false,"group":true}},"count":{"cameras":1,"lenses":0,"countries":2,"photos":126,"videos":0,"hidden":3,"favorites":1,"private":0,"review":0,"stories":0,"albums":0,"moments":0,"months":0,"folders":0,"files":255,"places":0,"labels":13,"labelMaxPhotos":1},"pos":{"uid":"","loc":"","utc":"0001-01-01T00:00:00Z","lat":0,"lng":0},"years":[2003,2002],"colors":[{"Example":"#AB47BC","Name":"Purple","Slug":"purple"},{"Example":"#FF00FF","Name":"Magenta","Slug":"magenta"},{"Example":"#EC407A","Name":"Pink","Slug":"pink"},{"Example":"#EF5350","Name":"Red","Slug":"red"},{"Example":"#FFA726","Name":"Orange","Slug":"orange"},{"Example":"#D4AF37","Name":"Gold","Slug":"gold"},{"Example":"#FDD835","Name":"Yellow","Slug":"yellow"},{"Example":"#CDDC39","Name":"Lime","Slug":"lime"},{"Example":"#66BB6A","Name":"Green","Slug":"green"},{"Example":"#009688","Name":"Teal","Slug":"teal"},{"Example":"#00BCD4","Name":"Cyan","Slug":"cyan"},{"Example":"#2196F3","Name":"Blue","Slug":"blue"},{"Example":"#A1887F","Name":"Brown","Slug":"brown"},{"Example":"#F5F5F5","Name":"White","Slug":"white"},{"Example":"#9E9E9E","Name":"Grey","Slug":"grey"},{"Example":"#212121","Name":"Black","Slug":"black"}],"categories":[{"UID":"lqb6y631re96cper","Slug":"animal","Name":"Animal"},{"UID":"lqb6y5gvo9avdfx5","Slug":"architecture","Name":"Architecture"},{"UID":"lqb6y633nhfj1uzt","Slug":"bird","Name":"Bird"},{"UID":"lqb6y633g3hxg1aq","Slug":"farm","Name":"Farm"},{"UID":"lqb6y4i1ez9cw5bi","Slug":"nature","Name":"Nature"},{"UID":"lqb6y4f2v7dw8irs","Slug":"plant","Name":"Plant"},{"UID":"lqb6y6s2ohhmu0fn","Slug":"reptile","Name":"Reptile"},{"UID":"lqb6y6ctgsq2g2np","Slug":"water","Name":"Water"}],"clip":160,"server":{"cores":2,"routines":23,"memory":{"used":1224531272,"reserved":1416904088,"info":"Used 1.2 GB / Reserved 1.4 GB"}}};
let chai = require("chai/chai"); let chai = require("chai/chai");
let assert = chai.assert; let assert = chai.assert;

View file

@ -2,12 +2,10 @@ package api
import ( import (
"archive/zip" "archive/zip"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"path" "path"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -20,7 +18,6 @@ import (
"github.com/photoprism/photoprism/internal/photoprism" "github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service" "github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
@ -503,115 +500,3 @@ func DownloadAlbum(router *gin.RouterGroup) {
} }
}) })
} }
// GET /api/v1/albums/:uid/t/:token/:type
//
// Parameters:
// uid: string Album UID
// type: string Thumbnail type, see photoprism.ThumbnailTypes
func AlbumThumbnail(router *gin.RouterGroup) {
router.GET("/albums/:uid/t/:token/:type", func(c *gin.Context) {
if InvalidPreviewToken(c) {
c.Data(http.StatusForbidden, "image/svg+xml", albumIconSvg)
return
}
start := time.Now()
conf := service.Config()
typeName := c.Param("type")
uid := c.Param("uid")
thumbType, ok := thumb.Types[typeName]
if !ok {
log.Errorf("album-thumbnail: invalid type %s", typeName)
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
return
}
cache := service.Cache()
cacheKey := fmt.Sprintf("album-thumbnail:%s:%s", uid, typeName)
if cacheData, err := cache.Get(cacheKey); err == nil {
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
var cached ThumbCache
if err := json.Unmarshal(cacheData, &cached); err != nil {
log.Errorf("album-thumbnail: %s not found", uid)
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
return
}
if !fs.FileExists(cached.FileName) {
log.Errorf("album-thumbnail: %s not found", uid)
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
return
}
if c.Query("download") != "" {
c.FileAttachment(cached.FileName, cached.ShareName)
} else {
c.File(cached.FileName)
}
return
}
f, err := query.AlbumCoverByUID(uid)
if err != nil {
log.Debugf("album-thumbnail: no photos yet, using generic image for %s", uid)
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
return
}
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
log.Errorf("album-thumbnail: could not find original for %s", fileName)
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore.
log.Warnf("album-thumbnail: %s is missing", txt.Quote(f.FileName))
logError("album-thumbnail", f.Update("FileMissing", true))
return
}
// Use original file if thumb size exceeds limit, see https://github.com/photoprism/photoprism/issues/157
if thumbType.ExceedsLimit() && c.Query("download") == "" {
log.Debugf("album-thumbnail: using original, size exceeds limit (width %d, height %d)", thumbType.Width, thumbType.Height)
c.File(fileName)
return
}
var thumbnail string
if conf.ThumbUncached() || thumbType.OnDemand() {
thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
} else {
thumbnail, err = thumb.FromCache(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
}
if err != nil {
log.Errorf("album: %s", err)
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
return
} else if thumbnail == "" {
log.Errorf("album-thumbnail: %s has empty thumb name - bug?", filepath.Base(fileName))
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
return
}
if cached, err := json.Marshal(ThumbCache{thumbnail, f.ShareFileName()}); err == nil {
logError("album-thumbnail", cache.Set(cacheKey, cached))
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
}
if c.Query("download") != "" {
c.FileAttachment(thumbnail, f.ShareFileName())
} else {
c.File(thumbnail)
}
})
}

View file

@ -266,25 +266,3 @@ func TestDownloadAlbum(t *testing.T) {
assert.Equal(t, http.StatusOK, r.Code) assert.Equal(t, http.StatusOK, r.Code)
}) })
} }
func TestAlbumThumbnail(t *testing.T) {
t.Run("invalid type", func(t *testing.T) {
app, router, conf := NewApiTest()
AlbumThumbnail(router)
r := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba7/t/"+conf.PreviewToken()+"/xxx")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("album has no photo (because is not existing)", func(t *testing.T) {
app, router, conf := NewApiTest()
AlbumThumbnail(router)
r := PerformRequest(app, "GET", "/api/v1/albums/987-986435/t/"+conf.PreviewToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("album: could not find original", func(t *testing.T) {
app, router, conf := NewApiTest()
AlbumThumbnail(router)
r := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba8/t/"+conf.PreviewToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
}

View file

@ -1,12 +1,8 @@
package api package api
import ( import (
"encoding/json"
"fmt"
"net/http" "net/http"
"path/filepath"
"strconv" "strconv"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
@ -15,11 +11,7 @@ import (
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/i18n" "github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
) )
@ -168,117 +160,3 @@ func DislikeLabel(router *gin.RouterGroup) {
c.JSON(http.StatusOK, http.Response{}) c.JSON(http.StatusOK, http.Response{})
}) })
} }
// GET /api/v1/labels/:uid/t/:token/:type
//
// Parameters:
// uid: string Label UID
// type: string Thumbnail type, see photoprism.ThumbnailTypes
func LabelThumbnail(router *gin.RouterGroup) {
router.GET("/labels/:uid/t/:token/:type", func(c *gin.Context) {
if InvalidPreviewToken(c) {
c.Data(http.StatusForbidden, "image/svg+xml", labelIconSvg)
return
}
start := time.Now()
conf := service.Config()
typeName := c.Param("type")
uid := c.Param("uid")
thumbType, ok := thumb.Types[typeName]
if !ok {
log.Errorf("label-thumbnail: invalid type %s", txt.Quote(typeName))
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
return
}
cache := service.Cache()
cacheKey := fmt.Sprintf("label-thumbnail:%s:%s", uid, typeName)
if cacheData, err := cache.Get(cacheKey); err == nil {
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
var cached ThumbCache
if err := json.Unmarshal(cacheData, &cached); err != nil {
log.Errorf("label-thumbnail: %s not found", uid)
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
return
}
if !fs.FileExists(cached.FileName) {
log.Errorf("label-thumbnail: %s not found", uid)
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
return
}
if c.Query("download") != "" {
c.FileAttachment(cached.FileName, cached.ShareName)
} else {
c.File(cached.FileName)
}
return
}
f, err := query.LabelThumbByUID(uid)
if err != nil {
log.Errorf(err.Error())
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
return
}
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
log.Errorf("label-thumbnail: file %s is missing", txt.Quote(f.FileName))
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore.
logError("label-thumbnail", f.Update("FileMissing", true))
return
}
// Use original file if thumb size exceeds limit, see https://github.com/photoprism/photoprism/issues/157
if thumbType.ExceedsLimit() {
log.Debugf("label-thumbnail: using original, size exceeds limit (width %d, height %d)", thumbType.Width, thumbType.Height)
c.File(fileName)
return
}
var thumbnail string
if conf.ThumbUncached() || thumbType.OnDemand() {
thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
} else {
thumbnail, err = thumb.FromCache(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
}
if err != nil {
log.Errorf("label-thumbnail: %s", err)
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
return
} else if thumbnail == "" {
log.Errorf("label-thumbnail: %s has empty thumb name - bug?", filepath.Base(fileName))
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
return
}
if cached, err := json.Marshal(ThumbCache{thumbnail, f.ShareFileName()}); err == nil {
logError("label-thumbnail", cache.Set(cacheKey, cached))
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
}
if c.Query("download") != "" {
c.FileAttachment(thumbnail, f.ShareFileName())
} else {
c.File(thumbnail)
}
})
}

View file

@ -108,25 +108,3 @@ func TestDislikeLabel(t *testing.T) {
assert.Equal(t, "false", val2.String()) assert.Equal(t, "false", val2.String())
}) })
} }
func TestLabelThumbnail(t *testing.T) {
t.Run("invalid type", func(t *testing.T) {
app, router, conf := NewApiTest()
LabelThumbnail(router)
r := PerformRequest(app, "GET", "/api/v1/labels/lt9k3pw1wowuy3c2/t/"+conf.PreviewToken()+"/xxx")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("invalid label", func(t *testing.T) {
app, router, conf := NewApiTest()
LabelThumbnail(router)
r := PerformRequest(app, "GET", "/api/v1/labels/xxx/t/"+conf.PreviewToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("could not find original", func(t *testing.T) {
app, router, conf := NewApiTest()
LabelThumbnail(router)
r := PerformRequest(app, "GET", "/api/v1/labels/lt9k3pw1wowuy3c3/t/"+conf.PreviewToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
}

View file

@ -329,85 +329,3 @@ func PhotoFilePrimary(router *gin.RouterGroup) {
c.JSON(http.StatusOK, p) c.JSON(http.StatusOK, p)
}) })
} }
// POST /api/v1/photos/:uid/files/:file_uid/unstack
//
// Parameters:
// uid: string Photo UID as returned by the API
// file_uid: string File UID as returned by the API
func PhotoFileUnstack(router *gin.RouterGroup) {
router.POST("/photos/:uid/files/:file_uid/unstack", func(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
if s.Invalid() {
AbortUnauthorized(c)
return
}
photoUID := c.Param("uid")
fileUID := c.Param("file_uid")
file, err := query.FileByUID(fileUID)
if err != nil {
log.Errorf("photo: %s (unstack)", err)
AbortEntityNotFound(c)
return
}
if file.FilePrimary {
log.Errorf("photo: can't unstack primary file")
AbortBadRequest(c)
return
}
existingPhoto := *file.Photo
newPhoto := entity.NewPhoto()
if err := newPhoto.Create(); err != nil {
log.Errorf("photo: %s (unstack)", err.Error())
AbortSaveFailed(c)
return
}
file.Photo = &newPhoto
file.PhotoID = newPhoto.ID
file.PhotoUID = newPhoto.PhotoUID
if err := file.Save(); err != nil {
log.Errorf("photo: %s (unstack)", err.Error())
AbortSaveFailed(c)
return
}
fileName := photoprism.FileName(file.FileRoot, file.FileName)
f, err := photoprism.NewMediaFile(fileName)
if err != nil {
log.Errorf("photo: %s (unstack)", err)
AbortEntityNotFound(c)
return
}
if err := service.Index().MediaFile(f, photoprism.IndexOptions{Rescan: true}, existingPhoto.OriginalName).Error; err != nil {
log.Errorf("photo: %s (unstack)", err)
AbortSaveFailed(c)
return
}
PublishPhotoEvent(EntityCreated, file.PhotoUID, c)
PublishPhotoEvent(EntityUpdated, photoUID, c)
event.SuccessMsg(i18n.MsgFileUnstacked)
p, err := query.PhotoPreloadByUID(photoUID)
if err != nil {
AbortEntityNotFound(c)
return
}
c.JSON(http.StatusOK, p)
})
}

View file

@ -1,162 +0,0 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"time"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
)
type ThumbCache struct {
FileName string
ShareName string
}
type ByteCache struct {
Data []byte
}
// GET /api/v1/t/:hash/:token/:type
//
// Parameters:
// hash: string The file hash as returned by the search API
// type: string Thumbnail type, see photoprism.ThumbnailTypes
func GetThumbnail(router *gin.RouterGroup) {
router.GET("/t/:hash/:token/:type", func(c *gin.Context) {
if InvalidPreviewToken(c) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}
start := time.Now()
conf := service.Config()
fileHash := c.Param("hash")
typeName := c.Param("type")
thumbType, ok := thumb.Types[typeName]
if !ok {
log.Errorf("thumbnail: invalid type %s", txt.Quote(typeName))
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
return
}
cache := service.Cache()
cacheKey := fmt.Sprintf("thumbnail:%s:%s", fileHash, typeName)
if cacheData, err := cache.Get(cacheKey); err == nil {
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
var cached ThumbCache
if err := json.Unmarshal(cacheData, &cached); err != nil {
log.Errorf("thumbnail: %s not found", fileHash)
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
return
}
if !fs.FileExists(cached.FileName) {
log.Errorf("thumbnail: %s not found", fileHash)
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
return
}
if c.Query("download") != "" {
c.FileAttachment(cached.FileName, cached.ShareName)
} else {
c.File(cached.FileName)
}
return
}
f, err := query.FileByHash(fileHash)
if err != nil {
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
return
}
// Find fallback if file is not a JPEG image.
if f.NoJPEG() {
f, err = query.FileByPhotoUID(f.PhotoUID)
if err != nil {
c.Data(http.StatusOK, "image/svg+xml", fileIconSvg)
return
}
}
// Return SVG icon as placeholder if file has errors.
if f.FileError != "" {
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
return
}
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
log.Errorf("thumbnail: file %s is missing", txt.Quote(f.FileName))
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore.
logError("thumbnail", f.Update("FileMissing", true))
if f.AllFilesMissing() {
log.Infof("thumbnail: deleting photo, all files missing for %s", txt.Quote(f.FileName))
logError("thumbnail", f.RelatedPhoto().Delete(false))
}
return
}
// Use original file if thumb size exceeds limit, see https://github.com/photoprism/photoprism/issues/157
if thumbType.ExceedsLimit() && c.Query("download") == "" {
log.Debugf("thumbnail: using original, size exceeds limit (width %d, height %d)", thumbType.Width, thumbType.Height)
c.File(fileName)
return
}
var thumbnail string
if conf.ThumbUncached() || thumbType.OnDemand() {
thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
} else {
thumbnail, err = thumb.FromCache(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
}
if err != nil {
log.Errorf("thumbnail: %s", err)
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
return
} else if thumbnail == "" {
log.Errorf("thumbnail: %s has empty thumb name - bug?", filepath.Base(fileName))
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
return
}
// Cache thumbnail filename.
if cached, err := json.Marshal(ThumbCache{thumbnail, f.ShareFileName()}); err == nil {
logError("thumbnail", cache.Set(cacheKey, cached))
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
}
if c.Query("download") != "" {
c.FileAttachment(thumbnail, f.ShareFileName())
} else {
c.File(thumbnail)
}
})
}

View file

@ -1,31 +0,0 @@
package api
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetThumbnail(t *testing.T) {
t.Run("invalid type", func(t *testing.T) {
app, router, conf := NewApiTest()
GetThumbnail(router)
r := PerformRequest(app, "GET", "/api/v1/t/1/"+conf.PreviewToken()+"/xxx")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("invalid hash", func(t *testing.T) {
app, router, conf := NewApiTest()
GetThumbnail(router)
r := PerformRequest(app, "GET", "/api/v1/t/1/"+conf.PreviewToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("could not find original", func(t *testing.T) {
app, router, conf := NewApiTest()
GetThumbnail(router)
r := PerformRequest(app, "GET", "/api/v1/t/2cad9168fa6acc5c5c2965ddf6ec465ca42fd818/"+conf.PreviewToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
}

View file

@ -0,0 +1,96 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
)
// POST /api/v1/photos/:uid/files/:file_uid/unstack
//
// Parameters:
// uid: string Photo UID as returned by the API
// file_uid: string File UID as returned by the API
func PhotoFileUnstack(router *gin.RouterGroup) {
router.POST("/photos/:uid/files/:file_uid/unstack", func(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
if s.Invalid() {
AbortUnauthorized(c)
return
}
photoUID := c.Param("uid")
fileUID := c.Param("file_uid")
file, err := query.FileByUID(fileUID)
if err != nil {
log.Errorf("photo: %s (unstack)", err)
AbortEntityNotFound(c)
return
}
if file.FilePrimary {
log.Errorf("photo: can't unstack primary file")
AbortBadRequest(c)
return
}
existingPhoto := *file.Photo
newPhoto := entity.NewPhoto()
if err := newPhoto.Create(); err != nil {
log.Errorf("photo: %s (unstack)", err.Error())
AbortSaveFailed(c)
return
}
file.Photo = &newPhoto
file.PhotoID = newPhoto.ID
file.PhotoUID = newPhoto.PhotoUID
if err := file.Save(); err != nil {
log.Errorf("photo: %s (unstack)", err.Error())
AbortSaveFailed(c)
return
}
fileName := photoprism.FileName(file.FileRoot, file.FileName)
f, err := photoprism.NewMediaFile(fileName)
if err != nil {
log.Errorf("photo: %s (unstack)", err)
AbortEntityNotFound(c)
return
}
if err := service.Index().MediaFile(f, photoprism.IndexOptions{Rescan: true}, existingPhoto.OriginalName).Error; err != nil {
log.Errorf("photo: %s (unstack)", err)
AbortSaveFailed(c)
return
}
PublishPhotoEvent(EntityCreated, file.PhotoUID, c)
PublishPhotoEvent(EntityUpdated, photoUID, c)
event.SuccessMsg(i18n.MsgFileUnstacked)
p, err := query.PhotoPreloadByUID(photoUID)
if err != nil {
AbortEntityNotFound(c)
return
}
c.JSON(http.StatusOK, p)
})
}

391
internal/api/thumbs.go Normal file
View file

@ -0,0 +1,391 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"time"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
)
type ThumbCache struct {
FileName string
ShareName string
}
type ByteCache struct {
Data []byte
}
// GET /api/v1/t/:hash/:token/:type
//
// Parameters:
// hash: string file hash as returned by the search API
// token: string security token (see config)
// type: string thumb type, see photoprism.ThumbnailTypes
func GetThumb(router *gin.RouterGroup) {
router.GET("/t/:hash/:token/:type", func(c *gin.Context) {
if InvalidPreviewToken(c) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}
start := time.Now()
conf := service.Config()
fileHash := c.Param("hash")
typeName := c.Param("type")
thumbType, ok := thumb.Types[typeName]
if !ok {
log.Errorf("thumbs: invalid type %s", txt.Quote(typeName))
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
return
}
cache := service.Cache()
cacheKey := fmt.Sprintf("thumbs:%s:%s", fileHash, typeName)
if cacheData, err := cache.Get(cacheKey); err == nil {
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
var cached ThumbCache
if err := json.Unmarshal(cacheData, &cached); err != nil {
log.Errorf("thumbs: %s not found", fileHash)
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
return
}
if !fs.FileExists(cached.FileName) {
log.Errorf("thumbs: %s not found", fileHash)
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
return
}
if c.Query("download") != "" {
c.FileAttachment(cached.FileName, cached.ShareName)
} else {
c.File(cached.FileName)
}
return
}
f, err := query.FileByHash(fileHash)
if err != nil {
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
return
}
// Find fallback if file is not a JPEG image.
if f.NoJPEG() {
f, err = query.FileByPhotoUID(f.PhotoUID)
if err != nil {
c.Data(http.StatusOK, "image/svg+xml", fileIconSvg)
return
}
}
// Return SVG icon as placeholder if file has errors.
if f.FileError != "" {
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
return
}
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
log.Errorf("thumbs: file %s is missing", txt.Quote(f.FileName))
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore.
logError("thumbnail", f.Update("FileMissing", true))
if f.AllFilesMissing() {
log.Infof("thumbs: deleting photo, all files missing for %s", txt.Quote(f.FileName))
logError("thumbnail", f.RelatedPhoto().Delete(false))
}
return
}
// Use original file if thumb size exceeds limit, see https://github.com/photoprism/photoprism/issues/157
if thumbType.ExceedsLimit() && c.Query("download") == "" {
log.Debugf("thumbs: using original, size exceeds limit (width %d, height %d)", thumbType.Width, thumbType.Height)
c.File(fileName)
return
}
var thumbnail string
if conf.ThumbUncached() || thumbType.OnDemand() {
thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
} else {
thumbnail, err = thumb.FromCache(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
}
if err != nil {
log.Errorf("thumbs: %s", err)
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
return
} else if thumbnail == "" {
log.Errorf("thumbs: %s has empty thumb name - bug?", filepath.Base(fileName))
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
return
}
// Cache thumbnail filename.
if cached, err := json.Marshal(ThumbCache{thumbnail, f.ShareFileName()}); err == nil {
logError("thumbnail", cache.Set(cacheKey, cached))
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
}
if c.Query("download") != "" {
c.FileAttachment(thumbnail, f.ShareFileName())
} else {
c.File(thumbnail)
}
})
}
// GET /api/v1/albums/:uid/t/:token/:type
//
// Parameters:
// uid: string album uid
// token: string security token (see config)
// type: string thumb type, see photoprism.ThumbnailTypes
func AlbumThumb(router *gin.RouterGroup) {
router.GET("/albums/:uid/t/:token/:type", func(c *gin.Context) {
if InvalidPreviewToken(c) {
c.Data(http.StatusForbidden, "image/svg+xml", albumIconSvg)
return
}
start := time.Now()
conf := service.Config()
typeName := c.Param("type")
uid := c.Param("uid")
thumbType, ok := thumb.Types[typeName]
if !ok {
log.Errorf("album-thumbs: invalid type %s", typeName)
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
return
}
cache := service.Cache()
cacheKey := fmt.Sprintf("album-thumbs:%s:%s", uid, typeName)
if cacheData, err := cache.Get(cacheKey); err == nil {
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
var cached ThumbCache
if err := json.Unmarshal(cacheData, &cached); err != nil {
log.Errorf("album-thumbs: %s not found", uid)
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
return
}
if !fs.FileExists(cached.FileName) {
log.Errorf("album-thumbs: %s not found", uid)
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
return
}
if c.Query("download") != "" {
c.FileAttachment(cached.FileName, cached.ShareName)
} else {
c.File(cached.FileName)
}
return
}
f, err := query.AlbumCoverByUID(uid)
if err != nil {
log.Debugf("album-thumbs: no photos yet, using generic image for %s", uid)
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
return
}
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
log.Errorf("album-thumbs: could not find original for %s", fileName)
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore.
log.Warnf("album-thumbs: %s is missing", txt.Quote(f.FileName))
logError("album-thumbnail", f.Update("FileMissing", true))
return
}
// Use original file if thumb size exceeds limit, see https://github.com/photoprism/photoprism/issues/157
if thumbType.ExceedsLimit() && c.Query("download") == "" {
log.Debugf("album-thumbs: using original, size exceeds limit (width %d, height %d)", thumbType.Width, thumbType.Height)
c.File(fileName)
return
}
var thumbnail string
if conf.ThumbUncached() || thumbType.OnDemand() {
thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
} else {
thumbnail, err = thumb.FromCache(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
}
if err != nil {
log.Errorf("album: %s", err)
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
return
} else if thumbnail == "" {
log.Errorf("album-thumbs: %s has empty thumb name - bug?", filepath.Base(fileName))
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
return
}
if cached, err := json.Marshal(ThumbCache{thumbnail, f.ShareFileName()}); err == nil {
logError("album-thumbnail", cache.Set(cacheKey, cached))
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
}
if c.Query("download") != "" {
c.FileAttachment(thumbnail, f.ShareFileName())
} else {
c.File(thumbnail)
}
})
}
// GET /api/v1/labels/:uid/t/:token/:type
//
// Parameters:
// uid: string label uid
// token: string security token (see config)
// type: string thumb type, see photoprism.ThumbnailTypes
func LabelThumb(router *gin.RouterGroup) {
router.GET("/labels/:uid/t/:token/:type", func(c *gin.Context) {
if InvalidPreviewToken(c) {
c.Data(http.StatusForbidden, "image/svg+xml", labelIconSvg)
return
}
start := time.Now()
conf := service.Config()
typeName := c.Param("type")
uid := c.Param("uid")
thumbType, ok := thumb.Types[typeName]
if !ok {
log.Errorf("label-thumbs: invalid type %s", txt.Quote(typeName))
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
return
}
cache := service.Cache()
cacheKey := fmt.Sprintf("label-thumbs:%s:%s", uid, typeName)
if cacheData, err := cache.Get(cacheKey); err == nil {
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
var cached ThumbCache
if err := json.Unmarshal(cacheData, &cached); err != nil {
log.Errorf("label-thumbs: %s not found", uid)
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
return
}
if !fs.FileExists(cached.FileName) {
log.Errorf("label-thumbs: %s not found", uid)
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
return
}
if c.Query("download") != "" {
c.FileAttachment(cached.FileName, cached.ShareName)
} else {
c.File(cached.FileName)
}
return
}
f, err := query.LabelThumbByUID(uid)
if err != nil {
log.Errorf(err.Error())
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
return
}
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
log.Errorf("label-thumbs: file %s is missing", txt.Quote(f.FileName))
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore.
logError("label-thumbnail", f.Update("FileMissing", true))
return
}
// Use original file if thumb size exceeds limit, see https://github.com/photoprism/photoprism/issues/157
if thumbType.ExceedsLimit() {
log.Debugf("label-thumbs: using original, size exceeds limit (width %d, height %d)", thumbType.Width, thumbType.Height)
c.File(fileName)
return
}
var thumbnail string
if conf.ThumbUncached() || thumbType.OnDemand() {
thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
} else {
thumbnail, err = thumb.FromCache(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
}
if err != nil {
log.Errorf("label-thumbs: %s", err)
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
return
} else if thumbnail == "" {
log.Errorf("label-thumbs: %s has empty thumb name - bug?", filepath.Base(fileName))
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
return
}
if cached, err := json.Marshal(ThumbCache{thumbnail, f.ShareFileName()}); err == nil {
logError("label-thumbnail", cache.Set(cacheKey, cached))
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
}
if c.Query("download") != "" {
c.FileAttachment(thumbnail, f.ShareFileName())
} else {
c.File(thumbnail)
}
})
}

View file

@ -0,0 +1,75 @@
package api
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetThumb(t *testing.T) {
t.Run("invalid type", func(t *testing.T) {
app, router, conf := NewApiTest()
GetThumb(router)
r := PerformRequest(app, "GET", "/api/v1/t/1/"+conf.PreviewToken()+"/xxx")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("invalid hash", func(t *testing.T) {
app, router, conf := NewApiTest()
GetThumb(router)
r := PerformRequest(app, "GET", "/api/v1/t/1/"+conf.PreviewToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("could not find original", func(t *testing.T) {
app, router, conf := NewApiTest()
GetThumb(router)
r := PerformRequest(app, "GET", "/api/v1/t/2cad9168fa6acc5c5c2965ddf6ec465ca42fd818/"+conf.PreviewToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
}
func TestAlbumThumb(t *testing.T) {
t.Run("invalid type", func(t *testing.T) {
app, router, conf := NewApiTest()
AlbumThumb(router)
r := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba7/t/"+conf.PreviewToken()+"/xxx")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("album has no photo (because is not existing)", func(t *testing.T) {
app, router, conf := NewApiTest()
AlbumThumb(router)
r := PerformRequest(app, "GET", "/api/v1/albums/987-986435/t/"+conf.PreviewToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("album: could not find original", func(t *testing.T) {
app, router, conf := NewApiTest()
AlbumThumb(router)
r := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba8/t/"+conf.PreviewToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
}
func TestLabelThumb(t *testing.T) {
t.Run("invalid type", func(t *testing.T) {
app, router, conf := NewApiTest()
LabelThumb(router)
r := PerformRequest(app, "GET", "/api/v1/labels/lt9k3pw1wowuy3c2/t/"+conf.PreviewToken()+"/xxx")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("invalid label", func(t *testing.T) {
app, router, conf := NewApiTest()
LabelThumb(router)
r := PerformRequest(app, "GET", "/api/v1/labels/xxx/t/"+conf.PreviewToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("could not find original", func(t *testing.T) {
app, router, conf := NewApiTest()
LabelThumb(router)
r := PerformRequest(app, "GET", "/api/v1/labels/lt9k3pw1wowuy3c3/t/"+conf.PreviewToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
}

View file

@ -100,7 +100,7 @@ func configAction(ctx *cli.Context) error {
// Places / Geocoding API configuration. // Places / Geocoding API configuration.
fmt.Printf("%-25s %s\n", "geocoding-api", conf.GeoCodingApi()) fmt.Printf("%-25s %s\n", "geocoding-api", conf.GeoCodingApi())
// Thumbnails, resampling and download security token. // Thumbs, resampling and download security token.
fmt.Printf("%-25s %s\n", "download-token", conf.DownloadToken()) fmt.Printf("%-25s %s\n", "download-token", conf.DownloadToken())
fmt.Printf("%-25s %s\n", "preview-token", conf.PreviewToken()) fmt.Printf("%-25s %s\n", "preview-token", conf.PreviewToken())
fmt.Printf("%-25s %s\n", "thumb-filter", conf.ThumbFilter()) fmt.Printf("%-25s %s\n", "thumb-filter", conf.ThumbFilter())

View file

@ -33,7 +33,7 @@ type ClientConfig struct {
Cameras []entity.Camera `json:"cameras"` Cameras []entity.Camera `json:"cameras"`
Lenses []entity.Lens `json:"lenses"` Lenses []entity.Lens `json:"lenses"`
Countries []entity.Country `json:"countries"` Countries []entity.Country `json:"countries"`
Thumbnails []Thumbnail `json:"thumbnails"` Thumbs []Thumb `json:"thumbs"`
DownloadToken string `json:"downloadToken"` DownloadToken string `json:"downloadToken"`
PreviewToken string `json:"previewToken"` PreviewToken string `json:"previewToken"`
JSHash string `json:"jsHash"` JSHash string `json:"jsHash"`
@ -138,7 +138,7 @@ func (c *Config) PublicConfig() ClientConfig {
ReadOnly: c.ReadOnly(), ReadOnly: c.ReadOnly(),
Public: c.Public(), Public: c.Public(),
Experimental: c.Experimental(), Experimental: c.Experimental(),
Thumbnails: Thumbnails, Thumbs: Thumbs,
Colors: colors.All.List(), Colors: colors.All.List(),
JSHash: fs.Checksum(c.BuildPath() + "/app.js"), JSHash: fs.Checksum(c.BuildPath() + "/app.js"),
CSSHash: fs.Checksum(c.BuildPath() + "/app.css"), CSSHash: fs.Checksum(c.BuildPath() + "/app.css"),
@ -178,7 +178,7 @@ func (c *Config) GuestConfig() ClientConfig {
Public: true, Public: true,
Experimental: false, Experimental: false,
Colors: colors.All.List(), Colors: colors.All.List(),
Thumbnails: Thumbnails, Thumbs: Thumbs,
DownloadToken: c.DownloadToken(), DownloadToken: c.DownloadToken(),
PreviewToken: c.PreviewToken(), PreviewToken: c.PreviewToken(),
JSHash: fs.Checksum(c.BuildPath() + "/share.js"), JSHash: fs.Checksum(c.BuildPath() + "/share.js"),
@ -210,7 +210,7 @@ func (c *Config) UserConfig() ClientConfig {
Public: c.Public(), Public: c.Public(),
Experimental: c.Experimental(), Experimental: c.Experimental(),
Colors: colors.All.List(), Colors: colors.All.List(),
Thumbnails: Thumbnails, Thumbs: Thumbs,
DownloadToken: c.DownloadToken(), DownloadToken: c.DownloadToken(),
PreviewToken: c.PreviewToken(), PreviewToken: c.PreviewToken(),
JSHash: fs.Checksum(c.BuildPath() + "/app.js"), JSHash: fs.Checksum(c.BuildPath() + "/app.js"),

View file

@ -37,7 +37,7 @@ func init() {
t := thumb.Types[size] t := thumb.Types[size]
if t.Public { if t.Public {
Thumbnails = append(Thumbnails, Thumbnail{Size: size, Use: t.Use, Width: t.Width, Height: t.Height}) Thumbs = append(Thumbs, Thumb{Size: size, Use: t.Use, Width: t.Width, Height: t.Height})
} }
} }
} }

View file

@ -314,7 +314,7 @@ func TestConfig_ClientConfig(t *testing.T) {
assert.NotEmpty(t, cc.Name) assert.NotEmpty(t, cc.Name)
assert.NotEmpty(t, cc.Version) assert.NotEmpty(t, cc.Version)
assert.NotEmpty(t, cc.Copyright) assert.NotEmpty(t, cc.Copyright)
assert.NotEmpty(t, cc.Thumbnails) assert.NotEmpty(t, cc.Thumbs)
assert.NotEmpty(t, cc.JSHash) assert.NotEmpty(t, cc.JSHash)
assert.NotEmpty(t, cc.CSSHash) assert.NotEmpty(t, cc.CSSHash)
assert.Equal(t, true, cc.Debug) assert.Equal(t, true, cc.Debug)

View file

@ -1,12 +0,0 @@
package config
// Thumbnail gives direct access to width and height for a thumbnail setting
type Thumbnail struct {
Size string `json:"size"`
Use string `json:"use"`
Width int `json:"w"`
Height int `json:"h"`
}
// Thumbnails is a list of default thumbnail size available for the app
var Thumbnails []Thumbnail

12
internal/config/thumbs.go Normal file
View file

@ -0,0 +1,12 @@
package config
// Thumb represents thumbnail info for use in client apps.
type Thumb struct {
Size string `json:"size"`
Use string `json:"use"`
Width int `json:"w"`
Height int `json:"h"`
}
// Thumbs is a list of thumbnails for use in client apps.
var Thumbs []Thumb

View file

@ -29,7 +29,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
api.CreateSession(v1) api.CreateSession(v1)
api.DeleteSession(v1) api.DeleteSession(v1)
api.GetThumbnail(v1) api.GetThumb(v1)
api.GetDownload(v1) api.GetDownload(v1)
api.GetVideo(v1) api.GetVideo(v1)
api.CreateZip(v1) api.CreateZip(v1)
@ -64,7 +64,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
api.DeleteLabelLink(v1) api.DeleteLabelLink(v1)
api.LikeLabel(v1) api.LikeLabel(v1)
api.DislikeLabel(v1) api.DislikeLabel(v1)
api.LabelThumbnail(v1) api.LabelThumb(v1)
api.GetFoldersOriginals(v1) api.GetFoldersOriginals(v1)
api.GetFoldersImport(v1) api.GetFoldersImport(v1)
@ -93,7 +93,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
api.DeleteAlbumLink(v1) api.DeleteAlbumLink(v1)
api.LikeAlbum(v1) api.LikeAlbum(v1)
api.DislikeAlbum(v1) api.DislikeAlbum(v1)
api.AlbumThumbnail(v1) api.AlbumThumb(v1)
api.CloneAlbums(v1) api.CloneAlbums(v1)
api.AddPhotosToAlbum(v1) api.AddPhotosToAlbum(v1)
api.RemovePhotosFromAlbum(v1) api.RemovePhotosFromAlbum(v1)