UI improved
This commit is contained in:
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user