diff --git a/appinfo/routes.php b/appinfo/routes.php index f71d51f9..04f0b60c 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -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'], diff --git a/lib/Controller/DaysController.php b/lib/Controller/DaysController.php index 0ae9d1ab..e131a97a 100644 --- a/lib/Controller/DaysController.php +++ b/lib/Controller/DaysController.php @@ -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); }); } diff --git a/lib/Controller/FoldersController.php b/lib/Controller/FoldersController.php new file mode 100644 index 00000000..874e87ad --- /dev/null +++ b/lib/Controller/FoldersController.php @@ -0,0 +1,56 @@ +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); + }); + } +} diff --git a/lib/Controller/FoldersTrait.php b/lib/Controller/FoldersTrait.php deleted file mode 100644 index 0bafbc55..00000000 --- a/lib/Controller/FoldersTrait.php +++ /dev/null @@ -1,56 +0,0 @@ -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]); - } -} diff --git a/lib/Db/TimelineQueryFolders.php b/lib/Db/TimelineQueryFolders.php index c5a5bed8..e2622dd2 100644 --- a/lib/Db/TimelineQueryFolders.php +++ b/lib/Db/TimelineQueryFolders.php @@ -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(); diff --git a/lib/Db/TimelineRoot.php b/lib/Db/TimelineRoot.php index dc5d1ab6..4f3583a8 100644 --- a/lib/Db/TimelineRoot.php +++ b/lib/Db/TimelineRoot.php @@ -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; } diff --git a/lib/Util.php b/lib/Util.php index 60b0d0fe..d4d81a28 100644 --- a/lib/Util.php +++ b/lib/Util.php @@ -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'; /** diff --git a/src/components/FolderGrid.vue b/src/components/FolderGrid.vue new file mode 100644 index 00000000..cc30264f --- /dev/null +++ b/src/components/FolderGrid.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/src/components/ScrollerManager.vue b/src/components/ScrollerManager.vue index 5d64d528..35a6d641 100644 --- a/src/components/ScrollerManager.vue +++ b/src/components/ScrollerManager.vue @@ -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; diff --git a/src/components/SelectionManager.vue b/src/components/SelectionManager.vue index 9dfa0551..94cd1a8d 100644 --- a/src/components/SelectionManager.vue +++ b/src/components/SelectionManager.vue @@ -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 } diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index fe0cd0e4..fd0b8d9c 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -36,6 +36,11 @@ @load="scrollerManager.adjust()" > + + @@ -67,10 +72,7 @@ transform: `translate(${photo.dispX}px, ${photo.dispY}px`, }" > - - 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 && - ((p).name.startsWith(".") || - !(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, diff --git a/src/components/viewer/Viewer.vue b/src/components/viewer/Viewer.vue index f2e36ca0..4250f49f 100644 --- a/src/components/viewer/Viewer.vue +++ b/src/components/viewer/Viewer.vue @@ -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; diff --git a/src/services/API.ts b/src/services/API.ts index 1c6e6737..5f234fea 100644 --- a/src/services/API.ts +++ b/src/services/API.ts @@ -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}`); } diff --git a/src/services/Utils.ts b/src/services/Utils.ts index b8aff66f..973f6cbb 100644 --- a/src/services/Utils.ts +++ b/src/services/Utils.ts @@ -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 */ diff --git a/src/types.ts b/src/types.ts index b811efaa..7d69b663 100644 --- a/src/types.ts +++ b/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 = { diff --git a/src/vue-globals.d.ts b/src/vue-globals.d.ts index e173bc20..9483b803 100644 --- a/src/vue-globals.d.ts +++ b/src/vue-globals.d.ts @@ -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;