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://*.a.ssl.fastly.net');
|
||||
|
||||
// Allow Nominatim
|
||||
$policy->addAllowedConnectDomain('nominatim.openstreetmap.org');
|
||||
|
||||
return $policy;
|
||||
}
|
||||
|
||||
|
|
|
@ -163,6 +163,8 @@ export default defineComponent({
|
|||
subtitle: [],
|
||||
icon: LocationIcon,
|
||||
href: this.mapFullUrl,
|
||||
edit: () =>
|
||||
globalThis.editMetadata([globalThis.currentViewerPhoto], [4]),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -7,10 +7,13 @@
|
|||
<NcTextField
|
||||
class="field"
|
||||
:id="'exif-field-' + field.field"
|
||||
:value.sync="exif[field.field]"
|
||||
:label-outside="true"
|
||||
:value.sync="exif[field.field]"
|
||||
:placeholder="placeholder(field)"
|
||||
@input="dirty[field.field] = true"
|
||||
trailing-button-icon="close"
|
||||
:show-trailing-button="dirty[field.field]"
|
||||
@trailing-button-click="reset(field)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -119,6 +122,11 @@ export default defineComponent({
|
|||
? t("memories", "Empty")
|
||||
: t("memories", "Unchanged");
|
||||
},
|
||||
|
||||
reset(field: any) {
|
||||
this.exif[field.field] = "";
|
||||
this.dirty[field.field] = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
</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>
|
||||
<EditExif ref="editExif" :photos="photos" />
|
||||
</div>
|
||||
|
||||
<div v-if="sections.includes(4)">
|
||||
<div class="title-text">
|
||||
{{ t("memories", "Geolocation") }}
|
||||
</div>
|
||||
<EditLocation ref="editLocation" :photos="photos" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="processing">
|
||||
|
@ -56,9 +63,10 @@ const NcProgressBar = () =>
|
|||
import("@nextcloud/vue/dist/Components/NcProgressBar");
|
||||
import Modal from "./Modal.vue";
|
||||
|
||||
import EditExif from "./EditExif.vue";
|
||||
import EditDate from "./EditDate.vue";
|
||||
import EditTags from "./EditTags.vue";
|
||||
import EditExif from "./EditExif.vue";
|
||||
import EditLocation from "./EditLocation.vue";
|
||||
|
||||
import { showError } from "@nextcloud/dialogs";
|
||||
import { emit } from "@nextcloud/event-bus";
|
||||
|
@ -74,9 +82,10 @@ export default defineComponent({
|
|||
NcProgressBar,
|
||||
Modal,
|
||||
|
||||
EditExif,
|
||||
EditDate,
|
||||
EditTags,
|
||||
EditExif,
|
||||
EditLocation,
|
||||
},
|
||||
|
||||
mixins: [UserConfig],
|
||||
|
@ -95,7 +104,7 @@ export default defineComponent({
|
|||
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());
|
||||
this.show = true;
|
||||
this.processing = true;
|
||||
|
@ -164,7 +173,10 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// Start processing
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
:label="t('memories', 'Search')"
|
||||
:placeholder="t('memories', 'Search')"
|
||||
@input="searchChanged"
|
||||
/>
|
||||
>
|
||||
<Magnify :size="16" />
|
||||
</NcTextField>
|
||||
</div>
|
||||
|
||||
<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 Fuse from "fuse.js";
|
||||
|
||||
import Magnify from "vue-material-design-icons/Magnify.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "FaceList",
|
||||
components: {
|
||||
Tag,
|
||||
NcTextField,
|
||||
Magnify,
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
|
|
Loading…
Reference in New Issue