Hide folders without photos (fix #163)
parent
38475f071b
commit
6f3cb99ddb
|
@ -5,8 +5,9 @@ This file is manually updated. Please file an issue if something is missing.
|
|||
## v4.6.1, v3.6.1
|
||||
|
||||
- **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**: Folders view will now show only folders with photos ([#163](https://github.com/pulsejet/memories/issues/163))
|
||||
- Improvements to viewer UX
|
||||
|
||||
## v4.6.0, v3.6.0 (2022-11-06)
|
||||
|
|
|
@ -25,13 +25,16 @@ namespace OCA\Memories\Controller;
|
|||
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\Files\FileInfo;
|
||||
use OCP\Files\Folder;
|
||||
|
||||
class DaysController extends ApiBase
|
||||
{
|
||||
use FoldersTrait;
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @NoCSRFRequired
|
||||
*
|
||||
*
|
||||
* @PublicPage
|
||||
*/
|
||||
|
@ -167,41 +170,6 @@ class DaysController extends ApiBase
|
|||
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.
|
||||
*
|
||||
|
|
|
@ -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, []),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ class TimelineQuery
|
|||
use TimelineQueryFaces;
|
||||
use TimelineQueryFilters;
|
||||
use TimelineQueryTags;
|
||||
use TimelineQueryFolders;
|
||||
|
||||
protected IDBConnection $connection;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -938,11 +938,14 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
|||
data.forEach(utils.convertFlags);
|
||||
|
||||
// Filter out items we don't want to show at all
|
||||
if (!this.config_showHidden) {
|
||||
// Hidden folders
|
||||
if (!this.config_showHidden && dayId === this.TagDayID.FOLDERS) {
|
||||
// Hidden folders and folders without previews
|
||||
data = data.filter(
|
||||
(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)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
draggable="false"
|
||||
class="folder fill-block"
|
||||
:class="{
|
||||
hasPreview: previewFileInfos.length > 0,
|
||||
onePreview: previewFileInfos.length === 1,
|
||||
hasPreview: previews.length > 0,
|
||||
onePreview: previews.length === 1,
|
||||
hasError: error,
|
||||
}"
|
||||
:to="target"
|
||||
|
@ -15,17 +15,11 @@
|
|||
</div>
|
||||
|
||||
<div class="previews fill-block">
|
||||
<div
|
||||
class="img-outer"
|
||||
v-for="info of previewFileInfos"
|
||||
:key="info.fileid"
|
||||
>
|
||||
<div class="img-outer" v-for="info of previews" :key="info.fileid">
|
||||
<img
|
||||
class="fill-block"
|
||||
:class="{ error: info.flag & c.FLAG_LOAD_FAIL }"
|
||||
:key="'fpreview-' + info.fileid"
|
||||
:src="getPreviewUrl(info, true, 256)"
|
||||
@error="info.flag |= c.FLAG_LOAD_FAIL"
|
||||
@error="$event.target.classList.add('error')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -34,11 +28,10 @@
|
|||
|
||||
<script lang="ts">
|
||||
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 UserConfig from "../../mixins/UserConfig";
|
||||
|
||||
import * as dav from "../../services/DavRequests";
|
||||
import { getPreviewUrl } from "../../services/FileUtils";
|
||||
|
||||
import FolderIcon from "vue-material-design-icons/Folder.vue";
|
||||
|
@ -52,7 +45,7 @@ export default class Folder extends Mixins(GlobalMixin, UserConfig) {
|
|||
@Prop() data: IFolder;
|
||||
|
||||
// Separate property because the one on data isn't reactive
|
||||
private previewFileInfos: IFileInfo[] = [];
|
||||
private previews: IPhoto[] = [];
|
||||
|
||||
// Error occured fetching thumbs
|
||||
private error = false;
|
||||
|
@ -81,29 +74,13 @@ export default class Folder extends Mixins(GlobalMixin, UserConfig) {
|
|||
}
|
||||
|
||||
// Get preview infos
|
||||
if (!this.data.previewFileInfos) {
|
||||
const folderPath = this.data.path.split("/").slice(3).join("/");
|
||||
dav
|
||||
.getFolderPreviewFileIds(folderPath, 4)
|
||||
.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;
|
||||
});
|
||||
const previews = this.data.previews;
|
||||
if (previews) {
|
||||
if (previews.length > 0 && previews.length < 4) {
|
||||
this.previews = [previews[0]];
|
||||
} else {
|
||||
this.previewFileInfos = this.data.previewFileInfos;
|
||||
this.previews = previews.slice(0, 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -92,8 +92,8 @@ export type IPhoto = {
|
|||
export interface IFolder extends IPhoto {
|
||||
/** Path to folder */
|
||||
path: string;
|
||||
/** FileInfos for preview images */
|
||||
previewFileInfos?: IFileInfo[];
|
||||
/** Photos for preview images */
|
||||
previews?: IPhoto[];
|
||||
/** Name of folder */
|
||||
name: string;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue