feat(sidebar): show empty folders and create folders from the sidebar (#999)

Empty folders (tracked in the scan-recorded all_folders list, same source
the move/download destination picker uses) can now be surfaced in the
folder sidebar via a view-options toggle, dimmed when their subtree holds
no models. Folders can be created directly from the sidebar through a new
POST /api/lm/{prefix}/create-folder endpoint with library-root
containment checks; the scanner records the new directory incrementally
so the tree reflects it without a rescan.

The sidebar header moves its view toggles (tree/list, recursive, empty
folders) into a "..." menu to fit the new create-folder button.
This commit is contained in:
Will Miao
2026-09-15 15:14:56 +08:00
parent 2ceb1e2850
commit 9734df15b4
24 changed files with 1260 additions and 173 deletions
+28
View File
@@ -1477,6 +1477,34 @@ class ModelScanner:
return sorted(folders, key=lambda x: x.lower())
async def add_known_folder(self, folder: str) -> None:
"""Record a folder (and its parents) in the known folder list.
Called when a directory is created between scans (e.g. via the
create-folder API) so folder trees reflect it immediately without
waiting for the next reconciliation. When ``all_folders`` has not
been recorded yet (legacy snapshot), this is a no-op — the scheduled
backfill walk discovers the directory from disk instead.
"""
normalized = folder.replace("\\", "/").strip("/")
parts = [part for part in normalized.split("/") if part]
if not parts:
return
cache = self._cache
if cache is None:
return
recorded = getattr(cache, "all_folders", None)
if recorded is None:
return
known = set(recorded)
for i in range(1, len(parts) + 1):
known.add("/".join(parts[:i]))
updated = sorted(known, key=lambda x: x.lower())
if updated != list(recorded):
cache.all_folders = updated
await self._persist_current_cache()
self.bump_cache_version()
def _schedule_all_folders_backfill(self) -> None:
"""Kick off a one-shot background folder walk if none is running."""
if self._all_folders_backfill_running: