Tab -> Space

pull/37/head
Varun Patil 2022-09-09 00:31:42 -07:00
parent f0c0e03f2c
commit af38c24198
18 changed files with 580 additions and 580 deletions

View File

@ -35,8 +35,8 @@
<nextcloud min-version="22" max-version="24"/> <nextcloud min-version="22" max-version="24"/>
</dependencies> </dependencies>
<commands> <commands>
<command>OCA\Memories\Command\Index</command> <command>OCA\Memories\Command\Index</command>
</commands> </commands>
<navigations> <navigations>
<navigation> <navigation>
<name>Memories</name> <name>Memories</name>

View File

@ -4,13 +4,13 @@ return [
// Days and folder API // Days and folder API
['name' => 'page#main', 'url' => '/', 'verb' => 'GET'], ['name' => 'page#main', 'url' => '/', 'verb' => 'GET'],
['name' => 'page#folder', 'url' => '/folders/{path}', 'verb' => 'GET', ['name' => 'page#folder', 'url' => '/folders/{path}', 'verb' => 'GET',
'requirements' => [ 'requirements' => [
'path' => '.*', 'path' => '.*',
], ],
'defaults' => [ 'defaults' => [
'path' => '', 'path' => '',
] ]
], ],
// API // API
['name' => 'api#days', 'url' => '/api/days', 'verb' => 'GET'], ['name' => 'api#days', 'url' => '/api/days', 'verb' => 'GET'],

View File

@ -22,7 +22,7 @@
*/ */
.icon-folder.icon-dark { .icon-folder.icon-dark {
@include icon-color('folder', 'filetypes', $color-black, 1, true); @include icon-color('folder', 'filetypes', $color-black, 1, true);
} }
@include icon-black-white('yourmemories', 'memories', 1); @include icon-black-white('yourmemories', 'memories', 1);

View File

@ -37,40 +37,40 @@ use OCP\Files\Events\Node\NodeDeletedEvent;
use OCP\Files\Events\Node\NodeTouchedEvent; use OCP\Files\Events\Node\NodeTouchedEvent;
class Application extends App implements IBootstrap { class Application extends App implements IBootstrap {
public const APPNAME = 'memories'; public const APPNAME = 'memories';
public const IMAGE_MIMES = [ public const IMAGE_MIMES = [
'image/png', 'image/png',
'image/jpeg', 'image/jpeg',
'image/heic', 'image/heic',
'image/png', 'image/png',
'image/tiff', 'image/tiff',
// 'image/gif', // too rarely used for photos // 'image/gif', // too rarely used for photos
// 'image/x-xbitmap', // too rarely used for photos // 'image/x-xbitmap', // too rarely used for photos
// 'image/bmp', // too rarely used for photos // 'image/bmp', // too rarely used for photos
// 'image/svg+xml', // too rarely used for photos // 'image/svg+xml', // too rarely used for photos
]; ];
public const VIDEO_MIMES = [ public const VIDEO_MIMES = [
'video/mpeg', 'video/mpeg',
// 'video/ogg', // too rarely used for photos // 'video/ogg', // too rarely used for photos
// 'video/webm', // too rarely used for photos // 'video/webm', // too rarely used for photos
'video/mp4', 'video/mp4',
// 'video/x-m4v', // too rarely used for photos // 'video/x-m4v', // too rarely used for photos
'video/quicktime', 'video/quicktime',
'video/x-matroska', 'video/x-matroska',
]; ];
public function __construct() { public function __construct() {
parent::__construct(self::APPNAME); parent::__construct(self::APPNAME);
} }
public function register(IRegistrationContext $context): void { public function register(IRegistrationContext $context): void {
$context->registerEventListener(NodeWrittenEvent::class, PostWriteListener::class); $context->registerEventListener(NodeWrittenEvent::class, PostWriteListener::class);
$context->registerEventListener(NodeTouchedEvent::class, PostWriteListener::class); $context->registerEventListener(NodeTouchedEvent::class, PostWriteListener::class);
$context->registerEventListener(NodeDeletedEvent::class, PostDeleteListener::class); $context->registerEventListener(NodeDeletedEvent::class, PostDeleteListener::class);
} }
public function boot(IBootContext $context): void { public function boot(IBootContext $context): void {
} }
} }

View File

@ -46,149 +46,149 @@ use Symfony\Component\Console\Output\OutputInterface;
class Index extends Command { class Index extends Command {
/** @var ?GlobalStoragesService */ /** @var ?GlobalStoragesService */
protected $globalService; protected $globalService;
/** @var int[][] */ /** @var int[][] */
protected array $sizes; protected array $sizes;
protected IUserManager $userManager; protected IUserManager $userManager;
protected IRootFolder $rootFolder; protected IRootFolder $rootFolder;
protected IPreview $previewGenerator; protected IPreview $previewGenerator;
protected IConfig $config; protected IConfig $config;
protected OutputInterface $output; protected OutputInterface $output;
protected IManager $encryptionManager; protected IManager $encryptionManager;
protected IDBConnection $connection; protected IDBConnection $connection;
protected TimelineWrite $timelineWrite; protected TimelineWrite $timelineWrite;
// Stats // Stats
private int $nProcessed = 0; private int $nProcessed = 0;
private int $nSkipped = 0; private int $nSkipped = 0;
private int $nInvalid = 0; private int $nInvalid = 0;
public function __construct(IRootFolder $rootFolder, public function __construct(IRootFolder $rootFolder,
IUserManager $userManager, IUserManager $userManager,
IPreview $previewGenerator, IPreview $previewGenerator,
IConfig $config, IConfig $config,
IManager $encryptionManager, IManager $encryptionManager,
IDBConnection $connection, IDBConnection $connection,
ContainerInterface $container) { ContainerInterface $container) {
parent::__construct(); parent::__construct();
$this->userManager = $userManager; $this->userManager = $userManager;
$this->rootFolder = $rootFolder; $this->rootFolder = $rootFolder;
$this->previewGenerator = $previewGenerator; $this->previewGenerator = $previewGenerator;
$this->config = $config; $this->config = $config;
$this->encryptionManager = $encryptionManager; $this->encryptionManager = $encryptionManager;
$this->connection = $connection; $this->connection = $connection;
$this->timelineWrite = new TimelineWrite($this->connection); $this->timelineWrite = new TimelineWrite($this->connection);
try { try {
$this->globalService = $container->get(GlobalStoragesService::class); $this->globalService = $container->get(GlobalStoragesService::class);
} catch (ContainerExceptionInterface $e) { } catch (ContainerExceptionInterface $e) {
$this->globalService = null; $this->globalService = null;
} }
} }
/** Make sure exiftool is available */ /** Make sure exiftool is available */
private function testExif() { private function testExif() {
$testfile = dirname(__FILE__). '/../../exiftest.jpg'; $testfile = dirname(__FILE__). '/../../exiftest.jpg';
$stream = fopen($testfile, 'rb'); $stream = fopen($testfile, 'rb');
if (!$stream) { if (!$stream) {
return false; return false;
} }
$exif = \OCA\Memories\Exif::getExifFromStream($stream); $exif = \OCA\Memories\Exif::getExifFromStream($stream);
fclose($stream); fclose($stream);
if (!$exif || $exif["DateTimeOriginal"] !== "2004:08:31 19:52:58") { if (!$exif || $exif["DateTimeOriginal"] !== "2004:08:31 19:52:58") {
return false; return false;
} }
return true; return true;
} }
protected function configure(): void { protected function configure(): void {
$this $this
->setName('memories:index') ->setName('memories:index')
->setDescription('Generate entries'); ->setDescription('Generate entries');
} }
protected function execute(InputInterface $input, OutputInterface $output): int { protected function execute(InputInterface $input, OutputInterface $output): int {
// Refuse to run without exiftool // Refuse to run without exiftool
\OCA\Memories\Exif::ensureStaticExiftoolProc(); \OCA\Memories\Exif::ensureStaticExiftoolProc();
if (!$this->testExif()) { if (!$this->testExif()) {
error_log('FATAL: exiftool could not be found or test failed'); error_log('FATAL: exiftool could not be found or test failed');
exit(1); exit(1);
} }
// Time measurement // Time measurement
$startTime = microtime(true); $startTime = microtime(true);
if ($this->encryptionManager->isEnabled()) { if ($this->encryptionManager->isEnabled()) {
$output->writeln('Encryption is enabled. Aborted.'); $output->writeln('Encryption is enabled. Aborted.');
return 1; return 1;
} }
$this->output = $output; $this->output = $output;
$this->userManager->callForSeenUsers(function (IUser $user) { $this->userManager->callForSeenUsers(function (IUser $user) {
$this->generateUserEntries($user); $this->generateUserEntries($user);
}); });
// Close the exiftool process // Close the exiftool process
\OCA\Memories\Exif::closeStaticExiftoolProc(); \OCA\Memories\Exif::closeStaticExiftoolProc();
// Show some stats // Show some stats
$endTime = microtime(true); $endTime = microtime(true);
$execTime = intval(($endTime - $startTime)*1000)/1000 ; $execTime = intval(($endTime - $startTime)*1000)/1000 ;
$nTotal = $this->nInvalid + $this->nSkipped + $this->nProcessed; $nTotal = $this->nInvalid + $this->nSkipped + $this->nProcessed;
$this->output->writeln("=========================================="); $this->output->writeln("==========================================");
$this->output->writeln("Checked $nTotal files in $execTime sec"); $this->output->writeln("Checked $nTotal files in $execTime sec");
$this->output->writeln($this->nInvalid . " not valid media items"); $this->output->writeln($this->nInvalid . " not valid media items");
$this->output->writeln($this->nSkipped . " skipped because unmodified"); $this->output->writeln($this->nSkipped . " skipped because unmodified");
$this->output->writeln($this->nProcessed . " (re-)processed"); $this->output->writeln($this->nProcessed . " (re-)processed");
$this->output->writeln("=========================================="); $this->output->writeln("==========================================");
return 0; return 0;
} }
private function generateUserEntries(IUser &$user): void { private function generateUserEntries(IUser &$user): void {
\OC_Util::tearDownFS(); \OC_Util::tearDownFS();
\OC_Util::setupFS($user->getUID()); \OC_Util::setupFS($user->getUID());
$userFolder = $this->rootFolder->getUserFolder($user->getUID()); $userFolder = $this->rootFolder->getUserFolder($user->getUID());
$this->parseFolder($userFolder); $this->parseFolder($userFolder);
} }
private function parseFolder(Folder &$folder): void { private function parseFolder(Folder &$folder): void {
try { try {
$folderPath = $folder->getPath(); $folderPath = $folder->getPath();
$this->output->writeln('Scanning folder ' . $folderPath); $this->output->writeln('Scanning folder ' . $folderPath);
$nodes = $folder->getDirectoryListing(); $nodes = $folder->getDirectoryListing();
foreach ($nodes as &$node) { foreach ($nodes as &$node) {
if ($node instanceof Folder) { if ($node instanceof Folder) {
$this->parseFolder($node); $this->parseFolder($node);
} elseif ($node instanceof File) { } elseif ($node instanceof File) {
$this->parseFile($node); $this->parseFile($node);
} }
} }
} catch (StorageNotAvailableException $e) { } catch (StorageNotAvailableException $e) {
$this->output->writeln(sprintf('<error>Storage for folder folder %s is not available: %s</error>', $this->output->writeln(sprintf('<error>Storage for folder folder %s is not available: %s</error>',
$folder->getPath(), $folder->getPath(),
$e->getHint() $e->getHint()
)); ));
} }
} }
private function parseFile(File &$file): void { private function parseFile(File &$file): void {
$res = $this->timelineWrite->processFile($file); $res = $this->timelineWrite->processFile($file);
if ($res === 2) { if ($res === 2) {
$this->nProcessed++; $this->nProcessed++;
} else if ($res === 1) { } else if ($res === 1) {
$this->nSkipped++; $this->nSkipped++;
} else { } else {
$this->nInvalid++; $this->nInvalid++;
} }
} }
} }

View File

@ -41,172 +41,172 @@ use OCP\Files\FileInfo;
use OCP\Files\Search\ISearchComparison; use OCP\Files\Search\ISearchComparison;
class ApiController extends Controller { class ApiController extends Controller {
private IConfig $config; private IConfig $config;
private IUserSession $userSession; private IUserSession $userSession;
private IDBConnection $connection; private IDBConnection $connection;
private IRootFolder $rootFolder; private IRootFolder $rootFolder;
private TimelineQuery $timelineQuery; private TimelineQuery $timelineQuery;
public function __construct( public function __construct(
IRequest $request, IRequest $request,
IConfig $config, IConfig $config,
IUserSession $userSession, IUserSession $userSession,
IDBConnection $connection, IDBConnection $connection,
IRootFolder $rootFolder) { IRootFolder $rootFolder) {
parent::__construct(Application::APPNAME, $request); parent::__construct(Application::APPNAME, $request);
$this->config = $config; $this->config = $config;
$this->userSession = $userSession; $this->userSession = $userSession;
$this->connection = $connection; $this->connection = $connection;
$this->timelineQuery = new TimelineQuery($this->connection); $this->timelineQuery = new TimelineQuery($this->connection);
$this->rootFolder = $rootFolder; $this->rootFolder = $rootFolder;
} }
/** /**
* @NoAdminRequired * @NoAdminRequired
* *
* @return JSONResponse * @return JSONResponse
*/ */
public function days(): JSONResponse { public function days(): JSONResponse {
$user = $this->userSession->getUser(); $user = $this->userSession->getUser();
if (is_null($user)) { if (is_null($user)) {
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED); return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
} }
$list = $this->timelineQuery->getDays($this->config, $user->getUID()); $list = $this->timelineQuery->getDays($this->config, $user->getUID());
return new JSONResponse($list, Http::STATUS_OK); return new JSONResponse($list, Http::STATUS_OK);
} }
/** /**
* @NoAdminRequired * @NoAdminRequired
* *
* @return JSONResponse * @return JSONResponse
*/ */
public function day(string $id): JSONResponse { public function day(string $id): JSONResponse {
$user = $this->userSession->getUser(); $user = $this->userSession->getUser();
if (is_null($user) || !is_numeric($id)) { if (is_null($user) || !is_numeric($id)) {
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED); return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
} }
$list = $this->timelineQuery->getDay($this->config, $user->getUID(), intval($id)); $list = $this->timelineQuery->getDay($this->config, $user->getUID(), intval($id));
return new JSONResponse($list, Http::STATUS_OK); return new JSONResponse($list, Http::STATUS_OK);
} }
/** /**
* Check if folder is allowed and get it if yes * Check if folder is allowed and get it if yes
*/ */
private function getAllowedFolder(int $folder, $user) { private function getAllowedFolder(int $folder, $user) {
// Get root if folder not specified // Get root if folder not specified
$root = $this->rootFolder->getUserFolder($user->getUID()); $root = $this->rootFolder->getUserFolder($user->getUID());
if ($folder === 0) { if ($folder === 0) {
$folder = $root->getId(); $folder = $root->getId();
} }
// Check access to folder // Check access to folder
$nodes = $root->getById($folder); $nodes = $root->getById($folder);
if (empty($nodes)) { if (empty($nodes)) {
return NULL; return NULL;
} }
// Check it is a folder // Check it is a folder
$node = $nodes[0]; $node = $nodes[0];
if (!$node instanceof \OCP\Files\Folder) { if (!$node instanceof \OCP\Files\Folder) {
return NULL; return NULL;
} }
return $node; return $node;
} }
/** /**
* @NoAdminRequired * @NoAdminRequired
* *
* @return JSONResponse * @return JSONResponse
*/ */
public function folder(string $folder): JSONResponse { public function folder(string $folder): JSONResponse {
$user = $this->userSession->getUser(); $user = $this->userSession->getUser();
if (is_null($user) || !is_numeric($folder)) { if (is_null($user) || !is_numeric($folder)) {
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED); return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
} }
// Check permissions // Check permissions
$node = $this->getAllowedFolder(intval($folder), $user); $node = $this->getAllowedFolder(intval($folder), $user);
if (is_null($node)) { if (is_null($node)) {
return new JSONResponse([], Http::STATUS_FORBIDDEN); return new JSONResponse([], Http::STATUS_FORBIDDEN);
} }
// Get response from db // Get response from db
$list = $this->timelineQuery->getDaysFolder($node->getId()); $list = $this->timelineQuery->getDaysFolder($node->getId());
// Get subdirectories // Get subdirectories
$sub = $node->search(new SearchQuery( $sub = $node->search(new SearchQuery(
new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', FileInfo::MIMETYPE_FOLDER), new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', FileInfo::MIMETYPE_FOLDER),
0, 0, [], $user)); 0, 0, [], $user));
$sub = array_filter($sub, function ($item) use ($node) { $sub = array_filter($sub, function ($item) use ($node) {
return $item->getParent()->getId() === $node->getId(); return $item->getParent()->getId() === $node->getId();
}); });
// Sort by name // Sort by name
usort($sub, function($a, $b) { usort($sub, function($a, $b) {
return strnatcmp($a->getName(), $b->getName()); return strnatcmp($a->getName(), $b->getName());
}); });
// Map sub to JSON array // Map sub to JSON array
$subdirArray = [ $subdirArray = [
"dayid" => -0.1, "dayid" => -0.1,
"detail" => array_map(function ($node) { "detail" => array_map(function ($node) {
return [ return [
"fileid" => $node->getId(), "fileid" => $node->getId(),
"name" => $node->getName(), "name" => $node->getName(),
"is_folder" => 1, "is_folder" => 1,
"path" => $node->getPath(), "path" => $node->getPath(),
]; ];
}, $sub, []), }, $sub, []),
]; ];
$subdirArray["count"] = count($subdirArray["detail"]); $subdirArray["count"] = count($subdirArray["detail"]);
array_unshift($list, $subdirArray); array_unshift($list, $subdirArray);
return new JSONResponse($list, Http::STATUS_OK); return new JSONResponse($list, Http::STATUS_OK);
} }
/** /**
* @NoAdminRequired * @NoAdminRequired
* *
* @return JSONResponse * @return JSONResponse
*/ */
public function folderDay(string $folder, string $dayId): JSONResponse { public function folderDay(string $folder, string $dayId): JSONResponse {
$user = $this->userSession->getUser(); $user = $this->userSession->getUser();
if (is_null($user) || !is_numeric($folder) || !is_numeric($dayId)) { if (is_null($user) || !is_numeric($folder) || !is_numeric($dayId)) {
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED); return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
} }
$node = $this->getAllowedFolder(intval($folder), $user); $node = $this->getAllowedFolder(intval($folder), $user);
if ($node === NULL) { if ($node === NULL) {
return new JSONResponse([], Http::STATUS_FORBIDDEN); return new JSONResponse([], Http::STATUS_FORBIDDEN);
} }
$list = $this->timelineQuery->getDayFolder($node->getId(), intval($dayId)); $list = $this->timelineQuery->getDayFolder($node->getId(), intval($dayId));
return new JSONResponse($list, Http::STATUS_OK); return new JSONResponse($list, Http::STATUS_OK);
} }
/** /**
* @NoAdminRequired * @NoAdminRequired
* *
* update preferences (user setting) * update preferences (user setting)
* *
* @param string key the identifier to change * @param string key the identifier to change
* @param string value the value to set * @param string value the value to set
* *
* @return JSONResponse an empty JSONResponse with respective http status code * @return JSONResponse an empty JSONResponse with respective http status code
*/ */
public function setUserConfig(string $key, string $value): JSONResponse { public function setUserConfig(string $key, string $value): JSONResponse {
$user = $this->userSession->getUser(); $user = $this->userSession->getUser();
if (is_null($user)) { if (is_null($user)) {
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED); return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
} }
$userId = $user->getUid(); $userId = $user->getUid();
$this->config->setUserValue($userId, Application::APPNAME, $key, $value); $this->config->setUserValue($userId, Application::APPNAME, $key, $value);
return new JSONResponse([], Http::STATUS_OK); return new JSONResponse([], Http::STATUS_OK);
} }
} }

