From 373eef2bda411469ebbd75bd5a2672225b755d6f Mon Sep 17 00:00:00 2001 From: Louis Chemineau Date: Wed, 7 May 2025 17:48:11 +0200 Subject: [PATCH 01/25] chore: Tidy logic sharing between AlbumRoots Signed-off-by: Louis Chemineau --- lib/Album/AlbumInfo.php | 5 + lib/Album/AlbumMapper.php | 76 +++-------- lib/Album/AlbumWithFiles.php | 2 +- lib/Sabre/Album/AlbumRoot.php | 182 ++------------------------- lib/Sabre/Album/AlbumRootBase.php | 180 ++++++++++++++++++++++++++ lib/Sabre/Album/AlbumsHome.php | 9 +- lib/Sabre/Album/PublicAlbumRoot.php | 32 +---- lib/Sabre/Album/SharedAlbumRoot.php | 42 +++---- lib/Sabre/Album/SharedAlbumsHome.php | 7 +- lib/Sabre/CollectionPhoto.php | 5 +- lib/Sabre/PhotosHome.php | 4 +- lib/Sabre/Place/PlaceRoot.php | 13 -- lib/Sabre/PropFindPlugin.php | 14 ++- lib/Sabre/PublicRootCollection.php | 3 + 14 files changed, 259 insertions(+), 315 deletions(-) create mode 100644 lib/Sabre/Album/AlbumRootBase.php diff --git a/lib/Album/AlbumInfo.php b/lib/Album/AlbumInfo.php index 437a374ab..fc4295b2c 100644 --- a/lib/Album/AlbumInfo.php +++ b/lib/Album/AlbumInfo.php @@ -17,6 +17,7 @@ public function __construct( private readonly int $created, private readonly int $lastAdded, private readonly ?int $receivedFrom = null, + private readonly ?array $filters = null, ) { } @@ -47,4 +48,8 @@ public function getLastAddedPhoto(): int { public function getReceivedFrom(): ?int { return $this->receivedFrom; } + + public function getFilters(): ?array { + return $this->filters; + } } diff --git a/lib/Album/AlbumMapper.php b/lib/Album/AlbumMapper.php index be8fdaaf9..59bc62788 100644 --- a/lib/Album/AlbumMapper.php +++ b/lib/Album/AlbumMapper.php @@ -20,13 +20,6 @@ use OCP\Security\ISecureRandom; class AlbumMapper { - private readonly IDBConnection $connection; - private readonly IMimeTypeLoader $mimeTypeLoader; - private readonly ITimeFactory $timeFactory; - private readonly IUserManager $userManager; - private readonly IGroupManager $groupManager; - protected IL10N $l; - protected ISecureRandom $random; // Same mapping as IShare. public const TYPE_USER = 0; @@ -34,21 +27,14 @@ class AlbumMapper { public const TYPE_LINK = 3; public function __construct( - IDBConnection $connection, - IMimeTypeLoader $mimeTypeLoader, - ITimeFactory $timeFactory, - IUserManager $userManager, - IGroupManager $groupManager, - IL10N $l, - ISecureRandom $random, + private readonly IDBConnection $connection, + private readonly IMimeTypeLoader $mimeTypeLoader, + private readonly ITimeFactory $timeFactory, + private readonly IUserManager $userManager, + private readonly IGroupManager $groupManager, + private readonly IL10N $l, + private readonly ISecureRandom $random, ) { - $this->connection = $connection; - $this->mimeTypeLoader = $mimeTypeLoader; - $this->timeFactory = $timeFactory; - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->l = $l; - $this->random = $random; } public function create(string $userId, string $name, string $location = ''): AlbumInfo { @@ -82,7 +68,6 @@ public function get(int $id): ?AlbumInfo { } /** - * @param string $userId * @return AlbumInfo[] */ public function getForUser(string $userId): array { @@ -95,8 +80,6 @@ public function getForUser(string $userId): array { } /** - * @param string $albumName - * @param string $userName * @return AlbumInfo */ public function getByName(string $albumName, string $userName): ?AlbumInfo { @@ -114,7 +97,6 @@ public function getByName(string $albumName, string $userName): ?AlbumInfo { } /** - * @param int $fileId * @return AlbumInfo[] */ public function getForFile(int $fileId): array { @@ -128,8 +110,6 @@ public function getForFile(int $fileId): array { } /** - * @param string $userId - * @param int $fileId * @return AlbumInfo[] */ public function getForUserAndFile(string $userId, int $fileId): array { @@ -181,13 +161,12 @@ public function delete(int $id): void { } /** - * @param int $albumId - * @param string $userId * @return AlbumFile[] */ public function getForAlbumIdAndUserWithFiles(int $albumId, string $userId): array { + // TODO: Add search from filters $query = $this->connection->getQueryBuilder(); - $query->select('fileid', 'mimetype', 'a.album_id', 'size', 'mtime', 'etag', 'added', 'owner') + $query->select('fileid', 'mimetype', 'a.album_id', 'size', 'mtime', 'etag', 'added', 'owner', 'filters') ->selectAlias('f.name', 'file_name') ->selectAlias('a.name', 'album_name') ->from('photos_albums', 'a') @@ -208,11 +187,6 @@ public function getForAlbumIdAndUserWithFiles(int $albumId, string $userId): arr return $files; } - /** - * @param int $albumId - * @param int $fileId - * @return AlbumFile - */ public function getForAlbumIdAndFileId(int $albumId, int $fileId): AlbumFile { $query = $this->connection->getQueryBuilder(); $query->select('fileid', 'mimetype', 'a.album_id', 'user', 'size', 'mtime', 'etag', 'location', 'created', 'last_added_photo', 'added', 'owner') @@ -331,7 +305,6 @@ private function getLastAdded(int $albumId): int { } /** - * @param int $albumId * @return array */ public function getCollaborators(int $albumId): array { @@ -368,11 +341,6 @@ public function getCollaborators(int $albumId): array { } - /** - * @param int $albumId - * @param string $userId - * @return bool - */ public function isCollaborator(int $albumId, string $userId): bool { $query = $this->connection->getQueryBuilder(); $query->select('collaborator_id', 'collaborator_type') @@ -402,7 +370,6 @@ public function isCollaborator(int $albumId, string $userId): bool { } /** - * @param int $albumId * @param array{'id': string, 'type': int} $collaborators */ public function setCollaborators(int $albumId, array $collaborators): void { @@ -462,8 +429,6 @@ public function setCollaborators(int $albumId, array $collaborators): void { } /** - * @param string $collaboratorId - * @param int $collaboratorType * @return AlbumInfo[] */ public function getSharedAlbumsForCollaborator(string $collaboratorId, int $collaboratorType): array { @@ -488,8 +453,6 @@ public function getSharedAlbumsForCollaborator(string $collaboratorId, int $coll } /** - * @param string $collaboratorId - * @param string $collaboratorsType - The type of the collaborator, either a user or a group. * @return AlbumWithFiles[] */ public function getSharedAlbumsForCollaboratorWithFiles(string $collaboratorId, int $collaboratorType): array { @@ -535,11 +498,6 @@ public function getSharedAlbumsForCollaboratorWithFiles(string $collaboratorId, return $result; } - /** - * @param string $userId - * @param int $albumId - * @return void - */ public function deleteUserFromAlbumCollaboratorsList(string $userId, int $albumId): void { $query = $this->connection->getQueryBuilder(); $query->delete('photos_albums_collabs') @@ -552,11 +510,6 @@ public function deleteUserFromAlbumCollaboratorsList(string $userId, int $albumI $this->removeFilesForUser($albumId, $userId); } - /** - * @param string $groupId - * @param int $albumId - * @return void - */ public function deleteGroupFromAlbumCollaboratorsList(string $groupId, int $albumId): void { $query = $this->connection->getQueryBuilder(); $query->delete('photos_albums_collabs') @@ -567,9 +520,6 @@ public function deleteGroupFromAlbumCollaboratorsList(string $groupId, int $albu } /** - * @param string $collaboratorId - * @param int $collaboratorType - * @param int $fileId * @return AlbumInfo[] */ public function getAlbumsForCollaboratorIdAndFileId(string $collaboratorId, int $collaboratorType, int $fileId): array { @@ -596,4 +546,12 @@ public function getAlbumsForCollaboratorIdAndFileId(string $collaboratorId, int (int)$row['last_added_photo'] ), $rows); } + + public function setAlbumFilters(int $albumId, array $filters): void { + $query = $this->connection->getQueryBuilder(); + $query->update('photos_albums') + ->set('filters', $query->createNamedParameter(json_encode($filters))) + ->where($query->expr()->eq('album_id', $query->createNamedParameter($albumId, IQueryBuilder::PARAM_STR))); + $query->executeStatement(); + } } diff --git a/lib/Album/AlbumWithFiles.php b/lib/Album/AlbumWithFiles.php index f4c5ab0a8..c07fef282 100644 --- a/lib/Album/AlbumWithFiles.php +++ b/lib/Album/AlbumWithFiles.php @@ -13,7 +13,7 @@ public function __construct( private readonly AlbumInfo $info, private readonly AlbumMapper $albumMapper, /** @var AlbumFile[] */ - private array $files = [], + private readonly array $files = [], ) { } diff --git a/lib/Sabre/Album/AlbumRoot.php b/lib/Sabre/Album/AlbumRoot.php index 32dff4dba..9ecc066be 100644 --- a/lib/Sabre/Album/AlbumRoot.php +++ b/lib/Sabre/Album/AlbumRoot.php @@ -8,150 +8,31 @@ namespace OCA\Photos\Sabre\Album; -use OCA\DAV\Connector\Sabre\File; use OCA\Photos\Album\AlbumFile; -use OCA\Photos\Album\AlbumMapper; -use OCA\Photos\Album\AlbumWithFiles; -use OCA\Photos\Service\UserConfigService; -use OCP\Files\Folder; -use OCP\Files\InvalidDirectoryException; -use OCP\Files\IRootFolder; -use OCP\Files\NotFoundException; -use Psr\Log\LoggerInterface; use Sabre\DAV\Exception\Conflict; -use Sabre\DAV\Exception\Forbidden; -use Sabre\DAV\Exception\NotFound; use Sabre\DAV\ICollection; use Sabre\DAV\ICopyTarget; use Sabre\DAV\INode; -class AlbumRoot implements ICollection, ICopyTarget { - public function __construct( - protected AlbumMapper $albumMapper, - protected AlbumWithFiles $album, - protected IRootFolder $rootFolder, - protected string $userId, - protected UserConfigService $userConfigService, - protected LoggerInterface $logger, - ) { - } - - /** - * @return void - */ - public function delete() { +class AlbumRoot extends AlbumRootBase implements ICollection, ICopyTarget { + public function delete(): void { $this->albumMapper->delete($this->album->getAlbum()->getId()); } - public function getName(): string { - return basename($this->album->getAlbum()->getTitle()); - } - - /** - * @return void - */ - public function setName($name) { + public function setName($name): void { $this->albumMapper->rename($this->album->getAlbum()->getId(), $name); } - protected function getPhotosLocationInfo() { - $photosLocation = $this->userConfigService->getUserConfig('photosLocation'); - $userFolder = $this->rootFolder->getUserFolder($this->userId); - return [$photosLocation, $userFolder]; - } - - /** - * We cannot create files in an Album - * We add the file to the default Photos folder and then link it there. - * - * @param string $name - * @param null|resource|string $data - * @return string|null - */ public function createFile($name, $data = null) { - try { - [$photosLocation, $userFolder] = $this->getPhotosLocationInfo(); - - try { - $photosFolder = $userFolder->get($photosLocation); - } catch (NotFoundException $e) { - // If the folder does not exists, create it - $photosFolder = $userFolder->newFolder($photosLocation); - } - - // If the node is not a folder, we throw - if (!($photosFolder instanceof Folder)) { - throw new Conflict('The destination exists and is not a folder'); - } - - if ($photosFolder->isShared()) { - throw new InvalidDirectoryException('The destination is a received share'); - } - - // Check for conflict and rename the file accordingly - $newName = $photosFolder->getNonExistingName($name); - - $node = $photosFolder->newFile($newName, $data); - $this->addFile($node->getId(), $node->getOwner()->getUID()); - // Cheating with header because we are using fileID-fileName - // https://github.com/nextcloud/server/blob/af29b978078ffd9169a9bd9146feccbb7974c900/apps/dav/lib/Connector/Sabre/FilesPlugin.php#L564-L585 - \header('OC-FileId: ' . $node->getId()); - return '"' . $node->getEtag() . '"'; - } catch (\Exception $e) { - $this->logger->error('Could not create file', ['exception' => $e]); - throw new Forbidden('Could not create file'); - } - } - - /** - * @return never - */ - public function createDirectory($name): never { - throw new Forbidden('Not allowed to create directories in this folder'); + return parent::createFileInCurrentUserFolder($name, $data); } - /** - * @return AlbumPhoto[] - */ - public function getChildren(): array { - return array_map(fn (AlbumFile $file): AlbumPhoto => new AlbumPhoto($this->albumMapper, $this->album->getAlbum(), $file, $this->rootFolder, $this->rootFolder->getUserFolder($this->userId)), $this->album->getFiles()); - } - - public function getChild($name): AlbumPhoto { - foreach ($this->album->getFiles() as $file) { - if ($file->getFileId() . '-' . $file->getName() === $name) { - return new AlbumPhoto($this->albumMapper, $this->album->getAlbum(), $file, $this->rootFolder, $this->rootFolder->getUserFolder($this->userId)); - } - } - throw new NotFound("$name not found"); - } - - public function childExists($name): bool { - try { - $this->getChild($name); - return true; - } catch (NotFound) { - return false; - } - } - - public function getLastModified(): int { - return 0; + public function getAlbumPhoto(AlbumFile $file): AlbumPhoto { + return new AlbumPhoto($this->albumMapper, $this->album->getAlbum(), $file, $this->rootFolder, $this->rootFolder->getUserFolder($this->userId)); } public function copyInto($targetName, $sourcePath, INode $sourceNode): bool { - if (!$sourceNode instanceof File) { - throw new Forbidden('The source is not a file'); - } - - $sourceId = $sourceNode->getId(); - $ownerUID = $sourceNode->getFileInfo()->getOwner()->getUID(); - $uid = $this->userId; - if ($ownerUID !== $uid) { - throw new Forbidden("Can't add file to album, only files from $uid can be added"); - } - - return $this->addFile($sourceId, $ownerUID); + return parent::copyIntoAlbum($targetName, $sourcePath, $sourceNode); } protected function addFile(int $sourceId, string $ownerUID): bool { @@ -167,49 +48,6 @@ protected function addFile(int $sourceId, string $ownerUID): bool { return false; } - public function getAlbum(): AlbumWithFiles { - return $this->album; - } - - public function getDateRange(): array { - $earliestDate = null; - $latestDate = null; - - foreach ($this->getChildren() as $child) { - try { - $childCreationDate = $child->getFileInfo()->getMtime(); - } catch (NotFoundException) { - continue; - } - - if ($childCreationDate < $earliestDate || $earliestDate === null) { - $earliestDate = $childCreationDate; - } - - if ($childCreationDate > $earliestDate || $latestDate === null) { - $latestDate = $childCreationDate; - } - } - - return ['start' => $earliestDate, 'end' => $latestDate]; - } - - /** - * @return int|null - */ - public function getCover() { - $children = $this->getChildren(); - - if (count($children) > 0) { - return $children[0]->getFileId(); - } else { - return null; - } - } - - /** - * @return array{array{'nc:collaborator': array{'id': string, 'label': string, 'type': int}}} - */ public function getCollaborators(): array { return array_map( fn (array $collaborator): array => [ 'nc:collaborator' => $collaborator ], @@ -221,8 +59,12 @@ public function getCollaborators(): array { * @param array{'id': string, 'type': int} $collaborators * @return array{array{'nc:collaborator': array{'id': string, 'label': string, 'type': int}}} */ - public function setCollaborators($collaborators): array { + public function setCollaborators(array $collaborators): array { $this->albumMapper->setCollaborators($this->getAlbum()->getAlbum()->getId(), $collaborators); return $this->getCollaborators(); } + + public function setLocation(string $location): void { + $this->albumMapper->setLocation($this->getAlbum()->getAlbum()->getId(), $location); + } } diff --git a/lib/Sabre/Album/AlbumRootBase.php b/lib/Sabre/Album/AlbumRootBase.php new file mode 100644 index 000000000..fb0c3381d --- /dev/null +++ b/lib/Sabre/Album/AlbumRootBase.php @@ -0,0 +1,180 @@ +album->getAlbum()->getTitle()); + } + + abstract public function setName($name): void; + + abstract public function createFile($name, $data = null); + + private function getPhotosLocationInfo() { + $photosLocation = $this->userConfigService->getUserConfig('photosLocation'); + $userFolder = $this->rootFolder->getUserFolder($this->userId); + return [$photosLocation, $userFolder]; + } + + /** + * We cannot create files in an Album + * We add the file to the default Photos folder and then link it there. + * @param string $data + */ + public function createFileInCurrentUserFolder(string $name, ?string $data = null): string { + try { + [$photosLocation, $userFolder] = $this->getPhotosLocationInfo(); + + try { + $photosFolder = $userFolder->get($photosLocation); + } catch (NotFoundException $e) { + // If the folder does not exists, create it + $photosFolder = $userFolder->newFolder($photosLocation); + } + + // If the node is not a folder, we throw + if (!($photosFolder instanceof Folder)) { + throw new Conflict('The destination exists and is not a folder'); + } + + if ($photosFolder->isShared()) { + throw new InvalidDirectoryException('The destination is a received share'); + } + + // Check for conflict and rename the file accordingly + $newName = $photosFolder->getNonExistingName($name); + + $node = $photosFolder->newFile($newName, $data); + $this->addFile($node->getId(), $node->getOwner()->getUID()); + // Cheating with header because we are using fileID-fileName + // https://github.com/nextcloud/server/blob/af29b978078ffd9169a9bd9146feccbb7974c900/apps/dav/lib/Connector/Sabre/FilesPlugin.php#L564-L585 + \header('OC-FileId: ' . $node->getId()); + return '"' . $node->getEtag() . '"'; + } catch (\Exception $e) { + $this->logger->error('Could not create file', ['exception' => $e]); + throw new Forbidden('Could not create file'); + } + } + + public function copyIntoAlbum(string $targetName, string $sourcePath, INode $sourceNode): bool { + if (!$sourceNode instanceof File) { + throw new Forbidden('The source is not a file'); + } + + $sourceId = $sourceNode->getId(); + $ownerUID = $sourceNode->getFileInfo()->getOwner()->getUID(); + $uid = $this->userId; + if ($ownerUID !== $uid) { + throw new Forbidden("Can't add file to album, only files from $uid can be added"); + } + + return $this->addFile($sourceId, $ownerUID); + } + + final public function createDirectory($name) { + throw new Forbidden('Not allowed to create directories in this folder'); + } + + abstract public function getAlbumPhoto(AlbumFile $file): AlbumPhoto; + + /** + * @return AlbumPhoto[] + */ + final public function getChildren(): array { + return array_map(fn (AlbumFile $file) => $this->getAlbumPhoto($file), $this->album->getFiles()); + } + + final public function getChild($name): PublicAlbumPhoto { + foreach ($this->album->getFiles() as $file) { + if ($file->getFileId() . '-' . $file->getName() === $name) { + return $this->getAlbumPhoto($file); + } + } + + throw new NotFound("$name not found"); + } + + final public function childExists($name): bool { + try { + $this->getChild($name); + return true; + } catch (NotFound $e) { + return false; + } + } + + final public function getLastModified(): int { + return 0; + } + + abstract protected function addFile(int $sourceId, string $ownerUID); + + final public function getAlbum(): AlbumWithFiles { + return $this->album; + } + + final public function getDateRange(): array { + $earliestDate = null; + $latestDate = null; + + foreach ($this->getChildren() as $child) { + try { + $childCreationDate = $child->getFileInfo()->getMtime(); + } catch (NotFoundException $e) { + continue; + } + + if ($childCreationDate < $earliestDate || $earliestDate === null) { + $earliestDate = $childCreationDate; + } + + if ($childCreationDate > $earliestDate || $latestDate === null) { + $latestDate = $childCreationDate; + } + } + + return ['start' => $earliestDate, 'end' => $latestDate]; + } + + /** + * @return array{array{'nc:collaborator': array{'id': string, 'label': string, 'type': int}}} + */ + abstract public function getCollaborators(): array; +} diff --git a/lib/Sabre/Album/AlbumsHome.php b/lib/Sabre/Album/AlbumsHome.php index 3fb6ccb39..639a1872c 100644 --- a/lib/Sabre/Album/AlbumsHome.php +++ b/lib/Sabre/Album/AlbumsHome.php @@ -13,16 +13,17 @@ use OCA\Photos\Album\AlbumWithFiles; use OCA\Photos\Service\UserConfigService; use OCP\Files\IRootFolder; +use OCP\IUserManager; use Psr\Log\LoggerInterface; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\ICollection; class AlbumsHome implements ICollection { - public const NAME = 'albums'; + public const string NAME = 'albums'; /** - * @var AlbumRoot[] + * @var AlbumRootBase[]|null */ protected ?array $children = null; @@ -33,6 +34,7 @@ public function __construct( protected IRootFolder $rootFolder, protected UserConfigService $userConfigService, protected LoggerInterface $logger, + protected IUserManager $userManager, ) { } @@ -76,7 +78,7 @@ public function getChild($name) { } /** - * @return AlbumRoot[] + * @return AlbumRootBase[] */ public function getChildren(): array { if ($this->children === null) { @@ -88,6 +90,7 @@ public function getChildren(): array { $this->userId, $this->userConfigService, $this->logger, + $this->userManager, ), $albumInfos); } diff --git a/lib/Sabre/Album/PublicAlbumRoot.php b/lib/Sabre/Album/PublicAlbumRoot.php index 156549bdf..e1f1059da 100644 --- a/lib/Sabre/Album/PublicAlbumRoot.php +++ b/lib/Sabre/Album/PublicAlbumRoot.php @@ -10,10 +10,9 @@ use OCA\Photos\Album\AlbumFile; use Sabre\DAV\Exception\Forbidden; -use Sabre\DAV\Exception\NotFound; use Sabre\DAV\INode; -class PublicAlbumRoot extends AlbumRoot { +class PublicAlbumRoot extends AlbumRootBase { public function delete(): never { throw new Forbidden('Not allowed to delete a public album'); } @@ -26,14 +25,7 @@ public function copyInto($targetName, $sourcePath, INode $sourceNode): bool { throw new Forbidden('Not allowed to copy into a public album'); } - protected function getPhotosLocationInfo() { - $albumOwner = $this->album->getAlbum()->getUserId(); - $photosLocation = $this->userConfigService->getConfigForUser($albumOwner, 'photosLocation'); - $userFolder = $this->rootFolder->getUserFolder($albumOwner); - return [$photosLocation, $userFolder]; - } - - public function createFile($name, $data = null): never { + public function createFile($name, $data = null) { throw new Forbidden('Not allowed to create a file in a public album'); } @@ -41,27 +33,11 @@ protected function addFile(int $sourceId, string $ownerUID): bool { throw new Forbidden('Not allowed to add a file to a public album'); } - // Do not reveal collaborators for public albums. public function getCollaborators(): array { - /** @var array{array{'nc:collaborator': array{'id': string, 'label': string, 'type': int}}} */ return []; } - public function setCollaborators($collaborators): array { - throw new Forbidden('Not allowed to collaborators a public album'); - } - - public function getChildren(): array { - return array_map(fn (AlbumFile $file): PublicAlbumPhoto => new PublicAlbumPhoto($this->albumMapper, $this->album->getAlbum(), $file, $this->rootFolder, $this->rootFolder->getUserFolder($this->userId)), $this->album->getFiles()); - } - - public function getChild($name): PublicAlbumPhoto { - foreach ($this->album->getFiles() as $file) { - if ($file->getFileId() . '-' . $file->getName() === $name) { - return new PublicAlbumPhoto($this->albumMapper, $this->album->getAlbum(), $file, $this->rootFolder, $this->rootFolder->getUserFolder($this->userId)); - } - } - - throw new NotFound("$name not found"); + public function getAlbumPhoto(AlbumFile $file): AlbumPhoto { + return new PublicAlbumPhoto($this->albumMapper, $this->album->getAlbum(), $file, $this->rootFolder, $this->rootFolder->getUserFolder($this->userId)); } } diff --git a/lib/Sabre/Album/SharedAlbumRoot.php b/lib/Sabre/Album/SharedAlbumRoot.php index 893b19ee7..5b512ff5e 100644 --- a/lib/Sabre/Album/SharedAlbumRoot.php +++ b/lib/Sabre/Album/SharedAlbumRoot.php @@ -8,36 +8,14 @@ namespace OCA\Photos\Sabre\Album; -use OCA\Photos\Album\AlbumMapper; -use OCA\Photos\Album\AlbumWithFiles; -use OCA\Photos\Service\UserConfigService; -use OCP\Files\IRootFolder; -use OCP\IUserManager; -use Psr\Log\LoggerInterface; +use OCA\Photos\Album\AlbumFile; use Sabre\DAV\Exception\Conflict; use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\INode; -class SharedAlbumRoot extends AlbumRoot { - public function __construct( - AlbumMapper $albumMapper, - AlbumWithFiles $album, - IRootFolder $rootFolder, - string $userId, - UserConfigService $userConfigService, - LoggerInterface $logger, - private readonly IUserManager $userManager, - ) { - parent::__construct( - $albumMapper, - $album, - $rootFolder, - $userId, - $userConfigService, - $logger, - ); - } +class SharedAlbumRoot extends AlbumRootBase { - public function delete() { + public function delete(): void { $this->albumMapper->deleteUserFromAlbumCollaboratorsList($this->userId, $this->album->getAlbum()->getId()); } @@ -45,6 +23,18 @@ public function setName($name): never { throw new Forbidden('Not allowed to rename a shared album'); } + public function createFile($name, $data = null) { + return parent::createFileInCurrentUserFolder($name, $data); + } + + public function getAlbumPhoto(AlbumFile $file): AlbumPhoto { + return new AlbumPhoto($this->albumMapper, $this->album->getAlbum(), $file, $this->rootFolder, $this->rootFolder->getUserFolder($this->userId)); + } + + public function copyInto($targetName, $sourcePath, INode $sourceNode): bool { + return parent::copyIntoAlbum($targetName, $sourcePath, $sourceNode); + } + protected function addFile(int $sourceId, string $ownerUID): bool { if (in_array($sourceId, $this->album->getFileIds())) { throw new Conflict("File $sourceId is already in the folder"); diff --git a/lib/Sabre/Album/SharedAlbumsHome.php b/lib/Sabre/Album/SharedAlbumsHome.php index 911456803..c488c5c44 100644 --- a/lib/Sabre/Album/SharedAlbumsHome.php +++ b/lib/Sabre/Album/SharedAlbumsHome.php @@ -18,14 +18,14 @@ use Sabre\DAV\Exception\Forbidden; class SharedAlbumsHome extends AlbumsHome { - public const NAME = 'sharedalbums'; + public const string NAME = 'sharedalbums'; public function __construct( array $principalInfo, AlbumMapper $albumMapper, string $userId, IRootFolder $rootFolder, - private readonly IUserManager $userManager, + IUserManager $userManager, private readonly IGroupManager $groupManager, UserConfigService $userConfigService, LoggerInterface $logger, @@ -37,6 +37,7 @@ public function __construct( $rootFolder, $userConfigService, $logger, + $userManager, ); } @@ -48,7 +49,7 @@ public function createDirectory($name): never { } /** - * @return SharedAlbumRoot[] + * @return AlbumRootBase[] */ public function getChildren(): array { if ($this->children === null) { diff --git a/lib/Sabre/CollectionPhoto.php b/lib/Sabre/CollectionPhoto.php index 5d50b8601..db65b2b94 100644 --- a/lib/Sabre/CollectionPhoto.php +++ b/lib/Sabre/CollectionPhoto.php @@ -29,11 +29,8 @@ public function getName() { return $this->file->getFileId() . '-' . $this->file->getName(); } - /** - * @return never - */ public function setName($name): never { - throw new Forbidden('Can\'t rename photos trough this api'); + throw new Forbidden('Can\'t rename photos through this api'); } public function getLastModified() { diff --git a/lib/Sabre/PhotosHome.php b/lib/Sabre/PhotosHome.php index 3c4ed0f12..34af22a5e 100644 --- a/lib/Sabre/PhotosHome.php +++ b/lib/Sabre/PhotosHome.php @@ -71,7 +71,7 @@ public function createDirectory($name): never { public function getChild($name) { return match ($name) { - AlbumsHome::NAME => new AlbumsHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userConfigService, $this->logger), + AlbumsHome::NAME => new AlbumsHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userConfigService, $this->logger, $this->userManager), SharedAlbumsHome::NAME => new SharedAlbumsHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userManager, $this->groupManager, $this->userConfigService, $this->logger), PlacesHome::NAME => new PlacesHome($this->userId, $this->rootFolder, $this->reverseGeoCoderService, $this->placeMapper), default => throw new NotFound(), @@ -83,7 +83,7 @@ public function getChild($name) { */ public function getChildren(): array { return [ - new AlbumsHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userConfigService, $this->logger), + new AlbumsHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userConfigService, $this->logger, $this->userManager), new SharedAlbumsHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userManager, $this->groupManager, $this->userConfigService, $this->logger), new PlacesHome($this->userId, $this->rootFolder, $this->reverseGeoCoderService, $this->placeMapper), ]; diff --git a/lib/Sabre/Place/PlaceRoot.php b/lib/Sabre/Place/PlaceRoot.php index e09fd5c79..ae8058289 100644 --- a/lib/Sabre/Place/PlaceRoot.php +++ b/lib/Sabre/Place/PlaceRoot.php @@ -117,17 +117,4 @@ public function getFirstPhoto(): int { public function getFileIds(): array { return array_map(fn (PlacePhoto $file): int => $file->getFileId(), $this->getChildren()); } - - /** - * @return int|null - */ - public function getCover() { - $children = $this->getChildren(); - - if (count($children) > 0) { - return $children[0]->getFileId(); - } else { - return null; - } - } } diff --git a/lib/Sabre/PropFindPlugin.php b/lib/Sabre/PropFindPlugin.php index 644ad8397..2787b6050 100644 --- a/lib/Sabre/PropFindPlugin.php +++ b/lib/Sabre/PropFindPlugin.php @@ -9,9 +9,9 @@ namespace OCA\Photos\Sabre; use OCA\DAV\Connector\Sabre\FilesPlugin; -use OCA\Photos\Album\AlbumMapper; use OCA\Photos\Sabre\Album\AlbumPhoto; use OCA\Photos\Sabre\Album\AlbumRoot; +use OCA\Photos\Sabre\Album\AlbumRootBase; use OCA\Photos\Sabre\Album\PublicAlbumPhoto; use OCA\Photos\Sabre\Place\PlacePhoto; use OCA\Photos\Sabre\Place\PlaceRoot; @@ -19,6 +19,7 @@ use OCP\Files\NotFoundException; use OCP\FilesMetadata\IFilesMetadataManager; use OCP\IPreview; +use Sabre\DAV\ICollection; use Sabre\DAV\INode; use Sabre\DAV\PropFind; use Sabre\DAV\PropPatch; @@ -110,10 +111,13 @@ public function propFind(PropFind $propFind, INode $node): void { } - if ($node instanceof AlbumRoot) { + if ($node instanceof ICollection) { + $propFind->handle(self::NBITEMS_PROPERTYNAME, fn (): int => count($node->getChildren())); + } + + if ($node instanceof AlbumRootBase) { $propFind->handle(self::ORIGINAL_NAME_PROPERTYNAME, fn (): string => $node->getAlbum()->getAlbum()->getTitle()); $propFind->handle(self::LAST_PHOTO_PROPERTYNAME, fn (): int => $node->getAlbum()->getAlbum()->getLastAddedPhoto()); - $propFind->handle(self::NBITEMS_PROPERTYNAME, fn (): int => count($node->getChildren())); $propFind->handle(self::LOCATION_PROPERTYNAME, fn (): string => $node->getAlbum()->getAlbum()->getLocation()); $propFind->handle(self::DATE_RANGE_PROPERTYNAME, fn () => json_encode($node->getDateRange())); $propFind->handle(self::COLLABORATORS_PROPERTYNAME, fn (): array => $node->getCollaborators()); @@ -121,7 +125,6 @@ public function propFind(PropFind $propFind, INode $node): void { if ($node instanceof PlaceRoot) { $propFind->handle(self::LAST_PHOTO_PROPERTYNAME, fn (): int => $node->getFirstPhoto()); - $propFind->handle(self::NBITEMS_PROPERTYNAME, fn (): int => count($node->getChildren())); } } @@ -132,8 +135,7 @@ public function handleUpdateProperties($path, PropPatch $propPatch): void { if ($location instanceof Complex) { $location = $location->getXml(); } - - $this->albumMapper->setLocation($node->getAlbum()->getAlbum()->getId(), $location); + $node->setLocation($location); return true; }); $propPatch->handle(self::COLLABORATORS_PROPERTYNAME, function ($collaborators) use ($node) { diff --git a/lib/Sabre/PublicRootCollection.php b/lib/Sabre/PublicRootCollection.php index 72476feeb..658f8df82 100644 --- a/lib/Sabre/PublicRootCollection.php +++ b/lib/Sabre/PublicRootCollection.php @@ -13,6 +13,7 @@ use OCA\Photos\Service\UserConfigService; use OCP\Files\IRootFolder; use OCP\IRequest; +use OCP\IUserManager; use OCP\Security\Bruteforce\IThrottler; use Psr\Log\LoggerInterface; use Sabre\DAV\Exception\Forbidden; @@ -31,6 +32,7 @@ public function __construct( private readonly IRequest $request, private readonly IThrottler $throttler, private readonly LoggerInterface $logger, + protected readonly IUserManager $userManager, ) { parent::__construct($principalBackend, 'principals/token'); } @@ -78,6 +80,7 @@ public function getChild($name) { $albums[0]->getAlbum()->getUserId(), $this->userConfigService, $this->logger, + $this->userManager, ); } } From aabc175802c0586362b5cbfa34163d3c80311fd8 Mon Sep 17 00:00:00 2001 From: Louis Chemineau Date: Wed, 7 May 2025 17:35:32 +0200 Subject: [PATCH 02/25] feat: Support setting and getting filters in albums Signed-off-by: Louis Chemineau --- appinfo/info.xml | 2 +- lib/Album/AlbumInfo.php | 8 +- lib/Album/AlbumMapper.php | 43 +++--- .../Version32000Date20250507132617.php | 35 +++++ lib/Sabre/Album/AlbumRoot.php | 4 + lib/Sabre/Album/AlbumRootBase.php | 4 + lib/Sabre/PropFindPlugin.php | 13 +- package-lock.json | 4 +- package.json | 2 +- src/components/Albums/AlbumForm.vue | 66 +++++--- src/components/Albums/AlbumPicker.vue | 11 +- .../Albums/CollaboratorsSelectionForm.vue | 5 +- src/services/Albums.ts | 143 ------------------ src/services/collectionFetcher.ts | 2 + src/store/albums.ts | 17 ++- src/store/publicAlbums.ts | 8 +- src/views/AlbumContent.vue | 4 +- src/views/Albums.vue | 3 +- src/views/PublicAlbumContent.vue | 13 +- src/views/SharedAlbumContent.vue | 3 +- src/views/SharedAlbums.vue | 3 +- src/views/Timeline.vue | 14 +- 22 files changed, 185 insertions(+), 222 deletions(-) create mode 100644 lib/Migration/Version32000Date20250507132617.php delete mode 100644 src/services/Albums.ts diff --git a/appinfo/info.xml b/appinfo/info.xml index b93c79049..79c5f1d85 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -9,7 +9,7 @@ Photos Your memories under your control Your memories under your control - 5.0.0-dev.0 + 5.0.0-dev.1 agpl John Molakvoæ Photos diff --git a/lib/Album/AlbumInfo.php b/lib/Album/AlbumInfo.php index fc4295b2c..0591b393c 100644 --- a/lib/Album/AlbumInfo.php +++ b/lib/Album/AlbumInfo.php @@ -16,8 +16,8 @@ public function __construct( private readonly string $location, private readonly int $created, private readonly int $lastAdded, - private readonly ?int $receivedFrom = null, private readonly ?array $filters = null, + private readonly ?int $receivedFrom = null, ) { } @@ -49,7 +49,11 @@ public function getReceivedFrom(): ?int { return $this->receivedFrom; } - public function getFilters(): ?array { + public function getFilters(): ?string { return $this->filters; } + + public function getDecodedFilters(): array { + return json_decode($this->filters ?? '{}', true); + } } diff --git a/lib/Album/AlbumMapper.php b/lib/Album/AlbumMapper.php index 59bc62788..2b69e6e6f 100644 --- a/lib/Album/AlbumMapper.php +++ b/lib/Album/AlbumMapper.php @@ -37,7 +37,7 @@ public function __construct( ) { } - public function create(string $userId, string $name, string $location = ''): AlbumInfo { + public function create(string $userId, string $name, string $location = '', ?string $filters = null): AlbumInfo { $created = $this->timeFactory->getTime(); $query = $this->connection->getQueryBuilder(); $query->insert('photos_albums') @@ -47,21 +47,22 @@ public function create(string $userId, string $name, string $location = ''): Alb 'location' => $query->createNamedParameter($location), 'created' => $query->createNamedParameter($created, IQueryBuilder::PARAM_INT), 'last_added_photo' => $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT), + 'filters' => $query->createNamedParameter($filters, IQueryBuilder::PARAM_STR), ]); $query->executeStatement(); $id = $query->getLastInsertId(); - return new AlbumInfo($id, $userId, $name, $location, $created, -1); + return new AlbumInfo($id, $userId, $name, $location, $created, -1, $filters); } public function get(int $id): ?AlbumInfo { $query = $this->connection->getQueryBuilder(); - $query->select('name', 'user', 'location', 'created', 'last_added_photo') + $query->select('name', 'user', 'location', 'created', 'last_added_photo', 'filters') ->from('photos_albums') ->where($query->expr()->eq('album_id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT))); $row = $query->executeQuery()->fetch(); if ($row) { - return new AlbumInfo($id, $row['user'], $row['name'], $row['location'], (int)$row['created'], (int)$row['last_added_photo']); + return new AlbumInfo($id, $row['user'], $row['name'], $row['location'], (int)$row['created'], (int)$row['last_added_photo'], $row['filters']); } else { return null; } @@ -72,11 +73,11 @@ public function get(int $id): ?AlbumInfo { */ public function getForUser(string $userId): array { $query = $this->connection->getQueryBuilder(); - $query->select('album_id', 'name', 'location', 'created', 'last_added_photo') + $query->select('album_id', 'name', 'location', 'created', 'last_added_photo', 'filters') ->from('photos_albums') ->where($query->expr()->eq('user', $query->createNamedParameter($userId))); $rows = $query->executeQuery()->fetchAll(); - return array_map(fn (array $row): AlbumInfo => new AlbumInfo((int)$row['album_id'], $userId, $row['name'], $row['location'], (int)$row['created'], (int)$row['last_added_photo']), $rows); + return array_map(fn (array $row): AlbumInfo => new AlbumInfo((int)$row['album_id'], $userId, $row['name'], $row['location'], (int)$row['created'], (int)$row['last_added_photo'], $row['filters']), $rows); } /** @@ -84,13 +85,13 @@ public function getForUser(string $userId): array { */ public function getByName(string $albumName, string $userName): ?AlbumInfo { $query = $this->connection->getQueryBuilder(); - $query->select('album_id', 'location', 'created', 'last_added_photo') + $query->select('album_id', 'location', 'created', 'last_added_photo', 'filters') ->from('photos_albums') ->where($query->expr()->eq('name', $query->createNamedParameter($albumName))) ->andWhere($query->expr()->eq('user', $query->createNamedParameter($userName))); $row = $query->executeQuery()->fetch(); if ($row) { - return new AlbumInfo((int)$row['album_id'], $userName, $albumName, $row['location'], (int)$row['created'], (int)$row['last_added_photo']); + return new AlbumInfo((int)$row['album_id'], $userName, $albumName, $row['location'], (int)$row['created'], (int)$row['last_added_photo'], $row['filters']); } else { return null; } @@ -101,12 +102,12 @@ public function getByName(string $albumName, string $userName): ?AlbumInfo { */ public function getForFile(int $fileId): array { $query = $this->connection->getQueryBuilder(); - $query->select('a.album_id', 'name', 'user', 'location', 'created', 'last_added_photo') + $query->select('a.album_id', 'name', 'user', 'location', 'created', 'last_added_photo', 'filters') ->from('photos_albums', 'a') ->leftJoin('a', 'photos_albums_files', 'p', $query->expr()->eq('a.album_id', 'p.album_id')) ->where($query->expr()->eq('file_id', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); $rows = $query->executeQuery()->fetchAll(); - return array_map(fn (array $row): AlbumInfo => new AlbumInfo((int)$row['album_id'], $row['user'], $row['name'], $row['location'], (int)$row['created'], (int)$row['last_added_photo']), $rows); + return array_map(fn (array $row): AlbumInfo => new AlbumInfo((int)$row['album_id'], $row['user'], $row['name'], $row['location'], (int)$row['created'], (int)$row['last_added_photo'], $row['filters']), $rows); } /** @@ -114,13 +115,13 @@ public function getForFile(int $fileId): array { */ public function getForUserAndFile(string $userId, int $fileId): array { $query = $this->connection->getQueryBuilder(); - $query->select('a.album_id', 'name', 'user', 'location', 'created', 'last_added_photo') + $query->select('a.album_id', 'name', 'user', 'location', 'created', 'last_added_photo', 'filters') ->from('photos_albums', 'a') ->leftJoin('a', 'photos_albums_files', 'p', $query->expr()->eq('a.album_id', 'p.album_id')) ->where($query->expr()->eq('user', $query->createNamedParameter($userId))) ->andWhere($query->expr()->eq('file_id', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); $rows = $query->executeQuery()->fetchAll(); - return array_map(fn (array $row): AlbumInfo => new AlbumInfo((int)$row['album_id'], $row['user'], $row['name'], $row['location'], (int)$row['created'], (int)$row['last_added_photo']), $rows); + return array_map(fn (array $row): AlbumInfo => new AlbumInfo((int)$row['album_id'], $row['user'], $row['name'], $row['location'], (int)$row['created'], (int)$row['last_added_photo'], $row['filters']), $rows); } public function rename(int $id, string $newName): void { @@ -166,7 +167,7 @@ public function delete(int $id): void { public function getForAlbumIdAndUserWithFiles(int $albumId, string $userId): array { // TODO: Add search from filters $query = $this->connection->getQueryBuilder(); - $query->select('fileid', 'mimetype', 'a.album_id', 'size', 'mtime', 'etag', 'added', 'owner', 'filters') + $query->select('fileid', 'mimetype', 'a.album_id', 'size', 'mtime', 'etag', 'added', 'owner') ->selectAlias('f.name', 'file_name') ->selectAlias('a.name', 'album_name') ->from('photos_albums', 'a') @@ -434,7 +435,7 @@ public function setCollaborators(int $albumId, array $collaborators): void { public function getSharedAlbumsForCollaborator(string $collaboratorId, int $collaboratorType): array { $query = $this->connection->getQueryBuilder(); $rows = $query - ->select('a.album_id', 'name', 'user', 'location', 'created', 'last_added_photo') + ->select('a.album_id', 'name', 'user', 'location', 'created', 'last_added_photo', 'filters') ->from('photos_albums_collabs', 'c') ->leftJoin('c', 'photos_albums', 'a', $query->expr()->eq('a.album_id', 'c.album_id')) ->where($query->expr()->eq('collaborator_id', $query->createNamedParameter($collaboratorId))) @@ -448,7 +449,8 @@ public function getSharedAlbumsForCollaborator(string $collaboratorId, int $coll $row['name'] . ' (' . $row['user'] . ')', $row['location'], (int)$row['created'], - (int)$row['last_added_photo'] + (int)$row['last_added_photo'], + $row['filters'], ), $rows); } @@ -487,7 +489,7 @@ public function getSharedAlbumsForCollaboratorWithFiles(string $collaboratorId, if ($collaboratorType !== self::TYPE_LINK) { $albumName = $row['album_name'] . ' (' . $row['album_user'] . ')'; } - $albumsById[$albumId] = new AlbumInfo($albumId, $row['album_user'], $albumName, $row['location'], (int)$row['created'], (int)$row['last_added_photo'], $collaboratorType); + $albumsById[$albumId] = new AlbumInfo($albumId, $row['album_user'], $albumName, $row['location'], (int)$row['created'], (int)$row['last_added_photo'], $row['filters'], $collaboratorType); } } @@ -525,7 +527,7 @@ public function deleteGroupFromAlbumCollaboratorsList(string $groupId, int $albu public function getAlbumsForCollaboratorIdAndFileId(string $collaboratorId, int $collaboratorType, int $fileId): array { $query = $this->connection->getQueryBuilder(); $rows = $query - ->select('a.album_id', 'name', 'user', 'location', 'created', 'last_added_photo') + ->select('a.album_id', 'name', 'user', 'location', 'created', 'last_added_photo', 'filters') ->from('photos_albums_collabs', 'c') ->leftJoin('c', 'photos_albums', 'a', $query->expr()->eq('a.album_id', 'c.album_id')) ->leftJoin('a', 'photos_albums_files', 'p', $query->expr()->eq('a.album_id', 'p.album_id')) @@ -543,14 +545,15 @@ public function getAlbumsForCollaboratorIdAndFileId(string $collaboratorId, int $row['name'] . ' (' . $row['user'] . ')', $row['location'], (int)$row['created'], - (int)$row['last_added_photo'] + (int)$row['last_added_photo'], + $row['filters'], ), $rows); } - public function setAlbumFilters(int $albumId, array $filters): void { + public function setAlbumFilters(int $albumId, string $filters): void { $query = $this->connection->getQueryBuilder(); $query->update('photos_albums') - ->set('filters', $query->createNamedParameter(json_encode($filters))) + ->set('filters', $query->createNamedParameter($filters)) ->where($query->expr()->eq('album_id', $query->createNamedParameter($albumId, IQueryBuilder::PARAM_STR))); $query->executeStatement(); } diff --git a/lib/Migration/Version32000Date20250507132617.php b/lib/Migration/Version32000Date20250507132617.php new file mode 100644 index 000000000..03407f83b --- /dev/null +++ b/lib/Migration/Version32000Date20250507132617.php @@ -0,0 +1,35 @@ +getTable('photos_albums'); + + if (!$albumsTable->hasColumn('filters')) { + $albumsTable->addColumn('filters', Types::TEXT); + } + + return $schema; + } +} diff --git a/lib/Sabre/Album/AlbumRoot.php b/lib/Sabre/Album/AlbumRoot.php index 9ecc066be..b83a4c8aa 100644 --- a/lib/Sabre/Album/AlbumRoot.php +++ b/lib/Sabre/Album/AlbumRoot.php @@ -67,4 +67,8 @@ public function setCollaborators(array $collaborators): array { public function setLocation(string $location): void { $this->albumMapper->setLocation($this->getAlbum()->getAlbum()->getId(), $location); } + + public function setFilters(string $filters) { + $this->albumMapper->setAlbumFilters($this->getAlbum()->getAlbum()->getId(), $filters); + } } diff --git a/lib/Sabre/Album/AlbumRootBase.php b/lib/Sabre/Album/AlbumRootBase.php index fb0c3381d..eebb892ae 100644 --- a/lib/Sabre/Album/AlbumRootBase.php +++ b/lib/Sabre/Album/AlbumRootBase.php @@ -177,4 +177,8 @@ final public function getDateRange(): array { * @return array{array{'nc:collaborator': array{'id': string, 'label': string, 'type': int}}} */ abstract public function getCollaborators(): array; + + final public function getFilters(): ?string { + return $this->album->getAlbum()->getFilters(); + } } diff --git a/lib/Sabre/PropFindPlugin.php b/lib/Sabre/PropFindPlugin.php index 2787b6050..90b98cc34 100644 --- a/lib/Sabre/PropFindPlugin.php +++ b/lib/Sabre/PropFindPlugin.php @@ -37,17 +37,15 @@ class PropFindPlugin extends ServerPlugin { public const LAST_PHOTO_PROPERTYNAME = '{http://nextcloud.org/ns}last-photo'; public const NBITEMS_PROPERTYNAME = '{http://nextcloud.org/ns}nbItems'; public const COLLABORATORS_PROPERTYNAME = '{http://nextcloud.org/ns}collaborators'; + public const FILTERS_PROPERTYNAME = '{http://nextcloud.org/ns}filters'; public const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions'; - private readonly IPreview $previewManager; private ?Tree $tree = null; public function __construct( - IPreview $previewManager, - private readonly AlbumMapper $albumMapper, + private readonly IPreview $previewManager, private readonly IFilesMetadataManager $filesMetadataManager, ) { - $this->previewManager = $previewManager; } /** @@ -121,6 +119,7 @@ public function propFind(PropFind $propFind, INode $node): void { $propFind->handle(self::LOCATION_PROPERTYNAME, fn (): string => $node->getAlbum()->getAlbum()->getLocation()); $propFind->handle(self::DATE_RANGE_PROPERTYNAME, fn () => json_encode($node->getDateRange())); $propFind->handle(self::COLLABORATORS_PROPERTYNAME, fn (): array => $node->getCollaborators()); + $propFind->handle(self::FILTERS_PROPERTYNAME, fn (): ?string => $node->getFilters()); } if ($node instanceof PlaceRoot) { @@ -139,7 +138,11 @@ public function handleUpdateProperties($path, PropPatch $propPatch): void { return true; }); $propPatch->handle(self::COLLABORATORS_PROPERTYNAME, function ($collaborators) use ($node) { - $collaborators = $node->setCollaborators(json_decode($collaborators, true)); + $node->setCollaborators(json_decode($collaborators, true)); + return true; + }); + $propPatch->handle(self::FILTERS_PROPERTYNAME, function ($filters) use ($node) { + $node->setFilters($filters); return true; }); } diff --git a/package-lock.json b/package-lock.json index f36a769cb..076cf2a86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "photos", - "version": "5.0.0-dev.0", + "version": "5.0.0-dev.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "photos", - "version": "5.0.0-dev.0", + "version": "5.0.0-dev.1", "license": "AGPL-3.0-or-later", "dependencies": { "@essentials/request-timeout": "^1.3.0", diff --git a/package.json b/package.json index e3ed61d38..e0bf5824c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "photos", "description": "Your memories under your control", - "version": "5.0.0-dev.0", + "version": "5.0.0-dev.1", "author": "John Molakvoæ ", "contributors": [ "John Molakvoæ " diff --git a/src/components/Albums/AlbumForm.vue b/src/components/Albums/AlbumForm.vue index 23c67f7ba..f70041183 100644 --- a/src/components/Albums/AlbumForm.vue +++ b/src/components/Albums/AlbumForm.vue @@ -3,7 +3,7 @@ - SPDX-License-Identifier: AGPL-3.0-or-later --> diff --git a/src/components/TimelineFilters/PlacesFilter.vue b/src/components/PhotosFilters/PlacesFilter.vue similarity index 67% rename from src/components/TimelineFilters/PlacesFilter.vue rename to src/components/PhotosFilters/PlacesFilter.vue index d94119b06..80813ed23 100644 --- a/src/components/TimelineFilters/PlacesFilter.vue +++ b/src/components/PhotosFilters/PlacesFilter.vue @@ -18,14 +18,14 @@ diff --git a/src/components/PhotosFilters/PhotosFilters.vue b/src/components/PhotosFilters/PhotosFiltersInput.vue similarity index 88% rename from src/components/PhotosFilters/PhotosFilters.vue rename to src/components/PhotosFilters/PhotosFiltersInput.vue index c03fac253..9d251db19 100644 --- a/src/components/PhotosFilters/PhotosFilters.vue +++ b/src/components/PhotosFilters/PhotosFiltersInput.vue @@ -3,12 +3,12 @@ - SPDX-License-Identifier: AGPL-3.0-or-later --> @@ -41,7 +41,7 @@ function updateFilterValue(filterId: string, newFilterValue: unknown) { } diff --git a/src/components/PhotosFilters/PlacesFilter.vue b/src/components/PhotosFilters/PlacesFilterInput.vue similarity index 89% rename from src/components/PhotosFilters/PlacesFilter.vue rename to src/components/PhotosFilters/PlacesFilterInput.vue index 80813ed23..a9f934a5a 100644 --- a/src/components/PhotosFilters/PlacesFilter.vue +++ b/src/components/PhotosFilters/PlacesFilterInput.vue @@ -44,9 +44,7 @@ const emit = defineEmits<{ const loading = ref(true) const availableOptions = ref([]) -const selectedOptions = ref((props.value ?? []) - .map(place => availableOptions.value.find(option => option.label === place)) - .filter(place => place !== undefined)) +const selectedOptions = ref([]) watch(selectedOptions, (newSelectedOptionValue) => { if (newSelectedOptionValue.length === 0) { @@ -62,6 +60,11 @@ fetchCollections(placesPrefix) label: place.basename, previewUrl: generateUrl(`/apps/photos/api/v1/preview/${place.attributes['last-photo']}?x=${64}&y=${64}`), })) + + selectedOptions.value = (props.value ?? []) + .map(place => availableOptions.value.find(option => option.label === place)) + .filter(place => place !== undefined) + loading.value = false }) diff --git a/src/services/PhotosFilters/PhotosFilter.ts b/src/services/PhotosFilters/PhotosFilter.ts index 142d12798..bd630b088 100644 --- a/src/services/PhotosFilters/PhotosFilter.ts +++ b/src/services/PhotosFilters/PhotosFilter.ts @@ -7,6 +7,7 @@ import type { ComponentPublicInstanceConstructor } from 'vue/types/v3-component- export type PhotosFilter = { id: string - component: ComponentPublicInstanceConstructor + inputComponent: ComponentPublicInstanceConstructor + displayComponent: ComponentPublicInstanceConstructor getQuery(value: unknown): string } diff --git a/src/services/PhotosFilters/dateRangeFilter.ts b/src/services/PhotosFilters/dateRangeFilter.ts index a63c2d117..e6bedf61c 100644 --- a/src/services/PhotosFilters/dateRangeFilter.ts +++ b/src/services/PhotosFilters/dateRangeFilter.ts @@ -4,7 +4,8 @@ */ import type { PhotosFilter } from './PhotosFilter.ts' -import DateRangeFilter from '../../components/PhotosFilters/DateRangeFilter.vue' +import DateRangeFilterInput from '../../components/PhotosFilters/DateRangeFilterInput.vue'; +import DateRangeFilterDisplay from '../../components/PhotosFilters/DateRangeFilterDisplay.vue'; export type DateRangeValueType = { start: number; end: number }|undefined @@ -12,7 +13,8 @@ export const dateRangeFilterId = 'date-range' export const dateRangeFilter: PhotosFilter = { id: dateRangeFilterId, - component: DateRangeFilter, + inputComponent: DateRangeFilterInput, + displayComponent: DateRangeFilterDisplay, getQuery(dateRange: DateRangeValueType): string { if (dateRange === undefined) { return '' diff --git a/src/services/PhotosFilters/placesFilter.ts b/src/services/PhotosFilters/placesFilter.ts index 818139484..8627b1e9b 100644 --- a/src/services/PhotosFilters/placesFilter.ts +++ b/src/services/PhotosFilters/placesFilter.ts @@ -4,7 +4,8 @@ */ import type { PhotosFilter } from './PhotosFilter.ts' -import PlacesFilter from '../../components/PhotosFilters/PlacesFilter.vue' +import PlacesFilterInput from '../../components/PhotosFilters/PlacesFilterInput.vue' +import PlacesFilterDisplay from '../../components/PhotosFilters/PlacesFilterDisplay.vue' export type PlacesValueType = string[]|undefined @@ -12,7 +13,8 @@ export const placesFilterId = 'places' export const placesFilter: PhotosFilter = { id: placesFilterId, - component: PlacesFilter, + inputComponent: PlacesFilterInput, + displayComponent: PlacesFilterDisplay, getQuery(places: PlacesValueType): string { if (places === undefined) { return '' diff --git a/src/views/AlbumContent.vue b/src/views/AlbumContent.vue index 9805fe654..ecd72a8b8 100644 --- a/src/views/AlbumContent.vue +++ b/src/views/AlbumContent.vue @@ -34,23 +34,23 @@ {{ t('photos', 'Unselect all') }} - - + + + + - - @@ -177,8 +177,8 @@ import MapMarker from 'vue-material-design-icons/MapMarker.vue' import Pencil from 'vue-material-design-icons/Pencil.vue' import Plus from 'vue-material-design-icons/Plus.vue' import ShareVariant from 'vue-material-design-icons/ShareVariant.vue' -import Filter from 'vue-material-design-icons/Filter.vue' -import FilterOff from 'vue-material-design-icons/FilterOff.vue' +import FilterPlus from 'vue-material-design-icons/FilterPlus.vue' +import FilterCheck from 'vue-material-design-icons/FilterCheck.vue' import FetchFilesMixin from '../mixins/FetchFilesMixin.js' import FetchCollectionContentMixin from '../mixins/FetchCollectionContentMixin.js' @@ -188,7 +188,8 @@ import ActionFavorite from '../components/Actions/ActionFavorite.vue' import AlbumForm from '../components/Albums/AlbumForm.vue' import CollaboratorsSelectionForm from '../components/Albums/CollaboratorsSelectionForm.vue' import CollectionContent from '../components/Collection/CollectionContent.vue' -import PhotosFilters from '../components/PhotosFilters/PhotosFilters.vue' +import PhotosFiltersInput from '../components/PhotosFilters/PhotosFiltersInput.vue' +import PhotosFiltersDisplay from '../components/PhotosFilters/PhotosFiltersDisplay.vue' import PhotosPicker from '../components/PhotosPicker.vue' import HeaderNavigation from '../components/HeaderNavigation.vue' import logger from '../services/logger.js' @@ -222,9 +223,10 @@ export default { Pencil, Plus, ShareVariant, - FilterIcon: Filter, - FilterOff, - PhotosFilters, + FilterPlus, + FilterCheck, + PhotosFiltersInput, + PhotosFiltersDisplay, }, mixins: [ @@ -245,7 +247,7 @@ export default { showAddPhotosModal: false, showManageCollaboratorView: false, showEditAlbumForm: false, - showFilters: false, + editFilters: false, loadingAddCollaborators: false, } @@ -324,8 +326,8 @@ export default { }, toggleFilters() { - this.showFilters = !this.showFilters - if (!this.showFilters) { + this.editFilters = !this.editFilters + if (!this.editFilters) { this.extraFilters = {} this.resetFetchFilesState() } @@ -347,6 +349,11 @@ export default { :deep(.collection) { height: 100%; } + + &__filters { + display: flex; + gap: 8px; + } } .album { diff --git a/src/views/Albums.vue b/src/views/Albums.vue index ea9fd6e30..e924d84c5 100644 --- a/src/views/Albums.vue +++ b/src/views/Albums.vue @@ -31,9 +31,12 @@ :link="`/albums/${collection.basename}`" :alt-img="t('photos', 'Cover photo for album {albumName}', { albumName: collection.basename })" :cover-url="collection.attributes['last-photo'] | coverUrl"> - - {{ collection.basename }} - +