Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9eded21
add support for embedded tags in metadata display
bastiion Aug 17, 2025
c494373
add new optional fields for embedded tags and subjects in typings
bastiion Aug 17, 2025
ead51b8
add RatingStars component for interactive star rating functionality
bastiion Aug 17, 2025
a8029d4
implement rating functionality in Metadata component with editable st…
bastiion Aug 17, 2025
e7aa210
include EXIF data and implement minimum rating filter for photo queries
bastiion Aug 17, 2025
ddc68e8
add minimum rating parameter to DaysController
bastiion Aug 17, 2025
11ce542
add minRating filter to event bus and DaysFilterType enum
bastiion Aug 17, 2025
0527cda
Implement FilterComponent for filtering memories by minimum rating an…
bastiion Aug 17, 2025
f85e56b
Integrate filtering functionality in Timeline and RowHead components
bastiion Aug 17, 2025
9f44701
Add support for caching embedded tags from EXIF data on a per user basis
bastiion Aug 18, 2025
4af7464
fix uneeded callbacls
bastiion Aug 18, 2025
a3cbbb0
remove unused filter-related data and methods
bastiion Aug 18, 2025
c365881
Refactor embedded tag extraction into Exif class
bastiion Aug 18, 2025
aba54cf
Add routes to include new API endpoints for embedded tags.
bastiion Aug 18, 2025
014d0b7
Added embeddedTags parameter to TimelineQueryDays
bastiion Aug 18, 2025
4b7e70a
Add embedded tags support in API and typings
bastiion Aug 18, 2025
b4adcd3
Add EmbeddedTagSelector component for tag selection functionality
bastiion Aug 18, 2025
b9b8f6a
Add embedded tags filtering functionality to FilterComponent and Time…
bastiion Aug 18, 2025
4370bc3
introduces a new FilterDropdownButton component
bastiion Sep 2, 2025
a214b36
Add FilterDropdownButton to MobileHeader and SearchbarMenuItem
bastiion Sep 2, 2025
d16f878
fix EXIF filtering options for minimum rating and embedded tags in Da…
bastiion Sep 2, 2025
d88593a
Add RatingTags component for displaying photo ratings and hierarchica…
bastiion Sep 2, 2025
1aa9595
make Viewer component to display photo EXIF tags and ratings
bastiion Sep 2, 2025
37232a1
Add metadata options for slideshow and gallery
bastiion Sep 2, 2025
a90719f
Add metadata options for gallery and photo rating in settings
bastiion Sep 2, 2025
ad901d5
Add EXIF data handling functions for rating and tags extraction
bastiion Sep 2, 2025
26170ee
prop to conditionally hide rating stars
bastiion Sep 2, 2025
af8958f
Refactor EXIF data handling in Metadata component
bastiion Sep 2, 2025
39bfb6e
Add interactive rating and metadata overlays in Photo component
bastiion Sep 2, 2025
5f85d4c
Refactor currentRating and currentTags computed properties in Viewer …
bastiion Sep 2, 2025
d84aaa9
fix DaysController to filter photos by prefiltered fileIds
bastiion Sep 3, 2025
79f2709
Enhance FilterComponent to support collaborative tags filtering
bastiion Sep 3, 2025
3660ff3
fix: EmbeddedTagSelector to synchronize selected tags with value prop
bastiion Dec 21, 2025
5880c25
refactor: Switch to AND filtering logic in TimelineQueryDays and Time…
bastiion Dec 21, 2025
e8b6246
fix: FilterPanel keeps current filters across remounts
bastiion Dec 21, 2025
2746b8b
chore: Simplify data initialization and streamline emitFilterChange c…
bastiion Dec 21, 2025
9d5b8ed
fix: some design inconsistencies streamlined
bastiion Dec 21, 2025
1bfa438
feat: Implement embedded tags editing functionality and fix duplicate…
bastiion Dec 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ function w($base, $param)

['name' => 'Tags#set', 'url' => '/api/tags/set/{id}', 'verb' => 'PATCH'],

['name' => 'EmbeddedTags#flat', 'url' => '/api/embedded-tags/flat', 'verb' => 'GET'],
['name' => 'EmbeddedTags#hierarchical', 'url' => '/api/embedded-tags/hierarchical', 'verb' => 'GET'],
['name' => 'EmbeddedTags#count', 'url' => '/api/embedded-tags/count', 'verb' => 'GET'],

['name' => 'Map#clusters', 'url' => '/api/map/clusters', 'verb' => 'GET'],
['name' => 'Map#init', 'url' => '/api/map/init', 'verb' => 'GET'],

Expand Down
41 changes: 41 additions & 0 deletions lib/Controller/DaysController.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ public function day(array $dayIds): Http\Response
$this->isHidden(),
$this->isMonthView(),
$this->isReverse(),
$this->getMinRating(),
$this->getEmbeddedTags(),
$this->getTransformations(),
);

Expand Down Expand Up @@ -114,6 +116,18 @@ private function getTransformations(): array
$transforms[] = [$this->tq, 'transformMapBoundsFilter', $bounds];
}

// Min rating filter - only if SQL filtering is enabled
if ($this->tq->shouldFilterExifBySQL() && ($minRating = $this->getMinRating())) {
$transforms[] = [$this->tq, 'transformMinRatingFilter', $minRating];
}

// Embedded tags filter - only if SQL filtering is enabled
if ($this->tq->shouldFilterExifBySQL() && ($embeddedTags = $this->getEmbeddedTags())) {
if (!empty($embeddedTags)) {
$transforms[] = [$this->tq, 'transformEmbeddedTagsFilter', $embeddedTags];
}
}

