Add single file exif update (#42)
parent
df7866b876
commit
1e297f86f4
|
@ -17,6 +17,8 @@ return [
|
||||||
// API
|
// API
|
||||||
['name' => 'api#days', 'url' => '/api/days', 'verb' => 'GET'],
|
['name' => 'api#days', 'url' => '/api/days', 'verb' => 'GET'],
|
||||||
['name' => 'api#day', 'url' => '/api/days/{id}', 'verb' => 'GET'],
|
['name' => 'api#day', 'url' => '/api/days/{id}', 'verb' => 'GET'],
|
||||||
|
['name' => 'api#imageInfo', 'url' => '/api/info/{id}', 'verb' => 'GET'],
|
||||||
|
['name' => 'api#imageEdit', 'url' => '/api/edit/{id}', 'verb' => 'PATCH'],
|
||||||
|
|
||||||
// Config API
|
// Config API
|
||||||
['name' => 'api#setUserConfig', 'url' => '/api/config/{key}', 'verb' => 'PUT'],
|
['name' => 'api#setUserConfig', 'url' => '/api/config/{key}', 'verb' => 'PUT'],
|
||||||
|
|
|
@ -25,10 +25,9 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace OCA\Memories\Controller;
|
namespace OCA\Memories\Controller;
|
||||||
|
|
||||||
use OC\Files\Search\SearchComparison;
|
|
||||||
use OC\Files\Search\SearchQuery;
|
|
||||||
use OCA\Memories\AppInfo\Application;
|
use OCA\Memories\AppInfo\Application;
|
||||||
use OCA\Memories\Db\TimelineQuery;
|
use OCA\Memories\Db\TimelineQuery;
|
||||||
|
use OCA\Memories\Db\TimelineWrite;
|
||||||
use OCA\Memories\Exif;
|
use OCA\Memories\Exif;
|
||||||
use OCP\AppFramework\Controller;
|
use OCP\AppFramework\Controller;
|
||||||
use OCP\AppFramework\Http;
|
use OCP\AppFramework\Http;
|
||||||
|
@ -40,7 +39,6 @@ use OCP\IRequest;
|
||||||
use OCP\IUserSession;
|
use OCP\IUserSession;
|
||||||
use OCP\Files\FileInfo;
|
use OCP\Files\FileInfo;
|
||||||
use OCP\Files\Folder;
|
use OCP\Files\Folder;
|
||||||
use OCP\Files\Search\ISearchComparison;
|
|
||||||
|
|
||||||
class ApiController extends Controller {
|
class ApiController extends Controller {
|
||||||
private IConfig $config;
|
private IConfig $config;
|
||||||
|
@ -48,6 +46,7 @@ class ApiController extends Controller {
|
||||||
private IDBConnection $connection;
|
private IDBConnection $connection;
|
||||||
private IRootFolder $rootFolder;
|
private IRootFolder $rootFolder;
|
||||||
private TimelineQuery $timelineQuery;
|
private TimelineQuery $timelineQuery;
|
||||||
|
private TimelineWrite $timelineWrite;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
IRequest $request,
|
IRequest $request,
|
||||||
|
@ -63,6 +62,7 @@ class ApiController extends Controller {
|
||||||
$this->connection = $connection;
|
$this->connection = $connection;
|
||||||
$this->rootFolder = $rootFolder;
|
$this->rootFolder = $rootFolder;
|
||||||
$this->timelineQuery = new TimelineQuery($this->connection);
|
$this->timelineQuery = new TimelineQuery($this->connection);
|
||||||
|
$this->timelineWrite = new TimelineWrite($connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -232,6 +232,84 @@ class ApiController extends Controller {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
*
|
||||||
|
* Get image info for one file
|
||||||
|
* @param string fileid
|
||||||
|
*/
|
||||||
|
public function imageInfo(string $id): JSONResponse {
|
||||||
|
$user = $this->userSession->getUser();
|
||||||
|
if (is_null($user)) {
|
||||||
|
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
||||||
|
}
|
||||||
|
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
|
||||||
|
|
||||||
|
// Check for permissions and get numeric Id
|
||||||
|
$file = $userFolder->getById(intval($id));
|
||||||
|
if (count($file) === 0) {
|
||||||
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
$file = $file[0];
|
||||||
|
|
||||||
|
// Get the image info
|
||||||
|
$info = $this->timelineQuery->getInfoById($file->getId());
|
||||||
|
|
||||||
|
return new JSONResponse($info, Http::STATUS_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
* @NoCSRFRequired
|
||||||
|
*
|
||||||
|
* Change exif data for one file
|
||||||
|
* @param string fileid
|
||||||
|
*/
|
||||||
|
public function imageEdit(string $id): JSONResponse {
|
||||||
|
$user = $this->userSession->getUser();
|
||||||
|
if (is_null($user)) {
|
||||||
|
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
||||||
|
}
|
||||||
|
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
|
||||||
|
|
||||||
|
// Check for permissions and get numeric Id
|
||||||
|
$file = $userFolder->getById(intval($id));
|
||||||
|
if (count($file) === 0) {
|
||||||
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
$file = $file[0];
|
||||||
|
|
||||||
|
// TODO: check permissions
|
||||||
|
|
||||||
|
// Get new date from body
|
||||||
|
$body = $this->request->getParams();
|
||||||
|
if (!isset($body['date'])) {
|
||||||
|
return new JSONResponse(["message" => "Missing date"], Http::STATUS_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the date is valid
|
||||||
|
try {
|
||||||
|
Exif::parseExifDate($body['date']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return new JSONResponse(["message" => $e->getMessage()], Http::STATUS_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update date
|
||||||
|
try {
|
||||||
|
$res = Exif::updateExifDate($file, $body['date']);
|
||||||
|
if ($res === false) {
|
||||||
|
return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return new JSONResponse(["message" => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reprocess the file
|
||||||
|
$this->timelineWrite->processFile($file, true);
|
||||||
|
|
||||||
|
return $this->imageInfo($id);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @NoAdminRequired
|
* @NoAdminRequired
|
||||||
*
|
*
|
||||||
|
|
|
@ -14,4 +14,21 @@ class TimelineQuery {
|
||||||
public function __construct(IDBConnection $connection) {
|
public function __construct(IDBConnection $connection) {
|
||||||
$this->connection = $connection;
|
$this->connection = $connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getInfoById(int $id): array {
|
||||||
|
$qb = $this->connection->getQueryBuilder();
|
||||||
|
$qb->select('*')
|
||||||
|
->from('memories')
|
||||||
|
->where($qb->expr()->eq('fileid', $qb->createNamedParameter($id, \PDO::PARAM_INT)));
|
||||||
|
|
||||||
|
$result = $qb->executeQuery();
|
||||||
|
$row = $result->fetch();
|
||||||
|
$result->closeCursor();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'fileid' => intval($row['fileid']),
|
||||||
|
'dayid' => intval($row['dayid']),
|
||||||
|
'datetaken' => $row['datetaken'],
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
171
lib/Exif.php
171
lib/Exif.php
|
@ -243,6 +243,30 @@ class Exif {
|
||||||
return $json[0];
|
return $json[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse date from exif format and throw error if invalid
|
||||||
|
*
|
||||||
|
* @param string $dt
|
||||||
|
* @return int unix timestamp
|
||||||
|
*/
|
||||||
|
public static function parseExifDate($date) {
|
||||||
|
$dt = $date;
|
||||||
|
if (isset($dt) && is_string($dt) && !empty($dt)) {
|
||||||
|
$dt = explode('-', explode('+', $dt, 2)[0], 2)[0]; // get rid of timezone if present
|
||||||
|
$dt = \DateTime::createFromFormat('Y:m:d H:i:s', $dt);
|
||||||
|
if (!$dt) {
|
||||||
|
throw new \Exception("Invalid date: $date");
|
||||||
|
}
|
||||||
|
if ($dt && $dt->getTimestamp() > -5364662400) { // 1800 A.D.
|
||||||
|
return $dt->getTimestamp();
|
||||||
|
} else {
|
||||||
|
throw new \Exception("Date too old: $date");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new \Exception("No date provided");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the date taken from either the file or exif data if available.
|
* Get the date taken from either the file or exif data if available.
|
||||||
* @param File $file
|
* @param File $file
|
||||||
|
@ -255,13 +279,9 @@ class Exif {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if found something
|
// Check if found something
|
||||||
if (isset($dt) && is_string($dt) && !empty($dt)) {
|
try {
|
||||||
$dt = explode('-', explode('+', $dt, 2)[0], 2)[0]; // get rid of timezone if present
|
return self::parseExifDate($dt);
|
||||||
$dt = \DateTime::createFromFormat('Y:m:d H:i:s', $dt, new \DateTimeZone("UTC"));
|
} catch (\Exception $ex) {}
|
||||||
if ($dt && $dt->getTimestamp() > -5364662400) { // 1800 A.D.
|
|
||||||
return $dt->getTimestamp();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to creation time
|
// Fall back to creation time
|
||||||
$dateTaken = $file->getCreationTime();
|
$dateTaken = $file->getCreationTime();
|
||||||
|
@ -272,4 +292,141 @@ class Exif {
|
||||||
}
|
}
|
||||||
return $dateTaken;
|
return $dateTaken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update exif date using exiftool
|
||||||
|
*
|
||||||
|
* @param File $file
|
||||||
|
* @param string $newDate formatted in standard Exif format (YYYY:MM:DD HH:MM:SS)
|
||||||
|
*/
|
||||||
|
public static function updateExifDate(File &$file, string $newDate) {
|
||||||
|
// Check for local files -- this is easier
|
||||||
|
if (!$file->isEncrypted() && $file->getStorage()->isLocal()) {
|
||||||
|
$path = $file->getStorage()->getLocalFile($file->getInternalPath());
|
||||||
|
if (is_string($path)) {
|
||||||
|
return self::updateExifDateForLocalFile($path, $newDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a stream
|
||||||
|
return self::updateExifDateForStreamFile($file, $newDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update exif date using exiftool for a local file
|
||||||
|
*
|
||||||
|
* @param string $path
|
||||||
|
* @param string $newDate formatted in standard Exif format (YYYY:MM:DD HH:MM:SS)
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private static function updateExifDateForLocalFile(string $path, string $newDate) {
|
||||||
|
$cmd = ['exiftool', '-api', 'QuickTimeUTC=1', '-overwrite_original', '-DateTimeOriginal=' . $newDate, $path];
|
||||||
|
$proc = proc_open($cmd, [
|
||||||
|
1 => array('pipe', 'w'),
|
||||||
|
2 => array('pipe', 'w'),
|
||||||
|
], $pipes);
|
||||||
|
$stdout = self::readOrTimeout($pipes[1], 300000);
|
||||||
|
fclose($pipes[1]);
|
||||||
|
fclose($pipes[2]);
|
||||||
|
proc_terminate($proc);
|
||||||
|
if (strpos($stdout, 'error') !== false) {
|
||||||
|
error_log("Exiftool error: $stdout");
|
||||||
|
throw new \Exception("Could not update exif date: " . $stdout);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update exif date for stream
|
||||||
|
*
|
||||||
|
* @param File $file
|
||||||
|
* @param string $newDate formatted in standard Exif format (YYYY:MM:DD HH:MM:SS)
|
||||||
|
*/
|
||||||
|
public static function updateExifDateForStreamFile(File &$file, string $newDate) {
|
||||||
|
// Temp file for output, so we can compare sizes before writing to the actual file
|
||||||
|
$tmpfile = tmpfile();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Start exiftool and output to json
|
||||||
|
$pipes = [];
|
||||||
|
$proc = proc_open([
|
||||||
|
'exiftool', '-api', 'QuickTimeUTC=1',
|
||||||
|
'-overwrite_original', '-DateTimeOriginal=' . $newDate, '-'
|
||||||
|
], [
|
||||||
|
0 => array('pipe', 'rb'),
|
||||||
|
1 => array('pipe', 'w'),
|
||||||
|
2 => array('pipe', 'w'),
|
||||||
|
], $pipes);
|
||||||
|
|
||||||
|
// Write the file to exiftool's stdin
|
||||||
|
// Warning: this is slow for big files
|
||||||
|
$in = $file->fopen('rb');
|
||||||
|
if (!$in) {
|
||||||
|
throw new \Exception('Could not open file');
|
||||||
|
}
|
||||||
|
$origLen = stream_copy_to_stream($in, $pipes[0]);
|
||||||
|
fclose($in);
|
||||||
|
fclose($pipes[0]);
|
||||||
|
|
||||||
|
// Get output from exiftool
|
||||||
|
stream_set_blocking($pipes[1], false);
|
||||||
|
$newLen = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read and copy stdout of exiftool to the temp file
|
||||||
|
$waitedMs = 0;
|
||||||
|
$timeout = 300000;
|
||||||
|
while ($waitedMs < $timeout && !feof($pipes[1])) {
|
||||||
|
$r = stream_copy_to_stream($pipes[1], $tmpfile, 1024 * 1024);
|
||||||
|
$newLen += $r;
|
||||||
|
if ($r === 0) {
|
||||||
|
$waitedMs++;
|
||||||
|
usleep(1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($waitedMs >= $timeout) {
|
||||||
|
throw new \Exception('Timeout');
|
||||||
|
}
|
||||||
|
} catch (\Exception $ex) {
|
||||||
|
error_log("Exiftool timeout for file stream: " . $ex->getMessage());
|
||||||
|
throw new \Exception("Could not read from Exiftool");
|
||||||
|
} finally {
|
||||||
|
// Close the pipes
|
||||||
|
fclose($pipes[1]);
|
||||||
|
fclose($pipes[2]);
|
||||||
|
proc_terminate($proc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the new length of the file
|
||||||
|
// If the new length and old length are more different than 1KB, abort
|
||||||
|
if (abs($newLen - $origLen) > 1024) {
|
||||||
|
error_log("Exiftool error: new length $newLen, old length $origLen");
|
||||||
|
throw new \Exception("Exiftool error: new length $newLen, old length $origLen");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the temp file to the actual file
|
||||||
|
fseek($tmpfile, 0);
|
||||||
|
$out = $file->fopen('wb');
|
||||||
|
if (!$out) {
|
||||||
|
throw new \Exception('Could not open file for writing');
|
||||||
|
}
|
||||||
|
$wroteBytes = 0;
|
||||||
|
try {
|
||||||
|
$wroteBytes = stream_copy_to_stream($tmpfile, $out);
|
||||||
|
} finally {
|
||||||
|
fclose($out);
|
||||||
|
}
|
||||||
|
if ($wroteBytes !== $newLen) {
|
||||||
|
error_log("Exiftool error: wrote $r bytes, expected $newLen");
|
||||||
|
throw new \Exception("Could not write to file");
|
||||||
|
}
|
||||||
|
|
||||||
|
// All done at this point
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
// Close the temp file
|
||||||
|
fclose($tmpfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
<template>
|
||||||
|
<NcModal
|
||||||
|
v-if="photos.length > 0"
|
||||||
|
size="small"
|
||||||
|
@close="close"
|
||||||
|
:outTransition="true"
|
||||||
|
:hasNext="false"
|
||||||
|
:hasPrevious="false">
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="head">
|
||||||
|
<span>{{ t('memories', 'Edit Date/Time') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="photos.length === 1 && longDateStr">
|
||||||
|
{{ longDateStr }}
|
||||||
|
|
||||||
|
<div class="fields">
|
||||||
|
<NcTextField :value.sync="year"
|
||||||
|
class="field"
|
||||||
|
:label="t('memories', 'Year')" :label-visible="true"
|
||||||
|
:placeholder="t('memories', 'Year')" />
|
||||||
|
<NcTextField :value.sync="month"
|
||||||
|
class="field"
|
||||||
|
:label="t('memories', 'Month')" :label-visible="true"
|
||||||
|
:placeholder="t('memories', 'Month')" />
|
||||||
|
<NcTextField :value.sync="day"
|
||||||
|
class="field"
|
||||||
|
:label="t('memories', 'Day')" :label-visible="true"
|
||||||
|
:placeholder="t('memories', 'Day')" />
|
||||||
|
<NcTextField :value.sync="hour"
|
||||||
|
class="field"
|
||||||
|
:label="t('memories', 'Time')" :label-visible="true"
|
||||||
|
:placeholder="t('memories', 'Hour')" />
|
||||||
|
<NcTextField :value.sync="minute"
|
||||||
|
class="field"
|
||||||
|
:label="t('memories', 'Minute')"
|
||||||
|
:placeholder="t('memories', 'Minute')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<NcButton @click="save" class="button" type="primary">
|
||||||
|
{{ t('memories', 'Save') }}
|
||||||
|
</NcButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NcModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Mixins } from 'vue-property-decorator';
|
||||||
|
import GlobalMixin from '../mixins/GlobalMixin';
|
||||||
|
import { IPhoto } from '../types';
|
||||||
|
|
||||||
|
import { NcButton, NcModal, NcTextField } from '@nextcloud/vue';
|
||||||
|
import { showError } from '@nextcloud/dialogs'
|
||||||
|
import { generateUrl } from '@nextcloud/router'
|
||||||
|
import axios from '@nextcloud/axios'
|
||||||
|
import * as utils from '../services/Utils';
|
||||||
|
|
||||||
|
const INFO_API_URL = '/apps/memories/api/info/{id}';
|
||||||
|
const EDIT_API_URL = '/apps/memories/api/edit/{id}';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
NcButton,
|
||||||
|
NcModal,
|
||||||
|
NcTextField,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export default class EditDate extends Mixins(GlobalMixin) {
|
||||||
|
private photos: IPhoto[] = [];
|
||||||
|
|
||||||
|
private longDateStr: string = '';
|
||||||
|
private year: string = "0";
|
||||||
|
private month: string = "0";
|
||||||
|
private day: string = "0";
|
||||||
|
private hour: string = "0";
|
||||||
|
private minute: string = "0";
|
||||||
|
private second: string = "0";
|
||||||
|
|
||||||
|
public async open(photos: IPhoto[]) {
|
||||||
|
this.photos = photos;
|
||||||
|
if (photos.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await axios.get<any>(generateUrl(INFO_API_URL, { id: this.photos[0].fileid }));
|
||||||
|
if (typeof res.data.datetaken !== "string") {
|
||||||
|
console.error("Invalid date");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const utcEpoch = Date.parse(res.data.datetaken + " UTC");
|
||||||
|
const date = new Date(utcEpoch);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// Make PATCH request to update date
|
||||||
|
try {
|
||||||
|
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}`,
|
||||||
|
});
|
||||||
|
this.close();
|
||||||
|
} catch (e) {
|
||||||
|
if (e.response?.data?.message) {
|
||||||
|
showError(e.response.data.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.container {
|
||||||
|
margin: 20px;
|
||||||
|
|
||||||
|
.head {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fields {
|
||||||
|
margin-top: 5px;
|
||||||
|
.field {
|
||||||
|
width: 4.1em;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -111,8 +111,16 @@
|
||||||
{{ t('memories', 'Favorite') }}
|
{{ t('memories', 'Favorite') }}
|
||||||
<template #icon> <Star :size="20" /> </template>
|
<template #icon> <Star :size="20" /> </template>
|
||||||
</NcActionButton>
|
</NcActionButton>
|
||||||
|
<NcActionButton
|
||||||
|
:aria-label="t('memories', 'Edit Date/Time')"
|
||||||
|
@click="editDateSelection" close-after-click>
|
||||||
|
{{ t('memories', 'Edit Date/Time') }}
|
||||||
|
<template #icon> <EditIcon :size="20" /> </template>
|
||||||
|
</NcActionButton>
|
||||||
</NcActions>
|
</NcActions>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<EditDate ref="editDate" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -129,6 +137,7 @@ import * as utils from "../services/Utils";
|
||||||
import axios from '@nextcloud/axios'
|
import axios from '@nextcloud/axios'
|
||||||
import Folder from "./Folder.vue";
|
import Folder from "./Folder.vue";
|
||||||
import Photo from "./Photo.vue";
|
import Photo from "./Photo.vue";
|
||||||
|
import EditDate from "./EditDate.vue";
|
||||||
import FolderTopMatter from "./FolderTopMatter.vue";
|
import FolderTopMatter from "./FolderTopMatter.vue";
|
||||||
import UserConfig from "../mixins/UserConfig";
|
import UserConfig from "../mixins/UserConfig";
|
||||||
|
|
||||||
|
@ -137,6 +146,7 @@ import Download from 'vue-material-design-icons/Download.vue';
|
||||||
import Delete from 'vue-material-design-icons/Delete.vue';
|
import Delete from 'vue-material-design-icons/Delete.vue';
|
||||||
import Close from 'vue-material-design-icons/Close.vue';
|
import Close from 'vue-material-design-icons/Close.vue';
|
||||||
import CheckCircle from 'vue-material-design-icons/CheckCircle.vue';
|
import CheckCircle from 'vue-material-design-icons/CheckCircle.vue';
|
||||||
|
import EditIcon from 'vue-material-design-icons/ClockEdit.vue';
|
||||||
|
|
||||||
const SCROLL_LOAD_DELAY = 100; // Delay in loading data when scrolling
|
const SCROLL_LOAD_DELAY = 100; // Delay in loading data when scrolling
|
||||||
const MAX_PHOTO_WIDTH = 175; // Max width of a photo
|
const MAX_PHOTO_WIDTH = 175; // Max width of a photo
|
||||||
|
@ -155,6 +165,7 @@ for (const [key, value] of Object.entries(API_ROUTES)) {
|
||||||
components: {
|
components: {
|
||||||
Folder,
|
Folder,
|
||||||
Photo,
|
Photo,
|
||||||
|
EditDate,
|
||||||
FolderTopMatter,
|
FolderTopMatter,
|
||||||
NcActions,
|
NcActions,
|
||||||
NcActionButton,
|
NcActionButton,
|
||||||
|
@ -165,6 +176,7 @@ for (const [key, value] of Object.entries(API_ROUTES)) {
|
||||||
Delete,
|
Delete,
|
||||||
Close,
|
Close,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
|
EditIcon,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
|
@ -1057,6 +1069,13 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the edit date dialog
|
||||||
|
*/
|
||||||
|
async editDateSelection() {
|
||||||
|
(<any>this.$refs.editDate).open(Array.from(this.selection.values()));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete elements from main view with some animation
|
* Delete elements from main view with some animation
|
||||||
* This function looks horribly slow, probably isn't that bad
|
* This function looks horribly slow, probably isn't that bad
|
||||||
|
|
|
@ -15,12 +15,14 @@ export function getShortDateStr(date: Date) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get long date string with optional year if same as current */
|
/** Get long date string with optional year if same as current */
|
||||||
export function getLongDateStr(date: Date, skipYear=false) {
|
export function getLongDateStr(date: Date, skipYear=false, time=false) {
|
||||||
return date.toLocaleDateString(getCanonicalLocale(), {
|
return date.toLocaleDateString(getCanonicalLocale(), {
|
||||||
weekday: 'long',
|
weekday: 'long',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
year: (skipYear && date.getUTCFullYear() === new Date().getUTCFullYear()) ? undefined : 'numeric',
|
year: (skipYear && date.getUTCFullYear() === new Date().getUTCFullYear()) ? undefined : 'numeric',
|
||||||
timeZone: 'UTC',
|
timeZone: 'UTC',
|
||||||
|
hour: time ? 'numeric' : undefined,
|
||||||
|
minute: time ? 'numeric' : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
Loading…
Reference in New Issue