View File

@ -13,60 +13,60 @@ use OCP\IUserSession;
use OCP\Util; use OCP\Util;
class PageController extends Controller { class PageController extends Controller {
protected string $userId; protected string $userId;
protected $appName; protected $appName;
protected IEventDispatcher $eventDispatcher; protected IEventDispatcher $eventDispatcher;
private IInitialState $initialState; private IInitialState $initialState;
private IUserSession $userSession; private IUserSession $userSession;
private IConfig $config; private IConfig $config;
public function __construct( public function __construct(
string $AppName, string $AppName,
IRequest $request, IRequest $request,
string $UserId, string $UserId,
IEventDispatcher $eventDispatcher, IEventDispatcher $eventDispatcher,
IInitialState $initialState, IInitialState $initialState,
IUserSession $userSession, IUserSession $userSession,
IConfig $config) { IConfig $config) {
parent::__construct($AppName, $request); parent::__construct($AppName, $request);
$this->userId = $UserId; $this->userId = $UserId;
$this->appName = $AppName; $this->appName = $AppName;
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
$this->initialState = $initialState; $this->initialState = $initialState;
$this->userSession = $userSession; $this->userSession = $userSession;
$this->config = $config; $this->config = $config;
} }
/** /**
* @NoAdminRequired * @NoAdminRequired
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function main() { public function main() {
$user = $this->userSession->getUser(); $user = $this->userSession->getUser();
if (is_null($user)) { if (is_null($user)) {
return null; return null;
} }
Util::addScript($this->appName, 'memories-main'); Util::addScript($this->appName, 'memories-main');
Util::addStyle($this->appName, 'custom-icons'); Util::addStyle($this->appName, 'custom-icons');
$this->eventDispatcher->dispatchTyped(new LoadSidebar()); $this->eventDispatcher->dispatchTyped(new LoadSidebar());
$this->eventDispatcher->dispatchTyped(new LoadViewer()); $this->eventDispatcher->dispatchTyped(new LoadViewer());
$timelinePath = \OCA\Memories\Util::getPhotosPath($this->config, $user->getUid()); $timelinePath = \OCA\Memories\Util::getPhotosPath($this->config, $user->getUid());
$this->initialState->provideInitialState('timelinePath', $timelinePath); $this->initialState->provideInitialState('timelinePath', $timelinePath);
$response = new TemplateResponse($this->appName, 'main'); $response = new TemplateResponse($this->appName, 'main');
return $response; return $response;
} }
/** /**
* @NoAdminRequired * @NoAdminRequired
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function folder() { public function folder() {
return $this->main(); return $this->main();
} }
} }

View File

@ -8,11 +8,11 @@ use OCP\IConfig;
use OCP\IDBConnection; use OCP\IDBConnection;
class TimelineQuery { class TimelineQuery {
protected IDBConnection $connection; protected IDBConnection $connection;
public function __construct(IDBConnection $connection) { public function __construct(IDBConnection $connection) {
$this->connection = $connection; $this->connection = $connection;
} }
/** /**
* Process the days response * Process the days response
@ -104,7 +104,7 @@ class TimelineQuery {
ORDER BY `*PREFIX*memories`.`datetaken` DESC'; ORDER BY `*PREFIX*memories`.`datetaken` DESC';
$path = "files" . Exif::getPhotosPath($config, $user) . "%"; $path = "files" . Exif::getPhotosPath($config, $user) . "%";
$rows = $this->connection->executeQuery($sql, [$path, $user, $dayId], [ $rows = $this->connection->executeQuery($sql, [$path, $user, $dayId], [
\PDO::PARAM_STR, \PDO::PARAM_STR, \PDO::PARAM_INT, \PDO::PARAM_STR, \PDO::PARAM_STR, \PDO::PARAM_INT,
])->fetchAll(); ])->fetchAll();
return $this->processDay($rows); return $this->processDay($rows);
@ -126,7 +126,7 @@ class TimelineQuery {
AND (`*PREFIX*filecache`.`parent`=? OR `*PREFIX*filecache`.`fileid`=?) AND (`*PREFIX*filecache`.`parent`=? OR `*PREFIX*filecache`.`fileid`=?)
WHERE `*PREFIX*memories`.`dayid`=? WHERE `*PREFIX*memories`.`dayid`=?
ORDER BY `*PREFIX*memories`.`datetaken` DESC'; ORDER BY `*PREFIX*memories`.`datetaken` DESC';
$rows = $this->connection->executeQuery($sql, [$folderId, $folderId, $dayId], [ $rows = $this->connection->executeQuery($sql, [$folderId, $folderId, $dayId], [
\PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT,
])->fetchAll(); ])->fetchAll();
return $this->processDay($rows); return $this->processDay($rows);

View File

@ -9,11 +9,11 @@ use OCP\Files\File;
use OCP\IDBConnection; use OCP\IDBConnection;
class TimelineWrite { class TimelineWrite {
protected IDBConnection $connection; protected IDBConnection $connection;
public function __construct(IDBConnection $connection) { public function __construct(IDBConnection $connection) {
$this->connection = $connection; $this->connection = $connection;
} }
/** /**
* Process a file to insert Exif data into the database * Process a file to insert Exif data into the database

View File

@ -34,20 +34,20 @@ use OCP\IDBConnection;
class PostDeleteListener implements IEventListener { class PostDeleteListener implements IEventListener {
private TimelineWrite $util; private TimelineWrite $util;
public function __construct(IDBConnection $connection) { public function __construct(IDBConnection $connection) {
$this->util = new TimelineWrite($connection); $this->util = new TimelineWrite($connection);
} }
public function handle(Event $event): void { public function handle(Event $event): void {
if (!($event instanceof NodeDeletedEvent)) { if (!($event instanceof NodeDeletedEvent)) {
return; return;
} }
$node = $event->getNode(); $node = $event->getNode();
if ($node instanceof Folder) { if ($node instanceof Folder) {
return; return;
} }
$this->util->deleteFile($node); $this->util->deleteFile($node);
} }
} }

View File

@ -36,23 +36,23 @@ use OCP\IUserManager;
class PostWriteListener implements IEventListener { class PostWriteListener implements IEventListener {
private TimelineWrite $util; private TimelineWrite $util;
public function __construct(IDBConnection $connection, public function __construct(IDBConnection $connection,
IUserManager $userManager) { IUserManager $userManager) {
$this->userManager = $userManager; $this->userManager = $userManager;
$this->util = new TimelineWrite($connection); $this->util = new TimelineWrite($connection);
} }
public function handle(Event $event): void { public function handle(Event $event): void {
if (!($event instanceof NodeWrittenEvent) && if (!($event instanceof NodeWrittenEvent) &&
!($event instanceof NodeTouchedEvent)) { !($event instanceof NodeTouchedEvent)) {
return; return;
} }
$node = $event->getNode(); $node = $event->getNode();
if ($node instanceof Folder) { if ($node instanceof Folder) {
return; return;
} }
$this->util->processFile($node); $this->util->processFile($node);
} }
} }

View File

@ -58,19 +58,19 @@ class Version000000Date20220812163631 extends SimpleMigrationStep {
'notnull' => false, 'notnull' => false,
]); ]);
$table->addColumn('fileid', Types::BIGINT, [ $table->addColumn('fileid', Types::BIGINT, [
'notnull' => true, 'notnull' => true,
'length' => 20, 'length' => 20,
]); ]);
$table->addColumn('dayid', Types::INTEGER, [ $table->addColumn('dayid', Types::INTEGER, [
'notnull' => true, 'notnull' => true,
]); ]);
$table->addColumn('isvideo', Types::BOOLEAN, [ $table->addColumn('isvideo', Types::BOOLEAN, [
'notnull' => false, 'notnull' => false,
'default' => false 'default' => false
]); ]);
$table->addColumn('mtime', Types::INTEGER, [ $table->addColumn('mtime', Types::INTEGER, [
'notnull' => true, 'notnull' => true,
]); ]);
$table->setPrimaryKey(['id']); $table->setPrimaryKey(['id']);
$table->addIndex(['uid'], 'memories_uid_index'); $table->addIndex(['uid'], 'memories_uid_index');

View File

@ -1,43 +1,43 @@
<template> <template>
<NcContent app-name="memories"> <NcContent app-name="memories">
<NcAppNavigation> <NcAppNavigation>
<template id="app-memories-navigation" #list> <template id="app-memories-navigation" #list>
<NcAppNavigationItem :to="{name: 'timeline'}" <NcAppNavigationItem :to="{name: 'timeline'}"
:title="t('timeline', 'Timeline')" :title="t('timeline', 'Timeline')"
icon="icon-yourmemories" icon="icon-yourmemories"
exact> exact>
</NcAppNavigationItem> </NcAppNavigationItem>
<NcAppNavigationItem :to="{name: 'folders'}" <NcAppNavigationItem :to="{name: 'folders'}"
:title="t('folders', 'Folders')" :title="t('folders', 'Folders')"
icon="icon-files-dark"> icon="icon-files-dark">
</NcAppNavigationItem> </NcAppNavigationItem>
</template> </template>
<template #footer> <template #footer>
<NcAppNavigationSettings :title="t('memories', 'Settings')"> <NcAppNavigationSettings :title="t('memories', 'Settings')">
<Settings /> <Settings />
</NcAppNavigationSettings> </NcAppNavigationSettings>
</template> </template>
</NcAppNavigation> </NcAppNavigation>
<NcAppContent> <NcAppContent>
<div class="outer"> <div class="outer">
<router-view /> <router-view />
</div> </div>
</NcAppContent> </NcAppContent>
</NcContent> </NcContent>
</template> </template>
<style scoped> <style scoped>
.outer { .outer {
padding: 0 0 0 44px; padding: 0 0 0 44px;
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.outer { .outer {
padding-left: 5px; padding-left: 5px;
} }
} }
</style> </style>
@ -48,35 +48,35 @@ import Timeline from './components/Timeline.vue'
import Settings from './components/Settings.vue' import Settings from './components/Settings.vue'
export default { export default {
name: 'App', name: 'App',
components: { components: {
NcContent, NcContent,
NcAppContent, NcAppContent,
NcAppNavigation, NcAppNavigation,
NcAppNavigationItem, NcAppNavigationItem,
NcAppNavigationSettings, NcAppNavigationSettings,
Timeline, Timeline,
Settings, Settings,
}, },
data() { data() {
return { return {
loading: false, loading: false,
show: true, show: true,
starred: false, starred: false,
} }
}, },
methods: { methods: {
close() { close() {
this.show = false this.show = false
console.debug(arguments) console.debug(arguments)
}, },
newButtonAction() { newButtonAction() {
console.debug(arguments) console.debug(arguments)
}, },
log() { log() {
console.debug(arguments) console.debug(arguments)
}, },
}, },
} }
</script> </script>

View File

@ -21,7 +21,7 @@
--> -->
<template> <template>
<div> <div>
<label for="timeline-path">{{ t('memories', 'Timeline Path') }}</label> <label for="timeline-path">{{ t('memories', 'Timeline Path') }}</label>
<input id="timeline-path" <input id="timeline-path"
v-model="timelinePath" v-model="timelinePath"
@ -30,7 +30,7 @@
<button @click="updateAll()"> <button @click="updateAll()">
{{ t('memories', 'Update') }} {{ t('memories', 'Update') }}
</button> </button>
</div> </div>
</template> </template>
<style scoped> <style scoped>
@ -43,10 +43,10 @@ input[type=text] {
import UserConfig from '../mixins/UserConfig' import UserConfig from '../mixins/UserConfig'
export default { export default {
name: 'Settings', name: 'Settings',
mixins: [ mixins: [
UserConfig, UserConfig,
], ],
methods: { methods: {
async updateAll() { async updateAll() {

View File

@ -176,16 +176,16 @@ export default {
}, },
watch: { watch: {
$route(from, to) { $route(from, to) {
console.log('route changed', from, to) console.log('route changed', from, to)
this.resetState(); this.resetState();
this.fetchDays(); this.fetchDays();
}, },
}, },
beforeDestroy() { beforeDestroy() {
this.resetState(); this.resetState();
}, },
methods: { methods: {
/** Reset all state */ /** Reset all state */

View File

@ -29,10 +29,10 @@ import router from './router'
// Adding translations to the whole app // Adding translations to the whole app
Vue.mixin({ Vue.mixin({
methods: { methods: {
t, t,
n, n,
}, },
}) })
Vue.use(VueVirtualScroller) Vue.use(VueVirtualScroller)
@ -42,15 +42,15 @@ Vue.use(VueVirtualScroller)
// original scripts are loaded from // original scripts are loaded from
// https://github.com/nextcloud/server/blob/5bf3d1bb384da56adbf205752be8f840aac3b0c5/lib/private/legacy/template.php#L120-L122 // https://github.com/nextcloud/server/blob/5bf3d1bb384da56adbf205752be8f840aac3b0c5/lib/private/legacy/template.php#L120-L122
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
if (!window.OCA.Files) { if (!window.OCA.Files) {
window.OCA.Files = {} window.OCA.Files = {}
} }
// register unused client for the sidebar to have access to its parser methods // register unused client for the sidebar to have access to its parser methods
Object.assign(window.OCA.Files, { App: { fileList: { filesClient: OC.Files.getClient() } } }, window.OCA.Files) Object.assign(window.OCA.Files, { App: { fileList: { filesClient: OC.Files.getClient() } } }, window.OCA.Files)
}) })
export default new Vue({ export default new Vue({
el: '#content', el: '#content',
router, router,
render: h => h(App), render: h => h(App),
}) })

