Files
offlineu/OfflineU-project-summary.md
T
rmsitzandClaude Sonnet 5 38963b1d55 Add search, thumbnails, continue watching, playback speed, PWA install, sort, notes, and mark-watched
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>
2026-08-22 09:06:44 -04:00

10 KiB
Raw Blame History

OfflineU — Project Summary (for Claude Code handoff)

What this is

A fork of the self-hosted course viewer WhiskeyCoder/OfflineU (Flask app), customized for personal use. Deployed via Docker (Dockhand on a Ugreen NAS), built locally from a private Gitea repo rather than pulling the upstream image.

Deployment

  • docker-compose.yml: build: . + pull_policy: build (forces a real rebuild every deploy — without pull_policy: build, Compose can silently reuse a stale cached image).
  • network_mode: host, PUID=1000, PGID=10, TZ=America/New_York.
  • Volumes: courses at /volume1/files/training/app/courses, progress/settings data at /volume2/docker/offlineu/data/app/data.
  • Deployed from Gitea via Dockhand's "Deploy from Git" stack type.

Files touched this session

  • offlineu_core.py — all backend logic (Flask routes, settings, progress tracking)
  • templates/course_dashboard.html — main dashboard / library browser
  • templates/lesson_view.html — individual lesson page
  • templates/settings.html — new settings page
  • templates/help.html — new help page
  • static/theme.js — shared script that applies persisted display settings

