UI improved

This commit is contained in:
Harwinder Sekhon
2026-07-27 13:58:47 -04:00
parent e78f2a81e0
commit 31b01a1a18
11 changed files with 3257 additions and 412 deletions
+141 -7
View File
@@ -1,13 +1,27 @@
# CourseHub
Self-hosted course video player: PHP 8.0+ + MySQL/MariaDB + Bootstrap 5 + [Plyr](https://github.com/sampotts/plyr).
Self-hosted course video player: PHP 8.0+ + MySQL/MariaDB, its own CSS design
system, [Plyr](https://github.com/sampotts/plyr) and
[anime.js](https://animejs.com/). No CSS framework.
Single-page app: `index.php` is a static shell; all navigation, login, browsing,
playback, and scanning happen over AJAX against the JSON API in `api/`
(`auth.php`, `library.php`, `progress.php`, `scan.php`), with hash-based routes
(`#/`, `#/folder/N`, `#/watch/N`) so views switch without page reloads and
browser back/forward still work. Only `stream.php` (the mp4 bytes) is a plain
HTTP resource.
Single-page app: `index.php` is a static shell (sidebar, search header, mobile
tab bar); all navigation, login, browsing, playback, scanning and settings
happen over AJAX against the JSON API in `api/` (`auth.php`, `library.php`,
`progress.php`, `scan.php`, `quiz.php`, `reset.php`), with hash-based routes so
views switch without page reloads and browser back/forward still work:
| Route | Screen |
| --- | --- |
| `#/` | home — resume card, stats, in progress, last scan |
| `#/library`, `#/library/<shelf>` | every course, as shelves or a filtered catalog |
| `#/folder/N` | a course or section: sections + content |
| `#/watch/N` | player, playlist, section quizzes |
| `#/quiz/N` | take and grade a quiz |
| `#/search/Q` | courses, videos and quizzes matching `Q` |
| `#/settings` | library folder, preferences, reset |
Only `stream.php` (the mp4 bytes) and `thumb.php` (cached course thumbnails)
are plain HTTP resources.
- Recursively scans a videos directory for `.mp4` files and mirrors the folder
hierarchy (courses → sections → videos, any nesting depth).
@@ -16,6 +30,15 @@ HTTP resource.
percentage bar computed over its whole subtree.
- "Continue watching" strip, auto-play next video, playback speed, keyboard
shortcuts (Plyr), completed checkmarks, natural sorting (`Video2` before `Video10`).
- **Search** courses, sections, videos and quizzes from the top bar.
- **Reset progress** for one video, one quiz, a whole course/section, or the
entire account.
- Course and section listings switch between **grid and list** layout, and the
library between **shelves and catalog**; both choices are remembered.
- **Shelves** group courses by how far you are: in progress, finished, not
started. The app has no course categories, so progress is what sorts them.
- **Course thumbnails** — a real frame captured from each course's first
video, 40% of the way through it.
- Seekable streaming via HTTP Range requests; files are addressed by DB id only
(no path traversal).
@@ -51,6 +74,15 @@ Top-level folders become courses on the home page.
the web server / PHP user).
- DB credentials (change the password in production).
- `ALLOW_REGISTRATION`: set `false` once your users have accounts.
- `THUMBNAILS_DIR`: where generated course thumbnails are cached — must be
*writable* by the web server / PHP user (default: `cache/thumbnails`
next to `config.php`).
- `THUMBNAIL_SEEK_PERCENT`: how far into a course's first video to capture
its thumbnail (default `0.4`, i.e. 40%).
An `ffmpeg` binary on `$PATH` is required for thumbnails (and is the
fallback for video duration if the pure-PHP mp4 parser can't read a file).
Without it, courses just keep their colour-block placeholder.
3. **Run** — with the PHP built-in server for testing:
@@ -96,6 +128,108 @@ correct set. Best score, last score, and attempt count are stored per user;
a quiz is *completed* once every question is answered correctly in one
attempt, and quiz scores count toward folder progress alongside videos.
## Thumbnails
Each course's card is a real frame, not just a colour block: the scanner finds
the course's first video — in the same order you'd browse to it (subfolders in
listing order, then that folder's videos) — and asks `ffmpeg` for a frame
`THUMBNAIL_SEEK_PERCENT` (default 40%) of the way through it, scaled and
cropped to 640×360. The result is cached at `THUMBNAILS_DIR/<course id>.jpg`
and served by `thumb.php?course_id=N`, which looks the file up by that ID only
(same containment check as `stream.php`) and sends normal `ETag`/
`Last-Modified` caching headers.
- Regeneration only happens when a course's first video has changed since the
last scan (tracked via `folders.thumbnail_video_id`), so rescans stay fast.
- If `ffmpeg` is missing, a video's duration can't be read, or generation
otherwise fails, the course simply keeps its colour-block placeholder — the
`<img>` 404s and the client removes it (`onerror="this.remove()"`), no error
is shown to the user. Failures are logged (`error_log`) and rolled up into
the scan's `thumbnails_failed` count.
- `thumbnail_video_id`/`thumbnail_generated_at` on `folders` are added
automatically on first use (`ensureThumbnailColumns()`) if a database
created before this feature doesn't have them yet — no manual migration
needed, the same self-healing `app_meta` uses.
## Search
The top-bar box searches as you type (from two characters, debounced) and also
on Enter. It matches folder, video and quiz **names** and their relative
**paths**, so `Course 1/Intro` lists everything under that section. Results are
grouped into courses/sections, videos and quizzes, with your progress shown on
each row. Searches live at `#/search/<query>`, so a result page can be
bookmarked or shared.
## Resetting progress
`api/reset.php` deletes progress rows for the logged-in user only — never for
anyone else, and never any video, quiz or folder itself. Four scopes:
| Where | Scope | Clears |
| --- | --- | --- |
| Player page → **Reset** | `video` | that video's position + completion |
| Quiz page → **Reset attempts** | `quiz` | that quiz's attempts, best/last score |
| Folder page → **Reset progress** | `folder` | everything in that folder's subtree |
| Library list row → ↺ | `folder` | that one course's subtree |
| Settings / avatar menu → **Reset all progress** | `all` | every video and quiz on the account |
Each one asks for confirmation first, and the reply reports how many video and
quiz rows were removed. Resetting the video you are watching also suppresses
the player's teardown save, so the position you just cleared isn't written back.
## Look and feel
The UI implements the **Course Library** design (a Claude Design document).
`assets/style.css` is the whole design system — tokens first, then layout, then
components. There is no CSS framework under it, and the icons are inline SVG
from the design rather than an icon font.
| Token | Hex | Used for |
| --- | --- | --- |
| Paper | `#EFEAE1` | outermost ground |
| Surface | `#FAF8F5` | app background |
| Sidebar | `#F5F1EA` | left rail |
| Card | `#FFFFFF` | cards, rows, dialogs |
| Ink | `#1A1814` → `#B8B0A2` | four text weights (`--ink` … `--ink-4`) |
| Accent | `#1F6F5C` | primary action, links, progress, completion |
| Danger | `#B84A3C` | reset and errors |
Type: **Inter** throughout — headings, interface text, and counts/durations/
paths (the design's own 'Elms Sans' ships only inside the design document).
Numeric text uses `tabular-nums` so columns of counts and durations still
line up without a separate monospace face.
Courses have no cover art, so each one gets a hue from the design's six-colour
set (clay, sage, slate, sand, plum, sky), picked from its folder id so it stays
the same across renders and reloads. That hue drives its thumbnail, its initial
badge and its progress bar.
anime.js drives the motion: views fade and stagger in, progress bars grow from
zero (so a reset is visible), correct quiz answers settle and wrong ones shake.
Everything is skipped when the CDN script is unavailable, when the visitor has
`prefers-reduced-motion: reduce` set, or when **Animate transitions** is turned
off in settings — the views then render in their final state.
Below 900 px the layout follows the design's mobile frame: the sidebar gives way
to a bottom tab bar, the search field becomes a pill with a round scan button
next to it, and grids drop to two columns.
## Settings
`#/settings` shows what the server actually knows and what this browser
remembers:
- **Library folder** — `VIDEOS_ROOT`, whether it is readable, indexed counts,
total running time, the watched threshold, and when the last scan ran.
- **Preferences**, stored per browser: autoplay next video, animate
transitions, open the library in list view.
- **Reset progress** — the account-wide reset, with a count of how many courses
currently hold progress.
Scan time lives in an `app_meta` key/value table. Reads tolerate the table not
existing, so an install created before it was added keeps working until its
next scan creates it.
## How progress works
- The player POSTs `{video_id, position, duration}` to `api/progress.php`