Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: show photos of shared album in timeline #1371

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions lib/Controller/OtherController.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public function getUserConfig(): Http\Response

// general settings
'timeline_path' => $getAppConfig('timelinePath', SystemConfig::get('memories.timeline.default_path')),
'timeline_include_shared_albums' => $getAppConfig('timelineHasSharedAlbums', SystemConfig::get('memories.timeline.default_include_shared_albums')),
'enable_top_memories' => 'true' === $getAppConfig('enableTopMemories', 'true'),
'stack_raw_files' => 'true' === $getAppConfig('stackRawFiles', 'true'),

Expand Down
1 change: 1 addition & 0 deletions lib/Db/FsManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ public function populateRoot(TimelineRoot &$root, bool $recursive = true): Timel
$paths = [$path];
} else {
$paths = Util::getTimelinePaths($uid);
$root->addSharedAlbums(Util::getTimelineIncludeSharedAlbums($uid));
}

// Combined etag, for cache invalidation.
Expand Down
46 changes: 41 additions & 5 deletions lib/Db/TimelineQueryCTE.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,24 +83,22 @@ protected function CTE_FOLDERS(bool $hidden): string
{
$CLS_HIDDEN = $hidden ? 'MIN(hidden)' : '0';

$cte = "*PREFIX*cte_folders AS (
return "*PREFIX*cte_folders AS (
SELECT
fileid, ({$CLS_HIDDEN}) AS hidden
FROM
*PREFIX*cte_folders_all
GROUP BY
fileid
)";

return self::bundleCTEs([$this->CTE_FOLDERS_ALL($hidden), $cte]);
}

/**
* CTE to get all archive folders recursively in the given top folders.
*/
protected function CTE_FOLDERS_ARCHIVE(): string
{
$cte = "*PREFIX*cte_folders(fileid) AS (
return "*PREFIX*cte_folders(fileid) AS (
SELECT
cfa.fileid
FROM
Expand All @@ -117,8 +115,46 @@ protected function CTE_FOLDERS_ARCHIVE(): string
INNER JOIN *PREFIX*cte_folders c
ON (f.parent = c.fileid)
)";
}

protected function CTE_SHARED_ALBUM_FILES(): string
{
// Detect database type
$platform = $this->connection->getDatabasePlatform()::class;
$DISTINCT_ON = '';
$GROUP_BY = '';

if (preg_match('/mysql|mariadb/i', $platform)) {
$GROUP_BY = 'GROUP BY paf.file_id';
} elseif (preg_match('/postgres/i', $platform)) {
$DISTINCT_ON = 'DISTINCT ON (paf.file_id)';
} else {
throw new \Exception("Unsupported database detected: {$platform}");
}

return "*PREFIX*cte_shared_album_files(fileid, album_path) AS (
SELECT {$DISTINCT_ON}
paf.file_id AS fileid,
CONCAT(pa.user, '/', pa.name) AS album_path
FROM
*PREFIX*photos_albums_files paf
INNER JOIN
*PREFIX*photos_albums pa ON pa.album_id = paf.album_id
INNER JOIN
*PREFIX*photos_albums_collabs pac ON pac.album_id = paf.album_id
WHERE
pac.collaborator_id = :uid OR pa.user = :uid
{$GROUP_BY}
)";
}

return self::bundleCTEs([$this->CTE_FOLDERS_ALL(true), $cte]);
protected function CTE_SHARED_ALBUM_FILES_EMPTY(): string
{
return '*PREFIX*cte_shared_album_files(fileid, album_path) AS (
SELECT
0 AS fileid,
NULL AS album_path
)';
}

