7.9 KiB
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 — withoutpull_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 browsertemplates/lesson_view.html— individual lesson pagetemplates/settings.html— new settings pagetemplates/help.html— new help pagestatic/theme.js— shared script that applies persisted display settings
Features added, roughly in order
-
Library browser — lazy, drill-down directory browsing instead of typing a full path.
/librarylists 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. -
PDF preview fix — lesson pages were fetching PDFs as text and dumping raw bytes. Now PDFs (and HTML) render in an iframe;
.docx/.doc/.rtfshow a "no preview, download instead" message rather than garbled text. -
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. -
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.
-
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). -
Help page (
/help) — "How to Use" / "Supported File Types" moved here from the cluttered main dashboard. -
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). -
Watch-progress tracking —
Lesson.duration_secondsadded alongside the existingprogress_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). -
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 flaggedhidden: trueso they can be un-hidden.
Rough edges fixed this session
- Single-section course detection —
_looks_like_course()inofflineu_core.pynow 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 bylist_library_directory()whenskip_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 insettings.jsonfalls back gracefully to thedarkdefault on next load.
Mobile UI overhaul + in-app renaming (this session)
- Fixed the mobile text-overlap bug —
.lesson-item/.tree-headerincourse_dashboard.htmlhad nomin-width: 0or truncation on their flex children, so long course/lesson names (especially scene-release-style dotted names likeUdemy.crash.course.electronics...Mimirwith zero whitespace to wrap on) would overflow and visually overlap neighboring text on narrow screens. Fixed withmin-width:0+ ellipsis truncation (desktop/tablet) and a@media (max-width: 600px)block that stacks title/metadata into separate lines withoverflow-wrap: break-word(the dotted-name case needed this specifically — plainwhite-space: normaldoesn't create a break opportunity in a string with no spaces). - Library browser redesigned — replaced the nested-indent accordion
(
AI→ChatGPT→ 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 anyhidden_paths.json/recent_views.jsonentries 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.
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).