// Limit number of responses for day query
if ($limit = $this->request->getParam('limit')) {
$transforms[] = [$this->tq, 'transformLimit', (int) $limit];
Expand Down Expand Up @@ -168,6 +182,8 @@ private function preloadDays(array &$days): void
$this->isHidden(),
$this->isMonthView(),
$this->isReverse(),
$this->getMinRating(),
$this->getEmbeddedTags(),
$this->getTransformations(),
);

Expand All @@ -178,6 +194,15 @@ private function preloadDays(array &$days): void
continue;
}

// Only include photos that are in the fileIds array (if it exists)
$dayData = $drefMap[$dayId];
if (isset($dayData['fileIds']) && !empty($dayData['fileIds'])) {
$photoFileId = (int) $photo['fileid'];
if (!in_array($photoFileId, $dayData['fileIds'], true)) {
continue;
}
}

if (!($drefMap[$dayId]['detail'] ?? null)) {
$drefMap[$dayId]['detail'] = [];
}
Expand Down Expand Up @@ -215,4 +240,20 @@ private function isReverse(): bool
{
return null !== $this->request->getParam('reverse');
}

private function getMinRating(): int
{
return (int) $this->request->getParam('minRating') ?? 0;
}

private function getEmbeddedTags(): array
{
$embeddedTagsParam = $this->request->getParam('embeddedTags');
if ($embeddedTagsParam) {
// Decode URI-encoded string before splitting
$decoded = urldecode($embeddedTagsParam);
return explode(',', $decoded);
}
return [];
}
}
158 changes: 158 additions & 0 deletions lib/Controller/EmbeddedTagsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2022 Varun Patil <radialapps@gmail.com>
* @author Varun Patil <radialapps@gmail.com>
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

namespace OCA\Memories\Controller;

use OCA\Memories\Db\EmbeddedTagsQuery;
use OCA\Memories\Db\FsManager;
use OCA\Memories\Db\TimelineQuery;
use OCA\Memories\Exceptions;
use OCA\Memories\Util;
use OCP\App\IAppManager;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\JSONResponse;
use OCP\Files\IRootFolder;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IRequest;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;

class EmbeddedTagsController extends GenericApiController
{
public function __construct(
IRequest $request,
IConfig $config,
IUserSession $userSession,
IDBConnection $connection,
IRootFolder $rootFolder,
IAppManager $appManager,
LoggerInterface $logger,
TimelineQuery $tq,
FsManager $fs,
protected EmbeddedTagsQuery $etq,
) {
parent::__construct($request, $config, $userSession, $connection, $rootFolder, $appManager, $logger, $tq, $fs);
}

/**
* Get tags in flat manner with optional filtering and pagination
*/
#[NoAdminRequired]
public function flat(): Http\Response
{
return Util::guardEx(function () {
// Check if user is logged in
if (!Util::isLoggedIn()) {
throw Exceptions::NotLoggedIn();
}

// Get query parameters
$pattern = $this->request->getParam('pattern');
$limit = $this->request->getParam('limit');
$offset = $this->request->getParam('offset');

// Validate and sanitize parameters
$limit = $limit !== null ? max(1, min(1000, (int) $limit)) : null;
$offset = $offset !== null ? max(0, (int) $offset) : null;
$pattern = $pattern !== null ? (string) $pattern : null;

// Get tags
$tags = $this->etq->getTagsFlat($pattern, $limit, $offset);

// Get total count for pagination
$totalCount = null;
if ($limit !== null || $offset !== null) {
$totalCount = $this->etq->getTagsCount($pattern);
}

// Prepare response
$response = [
'tags' => $tags,
];

if ($totalCount !== null) {
$response['pagination'] = [
'total' => $totalCount,
'limit' => $limit,
'offset' => $offset ?? 0,
];
}

return new JSONResponse($response, Http::STATUS_OK);
});
}

/**
* Get tags in hierarchical structure
*/
#[NoAdminRequired]
public function hierarchical(): Http\Response
{
return Util::guardEx(function () {
// Check if user is logged in
if (!Util::isLoggedIn()) {
throw Exceptions::NotLoggedIn();
}

// Get query parameters
$pattern = $this->request->getParam('pattern');
$pattern = $pattern !== null ? (string) $pattern : null;

// Get tags in hierarchical structure
$tags = $this->etq->getTagsHierarchical($pattern);

return new JSONResponse([
'tags' => $tags,
'structure' => 'hierarchical'
], Http::STATUS_OK);
});
}

/**
* Get tags count (useful for pagination info)
*/
#[NoAdminRequired]
public function count(): Http\Response
{
return Util::guardEx(function () {
// Check if user is logged in
if (!Util::isLoggedIn()) {
throw Exceptions::NotLoggedIn();
}

// Get query parameters
$pattern = $this->request->getParam('pattern');
$pattern = $pattern !== null ? (string) $pattern : null;

// Get count
$count = $this->etq->getTagsCount($pattern);

return new JSONResponse([
'count' => $count,
'pattern' => $pattern
], Http::STATUS_OK);
});
}
}
3 changes: 3 additions & 0 deletions lib/Controller/OtherController.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ public function getUserConfig(): Http\Response
'livephoto_autoplay' => 'true' === $getAppConfig('livephotoAutoplay', 'false'),
'livephoto_loop' => 'true' === $getAppConfig('livephotoLoop', 'false'),
'sidebar_filepath' => 'true' === $getAppConfig('sidebarFilepath', false),
'metadata_in_slideshow' => 'true' === $getAppConfig('metadataInSlideshow', 'false'),
'metadata_in_gallery' => 'true' === $getAppConfig('metadataInGallery', 'false'),
'enable_exif_photo_rating_in_gallery' => 'true' === $getAppConfig('enableExifPhotoRatingInGallery', 'false'),

// folder settings
'folders_path' => $getAppConfig('foldersPath', '/'),
Expand Down
Loading