/**
Expand Down
52 changes: 36 additions & 16 deletions lib/Db/TimelineQueryDays.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use OCA\Memories\ClustersBackend;
use OCA\Memories\Exif;
use OCA\Memories\Settings\SystemConfig;
use OCA\Memories\Util;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;

Expand Down Expand Up @@ -146,7 +147,7 @@ public function getDay(
$query->innerJoin('m', 'filecache', 'f', $query->expr()->eq('m.fileid', 'f.fileid'));

// Filter for files in the timeline path
$query = $this->filterFilecache($query, null, $recursive, $archive, $hidden);
$query = $this->filterFilecache($query, null, $recursive, $archive, $hidden, /* sel_src= */ true);

// FETCH all photos in this day
$day = $this->executeQueryWithCTEs($query)->fetchAll();
Expand All @@ -170,14 +171,18 @@ public function executeQueryWithCTEs(IQueryBuilder $query, string $psql = ''): \
$params = $query->getParameters();
$types = $query->getParameterTypes();

// Get SQL
$CTE_SQL = \array_key_exists('cteFoldersArchive', $params)
? $this->CTE_FOLDERS_ARCHIVE()
: $this->CTE_FOLDERS(\array_key_exists('cteIncludeHidden', $params));
// Get CTE SQL
if (\array_key_exists('cteFoldersArchive', $params)) {
$ctes = [$this->CTE_FOLDERS_ALL(true), $this->CTE_FOLDERS_ARCHIVE()];
} else {
$hidden = \array_key_exists('cteIncludeHidden', $params);
$ctes = [$this->CTE_FOLDERS_ALL($hidden), $this->CTE_FOLDERS($hidden)];
}
$ctes[] = \array_key_exists('cteSharedAlbumFiles', $params) ? $this->CTE_SHARED_ALBUM_FILES() : $this->CTE_SHARED_ALBUM_FILES_EMPTY();