Features added, roughly in order

  1. Library browser — lazy, drill-down directory browsing instead of typing a full path. /library lists one directory level at a time; a folder is treated as a "course" once it (or 2+ of its immediate subfolders) contain video/audio files directly. Starts collapsed at the top level.

  2. PDF preview fix — lesson pages were fetching PDFs as text and dumping raw bytes. Now PDFs (and HTML) render in an iframe; .docx/.doc/.rtf show a "no preview, download instead" message rather than garbled text.

  3. Full-width, resizable layout — removed a hardcoded 800px video cap and 1000px page cap. Video player supports native corner-drag resizing (resize: both + object-fit: contain), and the last-used size persists via settings.

  4. Settings system (/settings, /api/settings) — server-side persisted (/app/data/settings.json), covering:

    • Theme (dark/light + 10 named presets: Dracula, Tokyo Night, Catppuccin Mocha, Ayu Dark, GitHub Dark, Atom One Dark, Houston, Night Owl, Dainty, Matcha)
    • Accent color (auto-syncs to a theme's signature color on switch, independently editable after)
    • Font family (system/sans/serif/monospace/Monaspace) and size
    • Layout width, density, card style, corner radius
    • Library root override (point the browser at a specific subfolder)
    • Video player size All applied via CSS custom properties, fetched once per page load by static/theme.js.
  5. Header/footer redesign — branded gradient header, sticky footer holding Settings + Help links (consolidated from several scattered links). Clicking the "OfflineU" wordmark anywhere calls /reset_course (clears the loaded course) rather than just / (which would keep showing the same course).

  6. Help page (/help) — "How to Use" / "Supported File Types" moved here from the cluttered main dashboard.

  7. Recently Viewed — tracks the last 5 lessons viewed across all courses (/app/data/recent_views.json), shown only on the no-course-loaded library screen (not on an individual course's page). Clicking an entry loads that lesson's course if it isn't already active, then jumps to the lesson (/recent/open).

  8. Watch-progress trackingLesson.duration_seconds added alongside the existing progress_seconds. Lesson rows in the course tree and in Recently Viewed show either a green checkmark (completed), an accent-colored "NN% watched" badge + thin progress bar (in progress), or a plain "○" (untouched).

  9. Library curation (Settings → Curate Library) — hide individual courses or entire folders from the browser without touching anything on disk. Persisted in /app/data/hidden_paths.json. Hiding a folder hides everything inside it. /library (normal browsing) filters hidden items out; /library/manage (used by the Settings UI) includes them flagged hidden: true so they can be un-hidden.

Rough edges fixed this session

  • Single-section course detection_looks_like_course() in offlineu_core.py now also recognizes a course with exactly one section subfolder, if that subfolder's name reads as a section label ("Section 1", "Module 2", "Chapter 3", etc. — see _SECTION_NAME_RE). This deliberately doesn't touch the general "publisher folder holding one course" case (e.g. Pluralsight/Docker Deep Dive/), since that subfolder is named after the course, not a section.
  • Empty folder after hiding all its courses — added _contains_visible_course(), used by list_library_directory() when skip_hidden=True, so a folder whose every course has been individually curated out no longer appears as a drillable (but empty) directory in normal browsing. The Settings curation UI (skip_hidden=False) is unaffected — it still needs to show those folders so courses can be un-hidden.
  • Matcha theme accuracy — replaced the hand-approximated Matcha palette with real values sourced from lucafalasco/matcha's published VS Code theme JSON.
  • Dainty theme swapped for Nord — turned out "Dainty" (HotWordland/dainty-vscode) is a Lab-space theme generator, not a fixed palette — there was no single official hex set to verify against, so approximating it was never really fixable. Replaced it with Nord, which has a well-documented, fully verifiable official palette (nordtheme.com). Anyone with theme: "dainty" already saved in settings.json falls back gracefully to the dark default on next load.

Mobile UI overhaul + in-app renaming (this session)

  • Fixed the mobile text-overlap bug.lesson-item/.tree-header in course_dashboard.html had no min-width: 0 or truncation on their flex children, so long course/lesson names (especially scene-release-style dotted names like Udemy.crash.course.electronics...Mimir with zero whitespace to wrap on) would overflow and visually overlap neighboring text on narrow screens. Fixed with min-width:0 + ellipsis truncation (desktop/tablet) and a @media (max-width: 600px) block that stacks title/metadata into separate lines with overflow-wrap: break-word (the dotted-name case needed this specifically — plain white-space: normal doesn't create a break opportunity in a string with no spaces).
  • Library browser redesigned — replaced the nested-indent accordion (AIChatGPT → course, each level eating horizontal space) with a single-level drill-down + breadcrumb (Library / AI / ChatGPT), the standard mobile file-browser pattern. Tapping a folder now replaces the list instead of nesting under it. In-course lesson tree keeps its old expand-in-place behavior (shallower, wasn't the reported problem).
  • Rename courses/folders from Settings — "Curate Library" is now "Manage Library": each row gets a ✏️ button alongside Hide/Show that renames the directory in place on disk via a new POST /api/rename-path. Handles path-traversal/collision validation, and rebases any hidden_paths.json / recent_views.json entries nested under the renamed path (progress files need no rebasing — they live inside the directory and are keyed relative to it). If you rename your currently loaded course's folder, the app resets to the library view rather than serving a stale path.

Eight usability features (this session)

Asked for after brainstorming what could make the app nicer to use — see .claude/plans/dazzling-yawning-glacier.md for the full scoping rationale.

  1. Course thumbnailsfind_course_thumbnail() looks for cover/folder/thumbnail/thumb/poster (.jpg/.jpeg/.png/.webp) directly inside a course folder; served via GET /library/thumbnail. Shown in the Library browser and Recently Viewed/Continue Watching, falling back to the emoji icon if there's no cover image.
  2. Library searchGET /library/search?q=... recursively walks the library (reusing list_library_directory's hidden-path filtering) and matches on course name. Debounced search box above the Library browser.
  3. Continue Watching — reuses the Recently-Viewed history (MAX_RECENT_VIEWS bumped 5→20) rather than a full library-wide progress index; split into "in progress" vs. "recently touched" on the dashboard.
  4. Playback speed controlsettings.playback_speed (0.75x2x), persisted the same way video player size already was; speed buttons on the lesson page, applied on load via the existing /api/settings fetch.
  5. Installable app (PWA)static/manifest.json + static/icons/icon-{192,512}.png, linked from every template's <head>. Install-only, deliberately no service worker/offline caching — this app has no real "offline" mode (it's a thin client over the Flask backend/NAS files), so a caching SW would just create stale-content bugs. Also fixed: lesson_view.html was missing the viewport meta tag entirely, so lesson/video pages weren't actually mobile-responsive until now.
  6. Sort in the Library browser — client-side Name A→Z/Z→A only; progress-based filtering (not-started/in-progress/done) was scoped out, since it'd need the same expensive per-course scan as #3's "ideal" version.
  7. Per-lesson notes — a note field alongside completed/ progress_seconds in each lesson's existing progress-file entry, via ProgressTracker.update_lesson_note() / POST /api/lesson-note. Fixed a real bug in update_lesson_progress while at it: it always overwrote the entire lesson entry, which would have silently deleted a saved note on the next routine playback-progress autosave.
  8. Mark course as watchedPOST /api/course/mark-watched, scoped to whatever course is currently loaded (not an arbitrary library path, to avoid re-validating/re-scanning an untrusted path). Button lives in the course stats card, behind a confirm prompt.

Known limitations still open

  • App is unauthenticated by design (matches upstream) — settings and hidden-path curation apply app-wide, not per-browser/per-user.

Workflow that's been in use

Edit locally → git add . && git commit -m "..." && git push to the private Gitea repo → redeploy the stack in Dockhand (which builds from the fresh git pull + pull_policy: build).