From 5b8bef1451f5677627ad61856e274cd08b96df07 Mon Sep 17 00:00:00 2001 From: Michael Sitz Date: Thu, 20 Aug 2026 18:57:27 -0400 Subject: [PATCH] Added a back to main clicking on the link, added help page, added a last 5 viewed --- offlineu_core.py | 99 +++++++++++++++++- templates/course_dashboard.html | 50 ++++----- templates/help.html | 174 ++++++++++++++++++++++++++++++++ templates/lesson_view.html | 5 + templates/settings.html | 5 + 5 files changed, 306 insertions(+), 27 deletions(-) create mode 100644 templates/help.html diff --git a/offlineu_core.py b/offlineu_core.py index c341fbc..b23c90c 100644 --- a/offlineu_core.py +++ b/offlineu_core.py @@ -439,6 +439,66 @@ def get_library_root() -> str: return LIBRARY_PATH +RECENT_VIEWS_FILE = os.path.join(DATA_DIR, 'recent_views.json') +MAX_RECENT_VIEWS = 5 + + +def record_recent_view(course_name: str, course_path: str, lesson_path: str, lesson_title: str) -> None: + """ + Record a lesson view for the cross-course 'Recently Viewed' list on the + dashboard, so jumping back to where you left off doesn't require + re-browsing the library - even for a different course than whichever + one happens to be loaded right now. + """ + entries = get_recent_views() + + # Drop any existing entry for this exact lesson so it moves to the + # front instead of appearing twice. + entries = [ + e for e in entries + if not (e.get('course_path') == course_path and e.get('lesson_path') == lesson_path) + ] + + entries.insert(0, { + 'course_name': course_name, + 'course_path': course_path, + 'lesson_path': lesson_path, + 'lesson_title': lesson_title, + 'viewed_at': datetime.now().isoformat() + }) + entries = entries[:MAX_RECENT_VIEWS] + + try: + os.makedirs(DATA_DIR, exist_ok=True) + with open(RECENT_VIEWS_FILE, 'w') as f: + json.dump(entries, f, indent=2) + except OSError as e: + print(f"Could not save recent views: {e}") + + +def get_recent_views() -> List[Dict[str, Any]]: + """Load the persisted 'Recently Viewed' list, newest first.""" + try: + if os.path.exists(RECENT_VIEWS_FILE): + with open(RECENT_VIEWS_FILE, 'r') as f: + return json.load(f) + except (json.JSONDecodeError, OSError) as e: + print(f"Could not load recent views: {e}") + return [] + + +def get_recent_views_for_display() -> List[Dict[str, Any]]: + """Recent views with a human-friendly timestamp added for the template.""" + entries = get_recent_views() + for entry in entries: + try: + dt = datetime.fromisoformat(entry['viewed_at']) + entry['viewed_display'] = dt.strftime('%b %d, %I:%M %p').replace(' 0', ' ') + except (KeyError, ValueError): + entry['viewed_display'] = '' + return entries + + class ProgressTracker: """Handles progress tracking and persistence""" @@ -527,7 +587,8 @@ def index(): # Show dashboard with course selection option return render_template('course_dashboard.html', course=None, - stats={'total_lessons': 0, 'completed_lessons': 0, 'completion_percentage': 0}) + stats={'total_lessons': 0, 'completed_lessons': 0, 'completion_percentage': 0}, + recent_views=get_recent_views_for_display()) # Apply progress data to tree ProgressTracker.apply_progress_to_tree(current_course) @@ -535,7 +596,8 @@ def index(): return render_template('course_dashboard.html', course=current_course, - stats=stats) + stats=stats, + recent_views=get_recent_views_for_display()) @app.route('/browse') @@ -724,6 +786,36 @@ def load_course(): return jsonify({'error': str(e)}), 500 +@app.route('/recent/open') +def open_recent(): + """ + Jump straight to a "Recently Viewed" entry from the dashboard: load its + course if it isn't already the active one, then go to that lesson. + """ + global current_course + + course_path = request.args.get('course_path', '') + lesson_path = request.args.get('lesson_path', '') + + if not course_path or not lesson_path or not os.path.exists(course_path): + return redirect(url_for('index')) + + try: + if not current_course or current_course.path != course_path: + current_course = DynamicCourseParser.scan_directory(course_path) + except Exception as e: + print(f"Could not load course for recent view: {e}") + return redirect(url_for('index')) + + return redirect(url_for('view_lesson', lesson_path=lesson_path)) + + +@app.route('/help') +def help_page(): + """Static help page: how to use the app, supported file types.""" + return render_template('help.html') + + @app.route('/lesson/') def view_lesson(lesson_path: str): """View specific lesson by path""" @@ -761,6 +853,9 @@ def view_lesson(lesson_path: str): # Update last accessed ProgressTracker.update_lesson_progress(current_course, lesson_path) + # Record for the cross-course "Recently Viewed" list on the dashboard + record_recent_view(current_course.name, current_course.path, lesson_path, lesson.title) + return render_template('lesson_view.html', course=current_course, lesson=lesson, diff --git a/templates/course_dashboard.html b/templates/course_dashboard.html index 009aac2..574bbc6 100644 --- a/templates/course_dashboard.html +++ b/templates/course_dashboard.html @@ -97,6 +97,9 @@ display: flex; align-items: center; gap: 14px; + text-decoration: none; + color: inherit; + cursor: pointer; } .brand-icon { @@ -480,13 +483,13 @@
- + {% if course %}
{{ course.name }}
{% endif %} @@ -494,6 +497,25 @@
+ {% if recent_views %} +
+
+

