Initial commit for folders and moments #154 #260 #331

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-05-23 20:58:58 +02:00
parent fc62279c0a
commit 03ec4b586d
150 changed files with 2499 additions and 1443 deletions

View file

@ -14,7 +14,9 @@ all: dep build
dep: dep-tensorflow dep-js dep-go
build: generate build-js build-go
install: install-bin install-assets
test: reset-test-db test-js test-go
test: test-js test-go
test-go: reset-test-db run-test-go
test-short: reset-test-db run-test-short
acceptance-all: start acceptance acceptance-firefox stop
test-all: test acceptance-all
fmt: fmt-js fmt-go
@ -91,8 +93,12 @@ acceptance-firefox:
$(info Running JS acceptance tests in Firefox...)
(cd frontend && npm run acceptance-firefox)
reset-test-db:
$(info Purging test database...)
mysql < scripts/reset-test-db.sql
test-go:
run-test-short:
$(info Running short Go unit tests in parallel mode...)
$(GOTEST) -parallel 2 -count 1 -cpu 2 -short -timeout 5m ./pkg/... ./internal/...
run-test-go:
$(info Running all Go unit tests...)
$(GOTEST) -parallel 1 -count 1 -cpu 1 -tags slow -timeout 20m ./pkg/... ./internal/...
test-parallel:
@ -101,9 +107,6 @@ test-parallel:
test-verbose:
$(info Running all Go unit tests in verbose mode...)
$(GOTEST) -parallel 1 -count 1 -cpu 1 -tags slow -timeout 20m -v ./pkg/... ./internal/...
test-short:
$(info Running short Go unit tests in parallel mode...)
$(GOTEST) -parallel 2 -count 1 -cpu 2 -short -timeout 5m ./pkg/... ./internal/...
test-race:
$(info Running all Go unit tests with race detection in verbose mode...)
$(GOTEST) -tags slow -race -timeout 60m -v ./pkg/... ./internal/...

View file

@ -5,33 +5,33 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ .clientConfig.title }}</title>
<title>{{ .config.title }}</title>
<meta property="og:title" content="{{ .clientConfig.title }}: {{ .clientConfig.subtitle }}"/>
<meta property="og:image" content="{{ .clientConfig.url }}api/v1/preview"/>
<meta property="og:url" content="{{ .clientConfig.url }}"/>
<meta property="og:description" content="{{ .clientConfig.description }}"/>
<meta property="og:title" content="{{ .config.title }}: {{ .config.subtitle }}"/>
<meta property="og:image" content="{{ .config.url }}api/v1/preview"/>
<meta property="og:url" content="{{ .config.url }}"/>
<meta property="og:description" content="{{ .config.description }}"/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:title" content="{{ .clientConfig.title }}: {{ .clientConfig.subtitle }}"/>
<meta name="twitter:description" content="{{ .clientConfig.description }}"/>
<meta name="twitter:image" content="{{ .clientConfig.url }}api/v1/preview"/>
<meta name="twitter:title" content="{{ .config.title }}: {{ .config.subtitle }}"/>
<meta name="twitter:description" content="{{ .config.description }}"/>
<meta name="twitter:image" content="{{ .config.url }}api/v1/preview"/>
<meta name="author" content="{{ .clientConfig.author }}">
<meta name="description" content="{{ .clientConfig.description }}"/>
<meta name="author" content="{{ .config.author }}">
<meta name="description" content="{{ .config.description }}"/>
<link rel="shortcut icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/static/favicons/favicon.png">
<link rel="icon" type="image/png" href="/static/favicons/favicon.png"/>
<link rel="stylesheet" href="/static/build/app.css?{{ .clientConfig.cssHash }}">
<link rel="stylesheet" href="/static/build/app.css?{{ .config.cssHash }}">
<link rel="manifest" href="/static/manifest.json">
<script>
window.clientConfig = {{ .clientConfig }};
window.__CONFIG__ = {{ .config }};
</script>
</head>
<body class="{{ .clientConfig.flags }}">
<body class="{{ .config.flags }}">
<!--[if lt IE 8]>
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade
your browser</a> to improve your experience.</p>
@ -44,6 +44,6 @@
<div id="p-busy-overlay"></div>
<script src="/static/build/app.js?{{ .clientConfig.jsHash }}"></script>
<script src="/static/build/app.js?{{ .config.jsHash }}"></script>
</body>
</html>

View file

@ -5,20 +5,20 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ .clientConfig.title }}</title>
<title>{{ .config.title }}</title>
<link rel="shortcut icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/static/favicons/favicon.png">
<link rel="icon" type="image/png" href="/static/favicons/favicon.png"/>
<link rel="stylesheet" href="/static/build/app.css?{{ .clientConfig.cssHash }}">
<link rel="stylesheet" href="/static/build/app.css?{{ .config.cssHash }}">
<link rel="manifest" href="/static/manifest.json">
<script>
window.clientConfig = {{ .clientConfig }};
window.__CONFIG__ = {{ .config }};
</script>
</head>
<body class="{{ .clientConfig.flags }}">
<body class="{{ .config.flags }}">
<!--[if lt IE 8]>
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade
your browser</a> to improve your experience.</p>
@ -31,6 +31,6 @@
<div id="p-busy-overlay"></div>
<script src="/static/build/app.js?{{ .clientConfig.jsHash }}"></script>
<script src="/static/build/app.js?{{ .config.jsHash }}"></script>
</body>
</html>

View file

