refactor: separate folder logic

Signed-off-by: Varun Patil <varunpatil@ucla.edu>
pull/563/head
Varun Patil 2023-03-24 15:53:26 -07:00
parent 37a108c2fc
commit eb3c834241
16 changed files with 189 additions and 168 deletions

View File

@ -51,6 +51,7 @@ return [
['name' => 'Days#days', 'url' => '/api/days', 'verb' => 'GET'], ['name' => 'Days#days', 'url' => '/api/days', 'verb' => 'GET'],
['name' => 'Days#day', 'url' => '/api/days/{id}', 'verb' => 'GET'], ['name' => 'Days#day', 'url' => '/api/days/{id}', 'verb' => 'GET'],
['name' => 'Days#dayPost', 'url' => '/api/days', 'verb' => 'POST'], ['name' => 'Days#dayPost', 'url' => '/api/days', 'verb' => 'POST'],
['name' => 'Folders#sub', 'url' => '/api/folders/sub', 'verb' => 'GET'],
['name' => 'Clusters#list', 'url' => '/api/clusters/{backend}', 'verb' => 'GET'], ['name' => 'Clusters#list', 'url' => '/api/clusters/{backend}', 'verb' => 'GET'],
['name' => 'Clusters#preview', 'url' => '/api/clusters/{backend}/preview/{name}', 'verb' => 'GET'], ['name' => 'Clusters#preview', 'url' => '/api/clusters/{backend}/preview/{name}', 'verb' => 'GET'],

View File

@ -31,8 +31,6 @@ use OCP\AppFramework\Http\JSONResponse;
class DaysController extends GenericApiController class DaysController extends GenericApiController
{ {
use FoldersTrait;
/** /**
* @NoAdminRequired * @NoAdminRequired
* *
@ -60,12 +58,6 @@ class DaysController extends GenericApiController
$list = array_reverse($list); $list = array_reverse($list);
} }
// Add subfolder info if querying non-recursively
if (!$this->isRecursive()) {
$root = $this->timelineQuery->root();
array_unshift($list, $this->getSubfoldersEntry($root->getFolder($root->getOneId())));
}
return new JSONResponse($list, Http::STATUS_OK); return new JSONResponse($list, Http::STATUS_OK);
}); });
} }

View File

@ -0,0 +1,56 @@
<?php
namespace OCA\Memories\Controller;
use OCA\Memories\Db\TimelineQuery;
use OCA\Memories\Exceptions;
use OCA\Memories\Util;
use OCP\AppFramework\Http;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
class FoldersController extends GenericApiController
{
protected TimelineQuery $timelineQuery;
/**
* @NoAdminRequired
*/
public function sub(string $folder): Http\Response
{
return Util::guardEx(function () use ($folder) {
try {
$node = Util::getUserFolder()->get($folder);
} catch (\OCP\Files\NotFoundException $e) {
throw Exceptions::NotFound('Folder not found');
}
if (!$node instanceof Folder) {
throw Exceptions::NotFound('Path is not a 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($node);
// Get the subfolders
$folders = $view->getDirectoryContent($node->getPath(), FileInfo::MIMETYPE_FOLDER, $node);
// Sort by name
usort($folders, fn ($a, $b) => strnatcmp($a->getName(), $b->getName()));
// Process to response type
$list = array_map(fn ($node) => [
'fileid' => $node->getId(),
'name' => $node->getName(),
'path' => $node->getPath(),
'previews' => $this->timelineQuery->getFolderPreviews($node),
], $folders);
return new Http\JSONResponse($list, Http::STATUS_OK);
});
}
}

View File

@ -1,56 +0,0 @@
<?php
namespace OCA\Memories\Controller;
use OCA\Memories\Db\TimelineQuery;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
trait FoldersTrait
{
protected TimelineQuery $timelineQuery;
/**
* 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, fn ($a, $b) => 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, []),
];
}
private function getFolderPreviews(Folder &$parent, FileInfo &$fileInfo)
{
$folder = $parent->getById($fileInfo->getId());
if (0 === \count($folder)) {
return [];
}
return $this->timelineQuery->getFolderPreviews($folder[0]);
}
}

View File

@ -4,14 +4,14 @@ declare(strict_types=1);
namespace OCA\Memories\Db; namespace OCA\Memories\Db;
use OCP\Files\Folder; use OCP\Files\FileInfo;
use OCP\IDBConnection; use OCP\IDBConnection;
trait TimelineQueryFolders trait TimelineQueryFolders
{ {
protected IDBConnection $connection; protected IDBConnection $connection;
public function getFolderPreviews(Folder &$folder) public function getFolderPreviews(FileInfo $folder)
{ {
$query = $this->connection->getQueryBuilder(); $query = $this->connection->getQueryBuilder();

View File

@ -4,8 +4,7 @@ declare(strict_types=1);
namespace OCA\Memories\Db; namespace OCA\Memories\Db;
use OCP\Files\Folder; use OCP\Files\FileInfo;
use OCP\Files\Node;
class TimelineRoot class TimelineRoot
{ {
@ -28,25 +27,23 @@ class TimelineRoot
/** /**
* Add a folder to the root. * Add a folder to the root.
* *
* @param Node $folder Node to add
*
* @throws \Exception if node is not valid readable folder * @throws \Exception if node is not valid readable folder
*/ */
public function addFolder(Node &$folder) public function addFolder(FileInfo $info)
{ {
$folderPath = $folder->getPath(); $folderPath = $info->getPath();
if (!$folder instanceof Folder) { if (FileInfo::MIMETYPE_FOLDER !== $info->getMimetype()) {
throw new \Exception("Not a folder: {$folderPath}"); throw new \Exception("Not a folder: {$folderPath}");
} }
if (!$folder->isReadable()) { if (!$info->isReadable()) {
throw new \Exception("Folder not readable: {$folderPath}"); throw new \Exception("Folder not readable: {$folderPath}");
} }
// Add top level folder // Add top level folder
$id = $folder->getId(); $id = $info->getId();
$this->folders[$id] = $folder; $this->folders[$id] = $info;
$this->folderPaths[$id] = $folderPath; $this->folderPaths[$id] = $folderPath;
} }

View File

@ -18,9 +18,6 @@ class Util
{ {
use UtilController; use UtilController;
public static $TAG_DAYID_START = -(1 << 30); // the world surely didn't exist
public static $TAG_DAYID_FOLDERS = -(1 << 30) + 1;
public static $ARCHIVE_FOLDER = '.archive'; public static $ARCHIVE_FOLDER = '.archive';
/** /**

View File

@ -0,0 +1,90 @@
<template>
<div class="grid" v-if="items.length">
<div class="grid-item fill-block" v-for="item of items" :key="item.fileid">
<Folder :data="item" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import axios from "@nextcloud/axios";
import UserConfig from "../mixins/UserConfig";
import Folder from "./frame/Folder.vue";
import * as utils from "../services/Utils";
import { IFolder } from "../types";
import { API } from "../services/API";
export default defineComponent({
name: "ClusterGrid",
components: {
Folder,
},
mixins: [UserConfig],
data: () => ({
items: [] as IFolder[],
path: "",
}),
mounted() {
this.refresh();
},
watch: {
$route() {
this.items = [];
this.refresh();
},
config_showHidden() {
this.refresh();
},
},
methods: {
async refresh() {
// Get folder path
const folder = (this.path = utils.getFolderRoutePath(
this.config_foldersPath
));
// Get subfolders for this folder
const res = await axios.get<IFolder[]>(
API.Q(API.FOLDERS_SUB(), { folder })
);
if (folder !== this.path) return;
this.items = res.data;
this.$emit("load", this.items);
// Filter out hidden folders
if (!this.config_showHidden) {
this.items = this.items.filter(
(f) => !f.name.startsWith(".") && f.previews.length
);
}
},
},
});
</script>
<style lang="scss" scoped>
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100% / 3, 165px), 1fr));
width: calc(100% - 40px); // leave space for scroller
@media (max-width: 768px) {
width: calc(100% - 2px); // compensation for negative margin
}
.grid-item {
aspect-ratio: 1 / 1;
position: relative;
}
}
</style>

View File

@ -242,24 +242,18 @@ export default defineComponent({
// Iterate over rows // Iterate over rows
for (const row of this.rows) { for (const row of this.rows) {
if (row.type === IRowType.HEAD) { if (row.type === IRowType.HEAD) {
// Make date string
const dateTaken = utils.dayIdToDate(row.dayId);
// Create tick // Create tick
if (this.TagDayIDValueSet.has(row.dayId)) { const dtYear = dateTaken.getUTCFullYear();
// Blank tick const dtMonth = dateTaken.getUTCMonth();
this.ticks.push(getTick(row.dayId)); const isMonth = dtMonth !== prevMonth || dtYear !== prevYear;
} else { const text = dtYear === prevYear ? undefined : dtYear;
// Make date string this.ticks.push(getTick(row.dayId, isMonth, text));
const dateTaken = utils.dayIdToDate(row.dayId);
// Create tick prevMonth = dtMonth;
const dtYear = dateTaken.getUTCFullYear(); prevYear = dtYear;
const dtMonth = dateTaken.getUTCMonth();
const isMonth = dtMonth !== prevMonth || dtYear !== prevYear;
const text = dtYear === prevYear ? undefined : dtYear;
this.ticks.push(getTick(row.dayId, isMonth, text));
prevMonth = dtMonth;
prevYear = dtYear;
}
} }
} }
}, },
@ -417,7 +411,7 @@ export default defineComponent({
const dayId = this.ticks[idx]?.dayId; const dayId = this.ticks[idx]?.dayId;
// Special days // Special days
if (dayId === undefined || this.TagDayIDValueSet.has(dayId)) { if (dayId === undefined) {
this.hoverCursorText = ""; this.hoverCursorText = "";
return; return;
} }
@ -538,6 +532,7 @@ export default defineComponent({
width: 36px; width: 36px;
top: 0; top: 0;
right: 0; right: 0;
z-index: 100;
cursor: ns-resize; cursor: ns-resize;
opacity: 0; opacity: 0;
transition: opacity 0.2s ease-in-out; transition: opacity 0.2s ease-in-out;

View File

@ -526,10 +526,7 @@ export default defineComponent({
/** Add a photo to selection list */ /** Add a photo to selection list */
selectPhoto(photo: IPhoto, val?: boolean, noUpdate?: boolean) { selectPhoto(photo: IPhoto, val?: boolean, noUpdate?: boolean) {
if ( if (photo.flag & this.c.FLAG_PLACEHOLDER) {
photo.flag & this.c.FLAG_PLACEHOLDER ||
photo.flag & this.c.FLAG_IS_FOLDER
) {
return; // ignore placeholders return; // ignore placeholders
} }

View File

@ -36,6 +36,11 @@
@load="scrollerManager.adjust()" @load="scrollerManager.adjust()"
> >
</OnThisDay> </OnThisDay>
<FolderGrid
v-if="routeIsFolders && !$route.query.recursive"
@load="scrollerManager.adjust()"
/>
</div> </div>
</template> </template>
@ -67,10 +72,7 @@
transform: `translate(${photo.dispX}px, ${photo.dispY}px`, transform: `translate(${photo.dispX}px, ${photo.dispY}px`,
}" }"
> >
<Folder v-if="photo.flag & c.FLAG_IS_FOLDER" :data="photo" />
<Photo <Photo
v-else
:data="photo" :data="photo"
:day="item.day" :day="item.day"
@select="selectionManager.selectPhoto" @select="selectionManager.selectPhoto"
@ -123,10 +125,10 @@ import { showError } from "@nextcloud/dialogs";
import { subscribe, unsubscribe } from "@nextcloud/event-bus"; import { subscribe, unsubscribe } from "@nextcloud/event-bus";
import { getLayout } from "../services/Layout"; import { getLayout } from "../services/Layout";
import { IDay, IFolder, IHeadRow, IPhoto, IRow, IRowType } from "../types"; import { IDay, IHeadRow, IPhoto, IRow, IRowType } from "../types";
import UserConfig from "../mixins/UserConfig"; import UserConfig from "../mixins/UserConfig";
import Folder from "./frame/Folder.vue"; import FolderGrid from "./FolderGrid.vue";
import Photo from "./frame/Photo.vue"; import Photo from "./frame/Photo.vue";
import ScrollerManager from "./ScrollerManager.vue"; import ScrollerManager from "./ScrollerManager.vue";
import SelectionManager from "./SelectionManager.vue"; import SelectionManager from "./SelectionManager.vue";
@ -151,7 +153,7 @@ export default defineComponent({
name: "Timeline", name: "Timeline",
components: { components: {
Folder, FolderGrid,
Photo, Photo,
EmptyContent, EmptyContent,
OnThisDay, OnThisDay,
@ -244,6 +246,9 @@ export default defineComponent({
routeIsArchive(): boolean { routeIsArchive(): boolean {
return this.$route.name === "archive"; return this.$route.name === "archive";
}, },
routeIsFolders(): boolean {
return this.$route.name === "folders";
},
isMonthView(): boolean { isMonthView(): boolean {
if (this.$route.query.sort === "timeline") return false; if (this.$route.query.sort === "timeline") return false;
@ -635,11 +640,6 @@ export default defineComponent({
return head.name; return head.name;
} }
// Special headers
if (this.TagDayIDValueSet.has(head.dayId)) {
return (head.name = "");
}
// Make date string // Make date string
// The reason this function is separate from processDays is // The reason this function is separate from processDays is
// because this call is terribly slow even on desktop // because this call is terribly slow even on desktop
@ -756,9 +756,7 @@ export default defineComponent({
}; };
// Special headers // Special headers
if (this.TagDayIDValueSet.has(day.dayid)) { if (
head.size = 10;
} else if (
this.$route.name === "thisday" && this.$route.name === "thisday" &&
(!prevDay || Math.abs(prevDay.dayid - day.dayid) > 30) (!prevDay || Math.abs(prevDay.dayid - day.dayid) > 30)
) { ) {
@ -939,19 +937,6 @@ export default defineComponent({
// Convert server flags to bitflags // Convert server flags to bitflags
data.forEach(utils.convertFlags); data.forEach(utils.convertFlags);
// Filter out items we don't want to show at all
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(".") ||
!(<IFolder>p).previews.length)
)
);
}
// Set and make reactive // Set and make reactive
day.count = data.length; day.count = data.length;
day.detail = data; day.detail = data;
@ -970,7 +955,7 @@ export default defineComponent({
return { return {
width: p.w || this.rowHeight, width: p.w || this.rowHeight,
height: p.h || this.rowHeight, height: p.h || this.rowHeight,
forceSquare: Boolean(p.flag & this.c.FLAG_IS_FOLDER), forceSquare: false,
}; };
}), }),
{ {
@ -1167,17 +1152,12 @@ export default defineComponent({
/** Add and get a new blank photos row */ /** Add and get a new blank photos row */
addRow(day: IDay): IRow { addRow(day: IDay): IRow {
let rowType = IRowType.PHOTOS;
if (day.dayid === this.TagDayID.FOLDERS) {
rowType = IRowType.FOLDERS;
}
// Create new row // Create new row
const row = { const row = {
id: `${day.dayid}-${day.rows.length}`, id: `${day.dayid}-${day.rows.length}`,
num: day.rows.length, num: day.rows.length,
photos: [], photos: [],
type: rowType, type: IRowType.PHOTOS,
size: this.rowHeight, size: this.rowHeight,
dayId: day.dayid, dayId: day.dayid,
day: day, day: day,

View File

@ -631,8 +631,6 @@ export default defineComponent({
// Get days list and map // Get days list and map
for (const r of rows) { for (const r of rows) {
if (r.type === IRowType.HEAD) { if (r.type === IRowType.HEAD) {
if (this.TagDayIDValueSet.has(r.dayId)) continue;
if (r.day.dayid == anchorPhoto.d.dayid) { if (r.day.dayid == anchorPhoto.d.dayid) {
startIndex = r.day.detail.indexOf(anchorPhoto); startIndex = r.day.detail.indexOf(anchorPhoto);
this.globalAnchor = this.globalCount; this.globalAnchor = this.globalCount;

View File

@ -70,6 +70,10 @@ export class API {
query[filter] = value; query[filter] = value;
} }
static FOLDERS_SUB() {
return tok(gen(`${BASE}/folders/sub`));
}
static ALBUM_LIST(t: 1 | 2 | 3 = 3) { static ALBUM_LIST(t: 1 | 2 | 3 = 3) {
return gen(`${BASE}/clusters/albums?t=${t}`); return gen(`${BASE}/clusters/albums?t=${t}`);
} }

View File

@ -208,10 +208,6 @@ export function convertFlags(photo: IPhoto) {
photo.flag |= constants.c.FLAG_IS_FAVORITE; photo.flag |= constants.c.FLAG_IS_FAVORITE;
delete photo.isfavorite; delete photo.isfavorite;
} }
if (photo.isfolder) {
photo.flag |= constants.c.FLAG_IS_FOLDER;
delete photo.isfolder;
}
} }
/** /**
@ -287,15 +283,6 @@ export function setRenewingTimeout(
}, delay); }, delay);
} }
// Outside for set
const TagDayID = {
START: -(1 << 30),
FOLDERS: -(1 << 30) + 1,
TAGS: -(1 << 30) + 2,
FACES: -(1 << 30) + 3,
ALBUMS: -(1 << 30) + 4,
};
/** Global constants */ /** Global constants */
export const constants = { export const constants = {
c: { c: {
@ -303,13 +290,9 @@ export const constants = {
FLAG_LOAD_FAIL: 1 << 1, FLAG_LOAD_FAIL: 1 << 1,
FLAG_IS_VIDEO: 1 << 2, FLAG_IS_VIDEO: 1 << 2,
FLAG_IS_FAVORITE: 1 << 3, FLAG_IS_FAVORITE: 1 << 3,
FLAG_IS_FOLDER: 1 << 4, FLAG_SELECTED: 1 << 4,
FLAG_SELECTED: 1 << 5, FLAG_LEAVING: 1 << 5,
FLAG_LEAVING: 1 << 6,
}, },
TagDayID: TagDayID,
TagDayIDValueSet: new Set(Object.values(TagDayID)),
}; };
/** Cache store */ /** Cache store */

View File

@ -71,16 +71,6 @@ export type IPhoto = {
video_duration?: number; video_duration?: number;
/** Favorite flag from server */ /** Favorite flag from server */
isfavorite?: boolean; isfavorite?: boolean;
/** Is this a folder */
isfolder?: boolean;
/** Is this a tag */
istag?: boolean;
/** Is this an album */
isalbum?: boolean;
/** Is this a face */
isface?: "recognize" | "facerecognition";
/** Is this a place */
isplace?: boolean;
/** Optional datetaken epoch */ /** Optional datetaken epoch */
datetaken?: number; datetaken?: number;
}; };
@ -196,7 +186,6 @@ export type IHeadRow = IRow & {
export enum IRowType { export enum IRowType {
HEAD = 0, HEAD = 0,
PHOTOS = 1, PHOTOS = 1,
FOLDERS = 2,
} }
export type ITick = { export type ITick = {

View File

@ -8,8 +8,6 @@ declare module "vue" {
n: typeof n; n: typeof n;
c: typeof constants.c; c: typeof constants.c;
TagDayID: typeof constants.TagDayID;
TagDayIDValueSet: typeof constants.TagDayIDValueSet;
state_noDownload: boolean; state_noDownload: boolean;