Fixed themes, empty folder showing and single section course detection

This commit is contained in:
2026-08-21 09:49:44 -04:00
parent 0e9fff65b0
commit fa00a0b0c4
2 changed files with 184 additions and 22 deletions
+77 -22
View File
@@ -60,7 +60,7 @@ VIDEO_SIZE_BOUNDS = {'video_width': (200, 4000), 'video_height': (120, 3000)}
SETTINGS_CHOICES = {
'theme': {
'dark', 'light', 'dracula', 'tokyo_night', 'catppuccin_mocha', 'ayu_dark',
'github_dark', 'atom_one_dark', 'houston', 'night_owl', 'dainty', 'matcha',
'github_dark', 'atom_one_dark', 'houston', 'night_owl', 'nord', 'matcha',
},
'font_family': {'system', 'sans', 'serif', 'monospace', 'monaspace'},
'font_size': {'small', 'medium', 'large', 'xlarge'},
@@ -77,19 +77,24 @@ THEME_DISPLAY_NAMES = {
'tokyo_night': 'Tokyo Night', 'catppuccin_mocha': 'Catppuccin Mocha',
'ayu_dark': 'Ayu Dark', 'github_dark': 'GitHub Dark',
'atom_one_dark': 'Atom One Dark', 'houston': 'Houston',
'night_owl': 'Night Owl', 'dainty': 'Dainty', 'matcha': 'Matcha',
'night_owl': 'Night Owl', 'nord': 'Nord', 'matcha': 'Matcha',
}
# Full color palette per theme. 'accent' here is only the *default* accent
# offered when a theme is first selected - the accent_color setting is what
# actually drives --accent afterward, so it stays independently editable.
# Sourced from each theme's official palette (Dracula, Tokyo Night,
# Catppuccin Mocha, Ayu, GitHub Dark, Atom One Dark, Houston all verified
# against upstream repos/specs). Dainty and Matcha don't have one single
# fixed official hex set (Dainty is a theme *generator*; Matcha's exact
# source values weren't available) - built in the spirit of their
# documented look (refined/minimal, earthy green-and-gray) rather than
# claimed as an exact match.
# Catppuccin Mocha, Ayu, GitHub Dark, Atom One Dark, Houston, Nord all
# verified against upstream repos/specs). Matcha is sourced from
# lucafalasco/matcha's published VS Code theme JSON (editor/sideBar/
# activityBar backgrounds, foreground, panel.border, and the
# statusBar/button accent color).
#
# Dainty used to be here instead of Nord, but it turned out to be a
# Lab-space theme *generator* (HotWordland/dainty-vscode) with no fixed
# shipped palette - its output depends on whatever base theme you feed
# it, so there was no single "official Dainty" hex set to verify our
# approximation against. Swapped for Nord, which does have one.
THEME_PALETTES = {
'dark': {
'bg-primary': '#1a1a1a', 'bg-secondary': '#2d2d2d', 'bg-tertiary': '#3d3d3d',
@@ -141,15 +146,15 @@ THEME_PALETTES = {
'bg-tertiary-hover': '#123a5c', 'text-primary': '#d6deeb', 'text-muted': '#5f7e97',
'border-color': '#102a44', 'accent': '#82aaff', 'accent-hover': '#6690e0',
},
'dainty': {
'bg-primary': '#1c1c22', 'bg-secondary': '#232329', 'bg-tertiary': '#2c2c34',
'bg-tertiary-hover': '#35353e', 'text-primary': '#e8e6f0', 'text-muted': '#8a8894',
'border-color': '#38383f', 'accent': '#c9a0dc', 'accent-hover': '#b285c7',
'nord': {
'bg-primary': '#2e3440', 'bg-secondary': '#3b4252', 'bg-tertiary': '#434c5e',
'bg-tertiary-hover': '#4c566a', 'text-primary': '#d8dee9', 'text-muted': '#4c566a',
'border-color': '#434c5e', 'accent': '#88c0d0', 'accent-hover': '#5e81ac',
},
'matcha': {
'bg-primary': '#1e2320', 'bg-secondary': '#262b27', 'bg-tertiary': '#333a34',
'bg-tertiary-hover': '#3d4539', 'text-primary': '#d6ddd2', 'text-muted': '#7d8a7c',
'border-color': '#3d443e', 'accent': '#a3c585', 'accent-hover': '#8fb86e',
'bg-primary': '#1c2427', 'bg-secondary': '#273136', 'bg-tertiary': '#323e45',
'bg-tertiary-hover': '#3c4850', 'text-primary': '#d1ded3', 'text-muted': '#7c8885',
'border-color': '#707c4f', 'accent': '#a4b07e', 'accent-hover': '#8b966b',
},
}
@@ -480,6 +485,11 @@ def _has_direct_media(directory: Path) -> bool:
return False
_SECTION_NAME_RE = re.compile(
r'^(section|module|chapter|part|unit|lesson)\b', re.IGNORECASE
)
def _looks_like_course(directory: Path) -> bool:
"""
Heuristic for 'this folder is a course, stop recursing into it':
@@ -489,17 +499,31 @@ def _looks_like_course(directory: Path) -> bool:
Requiring 2+ matching subfolders (rather than just 1) avoids mistaking
a publisher/grouping folder that holds a single course - e.g.
Pluralsight/Docker Deep Dive/lesson.mp4 - for the course itself.
The one exception: a directory with exactly one media-holding subfolder
whose name reads as a section label ("Section 1", "Module 2", ...)
rather than a course title. That's still a course with just one
section, not a publisher folder - Pluralsight/Docker Deep Dive doesn't
get named "Section 1", so this doesn't reopen the ambiguity above.
"""
if _has_direct_media(directory):
return True
try:
children_with_media = [
children = [
child for child in directory.iterdir()
if child.is_dir() and not child.name.startswith('.') and _has_direct_media(child)
if child.is_dir() and not child.name.startswith('.')
]
except (PermissionError, OSError):
return False
return len(children_with_media) >= 2
children_with_media = [child for child in children if _has_direct_media(child)]
if len(children_with_media) >= 2:
return True
if (
len(children_with_media) == 1
and _SECTION_NAME_RE.match(children_with_media[0].name.strip())
):
return True
return False
HIDDEN_PATHS_FILE = os.path.join(DATA_DIR, 'hidden_paths.json')
@@ -533,6 +557,31 @@ def set_path_hidden(path: str, hidden: bool) -> List[str]:
return result
def _contains_visible_course(directory: Path, hidden_set: set) -> bool:
"""
Whether `directory` leads to at least one course that isn't curated
out - directly, or via a hidden ancestor folder within this subtree.
Used by the normal (skip_hidden=True) Library browser so a folder
whose every course has been individually hidden doesn't still show up
as a drillable directory that dead-ends empty once opened. Hiding a
folder hides everything inside it, so a hidden folder short-circuits
the walk rather than counting anything beneath it as visible.
"""
if os.path.abspath(str(directory)) in hidden_set:
return False
if _looks_like_course(directory):
return True
try:
children = [
c for c in directory.iterdir()
if c.is_dir() and not c.name.startswith('.')
]
except (PermissionError, OSError):
return False
return any(_contains_visible_course(child, hidden_set) for child in children)
def list_library_directory(dir_path: str, skip_hidden: bool = True) -> Dict[str, Any]:
"""
List only the immediate children of dir_path for the lazy-loading
@@ -584,10 +633,16 @@ def list_library_directory(dir_path: str, skip_hidden: bool = True) -> Dict[str,
'hidden': is_hidden
})
else:
has_course_inside = any(
f.is_file() and f.suffix.lower() in VIDEO_EXTENSIONS | AUDIO_EXTENSIONS
for f in entry.rglob('*')
)
if skip_hidden:
# Only count courses that aren't themselves curated out -
# otherwise a folder whose courses are all hidden
# individually would still show up, empty, once opened.
has_course_inside = _contains_visible_course(entry, hidden_set)
else:
has_course_inside = any(
f.is_file() and f.suffix.lower() in VIDEO_EXTENSIONS | AUDIO_EXTENSIONS
for f in entry.rglob('*')
)
if has_course_inside:
items.append({
'type': 'directory',