Merge remote-tracking branch 'origin/develop' into develop
This commit is contained in:
commit
f5b80ae395
|
@ -13,8 +13,10 @@ features:
|
|||
edit: true
|
||||
share: true
|
||||
library:
|
||||
rescan: false
|
||||
raw: false
|
||||
thumbs: false
|
||||
group: true
|
||||
rescan: false
|
||||
move: false
|
||||
private: true
|
||||
review: true
|
||||
group: true
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
INSERT INTO cameras (id, camera_slug, camera_model, camera_make, camera_type, camera_description, camera_notes, created_at, updated_at, deleted_at) VALUES (1, 'unknown', 'Unknown', '', '', '', '', '2020-01-06 02:06:29', '2020-01-06 02:07:26', null);
|
||||
INSERT INTO cameras (id, camera_slug, camera_model, camera_make, camera_type, camera_description, camera_notes, created_at, updated_at, deleted_at) VALUES (2, 'apple-iphone-se', 'iPhone SE', 'Apple', '', '', '', '2020-01-06 02:06:30', '2020-01-06 02:07:28', null);
|
||||
INSERT INTO cameras (id, camera_slug, camera_model, camera_make, camera_type, camera_description, camera_notes, created_at, updated_at, deleted_at) VALUES (3, 'canon-eos-5d', 'EOS 5D', 'Canon', '', '', '', '2020-01-06 02:06:32', '2020-01-06 02:06:32', null);
|
||||
INSERT INTO cameras (id, camera_slug, camera_model, camera_make, camera_type, camera_description, camera_notes, created_at, updated_at, deleted_at) VALUES (4, 'canon-eos-7d', 'EOS 7D', 'Canon', '', '', '', '2020-01-06 02:06:33', '2020-01-06 02:06:33', null);
|
||||
|
@ -17,12 +16,12 @@ INSERT INTO files (id, photo_id, photo_uuid, file_uuid, file_name, file_primary,
|
|||
INSERT INTO files (id, photo_id, photo_uuid, file_uuid, file_name, file_primary, file_hash, file_missing) VALUES (3, '2', '655', 'fq8ev8t1x0bwje4e', 'exampleXmpFile.xmp', 0, '125xxx', 0);
|
||||
INSERT INTO files (id, photo_id, photo_uuid, file_uuid, file_name, file_primary, file_hash, file_missing) VALUES (4, '5', '658', 'fq8ev9c3sp88uwzq', 'bridge.jpg', 1, '126xxx', 0);
|
||||
INSERT INTO files (id, photo_id, photo_uuid, file_uuid, file_name, file_primary, file_hash, file_missing) VALUES (5, '6', '659', 'fq8evan3urz3i48d', 'reunion.jpg', 1, '127xxx', 0);
|
||||
INSERT INTO photos (id, photo_uuid, photo_year, photo_month, photo_lat, photo_lng) VALUES (1, '654', 2790, 2, '48.519235', '9.057996666666666');
|
||||
INSERT INTO photos (id, photo_uuid, photo_year, photo_month, photo_lat, photo_lng) VALUES (2, '655', 2790, 2, '48.519235', '9.057996666666666');
|
||||
INSERT INTO photos (id, photo_uuid, photo_year, photo_month, photo_lat, photo_lng) VALUES (3, '656', 1990, 3, '48.519235', '9.057996666666666');
|
||||
INSERT INTO photos (id, photo_uuid, photo_year, photo_month, photo_lat, photo_lng) VALUES (4, '657', 1990, 4, '48.519235', '9.057996666666666');
|
||||
INSERT INTO photos (id, photo_uuid, taken_at, photo_lat, photo_lng, photo_title) VALUES (5, '658', '2014-07-17 15:42:12', '48.519235', '9.057996666666666', 'Neckarbrücke');
|
||||
INSERT INTO photos (id, photo_uuid, taken_at, photo_lat, photo_lng, photo_title) VALUES (6, '659', '2015-11-11 09:07:18', '-21.34263611111111', '55.466944444444444', 'Reunion');
|
||||
INSERT INTO photos (id, photo_uuid, photo_year, photo_month, photo_lat, photo_lng) VALUES (1, '654', 2790, 2, 48.519234, 9.057997);
|
||||
INSERT INTO photos (id, photo_uuid, photo_year, photo_month, photo_lat, photo_lng) VALUES (2, '655', 2790, 2, 48.519234, 9.057997);
|
||||
INSERT INTO photos (id, photo_uuid, photo_year, photo_month, photo_lat, photo_lng) VALUES (3, '656', 1990, 3, 48.519234, 9.057997);
|
||||
INSERT INTO photos (id, photo_uuid, photo_year, photo_month, photo_lat, photo_lng) VALUES (4, '657', 1990, 4, 48.519234, 9.057997);
|
||||
INSERT INTO photos (id, photo_uuid, taken_at, photo_lat, photo_lng, photo_title) VALUES (5, '658', '2014-07-17 15:42:12', 48.519235, 9.05799666, 'Neckarbrücke');
|
||||
INSERT INTO photos (id, photo_uuid, taken_at, photo_lat, photo_lng, photo_title) VALUES (6, '659', '2015-11-11 09:07:18', -21.342636, 55.466944, 'Reunion');
|
||||
INSERT INTO keywords (id, keyword, skip) VALUES (1, 'bridge', 0);
|
||||
INSERT INTO keywords (id, keyword, skip) VALUES (2, 'beach', 0);
|
||||
INSERT INTO photos_keywords (photo_id, keyword_id) VALUES (5, 1);
|
||||
|
|
|
@ -27,6 +27,8 @@ services:
|
|||
PHOTOPRISM_PID_FILENAME: "photoprism.pid"
|
||||
PHOTOPRISM_LOG_FILENAME: "photoprism.log"
|
||||
PHOTOPRISM_DETACH_SERVER: "true"
|
||||
PHOTOPRISM_UPLOAD_NSFW: "false"
|
||||
PHOTOPRISM_DETECT_NSFW: "true"
|
||||
CODECOV_TOKEN:
|
||||
CODECOV_ENV:
|
||||
CODECOV_URL:
|
||||
|
|
|
@ -23,7 +23,7 @@ services:
|
|||
PHOTOPRISM_PUBLIC: "false"
|
||||
PHOTOPRISM_EXPERIMENTAL: "true"
|
||||
PHOTOPRISM_UPLOAD_NSFW: "false"
|
||||
PHOTOPRISM_HIDE_NSFW: "false"
|
||||
PHOTOPRISM_DETECT_NSFW: "true"
|
||||
PHOTOPRISM_SERVER_MODE: "debug"
|
||||
PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets"
|
||||
PHOTOPRISM_CACHE_PATH: "/go/src/github.com/photoprism/photoprism/assets/cache"
|
||||
|
|
|
@ -11,7 +11,7 @@ ENV PHOTOPRISM_READONLY false
|
|||
ENV PHOTOPRISM_PUBLIC true
|
||||
ENV PHOTOPRISM_EXPERIMENTAL true
|
||||
ENV PHOTOPRISM_UPLOAD_NSFW false
|
||||
ENV PHOTOPRISM_HIDE_NSFW false
|
||||
ENV PHOTOPRISM_DETECT_NSFW true
|
||||
ENV PHOTOPRISM_THUMB_QUALITY 95
|
||||
ENV PHOTOPRISM_THUMB_SIZE 3840
|
||||
ENV PHOTOPRISM_THUMB_LIMIT 3840
|
||||
|
|
|
@ -13,6 +13,12 @@ services:
|
|||
- seccomp:unconfined
|
||||
ports:
|
||||
- 2342:2342 # [local port]:[container port]
|
||||
# - 4000:4000 # Internal database (MySQL compatible)
|
||||
healthcheck: # Optional
|
||||
test: "photoprism status"
|
||||
interval: 60s
|
||||
timeout: 15s
|
||||
retries: 5
|
||||
environment: # Run "photoprism help" and "photoprism config" too see all config options and current values
|
||||
PHOTOPRISM_URL: "https://demo.photoprism.org/"
|
||||
PHOTOPRISM_TITLE: "PhotoPrism"
|
||||
|
@ -21,7 +27,7 @@ services:
|
|||
PHOTOPRISM_AUTHOR: "Anonymous"
|
||||
PHOTOPRISM_TWITTER: "@browseyourlife"
|
||||
PHOTOPRISM_UPLOAD_NSFW: "true"
|
||||
PHOTOPRISM_HIDE_NSFW: "false"
|
||||
PHOTOPRISM_DETECT_NSFW: "false"
|
||||
PHOTOPRISM_EXPERIMENTAL: "false"
|
||||
PHOTOPRISM_DEBUG: "false"
|
||||
PHOTOPRISM_READONLY: "false"
|
||||
|
|
|
@ -12,7 +12,8 @@ services:
|
|||
restart: unless-stopped
|
||||
ports:
|
||||
- 2342:2342 # [local port]:[container port]
|
||||
healthcheck:
|
||||
# - 4000:4000 # Internal database (MySQL compatible)
|
||||
healthcheck: # Optional
|
||||
test: "photoprism status"
|
||||
interval: 60s
|
||||
timeout: 15s
|
||||
|
@ -25,7 +26,7 @@ services:
|
|||
PHOTOPRISM_AUTHOR: "Anonymous"
|
||||
PHOTOPRISM_TWITTER: "@browseyourlife"
|
||||
PHOTOPRISM_UPLOAD_NSFW: "true"
|
||||
PHOTOPRISM_HIDE_NSFW: "false"
|
||||
PHOTOPRISM_DETECT_NSFW: "true"
|
||||
PHOTOPRISM_EXPERIMENTAL: "false"
|
||||
PHOTOPRISM_DEBUG: "false"
|
||||
PHOTOPRISM_READONLY: "false"
|
||||
|
|
|
@ -21,7 +21,6 @@ import VueLuxon from "vue-luxon";
|
|||
import VueFilters from "vue2-filters";
|
||||
import VueFullscreen from "vue-fullscreen";
|
||||
import VueInfiniteScroll from "vue-infinite-scroll";
|
||||
import VueLongClick from "common/longclick";
|
||||
|
||||
// Initialize helpers
|
||||
const viewer = new Viewer();
|
||||
|
@ -53,8 +52,6 @@ Vue.use(GetTextPlugin, {
|
|||
defaultLanguage: Vue.config.language,
|
||||
});
|
||||
|
||||
Vue.directive("longclick", VueLongClick({delay: 450, interval: 0}));
|
||||
|
||||
Vue.use(VueLuxon);
|
||||
Vue.use(VueInfiniteScroll);
|
||||
Vue.use(VueFullscreen);
|
||||
|
|
|
@ -18,10 +18,12 @@ class Config {
|
|||
title: "PhotoPrism",
|
||||
};
|
||||
|
||||
this.$vuetify = null;
|
||||
|
||||
Event.subscribe("config.updated", (ev, data) => this.setValues(data));
|
||||
Event.subscribe("count", (ev, data) => this.onCount(ev, data));
|
||||
|
||||
if(this.hasValue("settings")) {
|
||||
if (this.hasValue("settings")) {
|
||||
this.setTheme(this.getValue("settings").theme);
|
||||
} else {
|
||||
this.setTheme("default");
|
||||
|
@ -41,6 +43,10 @@ class Config {
|
|||
}
|
||||
}
|
||||
|
||||
if (values.settings) {
|
||||
this.setTheme(values.settings.theme);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -73,14 +79,17 @@ class Config {
|
|||
this.values.count;
|
||||
}
|
||||
|
||||
updateSettings(settings, $vuetify) {
|
||||
this.setValue("settings", settings);
|
||||
this.setTheme(settings.theme);
|
||||
$vuetify.theme = this.theme;
|
||||
setVuetify(instance) {
|
||||
this.$vuetify = instance;
|
||||
}
|
||||
|
||||
setTheme(name) {
|
||||
this.theme = themes[name] ? themes[name] : themes["default"];
|
||||
|
||||
if (this.$vuetify) {
|
||||
this.$vuetify.theme = this.theme;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
VueJS long click directive
|
||||
|
||||
Based on https://github.com/ittus/vue-long-click (MIT License)
|
||||
Author: Thang Minh Vu (https://github.com/ittus)
|
||||
*/
|
||||
export default ({delay = 400, interval = 50}) => ({
|
||||
bind: function (el, binding, vNode) {
|
||||
if (typeof binding.value !== "function") {
|
||||
const compName = vNode.context.name;
|
||||
let warn = `[longclick:] provided expression '${binding.expression}' is not a function, but has to be`;
|
||||
if (compName) {
|
||||
warn += `Found in component '${compName}' `;
|
||||
}
|
||||
console.warn(warn) // eslint-disable-line
|
||||
}
|
||||
|
||||
let pressTimer = null;
|
||||
let pressInterval = null;
|
||||
|
||||
const start = (e) => {
|
||||
if (e.type === "click" && e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pressTimer === null) {
|
||||
pressTimer = setTimeout(() => {
|
||||
if (interval && interval > 0) {
|
||||
pressInterval = setInterval(() => {
|
||||
handler();
|
||||
}, interval);
|
||||
}
|
||||
handler();
|
||||
}, delay);
|
||||
}
|
||||
};
|
||||
|
||||
// Cancel Timeout
|
||||
const cancel = () => {
|
||||
if (pressTimer !== null) {
|
||||
clearTimeout(pressTimer);
|
||||
pressTimer = null;
|
||||
}
|
||||
if (pressInterval) {
|
||||
clearInterval(pressInterval);
|
||||
pressInterval = null;
|
||||
}
|
||||
};
|
||||
// Run Function
|
||||
const handler = (e) => {
|
||||
binding.value(e);
|
||||
}
|
||||
|
||||
;["mousedown", "touchstart"].forEach(e => el.addEventListener(e, start))
|
||||
;["click", "mouseout", "touchend", "touchcancel"].forEach(e => el.addEventListener(e, cancel));
|
||||
},
|
||||
});
|
|
@ -76,7 +76,7 @@ class Viewer {
|
|||
|
||||
this.gallery = gallery;
|
||||
|
||||
gallery.listen('beforeChange', function() {
|
||||
gallery.listen("beforeChange", function() {
|
||||
Event.publish("viewer.change", {gallery: gallery, item: gallery.currItem});
|
||||
});
|
||||
|
||||
|
|
|
@ -154,8 +154,8 @@
|
|||
{value: 'imported', text: this.$gettext('Recently imported')},
|
||||
{value: 'newest', text: this.$gettext('Newest first')},
|
||||
{value: 'oldest', text: this.$gettext('Oldest first')},
|
||||
{value: 'similar', text: this.$gettext('Similar')},
|
||||
{value: 'relevance', text: this.$gettext('Relevance')},
|
||||
{value: 'similar', text: this.$gettext('Group by similarity')},
|
||||
{value: 'relevance', text: this.$gettext('Most relevant')},
|
||||
],
|
||||
},
|
||||
labels: {
|
||||
|
|
|
@ -79,7 +79,7 @@
|
|||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile :to="{name: 'photos', query: { q: 'review:true' }}" :exact="true" @click="">
|
||||
<v-list-tile to="/review" @click="" v-if="config.settings.library.review">
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title>
|
||||
<translate>Review</translate>
|
||||
|
@ -87,6 +87,14 @@
|
|||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile to="/private" @click="" v-if="config.settings.library.private">
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title>
|
||||
<translate>Private</translate>
|
||||
</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile to="/archive" @click="" class="p-navigation-archive" v-if="$config.feature('archive')">
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title>
|
||||
|
@ -109,6 +117,19 @@
|
|||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile v-if="mini && config.settings.library.private" to="/private" @click=""
|
||||
class="p-navigation-private">
|
||||
<v-list-tile-action>
|
||||
<v-icon>lock</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title>
|
||||
<translate>Private</translate>
|
||||
</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile v-if="mini && $config.feature('archive')" to="/archive" @click=""
|
||||
class="p-navigation-archive">
|
||||
<v-list-tile-action>
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
>
|
||||
<v-hover>
|
||||
<v-card tile slot-scope="{ hover }"
|
||||
@contextmenu="contextMenu($event, photo, index)"
|
||||
@contextmenu="onContextMenu($event, index)"
|
||||
:dark="$clipboard.has(photo)"
|
||||
:class="$clipboard.has(photo) ? 'elevation-10 ma-0 accent darken-1 white--text' : 'elevation-0 ma-1 accent lighten-3'">
|
||||
<v-img
|
||||
|
@ -32,8 +32,8 @@
|
|||
v-bind:class="{ selected: $clipboard.has(photo) }"
|
||||
style="cursor: pointer;"
|
||||
class="accent lighten-2"
|
||||
v-longclick="longClick"
|
||||
@click="onClick($event, photo, index)"
|
||||
@mousedown="onMouseDown($event, index)"
|
||||
@click.stop.prevent="onClick($event, index)"
|
||||
>
|
||||
<v-layout
|
||||
slot="placeholder"
|
||||
|
@ -45,26 +45,31 @@
|
|||
<v-progress-circular indeterminate color="accent lighten-5"></v-progress-circular>
|
||||
</v-layout>
|
||||
|
||||
<v-btn v-if="hover || selection.length > 0" :flat="!hover" :ripple="false"
|
||||
icon large absolute
|
||||
:class="$clipboard.has(photo) ? 'p-photo-select' : 'p-photo-select opacity-50'"
|
||||
@click.stop.prevent="onSelect($event, photo, index)">
|
||||
<v-btn v-if="hidePrivate && photo.PhotoPrivate"
|
||||
icon flat large absolute
|
||||
class="p-photo-private opacity-75">
|
||||
<v-icon color="white">lock</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn v-if="hover || selection.length && $clipboard.has(photo)"
|
||||
icon flat large absolute
|
||||
:class="selection.length && $clipboard.has(photo) ? 'p-photo-select' : 'p-photo-select opacity-50'"
|
||||
@click.stop.prevent="onSelect($event, index)">
|
||||
<v-icon v-if="selection.length && $clipboard.has(photo)" color="white"
|
||||
class="t-select t-on">check_circle
|
||||
</v-icon>
|
||||
<v-icon v-else color="accent lighten-3" class="t-select t-off">radio_button_off</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn :flat="!hover" :ripple="false"
|
||||
icon large absolute
|
||||
<v-btn icon flat large absolute
|
||||
:class="photo.PhotoFavorite ? '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-else color="accent lighten-3" class="t-like t-off">favorite_border</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn v-if="photo.Files.length > 1" :flat="!hover" :ripple="false"
|
||||
icon large absolute class="p-photo-merged opacity-75"
|
||||
<v-btn v-if="photo.Files.length > 1"
|
||||
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>
|
||||
|
@ -115,51 +120,44 @@
|
|||
data() {
|
||||
return {
|
||||
showLocation: this.$config.settings().features.places,
|
||||
wasLong: false,
|
||||
hidePrivate: this.$config.settings().library.private,
|
||||
mouseDown: {
|
||||
index: -1,
|
||||
timeStamp: -1,
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
longClick() {
|
||||
this.wasLong = true;
|
||||
},
|
||||
onSelect(ev, model, index) {
|
||||
onSelect(ev, index) {
|
||||
if (ev.shiftKey) {
|
||||
this.selectRange(index);
|
||||
} else {
|
||||
this.$clipboard.toggle(model);
|
||||
this.$clipboard.toggle(this.photos[index]);
|
||||
}
|
||||
|
||||
this.wasLong = false;
|
||||
},
|
||||
onClick(ev, model, index) {
|
||||
if (this.wasLong || this.selection.length > 0) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
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 (this.wasLong || ev.shiftKey) {
|
||||
if (longClick || this.selection.length > 0) {
|
||||
if (longClick || ev.shiftKey) {
|
||||
this.selectRange(index);
|
||||
} else {
|
||||
this.$clipboard.toggle(model);
|
||||
this.$clipboard.toggle(this.photos[index]);
|
||||
}
|
||||
} else {
|
||||
this.openPhoto(index, false);
|
||||
}
|
||||
|
||||
this.wasLong = false;
|
||||
},
|
||||
contextMenu(ev, model, index) {
|
||||
onContextMenu(ev, index) {
|
||||
if (this.$isMobile) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this.wasLong) {
|
||||
this.selectRange(index);
|
||||
} else {
|
||||
this.$clipboard.toggle(model);
|
||||
}
|
||||
this.selectRange(index);
|
||||
}
|
||||
|
||||
this.wasLong = false;
|
||||
},
|
||||
selectRange(index) {
|
||||
this.$clipboard.addRange(index, this.photos);
|
||||
|
|
|
@ -48,6 +48,19 @@
|
|||
>
|
||||
<v-icon>edit</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
fab
|
||||
dark
|
||||
small
|
||||
:title="labels.private"
|
||||
color="private"
|
||||
:disabled="selection.length === 0"
|
||||
@click.stop="batchPrivate"
|
||||
v-if="context !== 'archive' && config.settings.library.private"
|
||||
class="p-photo-clipboard-private"
|
||||
>
|
||||
<v-icon>lock</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
fab
|
||||
dark
|
||||
|
@ -130,9 +143,9 @@
|
|||
<p-photo-album-dialog :show="dialog.album" @cancel="dialog.album = false"
|
||||
@confirm="addToAlbum"></p-photo-album-dialog>
|
||||
<p-photo-archive-dialog :show="dialog.archive" @cancel="dialog.archive = false"
|
||||
@confirm="batchArchivePhotos"></p-photo-archive-dialog>
|
||||
@confirm="batchArchivePhotos"></p-photo-archive-dialog>
|
||||
<p-photo-share-dialog :show="dialog.share" :selection="selection" :album="album" @cancel="dialog.share = false"
|
||||
@confirm="dialog.share = false"></p-photo-share-dialog>
|
||||
@confirm="dialog.share = false"></p-photo-share-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
@ -150,6 +163,7 @@
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
config: this.$config.values,
|
||||
expanded: false,
|
||||
dialog: {
|
||||
archive: false,
|
||||
|
@ -158,6 +172,7 @@
|
|||
},
|
||||
labels: {
|
||||
share: this.$gettext("Share"),
|
||||
private: this.$gettext("Change private flag"),
|
||||
edit: this.$gettext("Edit"),
|
||||
story: this.$gettext("Story"),
|
||||
addToAlbum: this.$gettext("Add to album"),
|
||||
|
@ -182,6 +197,12 @@
|
|||
Notify.success(this.$gettext("Photos archived"));
|
||||
this.clearClipboard();
|
||||
},
|
||||
batchPrivate() {
|
||||
Api.post("batch/photos/private", {"photos": this.selection}).then(() => this.onPrivateSaved());
|
||||
},
|
||||
onPrivateSaved() {
|
||||
this.clearClipboard();
|
||||
},
|
||||
batchRestorePhotos() {
|
||||
Api.post("batch/photos/restore", {"photos": this.selection}).then(() => this.onRestored());
|
||||
},
|
||||
|
@ -198,7 +219,7 @@
|
|||
this.clearClipboard();
|
||||
},
|
||||
removeFromAlbum() {
|
||||
if(!this.album) {
|
||||
if (!this.album) {
|
||||
this.$notify.error(this.$gettext("remove failed: unknown album"));
|
||||
return
|
||||
}
|
||||
|
@ -213,7 +234,7 @@
|
|||
this.clearClipboard();
|
||||
},
|
||||
download() {
|
||||
if(this.selection.length === 1) {
|
||||
if (this.selection.length === 1) {
|
||||
this.onDownload(`/api/v1/photos/${this.selection[0]}/download`);
|
||||
} else {
|
||||
Api.post("zip", {"photos": this.selection}).then(r => {
|
||||
|
|
|
@ -13,9 +13,9 @@
|
|||
<td style="user-select: none;">
|
||||
<v-img class="accent lighten-2" style="cursor: pointer" aspect-ratio="1"
|
||||
:src="props.item.getThumbnailUrl('tile_50')"
|
||||
v-longclick="longClick"
|
||||
@contextmenu="contextMenu($event, props.item, props.index)"
|
||||
@click="onClick($event, props.item, props.index)"
|
||||
@mousedown="onMouseDown($event, props.index)"
|
||||
@contextmenu="onContextMenu($event, props.index)"
|
||||
@click.stop.prevent="onClick($event, props.index)"
|
||||
>
|
||||
<v-layout
|
||||
slot="placeholder"
|
||||
|
@ -28,21 +28,18 @@
|
|||
color="accent lighten-5"></v-progress-circular>
|
||||
</v-layout>
|
||||
|
||||
<v-btn v-if="selection.length > 0" :flat="true" :ripple="false"
|
||||
icon large absolute
|
||||
class="p-photo-select">
|
||||
<v-icon v-if="selection.length && $clipboard.has(props.item)" color="white"
|
||||
class="t-select t-on">check_circle
|
||||
</v-icon>
|
||||
<v-btn v-if="selection.length && $clipboard.has(props.item)" :flat="true" :ripple="false"
|
||||
icon large absolute class="p-photo-select">
|
||||
<v-icon color="white" class="t-select t-on">check_circle</v-icon>
|
||||
</v-btn>
|
||||
</v-img>
|
||||
</td>
|
||||
<td class="p-photo-desc p-pointer" @click.exact="openPhoto(props.index)" style="user-select: none;">
|
||||
<td class="p-photo-desc p-pointer" @click.exact="editPhoto(props.index)" style="user-select: none;">
|
||||
{{ props.item.PhotoTitle }}
|
||||
</td>
|
||||
<td class="p-photo-desc hidden-xs-only" :title="props.item.TakenAt | luxon:format('dd/MM/yyyy HH:mm:ss')">
|
||||
<td class="p-photo-desc hidden-xs-only" :title="props.item.getDateString()">
|
||||
<button @click.stop.prevent="editPhoto(props.index)" style="user-select: none;">
|
||||
{{ props.item.TakenAt | luxon:locale }}
|
||||
{{ props.item.TakenAt | luxon:locale('DATE_MED') }}
|
||||
</button>
|
||||
</td>
|
||||
<td class="p-photo-desc hidden-sm-and-down" style="user-select: none;">
|
||||
|
@ -59,11 +56,16 @@
|
|||
{{ props.item.getLocation() }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<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-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-else color="accent lighten-4">favorite_border</v-icon>
|
||||
<v-icon v-else color="accent lighten-3">favorite_border</v-icon>
|
||||
</v-btn>
|
||||
</td>
|
||||
</template>
|
||||
|
@ -89,10 +91,14 @@
|
|||
{text: this.$gettext('Taken'), class: 'hidden-xs-only', value: 'TakenAt'},
|
||||
{text: this.$gettext('Camera'), class: 'hidden-sm-and-down', value: 'CameraModel'},
|
||||
{text: this.$gettext('Location'), class: 'hidden-xs-only', value: 'LocLabel'},
|
||||
{text: this.$gettext('Favorite'), value: 'PhotoFavorite', align: 'left'},
|
||||
{text: '', value: '', sortable: false, align: 'center'},
|
||||
],
|
||||
showLocation: this.$config.settings().features.places,
|
||||
wasLong: false,
|
||||
hidePrivate: this.$config.settings().library.private,
|
||||
mouseDown: {
|
||||
index: -1,
|
||||
timeStamp: -1,
|
||||
},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
|
@ -110,32 +116,36 @@
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
longClick() {
|
||||
this.wasLong = true;
|
||||
},
|
||||
onClick(ev, model, index) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this.wasLong || ev.shiftKey) {
|
||||
onSelect(ev, index) {
|
||||
if (ev.shiftKey) {
|
||||
this.selectRange(index);
|
||||
} else {
|
||||
this.$clipboard.toggle(model);
|
||||
this.$clipboard.toggle(this.photos[index]);
|
||||
}
|
||||
|
||||
this.wasLong = false;
|
||||
},
|
||||
contextMenu(ev, model, index) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
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 (this.wasLong) {
|
||||
this.selectRange(index);
|
||||
if (longClick || this.selection.length > 0) {
|
||||
if (longClick || ev.shiftKey) {
|
||||
this.selectRange(index);
|
||||
} else {
|
||||
this.$clipboard.toggle(this.photos[index]);
|
||||
}
|
||||
} else {
|
||||
this.$clipboard.toggle(model);
|
||||
this.openPhoto(index, false);
|
||||
}
|
||||
},
|
||||
onContextMenu(ev, index) {
|
||||
if (this.$isMobile) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.selectRange(index);
|
||||
}
|
||||
|
||||
this.wasLong = false;
|
||||
},
|
||||
selectRange(index) {
|
||||
this.$clipboard.addRange(index, this.photos);
|
||||
|
|
|
@ -23,15 +23,15 @@
|
|||
>
|
||||
<v-hover>
|
||||
<v-card tile slot-scope="{ hover }"
|
||||
@contextmenu="contextMenu($event, photo, index)"
|
||||
@contextmenu="onContextMenu($event, index)"
|
||||
:class="$clipboard.has(photo) ? 'elevation-10 ma-0' : 'elevation-0 ma-1'"
|
||||
:title="photo.PhotoTitle">
|
||||
<v-img :src="photo.getThumbnailUrl('tile_224')"
|
||||
aspect-ratio="1"
|
||||
class="accent lighten-2"
|
||||
style="cursor: pointer"
|
||||
v-longclick="longClick"
|
||||
@click="onClick($event, photo, index)"
|
||||
@mousedown="onMouseDown($event, index)"
|
||||
@click.stop.prevent="onClick($event, index)"
|
||||
>
|
||||
<v-layout
|
||||
slot="placeholder"
|
||||
|
@ -44,38 +44,35 @@
|
|||
color="accent lighten-5"></v-progress-circular>
|
||||
</v-layout>
|
||||
|
||||
<v-btn v-if="hover || selection.length > 0" :flat="!hover" :ripple="false"
|
||||
icon small absolute
|
||||
:class="$clipboard.has(photo) ? 'p-photo-select' : 'p-photo-select opacity-50'"
|
||||
@click.stop.prevent="onSelect($event, photo, index)">
|
||||
<v-btn v-if="hidePrivate && photo.PhotoPrivate"
|
||||
icon flat small absolute
|
||||
class="p-photo-private opacity-75">
|
||||
<v-icon color="white">lock</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn v-if="hover || selection.length && $clipboard.has(photo)"
|
||||
icon flat small absolute
|
||||
:class="selection.length && $clipboard.has(photo) ? 'p-photo-select' : 'p-photo-select opacity-50'"
|
||||
@click.stop.prevent="onSelect($event, index)">
|
||||
<v-icon v-if="selection.length && $clipboard.has(photo)" color="white"
|
||||
class="t-select t-on">check_circle
|
||||
</v-icon>
|
||||
<v-icon v-else color="accent lighten-3" class="t-select t-off">radio_button_off</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn :flat="!hover" :ripple="false"
|
||||
icon small absolute
|
||||
<v-btn icon flat small absolute
|
||||
:class="photo.PhotoFavorite ? '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-else color="accent lighten-3" class="t-like t-off">favorite_border</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn v-if="photo.Files.length > 1" :flat="!hover" :ripple="false"
|
||||
icon small absolute class="p-photo-merged opacity-75"
|
||||
<v-btn v-if="photo.Files.length > 1"
|
||||
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-if="hover" :flat="!hover" :ripple="false"
|
||||
icon small absolute
|
||||
class="p-photo-edit"
|
||||
@click.stop.prevent="editPhoto(index)">
|
||||
<v-icon color="white">edit</v-icon>
|
||||
</v-btn -->
|
||||
</v-img>
|
||||
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</v-flex>
|
||||
|
@ -94,51 +91,44 @@
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
wasLong: false,
|
||||
hidePrivate: this.$config.settings().library.private,
|
||||
mouseDown: {
|
||||
index: -1,
|
||||
timeStamp: -1,
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
longClick() {
|
||||
this.wasLong = true;
|
||||
},
|
||||
onSelect(ev, model, index) {
|
||||
onSelect(ev, index) {
|
||||
if (ev.shiftKey) {
|
||||
this.selectRange(index);
|
||||
} else {
|
||||
this.$clipboard.toggle(model);
|
||||
this.$clipboard.toggle(this.photos[index]);
|
||||
}
|
||||
|
||||
this.wasLong = false;
|
||||
},
|
||||
onClick(ev, model, index) {
|
||||
if (this.wasLong || this.selection.length > 0) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
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 (this.wasLong || ev.shiftKey) {
|
||||
if (longClick || this.selection.length > 0) {
|
||||
if (longClick || ev.shiftKey) {
|
||||
this.selectRange(index);
|
||||
} else {
|
||||
this.$clipboard.toggle(model);
|
||||
this.$clipboard.toggle(this.photos[index]);
|
||||
}
|
||||
} else {
|
||||
this.openPhoto(index, false);
|
||||
}
|
||||
|
||||
this.wasLong = false;
|
||||
},
|
||||
contextMenu(ev, model, index) {
|
||||
onContextMenu(ev, index) {
|
||||
if (this.$isMobile) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this.wasLong) {
|
||||
this.selectRange(index);
|
||||
} else {
|
||||
this.$clipboard.toggle(model);
|
||||
}
|
||||
this.selectRange(index);
|
||||
}
|
||||
|
||||
this.wasLong = false;
|
||||
},
|
||||
selectRange(index) {
|
||||
this.$clipboard.addRange(index, this.photos);
|
||||
|
|
|
@ -169,8 +169,8 @@
|
|||
{value: 'imported', text: this.$gettext('Recently imported')},
|
||||
{value: 'newest', text: this.$gettext('Newest first')},
|
||||
{value: 'oldest', text: this.$gettext('Oldest first')},
|
||||
{value: 'similar', text: this.$gettext('Similar')},
|
||||
{value: 'relevance', text: this.$gettext('Relevance')},
|
||||
{value: 'similar', text: this.$gettext('Group by similarity')},
|
||||
{value: 'relevance', text: this.$gettext('Most relevant')},
|
||||
],
|
||||
},
|
||||
labels: {
|
||||
|
|
|
@ -16,6 +16,12 @@
|
|||
left: 7px;
|
||||
}
|
||||
|
||||
#photoprism .p-photo-mosaic .p-photo-private,
|
||||
#photoprism .p-photo-cards .p-photo-private {
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
#photoprism .p-photo-mosaic .p-photo-edit,
|
||||
#photoprism .p-photo-cards .p-photo-edit {
|
||||
top: 4px;
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
<v-btn icon dark @click.stop="close">
|
||||
<v-icon>close</v-icon>
|
||||
</v-btn>
|
||||
<v-toolbar-title>{{ title }}</v-toolbar-title>
|
||||
<v-toolbar-title>{{ title }}
|
||||
<v-icon v-if="isPrivate" title="Private">lock</v-icon>
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-toolbar-items v-if="selection.length > 1">
|
||||
<v-btn icon disabled @click.stop="prev" :disabled="selected < 1">
|
||||
|
@ -84,6 +86,13 @@
|
|||
|
||||
this.$gettext("Edit Photo");
|
||||
},
|
||||
isPrivate: function () {
|
||||
if (this.model && this.model.PhotoPrivate && this.$config.settings().library.private) {
|
||||
return this.model.PhotoPrivate
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -355,7 +355,8 @@
|
|||
</v-btn>
|
||||
<v-btn color="secondary-dark" depressed dark @click.stop="save(false)"
|
||||
class="p-photo-dialog-confirm">
|
||||
<span>Apply</span>
|
||||
<span v-if="config.settings.library.review && model.PhotoQuality < 3">Approve</span>
|
||||
<span v-else>Apply</span>
|
||||
</v-btn>
|
||||
<v-btn color="secondary-dark" depressed dark @click.stop="save(true)"
|
||||
class="p-photo-dialog-confirm hidden-xs-only">
|
||||
|
|
|
@ -27,7 +27,6 @@ class Photo extends RestModel {
|
|||
PhotoFavorite: false,
|
||||
PhotoStory: false,
|
||||
PhotoPrivate: false,
|
||||
PhotoNSFW: false,
|
||||
PhotoResolution: 0,
|
||||
PhotoQuality: 0,
|
||||
PhotoLat: 0.0,
|
||||
|
@ -86,13 +85,13 @@ class Photo extends RestModel {
|
|||
|
||||
getColor() {
|
||||
switch (this.PhotoColor) {
|
||||
case "brown":
|
||||
case "black":
|
||||
case "white":
|
||||
case "grey":
|
||||
return "grey lighten-2";
|
||||
default:
|
||||
return this.PhotoColor + " lighten-4";
|
||||
case "brown":
|
||||
case "black":
|
||||
case "white":
|
||||
case "grey":
|
||||
return "grey lighten-2";
|
||||
default:
|
||||
return this.PhotoColor + " lighten-4";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -242,6 +241,12 @@ class Photo extends RestModel {
|
|||
}
|
||||
}
|
||||
|
||||
togglePrivate() {
|
||||
this.PhotoPrivate = !this.PhotoPrivate;
|
||||
|
||||
return Api.put(this.getEntityResource(), {PhotoPrivate: this.PhotoPrivate});
|
||||
}
|
||||
|
||||
setPrimary(fileUUID) {
|
||||
return Api.post(this.getEntityResource() + "/primary/" + fileUUID).then((r) => Promise.resolve(this.setValues(r.data)));
|
||||
}
|
||||
|
|
|
@ -92,10 +92,10 @@ class Thumb extends Model {
|
|||
if (!p.Files) return;
|
||||
|
||||
p.Files.forEach((f) => {
|
||||
if (f.FileType === "jpg") {
|
||||
result.push(this.fromFile(p, f));
|
||||
}
|
||||
if (f.FileType === "jpg") {
|
||||
result.push(this.fromFile(p, f));
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -124,7 +124,7 @@
|
|||
|
||||
if (photo.LocationID) {
|
||||
this.$router.push({name: "place", params: {q: "s2:" + photo.LocationID}});
|
||||
} else if (photo.PlaceID && photo.PlaceID !== "-") {
|
||||
} else if (photo.PlaceID.length > 3) {
|
||||
this.$router.push({name: "place", params: {q: "s2:" + photo.PlaceID}});
|
||||
}
|
||||
},
|
||||
|
@ -335,7 +335,7 @@
|
|||
|
||||
if (model) {
|
||||
for (let key in values) {
|
||||
if (values.hasOwnProperty(key)) {
|
||||
if (values.hasOwnProperty(key) && values[key] != null && typeof values[key] !== "object") {
|
||||
model[key] = values[key];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,15 +61,15 @@
|
|||
<v-hover>
|
||||
<v-card tile class="accent lighten-3"
|
||||
slot-scope="{ hover }"
|
||||
@contextmenu="contextMenu($event, album, index)"
|
||||
@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}}"
|
||||
>
|
||||
<v-img
|
||||
:src="album.getThumbnailUrl('tile_500')"
|
||||
v-longclick="longClick"
|
||||
@click="onClick($event, album, index)"
|
||||
@mousedown="onMouseDown($event, index)"
|
||||
@click="onClick($event, index)"
|
||||
aspect-ratio="1"
|
||||
class="accent lighten-2"
|
||||
>
|
||||
|
@ -84,10 +84,10 @@
|
|||
color="accent lighten-5"></v-progress-circular>
|
||||
</v-layout>
|
||||
|
||||
<v-btn v-if="hover || selection.length > 0" :flat="!hover" :ripple="false"
|
||||
<v-btn v-if="hover || selection.includes(album.AlbumUUID)" :flat="!hover" :ripple="false"
|
||||
icon large absolute
|
||||
:class="selection.includes(album.AlbumUUID) ? 'p-album-select' : 'p-album-select opacity-50'"
|
||||
@click.stop.prevent="onSelect($event, album, index)">
|
||||
@click.stop.prevent="onSelect($event, index)">
|
||||
<v-icon v-if="selection.includes(album.AlbumUUID)" color="white">check_circle
|
||||
</v-icon>
|
||||
<v-icon v-else color="accent lighten-3">radio_button_off</v-icon>
|
||||
|
@ -183,7 +183,10 @@
|
|||
search: this.$gettext("Search"),
|
||||
name: this.$gettext("Album Name"),
|
||||
},
|
||||
wasLong: false,
|
||||
mouseDown: {
|
||||
index: -1,
|
||||
timeStamp: -1,
|
||||
},
|
||||
lastId: "",
|
||||
};
|
||||
},
|
||||
|
@ -213,45 +216,40 @@
|
|||
|
||||
return (rangeEnd - rangeStart) + 1;
|
||||
},
|
||||
longClick() {
|
||||
this.wasLong = true;
|
||||
},
|
||||
onSelect(ev, model, index) {
|
||||
onSelect(ev, index) {
|
||||
if (ev.shiftKey) {
|
||||
this.selectRange(index, this.results);
|
||||
} else {
|
||||
this.toggleSelection(model.getId());
|
||||
this.toggleSelection(this.results[index].getId());
|
||||
}
|
||||
|
||||
this.wasLong = false;
|
||||
},
|
||||
onClick(ev, model, index) {
|
||||
if (this.wasLong || this.selection.length > 0) {
|
||||
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 (this.wasLong || ev.shiftKey) {
|
||||
if (longClick || ev.shiftKey) {
|
||||
this.selectRange(index, this.results);
|
||||
} else {
|
||||
this.toggleSelection(model.getId());
|
||||
this.toggleSelection(this.results[index].getId());
|
||||
}
|
||||
}
|
||||
|
||||
this.wasLong = false;
|
||||
},
|
||||
contextMenu(ev, model, index) {
|
||||
onContextMenu(ev, index) {
|
||||
if (this.$isMobile) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this.wasLong) {
|
||||
if(this.results[index]) {
|
||||
this.selectRange(index, this.results);
|
||||
} else {
|
||||
this.toggleSelection(model.getId());
|
||||
}
|
||||
}
|
||||
|
||||
this.wasLong = false;
|
||||
},
|
||||
clearQuery() {
|
||||
this.filter.q = '';
|
||||
|
|
|
@ -64,14 +64,14 @@
|
|||
<v-hover>
|
||||
<v-card tile class="accent lighten-3"
|
||||
slot-scope="{ hover }"
|
||||
@contextmenu="contextMenu($event, label, index)"
|
||||
@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)}}">
|
||||
<v-img
|
||||
:src="label.getThumbnailUrl('tile_500')"
|
||||
v-longclick="longClick"
|
||||
@click="onClick($event, label, index)"
|
||||
@mousedown="onMouseDown($event, index)"
|
||||
@click="onClick($event, index)"
|
||||
aspect-ratio="1"
|
||||
class="accent lighten-2"
|
||||
>
|
||||
|
@ -86,10 +86,10 @@
|
|||
color="accent lighten-5"></v-progress-circular>
|
||||
</v-layout>
|
||||
|
||||
<v-btn v-if="hover || selection.length > 0" :flat="!hover" :ripple="false"
|
||||
<v-btn v-if="hover || selection.includes(label.LabelUUID)" :flat="!hover" :ripple="false"
|
||||
icon large absolute
|
||||
:class="selection.includes(label.LabelUUID) ? 'p-label-select' : 'p-label-select opacity-50'"
|
||||
@click.stop.prevent="onSelect($event, label, index)">
|
||||
@click.stop.prevent="onSelect($event, index)">
|
||||
<v-icon v-if="selection.includes(label.LabelUUID)" color="white">check_circle
|
||||
</v-icon>
|
||||
<v-icon v-else color="accent lighten-3">radio_button_off</v-icon>
|
||||
|
@ -185,7 +185,10 @@
|
|||
name: this.$gettext("Label Name"),
|
||||
},
|
||||
titleRule: v => v.length <= 25 || this.$gettext("Name too long"),
|
||||
wasLong: false,
|
||||
mouseDown: {
|
||||
index: -1,
|
||||
timeStamp: -1,
|
||||
},
|
||||
lastId: "",
|
||||
};
|
||||
},
|
||||
|
@ -215,45 +218,40 @@
|
|||
|
||||
return (rangeEnd - rangeStart) + 1;
|
||||
},
|
||||
longClick() {
|
||||
this.wasLong = true;
|
||||
},
|
||||
onSelect(ev, model, index) {
|
||||
onSelect(ev, index) {
|
||||
if (ev.shiftKey) {
|
||||
this.selectRange(index, this.results);
|
||||
} else {
|
||||
this.toggleSelection(model.getId());
|
||||
this.toggleSelection(this.results[index].getId());
|
||||
}
|
||||
|
||||
this.wasLong = false;
|
||||
},
|
||||
onClick(ev, model, index) {
|
||||
if (this.wasLong || this.selection.length > 0) {
|
||||
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 (this.wasLong || ev.shiftKey) {
|
||||
if (longClick || ev.shiftKey) {
|
||||
this.selectRange(index, this.results);
|
||||
} else {
|
||||
this.toggleSelection(model.getId());
|
||||
this.toggleSelection(this.results[index].getId());
|
||||
}
|
||||
}
|
||||
|
||||
this.wasLong = false;
|
||||
},
|
||||
contextMenu(ev, model, index) {
|
||||
onContextMenu(ev, index) {
|
||||
if (this.$isMobile) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this.wasLong) {
|
||||
if(this.results[index]) {
|
||||
this.selectRange(index, this.results);
|
||||
} else {
|
||||
this.toggleSelection(model.getId());
|
||||
}
|
||||
}
|
||||
|
||||
this.wasLong = false;
|
||||
},
|
||||
onSave(label) {
|
||||
label.update();
|
||||
|
|
|
@ -17,9 +17,10 @@
|
|||
<v-layout wrap align-top class="pb-3">
|
||||
<v-flex xs12 sm6 lg4 class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
@change="onChange"
|
||||
:disabled="busy"
|
||||
class="ma-0 pa-0"
|
||||
v-model="options.raw"
|
||||
v-model="settings.library.raw"
|
||||
color="secondary-dark"
|
||||
:label="labels.raw"
|
||||
hint="RAWs need to be converted to JPEG so that they can be displayed in a browser. You can also do this manually."
|
||||
|
@ -31,9 +32,10 @@
|
|||
|
||||
<v-flex xs12 sm6 lg4 class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
@change="onChange"
|
||||
:disabled="busy"
|
||||
class="ma-0 pa-0"
|
||||
v-model="options.thumbs"
|
||||
v-model="settings.library.thumbs"
|
||||
color="secondary-dark"
|
||||
:label="labels.thumbs"
|
||||
hint="Pre-render thumbnails if not done already. On-demand rendering saves storage but requires a powerful CPU."
|
||||
|
@ -45,9 +47,10 @@
|
|||
|
||||
<v-flex xs12 sm6 lg4 class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
@change="onChange"
|
||||
:disabled="busy"
|
||||
class="ma-0 pa-0"
|
||||
v-model="options.rescan"
|
||||
v-model="settings.library.rescan"
|
||||
color="secondary-dark"
|
||||
:label="labels.rescan"
|
||||
hint="Re-index all originals, including already indexed and unchanged files."
|
||||
|
@ -88,13 +91,13 @@
|
|||
import Axios from "axios";
|
||||
import Notify from "common/notify";
|
||||
import Event from "pubsub-js";
|
||||
import Settings from "../../model/settings";
|
||||
|
||||
export default {
|
||||
name: 'p-tab-index',
|
||||
data() {
|
||||
let settings = this.$config.settings();
|
||||
|
||||
return {
|
||||
settings: new Settings(this.$config.settings()),
|
||||
readonly: this.$config.getValue("readonly"),
|
||||
started: false,
|
||||
busy: false,
|
||||
|
@ -103,11 +106,6 @@
|
|||
action: "",
|
||||
fileName: "",
|
||||
source: null,
|
||||
options: {
|
||||
rescan: settings.library.rescan,
|
||||
thumbs: settings.library.thumbs,
|
||||
raw: settings.library.raw,
|
||||
},
|
||||
labels: {
|
||||
rescan: this.$gettext("Complete rescan"),
|
||||
thumbs: this.$gettext("Create thumbnails"),
|
||||
|
@ -116,6 +114,9 @@
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
onChange() {
|
||||
this.settings.save();
|
||||
},
|
||||
submit() {
|
||||
// DO NOTHING
|
||||
},
|
||||
|
@ -132,7 +133,7 @@
|
|||
const ctx = this;
|
||||
Notify.blockUI();
|
||||
|
||||
Api.post('index', this.options, {cancelToken: this.source.token}).then(function () {
|
||||
Api.post('index', this.settings.library, {cancelToken: this.source.token}).then(function () {
|
||||
Notify.unblockUI();
|
||||
ctx.busy = false;
|
||||
ctx.completed = 100;
|
||||
|
|
|
@ -84,7 +84,16 @@
|
|||
order: order,
|
||||
q: q,
|
||||
};
|
||||
const settings = {view: view};
|
||||
|
||||
const settings = this.$config.settings();
|
||||
|
||||
if (settings.library.private) {
|
||||
filter.public = true;
|
||||
}
|
||||
|
||||
if (settings.library.review) {
|
||||
filter.quality = 3;
|
||||
}
|
||||
|
||||
return {
|
||||
subscriptions: [],
|
||||
|
@ -96,7 +105,7 @@
|
|||
offset: 0,
|
||||
page: 0,
|
||||
selection: this.$clipboard.selection,
|
||||
settings: settings,
|
||||
settings: {view: view},
|
||||
filter: filter,
|
||||
lastFilter: {},
|
||||
routeName: routeName,
|
||||
|
@ -152,7 +161,7 @@
|
|||
|
||||
if (photo.LocationID) {
|
||||
this.$router.push({name: "place", params: {q: "s2:" + photo.LocationID}});
|
||||
} else if (photo.PlaceID && photo.PlaceID !== "-") {
|
||||
} else if (photo.PlaceID.length > 3) {
|
||||
this.$router.push({name: "place", params: {q: "s2:" + photo.PlaceID}});
|
||||
}
|
||||
},
|
||||
|
@ -323,7 +332,7 @@
|
|||
|
||||
if (model) {
|
||||
for (let key in values) {
|
||||
if (values.hasOwnProperty(key)) {
|
||||
if (values.hasOwnProperty(key) && values[key] != null && typeof values[key] !== "object") {
|
||||
model[key] = values[key];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="p-tab p-settings-general">
|
||||
<v-form lazy-validation dense
|
||||
ref="form" class="p-form-settings" accept-charset="UTF-8"
|
||||
@submit.prevent="save">
|
||||
@submit.prevent="onChange">
|
||||
<v-card flat tile class="mt-0 px-1 application">
|
||||
<v-card-title primary-title class="pb-0">
|
||||
<h3 class="body-2 mb-0">Library</h3>
|
||||
|
@ -12,28 +12,14 @@
|
|||
<v-layout wrap align-top>
|
||||
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
@change="save"
|
||||
class="ma-0 pa-0"
|
||||
v-model="settings.library.raw"
|
||||
color="secondary-dark"
|
||||
:label="labels.raw"
|
||||
hint="RAWs need to be converted to JPEG so that they can be displayed in a browser. You can also do this manually."
|
||||
prepend-icon="photo_camera"
|
||||
persistent-hint
|
||||
>
|
||||
</v-checkbox>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
@change="save"
|
||||
@change="onChange"
|
||||
:disabled="busy"
|
||||
class="ma-0 pa-0"
|
||||
v-model="settings.library.thumbs"
|
||||
v-model="settings.library.private"
|
||||
color="secondary-dark"
|
||||
:label="labels.thumbs"
|
||||
hint="Pre-render thumbnails if not done already. On-demand rendering saves storage but requires a powerful CPU."
|
||||
prepend-icon="photo_size_select_large"
|
||||
:label="labels.private"
|
||||
hint="Exclude photos and videos marked as private from search results by default."
|
||||
prepend-icon="lock"
|
||||
persistent-hint
|
||||
>
|
||||
</v-checkbox>
|
||||
|
@ -41,7 +27,23 @@
|
|||
|
||||
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
@change="save"
|
||||
@change="onChange"
|
||||
:disabled="busy"
|
||||
class="ma-0 pa-0"
|
||||
v-model="settings.library.review"
|
||||
color="secondary-dark"
|
||||
:label="labels.review"
|
||||
hint="Low-quality photos and videos require a review before they appear in search results."
|
||||
prepend-icon="remove_red_eye"
|
||||
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.library.group"
|
||||
color="secondary-dark"
|
||||
|
@ -55,7 +57,8 @@
|
|||
|
||||
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
@change="save"
|
||||
@change="onChange"
|
||||
:disabled="busy"
|
||||
class="ma-0 pa-0"
|
||||
v-model="settings.library.move"
|
||||
color="secondary-dark"
|
||||
|
@ -80,7 +83,8 @@
|
|||
<v-layout wrap align-top>
|
||||
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
@change="save"
|
||||
@change="onChange"
|
||||
:disabled="busy"
|
||||
class="ma-0 pa-0"
|
||||
v-model="settings.features.places"
|
||||
color="secondary-dark"
|
||||
|
@ -94,7 +98,8 @@
|
|||
|
||||
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
@change="save"
|
||||
@change="onChange"
|
||||
:disabled="busy"
|
||||
class="ma-0 pa-0"
|
||||
v-model="settings.features.labels"
|
||||
color="secondary-dark"
|
||||
|
@ -108,7 +113,8 @@
|
|||
|
||||
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
@change="save"
|
||||
@change="onChange"
|
||||
:disabled="busy"
|
||||
class="ma-0 pa-0"
|
||||
v-model="settings.features.import"
|
||||
color="secondary-dark"
|
||||
|
@ -122,7 +128,8 @@
|
|||
|
||||
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
@change="save"
|
||||
@change="onChange"
|
||||
:disabled="busy"
|
||||
class="ma-0 pa-0"
|
||||
v-model="settings.features.archive"
|
||||
color="secondary-dark"
|
||||
|
@ -136,7 +143,8 @@
|
|||
|
||||
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
@change="save"
|
||||
@change="onChange"
|
||||
:disabled="busy"
|
||||
class="ma-0 pa-0"
|
||||
v-model="settings.features.upload"
|
||||
color="secondary-dark"
|
||||
|
@ -150,7 +158,8 @@
|
|||
|
||||
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
@change="save"
|
||||
@change="onChange"
|
||||
:disabled="busy"
|
||||
class="ma-0 pa-0"
|
||||
v-model="settings.features.download"
|
||||
color="secondary-dark"
|
||||
|
@ -164,7 +173,8 @@
|
|||
|
||||
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
@change="save"
|
||||
@change="onChange"
|
||||
:disabled="busy"
|
||||
class="ma-0 pa-0"
|
||||
v-model="settings.features.edit"
|
||||
color="secondary-dark"
|
||||
|
@ -178,7 +188,8 @@
|
|||
|
||||
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
@change="save"
|
||||
@change="onChange"
|
||||
:disabled="busy"
|
||||
class="ma-0 pa-0"
|
||||
v-model="settings.features.share"
|
||||
color="secondary-dark"
|
||||
|
@ -202,7 +213,8 @@
|
|||
<v-layout wrap align-top>
|
||||
<v-flex xs12 sm6 class="px-2 pb-2">
|
||||
<v-select
|
||||
@change="save"
|
||||
@change="onChange"
|
||||
:disabled="busy"
|
||||
:items="options.themes"
|
||||
:label="labels.theme"
|
||||
color="secondary-dark"
|
||||
|
@ -214,7 +226,8 @@
|
|||
|
||||
<v-flex xs12 sm6 class="px-2 pb-2">
|
||||
<v-select
|
||||
@change="save"
|
||||
@change="onChange"
|
||||
:disabled="busy"
|
||||
:items="options.languages"
|
||||
:label="labels.language"
|
||||
color="secondary-dark"
|
||||
|
@ -236,7 +249,8 @@
|
|||
<v-layout wrap align-top>
|
||||
<v-flex xs12 sm6 class="px-2 pb-2">
|
||||
<v-select
|
||||
@change="save"
|
||||
@change="onChange"
|
||||
:disabled="busy"
|
||||
:items="options.mapsStyle"
|
||||
:label="labels.mapsStyle"
|
||||
color="secondary-dark"
|
||||
|
@ -248,7 +262,8 @@
|
|||
|
||||
<v-flex xs12 sm6 class="px-2 pb-2">
|
||||
<v-select
|
||||
@change="save"
|
||||
@change="onChange"
|
||||
:disabled="busy"
|
||||
:items="options.mapsAnimate"
|
||||
:label="labels.mapsAnimate"
|
||||
color="secondary-dark"
|
||||
|
@ -293,7 +308,7 @@
|
|||
data() {
|
||||
return {
|
||||
readonly: this.$config.getValue("readonly"),
|
||||
settings: new Settings(this.$config.values.settings),
|
||||
settings: new Settings(this.$config.settings()),
|
||||
options: options,
|
||||
labels: {
|
||||
language: this.$gettext("Language"),
|
||||
|
@ -305,19 +320,24 @@
|
|||
raw: this.$gettext("Convert RAW files"),
|
||||
move: this.$gettext("Remove imported files"),
|
||||
group: this.$gettext("Group related files"),
|
||||
private: this.$gettext("Hide private content"),
|
||||
review: this.$gettext("Apply quality filter"),
|
||||
},
|
||||
busy: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
this.settings.load();
|
||||
},
|
||||
save() {
|
||||
onChange() {
|
||||
const reload = this.settings.changed("language");
|
||||
|
||||
this.settings.save().then((s) => {
|
||||
this.$config.updateSettings(s.getValues(), this.$vuetify);
|
||||
if (reload) {
|
||||
this.busy = true;
|
||||
}
|
||||
|
||||
this.settings.save().then((s) => {
|
||||
if (reload) {
|
||||
this.$notify.info(this.$gettext("Reloading..."));
|
||||
this.$notify.blockUI();
|
||||
|
@ -325,7 +345,7 @@
|
|||
} else {
|
||||
this.$notify.success(this.$gettext("Settings saved"));
|
||||
}
|
||||
})
|
||||
}).finally(() => this.busy = false)
|
||||
},
|
||||
},
|
||||
created() {
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
created() {
|
||||
window.addEventListener('touchstart', (e) => this.onTouchStart(e), {passive: true});
|
||||
window.addEventListener('touchmove', (e) => this.onTouchMove(e), {passive: true});
|
||||
this.$config.setVuetify(this.$vuetify);
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('touchstart', (e) => this.onTouchStart(e), false);
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"restore": "#64B5F6",
|
||||
"album": "#FFAB40",
|
||||
"download": "#07BD9F",
|
||||
"private": "#00B8D4",
|
||||
"edit": "#48BCD6",
|
||||
"share": "#0070A0",
|
||||
"love": "#EF5350",
|
||||
|
@ -38,6 +39,7 @@
|
|||
"restore": "#64B5F6",
|
||||
"album": "#FFAB00",
|
||||
"download": "#00BFA5",
|
||||
"private": "#00B8D4",
|
||||
"edit": "#00B8D4",
|
||||
"share": "#9575CD",
|
||||
"love": "#EF5350",
|
||||
|
@ -61,6 +63,7 @@
|
|||
"restore": "#64B5F6",
|
||||
"album": "#FFAB00",
|
||||
"download": "#00BFA5",
|
||||
"private": "#00B8D4",
|
||||
"edit": "#00B8D4",
|
||||
"share": "#9575CD",
|
||||
"love": "#EF5350",
|
||||
|
@ -84,6 +87,7 @@
|
|||
"restore": "#64B5F6",
|
||||
"album": "#FFAB00",
|
||||
"download": "#00BFA5",
|
||||
"private": "#00B8D4",
|
||||
"edit": "#00B8D4",
|
||||
"share": "#9575CD",
|
||||
"love": "#EF5350",
|
||||
|
@ -107,6 +111,7 @@
|
|||
"restore": "#64B5F6",
|
||||
"album": "#FFAB00",
|
||||
"download": "#00BFA5",
|
||||
"private": "#00B8D4",
|
||||
"edit": "#00B8D4",
|
||||
"share": "#9575CD",
|
||||
"love": "#EF5350",
|
||||
|
@ -130,6 +135,7 @@
|
|||
"restore": "#64B5F6",
|
||||
"album": "#FFAB00",
|
||||
"download": "#00BFA5",
|
||||
"private": "#00B8D4",
|
||||
"edit": "#00B8D4",
|
||||
"share": "#9575CD",
|
||||
"love": "#EF5350",
|
||||
|
@ -154,6 +160,7 @@
|
|||
"restore": "#64B5F6",
|
||||
"album": "#FFAB40",
|
||||
"download": "#07BD9F",
|
||||
"private": "#48BCD6",
|
||||
"edit": "#48BCD6",
|
||||
"share": "#0070A0",
|
||||
"terminal": "#333333",
|
||||
|
|
|
@ -51,6 +51,20 @@ export default [
|
|||
meta: {title: "Favorites", auth: true},
|
||||
props: {staticFilter: {favorites: true}},
|
||||
},
|
||||
{
|
||||
name: "review",
|
||||
path: "/review",
|
||||
component: Photos,
|
||||
meta: {title: "Review", auth: true},
|
||||
props: {staticFilter: {review: true}},
|
||||
},
|
||||
{
|
||||
name: "private",
|
||||
path: "/private",
|
||||
component: Photos,
|
||||
meta: {title: "Private", auth: true},
|
||||
props: {staticFilter: {private: true}},
|
||||
},
|
||||
{
|
||||
name: "archive",
|
||||
path: "/archive",
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
@ -157,7 +158,18 @@ func BatchPhotosPrivate(router *gin.RouterGroup, conf *config.Config) {
|
|||
|
||||
db := conf.Db()
|
||||
|
||||
db.Model(entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).UpdateColumn("photo_private", gorm.Expr("IF (`photo_private`, 0, 1)"))
|
||||
err := db.Model(entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).UpdateColumn("photo_private", gorm.Expr("IF (`photo_private`, 0, 1)")).Error
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed)
|
||||
return
|
||||
}
|
||||
|
||||
q := query.New(db)
|
||||
|
||||
if entities, err := q.PhotoSelection(f); err == nil {
|
||||
event.EntitiesUpdated("photos", entities)
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
|
||||
|
|
|
@ -56,12 +56,12 @@ func GetGeo(router *gin.RouterGroup, conf *config.Config) {
|
|||
}
|
||||
|
||||
for _, p := range photos {
|
||||
bboxMin(0, p.PhotoLng)
|
||||
bboxMin(1, p.PhotoLat)
|
||||
bboxMax(2, p.PhotoLng)
|
||||
bboxMax(3, p.PhotoLat)
|
||||
bboxMin(0, p.Lng())
|
||||
bboxMin(1, p.Lat())
|
||||
bboxMax(2, p.Lng())
|
||||
bboxMax(3, p.Lat())
|
||||
|
||||
feat := geojson.NewPointFeature([]float64{p.PhotoLng, p.PhotoLat})
|
||||
feat := geojson.NewPointFeature([]float64{p.Lng(), p.Lat()})
|
||||
feat.ID = p.ID
|
||||
feat.Properties = gin.H{
|
||||
"PhotoUUID": p.PhotoUUID,
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestGetPhoto(t *testing.T) {
|
||||
|
@ -13,7 +14,8 @@ func TestGetPhoto(t *testing.T) {
|
|||
GetPhoto(router, ctx)
|
||||
result := PerformRequest(app, "GET", "/api/v1/photos/654")
|
||||
assert.Equal(t, http.StatusOK, result.Code)
|
||||
assert.Contains(t, result.Body.String(), "\"PhotoLat\":48.519235")
|
||||
val := gjson.Get(result.Body.String(), "PhotoLat")
|
||||
assert.Equal(t, "48.519234", val.String())
|
||||
})
|
||||
t.Run("search for not existing photo", func(t *testing.T) {
|
||||
app, router, ctx := NewApiTest()
|
||||
|
|
|
@ -46,6 +46,6 @@ func SaveSettings(router *gin.RouterGroup, conf *config.Config) {
|
|||
event.Publish("config.updated", event.Data(conf.ClientConfig()))
|
||||
log.Infof("settings saved")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "saved"})
|
||||
c.JSON(http.StatusOK, s)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -167,15 +167,15 @@ func (c *Config) ClientConfig() ClientConfig {
|
|||
|
||||
db.Model(&entity.Country{}).
|
||||
Select("id, country_name").
|
||||
Order("country_name").
|
||||
Order("country_slug").
|
||||
Scan(&countries)
|
||||
|
||||
db.Where("deleted_at IS NULL").
|
||||
Limit(1000).Order("camera_model").
|
||||
Limit(10000).Order("camera_slug").
|
||||
Find(&cameras)
|
||||
|
||||
db.Where("deleted_at IS NULL").
|
||||
Limit(1000).Order("lens_model").
|
||||
Limit(10000).Order("lens_slug").
|
||||
Find(&lenses)
|
||||
|
||||
db.Where("deleted_at IS NULL AND album_favorite = 1").
|
||||
|
|
|
@ -85,6 +85,8 @@ func (c *Config) MigrateDb() {
|
|||
|
||||
entity.CreateUnknownPlace(db)
|
||||
entity.CreateUnknownCountry(db)
|
||||
entity.CreateUnknownCamera(db)
|
||||
entity.CreateUnknownLens(db)
|
||||
}
|
||||
|
||||
// DropTables drops all tables in the currently configured database (be careful!).
|
||||
|
|
|
@ -222,12 +222,12 @@ var GlobalFlags = []cli.Flag{
|
|||
},
|
||||
cli.BoolFlag{
|
||||
Name: "detect-nsfw",
|
||||
Usage: "flag photos that may be offensive",
|
||||
Usage: "flag photos as private that may be offensive",
|
||||
EnvVar: "PHOTOPRISM_DETECT_NSFW",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "upload-nsfw",
|
||||
Usage: "allow uploads that may contain offensive content",
|
||||
Usage: "allow uploads that may be offensive",
|
||||
EnvVar: "PHOTOPRISM_UPLOAD_NSFW",
|
||||
},
|
||||
cli.StringFlag{
|
||||
|
|
|
@ -20,11 +20,13 @@ type MapsSettings struct {
|
|||
}
|
||||
|
||||
type LibrarySettings struct {
|
||||
CompleteRescan bool `json:"rescan" yaml:"rescan"`
|
||||
ConvertRaw bool `json:"raw" yaml:"raw"`
|
||||
CreateThumbs bool `json:"thumbs" yaml:"thumbs"`
|
||||
GroupRelated bool `json:"group" yaml:"group"`
|
||||
CompleteRescan bool `json:"rescan" yaml:"rescan"`
|
||||
MoveImported bool `json:"move" yaml:"move"`
|
||||
HidePrivate bool `json:"private" yaml:"private"`
|
||||
RequireReview bool `json:"review" yaml:"review"`
|
||||
GroupRelated bool `json:"group" yaml:"group"`
|
||||
}
|
||||
|
||||
type FeatureSettings struct {
|
||||
|
@ -70,8 +72,10 @@ func NewSettings() *Settings {
|
|||
CompleteRescan: false,
|
||||
ConvertRaw: false,
|
||||
CreateThumbs: false,
|
||||
GroupRelated: true,
|
||||
MoveImported: false,
|
||||
GroupRelated: true,
|
||||
RequireReview: true,
|
||||
HidePrivate: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
6
internal/config/testdata/configEmpty.yml
vendored
6
internal/config/testdata/configEmpty.yml
vendored
|
@ -13,8 +13,10 @@ features:
|
|||
edit: true
|
||||
share: true
|
||||
library:
|
||||
rescan: false
|
||||
raw: false
|
||||
thumbs: false
|
||||
group: true
|
||||
rescan: false
|
||||
move: false
|
||||
private: true
|
||||
review: true
|
||||
group: true
|
||||
|
|
|
@ -24,12 +24,23 @@ type Camera struct {
|
|||
DeletedAt *time.Time `sql:"index"`
|
||||
}
|
||||
|
||||
var UnknownCamera = Camera{
|
||||
CameraModel: "Unknown",
|
||||
CameraMake: "",
|
||||
CameraSlug: "zz",
|
||||
}
|
||||
|
||||
// CreateUnknownCamera initializes the database with an unknown camera if not exists
|
||||
func CreateUnknownCamera(db *gorm.DB) {
|
||||
UnknownCamera.FirstOrCreate(db)
|
||||
}
|
||||
|
||||
// NewCamera creates a camera entity from a model name and a make name.
|
||||
func NewCamera(modelName string, makeName string) *Camera {
|
||||
makeName = strings.TrimSpace(makeName)
|
||||
|
||||
if modelName == "" {
|
||||
modelName = "Unknown"
|
||||
return &UnknownCamera
|
||||
} else if strings.HasPrefix(modelName, makeName) {
|
||||
modelName = strings.TrimSpace(modelName[len(makeName):])
|
||||
}
|
||||
|
|
|
@ -7,15 +7,10 @@ import (
|
|||
)
|
||||
|
||||
func TestNewCamera(t *testing.T) {
|
||||
t.Run("model Unknown make Nikon", func(t *testing.T) {
|
||||
t.Run("unknown camera", func(t *testing.T) {
|
||||
camera := NewCamera("", "Nikon")
|
||||
|
||||
expected := &Camera{
|
||||
CameraModel: "Unknown",
|
||||
CameraMake: "Nikon",
|
||||
CameraSlug: "nikon-unknown",
|
||||
}
|
||||
assert.Equal(t, expected, camera)
|
||||
assert.Equal(t, &UnknownCamera, camera)
|
||||
})
|
||||
t.Run("model EOS 6D make Canon", func(t *testing.T) {
|
||||
camera := NewCamera("EOS 6D", "Canon")
|
||||
|
@ -50,12 +45,7 @@ func TestNewCamera(t *testing.T) {
|
|||
t.Run("model Unknown make Unknown", func(t *testing.T) {
|
||||
camera := NewCamera("", "")
|
||||
|
||||
expected := &Camera{
|
||||
CameraModel: "Unknown",
|
||||
CameraMake: "",
|
||||
CameraSlug: "unknown",
|
||||
}
|
||||
assert.Equal(t, expected, camera)
|
||||
assert.Equal(t, &UnknownCamera, camera)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ var altCountryNames = map[string]string{
|
|||
|
||||
// Country represents a country location, used for labeling photos.
|
||||
type Country struct {
|
||||
ID string `gorm:"primary_key"`
|
||||
ID string `gorm:"type:varbinary(2);primary_key"`
|
||||
CountrySlug string `gorm:"type:varbinary(128);unique_index;"`
|
||||
CountryName string
|
||||
CountryDescription string `gorm:"type:text;"`
|
||||
|
@ -26,8 +26,12 @@ type Country struct {
|
|||
New bool `gorm:"-"`
|
||||
}
|
||||
|
||||
// UnknownCountry is the default country
|
||||
var UnknownCountry = NewCountry("zz", maps.CountryNames["zz"])
|
||||
// UnknownCountry is defined here to use it as a default
|
||||
var UnknownCountry = Country{
|
||||
ID: "zz",
|
||||
CountryName: maps.CountryNames["zz"],
|
||||
CountrySlug: "zz",
|
||||
}
|
||||
|
||||
// CreateUnknownCountry is used to initialize the database with the default country
|
||||
func CreateUnknownCountry(db *gorm.DB) {
|
||||
|
@ -37,7 +41,7 @@ func CreateUnknownCountry(db *gorm.DB) {
|
|||
// NewCountry creates a new country, with default country code if not provided
|
||||
func NewCountry(countryCode string, countryName string) *Country {
|
||||
if countryCode == "" {
|
||||
countryCode = "zz"
|
||||
return &UnknownCountry
|
||||
}
|
||||
|
||||
if altName, ok := altCountryNames[countryName]; ok {
|
||||
|
|
|
@ -17,9 +17,9 @@ type Event struct {
|
|||
EventNotes string `gorm:"type:text;"`
|
||||
EventBegin time.Time `gorm:"type:datetime;"`
|
||||
EventEnd time.Time `gorm:"type:datetime;"`
|
||||
EventLat float64
|
||||
EventLng float64
|
||||
EventDist float64
|
||||
EventLat float32 `gorm:"type:FLOAT;"`
|
||||
EventLng float32 `gorm:"type:FLOAT;"`
|
||||
EventDist float32 `gorm:"type:FLOAT;"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time `sql:"index"`
|
||||
|
|
|
@ -33,7 +33,7 @@ type File struct {
|
|||
FileWidth int
|
||||
FileHeight int
|
||||
FileOrientation int
|
||||
FileAspectRatio float64
|
||||
FileAspectRatio float32 `gorm:"type:FLOAT;"`
|
||||
FileMainColor string `gorm:"type:varbinary(16);index;"`
|
||||
FileColors string `gorm:"type:binary(9);"`
|
||||
FileLuminance string `gorm:"type:binary(9);"`
|
||||
|
|
|
@ -24,6 +24,17 @@ type Lens struct {
|
|||
DeletedAt *time.Time `sql:"index"`
|
||||
}
|
||||
|
||||
var UnknownLens = Lens{
|
||||
LensModel: "Unknown",
|
||||
LensMake: "",
|
||||
LensSlug: "zz",
|
||||
}
|
||||
|
||||
// CreateUnknownLens initializes the database with an unknown lens if not exists
|
||||
func CreateUnknownLens(db *gorm.DB) {
|
||||
UnknownLens.FirstOrCreate(db)
|
||||
}
|
||||
|
||||
// TableName returns Lens table identifier "lens"
|
||||
func (Lens) TableName() string {
|
||||
return "lenses"
|
||||
|
@ -33,13 +44,12 @@ func (Lens) TableName() string {
|
|||
func NewLens(modelName string, makeName string) *Lens {
|
||||
modelName = strings.TrimSpace(modelName)
|
||||
makeName = strings.TrimSpace(makeName)
|
||||
lensSlug := slug.MakeLang(modelName, "en")
|
||||
|
||||
if modelName == "" {
|
||||
modelName = "Unknown"
|
||||
return &UnknownLens
|
||||
}
|
||||
|
||||
lensSlug := slug.MakeLang(modelName, "en")
|
||||
|
||||
result := &Lens{
|
||||
LensModel: modelName,
|
||||
LensMake: makeName,
|
||||
|
@ -49,7 +59,7 @@ func NewLens(modelName string, makeName string) *Lens {
|
|||
return result
|
||||
}
|
||||
|
||||
// FirstOrCreate checks wether the lens already exists in the database
|
||||
// FirstOrCreate checks if the lens already exists in the database
|
||||
func (m *Lens) FirstOrCreate(db *gorm.DB) *Lens {
|
||||
mutex.Db.Lock()
|
||||
defer mutex.Db.Unlock()
|
||||
|
|
|
@ -17,7 +17,8 @@ func TestNewLens(t *testing.T) {
|
|||
lens := NewLens("", "")
|
||||
assert.Equal(t, "Unknown", lens.LensModel)
|
||||
assert.Equal(t, "", lens.LensMake)
|
||||
assert.Equal(t, "unknown", lens.LensSlug)
|
||||
assert.Equal(t, "zz", lens.LensSlug)
|
||||
assert.Equal(t, &UnknownLens, lens)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -23,18 +23,17 @@ type Location struct {
|
|||
}
|
||||
|
||||
// NewLocation creates a location using a token extracted from coordinate
|
||||
func NewLocation(lat, lng float64) *Location {
|
||||
func NewLocation(lat, lng float32) *Location {
|
||||
result := &Location{}
|
||||
|
||||
result.ID = s2.Token(lat, lng)
|
||||
result.ID = s2.Token(float64(lat), float64(lng))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Find gets the location using either the db or the api if not in the db
|
||||
func (m *Location) Find(db *gorm.DB, api string) error {
|
||||
if err := db.First(m, "id = ?", m.ID).Error; err == nil {
|
||||
m.Place = FindPlace(m.PlaceID, db)
|
||||
if err := db.Preload("Place").First(m, "id = ?", m.ID).Error; err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -46,14 +45,16 @@ func (m *Location) Find(db *gorm.DB, api string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
m.Place = FindPlaceByLabel(l.ID, l.LocLabel, db)
|
||||
|
||||
if m.Place.NoID() {
|
||||
m.Place.ID = l.ID
|
||||
m.Place.LocLabel = l.LocLabel
|
||||
m.Place.LocCity = l.LocCity
|
||||
m.Place.LocState = l.LocState
|
||||
m.Place.LocCountry = l.LocCountry
|
||||
if place := FindPlaceByLabel(l.ID, l.LocLabel, db); place != nil {
|
||||
m.Place = place
|
||||
} else {
|
||||
m.Place = &Place{
|
||||
ID: l.ID,
|
||||
LocLabel: l.LocLabel,
|
||||
LocCity: l.LocCity,
|
||||
LocState: l.LocState,
|
||||
LocCountry: l.LocCountry,
|
||||
}
|
||||
}
|
||||
|
||||
m.LocName = l.LocName
|
||||
|
@ -62,10 +63,7 @@ func (m *Location) Find(db *gorm.DB, api string) error {
|
|||
|
||||
if err := db.Create(m).Error; err == nil {
|
||||
return nil
|
||||
} else if err := db.First(m, "id = ?", m.ID).Error; err == nil {
|
||||
// avoid mutex by trying again to find location
|
||||
m.Place = FindPlace(m.PlaceID, db)
|
||||
} else {
|
||||
} else if err := db.Preload("Place").First(m, "id = ?", m.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -22,30 +22,29 @@ type Photo struct {
|
|||
TakenSrc string `gorm:"type:varbinary(8);" json:"TakenSrc"`
|
||||
PhotoUUID string `gorm:"type:varbinary(36);unique_index;index:idx_photos_taken_uuid;"`
|
||||
PhotoPath string `gorm:"type:varbinary(512);index;"`
|
||||
PhotoName string `gorm:"type:varbinary(256);"`
|
||||
PhotoTitle string `json:"PhotoTitle"`
|
||||
PhotoName string `gorm:"type:varbinary(255);"`
|
||||
PhotoTitle string `gorm:"type:varchar(200);" json:"PhotoTitle"`
|
||||
TitleSrc string `gorm:"type:varbinary(8);" json:"TitleSrc"`
|
||||
PhotoQuality int `gorm:"type:SMALLINT" json:"PhotoQuality"`
|
||||
PhotoResolution int `gorm:"type:SMALLINT" json:"PhotoResolution"`
|
||||
PhotoFavorite bool `json:"PhotoFavorite"`
|
||||
PhotoPrivate bool `json:"PhotoPrivate"`
|
||||
PhotoStory bool `json:"PhotoStory"`
|
||||
PhotoNSFW bool `json:"PhotoNSFW"`
|
||||
PhotoLat float64 `gorm:"index;" json:"PhotoLat"`
|
||||
PhotoLng float64 `gorm:"index;" json:"PhotoLng"`
|
||||
PhotoLat float32 `gorm:"type:FLOAT;index;" json:"PhotoLat"`
|
||||
PhotoLng float32 `gorm:"type:FLOAT;index;" json:"PhotoLng"`
|
||||
PhotoAltitude int `json:"PhotoAltitude"`
|
||||
PhotoIso int `json:"PhotoIso"`
|
||||
PhotoFocalLength int `json:"PhotoFocalLength"`
|
||||
PhotoFNumber float64 `json:"PhotoFNumber"`
|
||||
PhotoFNumber float32 `gorm:"type:FLOAT;" json:"PhotoFNumber"`
|
||||
PhotoExposure string `gorm:"type:varbinary(64);" json:"PhotoExposure"`
|
||||
CameraID uint `gorm:"index:idx_photos_camera_lens;" json:"CameraID"`
|
||||
CameraSerial string `gorm:"type:varbinary(128);" json:"CameraSerial"`
|
||||
CameraSrc string `gorm:"type:varbinary(8);" json:"CameraSrc"`
|
||||
LensID uint `gorm:"index:idx_photos_camera_lens;" json:"LensID"`
|
||||
PlaceID string `gorm:"type:varbinary(16);index;" json:"PlaceID"`
|
||||
PlaceID string `gorm:"type:varbinary(16);index;default:'zz'" json:"PlaceID"`
|
||||
LocationID string `gorm:"type:varbinary(16);index;" json:"LocationID"`
|
||||
LocationSrc string `gorm:"type:varbinary(8);" json:"LocationSrc"`
|
||||
PhotoCountry string `gorm:"index:idx_photos_country_year_month;" json:"PhotoCountry"`
|
||||
PhotoCountry string `gorm:"type:varbinary(2);index:idx_photos_country_year_month;default:'zz'" json:"PhotoCountry"`
|
||||
PhotoYear int `gorm:"index:idx_photos_country_year_month;"`
|
||||
PhotoMonth int `gorm:"index:idx_photos_country_year_month;"`
|
||||
TimeZone string `gorm:"type:varbinary(64);" json:"TimeZone"`
|
||||
|
@ -63,6 +62,7 @@ type Photo struct {
|
|||
Labels []PhotoLabel
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
EditedAt *time.Time
|
||||
DeletedAt *time.Time `sql:"index"`
|
||||
}
|
||||
|
||||
|
@ -101,6 +101,8 @@ func SavePhotoForm(model Photo, form form.Photo, db *gorm.DB, geoApi string) err
|
|||
log.Warnf("%s (%s)", err.Error(), model.PhotoUUID)
|
||||
}
|
||||
|
||||
edited := time.Now().UTC()
|
||||
model.EditedAt = &edited
|
||||
model.PhotoQuality = model.QualityScore()
|
||||
|
||||
return db.Unscoped().Save(&model).Error
|
||||
|
@ -286,12 +288,12 @@ func (m *Photo) NoLatLng() bool {
|
|||
|
||||
// NoPlace checks if the photo has no Place
|
||||
func (m *Photo) NoPlace() bool {
|
||||
return len(m.PlaceID) < 2
|
||||
return m.PlaceID == "" || m.PlaceID == UnknownPlace.ID
|
||||
}
|
||||
|
||||
// HasPlace checks if the photo has a Place
|
||||
func (m *Photo) HasPlace() bool {
|
||||
return len(m.PlaceID) >= 2
|
||||
return !m.NoPlace()
|
||||
}
|
||||
|
||||
// NoTitle checks if the photo has no Title
|
||||
|
|
|
@ -15,8 +15,8 @@ func (m *Photo) GetTimeZone() string {
|
|||
|
||||
if m.HasLatLng() {
|
||||
zones, err := tz.GetZone(tz.Point{
|
||||
Lat: m.PhotoLat,
|
||||
Lon: m.PhotoLng,
|
||||
Lat: float64(m.PhotoLat),
|
||||
Lon: float64(m.PhotoLng),
|
||||
})
|
||||
|
||||
if err == nil && len(zones) > 0 {
|
||||
|
@ -84,7 +84,7 @@ func (m *Photo) UpdateLocation(db *gorm.DB, geoApi string) (keywords []string, l
|
|||
} else {
|
||||
log.Warn(err)
|
||||
|
||||
m.Place = UnknownPlace
|
||||
m.Place = &UnknownPlace
|
||||
m.PlaceID = UnknownPlace.ID
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package entity
|
|||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
@ -12,6 +13,11 @@ var QualityBlacklist = map[string]bool{
|
|||
"info": true,
|
||||
}
|
||||
|
||||
var (
|
||||
year2008 = time.Date(2008, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
year2012 = time.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
)
|
||||
|
||||
// QualityScore returns a score based on photo properties like size and metadata.
|
||||
func (m *Photo) QualityScore() (score int) {
|
||||
if m.PhotoFavorite {
|
||||
|
@ -26,7 +32,11 @@ func (m *Photo) QualityScore() (score int) {
|
|||
score++
|
||||
}
|
||||
|
||||
if m.PhotoResolution >= 2 {
|
||||
if m.TakenAt.Before(year2008) {
|
||||
score++
|
||||
} else if m.TakenAt.Before(year2012) && m.PhotoResolution >= 1 {
|
||||
score++
|
||||
} else if m.PhotoResolution >= 2 {
|
||||
score++
|
||||
}
|
||||
|
||||
|
@ -49,5 +59,9 @@ func (m *Photo) QualityScore() (score int) {
|
|||
score++
|
||||
}
|
||||
|
||||
if score < 3 && m.EditedAt != nil {
|
||||
score = 3
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
|
|
@ -22,8 +22,14 @@ type Place struct {
|
|||
New bool `gorm:"-"`
|
||||
}
|
||||
|
||||
// UnknownPlace is the default unknown place
|
||||
var UnknownPlace = NewPlace("-", "Unknown", "Unknown", "Unknown", "zz")
|
||||
// UnknownPlace is defined here to use it as a default
|
||||
var UnknownPlace = Place{
|
||||
ID: "zz",
|
||||
LocLabel: "Unknown",
|
||||
LocCity: "Unknown",
|
||||
LocState: "Unknown",
|
||||
LocCountry: "zz",
|
||||
}
|
||||
|
||||
// CreateUnknownPlace initializes default place in the database
|
||||
func CreateUnknownPlace(db *gorm.DB) {
|
||||
|
@ -35,41 +41,18 @@ func (m *Place) AfterCreate(scope *gorm.Scope) error {
|
|||
return scope.SetColumn("New", true)
|
||||
}
|
||||
|
||||
// FindPlace returns place from a token
|
||||
func FindPlace(token string, db *gorm.DB) *Place {
|
||||
place := &Place{}
|
||||
|
||||
if err := db.First(place, "id = ?", token).Error; err != nil {
|
||||
log.Debugf("place: %s for token %s", err.Error(), token)
|
||||
}
|
||||
|
||||
return place
|
||||
}
|
||||
|
||||
// FindPlaceByLabel returns a place from an id or a label
|
||||
func FindPlaceByLabel(id string, label string, db *gorm.DB) *Place {
|
||||
place := &Place{}
|
||||
|
||||
if err := db.First(place, "id = ? OR loc_label = ?", id, label).Error; err != nil {
|
||||
log.Debugf("place: %s for id %s or label \"%s\"", err.Error(), id, label)
|
||||
return nil
|
||||
}
|
||||
|
||||
return place
|
||||
}
|
||||
|
||||
// NewPlace registers a new place in database
|
||||
func NewPlace(id, label, city, state, countryCode string) *Place {
|
||||
result := &Place{
|
||||
ID: id,
|
||||
LocLabel: label,
|
||||
LocCity: city,
|
||||
LocState: state,
|
||||
LocCountry: countryCode,
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Find returns db record of place
|
||||
func (m *Place) Find(db *gorm.DB) error {
|
||||
if err := db.First(m, "id = ?", m.ID).Error; err != nil {
|
||||
|
@ -91,9 +74,9 @@ func (m *Place) FirstOrCreate(db *gorm.DB) *Place {
|
|||
return m
|
||||
}
|
||||
|
||||
// NoID checks is the place has no id
|
||||
func (m Place) NoID() bool {
|
||||
return m.ID == ""
|
||||
// Unknown returns true if this is an unknown place
|
||||
func (m Place) Unknown() bool {
|
||||
return m.ID == UnknownPlace.ID
|
||||
}
|
||||
|
||||
// Label returns place label
|
||||
|
|
|
@ -8,8 +8,8 @@ type GeoSearch struct {
|
|||
Before time.Time `form:"before" time_format:"2006-01-02"`
|
||||
After time.Time `form:"after" time_format:"2006-01-02"`
|
||||
Favorite bool `form:"favorite"`
|
||||
Lat float64 `form:"lat"`
|
||||
Lng float64 `form:"lng"`
|
||||
Lat float32 `form:"lat"`
|
||||
Lng float32 `form:"lng"`
|
||||
S2 string `form:"s2"`
|
||||
Olc string `form:"olc"`
|
||||
Dist uint `form:"dist"`
|
||||
|
|
|
@ -24,6 +24,6 @@ func TestGeoSearch(t *testing.T) {
|
|||
assert.Equal(t, "foobar baz", form.Query)
|
||||
assert.Equal(t, time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), form.Before)
|
||||
assert.Equal(t, uint(0x61a8), form.Dist)
|
||||
assert.Equal(t, 33.45343166666667, form.Lat)
|
||||
assert.Equal(t, float32(33.45343), form.Lat)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -27,15 +27,14 @@ type Photo struct {
|
|||
DescriptionSrc string `json:"DescriptionSrc"`
|
||||
PhotoFavorite bool `json:"PhotoFavorite"`
|
||||
PhotoPrivate bool `json:"PhotoPrivate"`
|
||||
PhotoNSFW bool `json:"PhotoNSFW"`
|
||||
PhotoStory bool `json:"PhotoStory"`
|
||||
PhotoReview bool `json:"PhotoReview"`
|
||||
PhotoLat float64 `json:"PhotoLat"`
|
||||
PhotoLng float64 `json:"PhotoLng"`
|
||||
PhotoLat float32 `json:"PhotoLat"`
|
||||
PhotoLng float32 `json:"PhotoLng"`
|
||||
PhotoAltitude int `json:"PhotoAltitude"`
|
||||
PhotoIso int `json:"PhotoIso"`
|
||||
PhotoFocalLength int `json:"PhotoFocalLength"`
|
||||
PhotoFNumber float64 `json:"PhotoFNumber"`
|
||||
PhotoFNumber float32 `json:"PhotoFNumber"`
|
||||
PhotoExposure string `json:"PhotoExposure"`
|
||||
CameraID uint `json:"CameraID"`
|
||||
CameraSrc string `json:"CameraSrc"`
|
||||
|
|
|
@ -13,11 +13,11 @@ type PhotoSearch struct {
|
|||
Duplicate bool `form:"duplicate"`
|
||||
Archived bool `form:"archived"`
|
||||
Error bool `form:"error"`
|
||||
Lat float64 `form:"lat"`
|
||||
Lng float64 `form:"lng"`
|
||||
Lat float32 `form:"lat"`
|
||||
Lng float32 `form:"lng"`
|
||||
Dist uint `form:"dist"`
|
||||
Fmin float64 `form:"fmin"`
|
||||
Fmax float64 `form:"fmax"`
|
||||
Fmin float32 `form:"fmin"`
|
||||
Fmax float32 `form:"fmax"`
|
||||
Chroma uint8 `form:"chroma"`
|
||||
Diff uint32 `form:"diff"`
|
||||
Mono bool `form:"mono"`
|
||||
|
@ -37,6 +37,7 @@ type PhotoSearch struct {
|
|||
After time.Time `form:"after" time_format:"2006-01-02"`
|
||||
Favorites bool `form:"favorites"`
|
||||
Public bool `form:"public"`
|
||||
Private bool `form:"private"`
|
||||
Story bool `form:"story"`
|
||||
Safe bool `form:"safe"`
|
||||
Nsfw bool `form:"nsfw"`
|
||||
|
|
|
@ -34,7 +34,7 @@ func TestParseQueryString(t *testing.T) {
|
|||
assert.Equal(t, time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), form.Before)
|
||||
assert.Equal(t, false, form.Favorites)
|
||||
assert.Equal(t, uint(0x61a8), form.Dist)
|
||||
assert.Equal(t, 33.45343166666667, form.Lat)
|
||||
assert.Equal(t, float32(33.45343), form.Lat)
|
||||
})
|
||||
t.Run("valid query 2", func(t *testing.T) {
|
||||
form := &PhotoSearch{Query: "chroma:200 title:\"test\" after:2018-01-15 duplicate:false favorites:true lng:33.45343166666667"}
|
||||
|
@ -51,7 +51,7 @@ func TestParseQueryString(t *testing.T) {
|
|||
assert.Equal(t, "test", form.Title)
|
||||
assert.Equal(t, time.Date(2018, 01, 15, 0, 0, 0, 0, time.UTC), form.After)
|
||||
assert.Equal(t, false, form.Duplicate)
|
||||
assert.Equal(t, 33.45343166666667, form.Lng)
|
||||
assert.Equal(t, float32(33.45343), form.Lng)
|
||||
})
|
||||
t.Run("valid query with umlauts", func(t *testing.T) {
|
||||
form := &PhotoSearch{Query: "title:\"tübingen\""}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
gc "github.com/patrickmn/go-cache"
|
||||
"github.com/photoprism/photoprism/pkg/s2"
|
||||
|
@ -23,6 +24,7 @@ type Location struct {
|
|||
}
|
||||
|
||||
var ReverseLookupURL = "https://places.photoprism.org/v1/location/%s"
|
||||
var client = &http.Client{Timeout: 30 * time.Second} // TODO: Change timeout if needed
|
||||
|
||||
func NewLocation(id string, lat float64, lng float64, name string, category string, place Place, cached bool) *Location {
|
||||
result := &Location{
|
||||
|
@ -59,13 +61,24 @@ func FindLocation(id string) (result Location, err error) {
|
|||
|
||||
log.Debugf("places: query %s", url)
|
||||
|
||||
r, err := http.Get(url)
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("places: %s", err.Error())
|
||||
return result, err
|
||||
}
|
||||
|
||||
r, err := client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("places: %s", err.Error())
|
||||
return result, err
|
||||
} else if r.StatusCode >= 400 {
|
||||
err = fmt.Errorf("places: request failed with status code %d", r.StatusCode)
|
||||
log.Error(err)
|
||||
return result, err
|
||||
}
|
||||
|
||||
err = json.NewDecoder(r.Body).Decode(&result)
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -26,11 +26,11 @@ type Data struct {
|
|||
Flash bool
|
||||
FocalLength int
|
||||
Exposure string
|
||||
Aperture float64
|
||||
FNumber float64
|
||||
Aperture float32
|
||||
FNumber float32
|
||||
Iso int
|
||||
Lat float64
|
||||
Lng float64
|
||||
Lat float32
|
||||
Lng float32
|
||||
Altitude int
|
||||
Width int
|
||||
Height int
|
||||
|
|
|
@ -167,7 +167,7 @@ func Exif(filename string) (data Data, err error) {
|
|||
number, _ := strconv.ParseFloat(values[0], 64)
|
||||
denom, _ := strconv.ParseFloat(values[1], 64)
|
||||
|
||||
data.FNumber = math.Round((number/denom)*1000) / 1000
|
||||
data.FNumber = float32(math.Round((number/denom)*1000) / 1000)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -178,7 +178,7 @@ func Exif(filename string) (data Data, err error) {
|
|||
number, _ := strconv.ParseFloat(values[0], 64)
|
||||
denom, _ := strconv.ParseFloat(values[1], 64)
|
||||
|
||||
data.Aperture = math.Round((number/denom)*1000) / 1000
|
||||
data.Aperture = float32(math.Round((number/denom)*1000) / 1000)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -243,16 +243,16 @@ func Exif(filename string) (data Data, err error) {
|
|||
|
||||
if ifd, err := index.RootIfd.ChildWithIfdPath(exifcommon.IfdPathStandardGps); err == nil {
|
||||
if gi, err := ifd.GpsInfo(); err == nil {
|
||||
data.Lat = gi.Latitude.Decimal()
|
||||
data.Lng = gi.Longitude.Decimal()
|
||||
data.Lat = float32(gi.Latitude.Decimal())
|
||||
data.Lng = float32(gi.Longitude.Decimal())
|
||||
data.Altitude = gi.Altitude
|
||||
}
|
||||
}
|
||||
|
||||
if data.Lat != 0 && data.Lng != 0 {
|
||||
zones, err := tz.GetZone(tz.Point{
|
||||
Lat: data.Lat,
|
||||
Lon: data.Lng,
|
||||
Lat: float64(data.Lat),
|
||||
Lon: float64(data.Lng),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -21,8 +21,8 @@ func TestExif(t *testing.T) {
|
|||
assert.Equal(t, "This is a legal notice", data.Copyright)
|
||||
assert.Equal(t, 2736, data.Height)
|
||||
assert.Equal(t, 3648, data.Width)
|
||||
assert.Equal(t, 52.459690093888895, data.Lat)
|
||||
assert.Equal(t, 13.321831703055555, data.Lng)
|
||||
assert.Equal(t, float32(52.45969), data.Lat)
|
||||
assert.Equal(t, float32(13.321832), data.Lng)
|
||||
assert.Equal(t, 0, data.Altitude)
|
||||
assert.Equal(t, "HUAWEI", data.CameraMake)
|
||||
assert.Equal(t, "ELE-L29", data.CameraModel)
|
||||
|
@ -53,8 +53,8 @@ func TestExif(t *testing.T) {
|
|||
assert.Equal(t, "", data.Copyright)
|
||||
assert.Equal(t, 540, data.Height)
|
||||
assert.Equal(t, 720, data.Width)
|
||||
assert.Equal(t, 51.25485166666667, data.Lat)
|
||||
assert.Equal(t, 7.389468333333333, data.Lng)
|
||||
assert.Equal(t, float32(51.254852), data.Lat)
|
||||
assert.Equal(t, float32(7.389468), data.Lng)
|
||||
assert.Equal(t, 0, data.Altitude)
|
||||
assert.Equal(t, "Canon", data.CameraMake)
|
||||
assert.Equal(t, "Canon EOS 50D", data.CameraModel)
|
||||
|
@ -82,7 +82,7 @@ func TestExif(t *testing.T) {
|
|||
assert.Equal(t, "", data.Copyright)
|
||||
assert.Equal(t, 2880, data.Height)
|
||||
assert.Equal(t, 3840, data.Width)
|
||||
assert.Equal(t, 0.0, data.Lng)
|
||||
assert.Equal(t, float32(0), data.Lng)
|
||||
assert.Equal(t, 0, data.Altitude)
|
||||
assert.Equal(t, "GoPro", data.CameraMake)
|
||||
assert.Equal(t, "HD2", data.CameraModel)
|
||||
|
@ -107,8 +107,8 @@ func TestExif(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "2018-09-10T03:16:13Z", data.TakenAt.Format("2006-01-02T15:04:05Z"))
|
||||
assert.Equal(t, "2018-09-10T12:16:13Z", data.TakenAtLocal.Format("2006-01-02T15:04:05Z"))
|
||||
assert.Equal(t, 34.79745, data.Lat)
|
||||
assert.Equal(t, 134.76463333333334, data.Lng)
|
||||
assert.Equal(t, float32(34.79745), data.Lat)
|
||||
assert.Equal(t, float32(134.76463), data.Lng)
|
||||
assert.Equal(t, 0, data.Altitude)
|
||||
assert.Equal(t, "Apple", data.CameraMake)
|
||||
assert.Equal(t, "iPhone 7", data.CameraModel)
|
||||
|
|
|
@ -144,7 +144,10 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
if !ind.conf.DisableTensorFlow() && (fileChanged || o.UpdateKeywords || o.UpdateLabels || o.UpdateTitle) {
|
||||
// Image classification via TensorFlow
|
||||
labels = ind.classifyImage(m)
|
||||
photo.PhotoNSFW = ind.isNSFW(m)
|
||||
|
||||
if !photoExists && ind.conf.DetectNSFW() {
|
||||
photo.PhotoPrivate = ind.NSFW(m)
|
||||
}
|
||||
}
|
||||
|
||||
if fileChanged || o.UpdateExif {
|
||||
|
@ -165,7 +168,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
}
|
||||
|
||||
if metaData.Title != "" && (photo.NoTitle() || photo.TitleSrc == entity.SrcExif) {
|
||||
photo.PhotoTitle = metaData.Title
|
||||
photo.PhotoTitle = txt.Clip(metaData.Title, 200)
|
||||
photo.TitleSrc = entity.SrcExif
|
||||
}
|
||||
|
||||
|
@ -229,7 +232,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
} else {
|
||||
log.Info("index: no latitude and longitude in metadata")
|
||||
|
||||
photo.Place = entity.UnknownPlace
|
||||
photo.Place = &entity.UnknownPlace
|
||||
photo.PlaceID = entity.UnknownPlace.ID
|
||||
}
|
||||
}
|
||||
|
@ -237,7 +240,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
// TODO: Proof-of-concept for indexing XMP sidecar files
|
||||
if data, err := meta.XMP(m.FileName()); err == nil {
|
||||
if data.Title != "" && photo.TitleSrc == entity.SrcAuto {
|
||||
photo.PhotoTitle = data.Title
|
||||
photo.PhotoTitle = txt.Clip(data.Title, 200)
|
||||
photo.TitleSrc = entity.SrcXmp
|
||||
}
|
||||
|
||||
|
@ -260,7 +263,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
}
|
||||
|
||||
if photo.Place == nil {
|
||||
photo.Place = entity.UnknownPlace
|
||||
photo.Place = &entity.UnknownPlace
|
||||
photo.PlaceID = entity.UnknownPlace.ID
|
||||
}
|
||||
|
||||
|
@ -443,12 +446,8 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
return result
|
||||
}
|
||||
|
||||
// isNSFW returns true if media file might be offensive and detection is enabled.
|
||||
func (ind *Index) isNSFW(jpeg *MediaFile) bool {
|
||||
if !ind.conf.DetectNSFW() {
|
||||
return false
|
||||
}
|
||||
|
||||
// NSFW returns true if media file might be offensive and detection is enabled.
|
||||
func (ind *Index) NSFW(jpeg *MediaFile) bool {
|
||||
filename, err := jpeg.Thumbnail(ind.thumbnailsPath(), "fit_720")
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -177,10 +177,10 @@ func (m *MediaFile) FocalLength() int {
|
|||
}
|
||||
|
||||
// FNumber returns the F number with which the media file was created.
|
||||
func (m *MediaFile) FNumber() float64 {
|
||||
func (m *MediaFile) FNumber() float32 {
|
||||
info, err := m.MetaData()
|
||||
|
||||
var result float64
|
||||
var result float32
|
||||
|
||||
if err == nil {
|
||||
result = info.FNumber
|
||||
|
@ -653,7 +653,7 @@ func (m *MediaFile) Height() int {
|
|||
}
|
||||
|
||||
// AspectRatio returns the aspect ratio of a MediaFile.
|
||||
func (m *MediaFile) AspectRatio() float64 {
|
||||
func (m *MediaFile) AspectRatio() float32 {
|
||||
width := float64(m.Width())
|
||||
height := float64(m.Height())
|
||||
|
||||
|
@ -661,7 +661,7 @@ func (m *MediaFile) AspectRatio() float64 {
|
|||
return 0
|
||||
}
|
||||
|
||||
aspectRatio := width / height
|
||||
aspectRatio := float32(width / height)
|
||||
|
||||
return aspectRatio
|
||||
}
|
||||
|
|
|
@ -151,14 +151,14 @@ func TestMediaFile_FNumber(t *testing.T) {
|
|||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/cat_brown.jpg")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2.2, mediaFile.FNumber())
|
||||
assert.Equal(t, float32(2.2), mediaFile.FNumber())
|
||||
})
|
||||
t.Run("/elephants.jpg", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/elephants.jpg")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 10.0, mediaFile.FNumber())
|
||||
assert.Equal(t, float32(10.0), mediaFile.FNumber())
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -977,7 +977,7 @@ func TestMediaFile_AspectRatio(t *testing.T) {
|
|||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic")
|
||||
assert.Nil(t, err)
|
||||
ratio := mediaFile.AspectRatio()
|
||||
assert.Equal(t, 0.75, ratio)
|
||||
assert.Equal(t, float32(0.75), ratio)
|
||||
})
|
||||
t.Run("/fern_green.jpg", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
@ -985,7 +985,7 @@ func TestMediaFile_AspectRatio(t *testing.T) {
|
|||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/fern_green.jpg")
|
||||
assert.Nil(t, err)
|
||||
ratio := mediaFile.AspectRatio()
|
||||
assert.Equal(t, float64(1), ratio)
|
||||
assert.Equal(t, float32(1), ratio)
|
||||
})
|
||||
t.Run("/elephants.jpg", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
@ -993,7 +993,7 @@ func TestMediaFile_AspectRatio(t *testing.T) {
|
|||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/elephants.jpg")
|
||||
assert.Nil(t, err)
|
||||
ratio := mediaFile.AspectRatio()
|
||||
assert.Equal(t, 1.501510574018127, ratio)
|
||||
assert.Equal(t, float32(1.5015106), ratio)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -35,11 +35,11 @@ func TestMediaFile_Exif_JPEG(t *testing.T) {
|
|||
assert.Equal(t, "", info.Artist)
|
||||
assert.Equal(t, 111, info.FocalLength)
|
||||
assert.Equal(t, "1/640", info.Exposure)
|
||||
assert.Equal(t, 6.644, info.Aperture)
|
||||
assert.Equal(t, 10.0, info.FNumber)
|
||||
assert.Equal(t, float32(6.644), info.Aperture)
|
||||
assert.Equal(t, float32(10), info.FNumber)
|
||||
assert.Equal(t, 200, info.Iso)
|
||||
assert.Equal(t, -33.45347, info.Lat)
|
||||
assert.Equal(t, 25.764645, info.Lng)
|
||||
assert.Equal(t, float32(-33.45347), info.Lat)
|
||||
assert.Equal(t, float32(25.764645), info.Lng)
|
||||
assert.Equal(t, 190, info.Altitude)
|
||||
assert.Equal(t, 2048, info.Width)
|
||||
assert.Equal(t, 1365, info.Height)
|
||||
|
@ -70,8 +70,8 @@ func TestMediaFile_Exif_JPEG(t *testing.T) {
|
|||
assert.Equal(t, "", info.Artist)
|
||||
assert.Equal(t, 100, info.FocalLength)
|
||||
assert.Equal(t, "1/250", info.Exposure)
|
||||
assert.Equal(t, 6.644, info.Aperture)
|
||||
assert.Equal(t, 10.0, info.FNumber)
|
||||
assert.Equal(t, float32(6.644), info.Aperture)
|
||||
assert.Equal(t, float32(10), info.FNumber)
|
||||
assert.Equal(t, 200, info.Iso)
|
||||
assert.Equal(t, 0, info.Altitude)
|
||||
assert.Equal(t, 2048, info.Width)
|
||||
|
@ -110,10 +110,10 @@ func TestMediaFile_Exif_DNG(t *testing.T) {
|
|||
assert.Equal(t, "", info.Artist)
|
||||
assert.Equal(t, 65, info.FocalLength)
|
||||
assert.Equal(t, "1/60", info.Exposure)
|
||||
assert.Equal(t, 4.971, info.Aperture)
|
||||
assert.Equal(t, float32(4.971), info.Aperture)
|
||||
assert.Equal(t, 1000, info.Iso)
|
||||
assert.Equal(t, 0.0, info.Lat)
|
||||
assert.Equal(t, 0.0, info.Lng)
|
||||
assert.Equal(t, float32(0), info.Lat)
|
||||
assert.Equal(t, float32(0), info.Lng)
|
||||
assert.Equal(t, 0, info.Altitude)
|
||||
assert.Equal(t, 256, info.Width)
|
||||
assert.Equal(t, 171, info.Height)
|
||||
|
@ -162,10 +162,10 @@ func TestMediaFile_Exif_HEIF(t *testing.T) {
|
|||
assert.Equal(t, "", jpegInfo.Artist)
|
||||
assert.Equal(t, 74, jpegInfo.FocalLength)
|
||||
assert.Equal(t, "1/4000", jpegInfo.Exposure)
|
||||
assert.Equal(t, 1.696, jpegInfo.Aperture)
|
||||
assert.Equal(t, float32(1.696), jpegInfo.Aperture)
|
||||
assert.Equal(t, 20, jpegInfo.Iso)
|
||||
assert.Equal(t, 34.79745, jpegInfo.Lat)
|
||||
assert.Equal(t, 134.76463333333334, jpegInfo.Lng)
|
||||
assert.Equal(t, float32(34.79745), jpegInfo.Lat)
|
||||
assert.Equal(t, float32(134.76463), jpegInfo.Lng)
|
||||
assert.Equal(t, 0, jpegInfo.Altitude)
|
||||
assert.Equal(t, 4032, jpegInfo.Width)
|
||||
assert.Equal(t, 3024, jpegInfo.Height)
|
||||
|
|
|
@ -20,7 +20,7 @@ func TestNewCountry(t *testing.T) {
|
|||
country := entity.NewCountry("", "")
|
||||
assert.Equal(t, "zz", country.ID)
|
||||
assert.Equal(t, "Unknown", country.CountryName)
|
||||
assert.Equal(t, "unknown", country.CountrySlug)
|
||||
assert.Equal(t, "zz", country.CountrySlug)
|
||||
})
|
||||
}
|
||||
func TestCountry_FirstOrCreate(t *testing.T) {
|
||||
|
|
|
@ -14,8 +14,8 @@ import (
|
|||
// GeoResult represents a photo for displaying it on a map.
|
||||
type GeoResult struct {
|
||||
ID string `json:"ID"`
|
||||
PhotoLat float64 `json:"Lat"`
|
||||
PhotoLng float64 `json:"Lng"`
|
||||
PhotoLat float32 `json:"Lat"`
|
||||
PhotoLng float32 `json:"Lng"`
|
||||
PhotoUUID string `json:"PhotoUUID"`
|
||||
PhotoTitle string `json:"PhotoTitle"`
|
||||
PhotoFavorite bool `json:"PhotoFavorite"`
|
||||
|
@ -25,6 +25,14 @@ type GeoResult struct {
|
|||
TakenAt time.Time `json:"TakenAt"`
|
||||
}
|
||||
|
||||
func (g GeoResult) Lat() float64 {
|
||||
return float64(g.PhotoLat)
|
||||
}
|
||||
|
||||
func (g GeoResult) Lng() float64 {
|
||||
return float64(g.PhotoLng)
|
||||
}
|
||||
|
||||
// Geo searches for photos based on a Form and returns a PhotoResult slice.
|
||||
func (q *Query) Geo(f form.GeoSearch) (results []GeoResult, err error) {
|
||||
if err := f.ParseQueryString(); err != nil {
|
||||
|
@ -69,14 +77,14 @@ func (q *Query) Geo(f form.GeoSearch) (results []GeoResult, err error) {
|
|||
} else {
|
||||
// Inaccurate distance search, but probably 'good enough' for now
|
||||
if f.Lat > 0 {
|
||||
latMin := f.Lat - SearchRadius*float64(f.Dist)
|
||||
latMax := f.Lat + SearchRadius*float64(f.Dist)
|
||||
latMin := f.Lat - SearchRadius*float32(f.Dist)
|
||||
latMax := f.Lat + SearchRadius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latMin, latMax)
|
||||
}
|
||||
|
||||
if f.Lng > 0 {
|
||||
lngMin := f.Lng - SearchRadius*float64(f.Dist)
|
||||
lngMax := f.Lng + SearchRadius*float64(f.Dist)
|
||||
lngMin := f.Lng - SearchRadius*float32(f.Dist)
|
||||
lngMax := f.Lng + SearchRadius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngMin, lngMax)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,12 +33,12 @@ type PhotoResult struct {
|
|||
PhotoCountry string
|
||||
PhotoFavorite bool
|
||||
PhotoPrivate bool
|
||||
PhotoLat float64
|
||||
PhotoLng float64
|
||||
PhotoLat float32
|
||||
PhotoLng float32
|
||||
PhotoAltitude int
|
||||
PhotoIso int
|
||||
PhotoFocalLength int
|
||||
PhotoFNumber float64
|
||||
PhotoFNumber float32
|
||||
PhotoExposure string
|
||||
PhotoQuality int
|
||||
PhotoResolution int
|
||||
|
@ -74,7 +74,7 @@ type PhotoResult struct {
|
|||
FileWidth int
|
||||
FileHeight int
|
||||
FileOrientation int
|
||||
FileAspectRatio float64
|
||||
FileAspectRatio float32
|
||||
FileColors string // todo: remove from result?
|
||||
FileChroma uint8 // todo: remove from result?
|
||||
FileLuminance string // todo: remove from result?
|
||||
|
@ -244,6 +244,18 @@ func (q *Query) Photos(f form.PhotoSearch) (results PhotoResults, count int, err
|
|||
s = s.Where("photos.deleted_at IS NOT NULL")
|
||||
} else {
|
||||
s = s.Where("photos.deleted_at IS NULL")
|
||||
|
||||
if f.Private {
|
||||
s = s.Where("photos.photo_private = 1")
|
||||
} else if f.Public {
|
||||
s = s.Where("photos.photo_private = 0")
|
||||
}
|
||||
|
||||
if f.Review {
|
||||
s = s.Where("photos.photo_quality < 3")
|
||||
} else if f.Quality != 0 && f.Private == false {
|
||||
s = s.Where("photos.photo_quality >= ?", f.Quality)
|
||||
}
|
||||
}
|
||||
|
||||
if f.Error {
|
||||
|
@ -278,18 +290,6 @@ func (q *Query) Photos(f form.PhotoSearch) (results PhotoResults, count int, err
|
|||
s = s.Where("photos.photo_favorite = 1")
|
||||
}
|
||||
|
||||
if f.Public {
|
||||
s = s.Where("photos.photo_private = 0")
|
||||
}
|
||||
|
||||
if f.Safe {
|
||||
s = s.Where("photos.photo_nsfw = 0")
|
||||
}
|
||||
|
||||
if f.Nsfw {
|
||||
s = s.Where("photos.photo_nsfw = 1")
|
||||
}
|
||||
|
||||
if f.Story {
|
||||
s = s.Where("photos.photo_story = 1")
|
||||
}
|
||||
|
@ -322,12 +322,6 @@ func (q *Query) Photos(f form.PhotoSearch) (results PhotoResults, count int, err
|
|||
s = s.Where("files.file_chroma > 0 AND files.file_chroma <= ?", f.Chroma)
|
||||
}
|
||||
|
||||
if f.Review {
|
||||
s = s.Where("photos.photo_quality < 3")
|
||||
} else if f.Quality != 0 {
|
||||
s = s.Where("photos.photo_quality >= ?", f.Quality)
|
||||
}
|
||||
|
||||
if f.Diff != 0 {
|
||||
s = s.Where("files.file_diff = ?", f.Diff)
|
||||
}
|
||||
|
@ -348,14 +342,14 @@ func (q *Query) Photos(f form.PhotoSearch) (results PhotoResults, count int, err
|
|||
|
||||
// Inaccurate distance search, but probably 'good enough' for now
|
||||
if f.Lat > 0 {
|
||||
latMin := f.Lat - SearchRadius*float64(f.Dist)
|
||||
latMax := f.Lat + SearchRadius*float64(f.Dist)
|
||||
latMin := f.Lat - SearchRadius*float32(f.Dist)
|
||||
latMax := f.Lat + SearchRadius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latMin, latMax)
|
||||
}
|
||||
|
||||
if f.Lng > 0 {
|
||||
lngMin := f.Lng - SearchRadius*float64(f.Dist)
|
||||
lngMax := f.Lng + SearchRadius*float64(f.Dist)
|
||||
lngMin := f.Lng - SearchRadius*float32(f.Dist)
|
||||
lngMax := f.Lng + SearchRadius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngMin, lngMax)
|
||||
}
|
||||
|
||||
|
|
|
@ -12,9 +12,10 @@ package remote
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var client = &http.Client{}
|
||||
var client = &http.Client{Timeout: 30 * time.Second} // TODO: Change timeout if needed
|
||||
|
||||
const (
|
||||
ServiceWebDAV = "webdav"
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
|
@ -28,6 +29,8 @@ type Client struct {
|
|||
func New(url, user, pass string) Client {
|
||||
clt := gowebdav.NewClient(url, user, pass)
|
||||
|
||||
clt.SetTimeout(10 * time.Minute) // TODO: Change timeout if needed
|
||||
|
||||
result := Client{client: clt}
|
||||
|
||||
return result
|
||||
|
|
Loading…
Reference in a new issue