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
|
## 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)
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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 TimelineQueryFaces;
|
||||||
use TimelineQueryFilters;
|
use TimelineQueryFilters;
|
||||||
use TimelineQueryTags;
|
use TimelineQueryTags;
|
||||||
|
use TimelineQueryFolders;
|
||||||
|
|
||||||
protected IDBConnection $connection;
|
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);
|
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)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue