336 lines
17 KiB
Markdown
336 lines
17 KiB
Markdown
# 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 1–5 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 1–3 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.5–1 day |
|
||
| 2. App shell & navigation | 1 day |
|
||
| 3. List redesign | 1–1.5 days |
|
||
| 4. Thumbnails (front + back) | 0.5 day |
|
||
| 5. Polish & cleanup | 0.5 day |
|
||
| **Total** | **~3.5–4.5 days** |
|
||
|
||
Phases 1–3 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.
|