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"/>
</dependencies>
<commands>
<command>OCA\Memories\Command\Index</command>
</commands>
<command>OCA\Memories\Command\Index</command>
</commands>
<navigations>
<navigation>
<name>Memories</name>

View File

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

View File

@ -22,7 +22,7 @@
*/
.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);

View File

@ -37,40 +37,40 @@ use OCP\Files\Events\Node\NodeDeletedEvent;
use OCP\Files\Events\Node\NodeTouchedEvent;
class Application extends App implements IBootstrap {
public const APPNAME = 'memories';
public const APPNAME = 'memories';
public const IMAGE_MIMES = [
'image/png',
'image/jpeg',
'image/heic',
'image/png',
'image/tiff',
// 'image/gif', // too rarely used for photos
// 'image/x-xbitmap', // too rarely used for photos
// 'image/bmp', // too rarely used for photos
// 'image/svg+xml', // too rarely used for photos
];
public const IMAGE_MIMES = [
'image/png',
'image/jpeg',
'image/heic',
'image/png',
'image/tiff',
// 'image/gif', // too rarely used for photos
// 'image/x-xbitmap', // too rarely used for photos
// 'image/bmp', // too rarely used for photos
// 'image/svg+xml', // too rarely used for photos
];
public const VIDEO_MIMES = [
'video/mpeg',
// 'video/ogg', // too rarely used for photos
// 'video/webm', // too rarely used for photos
'video/mp4',
// 'video/x-m4v', // too rarely used for photos
'video/quicktime',
'video/x-matroska',
];
public const VIDEO_MIMES = [
'video/mpeg',
// 'video/ogg', // too rarely used for photos
// 'video/webm', // too rarely used for photos
'video/mp4',
// 'video/x-m4v', // too rarely used for photos
'video/quicktime',
'video/x-matroska',
];
public function __construct() {
parent::__construct(self::APPNAME);
}
public function __construct() {
parent::__construct(self::APPNAME);
}
public function register(IRegistrationContext $context): void {
$context->registerEventListener(NodeWrittenEvent::class, PostWriteListener::class);
public function register(IRegistrationContext $context): void {
$context->registerEventListener(NodeWrittenEvent::class, PostWriteListener::class);
$context->registerEventListener(NodeTouchedEvent::class, PostWriteListener::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 {
/** @var ?GlobalStoragesService */
protected $globalService;
/** @var ?GlobalStoragesService */
protected $globalService;
/** @var int[][] */
protected array $sizes;
/** @var int[][] */
protected array $sizes;
protected IUserManager $userManager;
protected IRootFolder $rootFolder;
protected IPreview $previewGenerator;
protected IConfig $config;
protected OutputInterface $output;
protected IManager $encryptionManager;
protected IDBConnection $connection;
protected TimelineWrite $timelineWrite;
protected IUserManager $userManager;
protected IRootFolder $rootFolder;
protected IPreview $previewGenerator;
protected IConfig $config;
protected OutputInterface $output;
protected IManager $encryptionManager;
protected IDBConnection $connection;
protected TimelineWrite $timelineWrite;
// Stats
private int $nProcessed = 0;
private int $nSkipped = 0;
private int $nInvalid = 0;
// Stats
private int $nProcessed = 0;
private int $nSkipped = 0;
private int $nInvalid = 0;
public function __construct(IRootFolder $rootFolder,
IUserManager $userManager,
IPreview $previewGenerator,
IConfig $config,
IManager $encryptionManager,
IDBConnection $connection,
ContainerInterface $container) {
parent::__construct();
public function __construct(IRootFolder $rootFolder,
IUserManager $userManager,
IPreview $previewGenerator,
IConfig $config,
IManager $encryptionManager,
IDBConnection $connection,
ContainerInterface $container) {
parent::__construct();
$this->userManager = $userManager;
$this->rootFolder = $rootFolder;
$this->previewGenerator = $previewGenerator;
$this->config = $config;
$this->encryptionManager = $encryptionManager;
$this->connection = $connection;
$this->timelineWrite = new TimelineWrite($this->connection);
$this->userManager = $userManager;
$this->rootFolder = $rootFolder;
$this->previewGenerator = $previewGenerator;
$this->config = $config;
$this->encryptionManager = $encryptionManager;
$this->connection = $connection;
$this->timelineWrite = new TimelineWrite($this->connection);
try {
$this->globalService = $container->get(GlobalStoragesService::class);
} catch (ContainerExceptionInterface $e) {
$this->globalService = null;
}
}
try {
$this->globalService = $container->get(GlobalStoragesService::class);
} catch (ContainerExceptionInterface $e) {
$this->globalService = null;
}
}
/** Make sure exiftool is available */
private function testExif() {
$testfile = dirname(__FILE__). '/../../exiftest.jpg';
$stream = fopen($testfile, 'rb');
if (!$stream) {
return false;
}
/** Make sure exiftool is available */
private function testExif() {
$testfile = dirname(__FILE__). '/../../exiftest.jpg';
$stream = fopen($testfile, 'rb');
if (!$stream) {
return false;
}
$exif = \OCA\Memories\Exif::getExifFromStream($stream);
fclose($stream);
$exif = \OCA\Memories\Exif::getExifFromStream($stream);
fclose($stream);
if (!$exif || $exif["DateTimeOriginal"] !== "2004:08:31 19:52:58") {
return false;
}
return true;
}
if (!$exif || $exif["DateTimeOriginal"] !== "2004:08:31 19:52:58") {
return false;
}
return true;
}
protected function configure(): void {
$this
->setName('memories:index')
->setDescription('Generate entries');
}
protected function configure(): void {
$this
->setName('memories:index')
->setDescription('Generate entries');
}
protected function execute(InputInterface $input, OutputInterface $output): int {
// Refuse to run without exiftool
\OCA\Memories\Exif::ensureStaticExiftoolProc();
if (!$this->testExif()) {
error_log('FATAL: exiftool could not be found or test failed');
exit(1);
}
protected function execute(InputInterface $input, OutputInterface $output): int {
// Refuse to run without exiftool
\OCA\Memories\Exif::ensureStaticExiftoolProc();
if (!$this->testExif()) {
error_log('FATAL: exiftool could not be found or test failed');
exit(1);
}
// Time measurement
$startTime = microtime(true);
// Time measurement
$startTime = microtime(true);
if ($this->encryptionManager->isEnabled()) {
$output->writeln('Encryption is enabled. Aborted.');
return 1;
}
$this->output = $output;
if ($this->encryptionManager->isEnabled()) {
$output->writeln('Encryption is enabled. Aborted.');
return 1;
}
$this->output = $output;
$this->userManager->callForSeenUsers(function (IUser $user) {
$this->generateUserEntries($user);
});
// Close the exiftool process
\OCA\Memories\Exif::closeStaticExiftoolProc();
// Close the exiftool process
\OCA\Memories\Exif::closeStaticExiftoolProc();
// Show some stats
$endTime = microtime(true);
$execTime = intval(($endTime - $startTime)*1000)/1000 ;
$nTotal = $this->nInvalid + $this->nSkipped + $this->nProcessed;
$this->output->writeln("==========================================");
$this->output->writeln("Checked $nTotal files in $execTime sec");
$this->output->writeln($this->nInvalid . " not valid media items");
$this->output->writeln($this->nSkipped . " skipped because unmodified");
$this->output->writeln($this->nProcessed . " (re-)processed");
$this->output->writeln("==========================================");
// Show some stats
$endTime = microtime(true);
$execTime = intval(($endTime - $startTime)*1000)/1000 ;
$nTotal = $this->nInvalid + $this->nSkipped + $this->nProcessed;
$this->output->writeln("==========================================");
$this->output->writeln("Checked $nTotal files in $execTime sec");
$this->output->writeln($this->nInvalid . " not valid media items");
$this->output->writeln($this->nSkipped . " skipped because unmodified");
$this->output->writeln($this->nProcessed . " (re-)processed");
$this->output->writeln("==========================================");
return 0;
}
return 0;
}
private function generateUserEntries(IUser &$user): void {
\OC_Util::tearDownFS();
\OC_Util::setupFS($user->getUID());
private function generateUserEntries(IUser &$user): void {
\OC_Util::tearDownFS();
\OC_Util::setupFS($user->getUID());
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
$this->parseFolder($userFolder);
}
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
$this->parseFolder($userFolder);
}
private function parseFolder(Folder &$folder): void {
try {
$folderPath = $folder->getPath();
$this->output->writeln('Scanning folder ' . $folderPath);
private function parseFolder(Folder &$folder): void {
try {
$folderPath = $folder->getPath();
$this->output->writeln('Scanning folder ' . $folderPath);
$nodes = $folder->getDirectoryListing();
$nodes = $folder->getDirectoryListing();
foreach ($nodes as &$node) {
if ($node instanceof Folder) {
$this->parseFolder($node);
} elseif ($node instanceof File) {
$this->parseFile($node);
}
}
} catch (StorageNotAvailableException $e) {
$this->output->writeln(sprintf('<error>Storage for folder folder %s is not available: %s</error>',
$folder->getPath(),
$e->getHint()
));
}
}
foreach ($nodes as &$node) {
if ($node instanceof Folder) {
$this->parseFolder($node);
} elseif ($node instanceof File) {
$this->parseFile($node);
}
}
} catch (StorageNotAvailableException $e) {
$this->output->writeln(sprintf('<error>Storage for folder folder %s is not available: %s</error>',
$folder->getPath(),
$e->getHint()
));
}
}
private function parseFile(File &$file): void {
$res = $this->timelineWrite->processFile($file);
if ($res === 2) {
$this->nProcessed++;
} else if ($res === 1) {
$this->nSkipped++;
} else {
$this->nInvalid++;
}
}
private function parseFile(File &$file): void {
$res = $this->timelineWrite->processFile($file);
if ($res === 2) {
$this->nProcessed++;
} else if ($res === 1) {
$this->nSkipped++;
} else {
$this->nInvalid++;
}
}
}

View File

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

View File

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

View File

@ -8,11 +8,11 @@ use OCP\IConfig;
use OCP\IDBConnection;
class TimelineQuery {
protected IDBConnection $connection;
protected IDBConnection $connection;
public function __construct(IDBConnection $connection) {
$this->connection = $connection;
}
public function __construct(IDBConnection $connection) {
$this->connection = $connection;
}
/**
* Process the days response
@ -104,7 +104,7 @@ class TimelineQuery {
ORDER BY `*PREFIX*memories`.`datetaken` DESC';
$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,
])->fetchAll();
return $this->processDay($rows);
@ -126,7 +126,7 @@ class TimelineQuery {
AND (`*PREFIX*filecache`.`parent`=? OR `*PREFIX*filecache`.`fileid`=?)
WHERE `*PREFIX*memories`.`dayid`=?
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,
])->fetchAll();
return $this->processDay($rows);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,10 +29,10 @@ import router from './router'
// Adding translations to the whole app
Vue.mixin({
methods: {
t,
n,
},
methods: {
t,
n,
},
})
Vue.use(VueVirtualScroller)
@ -42,15 +42,15 @@ Vue.use(VueVirtualScroller)
// original scripts are loaded from
// https://github.com/nextcloud/server/blob/5bf3d1bb384da56adbf205752be8f840aac3b0c5/lib/private/legacy/template.php#L120-L122
window.addEventListener('DOMContentLoaded', () => {
if (!window.OCA.Files) {
window.OCA.Files = {}
}
// 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)
if (!window.OCA.Files) {
window.OCA.Files = {}
}
// 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)
})
export default new Vue({
el: '#content',
router,
render: h => h(App),
el: '#content',
router,
render: h => h(App),
})

View File

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

View File

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