17 KiB
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 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 brokentest/ 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/thumbnailendpoint.
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:
- 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. - A stats strip (active count, total speed, queue count, library size) replaces the navbar counters.
- Active downloads become a pinned thumbnail panel with progress, speed, ETA and cancel, instead of the top tab of a table.
- Completed items get a media-library grid with a list/grid toggle, duration badges and hover actions.
- A floating batch bar (Start / Move / Delete / clear) replaces the always-visible disabled toolbar buttons.
- Errors and feedback move to toasts with optional action buttons.
- Empty states for every list, and a single-row command bar (URL input + quality/format chips + folder picker) instead of the full form.
- 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):
MKVformat chip: the backend'sFormatslist has nomkv; 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
—untilsizeis 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-monoin 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 overngx-cookie-serviceformetube_quality,metube_format,metube_auto_start,metube_sort_order,metube_active_tab,metube_theme, plus newmetube_status,metube_folder,metube_viewkeys.toast.service.ts—show(message, {error?, actionLabel?, onAction?}), auto-dismiss, rendered byToastsComponent.downloads.service.ts— kept as-is except: add optionalduration?: number/thumbnail?: stringto theDownloadinterface (backend §5) and addmoveById(ids, folder)convenience that calls the existingPOST /set_folderwith 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.
npm install @angular/cdk@^19(needed for virtualization in Phase 3).- Add
_tokens.sass; wire intostyles.sass(no visual change yet). - Add
preferences.service.ts(migrate existing cookie reads/writes) andtoast.service.ts+ToastsComponent. - Extract
CommandBarComponent,AdvancedOptionsComponent,ActiveDownloadsComponent,DownloadListComponent+ row/card components from the monolith, keeping the current tables and behavior identical. - Register everything in
app.module.ts; delete now-unused@ViewChildnative-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.
- Sidebar: brand + version (from existing
/versionfetch), status nav (正在下载 / 排队中 / 已完成 with live count badges), folder nav (全部 + each distinctfoldervalue with counts, 未分类 for''), footer with 高级选项, 日志, and a theme toggle (preserves existing light/dark/auto). Replace the Bootstrap tabs; persistmetube_status/metube_folder. - Stats strip: active count, total speed (
totalSpeed | speed), queue count, library size (sum ofsizeoverdone,FileSizePipe). - 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 currentsetQualities()behavior), folder picker (ng-select with tag creation), 高级选项 link. - 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. - 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.
- 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.
- 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 toAppComponent, which callsstartById/delById/setFolderdirectly (removes the currentdl.checkedmutation andMasterCheckboxmachinery). - 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...").
- 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). - 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. - Virtualization (queued list + completed grid):
- Queued:
cdk-virtual-scroll-viewportwith fixed row height over a flat array. - Completed grid: chunk items into fixed-height rows of N columns (N from a
ResizeObserveron the container) and virtualize rows. Fallback if chunking proves fiddly: non-virtualized grid withloading="lazy"images (303 cards is acceptable; re-evaluate if scrolling degrades). trackByon item id.
- Queued:
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):
DownloadInfogainsself.thumbnailandself.durationextracted from the yt-dlpentry(entry.get('thumbnail') or thumbnails[-1],entry.get( 'duration')) — these already persist through the existing shelve files, so completed items survive restarts.- New route
GET /thumbnail?id=<download_id>:- Look up the id across queue/pending/done shelves, take the stored
thumbnailURL (never an arbitrary client-supplied URL). - Fetch with
aiohttp.ClientSession, cache bytes underSTATE_DIR/thumbnails/<id>.<ext>with long-livedCache-Control. - 404/placeholder SVG when no thumbnail exists (audio-only downloads).
- Look up the id across queue/pending/done shelves, take the stored
- Optionally, trim the socket payload: exclude the huge
entrydict from serializedDownloadInfonow that onlythumbnail/durationare needed client-side (keeps the existingallevent contract otherwise intact).
Frontend:
Downloadinterface gainsduration?: number,thumbnail?: string.thumbnailUrl(id)helper →/thumbnail?id=…;<img loading="lazy">in rows, cards and the active panel; placeholder block while missing/loading.- 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
- Dark theme pass over every surface (verify both palettes at 375/768/1280px).
- Mobile: sidebar drawer with hamburger (mockup behavior), command bar wraps, batch bar safe-area spacing, slide-over becomes a bottom sheet at narrow widths.
- Activity drawer (
ActivityDrawerComponent) for recent events with clear button, fed by existing/events+eventReceived. - Optionally bundle JetBrains Mono via
@fontsource/jetbrains-mono. - Dead tooling cleanup: remove
tslint.json/codelyzer/ng lintscript and the protractor e2e scaffolding, per the doc's §6.4 phase 5. - 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.shperAGENTS.md(bumps the version inDEPLOY.mdin 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.