The 34 kept themes were split 24 light-mode / 18 dark-mode - the light ones all shared the same washed-out near-white background (L=0.95), too bright per feedback. Added a force_mood override to build_theme() and regenerated all 24 as dark instead, same accent character, colored- dark background rather than near-white. That conversion collapsed a lot of previously-distinct light backgrounds into similar dark neutrals, so re-ran the same color-distance dedup pass across the full set: 8 more turned out to be near-twins once everything converged to dark (including a 3-way orange-on-dark cluster trimmed to one). Net: 53 candidates -> 34 kept, all dark/colored, zero white backgrounds - down from the 42 (24 light/18 dark) shipped last commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
9.0 KiB
OfflineU
Self-hosted course viewer for a local video/audio/text training library. Runs as a single Flask app in Docker, tracks watch progress and notes per lesson, and gives the whole library a dashboard-style home page instead of a bare file browser.
This is a personal deployment, not a public project — this doc is internal reference for running/maintaining it, not a pitch.
Screenshot predates the current theming/icon work — kept for a rough idea
of layout, not pixel-accurate.
Deployment
Deployed via docker-compose.yml, built directly from the Dockerfile in
this repo (pull_policy: build, not a registry pull):
services:
offlineu:
build: .
pull_policy: build
container_name: offlineu
network_mode: host
ports:
- "5000:5000"
environment:
- PUID=1000
- PGID=10
- TZ=America/New_York
- FLASK_ENV=production
volumes:
- /volume1/files/training:/app/courses # course library
- /volume2/docker/offlineu/data:/app/data # settings/progress/notes
restart: unless-stopped
Auto-deploy: the Gitea repo has a webhook to Dockhand — a push to main
triggers a full image rebuild from the Dockerfile (confirmed, not just a
container restart), so Dockerfile changes (e.g. adding ffmpeg) take effect
on the very next push without any manual step.
Local development
pip install -r requirements.txt
python offlineu_core.py --library-path /path/to/courses --debug
Opens on http://127.0.0.1:5000. ffprobe (from ffmpeg) needs to be on
PATH for video-duration lookups to work locally; without it those just
silently stay blank instead of erroring.
CLI options
| Option | Description |
|---|---|
--host |
Bind host (default 0.0.0.0) |
--port |
Bind port (default 5000) |
--debug |
Enable Flask debug mode |
--create-templates |
Regenerate default templates if missing |
--library-path |
Course library root (overrides COURSES_LIBRARY_PATH) |
course_path (positional) |
Load a specific course directly at startup |
Environment variables
| Variable | Default | Purpose |
|---|---|---|
COURSES_LIBRARY_PATH |
/app/courses |
Course library root |
OFFLINEU_DATA_DIR |
/app/data |
Where settings/progress/notes data lives |
AUTO_LOAD_COURSE |
— | Course path to load automatically on start |
Features
Library browsing
- Lazy-loading folder browser (grid or list view) with search, including an opt-in transcript search across subtitle files
- Course cards show media file count, total video/audio runtime, and completion % at a glance
- Cover art: uses a manually-placed
cover/folder/thumbnail/thumb/posterimage if a course has one, otherwise auto-generates one viaffmpegfrom a frame of the course's first video - Hide courses/folders from the browser without touching anything on disk; bulk-select to hide or queue several at once
- Bulk rename across course/folder names (find & replace)
Dashboard
- Library-wide stats (courses, lessons completed, time watched, time remaining, day streak) and a 90-day activity heatmap
- Next Up queue (manually curated, reorderable)
- Pick Back Up (courses with progress that have gone stale)
- Recently Added / Recently Viewed
- Surprise Me — dice icon in the header, random pick weighted toward incomplete courses
Playback & progress
- Video/audio player with resize, playback-speed presets, and resume-from- last-position
- Auto-tracks watch progress and completion per lesson
- Video/audio durations read via
ffprobeand cached persistently per course (.offlineu_duration_cache.json), so total runtime and per-lesson length show up before you've ever pressed play — not just after; the same cache backs the library-wide "time remaining" stat - Settings → "Precompute Lengths & Cover Art" walks the whole library in the background to populate durations and thumbnails up front, with live progress
Notes
- Timestamped notes per lesson, capturable via a keyboard shortcut without leaving the player
- Notes Hub: every note across the whole library, searchable, in one list
- Per-course study guide export (markdown, organized by section)
Personalization (Settings)
- 46 built-in themes (grouped in the dropdown: Base, Editor Themes, and 5 categories of website-inspired schemes sourced from Figma's "53 Unique Website Color Schemes" - 34 of the 53 kept, all as dark/colored backgrounds rather than the source page's mostly-white mockups, after cutting a couple of neon-bright accents and pruning near-duplicates by actual color distance), or a fully custom accent color, corner radius, and card style
- Font, text size, page width, and spacing density
- Default library path override + manual "Refresh Library" (bypasses the 5-minute filesystem-scan cache)
Backup & integrations
- One-click backup export and restore: settings, hidden-path choices, the Next Up queue, recent-view history, Outline config, and every course's progress/notes as a zip (not the course files themselves). Restore overwrites current data and needs a matching course folder to already exist for each course's progress to land - it's a "put my data back" action, not a merge
- Optional Outline integration — push a lesson's notes to an Outline document/collection
Data files
Everything under OFFLINEU_DATA_DIR (app-wide, not tied to a course):
| File | Contents |
|---|---|
settings.json |
Theme, layout, library path, etc. |
hidden_paths.json |
Courses/folders curated out of the browser |
next_up.json |
The Next Up queue, in order |
recent_views.json |
Cross-course "Recently Viewed" history |
outline_config.json |
Outline API token/collection mapping |
Per-course, written inside the course's own folder on the library volume:
| File | Contents |
|---|---|
.offlineu_progress.json |
Per-lesson completed/progress/duration/notes |
.offlineu_duration_cache.json |
ffprobe duration cache, keyed by (path, size, mtime) |
.offlineu_thumbnail.jpg |
Auto-generated cover art (only if no manual cover exists) |
Folder structure example
MyCourse/
├── Section 1/
│ ├── 01 - Intro.mp4
│ ├── 02 - Setup Guide.pdf
│ └── 03 - Quiz.html
├── Section 2/
│ ├── 04 - Advanced Tips.mp4
│ └── resources/
│ └── extras.md
├── .offlineu_progress.json ← created automatically
├── .offlineu_duration_cache.json ← created automatically
└── .offlineu_thumbnail.jpg ← created automatically, only if no manual cover exists
No metadata files needed — course/section/lesson names come straight from folder and file names.
Supported file types
| Type | Extensions |
|---|---|
| Video | .mp4 .mkv .avi .mov .webm .m4v .flv .wmv |
| Audio | .mp3 .wav .m4a .aac .ogg .flac |
| Documents | .txt .md .html .htm .pdf .docx .doc .rtf |
| Subtitles | .srt .vtt .ass .sub .sbv |
| Quizzes | Any doc file whose name contains quiz, exam, test, assessment, exercise, assignment, or homework |
Tools
tools/generate_website_scheme_themes.py is the one-off script that
produced the 34 "website scheme" theme entries in THEME_PALETTES (of
53 candidates - all 24 light-mode ones were converted to dark/colored
backgrounds per request, then 19 were cut across two review passes: 2
for a neon-bright accent, 17 for landing too close to another kept
scheme once mapped into actual UI colors, since forcing everything dark
made several collide that hadn't before) - not part of the running app,
kept for reference if those palettes ever need regenerating or
extending. build_theme() takes a force_mood='dark'|'light' override
for exactly this kind of bulk remapping. See its module docstring for
how it turned Figma's color-scheme screenshots into actual UI palettes.