feat: add gps data editor (close #418)
parent
608e8556d8
commit
ffd105eac6
|
@ -133,6 +133,9 @@ class PageController extends Controller
|
||||||
$addImageDomain('https://*.tile.openstreetmap.org');
|
$addImageDomain('https://*.tile.openstreetmap.org');
|
||||||
$addImageDomain('https://*.a.ssl.fastly.net');
|
$addImageDomain('https://*.a.ssl.fastly.net');
|
||||||
|
|
||||||
|
// Allow Nominatim
|
||||||
|
$policy->addAllowedConnectDomain('nominatim.openstreetmap.org');
|
||||||
|
|
||||||
return $policy;
|
return $policy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -163,6 +163,8 @@ export default defineComponent({
|
||||||
subtitle: [],
|
subtitle: [],
|
||||||
icon: LocationIcon,
|
icon: LocationIcon,
|
||||||
href: this.mapFullUrl,
|
href: this.mapFullUrl,
|
||||||
|
edit: () =>
|
||||||
|
globalThis.editMetadata([globalThis.currentViewerPhoto], [4]),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,13 @@
|
||||||
<NcTextField
|
<NcTextField
|
||||||
class="field"
|
class="field"
|
||||||
:id="'exif-field-' + field.field"
|
:id="'exif-field-' + field.field"
|
||||||
:value.sync="exif[field.field]"
|
|
||||||
:label-outside="true"
|
:label-outside="true"
|
||||||
|
:value.sync="exif[field.field]"
|
||||||
:placeholder="placeholder(field)"
|
:placeholder="placeholder(field)"
|
||||||
@input="dirty[field.field] = true"
|
@input="dirty[field.field] = true"
|
||||||
|
trailing-button-icon="close"
|
||||||
|
:show-trailing-button="dirty[field.field]"
|
||||||
|
@trailing-button-click="reset(field)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -119,6 +122,11 @@ export default defineComponent({
|
||||||
? t("memories", "Empty")
|
? t("memories", "Empty")
|
||||||
: t("memories", "Unchanged");
|
: t("memories", "Unchanged");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
reset(field: any) {
|
||||||
|
this.exif[field.field] = "";
|
||||||
|
this.dirty[field.field] = false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1,194 @@
|
||||||
|
<template>
|
||||||
|
<div class="outer">
|
||||||
|
<div class="lat-lon">
|
||||||
|
<span>{{ loc }}</span> {{ dirty ? "*" : "" }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NcTextField
|
||||||
|
:value.sync="searchBar"
|
||||||
|
:placeholder="t('memories', 'Search location / landmark')"
|
||||||
|
trailing-button-icon="arrowRight"
|
||||||
|
:show-trailing-button="searchBar.length > 0 && !loading"
|
||||||
|
@trailing-button-click="search"
|
||||||
|
@keypress.enter="search"
|
||||||
|
>
|
||||||
|
<Magnify :size="16" />
|
||||||
|
</NcTextField>
|
||||||
|
|
||||||
|
<NcLoadingIcon class="loading-spinner" v-if="loading" />
|
||||||
|
|
||||||
|
<ul v-if="options.length > 0">
|
||||||
|
<li
|
||||||
|
v-for="option in options"
|
||||||
|
:key="option.osm_id"
|
||||||
|
@click="select(option)"
|
||||||
|
>
|
||||||
|
{{ option.display_name }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { IPhoto } from "../../types";
|
||||||
|
|
||||||
|
import axios from "@nextcloud/axios";
|
||||||
|
import { showError } from "@nextcloud/dialogs";
|
||||||
|
|
||||||
|
const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField");
|
||||||
|
const NcListItem = () => import("@nextcloud/vue/dist/Components/NcListItem");
|
||||||
|
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon";
|
||||||
|
|
||||||
|
import Magnify from "vue-material-design-icons/Magnify.vue";
|
||||||
|
|
||||||
|
type NLocation = {
|
||||||
|
osm_id: number;
|
||||||
|
type: string;
|
||||||
|
icon: string;
|
||||||
|
display_name: string;
|
||||||
|
lat: string;
|
||||||
|
lon: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
NcTextField,
|
||||||
|
NcListItem,
|
||||||
|
NcLoadingIcon,
|
||||||
|
Magnify,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
photos: {
|
||||||
|
type: Array<IPhoto>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
dirty: false,
|
||||||
|
lat: null as number | null,
|
||||||
|
lon: null as number | null,
|
||||||
|
searchBar: "",
|
||||||
|
loading: false,
|
||||||
|
|
||||||
|
options: [] as NLocation[],
|
||||||
|
}),
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
loc() {
|
||||||
|
if (this.lat && this.lon) {
|
||||||
|
return `${this.lat.toFixed(6)}, ${this.lon.toFixed(6)}`;
|
||||||
|
}
|
||||||
|
return this.t("memories", "Unknown coordinates");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
const photos = this.photos as IPhoto[];
|
||||||
|
|
||||||
|
let lat = 0,
|
||||||
|
lon = 0,
|
||||||
|
count = 0;
|
||||||
|
for (const photo of photos) {
|
||||||
|
const exif = photo.imageInfo?.exif;
|
||||||
|
if (!exif) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exif.GPSLatitude && exif.GPSLongitude) {
|
||||||
|
lat += Number(exif.GPSLatitude);
|
||||||
|
lon += Number(exif.GPSLongitude);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
this.lat = lat / count;
|
||||||
|
this.lon = lon / count;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
search() {
|
||||||
|
if (this.loading || this.searchBar.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
const q = window.encodeURIComponent(this.searchBar);
|
||||||
|
axios
|
||||||
|
.get(
|
||||||
|
`https://nominatim.openstreetmap.org/search.php?q=${q}&format=jsonv2`
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
this.loading = false;
|
||||||
|
this.options = response.data.filter((x: NLocation) => {
|
||||||
|
return x.lat && x.lon && x.display_name;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.loading = false;
|
||||||
|
console.error(error);
|
||||||
|
showError(
|
||||||
|
this.t("memories", "Failed to search for location with Nominatim.")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
select(option: NLocation) {
|
||||||
|
this.dirty = true;
|
||||||
|
this.lat = Number(option.lat);
|
||||||
|
this.lon = Number(option.lon);
|
||||||
|
this.options = [];
|
||||||
|
this.searchBar = "";
|
||||||
|
},
|
||||||
|
|
||||||
|
result() {
|
||||||
|
if (!this.dirty || !this.lat || !this.lon) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
GPSLatitude: this.lat,
|
||||||
|
GPSLongitude: this.lon,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.outer {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.lat-lon {
|
||||||
|
padding: 4px;
|
||||||
|
> span {
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 10px 0;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: 5px 10px;
|
||||||
|
margin: 2px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-background-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -37,6 +37,13 @@
|
||||||
</div>
|
</div>
|
||||||
<EditExif ref="editExif" :photos="photos" />
|
<EditExif ref="editExif" :photos="photos" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="sections.includes(4)">
|
||||||
|
<div class="title-text">
|
||||||
|
{{ t("memories", "Geolocation") }}
|
||||||
|
</div>
|
||||||
|
<EditLocation ref="editLocation" :photos="photos" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="processing">
|
<div v-if="processing">
|
||||||
|
@ -56,9 +63,10 @@ const NcProgressBar = () =>
|
||||||
import("@nextcloud/vue/dist/Components/NcProgressBar");
|
import("@nextcloud/vue/dist/Components/NcProgressBar");
|
||||||
import Modal from "./Modal.vue";
|
import Modal from "./Modal.vue";
|
||||||
|
|
||||||
import EditExif from "./EditExif.vue";
|
|
||||||
import EditDate from "./EditDate.vue";
|
import EditDate from "./EditDate.vue";
|
||||||
import EditTags from "./EditTags.vue";
|
import EditTags from "./EditTags.vue";
|
||||||
|
import EditExif from "./EditExif.vue";
|
||||||
|
import EditLocation from "./EditLocation.vue";
|
||||||
|
|
||||||
import { showError } from "@nextcloud/dialogs";
|
import { showError } from "@nextcloud/dialogs";
|
||||||
import { emit } from "@nextcloud/event-bus";
|
import { emit } from "@nextcloud/event-bus";
|
||||||
|
@ -74,9 +82,10 @@ export default defineComponent({
|
||||||
NcProgressBar,
|
NcProgressBar,
|
||||||
Modal,
|
Modal,
|
||||||
|
|
||||||
EditExif,
|
|
||||||
EditDate,
|
EditDate,
|
||||||
EditTags,
|
EditTags,
|
||||||
|
EditExif,
|
||||||
|
EditLocation,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [UserConfig],
|
mixins: [UserConfig],
|
||||||
|
@ -95,7 +104,7 @@ export default defineComponent({
|
||||||
this.$emit("refresh", val);
|
this.$emit("refresh", val);
|
||||||
},
|
},
|
||||||
|
|
||||||
async open(photos: IPhoto[], sections: number[] = [1, 2, 3]) {
|
async open(photos: IPhoto[], sections: number[] = [1, 2, 3, 4]) {
|
||||||
const state = (this.state = Math.random());
|
const state = (this.state = Math.random());
|
||||||
this.show = true;
|
this.show = true;
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
@ -164,7 +173,10 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get exif fields diff
|
// Get exif fields diff
|
||||||
const exifResult = (<any>this.$refs.editExif)?.result?.() || {};
|
const exifResult = {
|
||||||
|
...((<any>this.$refs.editExif)?.result?.() || {}),
|
||||||
|
...((<any>this.$refs.editLocation)?.result?.() || {}),
|
||||||
|
};
|
||||||
const tagsResult = (<any>this.$refs.editTags)?.result?.() || null;
|
const tagsResult = (<any>this.$refs.editTags)?.result?.() || null;
|
||||||
|
|
||||||
// Start processing
|
// Start processing
|
||||||
|
|
|
@ -6,7 +6,9 @@
|
||||||
:label="t('memories', 'Search')"
|
:label="t('memories', 'Search')"
|
||||||
:placeholder="t('memories', 'Search')"
|
:placeholder="t('memories', 'Search')"
|
||||||
@input="searchChanged"
|
@input="searchChanged"
|
||||||
/>
|
>
|
||||||
|
<Magnify :size="16" />
|
||||||
|
</NcTextField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="photo" v-for="photo of detail" :key="photo.fileid">
|
<div class="photo" v-for="photo of detail" :key="photo.fileid">
|
||||||
|
@ -28,11 +30,14 @@ import NcTextField from "@nextcloud/vue/dist/Components/NcTextField";
|
||||||
import * as dav from "../../services/DavRequests";
|
import * as dav from "../../services/DavRequests";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
|
|
||||||
|
import Magnify from "vue-material-design-icons/Magnify.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "FaceList",
|
name: "FaceList",
|
||||||
components: {
|
components: {
|
||||||
Tag,
|
Tag,
|
||||||
NcTextField,
|
NcTextField,
|
||||||
|
Magnify,
|
||||||
},
|
},
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
|
Loading…
Reference in New Issue