Shows every section and lesson in the course - videos and standalone
documents alike - so jumping to a different section no longer means
backing out to the course page first. Current lesson is highlighted,
its section auto-expands and scrolls into view; sticky on desktop,
stacks below the player on narrow viewports. Replaces the old
"Lessons in this section" list, which only showed the current
section.
The tree-rendering markup (shared with the loaded-course dashboard
view) is now a single macro in templates/_course_tree.html instead of
being duplicated, so both views stay in sync going forward.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Auto-play next lesson on end, with a cancelable countdown and a
Settings toggle (default on).
- Grid-view course cards show a small progress ring (checkmark at
100%) instead of a separate bar.
- Duplicate Lesson Files: scans within each course/folder for media
files that look like the same lesson downloaded twice, with per-file
delete and a shared ignore list with Duplicate Courses.
- Auto-generated cover art now samples a few early candidate frames
and keeps the largest JPEG, favoring an intro title card over a
blank fade-in or a plain presenter frame.
- Settings -> "Regenerate Thumbnails" re-runs that logic for every
course with an auto-generated thumbnail (never touches manual
covers), so already-cached thumbnails can pick up the improvement.
- Add .gitignore for __pycache__/.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Favorites: star toggle on course rows, the course page, and a new
dashboard card; rebased on rename/move, backed up/restored, and
caught by stale-reference cleanup (also fixed a pre-existing gap
where favorites.json wasn't in the backup file list).
- Storage Usage: click a folder row to drill into its contents one
level at a time, with a Back button.
- Lesson player: added ,/. (speed step), [/] (prev/next lesson), and
F (fullscreen) keyboard shortcuts.
- Settings: a CHANGELOG.md-backed "What's New" list, since VERSION
alone only ever shows the current build.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace the dashboard-only footer link with a "Files" item in the
persistent bottom tab bar (Home/Notes/Help/Settings) on every page, so
it's reachable the same way everywhere instead of only from the
dashboard.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix Sort Unsorted picking a wrong destination on words that are common
clutter across many categories (a creator's name, generic marketing
filler) by ranking matches on a folder's own deliberate name over
incidental sibling-course words, and downweighting borrowed words that
recur across many categories. Add a folder-picker/create-new/rename UI
to override any proposal.
Turn /unsorted into a general File Management page: move Refresh
Library, Bulk Rename, and Manage Library (hide/rename) here from
Settings, add a Move action to Manage Library using the same picker,
and add Duplicate Courses and Clean Up Stale References tools. Add a
footer link to the page from the dashboard.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Auto-generated course thumbnails: find_course_thumbnail() falls back to
grabbing a frame from a course's first video via ffmpeg when there's no
manual cover image, cached to .offlineu_thumbnail.jpg so it only ever
runs once. Folded into the "Precompute" prewarm button, now "Precompute
Lengths & Cover Art".
Library-wide "Remaining" stat: sums every course's already-cached
duration (no ffprobe, just a JSON read) into the dashboard's Library
Stats card, alongside the existing "Watched" figure.
Backup restore: new /api/backup/restore endpoint and a "Choose Backup
File..." control in Settings, with zip-slip and missing-course guards.
Also fixed a gap in the export itself - next_up.json and
outline_config.json weren't being backed up before, so restore wouldn't
have been a true round trip.
Move Surprise Me from a full-width button above the course list to a
dice-icon button in the dashboard header, swapping with the course-name
badge depending on whether a course is loaded.
Update README to cover all of the above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Settings gets a "Precompute Video Lengths" button that walks the whole
library in a background thread, populating every course's persistent
ffprobe duration cache up front instead of paying that cost the first
time each course card is viewed. Progress polls live while it runs and
picks back up correctly if you navigate away mid-scan.
Separately, the loaded-course lesson tree only ever showed a lesson's
duration once you'd actually played it (from progress.json) - a freshly
opened course had no time info anywhere, including the course header's
"~X remaining" line, which existed but was always empty as a result.
apply_progress_to_tree now falls back to the same ffprobe cache for any
lesson without a known duration yet, so per-lesson lengths and the
remaining-time estimate work from the very first visit. Refactored the
duration-cache read/write into a shared helper so the library browser,
the prewarm button, and this tree view all go through one path.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add ffmpeg to the Docker image so ffprobe can read each video/audio
file's duration server-side. Results are cached to a small persistent
per-course JSON file keyed by (relative path, size, mtime), so a file
only gets probed once - a container restart or the library scan's
in-memory cache expiring never re-runs ffprobe on unchanged files.
Course cards in the library browser (grid, list, and search) now show
total runtime alongside the media-file count, so you know the time
commitment before opening a course. Durations under a minute are
suppressed rather than showing a noisy "0m".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add a shared Jinja icon macro (templates/_icons.html) with 27 Feather-style
stroke SVGs and import it across every live template - tab bars, buttons,
lesson-type/status icons, folder/course icons, and the lesson player's
footer brand mark all now use currentColor SVGs instead of emoji, matching
the app's vector logo instead of rendering inconsistently per platform.
A few icons built dynamically in JS (thumbnail-load fallbacks, toggled
button states) get a small ICON_SVGS constant per file, since JS template
literals can't call the Jinja macro.
Left pure directional glyphs (expand/collapse chevrons, move up/down
arrows, keyboard-shortcut arrows) as plain Unicode - they already render
as monochrome text, matching the existing collapsible-header chevron
pattern rather than the colorful pictographic emoji this was aimed at.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Replace card-header emoji with a matching stroke-style SVG icon set that
inherits theme color via currentColor. Drop the accent-stripe border from
every card and keep it only on the Your Courses card, the one primary
action. Merge Continue Watching and Recently Viewed into a single
true-recency list so an old completed item can no longer outrank a newer
in-progress one across two separately-capped lists.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Swap the 📚 emoji in the header for an inline SVG mark - a rounded
square with a play triangle, echoing that this is a video-course
library. Uses var(--accent)/var(--bg-secondary) rather than fixed
colors so it re-colors automatically with whichever of the app's
theme presets is active, same as everything else in the header.
Applied consistently across all 5 templates; the small secondary
footer brand mark (still plain 📚 text) is untouched - this only
replaces the prominent header lockup.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three visual/structural changes:
- Grid cards without a thumbnail (nearly all real courses in this
library) showed the same generic graduation-cap icon, making the
poster grid hard to visually scan. Replace it with a deterministic
color+initial per course name (same idea as Slack/Google Photos
default avatars), so cards without real cover art still look
distinct from each other.
- Notes/Help/Settings only existed as small footer text links,
meaning a scroll to the bottom of every page to navigate. Add a
persistent bottom tab bar (Home/Notes/Help/Settings, current page
highlighted via a new active_tab context var) to the dashboard,
Notes Hub, Settings, and Help pages. Deliberately left the lesson
page alone - it already has its own fixed-bottom notes panel, and a
second fixed bar there would just eat into an already content-dense
page.
- The no-course dashboard stacked five informational/queue cards
(Library Stats, Next Up, Pick Back Up, Recently Added, Continue
Watching, Recently Viewed) before "Your Courses," so the actual
library browser was the last thing on the page. Move it up to right
after the Surprise Me banner - it was already unconditional, so this
only changes where it renders, not when.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Grid card names are clamped to 2 lines and clipped, so a long
dot-separated course name (the common case for this library) had no
way to be read in full without switching to list view. Add a title
attribute so hovering reveals the complete name.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
The course dashboard's <h2>{{ course.name }}</h2> heading had no wrap
protection, and course.name is the raw folder name - unlike lesson
titles (cleaned up via _clean_lesson_name), it's never given real
spaces. Most real course folders are dot-separated scene-release-style
names with no natural break point at all, so the browser had nowhere
to wrap and widened the whole page instead - confirmed live against
the deployed site (page rendered at 1066px on a 375px mobile
viewport). Every other place that shows a long course/lesson name
already truncates correctly via flex + overflow:hidden; this heading
was the one gap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three comparison-driven upgrades against typical course/media platforms:
- Lesson page gets a collapsible "Lessons in this section" list so you
can jump between siblings without leaving the page, instead of only
global Prev/Next buttons. find_lesson_in_tree() now also returns the
DirectoryNode that owns the lesson (its .lessons list is the sibling
set) since DirectoryNode has no parent pointer; a new
get_section_lessons() resolves each sibling's progress the same way
view_lesson() already does for the current lesson.
- Course tree section headers show "X/Y watched" instead of a flat item
count, via DynamicCourseParser._calculate_completion_stats (already
computed recursive completion for any node, just never called
per-section) exposed as a Jinja global.
- Library browser gets a list/grid toggle for browsing large category
folders by poster-style thumbnail instead of a 32px-icon list.
Client-side only, persisted via localStorage, defaults to the
existing list view; both folder browsing and search results already
funnel through the same render function so grid mode covers both.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every course-loading path (Library browser, Continue Watching, Next
Up, ...) goes through loadCoursePath(), which reloads the page after
POSTing to /load_course - a plain reload never touches history, so
once you were on the course view, back had nothing of ours to land on
and fell straight through to exiting the app.
Push a history entry before that reload, and teach the back handler
that popping back to the course view means "unload the course and
reload" (there's no client-side way to patch back to the library
view's markup - it's a distinct server render tied to current_course).
Also restore the exact library folder trail on that reload instead of
resetting to the root, so back genuinely returns to where you were
browsing, not just the top level.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Drilling into a Library folder only ever swapped #library-groups's
contents via fetch, never touching browser history - so back had
nothing of the app's own to step through once you were a few folders
deep, and fell straight through to exiting the app instead of going up
a level.
Push a history entry per drill-down and breadcrumb jump, and restore
libraryTrail/re-fetch that level on popstate, so back now un-drills
one folder at a time and only reaches the app's actual exit once
you're back at the library root.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Templates only, no backend changes:
- Fixed the real bug: JS-generated status/loading text and a few component
styles used hardcoded hex (#007acc, #999/#666, #28a745, #ff6b6b) instead
of the CSS variables the theme system already provides, so they didn't
track the user's chosen theme/accent - most visible on Light or any named
theme other than the default. Added --success/--error to all 5 templates'
:root blocks and replaced every non-:root occurrence. Left the
.lesson-type badge colors and generic UI chrome grays alone (intentional,
not theme-dependent). Verified live by switching to Nord and Light themes.
- Dashboard cards (Library Stats, Next Up, Pick Back Up, Recently Added,
Continue Watching, Recently Viewed) are now collapsible, state remembered
per device via localStorage - the no-course dashboard had grown to 7
stacked cards.
- Course page's three action buttons get consistent compact sizing and
icons instead of a plain stack.
- Library browser and transcript search show shimmering skeleton rows
instead of bare "Loading..."/"Searching..." text while a fetch is in
flight.
- Hover treatment made consistent: transcript results get the same lift
.lesson-item already had, heatmap cells get a GitHub-style hover
scale-up, and Notes Hub links get a background-chip hover.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Six more dashboard features, all built on the per-course progress-file
scanning infrastructure from earlier sessions:
- Notes Hub (/notes): every lesson note across the library in one place,
newest first, reusing the existing /recent/open cross-course jump route.
- Transcript search: opt-in checkbox on the Library search box, searching
inside .srt/.vtt files. Kept cheap by doing a raw substring match as the
filter step and only extracting a snippet for files that actually match.
- Stale course nudges ("Pick Back Up"): courses with progress that haven't
been touched in 14+ days.
- Study guide export: compiles a course's notes into one downloadable
markdown file, section/lesson structure preserved.
- Next Up queue: an ordered, persisted "what to tackle next" list with
up/down reordering and a queue button on every course row.
- Activity heatmap: a 90-day contribution-style calendar in the Library
Stats card.
_scan_library_activity() now does one walk over every course's progress
file per dashboard load; library stats, stale courses, and the heatmap all
derive from that single scan instead of three independent ones.
Also refactored the duplicated lesson-progress-key lookup (used in three
places now) into _resolve_lesson_progress_key(), and flagged a pre-existing
bug found along the way (not fixed here, kept out of scope): subtitle files
never actually attach to a Lesson object, so the video player's caption
track has never had anything to render.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Five more usability features on top of the search/thumbnails batch:
- Recently Added: dashboard card of courses by folder mtime, separate from
Recently Viewed (watched vs. just showed up on disk).
- Bulk find/replace rename across every course/folder name at once, with a
mandatory preview step before anything touches disk. Refactored the
single-item rename route to share the same validate/apply logic.
- Estimated time remaining on the loaded course's stats card, computed only
from lessons that have actually reported a duration.
- Library-wide stats overview: total courses, lessons tracked, time
watched, daily streak - read from each course's small progress file
rather than re-scanning course contents.
- One-click backup/export zip of settings, hidden-paths, recently-viewed,
and every course's progress/notes.
Also fixes a real performance bug found along the way: search was routing
through list_library_directory, which computes a full recursive file count
and thumbnail lookup for every course at every level regardless of match -
turning a search into an O(every file in the library) scan. Gave search its
own lightweight directory-only walk (iter_all_courses), now shared by
Recently Added and the stats overview too, so the expensive per-course work
only runs for courses that actually match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Eight usability additions on top of the mobile redesign: course cover-image
thumbnails, a debounced library search, a Continue Watching section split
from Recently Viewed, per-lesson playback speed control, an installable
PWA manifest/icons, name sort in the library browser, per-lesson notes, and
a bulk mark-course-as-watched action. Also fixes two real bugs found along
the way: lesson_view.html was missing the viewport meta tag (so lesson
pages weren't actually mobile-responsive), and update_lesson_progress
always overwrote the whole progress entry, which would have silently
deleted a saved note on the next routine autosave.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>