Add range date edit
parent
1e297f86f4
commit
c7a60473a4
|
@ -12,38 +12,91 @@
|
|||
<span>{{ t('memories', 'Edit Date/Time') }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="photos.length === 1 && longDateStr">
|
||||
<div v-if="longDateStr">
|
||||
|
||||
<span v-if="photos.length > 1">
|
||||
[{{ t('memories', 'Newest') }}]
|
||||
</span>
|
||||
{{ longDateStr }}
|
||||
|
||||
<div class="fields">
|
||||
<div class="fields memories__editdate__fields">
|
||||
<NcTextField :value.sync="year"
|
||||
class="field"
|
||||
@input="newestChange()"
|
||||
:label="t('memories', 'Year')" :label-visible="true"
|
||||
:placeholder="t('memories', 'Year')" />
|
||||
<NcTextField :value.sync="month"
|
||||
class="field"
|
||||
@input="newestChange()"
|
||||
:label="t('memories', 'Month')" :label-visible="true"
|
||||
:placeholder="t('memories', 'Month')" />
|
||||
<NcTextField :value.sync="day"
|
||||
class="field"
|
||||
@input="newestChange()"
|
||||
:label="t('memories', 'Day')" :label-visible="true"
|
||||
:placeholder="t('memories', 'Day')" />
|
||||
<NcTextField :value.sync="hour"
|
||||
class="field"
|
||||
@input="newestChange(true)"
|
||||
:label="t('memories', 'Time')" :label-visible="true"
|
||||
:placeholder="t('memories', 'Hour')" />
|
||||
<NcTextField :value.sync="minute"
|
||||
class="field"
|
||||
@input="newestChange(true)"
|
||||
:label="t('memories', 'Minute')"
|
||||
:placeholder="t('memories', 'Minute')" />
|
||||
</div>
|
||||
|
||||
<div v-if="photos.length > 1" class="oldest">
|
||||
<span>
|
||||
[{{ t('memories', 'Oldest') }}]
|
||||
</span>
|
||||
{{ longDateStrLast }}
|
||||
|
||||
<div class="fields memories__editdate__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">
|
||||
{{ t('memories', 'Processing ... {n}/{m}', {
|
||||
n: photosDone,
|
||||
m: photos.length,
|
||||
}) }}
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<NcButton @click="save" class="button" type="primary">
|
||||
{{ t('memories', 'Save') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
{{ t('memories', 'Loading data ... {n}/{m}', {
|
||||
n: photosDone,
|
||||
m: photos.length,
|
||||
}) }}
|
||||
</div>
|
||||
</div>
|
||||
</NcModal>
|
||||
</template>
|
||||
|
@ -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<any>(generateUrl(INFO_API_URL, { id: this.photos[0].fileid }));
|
||||
const calls = photos.map((p) => async () => {
|
||||
try {
|
||||
const res = await axios.get<any>(generateUrl(INFO_API_URL, { id: p.fileid }));
|
||||
if (typeof res.data.datetaken !== "string") {
|
||||
console.error("Invalid date");
|
||||
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++;
|
||||
}
|
||||
});
|
||||
|
||||
const utcEpoch = Date.parse(res.data.datetaken + " UTC");
|
||||
const date = new Date(utcEpoch);
|
||||
for await (const _ of dav.runInParallel(calls, 10)) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
// 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<any>(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<any>(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;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -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;
|
||||
|
@ -158,3 +372,11 @@ export default class EditDate extends Mixins(GlobalMixin) {
|
|||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.memories__editdate__fields label {
|
||||
font-size: 0.8em;
|
||||
padding: 0 !important;
|
||||
padding-left: 3px !important;
|
||||
}
|
||||
</style>
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue