Exclude course/item leaves from destination picker; ease new-folder UX

The destination picker (Sort Unsorted, Manage Library Move) was
listing individual course/item folders as pickable destinations
whenever they held content course detection doesn't recognize as
video/audio - ebooks, audiobooks in unsupported formats, etc. Add a
leaf-item check scoped to the picker's own category index so these no
longer show up, without touching the shared course-detection heuristic
used elsewhere (Library browsing, search, stats).

Also replace "Create new folder"'s single free-text path field with a
parent-folder picker plus a plain new-folder-name field and a live
"Will create: X/Y" preview, so the resulting path is confirmed before
applying instead of hand-typed blind.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 13:13:09 -04:00
co-authored by Claude Sonnet 5
parent bcd1734c94
commit 62bb5e1318
3 changed files with 141 additions and 42 deletions
+31 -5
View File
@@ -1263,6 +1263,29 @@ def _tokenize_ordered(name: str) -> List[str]:
return ordered
def _is_unrecognized_leaf_item(directory: Path) -> bool:
"""
A folder with files directly inside but no subfolders at all -
virtually always a single downloaded item (an ebook, an audiobook in a
format this app doesn't play, or some other misc file drop) rather
than an organizational category, even when _looks_like_course's
video/audio check doesn't recognize it as a course (e.g. an EPUB-only
folder has no media files at all, so _has_direct_media never fires).
Scoped to _build_category_index only - not a general "is this a
course" replacement, since this app still can't do anything useful
with the file itself, so it'd be wrong to surface it as a course
elsewhere (Library browsing, stats, search, ...).
"""
try:
entries = list(directory.iterdir())
except (PermissionError, OSError):
return False
has_subdir = any(e.is_dir() and not e.name.startswith('.') for e in entries)
if has_subdir:
return False
return any(e.is_file() and not e.name.startswith('.') for e in entries)
def _build_category_index() -> List[Dict[str, Any]]:
"""
Every category/subcategory folder currently in the library (e.g. IT,
@@ -1282,10 +1305,13 @@ def _build_category_index() -> List[Dict[str, Any]]:
categories' course titles) can't outweigh a specific match
elsewhere just by sharing more of it.
Stops recursing into a folder once it reads as a course itself
(_looks_like_course, the same rule the Library browser uses), so
individual courses never show up as if they were categories to file
things under. The Unsorted folder itself is excluded - it's the
source, never a valid destination.
(_looks_like_course, the same rule the Library browser uses) or as an
unrecognized leaf item (_is_unrecognized_leaf_item - an ebook/
audiobook/misc file that isn't video or audio, so _looks_like_course
doesn't catch it either), so individual courses and standalone files
never show up as if they were categories to file things under. The
Unsorted folder itself is excluded - it's the source, never a valid
destination.
"""
library_root = Path(get_library_root())
try:
@@ -1308,7 +1334,7 @@ def _build_category_index() -> List[Dict[str, Any]]:
continue
except OSError:
pass
if _looks_like_course(entry):
if _looks_like_course(entry) or _is_unrecognized_leaf_item(entry):
continue
new_parts = path_parts + [entry.name]
path_tokens: Set[str] = set()