๐Ÿ• Recently Viewed

+
+ {% for view in recent_views %} +
+
+ โ–ถ๏ธ + {{ view.lesson_title }} +
+ {{ view.course_name }}{% if view.viewed_display %} ยท {{ view.viewed_display }}{% endif %} +
+ {% endfor %} +
+
+
+ {% endif %} {% if course %} - -
-
-

How to Use OfflineU:

-
    -
  • Prepare your course files in a directory structure
  • -
  • Copy the full path to your course directory
  • -
  • Paste the path in the input field above
  • -
  • Click "Load Course" or press Enter
  • -
  • Start learning! Your progress will be saved automatically
  • -
-
- -
-

Supported File Types:

-
    -
  • Videos: .mp4, .mkv, .avi, .mov, .webm
  • -
  • Audio: .mp3, .wav, .m4a, .aac
  • -
  • Documents: .txt, .md, .html, .pdf
  • -
  • Subtitles: .srt, .vtt
  • -
-
-
{% endif %}
@@ -641,6 +640,7 @@ ๐Ÿ“š OfflineU diff --git a/templates/help.html b/templates/help.html new file mode 100644 index 0000000..16861f1 --- /dev/null +++ b/templates/help.html @@ -0,0 +1,174 @@ + + + + + + Help - OfflineU + + + +
+
+ ๐Ÿ“š + OfflineU +
+
+ +
+
+

Help

+ +
+

How to Use OfflineU

+
    +
  • Prepare your course files in a directory structure
  • +
  • Browse your library from the main page, or enter a path manually
  • +
  • Click a course to load it
  • +
  • Start learning! Your progress will be saved automatically
  • +
+
+ +
+

Supported File Types

+
    +
  • Videos: .mp4, .mkv, .avi, .mov, .webm
  • +
  • Audio: .mp3, .wav, .m4a, .aac
  • +
  • Documents: .txt, .md, .html, .pdf
  • +
  • Subtitles: .srt, .vtt
  • +
+
+
+
+ + + + + + diff --git a/templates/lesson_view.html b/templates/lesson_view.html index f4686d6..8107607 100644 --- a/templates/lesson_view.html +++ b/templates/lesson_view.html @@ -104,6 +104,10 @@ align-items: center; gap: 8px; } + .footer-links { + display: flex; + gap: 20px; + } .footer-links a { color: var(--text-muted); text-decoration: none; @@ -509,6 +513,7 @@ ๐Ÿ“š OfflineU diff --git a/templates/settings.html b/templates/settings.html index 94ccb98..d053622 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -95,6 +95,10 @@ align-items: center; gap: 8px; } + .footer-links { + display: flex; + gap: 20px; + } .footer-links a { color: var(--accent); text-decoration: none; @@ -340,6 +344,7 @@ ๐Ÿ“š OfflineU