# 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` 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=`: - 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/.` 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=…`; `` 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.