metube/UI_IMPLEMENTATION_PLAN.md

336 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MeTube UI Redesign — Implementation Plan
Date: 2026-08-11
Status: Plan (ready to implement)
Design source: [`design/metube-redesign.html`](design/metube-redesign.html) (this plan supersedes §6 of [`UI_REDESIGN.md`](UI_REDESIGN.md))
> **Implementation log (2026-08-11):** Phases 15 are implemented and
> verified. Deltas from this plan: the completed **grid** uses the accepted
> fallback (§Phase 3 item 4) — plain CSS grid with `loading="lazy"` images
> rather than chunked row virtualization; the pre-existing broken `test`/
> karma setup and the tslint/codelyzer/protractor scaffolding were removed in
> Phase 5; the UI was verified headless (desktop + 375px mobile) against a
> live backend with seeded queue/completed data, including the `/thumbnail`
> endpoint.
## 0. What the mockup improves over the original proposal
`metube-redesign.html` implements the spirit of `UI_REDESIGN.md` but resolves
several open questions and simplifies the riskiest parts:
1. **Folder grouping moved from in-list headers to a sidebar nav.** The doc's
open question ("per-folder navigation") is answered: folders are a
first-class sidebar section with counts, and the list itself is flat. This
also removes the doc's biggest technical risk — flattening collapsible
group headers into a `cdk-virtual-scroll-viewport` — entirely.
2. **A stats strip** (active count, total speed, queue count, library size)
replaces the navbar counters.
3. **Active downloads become a pinned thumbnail panel** with progress, speed,
ETA and cancel, instead of the top tab of a table.
4. **Completed items get a media-library grid** with a list/grid toggle,
duration badges and hover actions.
5. **A floating batch bar** (Start / Move / Delete / clear) replaces the
always-visible disabled toolbar buttons.
6. **Errors and feedback move to toasts** with optional action buttons.
7. **Empty states** for every list, and a single-row command bar (URL input +
quality/format chips + folder picker) instead of the full form.
8. **Mobile**: the sidebar becomes a drawer and everything reflows to
single-column — no horizontal table scroll.
## 1. Constraints & assumptions
- **Stay on the current stack**: Angular 19 + Bootstrap 5.3 + ng-bootstrap +
ng-select + FontAwesome + PWA. No framework rewrite (same rationale as the
design doc).
- **Labels are Chinese** per the mockup (`lang="zh-CN"`). The current app is
English; the mockup is the source of truth, and this matches the fork's
usage. All strings are inline template text so switching language later is a
mechanical pass.
- **No new features beyond the design** — no playlist monitor, no new download
options. The one additive backend change is the thumbnail endpoint (§5).
- **Mockup data vs real data deltas** (implement from real data, not the
mockup):
- `MKV` format chip: the backend's `Formats` list has no `mkv`; chips are
derived from the real list (`any`, `mp4`, `m4a`, `mp3`, `opus`, `wav`,
`flac`, `thumbnail`).
- Queued rows show size in the mockup, but size is unknown until download
starts; render `—` until `size` is available.
- "Reveal in folder" has no backend endpoint; map it to opening the download
URL (skip the icon until a backend reveal exists).
- Folder nav counts are computed from real maps, not the mockup's numbers.
- **Theme**: keep the existing light/dark/auto Bootstrap mechanism; the
mockup's palette (green accent, oklch) is implemented as CSS custom
properties with dark-mode overrides (§3).
- **Fonts**: the mockup loads JetBrains Mono from Google Fonts. For a
self-hosted LAN app, do **not** depend on an external font CDN; use the
system mono fallback stack from the mockup and optionally bundle
`@fontsource/jetbrains-mono` in Phase 5.
## 2. Target component structure
Break the 744-line `app.component.ts` / 595-line template monolith into:
```
AppComponent app shell: sidebar + command bar + stats +
content + toasts + slide-over; owns nav state,
metrics, versions, theme
├── SidebarComponent brand/version, status nav (counts), folder
│ nav (counts), footer (advanced, activity,
│ theme toggle)
├── CommandBarComponent URL input + Add, quality/format chips,
│ folder picker (ng-select), advanced link
├── StatsStripComponent active count · total speed · queue · storage
├── ActiveDownloadsComponent pinned panel: rows with thumb, progress,
│ speed/ETA, cancel
├── DownloadListComponent reusable queued/completed list: filter, sort,
│ ├── DownloadRowComponent queue-style row (checkbox, thumb, meta, size,
│ │ hover actions)
│ └── DownloadCardComponent completed grid card (checkbox, thumb,
│ duration, title, folder, size, actions)
├── AdvancedOptionsComponent slide-over: download behavior, batch ops,
│ cookies
├── ActivityDrawerComponent recent events (Phase 5)
└── ToastsComponent + service replaces alert()
```
Shared services:
- `preferences.service.ts` — thin wrapper over `ngx-cookie-service` for
`metube_quality`, `metube_format`, `metube_auto_start`, `metube_sort_order`,
`metube_active_tab`, `metube_theme`, plus new `metube_status`,
`metube_folder`, `metube_view` keys.
- `toast.service.ts``show(message, {error?, actionLabel?, onAction?})`,
auto-dismiss, rendered by `ToastsComponent`.
- `downloads.service.ts` — kept **as-is** except: add optional
`duration?: number` / `thumbnail?: string` to the `Download` interface
(backend §5) and add `moveById(ids, folder)` convenience that calls the
existing `POST /set_folder` with an id list.
The triplicated table markup (queue/pending/done) collapses into two
`DownloadListComponent` instances.
## 3. Design tokens & styling
New `ui/src/styles/_tokens.sass` (imported from `styles.sass`), defining the
mockup's oklch palette as CSS custom properties, with dark overrides under
`[data-bs-theme="dark"]`:
| Token | Light | Dark |
|---|---|---|
| `--bg` | `oklch(98% 0.005 250)` | `oklch(16% 0.012 250)` |
| `--surface` | `oklch(100% 0 0)` | `oklch(20% 0.014 250)` |
| `--fg` | `oklch(22% 0.02 240)` | `oklch(92% 0.01 240)` |
| `--muted` | `oklch(50% 0.018 240)` | `oklch(68% 0.012 240)` |
| `--border` | `oklch(90% 0.008 240)` | `oklch(30% 0.014 250)` |
| `--accent` | `oklch(58% 0.16 145)` | `oklch(66% 0.15 145)` |
| `--accent-strong` | `oklch(48% 0.14 145)` | `oklch(74% 0.13 145)` |
| `--accent-tint` | `oklch(95% 0.03 145)` | `oklch(26% 0.05 145)` |
| `--warn` / `--danger` | `oklch(70% 0.15 75)` / `oklch(55% 0.19 25)` | same, lightened |
Plus `--font-mono` (JetBrains Mono → system fallback), `--radius: 8px`,
`--sidebar-w: 236px`. Global styles: `body` background/color/font, `.num`
tabular-nums utility, focus-visible accent outline.
Keep Bootstrap loaded (the app shell, ng-select, modals still use it), but the
redesigned surfaces use the tokens via component Sass. The legacy navbar /
`styles.sass` overrides are deleted once the shell lands.
## 4. Phases
Each phase ends with `cd ui && npm run build` green and the manual checklist
in §7 passing for the touched areas. Phases 13 deliver the bulk of the UX
gain; Phase 4 is separable if backend changes need to wait.
### Phase 1 — Foundations & component split (no visual change)
Goal: establish structure and services before touching the design, so
regression risk is isolated.
1. `npm install @angular/cdk@^19` (needed for virtualization in Phase 3).
2. Add `_tokens.sass`; wire into `styles.sass` (no visual change yet).
3. Add `preferences.service.ts` (migrate existing cookie reads/writes) and
`toast.service.ts` + `ToastsComponent`.
4. Extract `CommandBarComponent`, `AdvancedOptionsComponent`,
`ActiveDownloadsComponent`, `DownloadListComponent` + row/card components
from the monolith, keeping the current tables and behavior identical.
5. Register everything in `app.module.ts`; delete now-unused `@ViewChild`
native-element manipulation and triplicated markup as extraction proceeds.
Acceptance: UI looks/behaves exactly as before; `npm run build` passes.
### Phase 2 — App shell & navigation
Goal: the mockup's frame — sidebar, stats strip, command bar, slide-over,
pinned active panel — over the still-table-based lists.
1. **Sidebar**: brand + version (from existing `/version` fetch), status nav
(正在下载 / 排队中 / 已完成 with live count badges), folder nav (全部 +
each distinct `folder` value with counts, 未分类 for `''`), footer with
高级选项, 日志, and a theme toggle (preserves existing light/dark/auto).
Replace the Bootstrap tabs; persist `metube_status` / `metube_folder`.
2. **Stats strip**: active count, total speed (`totalSpeed | speed`), queue
count, library size (sum of `size` over `done`, `FileSizePipe`).
3. **Command bar**: URL input + 添加到队列, quality chips and format chips
derived from the real `Formats`/qualities (render as chips; if more than
~4, a "more" popover — keep current `setQualities()` behavior), folder
picker (ng-select with tag creation), 高级选项 link.
4. **AdvancedOptionsComponent slide-over** with scrim, Escape/click-close:
auto-start, strict playlist, playlist limit, name prefix, import/export/
copy URLs, cookies (existing `POST /cookie`; show saved cookie domain from
configuration if available). Wire batch import modal into the slide-over.
5. **ActiveDownloadsComponent** pinned above the list: rows with 16:9
thumbnail placeholder, title, progress bar, pct/size/speed/ETA, cancel.
Until Phase 4, thumbnails show the placeholder block.
6. **Events**: keep the existing inline events box under the new shell (or
move to a toasts-driven minimal version); full drawer is Phase 5.
Acceptance: all add/start/cancel/delete/folder/cookie/import-export flows work
from the new shell; tab cookie `metube_active_tab` still respected (maps
queue→downloading, pending→queued, done→completed).
### Phase 3 — List redesign
Goal: replace tables with the mockup's lists — filter, sort, multi-select,
batch bar, grid/list toggle, empty states, virtualization.
1. **DownloadListComponent** for queued and completed:
- Toolbar: filter input (title substring), sort select (date / title /
size) + direction toggle (preserve existing asc/desc cookie), view toggle
(list/grid; grid shown only for completed), 全选, result count.
- Flat items only — folder filtering comes from the sidebar nav, so no
group-header flattening is needed.
- Selection state: a `Set<string>` of ids owned by the list component,
cleared on nav change. Batch actions emit id lists up to `AppComponent`,
which calls `startById` / `delById` / `setFolder` directly (removes the
current `dl.checked` mutation and `MasterCheckbox` machinery).
- Batch bar: floating bottom bar (mockup styling) — queued: 开始 / 移动到…
/ 删除 / 取消选择; completed: 移动到… / 删除 / 取消选择 (start N/A).
Completed toolbar keeps 清除已完成 / 清除失败 / 重试失败 / 下载所选 as
ghost buttons.
- Empty states per mockup (正在下载 fixed-on-top hint; 没有匹配的条目 for
filter misses; plus a true empty-state for an empty queue/library).
- Skeleton rows while `downloads.loading` (first connect replaces
"Connecting to server...").
2. **DownloadRowComponent** (queued): checkbox, 88px thumb, title, folder +
quality·format badges, added time, size (`—` when unknown), hover actions:
start (pending only), cancel/delete, external link, folder edit (ng-select
with tag creation → `POST /set_folder`).
3. **DownloadCardComponent** (completed): hover checkbox, 16:9 thumb with
duration badge, 2-line title, folder badge, size, actions: download file,
external link, folder edit, delete, retry (error items). Grid uses
`repeat(auto-fill, minmax(218px, 1fr))` per mockup.
4. **Virtualization** (queued list + completed grid):
- Queued: `cdk-virtual-scroll-viewport` with fixed row height over a flat
array.
- Completed grid: chunk items into fixed-height rows of N columns (N from a
`ResizeObserver` on the container) and virtualize rows. Fallback if
chunking proves fiddly: non-virtualized grid with `loading="lazy"` images
(303 cards is acceptable; re-evaluate if scrolling degrades).
- `trackBy` on item id.
Acceptance: smooth filter/sort/scroll at 230 queued + 303 completed; batch
select/start/cancel/move/delete correct; folder nav filters; selection resets
on nav change; all previous row actions (folder edit, retry, download file)
still work.
### Phase 4 — Thumbnails (backend additive)
Goal: thumbnail-first rows/cards without hot-linking remote images.
Backend (`app/ytdl.py`, `app/main.py` — no queue/precheck/persistence changes):
1. `DownloadInfo` gains `self.thumbnail` and `self.duration` extracted from the
yt-dlp `entry` (`entry.get('thumbnail') or thumbnails[-1]`, `entry.get(
'duration')`) — these already persist through the existing shelve files, so
completed items survive restarts.
2. New route `GET /thumbnail?id=<download_id>`:
- Look up the id across queue/pending/done shelves, take the stored
`thumbnail` URL (never an arbitrary client-supplied URL).
- Fetch with `aiohttp.ClientSession`, cache bytes under
`STATE_DIR/thumbnails/<id>.<ext>` with long-lived `Cache-Control`.
- 404/placeholder SVG when no thumbnail exists (audio-only downloads).
3. Optionally, trim the socket payload: exclude the huge `entry` dict from
serialized `DownloadInfo` now that only `thumbnail`/`duration` are needed
client-side (keeps the existing `all` event contract otherwise intact).
Frontend:
1. `Download` interface gains `duration?: number`, `thumbnail?: string`.
2. `thumbnailUrl(id)` helper → `/thumbnail?id=…`; `<img loading="lazy">` in
rows, cards and the active panel; placeholder block while missing/loading.
3. Duration formatting pipe (`mm:ss` / `h:mm:ss`) for the badge.
Acceptance: thumbnails render for queued/active/completed after a cold reload;
no third-party image requests leave the host; audio items show placeholder.
### Phase 5 — Polish & cleanup
1. Dark theme pass over every surface (verify both palettes at 375/768/1280px).
2. Mobile: sidebar drawer with hamburger (mockup behavior), command bar
wraps, batch bar safe-area spacing, slide-over becomes a bottom sheet at
narrow widths.
3. Activity drawer (`ActivityDrawerComponent`) for recent events with clear
button, fed by existing `/events` + `eventReceived`.
4. Optionally bundle JetBrains Mono via `@fontsource/jetbrains-mono`.
5. Dead tooling cleanup: remove `tslint.json`/codelyzer/`ng lint` script and
the protractor e2e scaffolding, per the doc's §6.4 phase 5.
6. Update `app.component.spec.ts` (currently asserts on the removed template)
or delete it.
Acceptance: full manual QA (§7); PWA build works; no console errors.
## 5. Backend change summary (Phase 4 only)
| File | Change |
|---|---|
| `app/ytdl.py` | `DownloadInfo.__init__`: set `thumbnail`, `duration` from `entry` |
| `app/main.py` | add `GET /thumbnail?id=` route + cache dir; optional serialization trim |
Everything else in the backend (queue, precheck, conflict resolution,
persistence, socket events, cookie handling) is untouched.
## 6. Out of scope
- Framework/stack change; backend queue/persistence rework.
- Playlist monitor (separate planned effort).
- Reveal-in-folder OS action (no backend support).
- New download options; localization framework.
- Publishing: when the redesign is merged, publish via `./publish.sh` per
`AGENTS.md` (bumps the version in `DEPLOY.md` in the same commit).
## 7. Manual test checklist
Run through after Phase 3 (core) and again after Phase 5 (full):
- Add single URL and playlist URL; Enter key adds; invalid/empty URL toasts.
- Quality/format chips persist across reload; folder picker persists; tag
creation in picker works.
- Cancel active download; cancel/delete queued; delete completed; retry
failed; clear completed/failed; download selected files.
- Batch: select 1 and many; floating bar actions on queued vs completed;
move-to-folder batch moves files on disk (`POST /set_folder`).
- Import URLs (with cancel), export, copy; cookie paste flow.
- Filter by title; sort by date/title/size both directions; view toggle;
select-all respects filter; counts in sidebar/stat strip match maps.
- 230+ queued / 300+ completed: scrolling stays smooth, selection is
responsive, thumbnails lazy-load, no layout jump while filtering.
- Theme light/dark/auto persists and all surfaces are legible; PWA offline
shell loads.
- Mobile 375px and 768px: drawer navigation, no horizontal scroll, batch bar
reachable, slide-over usable.
## 8. Effort estimate
| Phase | Estimate |
|---|---|
| 1. Foundations & split | 0.51 day |
| 2. App shell & navigation | 1 day |
| 3. List redesign | 11.5 days |
| 4. Thumbnails (front + back) | 0.5 day |
| 5. Polish & cleanup | 0.5 day |
| **Total** | **~3.54.5 days** |
Phases 13 are the bulk of the UX gain; the mockup's folder sidebar removes
the doc's main virtualization risk, so the list phase is more contained than
originally estimated.