// Add WITH clause if needed
if (str_contains($sql, 'cte_folders')) {
$sql = $CTE_SQL.' '.$sql;
if (str_contains($sql, 'cte_folders') || str_contains($sql, 'cte_saf')) {
$sql = $this->bundleCTEs($ctes).' '.$sql;
}

return $this->connection->executeQuery($sql, $params, $types);
Expand All @@ -198,6 +203,7 @@ public function filterFilecache(
bool $recursive = true,
bool $archive = false,
bool $hidden = false,
bool $sel_src = false,
): IQueryBuilder {
// Get the timeline root object
if (null === $root) {
Expand Down Expand Up @@ -241,20 +247,27 @@ public function filterFilecache(
// Filter by folder (recursive or otherwise)
if ($recursive) {
// This are used later by the execution function
$this->addSubfolderJoinParams($query, $root, $archive, $hidden);
$this->addSubfolderJoinParams($query, $root, $archive, $hidden, !$archive && $root->includeSharedAlbums());

// Subquery to test parent folder
$sq = $query->getConnection()->getQueryBuilder();
$sq->select($sq->expr()->literal(1))
->from('cte_folders', 'cte_f')
->where($sq->expr()->eq($parent, 'cte_f.fileid'))
;
// Join to select files in one of the timeline folders
$query->leftJoin('m', 'cte_folders', 'cte_f', $query->expr()->eq($parent, 'cte_f.fileid'));

// Filter files in one of the timeline folders
$query->andWhere(SQL::exists($query, $sq));
// Join to optionally select files in shared albums
$query->leftJoin('m', 'cte_shared_album_files', 'cte_saf', $query->expr()->eq('m.fileid', 'cte_saf.fileid'));
if ($sel_src) {
$query->selectAlias('cte_saf.album_path', 'album_path');
$query->selectAlias('cte_f.fileid', 'timeline_path');
}

// Include either via the timeline paths or shared albums
$query->andWhere($query->expr()->orX($query->expr()->isNotNull('cte_f.fileid'), $query->expr()->isNotNull('cte_saf.album_path')));
} else {
// If getting non-recursively folder only check for parent
$query->andWhere($query->expr()->eq($parent, $query->createNamedParameter($root->getOneId(), IQueryBuilder::PARAM_INT)));
if ($sel_src) {
$query->selectAlias($query->expr()->literal('NULL'), 'album_path');
$query->selectAlias($query->expr()->literal('1'), 'timeline_path');
}
}

return $query;
Expand Down Expand Up @@ -315,6 +328,9 @@ private function postProcessDayPhoto(array &$row, bool $monthView = false): void
unset($row['liveid']);
}

$row['src'] = ($row['timeline_path'] ?? null) ? null : ($row['album_path'] ?? null);
unset($row['album_path'], $row['timeline_path']);

// Favorite field, may not be present
if ($row['categoryid'] ?? null) {
$row['isfavorite'] = 1;
Expand Down Expand Up @@ -355,12 +371,16 @@ private function addSubfolderJoinParams(
TimelineRoot &$root,
bool $archive,
bool $hidden,
bool $shared,
): void {
// Add query parameters
$query->setParameter('topFolderIds', $root->getIds(), IQueryBuilder::PARAM_INT_ARRAY);
$query->setParameter('uid', Util::getUID(), IQueryBuilder::PARAM_STR);

if ($archive) {
$query->setParameter('cteFoldersArchive', true, IQueryBuilder::PARAM_BOOL);
} elseif ($shared) {
$query->setParameter('cteSharedAlbumFiles', true, IQueryBuilder::PARAM_BOOL);
}

if ($hidden) {
Expand Down
12 changes: 12 additions & 0 deletions lib/Db/TimelineRoot.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class TimelineRoot
/** @var array<int, string> */
protected array $folderPaths = [];

protected bool $sharedAlbums = false;

/**
* Add a folder to the root.
*
Expand All @@ -35,6 +37,11 @@ public function addFolder(FileInfo $info): void
$this->setFolder($info->getId() ?? 0, $info, $path);
}

public function addSharedAlbums(bool $include = true): void
{
$this->sharedAlbums = $include;
}

/**
* Add mountpoints recursively.
*/
Expand Down Expand Up @@ -113,6 +120,11 @@ public function isEmpty(): bool
return empty($this->folderPaths);
}

public function includeSharedAlbums(): bool
{
return $this->sharedAlbums;
}

private function setFolder(int $id, ?FileInfo $fileInfo, ?string $path): void
{
if (null !== $path) {
Expand Down
3 changes: 3 additions & 0 deletions lib/Settings/SystemConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ class SystemConfig
// If set to '_empty_', the user is prompted to select a path
'memories.timeline.default_path' => '_empty_',

// Whether to include files accessible via shared albums in the timeline by default
'memories.timeline.default_include_shared_albums' => true,

// Default viewer high resolution image loading condition
// Valid values: 'always' | 'zoom' | 'never'
'memories.viewer.high_res_cond_default' => 'zoom',
Expand Down
12 changes: 12 additions & 0 deletions lib/Util.php
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,18 @@ public static function getTimelinePaths(string $uid): array
);
}

public static function getTimelineIncludeSharedAlbums(string $uid): bool
{
return 'true' === \OC::$server->get(IConfig::class)
->getUserValue(
$uid,
Application::APPNAME,
'timelineIncludeSharedAlbums',
SystemConfig::get('memories.timeline.default_include_shared_albums') ? 'true' : 'false',
)
;
}

/**
* Run a callback in a transaction.
* It returns the same type as the return type of the closure.
Expand Down
18 changes: 12 additions & 6 deletions src/components/SelectionManager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ export default defineComponent({
name: t('memories', 'Delete'),
icon: DeleteIcon,
callback: this.deleteSelection.bind(this),
if: () => !this.routeIsAlbums,
if: () => !this.routeIsAlbums && this.selectionInTimelinePath(),
},
{
name: t('memories', 'Remove from album'),
Expand All @@ -212,7 +212,7 @@ export default defineComponent({
name: t('memories', 'Share'),
icon: ShareIcon,
callback: this.shareSelection.bind(this),
if: () => !this.routeIsAlbums,
if: () => !this.routeIsAlbums && this.selectionInTimelinePath(),
},
{
name: t('memories', 'Download'),
Expand All @@ -230,7 +230,7 @@ export default defineComponent({
name: t('memories', 'Archive'),
icon: ArchiveIcon,
callback: this.archiveSelection.bind(this),
if: () => !this.routeIsArchiveFolder() && !this.routeIsAlbums,
if: () => !this.routeIsArchiveFolder() && !this.routeIsAlbums && this.selectionInTimelinePath(),
},
{
name: t('memories', 'Unarchive'),
Expand All @@ -242,17 +242,19 @@ export default defineComponent({
name: t('memories', 'Edit metadata'),
icon: EditFileIcon,
callback: this.editMetadataSelection.bind(this),
if: () => !this.routeIsAlbums && this.selectionInTimelinePath(),
},
{
name: t('memories', 'Rotate / Flip'),
icon: RotateLeftIcon,
callback: () => this.editMetadataSelection(this.selection, [5]),
if: () => !this.routeIsAlbums && this.selectionInTimelinePath(),
},
{
name: t('memories', 'View in folder'),
icon: OpenInNewIcon,
callback: this.viewInFolder.bind(this),
if: () => this.selection.size === 1 && !this.routeIsAlbums,
if: () => this.selection.size === 1 && !this.routeIsAlbums && this.selectionInTimelinePath(),
},
{
name: t('memories', 'Set as cover image'),
Expand All @@ -264,13 +266,13 @@ export default defineComponent({
name: t('memories', 'Move to folder'),
icon: FolderMoveIcon,
callback: this.moveToFolder.bind(this),
if: () => !this.routeIsAlbums && !this.routeIsArchiveFolder(),
if: () => !this.routeIsAlbums && !this.routeIsArchiveFolder() && this.selectionInTimelinePath(),
},
{
name: t('memories', 'Add to album'),
icon: AlbumsIcon,
callback: this.addToAlbum.bind(this),
if: (self: any) => self.config.albums_enabled && !self.routeIsAlbums,
if: (self: any) => self.config.albums_enabled && !self.routeIsAlbums && this.selectionInTimelinePath(),
},
{
id: 'face-move',
Expand Down Expand Up @@ -335,6 +337,10 @@ export default defineComponent({
return false;
},

selectionInTimelinePath() {
return !Array.from(this.selection.values()).some((photo) => photo.src);
},

/** Trigger to update props from selection set */
selectionChanged() {
this.show = this.selection.size > 0;
Expand Down
12 changes: 12 additions & 0 deletions src/components/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
readonly
/>

<NcCheckboxRadioSwitch
:checked.sync="config.timeline_include_shared_albums"
@update:checked="updateTimelineIncludeSharedAlbums"
type="switch"
>
{{ t('memories', 'Include shared albums in timeline') }}
</NcCheckboxRadioSwitch>

<NcCheckboxRadioSwitch :checked.sync="config.square_thumbs" @update:checked="updateSquareThumbs" type="switch">
{{ t('memories', 'Square grid mode') }}
</NcCheckboxRadioSwitch>
Expand Down Expand Up @@ -310,6 +318,10 @@ export default defineComponent({
},

// General settings
async updateTimelineIncludeSharedAlbums() {
await this.updateSetting('timeline_include_shared_albums', 'timelineIncludeSharedAlbums');
},

async updateSquareThumbs() {
await this.updateSetting('square_thumbs');
},
Expand Down
2 changes: 1 addition & 1 deletion src/components/viewer/Viewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ export default defineComponent({
name: this.t('memories', 'View in folder'),
icon: OpenInNewIcon,
callback: this.viewInFolder,
if: !this.routeIsPublic && !this.routeIsAlbums && !this.isLocal,
if: !this.routeIsPublic && !this.routeIsAlbums && !this.isLocal && !this.currentPhoto?.src,
},
{
id: 'slideshow',
Expand Down
Loading
Loading