refactor: separate folder logic
Signed-off-by: Varun Patil <varunpatil@ucla.edu>pull/563/head
parent
37a108c2fc
commit
eb3c834241
|
@ -51,6 +51,7 @@ return [
|
|||
['name' => 'Days#days', 'url' => '/api/days', 'verb' => 'GET'],
|
||||
['name' => 'Days#day', 'url' => '/api/days/{id}', 'verb' => 'GET'],
|
||||
['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#preview', 'url' => '/api/clusters/{backend}/preview/{name}', 'verb' => 'GET'],
|
||||
|
|
|
@ -31,8 +31,6 @@ use OCP\AppFramework\Http\JSONResponse;
|
|||
|
||||
class DaysController extends GenericApiController
|
||||
{
|
||||
use FoldersTrait;
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
|
@ -60,12 +58,6 @@ class DaysController extends GenericApiController
|
|||
$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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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]);
|
||||
}
|
||||
}
|
|
@ -4,14 +4,14 @@ declare(strict_types=1);
|
|||
|
||||
namespace OCA\Memories\Db;
|
||||
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\FileInfo;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
trait TimelineQueryFolders
|
||||
{
|
||||
protected IDBConnection $connection;
|
||||
|
||||
public function getFolderPreviews(Folder &$folder)
|
||||
public function getFolderPreviews(FileInfo $folder)
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
|
||||
|
|
|
@ -4,8 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace OCA\Memories\Db;
|
||||
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\Node;
|
||||
use OCP\Files\FileInfo;
|
||||
|
||||
class TimelineRoot
|
||||
{
|
||||
|
@ -28,25 +27,23 @@ class TimelineRoot
|
|||
/**
|
||||
* Add a folder to the root.
|
||||
*
|
||||
* @param Node $folder Node to add
|
||||
*
|
||||
* @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}");
|
||||
}
|
||||
|
||||
if (!$folder->isReadable()) {
|
||||
if (!$info->isReadable()) {
|
||||
throw new \Exception("Folder not readable: {$folderPath}");
|
||||
}
|
||||
|
||||
// Add top level folder
|
||||
$id = $folder->getId();
|
||||
$this->folders[$id] = $folder;
|
||||
$id = $info->getId();
|
||||
$this->folders[$id] = $info;
|
||||
$this->folderPaths[$id] = $folderPath;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,9 +18,6 @@ class Util
|
|||
{
|
||||
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';
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>
|
|
@ -242,24 +242,18 @@ export default defineComponent({
|
|||
// Iterate over rows
|
||||
for (const row of this.rows) {
|
||||
if (row.type === IRowType.HEAD) {
|
||||
// Make date string
|
||||
const dateTaken = utils.dayIdToDate(row.dayId);
|
||||
|
||||
// Create tick
|
||||
if (this.TagDayIDValueSet.has(row.dayId)) {
|
||||
// Blank tick
|
||||
this.ticks.push(getTick(row.dayId));
|
||||
} else {
|
||||
// Make date string
|
||||
const dateTaken = utils.dayIdToDate(row.dayId);
|
||||
const dtYear = dateTaken.getUTCFullYear();
|
||||
const dtMonth = dateTaken.getUTCMonth();
|
||||
const isMonth = dtMonth !== prevMonth || dtYear !== prevYear;
|
||||
const text = dtYear === prevYear ? undefined : dtYear;
|
||||
this.ticks.push(getTick(row.dayId, isMonth, text));
|
||||
|
||||
// Create tick
|
||||
const dtYear = dateTaken.getUTCFullYear();
|
||||
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;
|
||||
}
|
||||
prevMonth = dtMonth;
|
||||
prevYear = dtYear;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -417,7 +411,7 @@ export default defineComponent({
|
|||
const dayId = this.ticks[idx]?.dayId;
|
||||
|
||||
// Special days
|
||||
if (dayId === undefined || this.TagDayIDValueSet.has(dayId)) {
|
||||
if (dayId === undefined) {
|
||||
this.hoverCursorText = "";
|
||||
return;
|
||||
}
|
||||
|
@ -538,6 +532,7 @@ export default defineComponent({
|
|||
width: 36px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
cursor: ns-resize;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
|
|
|
@ -526,10 +526,7 @@ export default defineComponent({
|
|||
|
||||
/** Add a photo to selection list */
|
||||
selectPhoto(photo: IPhoto, val?: boolean, noUpdate?: boolean) {
|
||||
if (
|
||||
photo.flag & this.c.FLAG_PLACEHOLDER ||
|
||||
photo.flag & this.c.FLAG_IS_FOLDER
|
||||
) {
|
||||
if (photo.flag & this.c.FLAG_PLACEHOLDER) {
|
||||
return; // ignore placeholders
|
||||
}
|
||||
|
||||
|
|
|
@ -36,6 +36,11 @@
|
|||
@load="scrollerManager.adjust()"
|
||||
>
|
||||
</OnThisDay>
|
||||
|
||||
<FolderGrid
|
||||
v-if="routeIsFolders && !$route.query.recursive"
|
||||
@load="scrollerManager.adjust()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -67,10 +72,7 @@
|
|||
transform: `translate(${photo.dispX}px, ${photo.dispY}px`,
|
||||
}"
|
||||
>
|
||||
<Folder v-if="photo.flag & c.FLAG_IS_FOLDER" :data="photo" />
|
||||
|
||||
<Photo
|
||||
v-else
|
||||
:data="photo"
|
||||
:day="item.day"
|
||||
@select="selectionManager.selectPhoto"
|
||||
|
@ -123,10 +125,10 @@ import { showError } from "@nextcloud/dialogs";
|
|||
import { subscribe, unsubscribe } from "@nextcloud/event-bus";
|
||||
|
||||
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 Folder from "./frame/Folder.vue";
|
||||
import FolderGrid from "./FolderGrid.vue";
|
||||
import Photo from "./frame/Photo.vue";
|
||||
import ScrollerManager from "./ScrollerManager.vue";
|
||||
import SelectionManager from "./SelectionManager.vue";
|
||||
|
@ -151,7 +153,7 @@ export default defineComponent({
|
|||
name: "Timeline",
|
||||
|
||||
components: {
|
||||
Folder,
|
||||
FolderGrid,
|
||||
Photo,
|
||||
EmptyContent,
|
||||
OnThisDay,
|
||||
|
@ -244,6 +246,9 @@ export default defineComponent({
|
|||
routeIsArchive(): boolean {
|
||||
return this.$route.name === "archive";
|
||||
},
|
||||
routeIsFolders(): boolean {
|
||||
return this.$route.name === "folders";
|
||||
},
|
||||
isMonthView(): boolean {
|
||||
if (this.$route.query.sort === "timeline") return false;
|
||||
|
||||
|
@ -635,11 +640,6 @@ export default defineComponent({
|
|||
return head.name;
|
||||
}
|
||||
|
||||
// Special headers
|
||||
if (this.TagDayIDValueSet.has(head.dayId)) {
|
||||
return (head.name = "");
|
||||
}
|
||||
|
||||
// Make date string
|
||||
// The reason this function is separate from processDays is
|
||||
// because this call is terribly slow even on desktop
|
||||
|
@ -756,9 +756,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
// Special headers
|
||||
if (this.TagDayIDValueSet.has(day.dayid)) {
|
||||
head.size = 10;
|
||||
} else if (
|
||||
if (
|
||||
this.$route.name === "thisday" &&
|
||||
(!prevDay || Math.abs(prevDay.dayid - day.dayid) > 30)
|
||||
) {
|
||||
|
@ -939,19 +937,6 @@ export default defineComponent({
|
|||
// Convert server flags to bitflags
|
||||
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
|
||||
day.count = data.length;
|
||||
day.detail = data;
|
||||
|
@ -970,7 +955,7 @@ export default defineComponent({
|
|||
return {
|
||||
width: p.w || 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 */
|
||||
addRow(day: IDay): IRow {
|
||||
let rowType = IRowType.PHOTOS;
|
||||
if (day.dayid === this.TagDayID.FOLDERS) {
|
||||
rowType = IRowType.FOLDERS;
|
||||
}
|
||||
|
||||
// Create new row
|
||||
const row = {
|
||||
id: `${day.dayid}-${day.rows.length}`,
|
||||
num: day.rows.length,
|
||||
photos: [],
|
||||
type: rowType,
|
||||
type: IRowType.PHOTOS,
|
||||
size: this.rowHeight,
|
||||
dayId: day.dayid,
|
||||
day: day,
|
||||
|
|
|
@ -631,8 +631,6 @@ export default defineComponent({
|
|||
// Get days list and map
|
||||
for (const r of rows) {
|
||||
if (r.type === IRowType.HEAD) {
|
||||
if (this.TagDayIDValueSet.has(r.dayId)) continue;
|
||||
|
||||
if (r.day.dayid == anchorPhoto.d.dayid) {
|
||||
startIndex = r.day.detail.indexOf(anchorPhoto);
|
||||
this.globalAnchor = this.globalCount;
|
||||
|
|
|
@ -70,6 +70,10 @@ export class API {
|
|||
query[filter] = value;
|
||||
}
|
||||
|
||||
static FOLDERS_SUB() {
|
||||
return tok(gen(`${BASE}/folders/sub`));
|
||||
}
|
||||
|
||||
static ALBUM_LIST(t: 1 | 2 | 3 = 3) {
|
||||
return gen(`${BASE}/clusters/albums?t=${t}`);
|
||||
}
|
||||
|
|
|
@ -208,10 +208,6 @@ export function convertFlags(photo: IPhoto) {
|
|||
photo.flag |= constants.c.FLAG_IS_FAVORITE;
|
||||
delete photo.isfavorite;
|
||||
}
|
||||
if (photo.isfolder) {
|
||||
photo.flag |= constants.c.FLAG_IS_FOLDER;
|
||||
delete photo.isfolder;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -287,15 +283,6 @@ export function setRenewingTimeout(
|
|||
}, 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 */
|
||||
export const constants = {
|
||||
c: {
|
||||
|
@ -303,13 +290,9 @@ export const constants = {
|
|||
FLAG_LOAD_FAIL: 1 << 1,
|
||||
FLAG_IS_VIDEO: 1 << 2,
|
||||
FLAG_IS_FAVORITE: 1 << 3,
|
||||
FLAG_IS_FOLDER: 1 << 4,
|
||||
FLAG_SELECTED: 1 << 5,
|
||||
FLAG_LEAVING: 1 << 6,
|
||||
FLAG_SELECTED: 1 << 4,
|
||||
FLAG_LEAVING: 1 << 5,
|
||||
},
|
||||
|
||||
TagDayID: TagDayID,
|
||||
TagDayIDValueSet: new Set(Object.values(TagDayID)),
|
||||
};
|
||||
|
||||
/** Cache store */
|
||||
|
|
11
src/types.ts
11
src/types.ts
|
@ -71,16 +71,6 @@ export type IPhoto = {
|
|||
video_duration?: number;
|
||||
/** Favorite flag from server */
|
||||
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 */
|
||||
datetaken?: number;
|
||||
};
|
||||
|
@ -196,7 +186,6 @@ export type IHeadRow = IRow & {
|
|||
export enum IRowType {
|
||||
HEAD = 0,
|
||||
PHOTOS = 1,
|
||||
FOLDERS = 2,
|
||||
}
|
||||
|
||||
export type ITick = {
|
||||
|
|
|
@ -8,8 +8,6 @@ declare module "vue" {
|
|||
n: typeof n;
|
||||
|
||||
c: typeof constants.c;
|
||||
TagDayID: typeof constants.TagDayID;
|
||||
TagDayIDValueSet: typeof constants.TagDayIDValueSet;
|
||||
|
||||
state_noDownload: boolean;
|
||||
|
||||
|
|
Loading…
Reference in New Issue