@ -2,7 +2,7 @@ import Axios from "axios";
import Notify from "common/notify";
const testConfig = {"jsHash": "test", "version": "test"};
const config = window.clientConfig ? window.clientConfig : testConfig;
const config = window.__CONFIG__ ? window.__CONFIG__ : testConfig;
const Api = Axios.create({
baseURL: "/api/v1",

View file

@ -81,7 +81,7 @@ class Clipboard {
return;
}
let rangeStart = models.findIndex((photo) => photo.PhotoUUID === this.lastId);
let rangeStart = models.findIndex((photo) => photo.UID === this.lastId);
if(rangeStart === -1) {
this.toggle(models[rangeEnd]);

View file

@ -57,9 +57,18 @@ class Config {
case "videos":
this.values.count.videos += data.count;
break;
case "folders":
this.values.count.folders += data.count;
break;
case "moments":
this.values.count.moments += data.count;
break;
case "favorites":
this.values.count.favorites += data.count;
break;
case "review":
this.values.count.review += data.count;
break;
case "private":
this.values.count.private += data.count;
break;

View file

@ -136,9 +136,9 @@ export default class Session {
sendClientInfo() {
const clientInfo = {
"session": this.getToken(),
"js": window.clientConfig.jsHash,
"css": window.clientConfig.cssHash,
"version": window.clientConfig.version,
"js": window.__CONFIG__.jsHash,
"css": window.__CONFIG__.cssHash,
"version": window.__CONFIG__.version,
};
try {

View file

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

View file

@ -9,6 +9,7 @@ import PPhotoList from "./p-photo-list.vue";
import PPhotoSearch from "./p-photo-search.vue";
import PPhotoClipboard from "./p-photo-clipboard.vue";
import PLabelClipboard from "./p-label-clipboard.vue";
import PFolderClipboard from "./p-folder-clipboard.vue";
import PAlbumClipboard from "./p-album-clipboard.vue";
import PAlbumToolbar from "./p-album-toolbar.vue";
import PScrollTop from "./p-scroll-top.vue";
@ -27,6 +28,7 @@ components.install = (Vue) => {
Vue.component("p-photo-search", PPhotoSearch);
Vue.component("p-photo-clipboard", PPhotoClipboard);
Vue.component("p-label-clipboard", PLabelClipboard);
Vue.component("p-folder-clipboard", PFolderClipboard);
Vue.component("p-album-clipboard", PAlbumClipboard);
Vue.component("p-album-toolbar", PAlbumToolbar);
Vue.component("p-scroll-top", PScrollTop);

View file

@ -4,16 +4,16 @@
@submit.prevent="filterChange">
<v-toolbar flat color="secondary">
<v-edit-dialog
:return-value.sync="album.AlbumName"
:return-value.sync="album.Name"
lazy
@save="updateAlbum()"
class="p-inline-edit">
<v-toolbar-title>
{{ album.AlbumName }}
{{ album.Name }}
</v-toolbar-title>
<template v-slot:input>
<v-text-field
v-model="album.AlbumName"
v-model="album.Name"
:rules="[titleRule]"
:label="labels.name"
color="secondary-dark"
@ -67,8 +67,8 @@
:label="labels.country"
flat solo hide-details
color="secondary-dark"
item-value="code"
item-text="name"
item-value="ID"
item-text="Name"
v-model="filter.country"
:items="options.countries">
</v-select>
@ -79,7 +79,7 @@
flat solo hide-details
color="secondary-dark"
item-value="ID"
item-text="CameraModel"
item-text="Model"
v-model="filter.camera"
:items="options.cameras">
</v-select>
@ -111,7 +111,7 @@
:key="growDesc"
color="secondary-dark"
style="background-color: white"
v-model="album.AlbumDescription"
v-model="album.Description"
@change="updateAlbum">
</v-textarea>
</v-flex>
@ -133,11 +133,11 @@
data() {
const cameras = [{
ID: 0,
CameraModel: this.$gettext('All Cameras')
Model: this.$gettext('All Cameras')
}].concat(this.$config.get('cameras'));
const countries = [{
code: '',
name: this.$gettext('All Countries')
ID: '',
Name: this.$gettext('All Countries')
}].concat(this.$config.get('countries'));
return {

View file

@ -0,0 +1,121 @@
<template>
<div>
<v-container fluid class="pa-0" v-if="selection.length > 0">
<v-speed-dial
fixed
bottom
right
direction="top"
v-model="expanded"
transition="slide-y-reverse-transition"
class="p-clipboard p-folder-clipboard"
id="t-clipboard"
>
<v-btn
slot="activator"
color="accent darken-2"
dark
fab
class="p-folder-clipboard-menu"
>
<v-icon v-if="selection.length === 0">menu</v-icon>
<span v-else class="t-clipboard-count">{{ selection.length }}</span>
</v-btn>
<!-- v-btn
fab
dark
small
:title="labels.download"
color="download"
@click.stop="download()"
class="p-label-clipboard-download"
:disabled="selection.length !== 1"
>
<v-icon>cloud_download</v-icon>
</v-btn -->
<v-btn
fab
dark
small
:title="labels.addToAlbum"
color="album"
:disabled="selection.length === 0"
@click.stop="dialog.album = true"
class="p-photo-clipboard-album"
>
<v-icon>folder</v-icon>
</v-btn>
<v-btn
fab
dark
small
color="accent"
@click.stop="clearClipboard()"
class="p-folder-clipboard-clear"
>
<v-icon>clear</v-icon>
</v-btn>
</v-speed-dial>
</v-container>
<p-photo-album-dialog :show="dialog.album" @cancel="dialog.album = false"
@confirm="addToAlbum"></p-photo-album-dialog>
</div>
</template>
<script>
import Api from "common/api";
import Notify from "common/notify";
export default {
name: 'p-folder-clipboard',
props: {
selection: Array,
refresh: Function,
clearSelection: Function,
},
data() {
return {
expanded: false,
dialog: {
album: false,
edit: false,
},
labels: {
download: this.$gettext("Download"),
addToAlbum: this.$gettext("Add to album"),
removeFromAlbum: this.$gettext("Remove"),
},
};
},
methods: {
clearClipboard() {
this.clearSelection();
this.expanded = false;
},
addToAlbum(ppid) {
this.dialog.album = false;
Api.post(`albums/${ppid}/photos`, {"folders": this.selection}).then(() => this.onAdded());
},
onAdded() {
this.clearClipboard();
},
download() {
if(this.selection.length !== 1) {
Notify.error(this.$gettext("You can only download one folder"));
return;
}
this.onDownload(`/api/v1/folders/${this.selection[0]}/download`);
this.expanded = false;
},
onDownload(path) {
Notify.success(this.$gettext("Downloading..."));
window.open(path, "_blank");
},
}
};
</script>

View file

@ -110,10 +110,10 @@
this.clearSelection();
this.expanded = false;
},
addToAlbum(albumUUID) {
addToAlbum(ppid) {
this.dialog.album = false;
Api.post(`albums/${albumUUID}/photos`, {"labels": this.selection}).then(() => this.onAdded());
Api.post(`albums/${ppid}/photos`, {"labels": this.selection}).then(() => this.onAdded());
},
onAdded() {
this.clearClipboard();

View file

@ -55,7 +55,7 @@
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>{{ $gettext('Photos') }}</v-list-tile-title>
<v-list-tile-title><translate key="Photos">Photos</translate></v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
@ -63,7 +63,7 @@
<v-list-tile slot="activator" to="/photos" @click.stop="" class="p-navigation-photos">
<v-list-tile-content>
<v-list-tile-title>
<span>{{ $gettext('Photos') }}</span>
<translate key="Photos">Photos</translate>
<span v-if="config.count.photos > 0" class="p-navigation-count">{{ config.count.photos }}</span>
</v-list-tile-title>
</v-list-tile-content>
@ -71,19 +71,22 @@
<v-list-tile :to="{name: 'photos', query: { q: 'mono:true quality:3 photo:true' }}" :exact="true" @click="">
<v-list-tile-content>
<v-list-tile-title>{{ $gettext('Monochrome') }}</v-list-tile-title>
<v-list-tile-title><translate key="Monochrome">Monochrome</translate></v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile to="/review" @click="" v-if="$config.feature('review')">
<v-list-tile to="/review" @click="" v-if="$config.feature('review') && config.count.review > 0">
<v-list-tile-content>
<v-list-tile-title>{{ $gettext('Review') }}</v-list-tile-title>
<v-list-tile-title>
<translate key="Review">Review</translate>
<span v-show="config.count.review > 0" class="p-navigation-count">{{ config.count.review }}</span>
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile to="/archive" @click="" class="p-navigation-archive" v-show="$config.feature('archive')">
<v-list-tile-content>
<v-list-tile-title>{{ $gettext('Archive') }}</v-list-tile-title>
<v-list-tile-title><translate key="Archive">Archive</translate></v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list-group>
@ -95,7 +98,7 @@
<v-list-tile-content>
<v-list-tile-title>
<span>{{ $gettext('Favorites') }}</span>
<translate key="Favorites">Favorites</translate>
<span v-show="config.count.favorites > 0" class="p-navigation-count">{{ config.count.favorites }}</span>
</v-list-tile-title>
</v-list-tile-content>
@ -108,7 +111,7 @@
<v-list-tile-content>
<v-list-tile-title>
<span>{{ $gettext('Private') }}</span>
<translate key="Private">Private</translate>
<span v-show="config.count.private > 0" class="p-navigation-count">{{ config.count.private }}</span>
</v-list-tile-title>
</v-list-tile-content>
@ -121,7 +124,7 @@
<v-list-tile-content>
<v-list-tile-title>
<span>{{ $gettext('Videos') }}</span>
<translate key="Videos">Videos</translate>
<span v-show="config.count.videos > 0" class="p-navigation-count">{{ config.count.videos }}</span>
</v-list-tile-title>
</v-list-tile-content>
@ -134,45 +137,65 @@
<v-list-tile-content>
<v-list-tile-title>
<span>{{ $gettext('Albums') }}</span>
<translate key="Albums">Albums</translate>
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-group v-if="!mini" prepend-icon="folder" no-action :append-icon="albumExpandIcon">
<v-list-group v-if="!mini" prepend-icon="folder" no-action>
<v-list-tile slot="activator" to="/albums" @click.stop="">
<v-list-tile-content>
<v-list-tile-title>
<span>{{ $gettext('Albums') }}</span>
<translate key="Albums">Albums</translate>
<span v-if="config.count.albums > 0" class="p-navigation-count">{{ config.count.albums }}</span>
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile to="/folders" @click="" class="p-navigation-folders" v-show="$config.feature('folders')">
<v-list-tile-content>
<v-list-tile-title>
<translate key="Folders">Folders</translate>
<span v-show="config.count.folders > 0" class="p-navigation-count">{{ config.count.folders }}</span>
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile v-for="(album, index) in config.albums"
:key="index"
:to="{ name: 'album', params: { uuid: album.AlbumUUID, slug: album.AlbumSlug } }">
:to="{ name: 'album', params: { uid: album.UID, slug: album.Slug } }">
<v-list-tile-content>
<v-list-tile-title v-if="album.AlbumName">{{ album.AlbumName }}</v-list-tile-title>
<v-list-tile-title v-if="album.Name">{{ album.Name }}</v-list-tile-title>
<v-list-tile-title v-else>Untitled</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list-group>
<v-list-tile to="/labels" @click="" class="p-navigation-labels" v-show="$config.feature('labels')">
<v-list-tile :to="{ name: 'moments' }" @click="" class="p-navigation-moments"
v-show="config.experimental && $config.feature('moments')">
<v-list-tile-action>
<v-icon>label</v-icon>
<v-icon>star</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>
<span>{{ $gettext('Labels') }}</span>
<span v-show="config.count.labels > 0"
class="p-navigation-count">{{ config.count.labels }}</span>
<translate key="Moments">Moments</translate>
<span v-show="config.count.moments > 0"
class="p-navigation-count">{{ config.count.moments }}</span>
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<!-- v-list-tile to="/events" @click="" class="p-navigation-events">
<v-list-tile-action>
<v-icon>date_range</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>Events</v-list-tile-title>
</v-list-tile-content>
</v-list-tile -->
<v-list-tile :to="{ name: 'places' }" @click="" class="p-navigation-places"
v-show="$config.feature('places')">
<v-list-tile-action>
@ -181,7 +204,7 @@
<v-list-tile-content>
<v-list-tile-title>
<span>{{ $gettext('Places') }}</span>
<translate key="Places">Places</translate>
<span v-show="config.count.places > 0"
class="p-navigation-count">{{ config.count.places }}</span>
</v-list-tile-title>
@ -220,6 +243,34 @@
</v-list-tile-content>
</v-list-tile -->
<!-- v-list-tile to="/folders" @click="" class="p-navigation-folders">
<v-list-tile-action>
<v-icon>sd_storage</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>
<translate key="Folders">Folders</translate>
<span v-show="config.count.folders > 0" class="p-navigation-count">{{ config.count.folders }}</span>
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile -->
<v-list-tile to="/labels" @click="" class="p-navigation-labels" v-show="$config.feature('labels')">
<v-list-tile-action>
<v-icon>label</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>
<translate key="Labels">Labels</translate>
<span v-show="config.count.labels > 0"
class="p-navigation-count">{{ config.count.labels }}</span>
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile to="/library" @click="" class="p-navigation-library">
<v-list-tile-action>
<v-icon>camera_roll</v-icon>
@ -227,7 +278,7 @@
<v-list-tile-content>
<v-list-tile-title>
<span>{{ $gettext('Library') }}</span>
<translate key="Originals">Originals</translate>
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
@ -239,7 +290,7 @@
<v-list-tile-content>
<v-list-tile-title>
<span>{{ $gettext('Settings') }}</span>
<translate key="Settings">Settings</translate>
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
@ -251,7 +302,7 @@
<v-list-tile-content>
<v-list-tile-title>
<span>{{ $gettext('Logout') }}</span>
<translate key="Logout">Logout</translate>
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
@ -263,7 +314,7 @@
<v-list-tile-content>
<v-list-tile-title>
<span>{{ $gettext('Login') }}</span>
<translate key="Login">Login</translate>
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
@ -330,7 +381,7 @@
},
createAlbum() {
let name = "New Album";
const album = new Album({AlbumName: name, AlbumFavorite: true});
const album = new Album({Name: name, Favorite: true});
album.save();
},
logout() {

View file

@ -54,7 +54,7 @@
ma-0
class="p-photo-live"
style="overflow: hidden;"
v-if="photo.PhotoType === 'live'"
v-if="photo.Type === 'live'"
v-show="hover"
>
<video width="500" height="500" autoplay loop muted playsinline>
@ -62,7 +62,7 @@
</video>
</v-layout>
<v-btn v-if="hidePrivate && photo.PhotoPrivate" :ripple="false"
<v-btn v-if="hidePrivate && photo.Private" :ripple="false"
icon flat large absolute
class="p-photo-private opacity-75">
<v-icon color="white">lock</v-icon>
@ -79,14 +79,14 @@
</v-btn>
<v-btn icon flat large absolute :ripple="false"
:class="photo.PhotoFavorite ? 'p-photo-like opacity-75' : 'p-photo-like opacity-50'"
:class="photo.Favorite ? 'p-photo-like opacity-75' : 'p-photo-like opacity-50'"
@click.stop.prevent="photo.toggleLike()">
<v-icon v-if="photo.PhotoFavorite" color="white" class="t-like t-on">favorite</v-icon>
<v-icon v-if="photo.Favorite" color="white" class="t-like t-on">favorite</v-icon>
<v-icon v-else color="accent lighten-3" class="t-like t-off">favorite_border</v-icon>
</v-btn>
<template v-if="photo.isPlayable()">
<v-btn v-if="photo.PhotoType === 'live'" :ripple="false"
<v-btn v-if="photo.Type === 'live'" :ripple="false"
icon flat large absolute class="p-photo-live opacity-75"
@click.stop.prevent="openPhoto(index, true)" title="Live Photo">
<v-icon color="white" class="action-play">adjust</v-icon>
@ -97,12 +97,12 @@
<v-icon color="white" class="action-play">play_arrow</v-icon>
</v-btn>
</template>
<v-btn v-else-if="photo.PhotoType === 'image' && photo.Files.length > 1" :ripple="false"
<v-btn v-else-if="photo.Type === 'image' && photo.Files.length > 1" :ripple="false"
icon flat large absolute class="p-photo-merged opacity-75"
@click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-burst">burst_mode</v-icon>
</v-btn>
<v-btn v-else-if="photo.PhotoType === 'raw'" :ripple="false"
<v-btn v-else-if="photo.Type === 'raw'" :ripple="false"
icon flat large absolute class="p-photo-merged opacity-75"
@click.stop.prevent="openPhoto(index, true)" title="RAW">
<v-icon color="white" class="action-burst">photo_camera</v-icon>
@ -111,22 +111,22 @@
<v-card-title primary-title class="pa-3 p-photo-desc" style="user-select: none;">
<div>
<h3 class="body-2 mb-2" :title="photo.PhotoTitle">
<h3 class="body-2 mb-2" :title="photo.Title">
<button @click.exact="editPhoto(index)">
{{ photo.PhotoTitle | truncate(80) }}
{{ photo.Title | truncate(80) }}
</button>
</h3>
<div class="caption mb-2" v-if="photo.PhotoDescription" title="Description">
{{ photo.PhotoDescription }}
<div class="caption mb-2" v-if="photo.Description" title="Description">
{{ photo.Description }}
</div>
<div class="caption">
<button @click.exact="editPhoto(index)">
<v-icon size="14" title="Taken">date_range</v-icon>
{{ photo.getDateString() }}
</button>
<template v-if="!photo.PhotoDescription">
<template v-if="!photo.Description">
<br/>
<button v-if="photo.PhotoType === 'video'" @click.exact="openPhoto(index, true)"
<button v-if="photo.Type === 'video'" @click.exact="openPhoto(index, true)"
title="Video">
<v-icon size="14">movie</v-icon>
{{ photo.getVideoInfo() }}
@ -187,7 +187,7 @@
downloadFile(index) {
const photo = this.photos[index];
const link = document.createElement('a')
link.href = "/api/v1/download/" + photo.FileHash;
link.href = "/api/v1/download/" + photo.Hash;
link.download = photo.FileName;
link.click()
},

View file

@ -209,10 +209,10 @@
Notify.success(this.$gettext("Photos restored"));
this.clearClipboard();
},
addToAlbum(albumUUID) {
addToAlbum(ppid) {
this.dialog.album = false;
Api.post(`albums/${albumUUID}/photos`, {"photos": this.selection}).then(() => this.onAdded());
Api.post(`albums/${ppid}/photos`, {"photos": this.selection}).then(() => this.onAdded());
},
onAdded() {
this.clearClipboard();
@ -223,11 +223,11 @@
return
}
const albumUUID = this.album.AlbumUUID;
const uid = this.album.UID;
this.dialog.album = false;
Api.delete(`albums/${albumUUID}/photos`, {"data": {"photos": this.selection}}).then(() => this.onRemoved());
Api.delete(`albums/${uid}/photos`, {"data": {"photos": this.selection}}).then(() => this.onRemoved());
},
onRemoved() {
this.clearClipboard();

View file

@ -32,7 +32,7 @@
flat icon large absolute class="p-photo-select">
<v-icon color="white" class="t-select t-on">check_circle</v-icon>
</v-btn>
<v-btn v-else-if="!selection.length && props.item.PhotoType === 'video' && props.item.isPlayable()" :ripple="false"
<v-btn v-else-if="!selection.length && props.item.Type === 'video' && props.item.isPlayable()" :ripple="false"
flat icon large absolute class="p-photo-play opacity-75"
@click.stop.prevent="openPhoto(props.index, true)">
<v-icon color="white" class="action-play">play_arrow</v-icon>
@ -40,7 +40,7 @@
</v-img>
</td>
<td class="p-photo-desc p-pointer" @click.exact="editPhoto(props.index)" style="user-select: none;">
{{ props.item.PhotoTitle }}
{{ props.item.Title }}
</td>
<td class="p-photo-desc hidden-xs-only" :title="props.item.getDateString()">
<button @click.stop.prevent="editPhoto(props.index)" style="user-select: none;">
@ -68,12 +68,12 @@
<td class="text-xs-center">
<v-btn v-if="hidePrivate" class="p-photo-private" icon small flat :ripple="false"
@click.stop.prevent="props.item.togglePrivate()">
<v-icon v-if="props.item.PhotoPrivate" color="secondary-dark">lock</v-icon>
<v-icon v-if="props.item.Private" color="secondary-dark">lock</v-icon>
<v-icon v-else color="accent lighten-3">lock_open</v-icon>
</v-btn>
<v-btn class="p-photo-like" icon small flat :ripple="false"
@click.stop.prevent="props.item.toggleLike()">
<v-icon v-if="props.item.PhotoFavorite" color="pink lighten-3">favorite</v-icon>
<v-icon v-if="props.item.Favorite" color="pink lighten-3">favorite</v-icon>
<v-icon v-else color="accent lighten-3">favorite_border</v-icon>
</v-btn>
</td>
@ -106,7 +106,7 @@
'selected': [],
'listColumns': [
{text: '', value: '', align: 'center', sortable: false, class: 'p-col-select'},
{text: this.$gettext('Title'), value: 'PhotoTitle'},
{text: this.$gettext('Title'), value: 'Title'},
{text: this.$gettext('Taken'), class: 'hidden-xs-only', value: 'TakenAt'},
{text: this.$gettext('Camera'), class: 'hidden-sm-and-down', value: 'CameraModel'},
{text: showName ? this.$gettext('Name') : this.$gettext('Location'), class: 'hidden-xs-only', value: showName ? 'FileName' : 'LocLabel'},
@ -139,7 +139,7 @@
downloadFile(index) {
const photo = this.photos[index];
const link = document.createElement('a')
link.href = "/api/v1/download/" + photo.FileHash;
link.href = "/api/v1/download/" + photo.Hash;
link.download = photo.FileName;
link.click()
},
@ -166,7 +166,7 @@
} else if(this.photos[index]) {
let photo = this.photos[index];
if(photo.PhotoType === 'video' && photo.isPlayable()) {
if(photo.Type === 'video' && photo.isPlayable()) {
this.openPhoto(index, true);
} else {
this.openPhoto(index, false);

View file

@ -27,7 +27,7 @@
<v-card tile slot-scope="{ hover }"
@contextmenu="onContextMenu($event, index)"
:class="$clipboard.has(photo) ? 'elevation-10 ma-0' : 'elevation-0 ma-1'"
:title="photo.PhotoTitle">
:title="photo.Title">
<v-img :src="photo.thumbnailUrl('tile_224')"
aspect-ratio="1"
class="accent lighten-2"
@ -53,7 +53,7 @@
ma-0
class="p-photo-live"
style="overflow: hidden;"
v-if="photo.PhotoType === 'live'"
v-if="photo.Type === 'live'"
v-show="hover"
>
<video width="224" height="224" autoplay loop muted playsinline>
@ -61,7 +61,7 @@
</video>
</v-layout>
<v-btn v-if="hidePrivate && photo.PhotoPrivate" :ripple="false"
<v-btn v-if="hidePrivate && photo.Private" :ripple="false"
icon flat small absolute
class="p-photo-private opacity-75">
<v-icon color="white">lock</v-icon>
@ -78,14 +78,14 @@
</v-btn>
<v-btn icon flat small absolute :ripple="false"
:class="photo.PhotoFavorite ? 'p-photo-like opacity-75' : 'p-photo-like opacity-50'"
:class="photo.Favorite ? 'p-photo-like opacity-75' : 'p-photo-like opacity-50'"
@click.stop.prevent="photo.toggleLike()">
<v-icon v-if="photo.PhotoFavorite" color="white" class="t-like t-on">favorite</v-icon>
<v-icon v-if="photo.Favorite" color="white" class="t-like t-on">favorite</v-icon>
<v-icon v-else color="accent lighten-3" class="t-like t-off">favorite_border</v-icon>
</v-btn>
<template v-if="photo.isPlayable()">
<v-btn v-if="photo.PhotoType === 'live'" color="white"
<v-btn v-if="photo.Type === 'live'" color="white"
icon flat small absolute class="p-photo-live opacity-75" :depressed="false" :ripple="false"
@click.stop.prevent="openPhoto(index, true)" title="Live Photo">
<v-icon color="white" class="action-play">adjust</v-icon>
@ -96,12 +96,12 @@
<v-icon color="white" class="action-play">play_arrow</v-icon>
</v-btn>
</template>
<v-btn v-else-if="photo.PhotoType === 'image' && photo.Files.length > 1" :ripple="false"
<v-btn v-else-if="photo.Type === 'image' && photo.Files.length > 1" :ripple="false"
icon flat small absolute class="p-photo-merged opacity-75"
@click.stop.prevent="openPhoto(index, true)">
<v-icon color="white" class="action-burst">burst_mode</v-icon>
</v-btn>
<v-btn v-else-if="photo.PhotoType === 'raw'" :ripple="false"
<v-btn v-else-if="photo.Type === 'raw'" :ripple="false"
icon flat small absolute class="p-photo-merged opacity-75"
@click.stop.prevent="openPhoto(index, true)" title="RAW">
<v-icon color="white" class="action-burst">photo_camera</v-icon>

View file

@ -52,8 +52,8 @@
:label="labels.country"
flat solo hide-details
color="secondary-dark"
item-value="code"
item-text="name"
item-value="ID"
item-text="Name"
v-model="filter.country"
:items="countryOptions">
</v-select>
@ -63,8 +63,8 @@
:label="labels.year"
flat solo hide-details
color="secondary-dark"
item-value="year"
item-text="label"
item-value="Year"
item-text="Name"
v-model="filter.year"
:items="yearOptions">
</v-select>
@ -94,7 +94,7 @@
flat solo hide-details
color="secondary-dark"
item-value="ID"
item-text="CameraModel"
item-text="Model"
v-model="filter.camera"
:items="cameraOptions">
</v-select>
@ -105,7 +105,7 @@
flat solo hide-details
color="secondary-dark"
item-value="ID"
item-text="LensModel"
item-text="Model"
v-model="filter.lens"
:items="lensOptions">
</v-select>
@ -115,8 +115,8 @@
:label="labels.color"
flat solo hide-details
color="secondary-dark"
item-value="name"
item-text="label"
item-value="Slug"
item-text="Name"
v-model="filter.color"
:items="colorOptions">
</v-select>
@ -126,8 +126,8 @@
:label="labels.category"
flat solo hide-details
color="secondary-dark"
item-value="LabelName"
item-text="Title"
item-value="Slug"
item-text="Name"
v-model="filter.label"
:items="categoryOptions">
</v-select>
@ -154,11 +154,11 @@
config: this.$config.values,
searchExpanded: false,
all: {
countries: [{code: "", name: this.$gettext("All Countries")}],
cameras: [{ID: 0, CameraModel: this.$gettext("All Cameras")}],
lenses: [{ID: 0, LensModel: this.$gettext("All Lenses")}],
colors: [{label: this.$gettext("All Colors"), name: ""}],
categories: [{LabelName: "", Title: this.$gettext("All Categories")}],
countries: [{ID: "", Name: this.$gettext("All Countries")}],
cameras: [{ID: 0, Model: this.$gettext("All Cameras")}],
lenses: [{ID: 0, Model: this.$gettext("All Lenses")}],
colors: [{Slug: "", Name: this.$gettext("All Colors")}],
categories: [{Slug: "", Name: this.$gettext("All Categories")}],
},
options: {
'views': [
@ -208,16 +208,16 @@
},
yearOptions() {
let result = [
{"year": 0, "label": this.$gettext("All Years")},
{"Year": 0, "Name": this.$gettext("All Years")},
];
if (this.config.years) {
for (let i = 0; i < this.config.years.length; i++) {
result.push({"year": this.config.years[i], "label": this.config.years[i].toString()});
result.push({"Year": this.config.years[i], "Name": this.config.years[i].toString()});
}
}
result.push({"year": -1, "label": this.$gettext("Unknown")});
result.push({"Year": -1, "Name": this.$gettext("Unknown")});
return result;
},

View file

@ -103,7 +103,7 @@
playVideo() {
if(this.item && this.item.playable) {
let photo = new Photo();
photo.find(this.item.uuid).then((p) => {
photo.find(this.item.uid).then((p) => {
this.$modal.show('video', {video: p, album: null});
});
}
@ -139,15 +139,15 @@
// remove duplicates
let filtered = g.items.filter(function (p, i, s) {
return !(i > 0 && p.uuid === s[i - 1].uuid);
return !(i > 0 && p.uid === s[i - 1].uid);
});
let selection = filtered.map((p, i) => {
if (g.currItem.uuid === p.uuid) {
if (g.currItem.uid === p.uid) {
index = i;
}
return p.uuid
return p.uid
});
let album = null;

View file

@ -7,6 +7,7 @@
@import url("viewer.css");
@import url("photos.css");
@import url("labels.css");
@import url("folders.css");
html,
body {

View file

@ -0,0 +1,4 @@
#photoprism .p-folders-cards .p-folder-like {
left: 4px;
bottom: 4px;
}

View file

@ -66,7 +66,8 @@
}
#photoprism .p-albums-cards .p-album-select,
#photoprism .p-labels-cards .p-label-select {
#photoprism .p-labels-cards .p-label-select,
#photoprism .p-folders-cards .p-folder-select{
right: 4px;
bottom: 4px;
}

View file

@ -88,7 +88,7 @@
this.model.save().then((a) => {
this.loading = false;
this.$emit('confirm', a.AlbumUUID);
this.$emit('confirm', a.UID);
});
},
},

View file

@ -17,8 +17,8 @@
:loading="loading"
hide-details
hide-no-data
item-text="AlbumName"
item-value="AlbumUUID"
item-text="Name"
item-value="UID"
:label="labels.select"
color="secondary-dark"
flat solo
@ -73,7 +73,7 @@
this.newAlbum.save().then((a) => {
this.loading = false;
this.$emit('confirm', a.AlbumUUID);
this.$emit('confirm', a.UID);
});
} else {
this.$emit('confirm', this.album);
@ -96,7 +96,7 @@
this.loading = false;
if(response.models.length > 0 && !this.album) {
this.album = response.models[0].AlbumUUID;
this.album = response.models[0].UID;
}
this.albums = response.models;
@ -106,13 +106,13 @@
},
watch: {
search (q) {
const exists = this.albums.findIndex((album) => album.AlbumName === q);
const exists = this.albums.findIndex((album) => album.Name === q);
if (exists !== -1 || !q) {
this.items = this.albums;
this.newAlbum = null;
} else {
this.newAlbum = new Album({AlbumName: q, AlbumUUID: "", AlbumFavorite: true});
this.newAlbum = new Album({Name: q, UID: "", Favorite: true});
this.items = this.albums.concat([this.newAlbum]);
}
},

View file

@ -80,15 +80,15 @@
},
computed: {
title: function () {
if (this.model && this.model.PhotoTitle) {
return this.model.PhotoTitle
if (this.model && this.model.Title) {
return this.model.Title
}
this.$gettext("Edit Photo");
},
isPrivate: function () {
if (this.model && this.model.PhotoPrivate && this.$config.settings().features.private) {
return this.model.PhotoPrivate
if (this.model && this.model.Private && this.$config.settings().features.private) {
return this.model.Private
}
return false;

View file

@ -51,18 +51,18 @@
let width = 0;
let height = 0;
if (file.FileWidth > 0) {
width = file.FileWidth;
} else if (main && main.FileWidth > 0) {
width = main.FileWidth;
if (file.Width > 0) {
width = file.Width;
} else if (main && main.Width > 0) {
width = main.Width;
} else {
width = this.defaultWidth;
}
if (file.FileHeight > 0) {
height = file.FileHeight;
} else if (main && main.FileHeight > 0) {
height = main.FileHeight;
if (file.Height > 0) {
height = file.Height;
} else if (main && main.Height > 0) {
height = main.Height;
} else {
height = this.defaultHeight;
}

View file

@ -11,7 +11,7 @@
>
<v-card tile
class="ma-1 elevation-0"
:title="model.PhotoTitle">
:title="model.Title">
<v-img :src="model.thumbnailUrl('tile_500')"
aspect-ratio="1"
class="accent lighten-2 elevation-0"
@ -44,7 +44,7 @@
placeholder=""
color="secondary-dark"
browser-autocomplete="off"
v-model="model.PhotoTitle"
v-model="model.Title"
></v-text-field>
</v-flex>
@ -123,8 +123,8 @@
:label="labels.timezone"
hide-details
color="secondary-dark"
item-value="code"
item-text="name"
item-value="ID"
item-text="Name"
v-model="model.TimeZone"
:items="timeZones">
</v-autocomplete>
@ -138,7 +138,7 @@
label="Latitude"
placeholder=""
color="secondary-dark"
v-model="model.PhotoLat"
v-model="model.Lat"
></v-text-field>
</v-flex>
@ -150,7 +150,7 @@
label="Longitude"
placeholder=""
color="secondary-dark"
v-model="model.PhotoLng"
v-model="model.Lng"
></v-text-field>
</v-flex>
@ -162,7 +162,7 @@
label="Altitude (m)"
placeholder=""
color="secondary-dark"
v-model="model.PhotoAltitude"
v-model="model.Altitude"
></v-text-field>
</v-flex>
@ -175,7 +175,7 @@
color="secondary-dark"
item-value="Code"
item-text="Name"
v-model="model.PhotoCountry"
v-model="model.Country"
:items="countries">
</v-select>
</v-flex>
@ -188,7 +188,7 @@
hide-details
color="secondary-dark"
item-value="ID"
item-text="CameraModel"
item-text="Model"
v-model="model.CameraID"
:items="cameraOptions">
</v-select>
@ -202,7 +202,7 @@
label="ISO"
placeholder=""
color="secondary-dark"
v-model="model.PhotoIso"
v-model="model.Iso"
></v-text-field>
</v-flex>
@ -214,7 +214,7 @@
label="Exposure"
placeholder=""
color="secondary-dark"
v-model="model.PhotoExposure"
v-model="model.Exposure"
></v-text-field>
</v-flex>
@ -226,7 +226,7 @@
hide-details
color="secondary-dark"
item-value="ID"
item-text="LensModel"
item-text="Model"
v-model="model.LensID"
:items="lensOptions">
</v-select>
@ -240,7 +240,7 @@
label="F Number"
placeholder=""
color="secondary-dark"
v-model="model.PhotoFNumber"
v-model="model.FNumber"
></v-text-field>
</v-flex>
@ -252,7 +252,7 @@
label="Focal Length"
placeholder=""
color="secondary-dark"
v-model="model.PhotoFocalLength"
v-model="model.FocalLength"
></v-text-field>
</v-flex>
@ -322,7 +322,7 @@
placeholder=""
:rows="1"
color="secondary-dark"
v-model="model.PhotoDescription"
v-model="model.Description"
></v-textarea>
</v-flex>
@ -361,7 +361,7 @@
</v-btn>
<v-btn color="secondary-dark" depressed dark @click.stop="save(false)"
class="p-photo-dialog-confirm">
<span v-if="$config.feature('review') && model.PhotoQuality < 3">Approve</span>
<span v-if="$config.feature('review') && model.Quality < 3">Approve</span>
<span v-else>Apply</span>
</v-btn>
<v-btn color="secondary-dark" depressed dark @click.stop="save(true)"

View file

@ -12,19 +12,19 @@
>
<template slot="items" slot-scope="props" class="p-file">
<td>
<v-btn v-if="props.item.FileType === 'jpg'" flat :ripple="false" icon small
<v-btn v-if="props.item.Type === 'jpg'" flat :ripple="false" icon small
@click.stop.prevent="setPrimary(props.item)">
<v-icon v-if="props.item.FilePrimary" color="secondary-dark">radio_button_checked</v-icon>
<v-icon v-if="props.item.Primary" color="secondary-dark">radio_button_checked</v-icon>
<v-icon v-else color="secondary-dark">radio_button_unchecked</v-icon>
</v-btn>
</td>
<td>
<a :href="'/api/v1/download/' + props.item.FileHash" class="secondary-dark--text" target="_blank"
<a :href="'/api/v1/download/' + props.item.Hash" class="secondary-dark--text" target="_blank"
v-if="$config.feature('download')">
{{ props.item.FileName }}
{{ props.item.Name }}
</a>
<span v-else>
{{ props.item.FileName }}
{{ props.item.Name }}
</span>
</td>
<td class="hidden-sm-and-down">{{ fileDimensions(props.item) }}</td>
@ -50,10 +50,10 @@
readonly: this.$config.get("readonly"),
selected: [],
listColumns: [
{text: this.$gettext('Primary'), value: 'FilePrimary', sortable: false, align: 'center', class: 'p-col-primary'},
{text: this.$gettext('Name'), value: 'FileName', sortable: false, align: 'left'},
{text: this.$gettext('Primary'), value: 'Primary', sortable: false, align: 'center', class: 'p-col-primary'},
{text: this.$gettext('Name'), value: 'Name', sortable: false, align: 'left'},
{text: this.$gettext('Dimensions'), value: '', sortable: false, class: 'hidden-sm-and-down'},
{text: this.$gettext('Size'), value: 'FileSize', sortable: false, class: 'hidden-xs-only'},
{text: this.$gettext('Size'), value: 'Size', sortable: false, class: 'hidden-xs-only'},
{text: this.$gettext('Type'), value: '', sortable: false, align: 'left'},
{text: this.$gettext('Status'), value: '', sortable: false, align: 'left'},
],
@ -65,38 +65,38 @@
this.$viewer.show(Thumb.fromFiles([this.model]), 0)
},
setPrimary(file) {
this.model.setPrimary(file.FileUUID);
this.model.setPrimary(file.UID);
},
fileDimensions(file) {
if (!file.FileWidth || !file.FileHeight) {
if (!file.Width || !file.Height) {
return "";
}
return file.FileWidth + " × " + file.FileHeight;
return file.Width + " × " + file.Height;
},
fileSize(file) {
if (!file.FileSize) {
if (!file.Size) {
return "";
}
const size = Number.parseFloat(file.FileSize) / 1048576;
const size = Number.parseFloat(file.Size) / 1048576;
return size.toFixed(1) + " MB";
},
fileType(file) {
if (file.FileVideo) {
if (file.Video) {
return this.$gettext("Video");
} else if (file.FileSidecar) {
} else if (file.Sidecar) {
return this.$gettext("Sidecar");
}
return file.FileType.toUpperCase();
return file.Type.toUpperCase();
},
fileStatus(file) {
if (file.FileMissing) {
if (file.Missing) {
return this.$gettext("Missing");
} else if (file.FileError) {
return file.FileError;
} else if (file.Error) {
return file.Error;
} else if (file.Duplicate) {
return this.$gettext("Duplicate");
}

View file

@ -13,15 +13,15 @@
<template v-slot:items="props" class="p-file">
<td>
<v-edit-dialog
:return-value.sync="props.item.Label.LabelName"
:return-value.sync="props.item.Label.Name"
lazy
@save="renameLabel(props.item.Label)"
class="p-inline-edit"
>
{{ props.item.Label.LabelName | capitalize }}
{{ props.item.Label.Name | capitalize }}
<template v-slot:input>
<v-text-field
v-model="props.item.Label.LabelName"
v-model="props.item.Label.Name"
:rules="[nameRule]"
:label="labels.name"
color="secondary-dark"
@ -123,7 +123,7 @@
return
}
const name = label.LabelName;
const name = label.Name;
this.model.removeLabel(label.ID).then((m) => {
this.$notify.success("removed " + name);
@ -152,10 +152,10 @@
return
}
this.model.renameLabel(label.ID, label.LabelName);
this.model.renameLabel(label.ID, label.Name);
},
searchLabel(label) {
this.$router.push({name: 'photos', query: {q: 'label:' + label.LabelSlug}}).catch(err => {
this.$router.push({name: 'photos', query: {q: 'label:' + label.Slug}}).catch(err => {
});
this.$emit('close');
},

View file

@ -46,8 +46,8 @@ export class Account extends RestModel {
return Api.get(this.getEntityResource() + "/dirs").then((response) => Promise.resolve(response.data));
}
Share(UUIDs, dest) {
const values = {Photos: UUIDs, Destination: dest};
Share(photos, dest) {
const values = {Photos: photos, Destination: dest};
return Api.post(this.getEntityResource() + "/share", values).then((response) => Promise.resolve(response.data));
}

View file

@ -5,16 +5,19 @@ import {DateTime} from "luxon";
export class Album extends RestModel {
getDefaults() {
return {
ID: 0,
CoverUUID: "",
AlbumUUID: "",
AlbumSlug: "",
AlbumName: "",
AlbumDescription: "",
AlbumNotes: "",
AlbumOrder: "",
AlbumTemplate: "",
AlbumFavorite: true,
UID: "",
Cover: "",
Parent: "",
Folder: "",
Slug: "",
Type: "",
Name: "",
Description: "",
Notes: "",
Order: "",
Filter: "",
Template: "",
Favorite: true,
Links: [],
CreatedAt: "",
UpdatedAt: "",
@ -22,15 +25,15 @@ export class Album extends RestModel {
}
getEntityName() {
return this.AlbumSlug;
return this.Slug;
}
getId() {
return this.AlbumUUID;
return this.UID;
}
getTitle() {
return this.AlbumName;
return this.Name;
}
thumbnailUrl(type) {
@ -66,9 +69,9 @@ export class Album extends RestModel {
}
toggleLike() {
this.AlbumFavorite = !this.AlbumFavorite;
this.Favorite = !this.Favorite;
if (this.AlbumFavorite) {
if (this.Favorite) {
return Api.post(this.getEntityResource() + "/like");
} else {
return Api.delete(this.getEntityResource() + "/like");
@ -76,12 +79,12 @@ export class Album extends RestModel {
}
like() {
this.AlbumFavorite = true;
this.Favorite = true;
return Api.post(this.getEntityResource() + "/like");
}
unlike() {
this.AlbumFavorite = false;
this.Favorite = false;
return Api.delete(this.getEntityResource() + "/like");
}

View file

@ -2,15 +2,15 @@ import RestModel from "model/rest";
import Api from "common/api";
import {DateTime} from "luxon";
export const FolderRootOriginals = "originals"
export const FolderRootImport = "import"
export const FolderRootOriginals = "originals";
export const FolderRootImport = "import";
export class Folder extends RestModel {
getDefaults() {
return {
Root: "",
Path: "",
PPID: "",
UID: "",
Title: "",
Description: "",
Type: "",
@ -30,11 +30,11 @@ export class Folder extends RestModel {
}
getId() {
return this.PPID;
return this.UID;
}
thumbnailUrl(type) {
return "/api/v1/folder/" + this.getId() + "/thumbnail/" + type;
thumbnailUrl() {
return "/api/v1/svg/folder";
}
getDateString() {
@ -62,7 +62,11 @@ export class Folder extends RestModel {
}
static findAll(path) {
return this.search(path, {recursive: true})
return this.search(path, {recursive: true});
}
static originals(path, params) {
return this.search(FolderRootOriginals + "/" + path, params);
}
static search(path, params) {
@ -71,7 +75,7 @@ export class Folder extends RestModel {
};
if (!path || path[0] !== "/") {
path = "/" + path
path = "/" + path;
}
return Api.get(this.getCollectionResource() + path, options).then((response) => {

View file

@ -6,32 +6,32 @@ export class Label extends RestModel {
getDefaults() {
return {
ID: 0,
UID: "",
Slug: "",
CustomSlug: "",
Name: "",
Priority: 0,
Favorite: false,
Description: "",
Notes: "",
PhotoCount: 0,
Links: [],
CreatedAt: "",
UpdatedAt: "",
DeletedAt: "",
LabelUUID: "",
LabelSlug: "",
CustomSlug: "",
LabelName: "",
LabelPriority: 0,
LabelFavorite: false,
LabelDescription: "",
LabelNotes: "",
PhotoCount: 0,
Links: [],
};
}
getEntityName() {
return this.LabelSlug;
return this.Slug;
}
getId() {
return this.LabelUUID;
return this.UID;
}
getTitle() {
return this.LabelName;
return this.Name;
}
thumbnailUrl(type) {
@ -67,9 +67,9 @@ export class Label extends RestModel {
}
toggleLike() {
this.LabelFavorite = !this.LabelFavorite;
this.Favorite = !this.Favorite;
if (this.LabelFavorite) {
if (this.Favorite) {
return Api.post(this.getEntityResource() + "/like");
} else {
return Api.delete(this.getEntityResource() + "/like");
@ -77,27 +77,15 @@ export class Label extends RestModel {
}
like() {
this.LabelFavorite = true;
this.Favorite = true;
return Api.post(this.getEntityResource() + "/like");
}
unlike() {
this.LabelFavorite = false;
this.Favorite = false;
return Api.delete(this.getEntityResource() + "/like");
}
/* popularity(max) {
if (!this.PhotoCount) {
return 0;
}
if (this.PhotoCount >= max) {
return 100;
}
return Math.ceil(max / this.PhotoCount);
} */
static getCollectionResource() {
return "labels";
}

View file

@ -3,10 +3,10 @@ import RestModel from "model/rest";
export class Link extends RestModel {
getDefaults() {
return {
LinkToken: "",
LinkPassword: "",
LinkExpires: "",
ShareUUID: "",
Token: "",
Password: "",
Expires: "",
ShareUID: "",
CanComment: false,
CanEdit: false,
CreatedAt: "",
@ -16,7 +16,7 @@ export class Link extends RestModel {
}
getId() {
return this.LinkToken;
return this.Token;
}
static getCollectionResource() {

View file

@ -7,38 +7,38 @@ export const SrcManual = "manual";
export const CodecAvc1 = "avc1";
export const TypeMP4 = "mp4";
export const TypeJpeg = "jpg";
export const TypeImage = "image";
export const YearUnknown = -1;
export const MonthUnknown = -1;
export class Photo extends RestModel {
getDefaults() {
return {
ID: 0,
PhotoUUID: "",
PhotoType: "",
PhotoFavorite: false,
PhotoPrivate: false,
UID: "",
Type: TypeImage,
Favorite: false,
Private: false,
TakenAt: "",
TakenAtLocal: "",
TakenSrc: "",
TimeZone: "",
PhotoPath: "",
PhotoName: "",
FileName: "",
PhotoTitle: "",
Path: "",
Color: "",
Name: "",
Title: "",
TitleSrc: "",
PhotoDescription: "",
Description: "",
DescriptionSrc: "",
PhotoResolution: 0,
PhotoQuality: 0,
PhotoLat: 0.0,
PhotoLng: 0.0,
PhotoAltitude: 0,
PhotoIso: 0,
PhotoFocalLength: 0,
PhotoFNumber: 0.0,
PhotoExposure: "",
PhotoViews: 0,
Resolution: 0,
Quality: 0,
Lat: 0.0,
Lng: 0.0,
Altitude: 0,
Iso: 0,
FocalLength: 0,
FNumber: 0.0,
Exposure: "",
Views: 0,
Camera: {},
CameraID: 0,
CameraSrc: "",
@ -49,9 +49,9 @@ export class Photo extends RestModel {
LocationSrc: "",
Place: null,
PlaceID: "",
PhotoCountry: "",
PhotoYear: YearUnknown,
PhotoMonth: MonthUnknown,
Country: "",
Year: YearUnknown,
Month: MonthUnknown,
Details: {
Keywords: "",
Notes: "",
@ -68,47 +68,45 @@ export class Photo extends RestModel {
CreatedAt: "",
UpdatedAt: "",
DeletedAt: null,
// Additional fields for result lists.
LocLabel: "",
LocCity: "",
LocState: "",
LocCountry: "",
FileUID: "",
FileName: "",
Hash: "",
Width: "",
Height: "",
};
}
getEntityName() {
return this.PhotoTitle;
return this.Title;
}
getId() {
return this.PhotoUUID;
return this.UID;
}
getTitle() {
return this.PhotoTitle;
}
getColor() {
switch (this.PhotoColor) {
case "brown":
case "black":
case "white":
case "grey":
return "grey lighten-2";
default:
return this.PhotoColor + " lighten-4";
}
return this.Title;
}
getGoogleMapsLink() {
return "https://www.google.com/maps/place/" + this.PhotoLat + "," + this.PhotoLng;
return "https://www.google.com/maps/place/" + this.Lat + "," + this.Lng;
}
refreshFileAttr() {
const file = this.mainFile();
if (!file || !file.FileHash) {
if (!file || !file.Hash) {
return;
}
this.FileHash = file.FileHash;
this.FileWidth = file.FileWidth;
this.FileHeight = file.FileHeight;
this.Hash = file.Hash;
this.Width = file.Width;
this.Height = file.Height;
}
isPlayable() {
@ -116,7 +114,7 @@ export class Photo extends RestModel {
return false;
}
return this.Files.findIndex(f => f.FileCodec === CodecAvc1) !== -1 || this.Files.findIndex(f => f.FileType === TypeMP4) !== -1;
return this.Files.findIndex(f => f.Codec === CodecAvc1) !== -1 || this.Files.findIndex(f => f.Type === TypeMP4) !== -1;
}
videoFile() {
@ -124,14 +122,14 @@ export class Photo extends RestModel {
return false;
}
let file = this.Files.find(f => f.FileCodec === CodecAvc1);
let file = this.Files.find(f => f.Codec === CodecAvc1);
if (!file) {
file = this.Files.find(f => f.FileType === TypeMP4);
file = this.Files.find(f => f.Type === TypeMP4);
}
if (!file) {
file = this.Files.find(f => !!f.FileVideo);
file = this.Files.find(f => !!f.Video);
}
return file;
@ -144,7 +142,7 @@ export class Photo extends RestModel {
return "";
}
return "/api/v1/videos/" + file.FileHash + "/" + TypeMP4;
return "/api/v1/videos/" + file.Hash + "/" + TypeMP4;
}
mainFile() {
@ -152,10 +150,10 @@ export class Photo extends RestModel {
return false;
}
let file = this.Files.find(f => !!f.FilePrimary);
let file = this.Files.find(f => !!f.Primary);
if (!file) {
file = this.Files.find(f => f.FileType === TypeJpeg);
file = this.Files.find(f => f.Type === TypeJpeg);
}
return file;
@ -165,11 +163,11 @@ export class Photo extends RestModel {
if (this.Files) {
let file = this.mainFile();
if (file && file.FileHash) {
return file.FileHash;
if (file && file.Hash) {
return file.Hash;
}
} else if (this.FileHash) {
return this.FileHash;
} else if (this.Hash) {
return this.Hash;
}
return "";
@ -181,8 +179,8 @@ export class Photo extends RestModel {
if (!hash) {
let video = this.videoFile();
if (video && video.FileHash) {
return "/api/v1/thumbnails/" + video.FileHash + "/" + type;
if (video && video.Hash) {
return "/api/v1/thumbnails/" + video.Hash + "/" + type;
}
return "/api/v1/svg/photo";
@ -208,11 +206,11 @@ export class Photo extends RestModel {
}
calculateSize(width, height) {
if (width >= this.FileWidth && height >= this.FileHeight) { // Smaller
return {width: this.FileWidth, height: this.FileHeight};
if (width >= this.Width && height >= this.Height) { // Smaller
return {width: this.Width, height: this.Height};
}
const srcAspectRatio = this.FileWidth / this.FileHeight;
const srcAspectRatio = this.Width / this.Height;
const maxAspectRatio = width / height;
let newW, newH;
@ -242,7 +240,7 @@ export class Photo extends RestModel {
}
getDateString() {
if (!this.TakenAt || this.PhotoYear === YearUnknown) {
if (!this.TakenAt || this.Year === YearUnknown) {
return "Unknown";
}
@ -254,7 +252,7 @@ export class Photo extends RestModel {
}
shortDateString() {
if (!this.TakenAt || this.PhotoYear === YearUnknown) {
if (!this.TakenAt || this.Year === YearUnknown) {
return "Unknown";
}
@ -267,7 +265,7 @@ export class Photo extends RestModel {
}
hasLocation() {
return this.PhotoLat !== 0 || this.PhotoLng !== 0;
return this.Lat !== 0 || this.Lng !== 0;
}
getLocation() {
@ -283,21 +281,21 @@ export class Photo extends RestModel {
return;
}
if (file.FileWidth && file.FileHeight) {
info.push(file.FileWidth + " × " + file.FileHeight);
} else if (!file.FilePrimary) {
if (file.Width && file.Height) {
info.push(file.Width + " × " + file.Height);
} else if (!file.Primary) {
let main = this.mainFile();
if (main && main.FileWidth && main.FileHeight) {
info.push(main.FileWidth + " × " + main.FileHeight);
if (main && main.Width && main.Height) {
info.push(main.Width + " × " + main.Height);
}
}
if (file.FileSize > 102400) {
const size = Number.parseFloat(file.FileSize) / 1048576;
if (file.Size > 102400) {
const size = Number.parseFloat(file.Size) / 1048576;
info.push(size.toFixed(1) + " MB");
} else if (file.FileSize) {
const size = Number.parseFloat(file.FileSize) / 1024;
} else if (file.Size) {
const size = Number.parseFloat(file.Size) / 1024;
info.push(size.toFixed(1) + " KB");
}
@ -315,8 +313,8 @@ export class Photo extends RestModel {
return "Video";
}
if (file.FileDuration > 0) {
info.push(Util.duration(file.FileDuration));
if (file.Duration > 0) {
info.push(Util.duration(file.Duration));
}
this.addSizeInfo(file, info);
@ -332,7 +330,7 @@ export class Photo extends RestModel {
let info = [];
if (this.Camera) {
info.push(this.Camera.CameraMake + " " + this.Camera.CameraModel);
info.push(this.Camera.Make + " " + this.Camera.Model);
} else if (this.CameraModel && this.CameraMake) {
info.push(this.CameraMake + " " + this.CameraModel);
}
@ -350,7 +348,7 @@ export class Photo extends RestModel {
getCamera() {
if (this.Camera) {
return this.Camera.CameraMake + " " + this.Camera.CameraModel;
return this.Camera.Make + " " + this.Camera.Model;
} else if (this.CameraModel) {
return this.CameraMake + " " + this.CameraModel;
}
@ -359,9 +357,9 @@ export class Photo extends RestModel {
}
toggleLike() {
this.PhotoFavorite = !this.PhotoFavorite;
this.Favorite = !this.Favorite;
if (this.PhotoFavorite) {
if (this.Favorite) {
return Api.post(this.getEntityResource() + "/like");
} else {
return Api.delete(this.getEntityResource() + "/like");
@ -369,27 +367,27 @@ export class Photo extends RestModel {
}
togglePrivate() {
this.PhotoPrivate = !this.PhotoPrivate;
this.Private = !this.Private;
return Api.put(this.getEntityResource(), {PhotoPrivate: this.PhotoPrivate});
return Api.put(this.getEntityResource(), {Private: this.Private});
}
setPrimary(fileUUID) {
return Api.post(this.getEntityResource() + "/primary/" + fileUUID).then((r) => Promise.resolve(this.setValues(r.data)));
setPrimary(uid) {
return Api.post(this.getEntityResource() + "/primary/" + uid).then((r) => Promise.resolve(this.setValues(r.data)));
}
like() {
this.PhotoFavorite = true;
this.Favorite = true;
return Api.post(this.getEntityResource() + "/like");
}
unlike() {
this.PhotoFavorite = false;
this.Favorite = false;
return Api.delete(this.getEntityResource() + "/like");
}
addLabel(name) {
return Api.post(this.getEntityResource() + "/label", {LabelName: name, LabelPriority: 10})
return Api.post(this.getEntityResource() + "/label", {Name: name, Priority: 10})
.then((r) => Promise.resolve(this.setValues(r.data)));
}
@ -399,7 +397,7 @@ export class Photo extends RestModel {
}
renameLabel(id, name) {
return Api.put(this.getEntityResource() + "/label/" + id, {Label: {LabelName: name}})
return Api.put(this.getEntityResource() + "/label/" + id, {Label: {Name: name}})
.then((r) => Promise.resolve(this.setValues(r.data)));
}
@ -411,15 +409,15 @@ export class Photo extends RestModel {
update() {
const values = this.getValues(true);
if (values.PhotoTitle) {
if (values.Title) {
values.TitleSrc = SrcManual;
}
if (values.PhotoDescription) {
if (values.Description) {
values.DescriptionSrc = SrcManual;
}
if (values.PhotoLat || values.PhotoLng) {
if (values.Lat || values.Lng) {
values.LocationSrc = SrcManual;
}
@ -427,7 +425,7 @@ export class Photo extends RestModel {
values.TakenSrc = SrcManual;
}
if (values.CameraID || values.LensID || values.PhotoFocalLength || values.PhotoFNumber || values.PhotoIso || values.PhotoExposure) {
if (values.CameraID || values.LensID || values.FocalLength || values.FNumber || values.Iso || values.Exposure) {
values.CameraSrc = SrcManual;
}
@ -450,7 +448,7 @@ export class Photo extends RestModel {
if (response.models.length > 0) {
let i = results.length - 1;
if (results[i].PhotoUUID === response.models[0].PhotoUUID) {
if (results[i].UID === response.models[0].UID) {
const first = response.models.shift();
results[i].Files = results[i].Files.concat(first.Files);
}

View file

@ -1,12 +1,12 @@
import Model from "./model";
import Api from "../common/api";
const thumbs = window.clientConfig.thumbnails;
const thumbs = window.__CONFIG__.thumbnails;
export class Thumb extends Model {
getDefaults() {
return {
uuid: "",
uid: "",
title: "",
taken: "",
description: "",
@ -22,15 +22,15 @@ export class Thumb extends Model {
this.favorite = !this.favorite;
if (this.favorite) {
return Api.post("photos/" + this.uuid + "/like");
return Api.post("photos/" + this.uid + "/like");
} else {
return Api.delete("photos/" + this.uuid + "/like");
return Api.delete("photos/" + this.uid + "/like");
}
}
static thumbNotFound() {
const result = {
uuid: "",
uid: "",
title: "Not Found",
taken: "",
description: "",
@ -65,23 +65,23 @@ export class Thumb extends Model {
static fromPhoto(photo) {
if (photo.Files) {
return this.fromFile(photo, photo.Files.find(f => !!f.FilePrimary));
return this.fromFile(photo, photo.Files.find(f => !!f.Primary));
}
if (!photo || !photo.FileHash) {
if (!photo || !photo.Hash) {
return this.thumbNotFound();
}
const result = {
uuid: photo.PhotoUUID,
title: photo.PhotoTitle,
uid: photo.UID,
title: photo.Title,
taken: photo.getDateString(),
description: photo.PhotoDescription,
favorite: photo.PhotoFavorite,
description: photo.Description,
favorite: photo.Favorite,
playable: photo.isPlayable(),
download_url: this.downloadUrl(photo),
original_w: photo.FileWidth,
original_h: photo.FileHeight,
original_w: photo.Width,
original_h: photo.Height,
};
for (let i = 0; i < thumbs.length; i++) {
@ -98,20 +98,20 @@ export class Thumb extends Model {
}
static fromFile(photo, file) {
if (!photo || !file || !file.FileHash) {
if (!photo || !file || !file.Hash) {
return this.thumbNotFound();
}
const result = {
uuid: photo.PhotoUUID,
title: photo.PhotoTitle,
uid: photo.UID,
title: photo.Title,
taken: photo.getDateString(),
description: photo.PhotoDescription,
favorite: photo.PhotoFavorite,
description: photo.Description,
favorite: photo.Favorite,
playable: photo.isPlayable(),
download_url: this.downloadUrl(file),
original_w: file.FileWidth,
original_h: file.FileHeight,
original_w: file.Width,
original_h: file.Height,
};
thumbs.forEach((t) => {
@ -149,11 +149,11 @@ export class Thumb extends Model {
}
static calculateSize(file, width, height) {
if (width >= file.FileWidth && height >= file.FileHeight) { // Smaller
return {width: file.FileWidth, height: file.FileHeight};
if (width >= file.Width && height >= file.Height) { // Smaller
return {width: file.Width, height: file.Height};
}
const srcAspectRatio = file.FileWidth / file.FileHeight;
const srcAspectRatio = file.Width / file.Height;
const maxAspectRatio = width / height;
let newW, newH;
@ -171,20 +171,20 @@ export class Thumb extends Model {
}
static thumbnailUrl(file, type) {
if (!file.FileHash) {
if (!file.Hash) {
return "/api/v1/svg/photo";
}
return "/api/v1/thumbnails/" + file.FileHash + "/" + type;
return "/api/v1/thumbnails/" + file.Hash + "/" + type;
}
static downloadUrl(file) {
if (!file || !file.FileHash) {
if (!file || !file.Hash) {
return "";
}
return "/api/v1/download/" + file.FileHash;
return "/api/v1/download/" + file.Hash;
}
}

View file

@ -64,8 +64,8 @@
this.lastFilter = {};
this.routeName = this.$route.name;
if (this.uuid !== this.$route.params.uuid) {
this.uuid = this.$route.params.uuid;
if (this.uid !== this.$route.params.uid) {
this.uid = this.$route.params.uid;
this.findAlbum().then(() => this.search());
} else {
this.search();
@ -73,7 +73,7 @@
}
},
data() {
const uuid = this.$route.params.uuid;
const uid = this.$route.params.uid;
const query = this.$route.query;
const routeName = this.$route.name;
const order = query['order'] ? query['order'] : 'oldest';
@ -89,7 +89,7 @@
listen: false,
dirty: false,
model: new Album(),
uuid: uuid,
uid: uid,
results: [],
scrollDisabled: true,
pageSize: 60,
@ -144,7 +144,7 @@
return false;
}
if (showMerged && (this.results[index].PhotoType === 'video' || this.results[index].PhotoType === 'live')) {
if (showMerged && (this.results[index].Type === 'video' || this.results[index].Type === 'live')) {
if(this.results[index].isPlayable()) {
this.$modal.show('video', {video: this.results[index], album: this.album});
} else {
@ -170,7 +170,7 @@
const params = {
count: count,
offset: offset,
album: this.uuid,
album: this.uid,
merged: true,
};
@ -183,7 +183,7 @@
Photo.search(params).then(response => {
this.results = Photo.mergeResponse(this.results, response);
this.scrollDisabled = (response.models.length < count);
this.scrollDisabled = (response.count < count);
if (this.scrollDisabled) {
this.offset = offset;
@ -226,7 +226,7 @@
const params = {
count: this.pageSize,
offset: this.offset,
album: this.uuid,
album: this.uid,
merged: true,
};
@ -269,7 +269,7 @@
this.results = response.models;
this.scrollDisabled = (response.models.length < this.pageSize);
this.scrollDisabled = (response.count < this.pageSize);
if (this.scrollDisabled) {
if (!this.results.length) {
@ -291,11 +291,11 @@
});
},
findAlbum() {
return this.model.find(this.uuid).then(m => {
return this.model.find(this.uid).then(m => {
this.model = m;
this.filter.order = m.AlbumOrder;
window.document.title = `PhotoPrism: ${this.model.AlbumName}`;
window.document.title = `PhotoPrism: ${this.model.Name}`;
return Promise.resolve(this.model)
});
@ -308,7 +308,7 @@
}
for (let i = 0; i < data.entities.length; i++) {
if (this.model.AlbumUUID === data.entities[i].AlbumUUID) {
if (this.model.UID === data.entities[i].UID) {
let values = data.entities[i];
for (let key in values) {
@ -317,7 +317,7 @@
}
}
window.document.title = `PhotoPrism: ${this.model.AlbumName}`
window.document.title = `PhotoPrism: ${this.model.Name}`
this.dirty = true;
this.scrollDisabled = false;
@ -346,7 +346,7 @@
case 'updated':
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
const model = this.results.find((m) => m.PhotoUUID === values.PhotoUUID);
const model = this.results.find((m) => m.UID === values.UID);
if (model) {
for (let key in values) {
@ -368,8 +368,8 @@
this.dirty = true;
for (let i = 0; i < data.entities.length; i++) {
const uuid = data.entities[i];
const index = this.results.findIndex((m) => m.PhotoUUID === uuid);
const uid = data.entities[i];
const index = this.results.findIndex((m) => m.UID === uid);
if (index >= 0) {
this.results.splice(index, 1);
}

View file

@ -62,9 +62,9 @@
<v-card tile class="accent lighten-3"
slot-scope="{ hover }"
@contextmenu="onContextMenu($event, index)"
:dark="selection.includes(album.AlbumUUID)"
:class="selection.includes(album.AlbumUUID) ? 'elevation-10 ma-0 accent darken-1 white--text' : 'elevation-0 ma-1 accent lighten-3'"
:to="{name: 'album', params: {uuid: album.AlbumUUID, slug: album.AlbumSlug}}"
:dark="selection.includes(album.UID)"
:class="selection.includes(album.UID) ? 'elevation-10 ma-0 accent darken-1 white--text' : 'elevation-0 ma-1 accent lighten-3'"
:to="{name: 'album', params: {uid: album.UID, slug: album.Slug}}"
>
<v-img
:src="album.thumbnailUrl('tile_500')"
@ -84,11 +84,11 @@
color="accent lighten-5"></v-progress-circular>
</v-layout>
<v-btn v-if="hover || selection.includes(album.AlbumUUID)" :flat="!hover" :ripple="false"
<v-btn v-if="hover || selection.includes(album.UID)" :flat="!hover" :ripple="false"
icon large absolute
:class="selection.includes(album.AlbumUUID) ? 'p-album-select' : 'p-album-select opacity-50'"
:class="selection.includes(album.UID) ? 'p-album-select' : 'p-album-select opacity-50'"
@click.stop.prevent="onSelect($event, index)">
<v-icon v-if="selection.includes(album.AlbumUUID)" color="white">check_circle
<v-icon v-if="selection.includes(album.UID)" color="white">check_circle
</v-icon>
<v-icon v-else color="accent lighten-3">radio_button_off</v-icon>
</v-btn>
@ -96,20 +96,20 @@
<v-card-actions @click.stop.prevent="">
<v-edit-dialog
:return-value.sync="album.AlbumName"
:return-value.sync="album.Name"
lazy
@save="onSave(album)"
class="p-inline-edit"
>
<span v-if="album.AlbumName">
{{ album.AlbumName }}
<span v-if="album.Name">
{{ album.Name }}
</span>
<span v-else>
<v-icon>edit</v-icon>
</span>
<template v-slot:input>
<v-text-field
v-model="album.AlbumName"
v-model="album.Name"
:rules="[titleRule]"
:label="labels.name"
color="secondary-dark"
@ -121,7 +121,7 @@
<v-spacer></v-spacer>
<v-btn icon @click.stop.prevent="album.toggleLike()">
<v-icon v-if="album.AlbumFavorite" color="#FFD600">star
<v-icon v-if="album.Favorite" color="#FFD600">star
</v-icon>
<v-icon v-else color="accent lighten-2">star</v-icon>
</v-btn>
@ -386,39 +386,39 @@
create() {
let name = DateTime.local().toFormat("LLLL yyyy");
if (this.results.findIndex(a => a.AlbumName.startsWith(name)) !== -1) {
const existing = this.results.filter(a => a.AlbumName.startsWith(name));
if (this.results.findIndex(a => a.Name.startsWith(name)) !== -1) {
const existing = this.results.filter(a => a.Name.startsWith(name));
name = `${name} (${existing.length + 1})`
}
const album = new Album({"AlbumName": name, "AlbumFavorite": true});
const album = new Album({"Name": name, "Favorite": true});
album.save();
},
onSave(album) {
album.update();
},
addSelection(uuid) {
const pos = this.selection.indexOf(uuid);
addSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos === -1) {
this.selection.push(uuid)
this.lastId = uuid;
this.selection.push(uid)
this.lastId = uid;
}
},
toggleSelection(uuid) {
const pos = this.selection.indexOf(uuid);
toggleSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
} else {
this.selection.push(uuid);
this.lastId = uuid;
this.selection.push(uid);
this.lastId = uid;
}
},
removeSelection(uuid) {
const pos = this.selection.indexOf(uuid);
removeSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
@ -442,7 +442,7 @@
case 'updated':
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
const model = this.results.find((m) => m.AlbumUUID === values.AlbumUUID);
const model = this.results.find((m) => m.UID === values.UID);
for (let key in values) {
if (values.hasOwnProperty(key)) {
@ -455,14 +455,14 @@
this.dirty = true;
for (let i = 0; i < data.entities.length; i++) {
const uuid = data.entities[i];
const index = this.results.findIndex((m) => m.AlbumUUID === uuid);
const uid = data.entities[i];
const index = this.results.findIndex((m) => m.UID === uid);
if (index >= 0) {
this.results.splice(index, 1);
}
this.removeSelection(uuid)
this.removeSelection(uid)
}
break;
@ -471,7 +471,7 @@
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
const index = this.results.findIndex((m) => m.AlbumUUID === values.AlbumUUID);
const index = this.results.findIndex((m) => m.UID === values.UID);
if (index === -1) {
this.results.unshift(new Album(values));
}

View file

@ -0,0 +1,483 @@
<template>
<div class="p-page p-page-folders" v-infinite-scroll="loadMore" :infinite-scroll-disabled="scrollDisabled"
:infinite-scroll-distance="10" :infinite-scroll-listen-for-event="'scrollRefresh'">
<v-form ref="form" class="p-folders-search" lazy-validation @submit.prevent="updateQuery" dense>
<v-toolbar flat color="secondary">
<v-text-field class="pt-3 pr-3"
single-line
:label="labels.search"
prepend-inner-icon="search"
browser-autocomplete="off"
clearable
color="secondary-dark"
@click:clear="clearQuery"
v-model="filter.q"
@keyup.enter.native="updateQuery"
id="search"
></v-text-field>
<v-spacer></v-spacer>
<v-btn icon @click.stop="refresh">
<v-icon>refresh</v-icon>
</v-btn>
<v-btn v-if="!filter.all" icon @click.stop="showAll">
<v-icon>visibility</v-icon>
</v-btn>
<v-btn v-else icon @click.stop="showImportant">
<v-icon>visibility_off</v-icon>
</v-btn>
</v-toolbar>
</v-form>
<v-container fluid class="pa-4" v-if="loading">
<v-progress-linear color="secondary-dark" :indeterminate="true"></v-progress-linear>
</v-container>
<v-container fluid class="pa-0" v-else>
<p-folder-clipboard :refresh="refresh" :selection="selection"
:clear-selection="clearSelection"></p-folder-clipboard>
<p-scroll-top></p-scroll-top>
<v-container grid-list-xs fluid class="pa-2 p-folders p-folders-cards">
<v-card v-if="results.length === 0" class="p-folders-empty secondary-light lighten-1 ma-1" flat>
<v-card-title primary-title>
<div>
<h3 class="title mb-3">
<translate>No folders matched your search</translate>
</h3>
<div>
<translate>Try again using a related or otherwise similar term.</translate>
</div>
</div>
</v-card-title>
</v-card>
<v-layout row wrap class="p-folders-results">
<v-flex
v-for="(model, index) in results"
:key="index"
class="p-folder"
xs6 sm4 md3 lg2 d-flex
>
<v-hover>
<v-card tile class="accent lighten-3"
slot-scope="{ hover }"
@contextmenu="onContextMenu($event, index)"
:dark="selection.includes(model.UID)"
:class="selection.includes(model.UID) ? 'elevation-10 ma-0 accent darken-1 white--text' : 'elevation-0 ma-1 accent lighten-3'"
:to="{name: 'photos', query: {q: 'path:' + model.Path + '*' }}">
<v-img
:src="model.thumbnailUrl('tile_500')"
@mousedown="onMouseDown($event, index)"
@click="onClick($event, index)"
aspect-ratio="1"
class="accent lighten-2"
>
<v-layout
slot="placeholder"
fill-height
align-center
justify-center
ma-0
>
<v-progress-circular indeterminate
color="accent lighten-5"></v-progress-circular>
</v-layout>
<v-btn v-if="hover || selection.includes(model.UID)" :flat="!hover" :ripple="false"
icon large absolute
:class="selection.includes(model.UID) ? 'p-folder-select' : 'p-folder-select opacity-50'"
@click.stop.prevent="onSelect($event, index)">
<v-icon v-if="selection.includes(model.UID)" color="white">check_circle
</v-icon>
<v-icon v-else color="accent lighten-3">radio_button_off</v-icon>
</v-btn>
</v-img>
<v-card-actions @click.stop.prevent="">
<v-edit-dialog
:return-value.sync="model.Title"
lazy
@save="onSave(model)"
class="p-inline-edit"
>
<span v-if="model.Title">
{{ model.Title | capitalize }}
</span>
<span v-else>
<v-icon>edit</v-icon>
</span>
<template v-slot:input>
<v-text-field
v-model="model.Title"
:rules="[titleRule]"
:label="labels.name"
color="secondary-dark"
single-line
autofocus
></v-text-field>
</template>
</v-edit-dialog>
<v-spacer></v-spacer>
<v-btn icon @click.stop.prevent="model.toggleLike()">
<v-icon v-if="model.Favorite" color="#FFD600">star
</v-icon>
<v-icon v-else color="accent lighten-2">star</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</v-hover>
</v-flex>
</v-layout>
</v-container>
</v-container>
</div>
</template>
<script>
import Folder from "model/folder";
import Event from "pubsub-js";
import RestModel from "../model/rest";
export default {
name: 'p-page-folders',
props: {
staticFilter: Object
},
watch: {
'$route'() {
const query = this.$route.query;
this.filter.q = query['q'] ? query['q'] : '';
this.filter.all = query['all'] ? query['all'] : '';
this.lastFilter = {};
this.routeName = this.$route.name;
this.search();
}
},
data() {
const query = this.$route.query;
const routeName = this.$route.name;
const q = query['q'] ? query['q'] : '';
const all = query['all'] ? query['all'] : '';
const filter = {q: q, all: all};
const settings = {};
return {
config: this.$config.values,
subscriptions: [],
listen: false,
dirty: false,
results: [],
scrollDisabled: true,
loading: true,
pageSize: 24,
offset: 0,
page: 0,
selection: [],
settings: settings,
filter: filter,
lastFilter: {},
routeName: routeName,
labels: {
search: this.$gettext("Search"),
name: this.$gettext("Folder Name"),
},
titleRule: v => v.length <= this.$config.get('clip') || this.$gettext("Name too long"),
mouseDown: {
index: -1,
timeStamp: -1,
},
lastId: "",
};
},
methods: {
selectRange(rangeEnd, models) {
if (!models || !models[rangeEnd] || !(models[rangeEnd] instanceof RestModel)) {
console.warn("selectRange() - invalid arguments:", rangeEnd, models);
return;
}
let rangeStart = models.findIndex((m) => m.getId() === this.lastId);
if (rangeStart === -1) {
this.toggleSelection(models[rangeEnd].getId());
return 1;
}
if (rangeStart > rangeEnd) {
const newEnd = rangeStart;
rangeStart = rangeEnd;
rangeEnd = newEnd;
}
for (let i = rangeStart; i <= rangeEnd; i++) {
this.addSelection(models[i].getId());
}
return (rangeEnd - rangeStart) + 1;
},
onSelect(ev, index) {
if (ev.shiftKey) {
this.selectRange(index, this.results);
} else {
this.toggleSelection(this.results[index].getId());
}
},
onMouseDown(ev, index) {
this.mouseDown.index = index;
this.mouseDown.timeStamp = ev.timeStamp;
},
onClick(ev, index) {
let longClick = (this.mouseDown.index === index && ev.timeStamp - this.mouseDown.timeStamp > 400);
if (longClick || this.selection.length > 0) {
ev.preventDefault();
ev.stopPropagation();
if (longClick || ev.shiftKey) {
this.selectRange(index, this.results);
} else {
this.toggleSelection(this.results[index].getId());
}
}
},
onContextMenu(ev, index) {
if (this.$isMobile) {
ev.preventDefault();
ev.stopPropagation();
if(this.results[index]) {
this.selectRange(index, this.results);
}
}
},
onSave(model) {
model.update();
},
showAll() {
this.filter.all = "true";
this.updateQuery();
},
showImportant() {
this.filter.all = "";
this.updateQuery();
},
clearQuery() {
this.filter.q = '';
this.updateQuery();
},
addSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos === -1) {
this.selection.push(uid)
this.lastId = uid;
}
},
toggleSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
} else {
this.selection.push(uid);
this.lastId = uid;
}
},
removeSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
}
},
clearSelection() {
this.selection.splice(0, this.selection.length);
this.lastId = "";
},
loadMore() {
if (this.scrollDisabled) return;
this.scrollDisabled = true;
this.listen = false;
const count = this.dirty ? (this.page + 2) * this.pageSize : this.pageSize;
const offset = this.dirty ? 0 : this.offset;
const params = {
count: count,
offset: offset,
};
Object.assign(params, this.lastFilter);
if (this.staticFilter) {
Object.assign(params, this.staticFilter);
}
Folder.originals("", params).then(response => {
this.results = this.dirty ? response.models : this.results.concat(response.models);
this.scrollDisabled = (response.models.length < count);
if (this.scrollDisabled) {
this.offset = offset;
if (this.results.length > 1) {
this.$notify.info(this.$gettext('All ') + this.results.length + this.$gettext(' folders loaded'));
}
} else {
this.offset = offset + count;
this.page++;
}
}).catch(() => {
this.scrollDisabled = false;
}).finally(() => {
this.dirty = false;
this.loading = false;
this.listen = true;
});
},
updateQuery() {
const query = {
view: this.settings.view
};
Object.assign(query, this.filter);
for (let key in query) {
if (query[key] === undefined || !query[key]) {
delete query[key];
}
}
if (JSON.stringify(this.$route.query) === JSON.stringify(query)) {
return
}
this.$router.replace({query: query});
},
searchParams() {
const params = {
count: this.pageSize,
offset: this.offset,
};
Object.assign(params, this.filter);
if (this.staticFilter) {
Object.assign(params, this.staticFilter);
}
return params;
},
refresh() {
if (this.loading) return;
this.loading = true;
this.page = 0;
this.dirty = true;
this.scrollDisabled = false;
this.loadMore();
},
search() {
this.scrollDisabled = true;
// Don't query the same data more than once
if (JSON.stringify(this.lastFilter) === JSON.stringify(this.filter)) {
this.$nextTick(() => this.$emit("scrollRefresh"));
return;
}
Object.assign(this.lastFilter, this.filter);
this.offset = 0;
this.page = 0;
this.loading = true;
this.listen = false;
const params = this.searchParams();
Folder.originals("", params).then(response => {
this.offset = this.pageSize;
this.results = response.models;
this.scrollDisabled = (response.models.length < this.pageSize);
if (this.scrollDisabled) {
this.$notify.info(this.results.length + this.$gettext(' folders found'));
} else {
this.$notify.info(this.$gettext('More than 20 folders found'));
this.$nextTick(() => this.$emit("scrollRefresh"));
}
}).finally(() => {
this.dirty = false;
this.loading = false;
this.listen = true;
});
},
onUpdate(ev, data) {
if (!this.listen) return;
if (!data || !data.entities) {
return
}
const type = ev.split('.')[1];
switch (type) {
case 'updated':
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
const model = this.results.find((m) => m.UID === values.UID);
for (let key in values) {
if (values.hasOwnProperty(key)) {
model[key] = values[key];
}
}
}
break;
case 'deleted':
this.dirty = true;
for (let i = 0; i < data.entities.length; i++) {
const ppid = data.entities[i];
const index = this.results.findIndex((m) => m.UID === ppid);
if (index >= 0) {
this.results.splice(index, 1);
}
this.removeSelection(ppid)
}
break;
case 'created':
this.dirty = true;
break;
default:
console.warn("unexpected event type", ev);
}
}
},
created() {
this.search();
this.subscriptions.push(Event.subscribe("folders", (ev, data) => this.onUpdate(ev, data)));
this.subscriptions.push(Event.subscribe("touchmove.top", () => this.refresh()));
this.subscriptions.push(Event.subscribe("touchmove.bottom", () => this.loadMore()));
},
destroyed() {
for (let i = 0; i < this.subscriptions.length; i++) {
Event.unsubscribe(this.subscriptions[i]);
}
},
};
</script>

View file

@ -65,9 +65,9 @@
<v-card tile class="accent lighten-3"
slot-scope="{ hover }"
@contextmenu="onContextMenu($event, index)"
:dark="selection.includes(label.LabelUUID)"
:class="selection.includes(label.LabelUUID) ? 'elevation-10 ma-0 accent darken-1 white--text' : 'elevation-0 ma-1 accent lighten-3'"
:to="{name: 'photos', query: {q: 'label:' + (label.CustomSlug ? label.CustomSlug : label.LabelSlug)}}">
:dark="selection.includes(label.UID)"
:class="selection.includes(label.UID) ? 'elevation-10 ma-0 accent darken-1 white--text' : 'elevation-0 ma-1 accent lighten-3'"
:to="{name: 'photos', query: {q: 'label:' + (label.CustomSlug ? label.CustomSlug : label.Slug)}}">
<v-img
:src="label.thumbnailUrl('tile_500')"
@mousedown="onMouseDown($event, index)"
@ -96,11 +96,11 @@
>
</v-progress-circular -->
<v-btn v-if="hover || selection.includes(label.LabelUUID)" :flat="!hover" :ripple="false"
<v-btn v-if="hover || selection.includes(label.UID)" :flat="!hover" :ripple="false"
icon large absolute
:class="selection.includes(label.LabelUUID) ? 'p-label-select' : 'p-label-select opacity-50'"
:class="selection.includes(label.UID) ? 'p-label-select' : 'p-label-select opacity-50'"
@click.stop.prevent="onSelect($event, index)">
<v-icon v-if="selection.includes(label.LabelUUID)" color="white">check_circle
<v-icon v-if="selection.includes(label.UID)" color="white">check_circle
</v-icon>
<v-icon v-else color="accent lighten-3">radio_button_off</v-icon>
</v-btn>
@ -108,20 +108,20 @@
<v-card-actions @click.stop.prevent="">
<v-edit-dialog
:return-value.sync="label.LabelName"
:return-value.sync="label.Name"
lazy
@save="onSave(label)"
class="p-inline-edit"
>
<span v-if="label.LabelName">
{{ label.LabelName | capitalize }}
<span v-if="label.Name">
{{ label.Name | capitalize }}
</span>
<span v-else>
<v-icon>edit</v-icon>
</span>
<template v-slot:input>
<v-text-field
v-model="label.LabelName"
v-model="label.Name"
:rules="[titleRule]"
:label="labels.name"
color="secondary-dark"
@ -132,7 +132,7 @@
</v-edit-dialog>
<v-spacer></v-spacer>
<v-btn icon @click.stop.prevent="label.toggleLike()">
<v-icon v-if="label.LabelFavorite" color="#FFD600">star
<v-icon v-if="label.Favorite" color="#FFD600">star
</v-icon>
<v-icon v-else color="accent lighten-2">star</v-icon>
</v-btn>
@ -279,27 +279,27 @@
this.filter.q = '';
this.updateQuery();
},
addSelection(uuid) {
const pos = this.selection.indexOf(uuid);
addSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos === -1) {
this.selection.push(uuid)
this.lastId = uuid;
this.selection.push(uid)
this.lastId = uid;
}
},
toggleSelection(uuid) {
const pos = this.selection.indexOf(uuid);
toggleSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
} else {
this.selection.push(uuid);
this.lastId = uuid;
this.selection.push(uid);
this.lastId = uid;
}
},
removeSelection(uuid) {
const pos = this.selection.indexOf(uuid);
removeSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
@ -444,7 +444,7 @@
case 'updated':
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
const model = this.results.find((m) => m.LabelUUID === values.LabelUUID);
const model = this.results.find((m) => m.UID === values.UID);
for (let key in values) {
if (values.hasOwnProperty(key)) {
@ -457,14 +457,14 @@
this.dirty = true;
for (let i = 0; i < data.entities.length; i++) {
const uuid = data.entities[i];
const index = this.results.findIndex((m) => m.LabelUUID === uuid);
const uid = data.entities[i];
const index = this.results.findIndex((m) => m.UID === uid);
if (index >= 0) {
this.results.splice(index, 1);
}
this.removeSelection(uuid)
this.removeSelection(uid)
}
break;

View file

@ -9,15 +9,15 @@
height="64"
>
<v-tab id="tab-originals" ripple @click="changePath('/library')">
<translate>Originals</translate>
<translate key="Index">Index</translate>
</v-tab>
<v-tab id="tab-import" :disabled="readonly || !$config.feature('import')" ripple @click="changePath('/library/import')">
<translate>Import</translate>
<translate key="Copy">Copy</translate>
</v-tab>
<v-tab id="tab-logs" ripple @click="changePath('/library/logs')" v-if="$config.feature('logs')">
<translate>Logs</translate>
<translate key="Logs">Logs</translate>
</v-tab>
<v-tabs-items touchless>

View file

@ -4,9 +4,9 @@
<v-container fluid>
<p class="subheading">
<span v-if="fileName">{{ $gettext('Importing') }} {{fileName}}...</span>
<span v-else-if="busy">{{ $gettext('Importing files from import folder...') }}</span>
<span v-else-if="busy">{{ $gettext('Importing files to originals...') }}</span>
<span v-else-if="completed">{{ $gettext('Done.') }}</span>
<span v-else>{{ $gettext('Press button to start importing...') }}</span>
<span v-else>{{ $gettext('Press button to start copying to originals...') }}</span>
</p>
<v-autocomplete
@ -103,7 +103,7 @@
export default {
name: 'p-tab-import',
data() {
const root = {"path": "/", "name": this.$gettext("All files in import folder")}
const root = {"path": "/", "name": this.$gettext("All files from import folder")}
return {
settings: new Settings(this.$config.settings()),

View file

@ -181,7 +181,7 @@
return false;
}
if (showMerged && (this.results[index].PhotoType === 'video' || this.results[index].PhotoType === 'live')) {
if (showMerged && (this.results[index].Type === 'video' || this.results[index].Type === 'live')) {
if(this.results[index].isPlayable()) {
this.$modal.show('video', {video: this.results[index], album: null});
} else {
@ -341,7 +341,7 @@
case 'updated':
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
const model = this.results.find((m) => m.PhotoUUID === values.PhotoUUID);
const model = this.results.find((m) => m.UID === values.UID);
if (model) {
for (let key in values) {
@ -358,8 +358,8 @@
if (this.context !== "archive") break;
for (let i = 0; i < data.entities.length; i++) {
const uuid = data.entities[i];
const index = this.results.findIndex((m) => m.PhotoUUID === uuid);
const uid = data.entities[i];
const index = this.results.findIndex((m) => m.UID === uid);
if (index >= 0) {
this.results.splice(index, 1);
}
@ -372,8 +372,8 @@
if (this.context === "archive") break;
for (let i = 0; i < data.entities.length; i++) {
const uuid = data.entities[i];
const index = this.results.findIndex((m) => m.PhotoUUID === uuid);
const uid = data.entities[i];
const index = this.results.findIndex((m) => m.UID === uid);
if (index >= 0) {
this.results.splice(index, 1);
}

View file

@ -79,7 +79,7 @@
}
if (this.photos.length > 0) {
const index = this.photos.findIndex((p) => p.PhotoUUID === id);
const index = this.photos.findIndex((p) => p.UID === id);
this.$viewer.show(Thumb.fromPhotos(this.photos), index)
} else {
@ -169,14 +169,14 @@
if (!marker) {
let el = document.createElement('div');
el.className = 'marker';
el.title = props.PhotoTitle;
el.title = props.Title;
el.style.backgroundImage =
'url(/api/v1/thumbnails/' +
props.FileHash + '/tile_50)';
props.Hash + '/tile_50)';
el.style.width = '50px';
el.style.height = '50px';
el.addEventListener('click', () => this.openPhoto(props.PhotoUUID));
el.addEventListener('click', () => this.openPhoto(props.UID));
marker = this.markers[id] = new mapboxgl.Marker({
element: el
}).setLngLat(coords);

View file

@ -202,6 +202,36 @@
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
@change="onChange"
:disabled="busy"
class="ma-0 pa-0"
v-model="settings.features.folders"
color="secondary-dark"
:label="labels.folders"
:hint="hints.folders"
prepend-icon="folder"
persistent-hint
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
@change="onChange"
:disabled="busy"
class="ma-0 pa-0"
v-model="settings.features.moments"
color="secondary-dark"
:label="labels.moments"
:hint="hints.moments"
prepend-icon="star"
persistent-hint
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
@change="onChange"
@ -318,6 +348,7 @@
data() {
return {
readonly: this.$config.get("readonly"),
experimental: this.$config.get("experimental"),
settings: new Settings(this.$config.settings()),
options: options,
labels: {
@ -333,6 +364,8 @@
private: this.$gettext("Hide Private"),
review: this.$gettext("Quality Filter"),
places: this.$gettext("Places"),
folders: this.$gettext("Folders"),
moments: this.$gettext("Moments"),
labels: this.$gettext("Labels"),
import: this.$gettext("Import"),
upload: this.$gettext("Upload"),
@ -348,6 +381,8 @@
group: this.$gettext("Files with sequential names like 'IMG_1234 (2)' or 'IMG_1234 copy 2' belong to the same photo."),
move: this.$gettext("Move files from import to originals to save storage. Unsupported file types will never be deleted, they remain in their current location."),
places: this.$gettext("Search and display photos on a map."),
folders: this.$gettext("Browse existing folders as albums."),
moments: this.$gettext("Let PhotoPrism create albums from past events."),
labels: this.$gettext("Browse and edit image classification labels."),
import: this.$gettext("Imported files will be sorted by date and given a unique name."),
archive: this.$gettext("Hide photos that have been moved to archive."),

View file

@ -2,8 +2,8 @@ import Photos from "pages/photos.vue";
import Albums from "pages/albums.vue";
import AlbumPhotos from "pages/album/photos.vue";
import Places from "pages/places.vue";
import Folders from "pages/folders.vue";
import Labels from "pages/labels.vue";
import Events from "pages/events.vue";
import People from "pages/people.vue";
import Library from "pages/library.vue";
import Share from "pages/share.vue";
@ -12,7 +12,7 @@ import Login from "pages/login.vue";
import Discover from "pages/discover.vue";
import Todo from "pages/todo.vue";
const c = window.clientConfig;
const c = window.__CONFIG__;
export default [
{
@ -41,7 +41,7 @@ export default [
},
{
name: "album",
path: "/albums/:uuid",
path: "/albums/:uid",
component: AlbumPhotos,
meta: {title: "Album", auth: true},
},
@ -92,6 +92,12 @@ export default [
component: Places,
meta: {title: "Places", auth: true},
},
{
name: "folders",
path: "/folders",
component: Folders,
meta: {title: "Folders", auth: true},
},
{
name: "labels",
path: "/labels",
@ -99,10 +105,11 @@ export default [
meta: {title: "Labels", auth: true},
},
{
name: "events",
path: "/events",
component: Events,
meta: {title: "Events", auth: true},
name: "moments",
path: "/moments",
component: Photos,
meta: {title: "Moments", auth: true},
props: {},
},
{
name: "people",
@ -134,7 +141,7 @@ export default [
name: "library",
path: "/library",
component: Library,
meta: {title: "Photo Library", auth: true, background: "application-light"},
meta: {title: "Originals", auth: true, background: "application-light"},
props: {tab: 0},
},
{

View file

@ -1,7 +1,7 @@
import Config from "common/config";
import Session from "common/session";
export const config = new Config(window.localStorage, window.clientConfig);
export const config = new Config(window.localStorage, window.__CONFIG__);
export const session = new Session(window.localStorage, config);
export default session;

View file

@ -28,11 +28,11 @@ describe("common/clipboard", () => {
assert.equal(clipboard.storageKey, "clipboard");
assert.equal(clipboard.selection, "");
const values = {ID: 5, PhotoUUID: "ABC123", PhotoTitle: "Crazy Cat", PhotoColor: "brown"};
const values = {ID: 5, UID: "ABC123", Title: "Crazy Cat"};
const photo = new Photo(values);
clipboard.toggle(photo);
assert.equal(clipboard.selection[0], "ABC123");
const values2 = {ID: 8, PhotoUUID: "ABC124", PhotoTitle: "Crazy Cat", PhotoColor: "brown"};
const values2 = {ID: 8, UID: "ABC124", Title: "Crazy Cat"};
const photo2 = new Photo(values2);
clipboard.toggle(photo2);
assert.equal(clipboard.selection[0], "ABC123");
@ -66,7 +66,7 @@ describe("common/clipboard", () => {
assert.equal(clipboard.selection, "");
assert(spy.calledWith("Clipboard::add() - not a model:"));
const values = {ID: 5, PhotoUUID: "ABC124", PhotoTitle: "Crazy Cat", PhotoColor: "brown"};
const values = {ID: 5, UID: "ABC124", Title: "Crazy Cat"};
const photo = new Photo(values);
clipboard.add(photo);
assert.equal(clipboard.selection[0], "ABC124");
@ -97,13 +97,13 @@ describe("common/clipboard", () => {
assert.equal(clipboard.selection, "");
assert(spy.calledWith("Clipboard::has() - not a model:"));
const values = {ID: 5, PhotoUUID: "ABC124", PhotoTitle: "Crazy Cat", PhotoColor: "brown"};
const values = {ID: 5, UID: "ABC124", Title: "Crazy Cat"};
const photo = new Photo(values);
clipboard.add(photo);
assert.equal(clipboard.selection[0], "ABC124");
const result = clipboard.has(photo);
assert.equal(result, true);
const values2 = {ID: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019", AlbumUUID: 66};
const values2 = {ID: 5, Name: "Christmas 2019", Slug: "christmas-2019", UID: 66};
const album = new Album(values2);
const result2 = clipboard.has(album);
assert.equal(result2, false);
@ -133,14 +133,14 @@ describe("common/clipboard", () => {
assert.equal(clipboard.selection, "");
assert(spy.calledWith("Clipboard::remove() - not a model:"));
const values = {ID: 5, PhotoUUID: "ABC123", PhotoTitle: "Crazy Cat", PhotoColor: "brown"};
const values = {ID: 5, UID: "ABC123", Title: "Crazy Cat"};
const photo = new Photo(values);
clipboard.add(photo);
assert.equal(clipboard.selection[0], "ABC123");
clipboard.remove(photo);
assert.equal(clipboard.selection, "");
const values2 = {ID: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019", AlbumUUID: 66};
const values2 = {ID: 5, Name: "Christmas 2019", Slug: "christmas-2019", UID: 66};
const album = new Album(values2);
clipboard.remove(album);
assert.equal(clipboard.selection, "");

File diff suppressed because one or more lines are too long

View file

@ -13,49 +13,49 @@ mock
describe("model/album", () => {
it("should get album entity name", () => {
const values = {id: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019"};
const values = {id: 5, Name: "Christmas 2019", Slug: "christmas-2019"};
const album = new Album(values);
const result = album.getEntityName();
assert.equal(result, "christmas-2019");
});
it("should get album id", () => {
const values = {id: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019", AlbumUUID: 66};
const values = {id: 5, Name: "Christmas 2019", Slug: "christmas-2019", UID: 66};
const album = new Album(values);
const result = album.getId();
assert.equal(result, "66");
});
it("should get album title", () => {
const values = {id: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019"};
const values = {id: 5, Name: "Christmas 2019", Slug: "christmas-2019"};
const album = new Album(values);
const result = album.getTitle();
assert.equal(result, "Christmas 2019");
});
it("should get thumbnail url", () => {
const values = {id: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019", AlbumUUID: 66};
const values = {id: 5, Name: "Christmas 2019", Slug: "christmas-2019", UID: 66};
const album = new Album(values);
const result = album.thumbnailUrl("xyz");
assert.equal(result, "/api/v1/albums/66/thumbnail/xyz");
});
it("should get thumbnail src set", () => {
const values = {id: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019", AlbumUUID: 66};
const values = {id: 5, Name: "Christmas 2019", Slug: "christmas-2019", UID: 66};
const album = new Album(values);
const result = album.thumbnailSrcset("");
assert.equal(result, "/api/v1/albums/66/thumbnail/fit_720 720w, /api/v1/albums/66/thumbnail/fit_1280 1280w, /api/v1/albums/66/thumbnail/fit_1920 1920w, /api/v1/albums/66/thumbnail/fit_2560 2560w, /api/v1/albums/66/thumbnail/fit_3840 3840w");
});
it("should get thumbnail sizes", () => {
const values = {id: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019", CreatedAt: "2012-07-08T14:45:39Z"};
const values = {id: 5, Name: "Christmas 2019", Slug: "christmas-2019", CreatedAt: "2012-07-08T14:45:39Z"};
const album = new Album(values);
const result = album.thumbnailSizes();
assert.equal(result, "(min-width: 2560px) 3840px, (min-width: 1920px) 2560px, (min-width: 1280px) 1920px, (min-width: 720px) 1280px, 720px");
});
it("should get date string", () => {
const values = {ID: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019", CreatedAt: "2012-07-08T14:45:39Z"};
const values = {ID: 5, Name: "Christmas 2019", Slug: "christmas-2019", CreatedAt: "2012-07-08T14:45:39Z"};
const album = new Album(values);
const result = album.getDateString();
assert.equal(result, "Jul 8, 2012, 2:45 PM");
@ -72,28 +72,28 @@ describe("model/album", () => {
});
it("should like album", () => {
const values = {id: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019", AlbumFavorite: false};
const values = {id: 5, Name: "Christmas 2019", Slug: "christmas-2019", Favorite: false};
const album = new Album(values);
assert.equal(album.AlbumFavorite, false);
assert.equal(album.Favorite, false);
album.like();
assert.equal(album.AlbumFavorite, true);
assert.equal(album.Favorite, true);
});
it("should unlike album", () => {
const values = {id: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019", AlbumFavorite: true};
const values = {id: 5, Name: "Christmas 2019", Slug: "christmas-2019", Favorite: true};
const album = new Album(values);
assert.equal(album.AlbumFavorite, true);
assert.equal(album.Favorite, true);
album.unlike();
assert.equal(album.AlbumFavorite, false);
assert.equal(album.Favorite, false);
});
it("should toggle like", () => {
const values = {id: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019", AlbumFavorite: true};
const values = {id: 5, Name: "Christmas 2019", Slug: "christmas-2019", Favorite: true};
const album = new Album(values);
assert.equal(album.AlbumFavorite, true);
assert.equal(album.Favorite, true);
album.toggleLike();
assert.equal(album.AlbumFavorite, false);
assert.equal(album.Favorite, false);
album.toggleLike();
assert.equal(album.AlbumFavorite, true);
assert.equal(album.Favorite, true);
});
});

View file

@ -13,49 +13,49 @@ mock
describe("model/label", () => {
it("should get label entity name", () => {
const values = {ID: 5, LabelUUID: "ABC123", LabelName: "Black Cat", LabelSlug: "black-cat"};
const values = {ID: 5, UID: "ABC123", Name: "Black Cat", Slug: "black-cat"};
const label = new Label(values);
const result = label.getEntityName();
assert.equal(result, "black-cat");
});
it("should get label id", () => {
const values = {ID: 5, LabelUUID: "ABC123", LabelName: "Black Cat", LabelSlug: "black-cat"};
const values = {ID: 5, UID: "ABC123", Name: "Black Cat", Slug: "black-cat"};
const label = new Label(values);
const result = label.getId();
assert.equal(result, "ABC123");
});
it("should get label title", () => {
const values = {ID: 5, LabelUUID: "ABC123", LabelName: "Black Cat", LabelSlug: "black-cat"};
const values = {ID: 5, UID: "ABC123", Name: "Black Cat", Slug: "black-cat"};
const label = new Label(values);
const result = label.getTitle();
assert.equal(result, "Black Cat");
});
it("should get thumbnail url", () => {
const values = {ID: 5, LabelUUID: "ABC123", LabelName: "Black Cat", LabelSlug: "black-cat"};
const values = {ID: 5, UID: "ABC123", Name: "Black Cat", Slug: "black-cat"};
const label = new Label(values);
const result = label.thumbnailUrl("xyz");
assert.equal(result, "/api/v1/labels/ABC123/thumbnail/xyz");
});
it("should get thumbnail src set", () => {
const values = {ID: 5, LabelUUID: "ABC123", LabelName: "Black Cat", LabelSlug: "black-cat"};
const values = {ID: 5, UID: "ABC123", Name: "Black Cat", Slug: "black-cat"};
const label = new Label(values);
const result = label.thumbnailSrcset("");
assert.equal(result, "/api/v1/labels/ABC123/thumbnail/fit_720 720w, /api/v1/labels/ABC123/thumbnail/fit_1280 1280w, /api/v1/labels/ABC123/thumbnail/fit_1920 1920w, /api/v1/labels/ABC123/thumbnail/fit_2560 2560w, /api/v1/labels/ABC123/thumbnail/fit_3840 3840w");
});
it("should get thumbnail sizes", () => {
const values = {ID: 5, LabelUUID: "ABC123", LabelName: "Black Cat", LabelSlug: "black-cat"};
const values = {ID: 5, UID: "ABC123", Name: "Black Cat", Slug: "black-cat"};
const label = new Label(values);
const result = label.thumbnailSizes();
assert.equal(result, "(min-width: 2560px) 3840px, (min-width: 1920px) 2560px, (min-width: 1280px) 1920px, (min-width: 720px) 1280px, 720px");
});
it("should get date string", () => {
const values = {ID: 5, LabelUUID: "ABC123", LabelName: "Black Cat", LabelSlug: "black-cat", CreatedAt: "2012-07-08T14:45:39Z"};
const values = {ID: 5, UID: "ABC123", Name: "Black Cat", Slug: "black-cat", CreatedAt: "2012-07-08T14:45:39Z"};
const label = new Label(values);
const result = label.getDateString();
assert.equal(result, "Jul 8, 2012, 2:45 PM");
@ -72,28 +72,28 @@ describe("model/label", () => {
});
it("should like label", () => {
const values = {ID: 5, LabelUUID: "ABC123", LabelName: "Black Cat", LabelSlug: "black-cat", LabelFavorite: false};
const values = {ID: 5, UID: "ABC123", Name: "Black Cat", Slug: "black-cat", Favorite: false};
const label = new Label(values);
assert.equal(label.LabelFavorite, false);
assert.equal(label.Favorite, false);
label.like();
assert.equal(label.LabelFavorite, true);
assert.equal(label.Favorite, true);
});
it("should unlike label", () => {
const values = {ID: 5, LabelUUID: "ABC123",LabelName: "Black Cat", LabelSlug: "black-cat", LabelFavorite: true};
const values = {ID: 5, UID: "ABC123",Name: "Black Cat", Slug: "black-cat", Favorite: true};
const label = new Label(values);
assert.equal(label.LabelFavorite, true);
assert.equal(label.Favorite, true);
label.unlike();
assert.equal(label.LabelFavorite, false);
assert.equal(label.Favorite, false);
});
it("should toggle like", () => {
const values = {ID: 5, LabelUUID: "ABC123", LabelName: "Black Cat", LabelSlug: "black-cat", LabelFavorite: true};
const values = {ID: 5, UID: "ABC123", Name: "Black Cat", Slug: "black-cat", Favorite: true};
const label = new Label(values);
assert.equal(label.LabelFavorite, true);
assert.equal(label.Favorite, true);
label.toggleLike();
assert.equal(label.LabelFavorite, false);
assert.equal(label.Favorite, false);
label.toggleLike();
assert.equal(label.LabelFavorite, true);
assert.equal(label.Favorite, true);
});
});

View file

@ -13,77 +13,56 @@ mock
describe("model/photo", () => {
it("should get photo entity name", () => {
const values = {id: 5, PhotoTitle: "Crazy Cat"};
const values = {UID: 5, Title: "Crazy Cat"};
const photo = new Photo(values);
const result = photo.getEntityName();
assert.equal(result, "Crazy Cat");
});
it("should get photo uuid", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", PhotoUUID: 789};
const values = {ID: 5, Title: "Crazy Cat", UID: 789};
const photo = new Photo(values);
const result = photo.getId();
assert.equal(result, 789);
});
it("should get photo title", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", PhotoUUID: 789};
const values = {ID: 5, Title: "Crazy Cat", UID: 789};
const photo = new Photo(values);
const result = photo.getTitle();
assert.equal(result, "Crazy Cat");
});
it("should get photo color brown", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", PhotoColor: "brown"};
const photo = new Photo(values);
const result = photo.getColor();
assert.equal(result, "grey lighten-2");
});
it("should get photo color grey", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", PhotoColor: "grey"};
const photo = new Photo(values);
const result = photo.getColor();
assert.equal(result, "grey lighten-2");
});
it("should get photo color pink", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", PhotoColor: "pink"};
const photo = new Photo(values);
const result = photo.getColor();
assert.equal(result, "pink lighten-4");
});
it("should get photo maps link", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", PhotoLat: 36.442881666666665, PhotoLng: 28.229493333333334};
const values = {ID: 5, Title: "Crazy Cat", Lat: 36.442881666666665, Lng: 28.229493333333334};
const photo = new Photo(values);
const result = photo.getGoogleMapsLink();
assert.equal(result, "https://www.google.com/maps/place/36.442881666666665,28.229493333333334");
});
it("should get photo thumbnail url", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", FileHash: 345982};
const values = {ID: 5, Title: "Crazy Cat", Hash: 345982};
const photo = new Photo(values);
const result = photo.thumbnailUrl("tile500");
assert.equal(result, "/api/v1/thumbnails/345982/tile500");
});
it("should get photo download url", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", FileHash: 345982};
const values = {ID: 5, Title: "Crazy Cat", Hash: 345982};
const photo = new Photo(values);
const result = photo.getDownloadUrl();
assert.equal(result, "/api/v1/download/345982");
});
it("should get photo thumbnail src set", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", FileHash: 345982};
const values = {ID: 5, Title: "Crazy Cat", Hash: 345982};
const photo = new Photo(values);
const result = photo.thumbnailSrcset();
assert.equal(result, "/api/v1/thumbnails/345982/fit_720 720w, /api/v1/thumbnails/345982/fit_1280 1280w, /api/v1/thumbnails/345982/fit_1920 1920w, /api/v1/thumbnails/345982/fit_2560 2560w, /api/v1/thumbnails/345982/fit_3840 3840w");
});
it("should calculate photo size", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", FileWidth: 500, FileHeight: 200};
const values = {ID: 5, Title: "Crazy Cat", Width: 500,Height: 200};
const photo = new Photo(values);
const result = photo.calculateSize(500, 200);
assert.equal(result.width, 500);
@ -91,7 +70,7 @@ describe("model/photo", () => {
});
it("should calculate photo size with srcAspectRatio < maxAspectRatio", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", FileWidth: 500, FileHeight: 200};
const values = {ID: 5, Title: "Crazy Cat", Width: 500, Height: 200};
const photo = new Photo(values);
const result = photo.calculateSize(300, 50);
assert.equal(result.width, 125);
@ -99,7 +78,7 @@ describe("model/photo", () => {
});
it("should calculate photo size with srcAspectRatio > maxAspectRatio", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", FileWidth: 500, FileHeight: 200};
const values = {ID: 5, Title: "Crazy Cat", Width: 500, Height: 200};
const photo = new Photo(values);
const result = photo.calculateSize(400, 300);
assert.equal(result.width, 400);
@ -107,70 +86,70 @@ describe("model/photo", () => {
});
it("should get thumbnail sizes", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", FileWidth: 500, FileHeight: 200};
const values = {ID: 5, Title: "Crazy Cat", Width: 500, Height: 200};
const photo = new Photo(values);
const result = photo.thumbnailSizes();
assert.equal(result, "(min-width: 2560px) 3840px, (min-width: 1920px) 2560px, (min-width: 1280px) 1920px, (min-width: 720px) 1280px, 720px");
});
it("should get date string", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", TakenAt: "2012-07-08T14:45:39Z", TimeZone: "UTC"};
const values = {ID: 5, Title: "Crazy Cat", TakenAt: "2012-07-08T14:45:39Z", TimeZone: "UTC"};
const photo = new Photo(values);
const result = photo.getDateString();
assert.equal(result, "July 8, 2012, 2:45 PM UTC");
});
it("should test whether photo has location", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", PhotoLat: 36.442881666666665, PhotoLng: 28.229493333333334};
const values = {ID: 5, Title: "Crazy Cat", Lat: 36.442881666666665, Lng: 28.229493333333334};
const photo = new Photo(values);
const result = photo.hasLocation();
assert.equal(result, true);
});
it("should test whether photo has location", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", PhotoLat: 0, PhotoLng: 0};
const values = {ID: 5, Title: "Crazy Cat", Lat: 0, Lng: 0};
const photo = new Photo(values);
const result = photo.hasLocation();
assert.equal(result, false);
});
it("should get location", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", LocationID: 6, LocType: "viewpoint", LocLabel: "Cape Point, South Africa", LocCountry: "South Africa"};
const values = {ID: 5, Title: "Crazy Cat", LocationID: 6, LocType: "viewpoint", LocLabel: "Cape Point, South Africa", LocCountry: "South Africa"};
const photo = new Photo(values);
const result = photo.getLocation();
assert.equal(result, "Cape Point, South Africa");
});
it("should get location", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", LocationID: 6, LocType: "viewpoint", LocLabel: "Cape Point, State, South Africa", LocCountry: "South Africa", LocCity: "Cape Town", LocCounty: "County", LocState: "State"};
const values = {ID: 5, Title: "Crazy Cat", LocationID: 6, LocType: "viewpoint", LocLabel: "Cape Point, State, South Africa", LocCountry: "South Africa", LocCity: "Cape Town", LocCounty: "County", LocState: "State"};
const photo = new Photo(values);
const result = photo.getLocation();
assert.equal(result, "Cape Point, State, South Africa");
});
it("should get location", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", LocType: "viewpoint", LocName: "Cape Point", LocCountry: "Africa", LocCity: "Cape Town", LocCounty: "County", LocState: "State"};
const values = {ID: 5, Title: "Crazy Cat", LocType: "viewpoint", LocName: "Cape Point", LocCountry: "Africa", LocCity: "Cape Town", LocCounty: "County", LocState: "State"};
const photo = new Photo(values);
const result = photo.getLocation();
assert.equal(result, "Unknown");
});
it("should get location", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", CountryName: "Africa", LocCity: "Cape Town"};
const values = {ID: 5, Title: "Crazy Cat", CountryName: "Africa", LocCity: "Cape Town"};
const photo = new Photo(values);
const result = photo.getLocation();
assert.equal(result, "Unknown");
});
it("should get camera", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", CameraModel: "EOSD10", CameraMake: "Canon"};
const values = {ID: 5, Title: "Crazy Cat", CameraModel: "EOSD10", CameraMake: "Canon"};
const photo = new Photo(values);
const result = photo.getCamera();
assert.equal(result, "Canon EOSD10");
});
it("should get camera", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat"};
const values = {ID: 5, Title: "Crazy Cat"};
const photo = new Photo(values);
const result = photo.getCamera();
assert.equal(result, "Unknown");
@ -187,29 +166,29 @@ describe("model/photo", () => {
});
it("should like photo", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", CountryName: "Africa", PhotoFavorite: false};
const values = {ID: 5, Title: "Crazy Cat", CountryName: "Africa", Favorite: false};
const photo = new Photo(values);
assert.equal(photo.PhotoFavorite, false);
assert.equal(photo.Favorite, false);
photo.like();
assert.equal(photo.PhotoFavorite, true);
assert.equal(photo.Favorite, true);
});
it("should unlike photo", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", CountryName: "Africa", PhotoFavorite: true};
const values = {ID: 5, Title: "Crazy Cat", CountryName: "Africa", Favorite: true};
const photo = new Photo(values);
assert.equal(photo.PhotoFavorite, true);
assert.equal(photo.Favorite, true);
photo.unlike();
assert.equal(photo.PhotoFavorite, false);
assert.equal(photo.Favorite, false);
});
it("should toggle like", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", CountryName: "Africa", PhotoFavorite: true};
const values = {ID: 5, Title: "Crazy Cat", CountryName: "Africa", Favorite: true};
const photo = new Photo(values);
assert.equal(photo.PhotoFavorite, true);
assert.equal(photo.Favorite, true);
photo.toggleLike();
assert.equal(photo.PhotoFavorite, false);
assert.equal(photo.Favorite, false);
photo.toggleLike();
assert.equal(photo.PhotoFavorite, true);
assert.equal(photo.Favorite, true);
});
});

View file

@ -10,36 +10,36 @@ let assert = chai.assert;
describe("model/abstract", () => {
const mock = new MockAdapter(Api);
it("should set values", () => {
const values = {id: 5, LabelName: "Black Cat", LabelSlug: "black-cat"};
const values = {id: 5, Name: "Black Cat", Slug: "black-cat"};
const label = new Label(values);
assert.equal(label.LabelName, "Black Cat");
assert.equal(label.LabelSlug, "black-cat");
assert.equal(label.Name, "Black Cat");
assert.equal(label.Slug, "black-cat");
label.setValues();
assert.equal(label.LabelName, "Black Cat");
assert.equal(label.LabelSlug, "black-cat");
const values2 = {id: 6, LabelName: "White Cat", LabelSlug: "white-cat"};
assert.equal(label.Name, "Black Cat");
assert.equal(label.Slug, "black-cat");
const values2 = {id: 6, Name: "White Cat", Slug: "white-cat"};
label.setValues(values2);
assert.equal(label.LabelName, "White Cat");
assert.equal(label.LabelSlug, "white-cat");
assert.equal(label.Name, "White Cat");
assert.equal(label.Slug, "white-cat");
});
it("should get values", () => {
const values = {id: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019", AlbumUUID: 66};
const values = {id: 5, Name: "Christmas 2019", Slug: "christmas-2019", UID: 66};
const album = new Album(values);
const result = album.getValues();
assert.equal(result.AlbumName, "Christmas 2019");
assert.equal(result.AlbumUUID, 66);
assert.equal(result.Name, "Christmas 2019");
assert.equal(result.UID, 66);
});
it("should get id", () => {
const values = {id: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019", AlbumUUID: 66};
const values = {id: 5, Name: "Christmas 2019", Slug: "christmas-2019", UID: 66};
const album = new Album(values);
const result = album.getId();
assert.equal(result, 66);
});
it("should test if id exists", () => {
const values = {id: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019", AlbumUUID: 66};
const values = {id: 5, Name: "Christmas 2019", Slug: "christmas-2019", UID: 66};
const album = new Album(values);
const result = album.hasId();
assert.equal(result, true);
@ -51,47 +51,47 @@ describe("model/abstract", () => {
});
it("should update album", async() => {
mock.onPut().reply(200, {AlbumDescription: "Test description"});
const values = {id: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019", AlbumUUID: 66};
mock.onPut().reply(200, {Description: "Test description"});
const values = {id: 5, Name: "Christmas 2019", Slug: "christmas-2019", UID: 66};
const album = new Album(values);
assert.equal(album.AlbumDescription, undefined);
assert.equal(album.Description, undefined);
await album.update();
assert.equal(album.AlbumDescription, "Test description");
assert.equal(album.Description, "Test description");
mock.reset();
});
it("should save album", async() => {
mock.onPut().reply(200, {AlbumDescription: "Test description"});
const values = {id: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019", AlbumUUID: 66};
mock.onPut().reply(200, {Description: "Test description"});
const values = {id: 5, Name: "Christmas 2019", Slug: "christmas-2019", UID: 66};
const album = new Album(values);
assert.equal(album.AlbumDescription, undefined);
assert.equal(album.Description, undefined);
await album.save();
assert.equal(album.AlbumDescription, "Test description");
assert.equal(album.Description, "Test description");
mock.reset();
});
it("should save album", async() => {
mock.onPost().reply(200, {AlbumDescription: "Test description"});
const values = {id: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019"};
mock.onPost().reply(200, {Description: "Test description"});
const values = {id: 5, Name: "Christmas 2019", Slug: "christmas-2019"};
const album = new Album(values);
assert.equal(album.AlbumDescription, undefined);
assert.equal(album.Description, undefined);
await album.save();
assert.equal(album.AlbumDescription, "Test description");
assert.equal(album.Description, "Test description");
mock.reset();
});
it("should remove album", async() => {
mock.onDelete().reply(200);
const values = {id: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019"};
const values = {id: 5, Name: "Christmas 2019", Slug: "christmas-2019"};
const album = new Album(values);
assert.equal(album.AlbumName, "Christmas 2019");
assert.equal(album.Name, "Christmas 2019");
await album.remove();
mock.reset();
});
it("should get edit form", async() => {
mock.onAny().reply(200, "editForm");
const values = {id: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019"};
const values = {id: 5, Name: "Christmas 2019", Slug: "christmas-2019"};
const album = new Album(values);
const result = await album.getEditForm();
assert.equal(result.definition, "editForm");
@ -113,10 +113,10 @@ describe("model/abstract", () => {
});
it("should search label", async() => {
mock.onAny().reply(200, {"ID":51,"CreatedAt":"2019-07-03T18:48:07Z","UpdatedAt":"2019-07-25T01:04:44Z","DeletedAt":"0001-01-01T00:00:00Z","LabelSlug":"tabby-cat","LabelName":"tabby cat","LabelPriority":5,"LabelCount":9,"LabelFavorite":false,"LabelDescription":"","LabelNotes":""});
mock.onAny().reply(200, {"ID":51,"CreatedAt":"2019-07-03T18:48:07Z","UpdatedAt":"2019-07-25T01:04:44Z","DeletedAt":"0001-01-01T00:00:00Z","Slug":"tabby-cat","Name":"tabby cat","Priority":5,"LabelCount":9,"Favorite":false,"Description":"","Notes":""});
const result = await Album.search();
assert.equal(result.data.ID, 51);
assert.equal(result.data.LabelName, "tabby cat");
assert.equal(result.data.Name, "tabby cat");
mock.reset();
});

View file

@ -128,7 +128,7 @@ func ShareWithAccount(router *gin.RouterGroup, conf *config.Config) {
}
dst := f.Destination
files, err := query.FilesByUUID(f.Photos, 1000, 0)
files, err := query.FilesByUID(f.Photos, 1000, 0)
if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})

View file

@ -57,11 +57,11 @@ func GetAlbums(router *gin.RouterGroup, conf *config.Config) {
})
}
// GET /api/v1/albums/:uuid
// GET /api/v1/albums/:uid
func GetAlbum(router *gin.RouterGroup, conf *config.Config) {
router.GET("/albums/:uuid", func(c *gin.Context) {
id := c.Param("uuid")
m, err := query.AlbumByUUID(id)
router.GET("/albums/:uid", func(c *gin.Context) {
id := c.Param("uid")
m, err := query.AlbumByUID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
@ -87,7 +87,7 @@ func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
return
}
m := entity.NewAlbum(f.AlbumName)
m := entity.NewAlbum(f.AlbumName, entity.TypeDefault)
m.AlbumFavorite = f.AlbumFavorite
log.Debugf("create album: %+v %+v", f, m)
@ -102,22 +102,22 @@ func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
event.Publish("config.updated", event.Data(conf.ClientConfig()))
PublishAlbumEvent(EntityCreated, m.AlbumUUID, c)
PublishAlbumEvent(EntityCreated, m.AlbumUID, c)
c.JSON(http.StatusOK, m)
})
}
// PUT /api/v1/albums/:uuid
// PUT /api/v1/albums/:uid
func UpdateAlbum(router *gin.RouterGroup, conf *config.Config) {
router.PUT("/albums/:uuid", func(c *gin.Context) {
router.PUT("/albums/:uid", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
uuid := c.Param("uuid")
m, err := query.AlbumByUUID(uuid)
uid := c.Param("uid")
m, err := query.AlbumByUID(uid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
@ -147,23 +147,23 @@ func UpdateAlbum(router *gin.RouterGroup, conf *config.Config) {
event.Publish("config.updated", event.Data(conf.ClientConfig()))
event.Success("album saved")
PublishAlbumEvent(EntityUpdated, uuid, c)
PublishAlbumEvent(EntityUpdated, uid, c)
c.JSON(http.StatusOK, m)
})
}
// DELETE /api/v1/albums/:uuid
// DELETE /api/v1/albums/:uid
func DeleteAlbum(router *gin.RouterGroup, conf *config.Config) {
router.DELETE("/albums/:uuid", func(c *gin.Context) {
router.DELETE("/albums/:uid", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
id := c.Param("uuid")
id := c.Param("uid")
m, err := query.AlbumByUUID(id)
m, err := query.AlbumByUID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
@ -181,19 +181,19 @@ func DeleteAlbum(router *gin.RouterGroup, conf *config.Config) {
})
}
// POST /api/v1/albums/:uuid/like
// POST /api/v1/albums/:uid/like
//
// Parameters:
// uuid: string Album UUID
// uid: string Album UID
func LikeAlbum(router *gin.RouterGroup, conf *config.Config) {
router.POST("/albums/:uuid/like", func(c *gin.Context) {
router.POST("/albums/:uid/like", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
id := c.Param("uuid")
album, err := query.AlbumByUUID(id)
id := c.Param("uid")
album, err := query.AlbumByUID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
@ -210,19 +210,19 @@ func LikeAlbum(router *gin.RouterGroup, conf *config.Config) {
})
}
// DELETE /api/v1/albums/:uuid/like
// DELETE /api/v1/albums/:uid/like
//
// Parameters:
// uuid: string Album UUID
// uid: string Album UID
func DislikeAlbum(router *gin.RouterGroup, conf *config.Config) {
router.DELETE("/albums/:uuid/like", func(c *gin.Context) {
router.DELETE("/albums/:uid/like", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
id := c.Param("uuid")
album, err := query.AlbumByUUID(id)
id := c.Param("uid")
album, err := query.AlbumByUID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
@ -239,9 +239,9 @@ func DislikeAlbum(router *gin.RouterGroup, conf *config.Config) {
})
}
// POST /api/v1/albums/:uuid/photos
// POST /api/v1/albums/:uid/photos
func AddPhotosToAlbum(router *gin.RouterGroup, conf *config.Config) {
router.POST("/albums/:uuid/photos", func(c *gin.Context) {
router.POST("/albums/:uid/photos", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
@ -254,8 +254,8 @@ func AddPhotosToAlbum(router *gin.RouterGroup, conf *config.Config) {
return
}
uuid := c.Param("uuid")
a, err := query.AlbumByUUID(uuid)
uid := c.Param("uid")
a, err := query.AlbumByUID(uid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
@ -273,24 +273,24 @@ func AddPhotosToAlbum(router *gin.RouterGroup, conf *config.Config) {
var added []*entity.PhotoAlbum
for _, p := range photos {
added = append(added, entity.NewPhotoAlbum(p.PhotoUUID, a.AlbumUUID).FirstOrCreate())
added = append(added, entity.NewPhotoAlbum(p.PhotoUID, a.AlbumUID).FirstOrCreate())
}
if len(added) == 1 {
event.Success(fmt.Sprintf("one photo added to %s", a.AlbumName))
event.Success(fmt.Sprintf("one photo added to %s", txt.Quote(a.AlbumName)))
} else {
event.Success(fmt.Sprintf("%d photos added to %s", len(added), a.AlbumName))
event.Success(fmt.Sprintf("%d photos added to %s", len(added), txt.Quote(a.AlbumName)))
}
PublishAlbumEvent(EntityUpdated, a.AlbumUUID, c)
PublishAlbumEvent(EntityUpdated, a.AlbumUID, c)
c.JSON(http.StatusOK, gin.H{"message": "photos added to album", "album": a, "added": added})
})
}
// DELETE /api/v1/albums/:uuid/photos
// DELETE /api/v1/albums/:uid/photos
func RemovePhotosFromAlbum(router *gin.RouterGroup, conf *config.Config) {
router.DELETE("/albums/:uuid/photos", func(c *gin.Context) {
router.DELETE("/albums/:uid/photos", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
@ -309,29 +309,29 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup, conf *config.Config) {
return
}
a, err := query.AlbumByUUID(c.Param("uuid"))
a, err := query.AlbumByUID(c.Param("uid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
return
}
entity.Db().Where("album_uuid = ? AND photo_uuid IN (?)", a.AlbumUUID, f.Photos).Delete(&entity.PhotoAlbum{})
entity.Db().Where("album_uid = ? AND photo_uid IN (?)", a.AlbumUID, f.Photos).Delete(&entity.PhotoAlbum{})
event.Success(fmt.Sprintf("photos removed from %s", a.AlbumName))
PublishAlbumEvent(EntityUpdated, a.AlbumUUID, c)
PublishAlbumEvent(EntityUpdated, a.AlbumUID, c)
c.JSON(http.StatusOK, gin.H{"message": "photos removed from album", "album": a, "photos": f.Photos})
})
}
// GET /albums/:uuid/download
// GET /albums/:uid/download
func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
router.GET("/albums/:uuid/download", func(c *gin.Context) {
router.GET("/albums/:uid/download", func(c *gin.Context) {
start := time.Now()
a, err := query.AlbumByUUID(c.Param("uuid"))
a, err := query.AlbumByUID(c.Param("uid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
@ -339,7 +339,7 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
}
p, _, err := query.Photos(form.PhotoSearch{
Album: a.AlbumUUID,
Album: a.AlbumUID,
Count: 10000,
Offset: 0,
})
@ -411,15 +411,15 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
})
}
// GET /api/v1/albums/:uuid/thumbnail/:type
// GET /api/v1/albums/:uid/thumbnail/:type
//
// Parameters:
// uuid: string Album UUID
// uid: string Album UID
// type: string Thumbnail type, see photoprism.ThumbnailTypes
func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
router.GET("/albums/:uuid/thumbnail/:type", func(c *gin.Context) {
router.GET("/albums/:uid/thumbnail/:type", func(c *gin.Context) {
typeName := c.Param("type")
uuid := c.Param("uuid")
uid := c.Param("uid")
start := time.Now()
thumbType, ok := thumb.Types[typeName]
@ -431,18 +431,18 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
}
gc := service.Cache()
cacheKey := fmt.Sprintf("album-thumbnail:%s:%s", uuid, typeName)
cacheKey := fmt.Sprintf("album-thumbnail:%s:%s", uid, typeName)
if cacheData, ok := gc.Get(cacheKey); ok {
log.Debugf("%s cache hit [%s]", cacheKey, time.Since(start))
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
c.Data(http.StatusOK, "image/jpeg", cacheData.([]byte))
return
}
f, err := query.AlbumThumbByUUID(uuid)
f, err := query.AlbumThumbByUID(uid)
if err != nil {
log.Debugf("album: no photos yet, using generic image for %s", uuid)
log.Debugf("album: no photos yet, using generic image for %s", uid)
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
return
}
@ -494,7 +494,7 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
gc.Set(cacheKey, thumbData, time.Hour)
log.Debugf("%s cached [%s]", cacheKey, time.Since(start))
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
c.Data(http.StatusOK, "image/jpeg", thumbData)
})

View file

@ -31,7 +31,7 @@ func TestGetAlbum(t *testing.T) {
app, router, conf := NewApiTest()
GetAlbum(router, conf)
r := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba8")
val := gjson.Get(r.Body.String(), "AlbumSlug")
val := gjson.Get(r.Body.String(), "Slug")
assert.Equal(t, "holiday-2030", val.String())
assert.Equal(t, http.StatusOK, r.Code)
})
@ -49,34 +49,34 @@ func TestCreateAlbum(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
CreateAlbum(router, conf)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"AlbumName": "New created album", "AlbumNotes": "", "AlbumFavorite": true}`)
val := gjson.Get(r.Body.String(), "AlbumSlug")
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Name": "New created album", "Notes": "", "Favorite": true}`)
val := gjson.Get(r.Body.String(), "Slug")
assert.Equal(t, "new-created-album", val.String())
val2 := gjson.Get(r.Body.String(), "AlbumFavorite")
val2 := gjson.Get(r.Body.String(), "Favorite")
assert.Equal(t, "true", val2.String())
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
CreateAlbum(router, conf)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"AlbumName": 333, "AlbumDescription": "Created via unit test", "AlbumNotes": "", "AlbumFavorite": true}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Name": 333, "Description": "Created via unit test", "Notes": "", "Favorite": true}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
}
func TestUpdateAlbum(t *testing.T) {
app, router, conf := NewApiTest()
CreateAlbum(router, conf)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"AlbumName": "Update", "AlbumDescription": "To be updated", "AlbumNotes": "", "AlbumFavorite": true}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Name": "Update", "Description": "To be updated", "Notes": "", "Favorite": true}`)
assert.Equal(t, http.StatusOK, r.Code)
uuid := gjson.Get(r.Body.String(), "AlbumUUID").String()
uid := gjson.Get(r.Body.String(), "UID").String()
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
UpdateAlbum(router, conf)
r := PerformRequestWithBody(app, "PUT", "/api/v1/albums/"+uuid, `{"AlbumName": "Updated01", "AlbumNotes": "", "AlbumFavorite": false}`)
val := gjson.Get(r.Body.String(), "AlbumSlug")
r := PerformRequestWithBody(app, "PUT", "/api/v1/albums/"+uid, `{"Name": "Updated01", "Notes": "", "Favorite": false}`)
val := gjson.Get(r.Body.String(), "Slug")
assert.Equal(t, "updated01", val.String())
val2 := gjson.Get(r.Body.String(), "AlbumFavorite")
val2 := gjson.Get(r.Body.String(), "Favorite")
assert.Equal(t, "false", val2.String())
assert.Equal(t, http.StatusOK, r.Code)
})
@ -84,14 +84,14 @@ func TestUpdateAlbum(t *testing.T) {
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
UpdateAlbum(router, conf)
r := PerformRequestWithBody(app, "PUT", "/api/v1/albums"+uuid, `{"AlbumName": 333, "AlbumDescription": "Created via unit test", "AlbumNotes": "", "AlbumFavorite": true}`)
r := PerformRequestWithBody(app, "PUT", "/api/v1/albums"+uid, `{"Name": 333, "Description": "Created via unit test", "Notes": "", "Favorite": true}`)
assert.Equal(t, http.StatusNotFound, r.Code)
})
t.Run("not found", func(t *testing.T) {
app, router, conf := NewApiTest()
UpdateAlbum(router, conf)
r := PerformRequestWithBody(app, "PUT", "/api/v1/albums/xxx", `{"AlbumName": "Update03", "AlbumDescription": "Created via unit test", "AlbumNotes": "", "AlbumFavorite": true}`)
r := PerformRequestWithBody(app, "PUT", "/api/v1/albums/xxx", `{"Name": "Update03", "Description": "Created via unit test", "Notes": "", "Favorite": true}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Album not found", val.String())
assert.Equal(t, http.StatusNotFound, r.Code)
@ -100,19 +100,19 @@ func TestUpdateAlbum(t *testing.T) {
func TestDeleteAlbum(t *testing.T) {
app, router, conf := NewApiTest()
CreateAlbum(router, conf)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"AlbumName": "Delete", "AlbumDescription": "To be deleted", "AlbumNotes": "", "AlbumFavorite": true}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Name": "Delete", "Description": "To be deleted", "Notes": "", "Favorite": true}`)
assert.Equal(t, http.StatusOK, r.Code)
uuid := gjson.Get(r.Body.String(), "AlbumUUID").String()
uid := gjson.Get(r.Body.String(), "UID").String()
t.Run("delete existing album", func(t *testing.T) {
app, router, conf := NewApiTest()
DeleteAlbum(router, conf)
r := PerformRequest(app, "DELETE", "/api/v1/albums/"+uuid)
r := PerformRequest(app, "DELETE", "/api/v1/albums/"+uid)
assert.Equal(t, http.StatusOK, r.Code)
val := gjson.Get(r.Body.String(), "AlbumSlug")
val := gjson.Get(r.Body.String(), "Slug")
assert.Equal(t, "delete", val.String())
GetAlbums(router, conf)
r2 := PerformRequest(app, "GET", "/api/v1/albums/"+uuid)
r2 := PerformRequest(app, "GET", "/api/v1/albums/"+uid)
assert.Equal(t, http.StatusNotFound, r2.Code)
})
t.Run("delete not existing album", func(t *testing.T) {
@ -142,7 +142,7 @@ func TestLikeAlbum(t *testing.T) {
assert.Equal(t, http.StatusOK, r.Code)
GetAlbum(router, ctx)
r2 := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba7")
val := gjson.Get(r2.Body.String(), "AlbumFavorite")
val := gjson.Get(r2.Body.String(), "Favorite")
assert.Equal(t, "true", val.String())
})
}
@ -165,7 +165,7 @@ func TestDislikeAlbum(t *testing.T) {
assert.Equal(t, http.StatusOK, r.Code)
GetAlbum(router, conf)
r2 := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba8")
val := gjson.Get(r2.Body.String(), "AlbumFavorite")
val := gjson.Get(r2.Body.String(), "Favorite")
assert.Equal(t, "false", val.String())
})
}
@ -173,14 +173,14 @@ func TestDislikeAlbum(t *testing.T) {
func TestAddPhotosToAlbum(t *testing.T) {
app, router, conf := NewApiTest()
CreateAlbum(router, conf)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"AlbumName": "Add photos", "AlbumDescription": "", "AlbumNotes": "", "AlbumFavorite": true}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Name": "Add photos", "Description": "", "Notes": "", "Favorite": true}`)
assert.Equal(t, http.StatusOK, r.Code)
uuid := gjson.Get(r.Body.String(), "AlbumUUID").String()
uid := gjson.Get(r.Body.String(), "UID").String()
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
AddPhotosToAlbum(router, conf)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums/"+uuid+"/photos", `{"photos": ["pt9jtdre2lvl0y12", "pt9jtdre2lvl0y11"]}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums/"+uid+"/photos", `{"photos": ["pt9jtdre2lvl0y12", "pt9jtdre2lvl0y11"]}`)
val := gjson.Get(r.Body.String(), "message")
assert.Equal(t, "photos added to album", val.String())
assert.Equal(t, http.StatusOK, r.Code)
@ -188,7 +188,7 @@ func TestAddPhotosToAlbum(t *testing.T) {
t.Run("add one photo to album", func(t *testing.T) {
app, router, conf := NewApiTest()
AddPhotosToAlbum(router, conf)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums/"+uuid+"/photos", `{"photos": ["pt9jtdre2lvl0y12"]}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums/"+uid+"/photos", `{"photos": ["pt9jtdre2lvl0y12"]}`)
val := gjson.Get(r.Body.String(), "message")
assert.Equal(t, "photos added to album", val.String())
assert.Equal(t, http.StatusOK, r.Code)
@ -196,7 +196,7 @@ func TestAddPhotosToAlbum(t *testing.T) {
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
AddPhotosToAlbum(router, conf)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums/"+uuid+"/photos", `{"photos": [123, "pt9jtdre2lvl0yxx"]}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums/"+uid+"/photos", `{"photos": [123, "pt9jtdre2lvl0yxx"]}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("not found", func(t *testing.T) {
@ -210,17 +210,17 @@ func TestAddPhotosToAlbum(t *testing.T) {
func TestRemovePhotosFromAlbum(t *testing.T) {
app, router, conf := NewApiTest()
CreateAlbum(router, conf)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"AlbumName": "Remove photos", "AlbumDescription": "", "AlbumNotes": "", "AlbumFavorite": true}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Name": "Remove photos", "Description": "", "Notes": "", "Favorite": true}`)
assert.Equal(t, http.StatusOK, r.Code)
uuid := gjson.Get(r.Body.String(), "AlbumUUID").String()
uid := gjson.Get(r.Body.String(), "UID").String()
AddPhotosToAlbum(router, conf)
r2 := PerformRequestWithBody(app, "POST", "/api/v1/albums/"+uuid+"/photos", `{"photos": ["pt9jtdre2lvl0y12", "pt9jtdre2lvl0y11"]}`)
r2 := PerformRequestWithBody(app, "POST", "/api/v1/albums/"+uid+"/photos", `{"photos": ["pt9jtdre2lvl0y12", "pt9jtdre2lvl0y11"]}`)
assert.Equal(t, http.StatusOK, r2.Code)
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
RemovePhotosFromAlbum(router, conf)
r := PerformRequestWithBody(app, "DELETE", "/api/v1/albums/"+uuid+"/photos", `{"photos": ["pt9jtdre2lvl0y12", "pt9jtdre2lvl0y11"]}`)
r := PerformRequestWithBody(app, "DELETE", "/api/v1/albums/"+uid+"/photos", `{"photos": ["pt9jtdre2lvl0y12", "pt9jtdre2lvl0y11"]}`)
val := gjson.Get(r.Body.String(), "message")
assert.Equal(t, "photos removed from album", val.String())
assert.Equal(t, http.StatusOK, r.Code)
@ -236,7 +236,7 @@ func TestRemovePhotosFromAlbum(t *testing.T) {
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
RemovePhotosFromAlbum(router, conf)
r := PerformRequestWithBody(app, "DELETE", "/api/v1/albums/"+uuid+"/photos", `{"photos": [123, "pt9jtdre2lvl0yxx"]}`)
r := PerformRequestWithBody(app, "DELETE", "/api/v1/albums/"+uid+"/photos", `{"photos": [123, "pt9jtdre2lvl0yxx"]}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("album not found", func(t *testing.T) {

View file

@ -41,7 +41,7 @@ func BatchPhotosArchive(router *gin.RouterGroup, conf *config.Config) {
log.Infof("photos: archiving %#v", f.Photos)
err := entity.Db().Where("photo_uuid IN (?)", f.Photos).Delete(&entity.Photo{}).Error
err := entity.Db().Where("photo_uid IN (?)", f.Photos).Delete(&entity.Photo{}).Error
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed)
@ -87,7 +87,7 @@ func BatchPhotosRestore(router *gin.RouterGroup, conf *config.Config) {
log.Infof("restoring photos: %#v", f.Photos)
err := entity.Db().Unscoped().Model(&entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).
err := entity.Db().Unscoped().Model(&entity.Photo{}).Where("photo_uid IN (?)", f.Photos).
UpdateColumn("deleted_at", gorm.Expr("NULL")).Error
if err != nil {
@ -132,8 +132,8 @@ func BatchAlbumsDelete(router *gin.RouterGroup, conf *config.Config) {
log.Infof("albums: deleting %#v", f.Albums)
entity.Db().Where("album_uuid IN (?)", f.Albums).Delete(&entity.Album{})
entity.Db().Where("album_uuid IN (?)", f.Albums).Delete(&entity.PhotoAlbum{})
entity.Db().Where("album_uid IN (?)", f.Albums).Delete(&entity.Album{})
entity.Db().Where("album_uid IN (?)", f.Albums).Delete(&entity.PhotoAlbum{})
event.Publish("config.updated", event.Data(conf.ClientConfig()))
@ -168,7 +168,7 @@ func BatchPhotosPrivate(router *gin.RouterGroup, conf *config.Config) {
log.Infof("marking photos as private: %#v", f.Photos)
err := entity.Db().Model(entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).UpdateColumn("photo_private", gorm.Expr("IF (`photo_private`, 0, 1)")).Error
err := entity.Db().Model(entity.Photo{}).Where("photo_uid IN (?)", f.Photos).UpdateColumn("photo_private", gorm.Expr("IF (`photo_private`, 0, 1)")).Error
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed)
@ -214,7 +214,7 @@ func BatchLabelsDelete(router *gin.RouterGroup, conf *config.Config) {
log.Infof("labels: deleting %#v", f.Labels)
entity.Db().Where("label_uuid IN (?)", f.Labels).Delete(&entity.Label{})
entity.Db().Where("label_uid IN (?)", f.Labels).Delete(&entity.Label{})
event.Publish("config.updated", event.Data(conf.ClientConfig()))

View file

@ -90,25 +90,25 @@ func TestBatchPhotosRestore(t *testing.T) {
func TestBatchAlbumsDelete(t *testing.T) {
app, router, conf := NewApiTest()
CreateAlbum(router, conf)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"AlbumName": "BatchDelete", "AlbumDescription": "To be deleted", "AlbumNotes": "", "AlbumFavorite": true}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Name": "BatchDelete", "Description": "To be deleted", "Notes": "", "Favorite": true}`)
assert.Equal(t, http.StatusOK, r.Code)
uuid := gjson.Get(r.Body.String(), "AlbumUUID").String()
uid := gjson.Get(r.Body.String(), "UID").String()
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
GetAlbum(router, conf)
r := PerformRequest(app, "GET", "/api/v1/albums/"+uuid)
val := gjson.Get(r.Body.String(), "AlbumSlug")
r := PerformRequest(app, "GET", "/api/v1/albums/"+uid)
val := gjson.Get(r.Body.String(), "Slug")
assert.Equal(t, "batchdelete", val.String())
BatchAlbumsDelete(router, conf)
r2 := PerformRequestWithBody(app, "POST", "/api/v1/batch/albums/delete", fmt.Sprintf(`{"albums": ["%s", "pt9jtdre2lvl0ycc"]}`, uuid))
r2 := PerformRequestWithBody(app, "POST", "/api/v1/batch/albums/delete", fmt.Sprintf(`{"albums": ["%s", "pt9jtdre2lvl0ycc"]}`, uid))
val2 := gjson.Get(r2.Body.String(), "message")
assert.Contains(t, val2.String(), "albums deleted")
assert.Equal(t, http.StatusOK, r2.Code)
r3 := PerformRequest(app, "GET", "/api/v1/albums/"+uuid)
r3 := PerformRequest(app, "GET", "/api/v1/albums/"+uid)
val3 := gjson.Get(r3.Body.String(), "error")
assert.Equal(t, "Album not found", val3.String())
assert.Equal(t, http.StatusNotFound, r3.Code)
@ -135,7 +135,7 @@ func TestBatchPhotosPrivate(t *testing.T) {
GetPhoto(router, conf)
r := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh8")
assert.Equal(t, http.StatusOK, r.Code)
val := gjson.Get(r.Body.String(), "PhotoPrivate")
val := gjson.Get(r.Body.String(), "Private")
assert.Equal(t, "false", val.String())
BatchPhotosPrivate(router, conf)
@ -146,7 +146,7 @@ func TestBatchPhotosPrivate(t *testing.T) {
r3 := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh8")
assert.Equal(t, http.StatusOK, r3.Code)
val3 := gjson.Get(r3.Body.String(), "PhotoPrivate")
val3 := gjson.Get(r3.Body.String(), "Private")
assert.Equal(t, "true", val3.String())
})
t.Run("no photos selected", func(t *testing.T) {
@ -170,7 +170,7 @@ func TestBatchLabelsDelete(t *testing.T) {
app, router, conf := NewApiTest()
GetLabels(router, conf)
r := PerformRequest(app, "GET", "/api/v1/labels?count=15")
val := gjson.Get(r.Body.String(), `#(LabelName=="BatchDelete").LabelSlug`)
val := gjson.Get(r.Body.String(), `#(Name=="BatchDelete").Slug`)
assert.Equal(t, val.String(), "batchdelete")
BatchLabelsDelete(router, conf)
@ -180,7 +180,7 @@ func TestBatchLabelsDelete(t *testing.T) {
assert.Equal(t, http.StatusOK, r2.Code)
r3 := PerformRequest(app, "GET", "/api/v1/labels?count=15")
val3 := gjson.Get(r3.Body.String(), `#(LabelName=="BatchDelete").LabelSlug`)
val3 := gjson.Get(r3.Body.String(), `#(Name=="BatchDelete").Slug`)
assert.Equal(t, val3.String(), "")
})
t.Run("no labels selected", func(t *testing.T) {

View file

@ -13,8 +13,8 @@ import (
)
// TODO: GET /api/v1/dl/file/:hash
// TODO: GET /api/v1/dl/photo/:uuid
// TODO: GET /api/v1/dl/album/:uuid
// TODO: GET /api/v1/dl/photo/:uid
// TODO: GET /api/v1/dl/album/:uid
// GET /api/v1/download/:hash
//

View file

@ -17,8 +17,8 @@ const (
EntityDeleted EntityEvent = "deleted"
)
func PublishPhotoEvent(e EntityEvent, uuid string, c *gin.Context) {
f := form.PhotoSearch{ID: uuid, Merged: true}
func PublishPhotoEvent(e EntityEvent, uid string, c *gin.Context) {
f := form.PhotoSearch{ID: uid, Merged: true}
result, _, err := query.Photos(f)
if err != nil {
@ -30,8 +30,8 @@ func PublishPhotoEvent(e EntityEvent, uuid string, c *gin.Context) {
event.PublishEntities("photos", string(e), result)
}
func PublishAlbumEvent(e EntityEvent, uuid string, c *gin.Context) {
f := form.AlbumSearch{ID: uuid}
func PublishAlbumEvent(e EntityEvent, uid string, c *gin.Context) {
f := form.AlbumSearch{ID: uid}
result, err := query.Albums(f)
if err != nil {
@ -43,8 +43,8 @@ func PublishAlbumEvent(e EntityEvent, uuid string, c *gin.Context) {
event.PublishEntities("albums", string(e), result)
}
func PublishLabelEvent(e EntityEvent, uuid string, c *gin.Context) {
f := form.LabelSearch{ID: uuid}
func PublishLabelEvent(e EntityEvent, uid string, c *gin.Context) {
f := form.LabelSearch{ID: uid}
result, err := query.Labels(f)
if err != nil {

View file

@ -33,18 +33,18 @@ func GetFile(router *gin.RouterGroup, conf *config.Config) {
})
}
// POST /api/v1/files/:uuid/link
// POST /api/v1/files/:uid/link
//
// Parameters:
// uuid: string SHA-1 hash of the file
// uid: string SHA-1 hash of the file
func LinkFile(router *gin.RouterGroup, conf *config.Config) {
router.POST("/files/:uuid/link", func(c *gin.Context) {
router.POST("/files/:uid/link", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
m, err := query.FileByUUID(c.Param("uuid"))
m, err := query.FileByUID(c.Param("uid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrFileNotFound)

View file

@ -17,7 +17,7 @@ func TestGetFile(t *testing.T) {
r := PerformRequest(app, "GET", "/api/v1/files/2cad9168fa6acc5c5c2965ddf6ec465ca42fd818")
assert.Equal(t, http.StatusOK, r.Code)
val := gjson.Get(r.Body.String(), "FileName")
val := gjson.Get(r.Body.String(), "Name")
assert.Equal(t, "exampleFileName.jpg", val.String())
})
t.Run("search for not existing file", func(t *testing.T) {
@ -32,7 +32,7 @@ func TestLinkFile(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, ctx := NewApiTest()
LinkFile(router, ctx)
r := PerformRequestWithBody(app, "POST", "/api/v1/files/ft9es39w45bnlqdw/link", `{"password": "foobar123", "expires": 0, "edit": true}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/files/ft9es39w45bnlqdw/link", `{"Password": "foobar123", "Expires": 0, "CanEdit": true}`)
var label entity.Label
@ -54,7 +54,7 @@ func TestLinkFile(t *testing.T) {
t.Run("file not found", func(t *testing.T) {
app, router, ctx := NewApiTest()
LinkFile(router, ctx)
r := PerformRequestWithBody(app, "POST", "/api/v1/files/xxx/link", `{"password": "foobar", "expires": 0, "edit": true}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/files/xxx/link", `{"Password": "foobar", "Expires": 0, "CanEdit": true}`)
assert.Equal(t, http.StatusNotFound, r.Code)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "File not found", val.String())
@ -62,7 +62,7 @@ func TestLinkFile(t *testing.T) {
t.Run("invalid request", func(t *testing.T) {
app, router, ctx := NewApiTest()
LinkFile(router, ctx)
r := PerformRequestWithBody(app, "POST", "/api/v1/files/ft9es39w45bnlqdw/link", `{"xxx": 123, "expires": 0, "edit": "xxx"}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/files/ft9es39w45bnlqdw/link", `{"xxx": 123, "Expires": 0, "CanEdit": "xxx"}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
}

View file

@ -12,8 +12,8 @@ import (
)
// GetFolders is a reusable request handler for directory listings (GET /api/v1/folders/*).
func GetFolders(router *gin.RouterGroup, conf *config.Config, root, pathName string) {
router.GET("/folders/" + root, func(c *gin.Context) {
func GetFolders(router *gin.RouterGroup, conf *config.Config, root, pathName string) {
router.GET("/folders/"+root, func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
@ -26,7 +26,7 @@ func GetFolders(router *gin.RouterGroup, conf *config.Config, root, pathName str
cacheKey := fmt.Sprintf("folders:%s:%t", pathName, recursive)
if cacheData, ok := gc.Get(cacheKey); ok {
log.Debugf("%s cache hit [%s]", cacheKey, time.Since(start))
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
c.JSON(http.StatusOK, cacheData.([]entity.Folder))
return
}
@ -38,7 +38,7 @@ func GetFolders(router *gin.RouterGroup, conf *config.Config, root, pathName str
} else {
gc.Set(cacheKey, folders, time.Minute*5)
log.Debugf("%s cached [%s]", cacheKey, time.Since(start))
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
}
c.JSON(http.StatusOK, folders)

View file

@ -29,7 +29,7 @@ func TestGetFoldersOriginals(t *testing.T) {
t.Fatal(err)
}
if len(folders) != len(expected){
if len(folders) != len(expected) {
t.Fatalf("response contains %d folders", len(folders))
}
@ -43,7 +43,7 @@ func TestGetFoldersOriginals(t *testing.T) {
assert.Equal(t, entity.TypeDefault, folder.FolderType)
assert.Equal(t, entity.SortOrderName, folder.FolderOrder)
assert.Equal(t, entity.FolderRootOriginals, folder.Root)
assert.Equal(t, "", folder.FolderUUID)
assert.IsType(t, "", folder.FolderUID)
assert.Equal(t, false, folder.FolderFavorite)
assert.Equal(t, false, folder.FolderHidden)
assert.Equal(t, false, folder.FolderIgnore)
@ -69,7 +69,7 @@ func TestGetFoldersOriginals(t *testing.T) {
t.Fatal(err)
}
if len(folders) != len(expected){
if len(folders) != len(expected) {
t.Fatalf("response contains %d folders", len(folders))
}
@ -78,7 +78,7 @@ func TestGetFoldersOriginals(t *testing.T) {
assert.Equal(t, entity.TypeDefault, folder.FolderType)
assert.Equal(t, entity.SortOrderName, folder.FolderOrder)
assert.Equal(t, entity.FolderRootOriginals, folder.Root)
assert.Equal(t, "", folder.FolderUUID)
assert.IsType(t, "", folder.FolderUID)
assert.Equal(t, false, folder.FolderFavorite)
assert.Equal(t, false, folder.FolderHidden)
assert.Equal(t, false, folder.FolderIgnore)
@ -108,7 +108,7 @@ func TestGetFoldersImport(t *testing.T) {
t.Fatal(err)
}
if len(folders) != len(expected){
if len(folders) != len(expected) {
t.Fatalf("response contains %d folders", len(folders))
}
@ -124,7 +124,7 @@ func TestGetFoldersImport(t *testing.T) {
assert.Equal(t, entity.TypeDefault, folder.FolderType)
assert.Equal(t, entity.SortOrderName, folder.FolderOrder)
assert.Equal(t, entity.FolderRootImport, folder.Root)
assert.Equal(t, "", folder.FolderUUID)
assert.IsType(t, "", folder.FolderUID)
assert.Equal(t, false, folder.FolderFavorite)
assert.Equal(t, false, folder.FolderHidden)
assert.Equal(t, false, folder.FolderIgnore)
@ -140,6 +140,7 @@ func TestGetFoldersImport(t *testing.T) {
if err != nil {
t.Fatal(err)
}
GetFoldersImport(router, conf)
r := PerformRequest(app, "GET", "/api/v1/folders/import?recursive=true")
var folders []entity.Folder
@ -149,7 +150,7 @@ func TestGetFoldersImport(t *testing.T) {
t.Fatal(err)
}
if len(folders) != len(expected){
if len(folders) != len(expected) {
t.Fatalf("response contains %d folders", len(folders))
}
@ -158,7 +159,7 @@ func TestGetFoldersImport(t *testing.T) {
assert.Equal(t, entity.TypeDefault, folder.FolderType)
assert.Equal(t, entity.SortOrderName, folder.FolderOrder)
assert.Equal(t, entity.FolderRootImport, folder.Root)
assert.Equal(t, "", folder.FolderUUID)
assert.IsType(t, "", folder.FolderUID)
assert.Equal(t, false, folder.FolderFavorite)
assert.Equal(t, false, folder.FolderHidden)
assert.Equal(t, false, folder.FolderIgnore)

View file

@ -4,6 +4,7 @@ import (
"net/http"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/txt"
@ -60,17 +61,30 @@ func GetGeo(router *gin.RouterGroup, conf *config.Config) {
bboxMax(2, p.Lng())
bboxMax(3, p.Lat())
props := gin.H{
"UID": p.PhotoUID,
"Hash": p.FileHash,
"Width": p.FileWidth,
"Height": p.FileHeight,
"TakenAt": p.TakenAt,
"Title": p.PhotoTitle,
}
if p.PhotoDescription != "" {
props["Description"] = p.PhotoDescription
}
if p.PhotoType != entity.TypeImage && p.PhotoType != entity.TypeDefault {
props["Type"] = p.PhotoType
}
if p.PhotoFavorite {
props["Favorite"] = true
}
feat := geojson.NewPointFeature([]float64{p.Lng(), p.Lat()})
feat.ID = p.ID
feat.Properties = gin.H{
"PhotoUUID": p.PhotoUUID,
"PhotoTitle": p.PhotoTitle,
"PhotoFavorite": p.PhotoFavorite,
"FileHash": p.FileHash,
"FileWidth": p.FileWidth,
"FileHeight": p.FileHeight,
"TakenAt": p.TakenAt,
}
feat.Properties = props
fc.AddFeature(feat)
}

View file

@ -53,9 +53,9 @@ func GetLabels(router *gin.RouterGroup, conf *config.Config) {
})
}
// PUT /api/v1/labels/:uuid
// PUT /api/v1/labels/:uid
func UpdateLabel(router *gin.RouterGroup, conf *config.Config) {
router.PUT("/labels/:uuid", func(c *gin.Context) {
router.PUT("/labels/:uid", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
@ -68,8 +68,8 @@ func UpdateLabel(router *gin.RouterGroup, conf *config.Config) {
return
}
id := c.Param("uuid")
m, err := query.LabelByUUID(id)
id := c.Param("uid")
m, err := query.LabelByUID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrLabelNotFound)
@ -87,19 +87,19 @@ func UpdateLabel(router *gin.RouterGroup, conf *config.Config) {
})
}
// POST /api/v1/labels/:uuid/like
// POST /api/v1/labels/:uid/like
//
// Parameters:
// uuid: string Label UUID
// uid: string Label UID
func LikeLabel(router *gin.RouterGroup, conf *config.Config) {
router.POST("/labels/:uuid/like", func(c *gin.Context) {
router.POST("/labels/:uid/like", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
id := c.Param("uuid")
label, err := query.LabelByUUID(id)
id := c.Param("uid")
label, err := query.LabelByUID(id)
if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": txt.UcFirst(err.Error())})
@ -121,19 +121,19 @@ func LikeLabel(router *gin.RouterGroup, conf *config.Config) {
})
}
// DELETE /api/v1/labels/:uuid/like
// DELETE /api/v1/labels/:uid/like
//
// Parameters:
// uuid: string Label UUID
// uid: string Label UID
func DislikeLabel(router *gin.RouterGroup, conf *config.Config) {
router.DELETE("/labels/:uuid/like", func(c *gin.Context) {
router.DELETE("/labels/:uid/like", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
id := c.Param("uuid")
label, err := query.LabelByUUID(id)
id := c.Param("uid")
label, err := query.LabelByUID(id)
if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": txt.UcFirst(err.Error())})
@ -155,17 +155,17 @@ func DislikeLabel(router *gin.RouterGroup, conf *config.Config) {
})
}
// GET /api/v1/labels/:uuid/thumbnail/:type
// GET /api/v1/labels/:uid/thumbnail/:type
//
// Example: /api/v1/labels/cheetah/thumbnail/tile_500
//
// Parameters:
// uuid: string Label UUID
// uid: string Label UID
// type: string Thumbnail type, see photoprism.ThumbnailTypes
func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
router.GET("/labels/:uuid/thumbnail/:type", func(c *gin.Context) {
router.GET("/labels/:uid/thumbnail/:type", func(c *gin.Context) {
typeName := c.Param("type")
labelUUID := c.Param("uuid")
labelUID := c.Param("uid")
start := time.Now()
thumbType, ok := thumb.Types[typeName]
@ -177,15 +177,15 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
}
gc := service.Cache()
cacheKey := fmt.Sprintf("label-thumbnail:%s:%s", labelUUID, typeName)
cacheKey := fmt.Sprintf("label-thumbnail:%s:%s", labelUID, typeName)
if cacheData, ok := gc.Get(cacheKey); ok {
log.Debugf("%s cache hit [%s]", cacheKey, time.Since(start))
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
c.Data(http.StatusOK, "image/jpeg", cacheData.([]byte))
return
}
f, err := query.LabelThumbByUUID(labelUUID)
f, err := query.LabelThumbByUID(labelUID)
if err != nil {
log.Errorf(err.Error())
@ -238,7 +238,7 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
gc.Set(cacheKey, thumbData, time.Hour*4)
log.Debugf("%s cached [%s]", cacheKey, time.Since(start))
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
c.Data(http.StatusOK, "image/jpeg", thumbData)
})

View file

@ -29,8 +29,8 @@ func TestUpdateLabel(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
UpdateLabel(router, conf)
r := PerformRequestWithBody(app, "PUT", "/api/v1/labels/lt9k3pw1wowuy3c7", `{"LabelName": "Updated01", "LabelPriority": 2}`)
val := gjson.Get(r.Body.String(), "LabelName")
r := PerformRequestWithBody(app, "PUT", "/api/v1/labels/lt9k3pw1wowuy3c7", `{"Name": "Updated01", "Priority": 2}`)
val := gjson.Get(r.Body.String(), "Name")
assert.Equal(t, "Updated01", val.String())
val2 := gjson.Get(r.Body.String(), "CustomSlug")
assert.Equal(t, "updated01", val2.String())
@ -40,7 +40,7 @@ func TestUpdateLabel(t *testing.T) {
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
UpdateLabel(router, conf)
r := PerformRequestWithBody(app, "PUT", "/api/v1/labels/lt9k3pw1wowuy3c7", `{"LabelName": 123, "LabelPriority": 4, "Uncertainty": 80}`)
r := PerformRequestWithBody(app, "PUT", "/api/v1/labels/lt9k3pw1wowuy3c7", `{"Name": 123, "Priority": 4, "Uncertainty": 80}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
@ -66,7 +66,7 @@ func TestLikeLabel(t *testing.T) {
GetLabels(router, ctx)
r2 := PerformRequest(app, "GET", "/api/v1/labels?count=1&q=likeLabel")
t.Log(r2.Body.String())
val := gjson.Get(r2.Body.String(), `#(LabelSlug=="likeLabel").LabelFavorite`)
val := gjson.Get(r2.Body.String(), `#(Slug=="likeLabel").Favorite`)
assert.Equal(t, "false", val.String())
LikeLabel(router, ctx)
r := PerformRequest(app, "POST", "/api/v1/labels/lt9k3pw1wowuy3c9/like")
@ -74,7 +74,7 @@ func TestLikeLabel(t *testing.T) {
assert.Equal(t, http.StatusOK, r.Code)
r3 := PerformRequest(app, "GET", "/api/v1/labels?count=1&q=likeLabel")
t.Log(r3.Body.String())
val2 := gjson.Get(r3.Body.String(), `#(LabelSlug=="likeLabel").LabelFavorite`)
val2 := gjson.Get(r3.Body.String(), `#(Slug=="likeLabel").Favorite`)
assert.Equal(t, "true", val2.String())
})
@ -93,7 +93,7 @@ func TestDislikeLabel(t *testing.T) {
app, router, ctx := NewApiTest()
GetLabels(router, ctx)
r2 := PerformRequest(app, "GET", "/api/v1/labels?count=1&q=landscape")
val := gjson.Get(r2.Body.String(), `#(LabelSlug=="landscape").LabelFavorite`)
val := gjson.Get(r2.Body.String(), `#(Slug=="landscape").Favorite`)
assert.Equal(t, "true", val.String())
DislikeLabel(router, ctx)
@ -102,7 +102,7 @@ func TestDislikeLabel(t *testing.T) {
assert.Equal(t, http.StatusOK, r.Code)
r3 := PerformRequest(app, "GET", "/api/v1/labels?count=1&q=landscape")
val2 := gjson.Get(r3.Body.String(), `#(LabelSlug=="landscape").LabelFavorite`)
val2 := gjson.Get(r3.Body.String(), `#(Slug=="landscape").Favorite`)
assert.Equal(t, "false", val2.String())
})
}

View file

@ -31,15 +31,15 @@ func newLink(c *gin.Context) (link entity.Link, err error) {
return link, nil
}
// POST /api/v1/albums/:uuid/link
// POST /api/v1/albums/:uid/link
func LinkAlbum(router *gin.RouterGroup, conf *config.Config) {
router.POST("/albums/:uuid/link", func(c *gin.Context) {
router.POST("/albums/:uid/link", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
m, err := query.AlbumByUUID(c.Param("uuid"))
m, err := query.AlbumByUID(c.Param("uid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
@ -59,15 +59,15 @@ func LinkAlbum(router *gin.RouterGroup, conf *config.Config) {
})
}
// POST /api/v1/photos/:uuid/link
// POST /api/v1/photos/:uid/link
func LinkPhoto(router *gin.RouterGroup, conf *config.Config) {
router.POST("/photos/:uuid/link", func(c *gin.Context) {
router.POST("/photos/:uid/link", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
m, err := query.PhotoByUUID(c.Param("uuid"))
m, err := query.PhotoByUID(c.Param("uid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@ -87,15 +87,15 @@ func LinkPhoto(router *gin.RouterGroup, conf *config.Config) {
})
}
// POST /api/v1/labels/:uuid/link
// POST /api/v1/labels/:uid/link
func LinkLabel(router *gin.RouterGroup, conf *config.Config) {
router.POST("/labels/:uuid/link", func(c *gin.Context) {
router.POST("/labels/:uid/link", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
m, err := query.LabelByUUID(c.Param("uuid"))
m, err := query.LabelByUID(c.Param("uid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrLabelNotFound)

View file

@ -18,7 +18,7 @@ func TestLinkAlbum(t *testing.T) {
LinkAlbum(router, ctx)
result1 := PerformRequestWithBody(app, "POST", "/api/v1/albums/at9lxuqxpogaaba7/link", `{"password": "foobar", "expires": 0, "edit": true}`)
result1 := PerformRequestWithBody(app, "POST", "/api/v1/albums/at9lxuqxpogaaba7/link", `{"Password": "foobar", "Expires": 0, "CanEdit": true}`)
assert.Equal(t, http.StatusOK, result1.Code)
if err := json.Unmarshal(result1.Body.Bytes(), &album); err != nil {
@ -31,12 +31,12 @@ func TestLinkAlbum(t *testing.T) {
link := album.Links[0]
assert.Equal(t, "foobar", link.LinkPassword)
assert.Equal(t, true, link.CanEdit)
assert.Nil(t, link.LinkExpires)
assert.False(t, link.CanComment)
assert.True(t, link.CanEdit)
result2 := PerformRequestWithBody(app, "POST", "/api/v1/albums/at9lxuqxpogaaba7/link", `{"password": "", "expires": 3600}`)
result2 := PerformRequestWithBody(app, "POST", "/api/v1/albums/at9lxuqxpogaaba7/link", `{"Password": "", "Expires": 3600}`)
assert.Equal(t, http.StatusOK, result2.Code)
@ -54,7 +54,7 @@ func TestLinkAlbum(t *testing.T) {
t.Run("album not found", func(t *testing.T) {
app, router, ctx := NewApiTest()
LinkAlbum(router, ctx)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums/xxx/link", `{"password": "foobar", "expires": 0, "edit": true}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums/xxx/link", `{"Password": "foobar", "Expires": 0, "CanEdit": true}`)
assert.Equal(t, http.StatusNotFound, r.Code)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Album not found", val.String())
@ -62,7 +62,7 @@ func TestLinkAlbum(t *testing.T) {
t.Run("invalid request", func(t *testing.T) {
app, router, ctx := NewApiTest()
LinkAlbum(router, ctx)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums/at9lxuqxpogaaba7/link", `{"xxx": 123, "expires": 0, "edit": "xxx"}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/albums/at9lxuqxpogaaba7/link", `{"xxx": 123, "Expires": 0, "CanEdit": "xxx"}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
}
@ -75,7 +75,7 @@ func TestLinkPhoto(t *testing.T) {
LinkPhoto(router, ctx)
result1 := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh7/link", `{"password": "foobar", "expires": 0, "edit": true}`)
result1 := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh7/link", `{"Password": "foobar", "Expires": 0, "CanEdit": true}`)
assert.Equal(t, http.StatusOK, result1.Code)
if err := json.Unmarshal(result1.Body.Bytes(), &photo); err != nil {
@ -93,7 +93,7 @@ func TestLinkPhoto(t *testing.T) {
assert.False(t, link.CanComment)
assert.True(t, link.CanEdit)
result2 := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh7/link", `{"password": "", "expires": 3600}`)
result2 := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh7/link", `{"Password": "", "Expires": 3600}`)
assert.Equal(t, http.StatusOK, result2.Code)
@ -108,7 +108,7 @@ func TestLinkPhoto(t *testing.T) {
t.Run("photo not found", func(t *testing.T) {
app, router, ctx := NewApiTest()
LinkPhoto(router, ctx)
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/xxx/link", `{"password": "foobar", "expires": 0, "edit": true}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/xxx/link", `{"Password": "foobar", "Expires": 0, "CanEdit": true}`)
assert.Equal(t, http.StatusNotFound, r.Code)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Photo not found", val.String())
@ -116,7 +116,7 @@ func TestLinkPhoto(t *testing.T) {
t.Run("invalid request", func(t *testing.T) {
app, router, ctx := NewApiTest()
LinkPhoto(router, ctx)
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh7/link", `{"xxx": 123, "expires": 0, "edit": "xxx"}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh7/link", `{"xxx": 123, "Expires": 0, "CanEdit": "xxx"}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
}
@ -129,7 +129,7 @@ func TestLinkLabel(t *testing.T) {
LinkLabel(router, ctx)
result1 := PerformRequestWithBody(app, "POST", "/api/v1/labels/lt9k3pw1wowuy3c2/link", `{"password": "foobar", "expires": 0, "edit": true}`)
result1 := PerformRequestWithBody(app, "POST", "/api/v1/labels/lt9k3pw1wowuy3c2/link", `{"Password": "foobar", "Expires": 0, "CanEdit": true}`)
assert.Equal(t, http.StatusOK, result1.Code)
if err := json.Unmarshal(result1.Body.Bytes(), &label); err != nil {
@ -147,7 +147,7 @@ func TestLinkLabel(t *testing.T) {
assert.False(t, link.CanComment)
assert.True(t, link.CanEdit)
result2 := PerformRequestWithBody(app, "POST", "/api/v1/labels/lt9k3pw1wowuy3c2/link", `{"password": "", "expires": 3600}`)
result2 := PerformRequestWithBody(app, "POST", "/api/v1/labels/lt9k3pw1wowuy3c2/link", `{"Password": "", "Expires": 3600}`)
assert.Equal(t, http.StatusOK, result2.Code)
@ -162,7 +162,7 @@ func TestLinkLabel(t *testing.T) {
t.Run("label not found", func(t *testing.T) {
app, router, ctx := NewApiTest()
LinkLabel(router, ctx)
r := PerformRequestWithBody(app, "POST", "/api/v1/labels/xxx/link", `{"password": "foobar", "expires": 0, "edit": true}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/labels/xxx/link", `{"Password": "foobar", "Expires": 0, "CanEdit": true}`)
assert.Equal(t, http.StatusNotFound, r.Code)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Label not found", val.String())
@ -170,7 +170,7 @@ func TestLinkLabel(t *testing.T) {
t.Run("invalid request", func(t *testing.T) {
app, router, ctx := NewApiTest()
LinkLabel(router, ctx)
r := PerformRequestWithBody(app, "POST", "/api/v1/labels/lt9k3pw1wowuy3c2/link", `{"xxx": 123, "expires": 0, "edit": "xxx"}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/labels/lt9k3pw1wowuy3c2/link", `{"xxx": 123, "Expires": 0, "CanEdit": "xxx"}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
}

View file

@ -29,18 +29,18 @@ func SavePhotoAsYaml(p entity.Photo, conf *config.Config) {
}
}
// GET /api/v1/photos/:uuid
// GET /api/v1/photos/:uid
//
// Parameters:
// uuid: string PhotoUUID as returned by the API
// uid: string PhotoUID as returned by the API
func GetPhoto(router *gin.RouterGroup, conf *config.Config) {
router.GET("/photos/:uuid", func(c *gin.Context) {
router.GET("/photos/:uid", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
p, err := query.PreloadPhotoByUUID(c.Param("uuid"))
p, err := query.PreloadPhotoByUID(c.Param("uid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@ -51,16 +51,16 @@ func GetPhoto(router *gin.RouterGroup, conf *config.Config) {
})
}
// PUT /api/v1/photos/:uuid
// PUT /api/v1/photos/:uid
func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
router.PUT("/photos/:uuid", func(c *gin.Context) {
router.PUT("/photos/:uid", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
uuid := c.Param("uuid")
m, err := query.PhotoByUUID(uuid)
uid := c.Param("uid")
m, err := query.PhotoByUID(uid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@ -91,11 +91,11 @@ func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
return
}
PublishPhotoEvent(EntityUpdated, uuid, c)
PublishPhotoEvent(EntityUpdated, uid, c)
event.Success("photo saved")
p, err := query.PreloadPhotoByUUID(uuid)
p, err := query.PreloadPhotoByUID(uid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@ -108,13 +108,13 @@ func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
})
}
// GET /api/v1/photos/:uuid/download
// GET /api/v1/photos/:uid/download
//
// Parameters:
// uuid: string PhotoUUID as returned by the API
// uid: string PhotoUID as returned by the API
func GetPhotoDownload(router *gin.RouterGroup, conf *config.Config) {
router.GET("/photos/:uuid/download", func(c *gin.Context) {
f, err := query.FileByPhotoUUID(c.Param("uuid"))
router.GET("/photos/:uid/download", func(c *gin.Context) {
f, err := query.FileByPhotoUID(c.Param("uid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@ -124,7 +124,7 @@ func GetPhotoDownload(router *gin.RouterGroup, conf *config.Config) {
fileName := path.Join(conf.OriginalsPath(), f.FileName)
if !fs.FileExists(fileName) {
log.Errorf("could not find original: %s", c.Param("uuid"))
log.Errorf("could not find original: %s", c.Param("uid"))
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore
@ -141,18 +141,18 @@ func GetPhotoDownload(router *gin.RouterGroup, conf *config.Config) {
})
}
// GET /api/v1/photos/:uuid/yaml
// GET /api/v1/photos/:uid/yaml
//
// Parameters:
// uuid: string PhotoUUID as returned by the API
// uid: string PhotoUID as returned by the API
func GetPhotoYaml(router *gin.RouterGroup, conf *config.Config) {
router.GET("/photos/:uuid/yaml", func(c *gin.Context) {
router.GET("/photos/:uid/yaml", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
p, err := query.PreloadPhotoByUUID(c.Param("uuid"))
p, err := query.PreloadPhotoByUID(c.Param("uid"))
if err != nil {
c.AbortWithStatus(http.StatusNotFound)
@ -167,26 +167,26 @@ func GetPhotoYaml(router *gin.RouterGroup, conf *config.Config) {
}
if c.Query("download") != "" {
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", c.Param("uuid")+fs.YamlExt))
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", c.Param("uid")+fs.YamlExt))
}
c.Data(http.StatusOK, "text/x-yaml; charset=utf-8", data)
})
}
// POST /api/v1/photos/:uuid/like
// POST /api/v1/photos/:uid/like
//
// Parameters:
// uuid: string PhotoUUID as returned by the API
// uid: string PhotoUID as returned by the API
func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
router.POST("/photos/:uuid/like", func(c *gin.Context) {
router.POST("/photos/:uid/like", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
id := c.Param("uuid")
m, err := query.PhotoByUUID(id)
id := c.Param("uid")
m, err := query.PhotoByUID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@ -209,19 +209,19 @@ func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
})
}
// DELETE /api/v1/photos/:uuid/like
// DELETE /api/v1/photos/:uid/like
//
// Parameters:
// uuid: string PhotoUUID as returned by the API
// uid: string PhotoUID as returned by the API
func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
router.DELETE("/photos/:uuid/like", func(c *gin.Context) {
router.DELETE("/photos/:uid/like", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
id := c.Param("uuid")
m, err := query.PhotoByUUID(id)
id := c.Param("uid")
m, err := query.PhotoByUID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@ -244,31 +244,31 @@ func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
})
}
// POST /api/v1/photos/:uuid/primary/:file_uuid
// POST /api/v1/photos/:uid/primary/:file_uid
//
// Parameters:
// uuid: string PhotoUUID as returned by the API
// uid: string PhotoUID as returned by the API
func SetPhotoPrimary(router *gin.RouterGroup, conf *config.Config) {
router.POST("/photos/:uuid/primary/:file_uuid", func(c *gin.Context) {
router.POST("/photos/:uid/primary/:file_uid", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
uuid := c.Param("uuid")
fileUUID := c.Param("file_uuid")
err := query.SetPhotoPrimary(uuid, fileUUID)
uid := c.Param("uid")
fileUID := c.Param("file_uid")
err := query.SetPhotoPrimary(uid, fileUID)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
return
}
PublishPhotoEvent(EntityUpdated, uuid, c)
PublishPhotoEvent(EntityUpdated, uid, c)
event.Success("photo saved")
p, err := query.PreloadPhotoByUUID(uuid)
p, err := query.PreloadPhotoByUID(uid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)

View file

@ -13,18 +13,18 @@ import (
"github.com/photoprism/photoprism/pkg/txt"
)
// POST /api/v1/photos/:uuid/label
// POST /api/v1/photos/:uid/label
//
// Parameters:
// uuid: string PhotoUUID as returned by the API
// uid: string PhotoUID as returned by the API
func AddPhotoLabel(router *gin.RouterGroup, conf *config.Config) {
router.POST("/photos/:uuid/label", func(c *gin.Context) {
router.POST("/photos/:uid/label", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
m, err := query.PhotoByUUID(c.Param("uuid"))
m, err := query.PhotoByUID(c.Param("uid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@ -59,7 +59,7 @@ func AddPhotoLabel(router *gin.RouterGroup, conf *config.Config) {
entity.Db().Save(&lm)
p, err := query.PreloadPhotoByUUID(c.Param("uuid"))
p, err := query.PreloadPhotoByUID(c.Param("uid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@ -71,7 +71,7 @@ func AddPhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return
}
PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c)
PublishPhotoEvent(EntityUpdated, c.Param("uid"), c)
event.Success("label updated")
@ -79,19 +79,19 @@ func AddPhotoLabel(router *gin.RouterGroup, conf *config.Config) {
})
}
// DELETE /api/v1/photos/:uuid/label/:id
// DELETE /api/v1/photos/:uid/label/:id
//
// Parameters:
// uuid: string PhotoUUID as returned by the API
// uid: string PhotoUID as returned by the API
// id: int LabelId as returned by the API
func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
router.DELETE("/photos/:uuid/label/:id", func(c *gin.Context) {
router.DELETE("/photos/:uid/label/:id", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
m, err := query.PhotoByUUID(c.Param("uuid"))
m, err := query.PhotoByUID(c.Param("uid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@ -119,7 +119,7 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
entity.Db().Save(&label)
}
p, err := query.PreloadPhotoByUUID(c.Param("uuid"))
p, err := query.PreloadPhotoByUID(c.Param("uid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@ -131,7 +131,7 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return
}
PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c)
PublishPhotoEvent(EntityUpdated, c.Param("uid"), c)
event.Success("label removed")
@ -139,13 +139,13 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
})
}
// PUT /api/v1/photos/:uuid/label/:id
// PUT /api/v1/photos/:uid/label/:id
//
// Parameters:
// uuid: string PhotoUUID as returned by the API
// uid: string PhotoUID as returned by the API
// id: int LabelId as returned by the API
func UpdatePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
router.PUT("/photos/:uuid/label/:id", func(c *gin.Context) {
router.PUT("/photos/:uid/label/:id", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
@ -153,7 +153,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
// TODO: Code clean-up, simplify
m, err := query.PhotoByUUID(c.Param("uuid"))
m, err := query.PhotoByUID(c.Param("uid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@ -184,7 +184,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return
}
p, err := query.PreloadPhotoByUUID(c.Param("uuid"))
p, err := query.PreloadPhotoByUID(c.Param("uid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@ -196,7 +196,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return
}
PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c)
PublishPhotoEvent(EntityUpdated, c.Param("uid"), c)
event.Success("label saved")

View file

@ -12,14 +12,14 @@ func TestAddPhotoLabel(t *testing.T) {
t.Run("add new label", func(t *testing.T) {
app, router, ctx := NewApiTest()
AddPhotoLabel(router, ctx)
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh8/label", `{"LabelName": "testAddLabel", "Uncertainty": 95, "LabelPriority": 2}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh8/label", `{"Name": "testAddLabel", "Uncertainty": 95, "Priority": 2}`)
assert.Equal(t, http.StatusOK, r.Code)
assert.Contains(t, r.Body.String(), "TestAddLabel")
})
t.Run("add existing label", func(t *testing.T) {
app, router, ctx := NewApiTest()
AddPhotoLabel(router, ctx)
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh8/label", `{"LabelName": "Flower", "Uncertainty": 10, "LabelPriority": 2}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh8/label", `{"Name": "Flower", "Uncertainty": 10, "Priority": 2}`)
assert.Equal(t, http.StatusOK, r.Code)
val := gjson.Get(r.Body.String(), "Labels.#(LabelID==1000001).Uncertainty")
assert.Equal(t, "10", val.String())
@ -27,7 +27,7 @@ func TestAddPhotoLabel(t *testing.T) {
t.Run("not found", func(t *testing.T) {
app, router, ctx := NewApiTest()
AddPhotoLabel(router, ctx)
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/xxx/label", `{"LabelName": "Flower", "Uncertainty": 10, "LabelPriority": 2}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/xxx/label", `{"Name": "Flower", "Uncertainty": 10, "Priority": 2}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Photo not found", val.String())
assert.Equal(t, http.StatusNotFound, r.Code)
@ -35,7 +35,7 @@ func TestAddPhotoLabel(t *testing.T) {
t.Run("invalid request", func(t *testing.T) {
app, router, ctx := NewApiTest()
AddPhotoLabel(router, ctx)
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh8/label", `{"LabelName": 123, "Uncertainty": 10, "LabelPriority": 2}`)
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh8/label", `{"Name": 123, "Uncertainty": 10, "Priority": 2}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
@ -94,15 +94,15 @@ func TestUpdatePhotoLabel(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, ctx := NewApiTest()
UpdatePhotoLabel(router, ctx)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/pt9jtdre2lvl0yh0/label/1000006", `{"Label": {"LabelName": "NewLabelName"}}`)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/pt9jtdre2lvl0yh0/label/1000006", `{"Label": {"Name": "NewLabelName"}}`)
assert.Equal(t, http.StatusOK, r.Code)
val := gjson.Get(r.Body.String(), "PhotoTitle")
val := gjson.Get(r.Body.String(), "Title")
assert.Contains(t, val.String(), "NewLabelName")
})
t.Run("photo not found", func(t *testing.T) {
app, router, ctx := NewApiTest()
UpdatePhotoLabel(router, ctx)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/xxx/label/1000006", `{"Label": {"LabelName": "NewLabelName"}}`)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/xxx/label/1000006", `{"Label": {"Name": "NewLabelName"}}`)
assert.Equal(t, http.StatusNotFound, r.Code)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Photo not found", val.String())
@ -110,19 +110,19 @@ func TestUpdatePhotoLabel(t *testing.T) {
t.Run("label not existing", func(t *testing.T) {
app, router, ctx := NewApiTest()
UpdatePhotoLabel(router, ctx)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/pt9jtdre2lvl0yh0/label/9000006", `{"Label": {"LabelName": "NewLabelName"}}`)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/pt9jtdre2lvl0yh0/label/9000006", `{"Label": {"Name": "NewLabelName"}}`)
assert.Equal(t, http.StatusNotFound, r.Code)
})
t.Run("label not linked to photo", func(t *testing.T) {
app, router, ctx := NewApiTest()
UpdatePhotoLabel(router, ctx)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/pt9jtdre2lvl0yh0/label/1000005", `{"Label": {"LabelName": "NewLabelName"}}`)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/pt9jtdre2lvl0yh0/label/1000005", `{"Label": {"Name": "NewLabelName"}}`)
assert.Equal(t, http.StatusNotFound, r.Code)
})
t.Run("bad request", func(t *testing.T) {
app, router, ctx := NewApiTest()
UpdatePhotoLabel(router, ctx)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/pt9jtdre2lvl0yh0/label/1000006", `{"Label": {"LabelName": 123}}`)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/pt9jtdre2lvl0yh0/label/1000006", `{"Label": {"Name": 123}}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
}

View file

@ -14,7 +14,7 @@ func TestGetPhoto(t *testing.T) {
GetPhoto(router, ctx)
r := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh7")
assert.Equal(t, http.StatusOK, r.Code)
val := gjson.Get(r.Body.String(), "PhotoLat")
val := gjson.Get(r.Body.String(), "Lat")
assert.Equal(t, "48.519234", val.String())
})
t.Run("search for not existing photo", func(t *testing.T) {
@ -29,10 +29,10 @@ func TestUpdatePhoto(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, conf := NewApiTest()
UpdatePhoto(router, conf)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/pt9jtdre2lvl0y13", `{"PhotoTitle": "Updated01", "PhotoCountry": "de"}`)
val := gjson.Get(r.Body.String(), "PhotoTitle")
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/pt9jtdre2lvl0y13", `{"Title": "Updated01", "Country": "de"}`)
val := gjson.Get(r.Body.String(), "Title")
assert.Equal(t, "Updated01", val.String())
val2 := gjson.Get(r.Body.String(), "PhotoCountry")
val2 := gjson.Get(r.Body.String(), "Country")
assert.Equal(t, "de", val2.String())
assert.Equal(t, http.StatusOK, r.Code)
})
@ -40,14 +40,14 @@ func TestUpdatePhoto(t *testing.T) {
t.Run("invalid request", func(t *testing.T) {
app, router, conf := NewApiTest()
UpdatePhoto(router, conf)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/pt9jtdre2lvl0y13", `{"PhotoName": "Updated01", "PhotoCountry": 123}`)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/pt9jtdre2lvl0y13", `{"Name": "Updated01", "Country": 123}`)
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("not found", func(t *testing.T) {
app, router, conf := NewApiTest()
UpdatePhoto(router, conf)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/xxx", `{"PhotoName": "Updated01", "PhotoCountry": "de"}`)
r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/xxx", `{"Name": "Updated01", "Country": "de"}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Photo not found", val.String())
assert.Equal(t, http.StatusNotFound, r.Code)
@ -77,7 +77,7 @@ func TestLikePhoto(t *testing.T) {
assert.Equal(t, http.StatusOK, r.Code)
GetPhoto(router, ctx)
r2 := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh9")
val := gjson.Get(r2.Body.String(), "PhotoFavorite")
val := gjson.Get(r2.Body.String(), "Favorite")
assert.Equal(t, "true", val.String())
})
t.Run("not existing photo", func(t *testing.T) {
@ -96,7 +96,7 @@ func TestDislikePhoto(t *testing.T) {
assert.Equal(t, http.StatusOK, r.Code)
GetPhoto(router, ctx)
r2 := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh8")
val := gjson.Get(r2.Body.String(), "PhotoFavorite")
val := gjson.Get(r2.Body.String(), "Favorite")
assert.Equal(t, "false", val.String())
})
t.Run("not existing photo", func(t *testing.T) {
@ -115,13 +115,13 @@ func TestSetPhotoPrimary(t *testing.T) {
assert.Equal(t, http.StatusOK, r.Code)
GetFile(router, ctx)
r2 := PerformRequest(app, "GET", "/api/v1/files/ocad9168fa6acc5c5c2965ddf6ec465ca42fd818")
val := gjson.Get(r2.Body.String(), "FilePrimary")
val := gjson.Get(r2.Body.String(), "Primary")
assert.Equal(t, "true", val.String())
r3 := PerformRequest(app, "GET", "/api/v1/files/3cad9168fa6acc5c5c2965ddf6ec465ca42fd818")
val2 := gjson.Get(r3.Body.String(), "FilePrimary")
val2 := gjson.Get(r3.Body.String(), "Primary")
assert.Equal(t, "false", val2.String())
})
t.Run("wrong photo uuid", func(t *testing.T) {
t.Run("wrong photo uid", func(t *testing.T) {
app, router, ctx := NewApiTest()
SetPhotoPrimary(router, ctx)
r := PerformRequest(app, "POST", "/api/v1/photos/xxx/primary/ft1es39w45bnlqdw")

View file

@ -38,7 +38,7 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
}
if f.FileVideo {
f, err = query.FileByPhotoUUID(f.PhotoUUID)
f, err = query.FileByPhotoUID(f.PhotoUID)
if err != nil {
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)

View file

@ -48,6 +48,10 @@ func GetSvg(router *gin.RouterGroup) {
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
})
router.GET("/svg/folder", func(c *gin.Context) {
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
})
router.GET("/svg/broken", func(c *gin.Context) {
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
})

View file

@ -40,7 +40,7 @@ func GetVideo(router *gin.RouterGroup, conf *config.Config) {
}
if !f.FileVideo {
f, err = query.VideoByPhotoUUID(f.PhotoUUID)
f, err = query.VideoByPhotoUID(f.PhotoUID)
if err != nil {
log.Errorf("video: %s", err.Error())

View file

@ -47,7 +47,7 @@ func CreateZip(router *gin.RouterGroup, conf *config.Config) {
return
}
files, err := query.FilesByUUID(f.Photos, 1000, 0)
files, err := query.FilesByUID(f.Photos, 1000, 0)
if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})

View file

@ -50,7 +50,7 @@ func (c *Config) PublicClientConfig() ClientConfig {
configFlags := c.Flags()
var noPos = struct {
PhotoUUID string `json:"photo"`
PhotoUID string `json:"photo"`
LocationID string `json:"location"`
TakenAt time.Time `json:"utc"`
PhotoLat float64 `json:"lat"`
@ -63,9 +63,12 @@ func (c *Config) PublicClientConfig() ClientConfig {
Hidden uint `json:"hidden"`
Favorites uint `json:"favorites"`
Private uint `json:"private"`
Review uint `json:"review"`
Stories uint `json:"stories"`
Labels uint `json:"labels"`
Albums uint `json:"albums"`
Folders uint `json:"folders"`
Moments uint `json:"moments"`
Countries uint `json:"countries"`
Places uint `json:"places"`
}{}
@ -112,12 +115,13 @@ func (c *Config) ClientConfig() ClientConfig {
db := c.Db()
var cameras []*entity.Camera
var lenses []*entity.Lens
var albums []*entity.Album
var cameras []entity.Camera
var lenses []entity.Lens
var albums []entity.Album
var countries []entity.Country
var position struct {
PhotoUUID string `json:"photo"`
PhotoUID string `json:"photo"`
LocationID string `json:"location"`
TakenAt time.Time `json:"utc"`
PhotoLat float64 `json:"lat"`
@ -125,7 +129,7 @@ func (c *Config) ClientConfig() ClientConfig {
}
db.Table("photos").
Select("photo_uuid, location_id, photo_lat, photo_lng, taken_at").
Select("photo_uid, location_id, photo_lat, photo_lng, taken_at").
Where("deleted_at IS NULL AND photo_lat != 0 AND photo_lng != 0").
Order("taken_at DESC").
Limit(1).Offset(0).
@ -137,7 +141,10 @@ func (c *Config) ClientConfig() ClientConfig {
Hidden uint `json:"hidden"`
Favorites uint `json:"favorites"`
Private uint `json:"private"`
Review uint `json:"review"`
Albums uint `json:"albums"`
Folders uint `json:"folders"`
Moments uint `json:"moments"`
Countries uint `json:"countries"`
Places uint `json:"places"`
Labels uint `json:"labels"`
@ -145,7 +152,7 @@ func (c *Config) ClientConfig() ClientConfig {
}{}
db.Table("photos").
Select("SUM(photo_type = 'video' AND photo_quality >= 0 AND photo_private = 0) AS videos, SUM(photo_quality = -1) AS hidden, SUM(photo_type IN ('image','raw','live') AND photo_private = 0 AND photo_quality >= 0) AS photos, SUM(photo_favorite = 1 AND photo_quality >= 0) AS favorites, SUM(photo_private = 1 AND photo_quality >= 0) AS private").
Select("SUM(photo_type = 'video' AND photo_quality >= 0 AND photo_private = 0) AS videos, SUM(photo_type IN ('image','raw','live') AND photo_quality < 3 AND photo_quality >= 0 AND photo_private = 0) AS review, SUM(photo_quality = -1) AS hidden, SUM(photo_type IN ('image','raw','live') AND photo_private = 0 AND photo_quality >= 0) AS photos, SUM(photo_favorite = 1 AND photo_quality >= 0) AS favorites, SUM(photo_private = 1 AND photo_quality >= 0) AS private").
Where("photos.id NOT IN (SELECT photo_id FROM files WHERE file_primary = 1 AND (file_missing = 1 OR file_error <> ''))").
Where("deleted_at IS NULL").
Take(&count)
@ -158,7 +165,13 @@ func (c *Config) ClientConfig() ClientConfig {
Take(&count)
db.Table("albums").
Select("COUNT(*) AS albums").
Select("SUM(album_type = '') AS albums, SUM(album_type = 'moment') AS moments, SUM(album_type = 'folder') AS folders").
Where("deleted_at IS NULL").
Take(&count)
db.Table("folders").
Select("COUNT(*) AS folders").
Where("folder_hidden = 0").
Where("deleted_at IS NULL").
Take(&count)
@ -171,17 +184,8 @@ func (c *Config) ClientConfig() ClientConfig {
Where("id != 'zz'").
Take(&count)
type country struct {
ID string `json:"code"`
CountryName string `json:"name"`
}
var countries []country
db.Model(&entity.Country{}).
Select("id, country_name").
Order("country_slug").
Scan(&countries)
db.Order("country_slug").
Find(&countries)
db.Where("deleted_at IS NULL").
Limit(10000).Order("camera_slug").
@ -203,24 +207,22 @@ func (c *Config) ClientConfig() ClientConfig {
Pluck("DISTINCT photo_year", &years)
type CategoryLabel struct {
LabelName string
Title string
LabelUID string `json:"UID"`
CustomSlug string `json:"Slug"`
LabelName string `json:"Name"`
}
var categories []CategoryLabel
db.Table("categories").
Select("l.label_name").
Select("l.label_uid, l.custom_slug, l.label_name").
Joins("JOIN labels l ON categories.category_id = l.id").
Group("l.label_name").
Order("l.label_name").
Where("l.deleted_at IS NULL").
Group("l.custom_slug").
Order("l.custom_slug").
Limit(1000).Offset(0).
Scan(&categories)
for i, l := range categories {
categories[i].Title = strings.Title(l.LabelName)
}
jsHash := fs.Checksum(c.HttpStaticBuildPath() + "/app.js")
cssHash := fs.Checksum(c.HttpStaticBuildPath() + "/app.css")
configFlags := c.Flags()

View file

@ -150,17 +150,17 @@ func (c *Params) Load(fileName string) error {
func (c *Params) SetContext(ctx *cli.Context) error {
v := reflect.ValueOf(c).Elem()
// Iterate through all config fields
// Iterate through all config fields.
for i := 0; i < v.NumField(); i++ {
fieldValue := v.Field(i)
tagValue := v.Type().Field(i).Tag.Get("flag")
// Automatically assign values to fields with "flag" tag
// Automatically assign values to fields with "flag" tag.
if tagValue != "" {
switch t := fieldValue.Interface().(type) {
case int, int64:
// Only if explicitly set or current value is empty (use default)
// Only if explicitly set or current value is empty (use default).
if ctx.IsSet(tagValue) {
f := ctx.Int64(tagValue)
fieldValue.SetInt(f)
@ -169,7 +169,7 @@ func (c *Params) SetContext(ctx *cli.Context) error {
fieldValue.SetInt(f)
}
case uint, uint64:
// Only if explicitly set or current value is empty (use default)
// Only if explicitly set or current value is empty (use default).
if ctx.IsSet(tagValue) {
f := ctx.Uint64(tagValue)
fieldValue.SetUint(f)

View file

@ -42,6 +42,8 @@ type FeatureSettings struct {
Review bool `json:"review" yaml:"review"`
Upload bool `json:"upload" yaml:"upload"`
Import bool `json:"import" yaml:"import"`
Folders bool `json:"folders" yaml:"folders"`
Moments bool `json:"moments" yaml:"moments"`
Labels bool `json:"labels" yaml:"labels"`
Places bool `json:"places" yaml:"places"`
Download bool `json:"download" yaml:"download"`
@ -79,6 +81,8 @@ func NewSettings() *Settings {
Private: true,
Upload: true,
Import: true,
Folders: true,
Moments: true,
Labels: true,
Places: true,
Download: true,

View file

@ -11,6 +11,8 @@ features:
review: true
upload: true
import: true
folders: true
moments: true
labels: true
places: true
download: true

View file

@ -14,43 +14,48 @@ import (
// Album represents a photo album
type Album struct {
ID uint `gorm:"primary_key"`
CoverUUID string `gorm:"type:varbinary(36);"`
AlbumUUID string `gorm:"type:varbinary(36);unique_index;"`
AlbumSlug string `gorm:"type:varbinary(255);index;"`
AlbumName string `gorm:"type:varchar(255);"`
AlbumDescription string `gorm:"type:text;"`
AlbumNotes string `gorm:"type:text;"`
AlbumOrder string `gorm:"type:varbinary(32);"`
AlbumTemplate string `gorm:"type:varbinary(255);"`
AlbumFavorite bool
Links []Link `gorm:"foreignkey:ShareUUID;association_foreignkey:AlbumUUID"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `sql:"index"`
ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
AlbumUID string `gorm:"type:varbinary(36);unique_index;" json:"UID" yaml:"UID"`
CoverUID string `gorm:"type:varbinary(36);" json:"CoverUID" yaml:"CoverUID,omitempty"`
ParentUID string `gorm:"type:varbinary(36);index;" json:"ParentUID" yaml:"ParentUID,omitempty"`
FolderUID string `gorm:"type:varbinary(36);index;" json:"FolderUID" yaml:"FolderUID,omitempty"`
AlbumSlug string `gorm:"type:varbinary(255);index;" json:"Slug" yaml:"Slug"`
AlbumName string `gorm:"type:varchar(255);" json:"Name" yaml:"Name"`
AlbumType string `gorm:"type:varbinary(8);default:'';" json:"Type" yaml:"Type"`
AlbumFilter string `gorm:"type:varchar(1024);" json:"Filter" yaml:"Filter,omitempty"`
AlbumDescription string `gorm:"type:text;" json:"Description" yaml:"Description,omitempty"`
AlbumNotes string `gorm:"type:text;" json:"Notes" yaml:"Notes,omitempty"`
AlbumOrder string `gorm:"type:varbinary(32);" json:"Order" yaml:"Order,omitempty"`
AlbumTemplate string `gorm:"type:varbinary(255);" json:"Template" yaml:"Template,omitempty"`
AlbumFavorite bool `json:"Favorite" yaml:"Favorite,omitempty"`
Links []Link `gorm:"foreignkey:ShareUID;association_foreignkey:AlbumUID" json:"Links" yaml:"-"`
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
DeletedAt *time.Time `sql:"index" json:"-" yaml:"-"`
}
// BeforeCreate creates a random UUID if needed before inserting a new row to the database.
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func (m *Album) BeforeCreate(scope *gorm.Scope) error {
if rnd.IsPPID(m.AlbumUUID, 'a') {
if rnd.IsPPID(m.AlbumUID, 'a') {
return nil
}
return scope.SetColumn("AlbumUUID", rnd.PPID('a'))
return scope.SetColumn("AlbumUID", rnd.PPID('a'))
}
// NewAlbum creates a new album; default name is current month and year
func NewAlbum(name string) *Album {
func NewAlbum(albumName, albumType string) *Album {
now := time.Now().UTC()
result := &Album{
AlbumUUID: rnd.PPID('a'),
AlbumUID: rnd.PPID('a'),
AlbumOrder: SortOrderOldest,
AlbumType: albumType,
CreatedAt: now,
UpdatedAt: now,
}
result.SetName(name)
result.SetName(albumName)
return result
}
@ -68,7 +73,7 @@ func (m *Album) SetName(name string) {
if len(m.AlbumName) < txt.ClipSlug {
m.AlbumSlug = slug.Make(m.AlbumName)
} else {
m.AlbumSlug = slug.Make(txt.Clip(m.AlbumName, txt.ClipSlug)) + "-" + m.AlbumUUID
m.AlbumSlug = slug.Make(txt.Clip(m.AlbumName, txt.ClipSlug)) + "-" + m.AlbumUID
}
}

View file

@ -11,7 +11,7 @@ func (m AlbumMap) Get(name string) Album {
return result
}
return *NewAlbum(name)
return *NewAlbum(name, TypeDefault)
}
func (m AlbumMap) Pointer(name string) *Album {
@ -19,15 +19,16 @@ func (m AlbumMap) Pointer(name string) *Album {
return &result
}
return NewAlbum(name)
return NewAlbum(name, TypeDefault)
}
var AlbumFixtures = AlbumMap{
"christmas2030": {
ID: 1000000,
CoverUUID: "",
AlbumUUID: "at9lxuqxpogaaba7",
CoverUID: "",
AlbumUID: "at9lxuqxpogaaba7",
AlbumSlug: "christmas2030",
AlbumType: TypeDefault,
AlbumName: "Christmas2030",
AlbumDescription: "Wonderful christmas",
AlbumNotes: "",
@ -41,9 +42,10 @@ var AlbumFixtures = AlbumMap{
},
"holiday-2030": {
ID: 1000001,
CoverUUID: "",
AlbumUUID: "at9lxuqxpogaaba8",
CoverUID: "",
AlbumUID: "at9lxuqxpogaaba8",
AlbumSlug: "holiday-2030",
AlbumType: TypeDefault,
AlbumName: "Holiday2030",
AlbumDescription: "Wonderful christmas",
AlbumNotes: "",
@ -57,9 +59,10 @@ var AlbumFixtures = AlbumMap{
},
"berlin-2019": {
ID: 1000002,
CoverUUID: "",
AlbumUUID: "at9lxuqxpogaaba9",
CoverUID: "",
AlbumUID: "at9lxuqxpogaaba9",
AlbumSlug: "berlin-2019",
AlbumType: TypeDefault,
AlbumName: "Berlin2019",
AlbumDescription: "Wonderful christmas",
AlbumNotes: "",

View file

@ -8,7 +8,7 @@ import (
func TestAlbumMap_Get(t *testing.T) {
t.Run("get existing album", func(t *testing.T) {
r := AlbumFixtures.Get("christmas2030")
assert.Equal(t, "at9lxuqxpogaaba7", r.AlbumUUID)
assert.Equal(t, "at9lxuqxpogaaba7", r.AlbumUID)
assert.Equal(t, "christmas2030", r.AlbumSlug)
assert.IsType(t, Album{}, r)
})
@ -22,7 +22,7 @@ func TestAlbumMap_Get(t *testing.T) {
func TestAlbumMap_Pointer(t *testing.T) {
t.Run("get existing album pointer", func(t *testing.T) {
r := AlbumFixtures.Pointer("christmas2030")
assert.Equal(t, "at9lxuqxpogaaba7", r.AlbumUUID)
assert.Equal(t, "at9lxuqxpogaaba7", r.AlbumUID)
assert.Equal(t, "christmas2030", r.AlbumSlug)
assert.IsType(t, &Album{}, r)
})

View file

@ -12,12 +12,12 @@ import (
func TestNewAlbum(t *testing.T) {
t.Run("name Christmas 2018", func(t *testing.T) {
album := NewAlbum("Christmas 2018")
album := NewAlbum("Christmas 2018", TypeDefault)
assert.Equal(t, "Christmas 2018", album.AlbumName)
assert.Equal(t, "christmas-2018", album.AlbumSlug)
})
t.Run("name empty", func(t *testing.T) {
album := NewAlbum("")
album := NewAlbum("", TypeDefault)
defaultName := time.Now().Format("January 2006")
defaultSlug := slug.Make(defaultName)
@ -29,7 +29,7 @@ func TestNewAlbum(t *testing.T) {
func TestAlbum_SetName(t *testing.T) {
t.Run("valid name", func(t *testing.T) {
album := NewAlbum("initial name")
album := NewAlbum("initial name", TypeDefault)
assert.Equal(t, "initial name", album.AlbumName)
assert.Equal(t, "initial-name", album.AlbumSlug)
album.SetName("New Album Name")
@ -37,7 +37,7 @@ func TestAlbum_SetName(t *testing.T) {
assert.Equal(t, "new-album-name", album.AlbumSlug)
})
t.Run("empty name", func(t *testing.T) {
album := NewAlbum("initial name")
album := NewAlbum("initial name", TypeDefault)
assert.Equal(t, "initial name", album.AlbumName)
assert.Equal(t, "initial-name", album.AlbumSlug)
@ -57,7 +57,7 @@ The discrepancy of 1 second meridian arc length between equator and pole is abou
is an oblate spheroid.`
expected := txt.Clip(longName, txt.ClipDefault)
slugExpected := txt.Clip(longName, txt.ClipSlug)
album := NewAlbum(longName)
album := NewAlbum(longName, TypeDefault)
assert.Equal(t, expected, album.AlbumName)
assert.Contains(t, album.AlbumSlug, slug.Make(slugExpected))
})
@ -65,7 +65,7 @@ is an oblate spheroid.`
func TestAlbum_Save(t *testing.T) {
t.Run("success", func(t *testing.T) {
album := NewAlbum("Old Name")
album := NewAlbum("Old Name", TypeDefault)
assert.Equal(t, "Old Name", album.AlbumName)
assert.Equal(t, "old-name", album.AlbumSlug)

View file

@ -11,16 +11,16 @@ import (
// Camera model and make (as extracted from UpdateExif metadata)
type Camera struct {
ID uint `gorm:"primary_key" yaml:"CameraID"`
CameraSlug string `gorm:"type:varbinary(255);unique_index;" yaml:"Slug"`
CameraModel string `gorm:"type:varchar(255);" yaml:"Model"`
CameraMake string `gorm:"type:varchar(255);" yaml:"Make"`
CameraType string `gorm:"type:varchar(255);" yaml:"Type,omitempty"`
CameraDescription string `gorm:"type:text;" yaml:"Description,omitempty"`
CameraNotes string `gorm:"type:text;" yaml:"Notes,omitempty"`
CreatedAt time.Time `yaml:"-"`
UpdatedAt time.Time `yaml:"-"`
DeletedAt *time.Time `sql:"index" yaml:"-"`
ID uint `gorm:"primary_key" json:"ID" yaml:"ID"`
CameraSlug string `gorm:"type:varbinary(255);unique_index;" json:"Slug" yaml:"-"`
CameraModel string `gorm:"type:varchar(255);" json:"Model" yaml:"Model"`
CameraMake string `gorm:"type:varchar(255);" json:"Make" yaml:"Make"`
CameraType string `gorm:"type:varchar(255);" json:"Type,omitempty" yaml:"Type,omitempty"`
CameraDescription string `gorm:"type:text;" json:"Description,omitempty" yaml:"Description,omitempty"`
CameraNotes string `gorm:"type:text;" json:"Notes,omitempty" yaml:"Notes,omitempty"`
CreatedAt time.Time `json:"-" yaml:"-"`
UpdatedAt time.Time `json:"-" yaml:"-"`
DeletedAt *time.Time `sql:"index" json:"-" yaml:"-"`
}
var UnknownCamera = Camera{

View file

@ -27,6 +27,8 @@ const (
TitleUnknown = "Unknown"
TypeDefault = ""
TypeFolder = "folder"
TypeMoment = "moment"
TypeImage = "image"
TypeLive = "live"
TypeVideo = "video"

View file

@ -15,14 +15,14 @@ var altCountryNames = map[string]string{
// Country represents a country location, used for labeling photos.
type Country struct {
ID string `gorm:"type:varbinary(2);primary_key"`
CountrySlug string `gorm:"type:varbinary(255);unique_index;"`
CountryName string
CountryDescription string `gorm:"type:text;"`
CountryNotes string `gorm:"type:text;"`
CountryPhoto *Photo
CountryPhotoID uint
New bool `gorm:"-"`
ID string `gorm:"type:varbinary(2);primary_key" json:"ID" yaml:"ID"`
CountrySlug string `gorm:"type:varbinary(255);unique_index;" json:"Slug" yaml:"-"`
CountryName string `json:"Name" yaml:"Name,omitempty"`
CountryDescription string `gorm:"type:text;" json:"Description,omitempty" yaml:"Description,omitempty"`
CountryNotes string `gorm:"type:text;" json:"Notes,omitempty" yaml:"Notes,omitempty"`
CountryPhoto *Photo `json:"-" yaml:"-"`
CountryPhotoID uint `json:"-" yaml:"-"`
New bool `gorm:"-" json:"-" yaml:"-"`
}
// UnknownCountry is defined here to use it as a default

View file

@ -13,45 +13,45 @@ import (
// File represents an image or sidecar file that belongs to a photo
type File struct {
ID uint `gorm:"primary_key"`
Photo *Photo
PhotoID uint `gorm:"index;"`
PhotoUUID string `gorm:"type:varbinary(36);index;"`
FileUUID string `gorm:"type:varbinary(36);unique_index;"`
FileName string `gorm:"type:varbinary(768);unique_index"`
OriginalName string `gorm:"type:varbinary(768);"`
FileHash string `gorm:"type:varbinary(128);index"`
FileModified time.Time
FileSize int64
FileCodec string `gorm:"type:varbinary(32)"`
FileType string `gorm:"type:varbinary(32)"`
FileMime string `gorm:"type:varbinary(64)"`
FilePrimary bool
FileSidecar bool
FileMissing bool
FileDuplicate bool
FilePortrait bool
FileVideo bool
FileDuration time.Duration
FileWidth int
FileHeight int
FileOrientation int
FileAspectRatio float32 `gorm:"type:FLOAT;"`
FileMainColor string `gorm:"type:varbinary(16);index;"`
FileColors string `gorm:"type:varbinary(9);"`
FileLuminance string `gorm:"type:varbinary(9);"`
FileDiff uint32
FileChroma uint8
FileNotes string `gorm:"type:text"`
FileError string `gorm:"type:varbinary(512)"`
Share []FileShare
Sync []FileSync
Links []Link `gorm:"foreignkey:ShareUUID;association_foreignkey:FileUUID"`
CreatedAt time.Time
CreatedIn int64
UpdatedAt time.Time
UpdatedIn int64
DeletedAt *time.Time `sql:"index"`
ID uint `gorm:"primary_key" json:"-" yaml:"-"`
Photo *Photo `json:"-" yaml:"-"`
PhotoID uint `gorm:"index;" json:"-" yaml:"-"`
PhotoUID string `gorm:"type:varbinary(36);index;" json:"PhotoUID" yaml:"PhotoUID"`
FileUID string `gorm:"type:varbinary(36);unique_index;" json:"UID" yaml:"UID"`
FileName string `gorm:"type:varbinary(768);unique_index" json:"Name" yaml:"Name"`
OriginalName string `gorm:"type:varbinary(768);" json:"OriginalName" yaml:"OriginalName,omitempty"`
FileHash string `gorm:"type:varbinary(128);index" json:"Hash" yaml:"Hash,omitempty"`
FileModified time.Time `json:"Modified" yaml:"Modified,omitempty"`
FileSize int64 `json:"Size" yaml:"Size,omitempty"`
FileCodec string `gorm:"type:varbinary(32)" json:"Codec" yaml:"Codec,omitempty"`
FileType string `gorm:"type:varbinary(32)" json:"Type" yaml:"Type,omitempty"`
FileMime string `gorm:"type:varbinary(64)" json:"Mime" yaml:"Mime,omitempty"`
FilePrimary bool `json:"Primary" yaml:"Primary,omitempty"`
FileSidecar bool `json:"Sidecar" yaml:"Sidecar,omitempty"`
FileMissing bool `json:"Missing" yaml:"Missing,omitempty"`
FileDuplicate bool `json:"Duplicate" yaml:"Duplicate,omitempty"`
FilePortrait bool `json:"Portrait" yaml:"Portrait,omitempty"`
FileVideo bool `json:"Video" yaml:"Video,omitempty"`
FileDuration time.Duration `json:"Duration" yaml:"Duration,omitempty"`
FileWidth int `json:"Width" yaml:"Width,omitempty"`
FileHeight int `json:"Height" yaml:"Height,omitempty"`
FileOrientation int `json:"Orientation" yaml:"Orientation,omitempty"`
FileAspectRatio float32 `gorm:"type:FLOAT;" json:"AspectRatio" yaml:"AspectRatio,omitempty"`
FileMainColor string `gorm:"type:varbinary(16);index;" json:"eMainColor" yaml:"eMainColor,omitempty"`
FileColors string `gorm:"type:varbinary(9);" json:"Colors" yaml:"Colors,omitempty"`
FileLuminance string `gorm:"type:varbinary(9);" json:"Luminance" yaml:"Luminance,omitempty"`
FileDiff uint32 `json:"Diff" yaml:"Diff,omitempty"`
FileChroma uint8 `json:"Chroma" yaml:"Chroma,omitempty"`
FileNotes string `gorm:"type:text" json:"Notes" yaml:"Notes,omitempty"`
FileError string `gorm:"type:varbinary(512)" json:"Error" yaml:"Error,omitempty"`
Share []FileShare `json:"-" yaml:"-"`
Sync []FileSync `json:"-" yaml:"-"`
Links []Link `gorm:"foreignkey:ShareUID;association_foreignkey:FileUID" json:"Links" yaml:"-"`
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
CreatedIn int64 `json:"CreatedIn" yaml:"-"`
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
UpdatedIn int64 `json:"UpdatedIn" yaml:"-"`
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`
}
type FileInfos struct {
@ -75,13 +75,13 @@ func FirstFileByHash(fileHash string) (File, error) {
return file, q.Error
}
// BeforeCreate creates a random UUID if needed before inserting a new row to the database.
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func (m *File) BeforeCreate(scope *gorm.Scope) error {
if rnd.IsPPID(m.FileUUID, 'f') {
if rnd.IsPPID(m.FileUID, 'f') {
return nil
}
return scope.SetColumn("FileUUID", rnd.PPID('f'))
return scope.SetColumn("FileUID", rnd.PPID('f'))
}
// ShareFileName returns a meaningful file name useful for sharing.
@ -95,7 +95,7 @@ func (m *File) ShareFileName() string {
if m.Photo.PhotoTitle != "" {
name = strings.Title(slug.MakeLang(m.Photo.PhotoTitle, "en"))
} else {
name = m.PhotoUUID
name = m.PhotoUID
}
taken := m.Photo.TakenAtLocal.Format("20060102-150405")
@ -144,7 +144,7 @@ func (m *File) AllFilesMissing() bool {
// Saves the file in the database.
func (m *File) Save() error {
if m.PhotoID == 0 {
return fmt.Errorf("file: photo id is empty (%s)", m.FileUUID)
return fmt.Errorf("file: photo id is empty (%s)", m.FileUID)
}
if err := Db().Save(m).Error; err != nil {

View file

@ -9,8 +9,8 @@ var FileFixtures = map[string]File{
ID: 1000000,
Photo: PhotoFixtures.Pointer("19800101_000002_D640C559"),
PhotoID: 1000000,
PhotoUUID: "pt9jtdre2lvl0yh7",
FileUUID: "ft8es39w45bnlqdw",
PhotoUID: "pt9jtdre2lvl0yh7",
FileUID: "ft8es39w45bnlqdw",
FileName: "exampleFileName.jpg",
OriginalName: "exampleFileNameOriginal.jpg",
FileHash: "2cad9168fa6acc5c5c2965ddf6ec465ca42fd818",
@ -51,8 +51,8 @@ var FileFixtures = map[string]File{
ID: 1000001,
Photo: PhotoFixtures.Pointer("Photo01"),
PhotoID: 1000001,
PhotoUUID: "pt9jtdre2lvl0yh8",
FileUUID: "ft9es39w45bnlqdw",
PhotoUID: "pt9jtdre2lvl0yh8",
FileUID: "ft9es39w45bnlqdw",
FileName: "exampleDNGFile.dng",
OriginalName: "exampleDNGFile.dng",
FileHash: "3cad9168fa6acc5c5c2965ddf6ec465ca42fd818",
@ -90,8 +90,8 @@ var FileFixtures = map[string]File{
ID: 1000002,
Photo: PhotoFixtures.Pointer("Photo01"),
PhotoID: 1000001,
PhotoUUID: "pt9jtdre2lvl0yh8",
FileUUID: "ft1es39w45bnlqdw",
PhotoUID: "pt9jtdre2lvl0yh8",
FileUID: "ft1es39w45bnlqdw",
FileName: "exampleXmpFile.xmp",
OriginalName: "exampleXmpFile.xmp",
FileHash: "ocad9168fa6acc5c5c2965ddf6ec465ca42fd818",
@ -129,8 +129,8 @@ var FileFixtures = map[string]File{
ID: 1000003,
Photo: PhotoFixtures.Pointer("Photo04"),
PhotoID: 1000004,
PhotoUUID: "pt9jtdre2lvl0y11",
FileUUID: "ft2es39w45bnlqdw",
PhotoUID: "pt9jtdre2lvl0y11",
FileUID: "ft2es39w45bnlqdw",
FileName: "bridge.jpg",
OriginalName: "bridgeOriginal.jpg",
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
@ -168,8 +168,8 @@ var FileFixtures = map[string]File{
ID: 1000004,
Photo: PhotoFixtures.Pointer("Photo05"),
PhotoID: 1000005,
PhotoUUID: "pt9jtdre2lvl0y12",
FileUUID: "ft3es39w45bnlqdw",
PhotoUID: "pt9jtdre2lvl0y12",
FileUID: "ft3es39w45bnlqdw",
FileName: "reunion.jpg",
OriginalName: "reunionOriginal.jpg",
FileHash: "acad9168fa6acc5c5c2965ddf6ec465ca42fd818",
@ -207,8 +207,8 @@ var FileFixtures = map[string]File{
ID: 1000005,
Photo: PhotoFixtures.Pointer("Photo17"),
PhotoID: 1000017,
PhotoUUID: "pt9jtdre2lvl0y24",
FileUUID: "ft4es39w45bnlqdw",
PhotoUID: "pt9jtdre2lvl0y24",
FileUID: "ft4es39w45bnlqdw",
FileName: "Quality1FavoriteTrue.jpg",
OriginalName: "Quality1FavoriteTrue.jpg",
FileHash: "acad9168fa6acc5c5c2965ddf6ec465ca42fd819",
@ -246,8 +246,8 @@ var FileFixtures = map[string]File{
ID: 1000006,
Photo: PhotoFixtures.Pointer("Photo15"),
PhotoID: 1000015,
PhotoUUID: "pt9jtdre2lvl0y22",
FileUUID: "ft5es39w45bnlqdw",
PhotoUID: "pt9jtdre2lvl0y22",
FileUID: "ft5es39w45bnlqdw",
FileName: "missing.jpg",
OriginalName: "missing.jpg",
FileHash: "acad9168fa6acc5c5c2965ddf6ec465ca42fd819",
@ -285,8 +285,8 @@ var FileFixtures = map[string]File{
ID: 1000007,
Photo: nil, // no pointer here because related photo is deleted
PhotoID: 1000018,
PhotoUUID: "pt9jtdre2lvl0y25",
FileUUID: "ft6es39w45bnlqdw",
PhotoUID: "pt9jtdre2lvl0y25",
FileUID: "ft6es39w45bnlqdw",
FileName: "Photo18.jpg",
OriginalName: "Photo18.jpg",
FileHash: "acad9168fa6acc5c5c2965ddf6ec465ca42fd820",
@ -324,8 +324,8 @@ var FileFixtures = map[string]File{
ID: 1000008,
Photo: PhotoFixtures.Pointer("Photo10"),
PhotoID: 1000010,
PhotoUUID: "pt9jtdre2lvl0y17",
FileUUID: "ft71s39w45bnlqdw",
PhotoUID: "pt9jtdre2lvl0y17",
FileUID: "ft71s39w45bnlqdw",
FileName: "Video.mp4",
OriginalName: "Video.mp4",
FileHash: "acad9168fa6acc5c5c2965ddf6ec465ca42fd831",
@ -363,8 +363,8 @@ var FileFixtures = map[string]File{
ID: 1000009,
Photo: PhotoFixtures.Pointer("Photo10"),
PhotoID: 1000010,
PhotoUUID: "pt9jtdre2lvl0y17",
FileUUID: "ft72s39w45bnlqdw",
PhotoUID: "pt9jtdre2lvl0y17",
FileUID: "ft72s39w45bnlqdw",
FileName: "VideoError.mp4",
OriginalName: "VideoError.mp4",
FileHash: "acad9168fa6acc5c5c2965ddf6ec465ca42fd832",

View file

@ -26,7 +26,7 @@ func TestFirstFileByHash(t *testing.T) {
func TestFile_DownloadFileName(t *testing.T) {
t.Run("photo with title", func(t *testing.T) {
photo := &Photo{TakenAtLocal: time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), PhotoTitle: "Berlin / Morning Mood"}
file := &File{Photo: photo, FileType: "jpg", FileUUID: "foobar345678765"}
file := &File{Photo: photo, FileType: "jpg", FileUID: "foobar345678765"}
filename := file.ShareFileName()
@ -35,7 +35,7 @@ func TestFile_DownloadFileName(t *testing.T) {
})
t.Run("photo without title", func(t *testing.T) {
photo := &Photo{TakenAtLocal: time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), PhotoTitle: ""}
file := &File{Photo: photo, FileType: "jpg", PhotoUUID: "123", FileUUID: "foobar345678765"}
file := &File{Photo: photo, FileType: "jpg", PhotoUID: "123", FileUID: "foobar345678765"}
filename := file.ShareFileName()
@ -43,7 +43,7 @@ func TestFile_DownloadFileName(t *testing.T) {
assert.Contains(t, filename, fs.JpegExt)
})
t.Run("photo without photo", func(t *testing.T) {
file := &File{Photo: nil, FileType: "jpg", FileHash: "123Hash", FileUUID: "foobar345678765"}
file := &File{Photo: nil, FileType: "jpg", FileHash: "123Hash", FileUID: "foobar345678765"}
filename := file.ShareFileName()
@ -85,8 +85,8 @@ func TestFile_Purge(t *testing.T) {
func TestFile_AllFilesMissing(t *testing.T) {
t.Run("true", func(t *testing.T) {
photo := &Photo{TakenAtLocal: time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), PhotoTitle: ""}
file := &File{Photo: photo, FileType: "jpg", PhotoUUID: "123", FileUUID: "123", FileMissing: true}
file2 := &File{Photo: photo, FileType: "jpg", PhotoUUID: "123", FileUUID: "456", FileMissing: true}
file := &File{Photo: photo, FileType: "jpg", PhotoUID: "123", FileUID: "123", FileMissing: true}
file2 := &File{Photo: photo, FileType: "jpg", PhotoUID: "123", FileUID: "456", FileMissing: true}
assert.True(t, file.AllFilesMissing())
assert.NotEmpty(t, file2)
})
@ -100,7 +100,7 @@ func TestFile_AllFilesMissing(t *testing.T) {
func TestFile_Save(t *testing.T) {
t.Run("save without photo", func(t *testing.T) {
file := &File{Photo: nil, FileType: "jpg", PhotoUUID: "123", FileUUID: "123"}
file := &File{Photo: nil, FileType: "jpg", PhotoUID: "123", FileUID: "123"}
err := file.Save()
if err == nil {

View file

@ -7,6 +7,7 @@ import (
"time"
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd"
@ -25,7 +26,7 @@ const (
type Folder struct {
Root string `gorm:"type:varbinary(255);unique_index:idx_folders_root_path;" json:"Root" yaml:"Root"`
Path string `gorm:"type:varbinary(1024);unique_index:idx_folders_root_path;" json:"Path" yaml:"Path"`
FolderUUID string `gorm:"type:varbinary(36);primary_key;" json:"PPID,omitempty" yaml:"PPID,omitempty"`
FolderUID string `gorm:"type:varbinary(36);primary_key;" json:"UID,omitempty" yaml:"UID,omitempty"`
FolderTitle string `gorm:"type:varchar(255);" json:"Title" yaml:"Title,omitempty"`
FolderDescription string `gorm:"type:text;" json:"Description,omitempty" yaml:"Description,omitempty"`
FolderType string `gorm:"type:varbinary(16);" json:"Type" yaml:"Type,omitempty"`
@ -34,20 +35,20 @@ type Folder struct {
FolderIgnore bool `json:"Ignore" yaml:"Ignore"`
FolderHidden bool `json:"Hidden" yaml:"Hidden"`
FolderWatch bool `json:"Watch" yaml:"Watch"`
Links []Link `gorm:"foreignkey:ShareUUID;association_foreignkey:FolderUUID" json:"-" yaml:"-"`
Links []Link `gorm:"foreignkey:ShareUID;association_foreignkey:FolderUID" json:"Links" json:"-" yaml:"-"`
CreatedAt time.Time `json:"-" yaml:"-"`
UpdatedAt time.Time `json:"-" yaml:"-"`
ModifiedAt *time.Time `json:"ModifiedAt,omitempty" yaml:"-"`
DeletedAt *time.Time `sql:"index" json:"-"`
}
// BeforeCreate creates a random UUID if needed before inserting a new row to the database.
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func (m *Folder) BeforeCreate(scope *gorm.Scope) error {
if rnd.IsPPID(m.FolderUUID, 'd') {
if rnd.IsPPID(m.FolderUID, 'd') {
return nil
}
return scope.SetColumn("FolderUUID", rnd.PPID('d'))
return scope.SetColumn("FolderUID", rnd.PPID('d'))
}
// NewFolder creates a new file system directory entity.
@ -61,6 +62,7 @@ func NewFolder(root, pathName string, modTime *time.Time) Folder {
}
result := Folder{
FolderUID: rnd.PPID('d'),
Root: root,
Path: pathName,
FolderType: TypeDefault,
@ -106,7 +108,15 @@ func (m *Folder) SetTitleFromPath() {
// Saves the complete entity in the database.
func (m *Folder) Create() error {
return Db().Create(m).Error
if err := Db().Create(m).Error; err != nil {
return err
}
event.Publish("count.folders", event.Data{
"count": 1,
})
return nil
}
// Updates selected properties in the database.

View file

@ -15,7 +15,7 @@ func TestNewFolder(t *testing.T) {
assert.Equal(t, "", folder.FolderDescription)
assert.Equal(t, "", folder.FolderType)
assert.Equal(t, SortOrderName, folder.FolderOrder)
assert.Equal(t, "", folder.FolderUUID)
assert.IsType(t, "", folder.FolderUID)
assert.Equal(t, false, folder.FolderFavorite)
assert.Equal(t, false, folder.FolderHidden)
assert.Equal(t, false, folder.FolderIgnore)

View file

@ -12,31 +12,31 @@ import (
// Label is used for photo, album and location categorization
type Label struct {
ID uint `gorm:"primary_key"`
LabelUUID string `gorm:"type:varbinary(36);unique_index;"`
LabelSlug string `gorm:"type:varbinary(255);unique_index;"`
CustomSlug string `gorm:"type:varbinary(255);index;"`
LabelName string `gorm:"type:varchar(255);"`
LabelPriority int
LabelFavorite bool
LabelDescription string `gorm:"type:text;"`
LabelNotes string `gorm:"type:text;"`
LabelCategories []*Label `gorm:"many2many:categories;association_jointable_foreignkey:category_id"`
Links []Link `gorm:"foreignkey:ShareUUID;association_foreignkey:LabelUUID"`
PhotoCount int `gorm:"default:1"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `sql:"index"`
New bool `gorm:"-"`
ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
LabelUID string `gorm:"type:varbinary(36);unique_index;" json:"UID" yaml:"UID"`
LabelSlug string `gorm:"type:varbinary(255);unique_index;" json:"Slug" yaml:"-"`
CustomSlug string `gorm:"type:varbinary(255);index;" json:"CustomSlug" yaml:"-"`
LabelName string `gorm:"type:varchar(255);" json:"Name" yaml:"Name"`
LabelPriority int `gorm:"type:varchar(255);" json:"Priority" yaml:"Priority,omitempty"`
LabelFavorite bool `gorm:"type:varchar(255);" json:"Favorite" yaml:"Favorite,omitempty"`
LabelDescription string `gorm:"type:text;" json:"Description" yaml:"Description,omitempty"`
LabelNotes string `gorm:"type:text;" json:"Notes" yaml:"Notes,omitempty"`
LabelCategories []*Label `gorm:"many2many:categories;association_jointable_foreignkey:category_id" json:"-" yaml:"-"`
Links []Link `gorm:"foreignkey:ShareUID;association_foreignkey:LabelUID" json:"Links" yaml:"-"`
PhotoCount int `gorm:"default:1" json:"PhotoCount" yaml:"-"`
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`
New bool `gorm:"-" json:"-" yaml:"-"`
}
// BeforeCreate creates a random UUID if needed before inserting a new row to the database.
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func (m *Label) BeforeCreate(scope *gorm.Scope) error {
if rnd.IsPPID(m.LabelUUID, 'l') {
if rnd.IsPPID(m.LabelUID, 'l') {
return nil
}
return scope.SetColumn("LabelUUID", rnd.PPID('l'))
return scope.SetColumn("LabelUID", rnd.PPID('l'))
}
// NewLabel creates a label in database with a given name and priority

View file

@ -34,7 +34,7 @@ func (m LabelMap) PhotoLabel(photoId uint, labelName string, uncertainty int, so
var LabelFixtures = LabelMap{
"landscape": {
ID: 1000000,
LabelUUID: "lt9k3pw1wowuy3c2",
LabelUID: "lt9k3pw1wowuy3c2",
LabelSlug: "landscape",
CustomSlug: "landscape",
LabelName: "Landscape",
@ -52,7 +52,7 @@ var LabelFixtures = LabelMap{
},
"flower": {
ID: 1000001,
LabelUUID: "lt9k3pw1wowuy3c3",
LabelUID: "lt9k3pw1wowuy3c3",
LabelSlug: "flower",
CustomSlug: "flower",
LabelName: "Flower",
@ -70,7 +70,7 @@ var LabelFixtures = LabelMap{
},
"cake": {
ID: 1000002,
LabelUUID: "lt9k3pw1wowuy3c4",
LabelUID: "lt9k3pw1wowuy3c4",
LabelSlug: "cake",
CustomSlug: "kuchen",
LabelName: "Cake",
@ -88,7 +88,7 @@ var LabelFixtures = LabelMap{
},
"cow": {
ID: 1000003,
LabelUUID: "lt9k3pw1wowuy3c5",
LabelUID: "lt9k3pw1wowuy3c5",
LabelSlug: "cow",
CustomSlug: "kuh",
LabelName: "COW",
@ -106,7 +106,7 @@ var LabelFixtures = LabelMap{
},
"batchdelete": {
ID: 1000004,
LabelUUID: "lt9k3pw1wowuy3c6",
LabelUID: "lt9k3pw1wowuy3c6",
LabelSlug: "batchdelete",
CustomSlug: "batchDelete",
LabelName: "BatchDelete",
@ -124,7 +124,7 @@ var LabelFixtures = LabelMap{
},
"updateLabel": {
ID: 1000005,
LabelUUID: "lt9k3pw1wowuy3c7",
LabelUID: "lt9k3pw1wowuy3c7",
LabelSlug: "updatelabel",
CustomSlug: "updateLabel",
LabelName: "updateLabel",
@ -142,7 +142,7 @@ var LabelFixtures = LabelMap{
},
"updatePhotoLabel": {
ID: 1000006,
LabelUUID: "lt9k3pw1wowuy3c8",
LabelUID: "lt9k3pw1wowuy3c8",
LabelSlug: "updatephotolabel",
CustomSlug: "updateLabelPhoto",
LabelName: "updatePhotoLabel",
@ -160,7 +160,7 @@ var LabelFixtures = LabelMap{
},
"likeLabel": {
ID: 1000007,
LabelUUID: "lt9k3pw1wowuy3c9",
LabelUID: "lt9k3pw1wowuy3c9",
LabelSlug: "likeLabel",
CustomSlug: "likeLabel",
LabelName: "likeLabel",

View file

@ -8,7 +8,7 @@ import (
func TestLabelMap_Get(t *testing.T) {
t.Run("get existing label", func(t *testing.T) {
r := LabelFixtures.Get("landscape")
assert.Equal(t, "lt9k3pw1wowuy3c2", r.LabelUUID)
assert.Equal(t, "lt9k3pw1wowuy3c2", r.LabelUID)
assert.Equal(t, "landscape", r.LabelSlug)
assert.IsType(t, Label{}, r)
})
@ -22,7 +22,7 @@ func TestLabelMap_Get(t *testing.T) {
func TestLabelMap_Pointer(t *testing.T) {
t.Run("get existing label pointer", func(t *testing.T) {
r := LabelFixtures.Pointer("landscape")
assert.Equal(t, "lt9k3pw1wowuy3c2", r.LabelUUID)
assert.Equal(t, "lt9k3pw1wowuy3c2", r.LabelUID)
assert.Equal(t, "landscape", r.LabelSlug)
assert.IsType(t, &Label{}, r)
})

View file

@ -9,16 +9,16 @@ import (
// Lens represents camera lens (as extracted from UpdateExif metadata)
type Lens struct {
ID uint `gorm:"primary_key" yaml:"LensID"`
LensSlug string `gorm:"type:varbinary(255);unique_index;" yaml:"Slug"`
LensModel string `yaml:"Model"`
LensMake string `yaml:"Make"`
LensType string `yaml:"Type,omitempty"`
LensDescription string `gorm:"type:text;" yaml:"Description,omitempty"`
LensNotes string `gorm:"type:text;" yaml:"Notes,omitempty"`
CreatedAt time.Time `yaml:"-"`
UpdatedAt time.Time `yaml:"-"`
DeletedAt *time.Time `sql:"index" yaml:"-"`
ID uint `gorm:"primary_key" json:"ID" yaml:"ID"`
LensSlug string `gorm:"type:varbinary(255);unique_index;" json:"Slug" yaml:"Slug,omitempty"`
LensModel string `json:"Model" yaml:"Model"`
LensMake string `json:"Make" yaml:"Make"`
LensType string `json:"Type" yaml:"Type,omitempty"`
LensDescription string `gorm:"type:text;" json:"Description,omitempty" yaml:"Description,omitempty"`
LensNotes string `gorm:"type:text;" json:"Notes,omitempty" yaml:"Notes,omitempty"`
CreatedAt time.Time `json:"-" yaml:"-"`
UpdatedAt time.Time `json:"-" yaml:"-"`
DeletedAt *time.Time `sql:"index" json:"-" yaml:"-"`
}
var UnknownLens = Lens{

View file

@ -9,18 +9,18 @@ import (
// Link represents a sharing link.
type Link struct {
LinkToken string `gorm:"type:varbinary(255);primary_key;"`
LinkPassword string `gorm:"type:varbinary(255);"`
LinkExpires *time.Time `gorm:"type:datetime;"`
ShareUUID string `gorm:"type:varbinary(36);index;"`
CanComment bool
CanEdit bool
CreatedAt time.Time `deepcopier:"skip"`
UpdatedAt time.Time `deepcopier:"skip"`
DeletedAt *time.Time `deepcopier:"skip" sql:"index"`
LinkToken string `gorm:"type:varbinary(255);primary_key;" json:"Token"`
LinkPassword string `gorm:"type:varbinary(255);" json:"Password"`
LinkExpires *time.Time `gorm:"type:datetime;" json:"Expires"`
ShareUID string `gorm:"type:varbinary(36);index;" json:"ShareUID"`
CanComment bool `json:"CanComment"`
CanEdit bool `json:"CanEdit"`
CreatedAt time.Time `deepcopier:"skip" json:"CreatedAt"`
UpdatedAt time.Time `deepcopier:"skip" json:"UpdatedAt"`
DeletedAt *time.Time `deepcopier:"skip" sql:"index" json:"DeletedAt,omitempty"`
}
// BeforeCreate creates a random UUID if needed before inserting a new row to the database.
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func (m *Link) BeforeCreate(scope *gorm.Scope) error {
if err := scope.SetColumn("LinkToken", rnd.Token(10)); err != nil {
return err

View file

@ -11,7 +11,7 @@ var LinkFixtures = LinkMap{
LinkToken: "1jxf3jfn2k",
LinkPassword: "somepassword",
LinkExpires: &date,
ShareUUID: "4",
ShareUID: "4",
CanComment: true,
CanEdit: false,
CreatedAt: time.Date(2020, 3, 6, 2, 6, 51, 0, time.UTC),

View file

@ -18,33 +18,33 @@ import (
// Photo represents a photo, all its properties, and link to all its images and sidecar files.
type Photo struct {
ID uint `gorm:"primary_key" yaml:"-"`
TakenAt time.Time `gorm:"type:datetime;index:idx_photos_taken_uuid;" json:"TakenAt" yaml:"Taken"`
TakenAt time.Time `gorm:"type:datetime;index:idx_photos_taken_uid;" json:"TakenAt" yaml:"TakenAt"`
TakenAtLocal time.Time `gorm:"type:datetime;" yaml:"-"`
TakenSrc string `gorm:"type:varbinary(8);" json:"TakenSrc" yaml:"TakenSrc,omitempty"`
PhotoUUID string `gorm:"type:varbinary(36);unique_index;index:idx_photos_taken_uuid;" yaml:"PPID"`
PhotoType string `gorm:"type:varbinary(8);default:'image';" json:"PhotoType" yaml:"Type"`
PhotoTitle string `gorm:"type:varchar(255);" json:"PhotoTitle" yaml:"Title"`
PhotoUID string `gorm:"type:varbinary(36);unique_index;index:idx_photos_taken_uid;" json:"UID" yaml:"UID"`
PhotoType string `gorm:"type:varbinary(8);default:'image';" json:"Type" yaml:"Type"`
PhotoTitle string `gorm:"type:varchar(255);" json:"Title" yaml:"Title"`
TitleSrc string `gorm:"type:varbinary(8);" json:"TitleSrc" yaml:"TitleSrc,omitempty"`
PhotoDescription string `gorm:"type:text;" json:"PhotoDescription" yaml:"Description,omitempty"`
PhotoDescription string `gorm:"type:text;" json:"Description" yaml:"Description,omitempty"`
DescriptionSrc string `gorm:"type:varbinary(8);" json:"DescriptionSrc" yaml:"DescriptionSrc,omitempty"`
Details Details `json:"Details" yaml:"Details"`
PhotoPath string `gorm:"type:varbinary(768);index;" yaml:"-"`
PhotoName string `gorm:"type:varbinary(255);" yaml:"-"`
PhotoFavorite bool `json:"PhotoFavorite" yaml:"Favorite,omitempty"`
PhotoPrivate bool `json:"PhotoPrivate" yaml:"Private,omitempty"`
PhotoFavorite bool `json:"Favorite" yaml:"Favorite,omitempty"`
PhotoPrivate bool `json:"Private" yaml:"Private,omitempty"`
TimeZone string `gorm:"type:varbinary(64);" json:"TimeZone" yaml:"-"`
PhotoLat float32 `gorm:"type:FLOAT;index;" json:"PhotoLat" yaml:"Lat,omitempty"`
PhotoLng float32 `gorm:"type:FLOAT;index;" json:"PhotoLng" yaml:"Lng,omitempty"`
PhotoAltitude int `json:"PhotoAltitude" yaml:"Altitude,omitempty"`
PhotoCountry string `gorm:"type:varbinary(2);index:idx_photos_country_year_month;default:'zz'" json:"PhotoCountry" yaml:"-"`
PhotoLat float32 `gorm:"type:FLOAT;index;" json:"Lat" yaml:"Lat,omitempty"`
PhotoLng float32 `gorm:"type:FLOAT;index;" json:"Lng" yaml:"Lng,omitempty"`
PhotoAltitude int `json:"Altitude" yaml:"Altitude,omitempty"`
PhotoCountry string `gorm:"type:varbinary(2);index:idx_photos_country_year_month;default:'zz'" json:"Country" yaml:"-"`
PhotoYear int `gorm:"index:idx_photos_country_year_month;" yaml:"-"`
PhotoMonth int `gorm:"index:idx_photos_country_year_month;" yaml:"-"`
PhotoIso int `json:"PhotoIso" yaml:"ISO,omitempty"`
PhotoExposure string `gorm:"type:varbinary(64);" json:"PhotoExposure" yaml:"Exposure,omitempty"`
PhotoFNumber float32 `gorm:"type:FLOAT;" json:"PhotoFNumber" yaml:"FNumber,omitempty"`
PhotoFocalLength int `json:"PhotoFocalLength" yaml:"FocalLength,omitempty"`
PhotoQuality int `gorm:"type:SMALLINT" json:"PhotoQuality" yaml:"-"`
PhotoResolution int `gorm:"type:SMALLINT" json:"PhotoResolution" yaml:"-"`
PhotoIso int `json:"Iso" yaml:"ISO,omitempty"`
PhotoExposure string `gorm:"type:varbinary(64);" json:"Exposure" yaml:"Exposure,omitempty"`
PhotoFNumber float32 `gorm:"type:FLOAT;" json:"FNumber" yaml:"FNumber,omitempty"`
PhotoFocalLength int `json:"FocalLength" yaml:"FocalLength,omitempty"`
PhotoQuality int `gorm:"type:SMALLINT" json:"Quality" yaml:"-"`
PhotoResolution int `gorm:"type:SMALLINT" json:"Resolution" yaml:"-"`
CameraID uint `gorm:"index:idx_photos_camera_lens;" json:"CameraID" yaml:"-"`
CameraSerial string `gorm:"type:varbinary(255);" json:"CameraSerial" yaml:"CameraSerial,omitempty"`
CameraSrc string `gorm:"type:varbinary(8);" json:"CameraSrc" yaml:"-"`
@ -56,15 +56,15 @@ type Photo struct {
Lens *Lens `json:"Lens" yaml:"-"`
Location *Location `json:"Location" yaml:"-"`
Place *Place `json:"-" yaml:"-"`
Links []Link `gorm:"foreignkey:ShareUUID;association_foreignkey:PhotoUUID" yaml:"-"`
Links []Link `gorm:"foreignkey:ShareUID;association_foreignkey:PhotoUID" json:"Links" yaml:"-"`
Keywords []Keyword `json:"-" yaml:"-"`
Albums []Album `json:"-" yaml:"-"`
Files []File `yaml:"-"`
Labels []PhotoLabel `yaml:"-"`
CreatedAt time.Time `yaml:"Created"`
UpdatedAt time.Time `yaml:"Updated"`
EditedAt *time.Time `yaml:"Edited,omitempty"`
DeletedAt *time.Time `sql:"index" yaml:"Deleted,omitempty"`
CreatedAt time.Time `yaml:"CreatedAt,omitempty"`
UpdatedAt time.Time `yaml:"UpdatedAt,omitempty"`
EditedAt *time.Time `yaml:"EditedAt,omitempty"`
DeletedAt *time.Time `sql:"index" yaml:"DeletedAt,omitempty"`
}
// SavePhotoForm saves a model in the database using form data.
@ -102,7 +102,7 @@ func SavePhotoForm(model Photo, form form.Photo, geoApi string) error {
}
if err := model.UpdateTitle(model.ClassifyLabels()); err != nil {
log.Warnf("%s (%s)", err.Error(), model.PhotoUUID)
log.Warnf("%s (%s)", err.Error(), model.PhotoUID)
}
if err := model.IndexKeywords(); err != nil {
@ -136,7 +136,7 @@ func (m *Photo) Save() error {
m.UpdateYearMonth()
if err := m.UpdateTitle(labels); err != nil {
log.Warnf("%s (%s)", err.Error(), m.PhotoUUID)
log.Warnf("%s (%s)", err.Error(), m.PhotoUID)
}
if m.DetailsLoaded() {
@ -178,7 +178,7 @@ func (m *Photo) ClassifyLabels() classify.Labels {
return result
}
// BeforeCreate creates a random UUID if needed before inserting a new row to the database.
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func (m *Photo) BeforeCreate(scope *gorm.Scope) error {
if m.TakenAt.IsZero() || m.TakenAtLocal.IsZero() {
now := time.Now()
@ -192,11 +192,11 @@ func (m *Photo) BeforeCreate(scope *gorm.Scope) error {
}
}
if rnd.IsPPID(m.PhotoUUID, 'p') {
if rnd.IsPPID(m.PhotoUID, 'p') {
return nil
}
return scope.SetColumn("PhotoUUID", rnd.PPID('p'))
return scope.SetColumn("PhotoUID", rnd.PPID('p'))
}
// BeforeSave ensures the existence of TakenAt properties before indexing or updating a photo
@ -219,7 +219,7 @@ func (m *Photo) BeforeSave(scope *gorm.Scope) error {
// IndexKeywords adds given keywords to the photo entry
func (m *Photo) IndexKeywords() error {
if !m.DetailsLoaded() {
return fmt.Errorf("photo: can't index keywords, details not loaded (%s)", m.PhotoUUID)
return fmt.Errorf("photo: can't index keywords, details not loaded (%s)", m.PhotoUID)
}
db := Db()
@ -289,7 +289,7 @@ func (m *Photo) PreloadAlbums() {
q := Db().NewScope(nil).DB().
Table("albums").
Select(`albums.*`).
Joins("JOIN photos_albums ON photos_albums.album_uuid = albums.album_uuid AND photos_albums.photo_uuid = ?", m.PhotoUUID).
Joins("JOIN photos_albums ON photos_albums.album_uid = albums.album_uid AND photos_albums.photo_uid = ?", m.PhotoUID).
Where("albums.deleted_at IS NULL").
Order("albums.album_name ASC")
@ -304,9 +304,9 @@ func (m *Photo) PreloadMany() {
m.PreloadAlbums()
}
// HasID checks if the photo has a database id and uuid.
// HasID checks if the photo has a database id and uid.
func (m *Photo) HasID() bool {
return m.ID > 0 && m.PhotoUUID != ""
return m.ID > 0 && m.PhotoUID != ""
}
// NoLocation checks if the photo has no location
@ -576,7 +576,7 @@ func (m *Photo) DeletePermanently() error {
Db().Unscoped().Delete(File{}, "photo_id = ?", m.ID)
Db().Unscoped().Delete(PhotoKeyword{}, "photo_id = ?", m.ID)
Db().Unscoped().Delete(PhotoLabel{}, "photo_id = ?", m.ID)
Db().Unscoped().Delete(PhotoAlbum{}, "photo_uuid = ?", m.PhotoUUID)
Db().Unscoped().Delete(PhotoAlbum{}, "photo_uid = ?", m.PhotoUID)
return Db().Unscoped().Delete(m).Error
}

Some files were not shown because too many files have changed in this diff Show more