Hide folders without photos (fix #163)

pull/175/head
Varun Patil 2022-11-06 20:48:10 -08:00
parent 38475f071b
commit 6f3cb99ddb
8 changed files with 125 additions and 78 deletions

View File

@ -5,8 +5,9 @@ This file is manually updated. Please file an issue if something is missing.
## v4.6.1, v3.6.1 ## v4.6.1, v3.6.1
- **Feature**: Native sharing from the viewer (images only) - **Feature**: Native sharing from the viewer (images only)
- **Feature**: Deep linking to photos - **Feature**: Deep linking to photos on opening viewer
- **Feature**: Password protected folder shares ([#165](https://github.com/pulsejet/memories/issues/165)) - **Feature**: Password protected folder shares ([#165](https://github.com/pulsejet/memories/issues/165))
- **Feature**: Folders view will now show only folders with photos ([#163](https://github.com/pulsejet/memories/issues/163))
- Improvements to viewer UX - Improvements to viewer UX
## v4.6.0, v3.6.0 (2022-11-06) ## v4.6.0, v3.6.0 (2022-11-06)

View File

@ -25,13 +25,16 @@ namespace OCA\Memories\Controller;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
use OCP\Files\FileInfo;
use OCP\Files\Folder; use OCP\Files\Folder;
class DaysController extends ApiBase class DaysController extends ApiBase
{ {
use FoldersTrait;
/** /**
* @NoAdminRequired * @NoAdminRequired
* @NoCSRFRequired
*
* *
* @PublicPage * @PublicPage
*/ */
@ -167,41 +170,6 @@ class DaysController extends ApiBase
return $this->day($id); return $this->day($id);
} }
/**
* Get subfolders entry for days response.
*/
public function getSubfoldersEntry(Folder &$folder)
{
// Ugly: get the view of the folder with reflection
// This is unfortunately the only way to get the contents of a folder
// matching a MIME type without using SEARCH, which is deep
$rp = new \ReflectionProperty('\OC\Files\Node\Node', 'view');
$rp->setAccessible(true);
$view = $rp->getValue($folder);
// Get the subfolders
$folders = $view->getDirectoryContent($folder->getPath(), FileInfo::MIMETYPE_FOLDER, $folder);
// Sort by name
usort($folders, function ($a, $b) {
return strnatcmp($a->getName(), $b->getName());
});
// Process to response type
return [
'dayid' => \OCA\Memories\Util::$TAG_DAYID_FOLDERS,
'count' => \count($folders),
'detail' => array_map(function ($node) {
return [
'fileid' => $node->getId(),
'name' => $node->getName(),
'isfolder' => 1,
'path' => $node->getPath(),
];
}, $folders, []),
];
}
/** /**
* Get transformations depending on the request. * Get transformations depending on the request.
* *

View File

@ -0,0 +1,55 @@
<?php
namespace OCA\Memories\Controller;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
use OCA\Memories\Db\TimelineQuery;
trait FoldersTrait {
protected TimelineQuery $timelineQuery;
private function getFolderPreviews(Folder &$parent, FileInfo &$fileInfo) {
$folder = $parent->getById($fileInfo->getId());
if (count($folder) === 0) {
return [];
}
return $this->timelineQuery->getFolderPreviews($folder[0]);
}
/**
* Get subfolders entry for days response.
*/
public function getSubfoldersEntry(Folder &$folder)
{
// Ugly: get the view of the folder with reflection
// This is unfortunately the only way to get the contents of a folder
// matching a MIME type without using SEARCH, which is deep
$rp = new \ReflectionProperty('\OC\Files\Node\Node', 'view');
$rp->setAccessible(true);
$view = $rp->getValue($folder);
// Get the subfolders
$folders = $view->getDirectoryContent($folder->getPath(), FileInfo::MIMETYPE_FOLDER, $folder);
// Sort by name
usort($folders, function ($a, $b) {
return strnatcmp($a->getName(), $b->getName());
});
// Process to response type
return [
'dayid' => \OCA\Memories\Util::$TAG_DAYID_FOLDERS,
'count' => \count($folders),
'detail' => array_map(function (&$node) use (&$folder) {
return [
'fileid' => $node->getId(),
'name' => $node->getName(),
'isfolder' => 1,
'path' => $node->getPath(),
'previews' => $this->getFolderPreviews($folder, $node),
];
}, $folders, []),
];
}
}

View File

@ -14,6 +14,7 @@ class TimelineQuery
use TimelineQueryFaces; use TimelineQueryFaces;
use TimelineQueryFilters; use TimelineQueryFilters;
use TimelineQueryTags; use TimelineQueryTags;
use TimelineQueryFolders;
protected IDBConnection $connection; protected IDBConnection $connection;

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace OCA\Memories\Db;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\Folder;
use OCP\IDBConnection;
trait TimelineQueryFolders
{
protected IDBConnection $connection;
public function getFolderPreviews(Folder &$folder)
{
$query = $this->connection->getQueryBuilder();
// SELECT all photos
$query->select('f.fileid', 'f.etag')->from('memories', 'm');
// WHERE these photos are in the user's requested folder recursively
$query = $this->joinFilecache($query, $folder, true, false);
// ORDER descending by fileid
$query->orderBy('f.fileid', 'DESC');
// MAX 4
$query->setMaxResults(4);
// FETCH tag previews
$cursor = $this->executeQueryWithCTEs($query);
$ans = $cursor->fetchAll();
// Post-process
foreach ($ans as &$row) {
$row['fileid'] = (int) $row['fileid'];
}
return $ans;
}
}

View File

@ -938,11 +938,14 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
data.forEach(utils.convertFlags); data.forEach(utils.convertFlags);
// Filter out items we don't want to show at all // Filter out items we don't want to show at all
if (!this.config_showHidden) { if (!this.config_showHidden && dayId === this.TagDayID.FOLDERS) {
// Hidden folders // Hidden folders and folders without previews
data = data.filter( data = data.filter(
(p) => (p) =>
!(p.flag & this.c.FLAG_IS_FOLDER && (<IFolder>p).name.startsWith(".")) !(
p.flag & this.c.FLAG_IS_FOLDER &&
((<IFolder>p).name.startsWith(".") || !(<IFolder>p).previews.length)
)
); );
} }

View File

@ -3,8 +3,8 @@
draggable="false" draggable="false"
class="folder fill-block" class="folder fill-block"
:class="{ :class="{
hasPreview: previewFileInfos.length > 0, hasPreview: previews.length > 0,
onePreview: previewFileInfos.length === 1, onePreview: previews.length === 1,
hasError: error, hasError: error,
}" }"
:to="target" :to="target"
@ -15,17 +15,11 @@
</div> </div>
<div class="previews fill-block"> <div class="previews fill-block">
<div <div class="img-outer" v-for="info of previews" :key="info.fileid">
class="img-outer"
v-for="info of previewFileInfos"
:key="info.fileid"
>
<img <img
class="fill-block" class="fill-block"
:class="{ error: info.flag & c.FLAG_LOAD_FAIL }"
:key="'fpreview-' + info.fileid"
:src="getPreviewUrl(info, true, 256)" :src="getPreviewUrl(info, true, 256)"
@error="info.flag |= c.FLAG_LOAD_FAIL" @error="$event.target.classList.add('error')"
/> />
</div> </div>
</div> </div>
@ -34,11 +28,10 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Watch, Mixins } from "vue-property-decorator"; import { Component, Prop, Watch, Mixins } from "vue-property-decorator";
import { IFileInfo, IFolder } from "../../types"; import { IFolder, IPhoto } from "../../types";
import GlobalMixin from "../../mixins/GlobalMixin"; import GlobalMixin from "../../mixins/GlobalMixin";
import UserConfig from "../../mixins/UserConfig"; import UserConfig from "../../mixins/UserConfig";
import * as dav from "../../services/DavRequests";
import { getPreviewUrl } from "../../services/FileUtils"; import { getPreviewUrl } from "../../services/FileUtils";
import FolderIcon from "vue-material-design-icons/Folder.vue"; import FolderIcon from "vue-material-design-icons/Folder.vue";
@ -52,7 +45,7 @@ export default class Folder extends Mixins(GlobalMixin, UserConfig) {
@Prop() data: IFolder; @Prop() data: IFolder;
// Separate property because the one on data isn't reactive // Separate property because the one on data isn't reactive
private previewFileInfos: IFileInfo[] = []; private previews: IPhoto[] = [];
// Error occured fetching thumbs // Error occured fetching thumbs
private error = false; private error = false;
@ -81,29 +74,13 @@ export default class Folder extends Mixins(GlobalMixin, UserConfig) {
} }
// Get preview infos // Get preview infos
if (!this.data.previewFileInfos) { const previews = this.data.previews;
const folderPath = this.data.path.split("/").slice(3).join("/"); if (previews) {
dav if (previews.length > 0 && previews.length < 4) {
.getFolderPreviewFileIds(folderPath, 4) this.previews = [previews[0]];
.then((fileInfos) => {
fileInfos = fileInfos.filter((f) => f.hasPreview);
fileInfos.forEach((f) => (f.flag = 0));
if (fileInfos.length > 0 && fileInfos.length < 4) {
fileInfos = [fileInfos[0]];
}
this.data.previewFileInfos = fileInfos;
this.previewFileInfos = fileInfos;
})
.catch(() => {
this.data.previewFileInfos = [];
this.previewFileInfos = [];
// Something is wrong with the folder
// e.g. external storage not available
this.error = true;
});
} else { } else {
this.previewFileInfos = this.data.previewFileInfos; this.previews = previews.slice(0, 4);
}
} }
} }

View File

@ -92,8 +92,8 @@ export type IPhoto = {
export interface IFolder extends IPhoto { export interface IFolder extends IPhoto {
/** Path to folder */ /** Path to folder */
path: string; path: string;
/** FileInfos for preview images */ /** Photos for preview images */
previewFileInfos?: IFileInfo[]; previews?: IPhoto[];
/** Name of folder */ /** Name of folder */
name: string; name: string;
} }