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 {