Fixed themes, empty folder showing and single section course detection
This commit is contained in:
+77
-22
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user