Add Resume button, Surprise Me random pick, and bulk select
Three quality-of-life additions to the Library browser and course pages: - A course page shows a "Resume: <lesson>" button when there's a last-visited lesson, using Course.last_accessed_path (already populated by apply_progress_to_tree, just not surfaced before) - no more scrolling the tree to find where you left off. - "Surprise Me" picks a random not-yet-fully-watched course and loads it, for when there's too much library to decide what to watch. Reuses the already-cached get_all_course_dirs() and the same per-course completed/total shape _scan_library_activity() already computes for Stale Courses - no new scanning. - A select-mode toggle in the Library browser adds checkboxes to every row/card (list and grid) with a bulk-action bar to hide or queue several courses/folders at once, via two new endpoints (/api/hidden-paths/bulk, /api/next-up/bulk) that reuse the existing single-item set_path_hidden()/set_path_queued() in a loop. Selection is scoped to the current folder view and clears on navigation. Also fixes runTranscriptSearch() going silently blank on zero results instead of showing a "no matches" message, and updates the Help page to cover all of this. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+68
-1
@@ -7,6 +7,7 @@ Enhanced version with dynamic subdirectory navigation
|
||||
import os
|
||||
import json
|
||||
import mimetypes
|
||||
import random
|
||||
import re
|
||||
import sys
|
||||
import argparse
|
||||
@@ -1120,6 +1121,19 @@ def _scan_library_activity() -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def get_random_incomplete_course() -> Optional[Path]:
|
||||
"""A random course for the dashboard's "Surprise Me" pick - excludes
|
||||
courses that are already fully watched where possible, falling back to
|
||||
the whole library if everything's done."""
|
||||
all_dirs = get_all_course_dirs()
|
||||
if not all_dirs:
|
||||
return None
|
||||
scan = _scan_library_activity()
|
||||
fully_completed = {c['path'] for c in scan['courses'] if c['total'] > 0 and c['completed'] >= c['total']}
|
||||
candidates = [d for d in all_dirs if str(d) not in fully_completed]
|
||||
return random.choice(candidates or all_dirs)
|
||||
|
||||
|
||||
def format_library_stats(scan: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Library-wide overview for the dashboard's stats card, from a _scan_library_activity() result."""
|
||||
return {
|
||||
@@ -1827,13 +1841,20 @@ def index():
|
||||
if stats.get('total_duration_seconds'):
|
||||
stats['remaining_display'] = format_duration(stats['remaining_seconds'])
|
||||
|
||||
resume_lesson = None
|
||||
if current_course.last_accessed_path:
|
||||
lesson, _ = find_lesson_in_tree(current_course.root_node, current_course.last_accessed_path)
|
||||
if lesson:
|
||||
resume_lesson = {'title': lesson.title, 'url': get_lesson_url(lesson, current_course.path)}
|
||||
|
||||
return render_template('course_dashboard.html',
|
||||
course=current_course,
|
||||
stats=stats,
|
||||
continue_watching=continue_watching,
|
||||
recent_views=recent_views,
|
||||
has_note=bool(course_has_any_notes(current_course)),
|
||||
is_queued=os.path.abspath(current_course.path) in get_next_up_paths())
|
||||
is_queued=os.path.abspath(current_course.path) in get_next_up_paths(),
|
||||
resume_lesson=resume_lesson)
|
||||
|
||||
|
||||
@app.route('/browse')
|
||||
@@ -2037,6 +2058,24 @@ def set_hidden_path_api():
|
||||
return jsonify({'success': True, 'hidden_paths': updated})
|
||||
|
||||
|
||||
@app.route('/api/hidden-paths/bulk', methods=['POST'])
|
||||
def bulk_set_hidden_paths_api():
|
||||
"""Hide or un-hide several courses/directories at once."""
|
||||
data = request.json or {}
|
||||
paths = data.get('paths') or []
|
||||
hidden = bool(data.get('hidden', True))
|
||||
if not isinstance(paths, list) or not paths:
|
||||
return jsonify({'error': 'paths is required'}), 400
|
||||
|
||||
library_root = os.path.abspath(get_library_root())
|
||||
updated = get_hidden_paths()
|
||||
for path in paths:
|
||||
target = os.path.abspath(path)
|
||||
if target == library_root or target.startswith(library_root + os.sep):
|
||||
updated = set_path_hidden(target, hidden)
|
||||
return jsonify({'success': True, 'hidden_paths': updated})
|
||||
|
||||
|
||||
def validate_new_name(new_name: str) -> Optional[str]:
|
||||
"""Validate a proposed directory name; returns an error message, or None if valid."""
|
||||
if not new_name:
|
||||
@@ -2302,6 +2341,17 @@ def load_course():
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/random-pick')
|
||||
def random_pick():
|
||||
""""Surprise Me" - load a random not-yet-fully-watched course and land on its dashboard."""
|
||||
global current_course
|
||||
course_dir = get_random_incomplete_course()
|
||||
if not course_dir:
|
||||
return redirect(url_for('index'))
|
||||
current_course = get_course_tree(str(course_dir))
|
||||
return redirect(url_for('index'))
|
||||
|
||||
|
||||
@app.route('/recent/open')
|
||||
def open_recent():
|
||||
"""
|
||||
@@ -2383,6 +2433,23 @@ def set_next_up_api():
|
||||
return jsonify({'success': True, 'next_up': updated})
|
||||
|
||||
|
||||
@app.route('/api/next-up/bulk', methods=['POST'])
|
||||
def bulk_set_next_up_api():
|
||||
"""Add several courses to the Next Up queue at once."""
|
||||
data = request.json or {}
|
||||
paths = data.get('paths') or []
|
||||
if not isinstance(paths, list) or not paths:
|
||||
return jsonify({'error': 'paths is required'}), 400
|
||||
|
||||
library_root = os.path.abspath(get_library_root())
|
||||
updated = get_next_up_paths()
|
||||
for path in paths:
|
||||
target = os.path.abspath(path)
|
||||
if target == library_root or target.startswith(library_root + os.sep):
|
||||
updated = set_path_queued(target, True)
|
||||
return jsonify({'success': True, 'next_up': updated})
|
||||
|
||||
|
||||
@app.route('/api/next-up/reorder', methods=['POST'])
|
||||
def reorder_next_up_api():
|
||||
"""Replace the Next Up order wholesale, from the client's up/down-reordered list."""
|
||||
|
||||
Reference in New Issue
Block a user