View File

@ -4,21 +4,21 @@ import { genFileInfo } from './FileUtils'
import client from './DavClient'; import client from './DavClient';
const props = ` const props = `
<oc:fileid /> <oc:fileid />
<oc:permissions /> <oc:permissions />
<d:getlastmodified /> <d:getlastmodified />
<d:getetag /> <d:getetag />
<d:getcontenttype /> <d:getcontenttype />
<d:getcontentlength /> <d:getcontentlength />
<nc:has-preview /> <nc:has-preview />
<oc:favorite /> <oc:favorite />
<d:resourcetype />`; <d:resourcetype />`;
const IMAGE_MIME_TYPES = [ const IMAGE_MIME_TYPES = [
'image/jpeg', 'image/jpeg',
'image/png', 'image/png',
'image/tiff', 'image/tiff',
'image/heic', 'image/heic',
]; ];
export async function getFiles(fileIds) { export async function getFiles(fileIds) {
@ -34,102 +34,102 @@ export async function getFiles(fileIds) {
`).join(''); `).join('');
const options = { const options = {
method: 'SEARCH', method: 'SEARCH',
headers: { headers: {
'content-Type': 'text/xml', 'content-Type': 'text/xml',
}, },
data: `<?xml version="1.0" encoding="UTF-8"?> data: `<?xml version="1.0" encoding="UTF-8"?>
<d:searchrequest xmlns:d="DAV:" <d:searchrequest xmlns:d="DAV:"
xmlns:oc="http://owncloud.org/ns" xmlns:oc="http://owncloud.org/ns"
xmlns:nc="http://nextcloud.org/ns" xmlns:nc="http://nextcloud.org/ns"
xmlns:ns="https://github.com/icewind1991/SearchDAV/ns" xmlns:ns="https://github.com/icewind1991/SearchDAV/ns"
xmlns:ocs="http://open-collaboration-services.org/ns"> xmlns:ocs="http://open-collaboration-services.org/ns">
<d:basicsearch> <d:basicsearch>
<d:select> <d:select>
<d:prop> <d:prop>
${props} ${props}
</d:prop> </d:prop>
</d:select> </d:select>
<d:from> <d:from>
<d:scope> <d:scope>
<d:href>${prefixPath}</d:href> <d:href>${prefixPath}</d:href>
<d:depth>0</d:depth> <d:depth>0</d:depth>
</d:scope> </d:scope>
</d:from> </d:from>
<d:where> <d:where>
<d:or> <d:or>
${filter} ${filter}
</d:or> </d:or>
</d:where> </d:where>
</d:basicsearch> </d:basicsearch>
</d:searchrequest>`, </d:searchrequest>`,
deep: true, deep: true,
details: true, details: true,
responseType: 'text', responseType: 'text',
}; };
let response = await client.getDirectoryContents('', options); let response = await client.getDirectoryContents('', options);
return response.data return response.data
.map(data => genFileInfo(data)) .map(data => genFileInfo(data))
.map(data => Object.assign({}, data, { filename: data.filename.replace(prefixPath, '') })); .map(data => Object.assign({}, data, { filename: data.filename.replace(prefixPath, '') }));
} }
export async function getFolderPreviewFileIds(folderPath, limit) { export async function getFolderPreviewFileIds(folderPath, limit) {
const prefixPath = `/files/${getCurrentUser().uid}`; const prefixPath = `/files/${getCurrentUser().uid}`;
const filter = IMAGE_MIME_TYPES.map(mime => ` const filter = IMAGE_MIME_TYPES.map(mime => `
<d:like> <d:like>
<d:prop> <d:prop>
<d:getcontenttype/> <d:getcontenttype/>
</d:prop> </d:prop>
<d:literal>${mime}</d:literal> <d:literal>${mime}</d:literal>
</d:like> </d:like>
`).join(''); `).join('');
const options = { const options = {
method: 'SEARCH', method: 'SEARCH',
headers: { headers: {
'content-Type': 'text/xml', 'content-Type': 'text/xml',
}, },
data: `<?xml version="1.0" encoding="UTF-8"?> data: `<?xml version="1.0" encoding="UTF-8"?>
<d:searchrequest xmlns:d="DAV:" <d:searchrequest xmlns:d="DAV:"
xmlns:oc="http://owncloud.org/ns" xmlns:oc="http://owncloud.org/ns"
xmlns:nc="http://nextcloud.org/ns" xmlns:nc="http://nextcloud.org/ns"
xmlns:ns="https://github.com/icewind1991/SearchDAV/ns" xmlns:ns="https://github.com/icewind1991/SearchDAV/ns"
xmlns:ocs="http://open-collaboration-services.org/ns"> xmlns:ocs="http://open-collaboration-services.org/ns">
<d:basicsearch> <d:basicsearch>
<d:select> <d:select>
<d:prop> <d:prop>
${props} ${props}
</d:prop> </d:prop>
</d:select> </d:select>
<d:from> <d:from>
<d:scope> <d:scope>
<d:href>${prefixPath}/${folderPath}</d:href> <d:href>${prefixPath}/${folderPath}</d:href>
<d:depth>0</d:depth> <d:depth>0</d:depth>
</d:scope> </d:scope>
</d:from> </d:from>
<d:where> <d:where>
<d:or> <d:or>
${filter} ${filter}
</d:or> </d:or>
</d:where> </d:where>
<d:limit> <d:limit>
<d:nresults>${limit}</d:nresults> <d:nresults>${limit}</d:nresults>
</d:limit> </d:limit>
</d:basicsearch> </d:basicsearch>
</d:searchrequest>`, </d:searchrequest>`,
deep: true, deep: true,
details: true, details: true,
responseType: 'text', responseType: 'text',
}; };
let response = await client.getDirectoryContents('', options); let response = await client.getDirectoryContents('', options);
return response.data return response.data
.map(data => genFileInfo(data)) .map(data => genFileInfo(data))
.map(data => Object.assign({}, data, { .map(data => Object.assign({}, data, {
filename: data.filename.replace(prefixPath, '') filename: data.filename.replace(prefixPath, '')
})); }));
} }
export async function deleteFile(path) { export async function deleteFile(path) {
@ -143,30 +143,30 @@ export async function deleteFile(path) {
* @param {string[]} fileNames - The file's names * @param {string[]} fileNames - The file's names
*/ */
export async function downloadFiles(fileNames) { export async function downloadFiles(fileNames) {
const randomToken = Math.random().toString(36).substring(2) const randomToken = Math.random().toString(36).substring(2)
const params = new URLSearchParams() const params = new URLSearchParams()
params.append('files', JSON.stringify(fileNames)) params.append('files', JSON.stringify(fileNames))
params.append('downloadStartSecret', randomToken) params.append('downloadStartSecret', randomToken)
const downloadURL = generateUrl(`/apps/files/ajax/download.php?${params}`) const downloadURL = generateUrl(`/apps/files/ajax/download.php?${params}`)
window.location = `${downloadURL}downloadStartSecret=${randomToken}` window.location = `${downloadURL}downloadStartSecret=${randomToken}`
return new Promise((resolve) => { return new Promise((resolve) => {
const waitForCookieInterval = setInterval( const waitForCookieInterval = setInterval(
() => { () => {
const cookieIsSet = document.cookie const cookieIsSet = document.cookie
.split(';') .split(';')
.map(cookie => cookie.split('=')) .map(cookie => cookie.split('='))
.findIndex(([cookieName, cookieValue]) => cookieName === 'ocDownloadStarted' && cookieValue === randomToken) .findIndex(([cookieName, cookieValue]) => cookieName === 'ocDownloadStarted' && cookieValue === randomToken)
if (cookieIsSet) { if (cookieIsSet) {
clearInterval(waitForCookieInterval) clearInterval(waitForCookieInterval)
resolve(true) resolve(true)
} }
}, },
50 50
) )
}) })
} }

View File

@ -21,10 +21,10 @@
*/ */
const isNumber = function(num) { const isNumber = function(num) {
if (!num) { if (!num) {
return false return false
} }
return Number(num).toString() === num.toString() return Number(num).toString() === num.toString()
} }
export { isNumber } export { isNumber }