Add per-course progress badges to library cards, replace alert() with toasts
Compute a cheap, uncached completion percentage per course (completed lessons / media files) and surface it as a badge + thin progress bar on library cards in both list and grid view, and in search results - untouched courses stay unbadged so the list doesn't get noisy with "0%". Replace the native alert() dialogs used for bulk-select validation and course-load errors with a small slide-in toast component that matches the rest of the UI instead of a jarring browser popup. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+35
-5
@@ -806,6 +806,7 @@ def list_library_directory(dir_path: str, skip_hidden: bool = True) -> Dict[str,
|
||||
if _looks_like_course(entry):
|
||||
item = _course_summary(entry)
|
||||
item['hidden'] = is_hidden
|
||||
item['completion_percentage'] = _course_completion_percentage(entry, item['media_files'])
|
||||
items.append(item)
|
||||
else:
|
||||
if skip_hidden:
|
||||
@@ -904,6 +905,32 @@ def _course_summary(course_dir: Path) -> Dict[str, Any]:
|
||||
return dict(cache_get_or_compute(f'course_summary:{course_dir}', compute))
|
||||
|
||||
|
||||
def _course_completion_percentage(course_dir: Path, media_files: int) -> Optional[float]:
|
||||
"""
|
||||
Cheap, uncached completion percentage for a single course card - reads
|
||||
only that course's own small progress JSON directly (same pattern as
|
||||
_scan_library_activity), never the cached _course_summary result, so a
|
||||
lesson finished a second ago shows up immediately instead of waiting out
|
||||
the library-scan cache TTL. None if nothing's completed yet, so
|
||||
untouched courses don't show a noisy "0%" badge.
|
||||
"""
|
||||
if media_files <= 0:
|
||||
return None
|
||||
try:
|
||||
with open(course_dir / '.offlineu_progress.json', 'r') as f:
|
||||
progress = json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
||||
return None
|
||||
|
||||
completed = sum(
|
||||
1 for key, entry in progress.items()
|
||||
if key != 'last_accessed_path' and isinstance(entry, dict) and entry.get('completed')
|
||||
)
|
||||
if completed == 0:
|
||||
return None
|
||||
return round(min(100.0, 100 * completed / media_files), 1)
|
||||
|
||||
|
||||
def search_library_courses(query: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Search the library for courses whose name contains `query`
|
||||
@@ -912,11 +939,14 @@ def search_library_courses(query: str) -> List[Dict[str, Any]]:
|
||||
get_all_course_dirs.
|
||||
"""
|
||||
query_lower = query.lower()
|
||||
return [
|
||||
_course_summary(course)
|
||||
for course in get_all_course_dirs()
|
||||
if query_lower in course.name.lower()
|
||||
]
|
||||
results = []
|
||||
for course in get_all_course_dirs():
|
||||
if query_lower not in course.name.lower():
|
||||
continue
|
||||
item = _course_summary(course)
|
||||
item['completion_percentage'] = _course_completion_percentage(course, item['media_files'])
|
||||
results.append(item)
|
||||
return results
|
||||
|
||||
|
||||
def _extract_subtitle_snippet(text: str, query_lower: str, context_chars: int = 80) -> str:
|
||||
|
||||
Reference in New Issue
Block a user