diff --git a/src/components/EditDate.vue b/src/components/EditDate.vue
index 9a7af468..3aff08c9 100644
--- a/src/components/EditDate.vue
+++ b/src/components/EditDate.vue
@@ -12,38 +12,91 @@
{{ t('memories', 'Edit Date/Time') }}
-
+
+
+
+ [{{ t('memories', 'Newest') }}]
+
{{ longDateStr }}
-
+
+
+
+ [{{ t('memories', 'Oldest') }}]
+
+ {{ longDateStrLast }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('memories', 'Processing ... {n}/{m}', {
+ n: photosDone,
+ m: photos.length,
+ }) }}
+
+
{{ t('memories', 'Save') }}
+
+
+ {{ t('memories', 'Loading data ... {n}/{m}', {
+ n: photosDone,
+ m: photos.length,
+ }) }}
+
@@ -58,6 +111,7 @@ import { showError } from '@nextcloud/dialogs'
import { generateUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'
import * as utils from '../services/Utils';
+import * as dav from "../services/DavRequests";
const INFO_API_URL = '/apps/memories/api/info/{id}';
const EDIT_API_URL = '/apps/memories/api/edit/{id}';
@@ -71,6 +125,8 @@ const EDIT_API_URL = '/apps/memories/api/edit/{id}';
})
export default class EditDate extends Mixins(GlobalMixin) {
private photos: IPhoto[] = [];
+ private photosDone: number = 0;
+ private processing: boolean = false;
private longDateStr: string = '';
private year: string = "0";
@@ -80,55 +136,210 @@ export default class EditDate extends Mixins(GlobalMixin) {
private minute: string = "0";
private second: string = "0";
+ private longDateStrLast: string = '';
+ private yearLast: string = "0";
+ private monthLast: string = "0";
+ private dayLast: string = "0";
+ private hourLast: string = "0";
+ private minuteLast: string = "0";
+ private secondLast: string = "0";
+
public async open(photos: IPhoto[]) {
this.photos = photos;
if (photos.length === 0) {
return;
}
+ this.photosDone = 0;
+ this.longDateStr = '';
- const res = await axios.get
(generateUrl(INFO_API_URL, { id: this.photos[0].fileid }));
- if (typeof res.data.datetaken !== "string") {
- console.error("Invalid date");
- return;
+ const calls = photos.map((p) => async () => {
+ try {
+ const res = await axios.get(generateUrl(INFO_API_URL, { id: p.fileid }));
+ if (typeof res.data.datetaken !== "string") {
+ console.error("Invalid date for", p.fileid);
+ return;
+ }
+ p.datetaken = Date.parse(res.data.datetaken + " UTC");
+ } catch (error) {
+ console.error('Failed to get date info for', p.fileid, error);
+ } finally {
+ this.photosDone++;
+ }
+ });
+
+ for await (const _ of dav.runInParallel(calls, 10)) {
+ // nothing to do
}
- const utcEpoch = Date.parse(res.data.datetaken + " UTC");
- const date = new Date(utcEpoch);
+ // Remove photos without datetaken
+ this.photos = this.photos.filter((p) => p.datetaken !== undefined);
+
+ // Sort photos by datetaken descending
+ this.photos.sort((a, b) => b.datetaken - a.datetaken);
+
+ // Get date of newest photo
+ let date = new Date(this.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);
+ 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);
+ }
+ }
+
+ public newestChange(time=false) {
+ if (this.photos.length === 0) {
+ return;
+ }
+
+ // 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);
+
+ this.yearLast = dateLastNew.getUTCFullYear().toString();
+ this.monthLast = (dateLastNew.getUTCMonth() + 1).toString();
+ this.dayLast = dateLastNew.getUTCDate().toString();
+
+ if (time) {
+ this.hourLast = dateLastNew.getUTCHours().toString();
+ this.minuteLast = dateLastNew.getUTCMinutes().toString();
+ this.secondLast = dateLastNew.getUTCSeconds().toString();
+ }
+ } catch (error) {}
}
public close() {
this.photos = [];
}
- public async save() {
- // Pad zeros to the left
- this.year = this.year.padStart(4, '0');
- this.month = this.month.padStart(2, '0');
- this.day = this.day.padStart(2, '0');
- this.hour = this.hour.padStart(2, '0');
- this.minute = this.minute.padStart(2, '0');
- this.second = this.second.padStart(2, '0');
-
+ public async saveOne() {
// Make PATCH request to update date
try {
+ this.processing = true;
const res = await axios.patch(generateUrl(EDIT_API_URL, { id: this.photos[0].fileid }), {
- date: `${this.year}:${this.month}:${this.day} ${this.hour}:${this.minute}:${this.second}`,
+ date: this.getExifFormat(this.getDate()),
});
this.close();
} catch (e) {
if (e.response?.data?.message) {
showError(e.response.data.message);
}
+ } finally {
+ this.processing = false;
}
}
+
+ public 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
+ const dateNew = this.getDate();
+ const dateLastNew = this.getDateLast();
+ const diffNew = dateNew.getTime() - dateLastNew.getTime();
+
+ // 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 {
+ const pDate = new Date(p.datetaken);
+ const offset = date.getTime() - pDate.getTime();
+ const pDateNew = new Date(dateNew.getTime() - offset * (diffNew / diff));
+ const res = await axios.patch(generateUrl(EDIT_API_URL, { id: p.fileid }), {
+ date: this.getExifFormat(pDateNew),
+ });
+ } catch (e) {
+ if (e.response?.data?.message) {
+ showError(e.response.data.message);
+ }
+ } finally {
+ this.photosDone++;
+ }
+ });
+
+ for await (const _ of dav.runInParallel(calls, 10)) {
+ // nothing to do
+ }
+ this.processing = false;
+ this.close();
+ }
+
+ public async save() {
+ if (this.photos.length === 0) {
+ return;
+ }
+
+ if (this.photos.length === 1) {
+ return await this.saveOne();
+ }
+
+ return await this.saveMany();
+ }
+
+ private getExifFormat(date: Date) {
+ const year = date.getUTCFullYear().toString().padStart(4, "0");
+ const month = (date.getUTCMonth() + 1).toString().padStart(2, "0");
+ const day = date.getUTCDate().toString().padStart(2, "0");
+ const hour = date.getUTCHours().toString().padStart(2, "0");
+ const minute = date.getUTCMinutes().toString().padStart(2, "0");
+ const second = date.getUTCSeconds().toString().padStart(2, "0");
+ return `${year}:${month}:${day} ${hour}:${minute}:${second}`;
+ }
+
+ public getDate() {
+ const dateNew = new Date();
+ dateNew.setUTCFullYear(parseInt(this.year));
+ dateNew.setUTCMonth(parseInt(this.month) - 1);
+ dateNew.setUTCDate(parseInt(this.day));
+ dateNew.setUTCHours(parseInt(this.hour));
+ dateNew.setUTCMinutes(parseInt(this.minute));
+ dateNew.setUTCSeconds(parseInt(this.second));
+ return dateNew;
+ }
+
+ public getDateLast() {
+ const dateLast = new Date();
+ dateLast.setUTCFullYear(parseInt(this.yearLast));
+ dateLast.setUTCMonth(parseInt(this.monthLast) - 1);
+ dateLast.setUTCDate(parseInt(this.dayLast));
+ dateLast.setUTCHours(parseInt(this.hourLast));
+ dateLast.setUTCMinutes(parseInt(this.minuteLast));
+ dateLast.setUTCSeconds(parseInt(this.secondLast));
+ return dateLast;
+ }
}
@@ -142,13 +353,16 @@ export default class EditDate extends Mixins(GlobalMixin) {
}
.fields {
- margin-top: 5px;
.field {
width: 4.1em;
display: inline-block;
}
}
+.oldest {
+ margin-top: 10px;
+}
+
.buttons {
margin-top: 10px;
text-align: right;
@@ -157,4 +371,12 @@ export default class EditDate extends Mixins(GlobalMixin) {
display: inline-block;
}
}
+
+
+
\ No newline at end of file
diff --git a/src/types.ts b/src/types.ts
index 3cc966e9..b7b1b403 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -36,6 +36,8 @@ export type IPhoto = {
isfavorite?: boolean;
/** Is this a folder */
isfolder?: boolean;
+ /** Optional datetaken epoch */
+ datetaken?: number;
}
export interface IFolder extends IPhoto {