edit-exif: combine dialogs

pull/461/head
Varun Patil 2023-03-07 18:49:13 -08:00
parent 538bca5bb4
commit 6b3eda89d1
5 changed files with 519 additions and 443 deletions

View File

@ -36,8 +36,7 @@
</div>
<!-- Selection Modals -->
<EditDate ref="editDate" @refresh="refresh" />
<EditExif ref="editExif" @refresh="refresh" />
<EditMetadata ref="editMetadata" @refresh="refresh" />
<FaceMoveModal
ref="faceMoveModal"
@moved="deletePhotos"
@ -71,8 +70,7 @@ import { getCurrentUser } from "@nextcloud/auth";
import * as dav from "../services/DavRequests";
import * as utils from "../services/Utils";
import EditDate from "./modal/EditDate.vue";
import EditExif from "./modal/EditExif.vue";
import EditMetadata from "./modal/EditMetadataModal.vue";
import FaceMoveModal from "./modal/FaceMoveModal.vue";
import AddToAlbumModal from "./modal/AddToAlbumModal.vue";
import MoveToFolderModal from "./modal/MoveToFolderModal.vue";
@ -98,8 +96,7 @@ export default defineComponent({
components: {
NcActions,
NcActionButton,
EditDate,
EditExif,
EditMetadata,
FaceMoveModal,
AddToAlbumModal,
MoveToFolderModal,
@ -174,16 +171,9 @@ export default defineComponent({
if: () => this.routeIsArchive(),
},
{
name: t("memories", "Edit Date/Time"),
icon: EditClockIcon,
callback: this.editDateSelection.bind(this),
if: () => !this.routeIsAlbum(),
},
{
name: t("memories", "Edit EXIF Data"),
name: t("memories", "Edit Metadata"),
icon: EditFileIcon,
callback: this.editExifSelection.bind(this),
if: () => this.selection.size === 1 && !this.routeIsAlbum(),
callback: this.editMetadataSelection.bind(this),
},
{
name: t("memories", "View in folder"),
@ -224,10 +214,8 @@ export default defineComponent({
sel.set(photo.fileid, photo);
return sel;
};
globalThis.editDate = (photo: IPhoto) =>
this.editDateSelection(getSel(photo));
globalThis.editExif = (photo: IPhoto) =>
this.editExifSelection(getSel(photo));
globalThis.editMetadata = (photo: IPhoto) =>
this.editMetadataSelection(getSel(photo));
},
watch: {
@ -761,16 +749,8 @@ export default defineComponent({
/**
* Open the edit date dialog
*/
async editDateSelection(selection: Selection) {
(<any>this.$refs.editDate).open(Array.from(selection.values()));
},
/**
* Open the edit date dialog
*/
async editExifSelection(selection: Selection) {
if (selection.size !== 1) return;
(<any>this.$refs.editExif).open(selection.values().next().value);
async editMetadataSelection(selection: Selection) {
(<any>this.$refs.editMetadata).open(Array.from(selection.values()));
},
/**

View File

@ -1,153 +1,129 @@
<template>
<Modal v-if="photos.length > 0" @close="close">
<template #title>
{{ t("memories", "Edit Date/Time") }}
</template>
<template #buttons>
<NcButton @click="save" class="button" type="error" v-if="longDateStr">
{{ t("memories", "Update Exif") }}
</NcButton>
</template>
<div v-if="longDateStr">
<div>
<div class="title-text">
<span v-if="photos.length > 1"> [{{ t("memories", "Newest") }}] </span>
{{ longDateStr }}
{{ newestDirty ? "*" : "" }}
</div>
<div class="fields">
<NcTextField
class="field"
:value.sync="year"
:label="t('memories', 'Year')"
:label-visible="true"
:placeholder="t('memories', 'Year')"
@input="newestChange()"
/>
<NcTextField
class="field"
:value.sync="month"
:label="t('memories', 'Month')"
:label-visible="true"
:placeholder="t('memories', 'Month')"
@input="newestChange()"
/>
<NcTextField
class="field"
:value.sync="day"
:label="t('memories', 'Day')"
:label-visible="true"
:placeholder="t('memories', 'Day')"
@input="newestChange()"
/>
<NcTextField
class="field"
:value.sync="hour"
:label="t('memories', 'Time')"
:label-visible="true"
:placeholder="t('memories', 'Hour')"
@input="newestChange(true)"
/>
<NcTextField
class="field"
:value.sync="minute"
:label="t('memories', 'Minute')"
:placeholder="t('memories', 'Minute')"
@input="newestChange(true)"
/>
</div>
<div v-if="photos.length > 1" class="oldest">
<div class="title-text">
<span> [{{ t("memories", "Oldest") }}] </span>
{{ longDateStrLast }}
{{ oldestDirty ? "*" : "" }}
</div>
<div class="fields">
<NcTextField
:value.sync="year"
class="field"
@input="newestChange()"
:value.sync="yearLast"
:label="t('memories', 'Year')"
:label-visible="true"
:placeholder="t('memories', 'Year')"
@input="oldestChange()"
/>
<NcTextField
:value.sync="month"
class="field"
@input="newestChange()"
:value.sync="monthLast"
:label="t('memories', 'Month')"
:label-visible="true"
:placeholder="t('memories', 'Month')"
@input="oldestChange()"
/>
<NcTextField
:value.sync="day"
class="field"
@input="newestChange()"
:value.sync="dayLast"
:label="t('memories', 'Day')"
:label-visible="true"
:placeholder="t('memories', 'Day')"
@input="oldestChange()"
/>
<NcTextField
:value.sync="hour"
class="field"
@input="newestChange(true)"
:value.sync="hourLast"
:label="t('memories', 'Time')"
:label-visible="true"
:placeholder="t('memories', 'Hour')"
@input="oldestChange()"
/>
<NcTextField
:value.sync="minute"
class="field"
@input="newestChange(true)"
:value.sync="minuteLast"
:label="t('memories', 'Minute')"
:placeholder="t('memories', 'Minute')"
@input="oldestChange()"
/>
</div>
<div v-if="photos.length > 1" class="oldest">
<span> [{{ t("memories", "Oldest") }}] </span>
{{ longDateStrLast }}
<div class="fields">
<NcTextField
:value.sync="yearLast"
class="field"
:label="t('memories', 'Year')"
:label-visible="true"
:placeholder="t('memories', 'Year')"
/>
<NcTextField
:value.sync="monthLast"
class="field"
:label="t('memories', 'Month')"
:label-visible="true"
:placeholder="t('memories', 'Month')"
/>
<NcTextField
:value.sync="dayLast"
class="field"
:label="t('memories', 'Day')"
:label-visible="true"
:placeholder="t('memories', 'Day')"
/>
<NcTextField
:value.sync="hourLast"
class="field"
:label="t('memories', 'Time')"
:label-visible="true"
:placeholder="t('memories', 'Hour')"
/>
<NcTextField
:value.sync="minuteLast"
class="field"
:label="t('memories', 'Minute')"
:placeholder="t('memories', 'Minute')"
/>
</div>
</div>
<div v-if="processing" class="info-pad">
{{
t("memories", "Processing … {n}/{m}", {
n: photosDone,
m: photos.length,
})
}}
</div>
</div>
<div v-else>
{{
t("memories", "Loading data … {n}/{m}", {
n: photosDone,
m: photos.length,
})
}}
</div>
</Modal>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { IPhoto } from "../../types";
import NcButton from "@nextcloud/vue/dist/Components/NcButton";
const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField");
import { showError } from "@nextcloud/dialogs";
import { emit } from "@nextcloud/event-bus";
import Modal from "./Modal.vue";
import axios from "@nextcloud/axios";
import * as utils from "../../services/Utils";
import * as dav from "../../services/DavRequests";
import { API } from "../../services/API";
export default defineComponent({
name: "EditDate",
components: {
NcButton,
NcTextField,
Modal,
},
props: {
photos: {
type: Array,
required: true,
},
},
data: () => ({
photos: [] as IPhoto[],
photosDone: 0,
processing: false,
sortedPhotos: [] as IPhoto[],
longDateStr: "",
year: "0",
month: "0",
day: "0",
@ -155,93 +131,164 @@ export default defineComponent({
minute: "0",
second: "0",
longDateStrLast: "",
yearLast: "0",
monthLast: "0",
dayLast: "0",
hourLast: "0",
minuteLast: "0",
secondLast: "0",
newestDirty: false,
oldestDirty: false,
}),
methods: {
emitRefresh(val: boolean) {
this.$emit("refresh", val);
mounted() {
this.init();
},
watch: {
photos() {
this.init();
},
},
computed: {
date() {
return this.makeDate(
this.year,
this.month,
this.day,
this.hour,
this.minute,
this.second
);
},
async open(photos: IPhoto[]) {
this.photos = photos;
if (photos.length === 0) {
return;
}
this.photosDone = 0;
this.longDateStr = "";
dateLast() {
return this.makeDate(
this.yearLast,
this.monthLast,
this.dayLast,
this.hourLast,
this.minuteLast,
this.secondLast
);
},
const calls = photos.map((p) => async () => {
try {
const res = await axios.get<any>(
API.Q(API.IMAGE_INFO(p.fileid), "basic=1")
);
if (typeof res.data.datetaken !== "number") {
console.error("Invalid date for", p.fileid);
return;
}
p.datetaken = res.data.datetaken * 1000;
} catch (error) {
console.error("Failed to get date info for", p.fileid, error);
} finally {
this.photosDone++;
}
});
dateDiff() {
return this.date.getTime() - this.dateLast.getTime();
},
for await (const _ of dav.runInParallel(calls, 10)) {
// nothing to do
}
origDateNewest() {
return new Date(this.sortedPhotos[0].datetaken);
},
// Remove photos without datetaken
this.photos = this.photos.filter((p) => p.datetaken !== undefined);
origDateOldest() {
return new Date(
this.sortedPhotos[this.sortedPhotos.length - 1].datetaken
);
},
origDateDiff() {
return this.origDateNewest.getTime() - this.origDateOldest.getTime();
},
scaleFactor() {
return this.origDateDiff > 0 ? this.dateDiff / this.origDateDiff : 0;
},
longDateStr() {
return this.date
? utils.getLongDateStr(this.date, false, true)
: this.t("memories", "Invalid Date");
},
longDateStrLast() {
return this.dateLast
? utils.getLongDateStr(this.dateLast, false, true)
: this.t("memories", "Invalid Date");
},
},
methods: {
init() {
const photos = (this.sortedPhotos = [...this.photos] as IPhoto[]);
// Sort photos by datetaken descending
this.photos.sort((a, b) => b.datetaken - a.datetaken);
photos.sort((a, b) => b.datetaken - a.datetaken);
// Get date of newest photo
let date = new Date(this.photos[0].datetaken);
let date = new Date(photos[0].datetaken);
this.year = date.getUTCFullYear().toString();
this.month = (date.getUTCMonth() + 1).toString();
this.day = date.getUTCDate().toString();
this.hour = date.getUTCHours().toString();
this.minute = date.getUTCMinutes().toString();
this.second = date.getUTCSeconds().toString();
this.longDateStr = utils.getLongDateStr(date, false, true);
// Get date of oldest photo
if (this.photos.length > 1) {
date = new Date(this.photos[this.photos.length - 1].datetaken);
if (photos.length > 1) {
date = new Date(photos[photos.length - 1].datetaken);
this.yearLast = date.getUTCFullYear().toString();
this.monthLast = (date.getUTCMonth() + 1).toString();
this.dayLast = date.getUTCDate().toString();
this.hourLast = date.getUTCHours().toString();
this.minuteLast = date.getUTCMinutes().toString();
this.secondLast = date.getUTCSeconds().toString();
this.longDateStrLast = utils.getLongDateStr(date, false, true);
}
},
validate() {
if (!this.date) {
throw new Error(this.t("memories", "Invalid Date"));
}
if (this.photos.length > 1) {
if (!this.dateLast) {
throw new Error(this.t("memories", "Invalid Date"));
}
if (this.dateDiff < -60000) {
// 1 minute
throw new Error(
this.t("memories", "Newest date is older than oldest date")
);
}
}
},
result(photo: IPhoto): undefined | string {
if (!this.oldestDirty && !this.newestDirty) {
return undefined;
}
if (this.sortedPhotos.length === 0) {
return undefined;
}
if (this.sortedPhotos.length === 1) {
return this.getExifFormat(this.date);
}
// Interpolate date
const dT = this.date.getTime();
const doT = this.origDateNewest.getTime();
const offset = (photo.datetaken || doT) - doT;
return this.getExifFormat(new Date(dT + offset * this.scaleFactor));
},
newestChange(time = false) {
if (this.photos.length === 0) {
if (this.sortedPhotos.length === 0) {
return;
}
this.newestDirty = true;
// Set the last date to have the same offset to newest date
try {
const date = new Date(this.photos[0].datetaken);
const dateLast = new Date(
this.photos[this.photos.length - 1].datetaken
);
const dateNew = this.getDate();
const offset = dateNew.getTime() - date.getTime();
const dateLastNew = new Date(dateLast.getTime() + offset);
const dateNew = this.date;
const offset = dateNew.getTime() - this.origDateNewest.getTime();
const dateLastNew = new Date(this.origDateOldest.getTime() + offset);
this.yearLast = dateLastNew.getUTCFullYear().toString();
this.monthLast = (dateLastNew.getUTCMonth() + 1).toString();
@ -255,116 +302,8 @@ export default defineComponent({
} catch (error) {}
},
close() {
this.photos = [];
},
async saveOne() {
// Make PATCH request to update date
try {
this.processing = true;
const fileid = this.photos[0].fileid;
await axios.patch<any>(API.IMAGE_SETEXIF(fileid), {
raw: {
DateTimeOriginal: this.getExifFormat(this.getDate()),
},
});
emit("files:file:updated", { fileid });
this.emitRefresh(true);
this.close();
} catch (e) {
if (e.response?.data?.message) {
showError(e.response.data.message);
} else {
showError(e);
}
} finally {
this.processing = false;
}
},
async saveMany() {
if (this.processing) {
return;
}
// Get difference between newest and oldest date
const date = new Date(this.photos[0].datetaken);
const dateLast = new Date(this.photos[this.photos.length - 1].datetaken);
const diff = date.getTime() - dateLast.getTime();
// Get new difference between newest and oldest date
let dateNew: Date;
let dateLastNew: Date;
let diffNew: number;
try {
dateNew = this.getDate();
dateLastNew = this.getDateLast();
diffNew = dateNew.getTime() - dateLastNew.getTime();
} catch (e) {
showError(e);
return;
}
// Validate if the old is still old
if (diffNew < 0) {
showError("The newest date must be newer than the oldest date");
return;
}
// Mark processing
this.processing = true;
this.photosDone = 0;
// Create PATCH requests
const calls = this.photos.map((p) => async () => {
try {
let pDate = new Date(p.datetaken);
// Fallback to start date if invalid date
if (isNaN(pDate.getTime())) {
pDate = date;
}
const offset = date.getTime() - pDate.getTime();
const scale = diff > 0 ? diffNew / diff : 0;
const pDateNew = new Date(dateNew.getTime() - offset * scale);
await axios.patch<any>(API.IMAGE_SETEXIF(p.fileid), {
raw: {
DateTimeOriginal: this.getExifFormat(pDateNew),
},
});
emit("files:file:updated", { fileid: p.fileid });
} catch (e) {
if (e.response?.data?.message) {
showError(e.response.data.message);
} else {
showError(e);
}
} finally {
this.photosDone++;
}
});
for await (const _ of dav.runInParallel(calls, 10)) {
// nothing to do
}
this.processing = false;
this.emitRefresh(true);
this.close();
},
async save() {
if (this.photos.length === 0) {
return;
}
if (this.photos.length === 1) {
return await this.saveOne();
}
return await this.saveMany();
oldestChange() {
this.oldestDirty = true;
},
getExifFormat(date: Date) {
@ -377,54 +316,36 @@ export default defineComponent({
return `${year}:${month}:${day} ${hour}:${minute}:${second}`;
},
getDate() {
const dateNew = new Date();
const year = parseInt(this.year, 10);
const month = parseInt(this.month, 10) - 1;
const day = parseInt(this.day, 10);
const hour = parseInt(this.hour, 10);
const minute = parseInt(this.minute, 10);
const second = parseInt(this.second, 10) || 0;
makeDate(
yearS: string,
monthS: string,
dayS: string,
hourS: string,
minuteS: string,
secondS: string
) {
const date = new Date();
const year = parseInt(yearS, 10);
const month = parseInt(monthS, 10) - 1;
const day = parseInt(dayS, 10);
const hour = parseInt(hourS, 10);
const minute = parseInt(minuteS, 10);
const second = parseInt(secondS, 10) || 0;
if (isNaN(year)) throw new Error("Invalid year");
if (isNaN(month)) throw new Error("Invalid month");
if (isNaN(day)) throw new Error("Invalid day");
if (isNaN(hour)) throw new Error("Invalid hour");
if (isNaN(minute)) throw new Error("Invalid minute");
if (isNaN(second)) throw new Error("Invalid second");
if (isNaN(year)) return null;
if (isNaN(month)) return null;
if (isNaN(day)) return null;
if (isNaN(hour)) return null;
if (isNaN(minute)) return null;
if (isNaN(second)) return null;
dateNew.setUTCFullYear(year);
dateNew.setUTCMonth(month);
dateNew.setUTCDate(day);
dateNew.setUTCHours(hour);
dateNew.setUTCMinutes(minute);
dateNew.setUTCSeconds(second);
return dateNew;
},
getDateLast() {
const dateNew = new Date();
const year = parseInt(this.yearLast, 10);
const month = parseInt(this.monthLast, 10) - 1;
const day = parseInt(this.dayLast, 10);
const hour = parseInt(this.hourLast, 10);
const minute = parseInt(this.minuteLast, 10);
const second = parseInt(this.secondLast, 10) || 0;
if (isNaN(year)) throw new Error("Invalid last year");
if (isNaN(month)) throw new Error("Invalid last month");
if (isNaN(day)) throw new Error("Invalid last day");
if (isNaN(hour)) throw new Error("Invalid last hour");
if (isNaN(minute)) throw new Error("Invalid last minute");
if (isNaN(second)) throw new Error("Invalid last second");
dateNew.setUTCFullYear(year);
dateNew.setUTCMonth(month);
dateNew.setUTCDate(day);
dateNew.setUTCHours(hour);
dateNew.setUTCMinutes(minute);
dateNew.setUTCSeconds(second);
return dateNew;
date.setUTCFullYear(year);
date.setUTCMonth(month);
date.setUTCDate(day);
date.setUTCHours(hour);
date.setUTCMinutes(minute);
date.setUTCSeconds(second);
return date;
},
},
});
@ -444,18 +365,12 @@ export default defineComponent({
}
}
.title-text {
font-size: 0.9em;
margin-left: 0.2em;
}
.oldest {
margin-top: 10px;
}
.info-pad {
margin-top: 6px;
margin-bottom: 2px;
&.warn {
color: #f44336;
font-size: 0.8em;
line-height: 1em;
}
}
</style>

View File

@ -1,64 +1,45 @@
<template>
<Modal v-if="show" @close="close">
<template #title>
{{ t("memories", "Edit EXIF Data") }}
</template>
<template #buttons>
<NcButton
@click="save"
class="button"
type="error"
v-if="exif"
:disabled="processing"
>
{{ t("memories", "Update Exif") }}
</NcButton>
</template>
<div v-if="exif">
<div class="fields">
<NcTextField
v-for="field of fields"
:key="field.field"
:value.sync="exif[field.field]"
class="field"
:label="field.label"
:label-visible="true"
:placeholder="field.label"
/>
</div>
<div class="fields" v-if="exif">
<div v-for="field of fields" :key="field.field">
<label :for="'exif-field-' + field.field">
{{ label(field) }}
</label>
<NcTextField
class="field"
:id="'exif-field-' + field.field"
:value.sync="exif[field.field]"
:label-outside="true"
:placeholder="placeholder(field)"
@input="dirty[field.field] = true"
/>
</div>
</Modal>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { IPhoto } from "../../types";
import NcButton from "@nextcloud/vue/dist/Components/NcButton";
const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField");
import { showError } from "@nextcloud/dialogs";
import { emit } from "@nextcloud/event-bus";
import axios from "@nextcloud/axios";
import { translate as t } from "@nextcloud/l10n";
import Modal from "./Modal.vue";
import { API } from "../../services/API";
export default defineComponent({
components: {
NcButton,
NcTextField,
Modal,
},
props: {
photos: {
type: Array,
required: true,
},
},
data: () => ({
photo: null as IPhoto,
show: false,
exif: null as any,
processing: false,
dirty: {},
fields: [
{
field: "Title",
@ -68,10 +49,6 @@ export default defineComponent({
field: "Description",
label: t("memories", "Description"),
},
{
field: "DateTimeOriginal",
label: t("memories", "Date Taken"),
},
{
field: "Label",
label: t("memories", "Label"),
@ -95,67 +72,52 @@ export default defineComponent({
],
}),
methods: {
emitRefresh(val: boolean) {
this.$emit("refresh", val);
},
mounted() {
let exif = {};
for (const field of this.fields) {
exif[field.field] = null;
this.dirty[field.field] = false;
}
async open(photo: IPhoto) {
this.show = true;
const res = await axios.get(API.IMAGE_INFO(photo.fileid));
if (!res.data?.exif) return;
const photos = this.photos as IPhoto[];
for (const photo of photos) {
if (!photo.imageInfo?.exif) {
continue;
}
const exif: any = {};
for (const field of this.fields) {
exif[field.field] = res.data.exif[field.field] || "";
}
this.photo = photo;
this.exif = exif;
},
close() {
this.exif = null;
this.photo = null;
this.show = false;
},
async saveOne() {
try {
// remove all null values from this.exif
const exif = JSON.parse(JSON.stringify(this.exif));
for (const key in exif) {
if (!exif[key]) {
delete exif[key];
}
}
// Make PATCH request to update date
this.processing = true;
const fileid = this.photo.fileid;
await axios.patch<any>(API.IMAGE_SETEXIF(fileid), {
raw: exif,
});
emit("files:file:updated", { fileid });
this.emitRefresh(true);
this.close();
} catch (e) {
if (e.response?.data?.message) {
showError(e.response.data.message);
const ePhoto = photo.imageInfo?.exif[field.field];
const eCurr = exif[field.field];
if (ePhoto && (eCurr === null || ePhoto === eCurr)) {
exif[field.field] = ePhoto;
} else {
showError(e);
exif[field.field] = "";
}
} finally {
this.processing = false;
}
}
this.exif = exif;
},
methods: {
changes() {
const diff = {};
for (const field of this.fields) {
if (this.dirty[field.field]) {
diff[field.field] = this.exif[field.field];
}
}
return diff;
},
async save() {
if (!this.photo) {
return;
}
label(field: any) {
return field.label + (this.dirty[field.field] ? "*" : "");
},
return await this.saveOne();
placeholder(field: any) {
return this.dirty[field.field]
? t("memories", "Empty")
: t("memories", "Unchanged");
},
},
});

View File

@ -0,0 +1,219 @@
<template>
<Modal v-if="show" @close="close">
<template #title>
{{ t("memories", "Edit Metadata") }}
</template>
<template #buttons>
<NcButton
@click="save"
class="button"
type="error"
v-if="photos"
:disabled="processing"
>
{{ t("memories", "Save") }}
</NcButton>
</template>
<div v-if="photos">
<div class="title-text">
{{ t("memories", "Date / Time") }}
</div>
<EditDate ref="editDate" :photos="photos" />
<div class="title-text">
{{ t("memories", "EXIF Fields") }}
</div>
<EditExif ref="editExif" :photos="photos" />
</div>
<div v-if="processing">
<NcProgressBar :value="progress" :error="true" />
</div>
</Modal>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { IPhoto } from "../../types";
import NcButton from "@nextcloud/vue/dist/Components/NcButton";
const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField");
const NcProgressBar = () =>
import("@nextcloud/vue/dist/Components/NcProgressBar");
import Modal from "./Modal.vue";
import EditExif from "./EditExif.vue";
import EditDate from "./EditDate.vue";
import { showError } from "@nextcloud/dialogs";
import { emit } from "@nextcloud/event-bus";
import axios from "@nextcloud/axios";
import * as dav from "../../services/DavRequests";
import { API } from "../../services/API";
export default defineComponent({
components: {
NcButton,
NcTextField,
NcProgressBar,
Modal,
EditExif,
EditDate,
},
data: () => ({
photos: null as IPhoto[],
show: false,
processing: false,
progress: 0,
state: 0,
}),
methods: {
emitRefresh(val: boolean) {
this.$emit("refresh", val);
},
async open(photos: IPhoto[]) {
const state = (this.state = Math.random());
this.show = true;
this.processing = true;
let done = 0;
this.progress = 0;
// Load metadata for all photos
const calls = photos.map((p) => async () => {
try {
const res = await axios.get<any>(API.IMAGE_INFO(p.fileid));
// Validate response
p.imageInfo = null;
if (typeof res.data.datetaken !== "number") {
console.error("Invalid date for", p.fileid);
return;
}
p.datetaken = res.data.datetaken * 1000;
p.imageInfo = res.data;
} catch (error) {
console.error("Failed to get date info for", p.fileid, error);
} finally {
done++;
this.progress = Math.round((done * 100) / photos.length);
}
});
for await (const _ of dav.runInParallel(calls, 8)) {
// nothing to do
}
// Check if already quit
if (!this.show || this.state !== state) return;
// Check for anything invalid
const invalid = photos.filter((p) => !p.imageInfo);
if (invalid.length > 0) {
showError(
this.t("memories", "Failed to load metadata for {n} photos.", {
n: invalid.length,
})
);
photos = photos.filter((p) => p.imageInfo);
}
this.photos = photos;
this.processing = false;
},
close() {
this.photos = null;
this.show = false;
},
async save() {
// Perform validation
try {
(<any>this.$refs.editDate).validate();
} catch (e) {
console.error(e);
showError(e);
return;
}
// Get exif fields diff
const exifChanges = (<any>this.$refs.editExif).changes();
// Start processing
let done = 0;
this.progress = 0;
this.processing = true;
// Update exif fields
const calls = this.photos.map((p) => async () => {
try {
const fileid = p.fileid;
// Basic EXIF fields
const raw = JSON.parse(JSON.stringify(exifChanges));
// Date
const date = (<any>this.$refs.editDate).result(p);
if (date) {
raw.DateTimeOriginal = date;
}
if (Object.keys(raw).length === 0) {
console.log("No changes for", p.fileid);
return;
} else {
console.log("Saving EXIF info for", p.fileid, raw);
}
await axios.patch<any>(API.IMAGE_SETEXIF(fileid), { raw });
// Clear imageInfo in photo
p.imageInfo = null;
// Emit event to update photo
emit("files:file:updated", { fileid });
} catch (e) {
console.error("Failed to save EXIF info for", p.fileid, e);
if (e.response?.data?.message) {
showError(e.response.data.message);
} else {
showError(e);
}
} finally {
done++;
this.progress = Math.round((done * 100) / this.photos.length);
}
});
for await (const _ of dav.runInParallel(calls, 8)) {
// nothing to do
}
this.processing = false;
this.close();
this.emitRefresh(true);
},
},
});
</script>
<style scoped lang="scss">
.title-text {
font-size: 1.05em;
font-weight: 500;
margin-top: 25px;
&:first-of-type {
margin-top: 10px;
}
}
</style>

View File

@ -132,12 +132,12 @@
</template>
</NcActionButton>
<NcActionButton
:aria-label="t('memories', 'Edit EXIF Data')"
:aria-label="t('memories', 'Edit Metadata')"
v-if="!routeIsPublic"
@click="editExif"
@click="editMetadata"
:close-after-click="true"
>
{{ t("memories", "Edit EXIF Data") }}
{{ t("memories", "Edit Metadata") }}
<template #icon>
<EditFileIcon :size="24" />
</template>
@ -1165,10 +1165,10 @@ export default defineComponent({
},
/**
* Edit EXIF data for current photo
* Edit Metadata for current photo
*/
editExif() {
globalThis.editExif(globalThis.currentViewerPhoto);
editMetadata() {
globalThis.editMetadata(globalThis.currentViewerPhoto);
},
},
});