metube/UI_IMPLEMENTATION_PLAN.md

17 KiB
Raw Permalink Blame History

MeTube UI Redesign — Implementation Plan

Date: 2026-08-11 Status: Plan (ready to implement) Design source: design/metube-redesign.html (this plan supersedes §6 of 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.tsshow(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.