# MeTube UI Redesign — Design Doc
Date: 2026-08-09
Status: Proposal (not yet implemented)
## 1. Background
MeTube (this fork) is a self-hosted web GUI for yt-dlp: Python 3.13 backend
(`aiohttp` + `python-socketio`, ~2,000 LOC) and an Angular 19 frontend
(Bootstrap 5.3 + ng-bootstrap, ~2,400 LOC, PWA).
The frontend is a single-screen app built around one monolithic component:
- `ui/src/app/app.component.ts` — 744 LOC
- `ui/src/app/app.component.html` — 595 LOC, with the same table markup
triplicated across three tabs
- `ui/src/app/app.component.sass` — 341 LOC
- Supporting: `downloads.service.ts` (socket/HTTP state), 2 checkbox
components, 4 pipes, theme/format helpers
The UI is stock Bootstrap: three tabs (Downloading / Pending / Completed),
each a 7-column `
`; an inline add-form; a collapsible "Advanced
Options" card; `alert()` for errors; cookie-persisted preferences.
## 2. Problems with the current UI
1. **Table-based lists**: 7 text columns, horizontal `overflow-auto` scroll on
mobile, no thumbnails — recognizing "which video is this" from text is slow.
2. **No design language**: default Bootstrap chrome, no empty states, no
skeleton loaders, "Connecting to server..." as the loading experience.
3. **Errors via `alert()`**: blocking, non-actionable, disappears.
4. **Monolithic component**: imperative `@ViewChild(...).nativeElement.disabled`
manipulation (7+ instances), triplicated template markup, dead tooling
(tslint/codelyzer/protractor). Every change is slower and riskier than it
should be.
5. **List tooling doesn't match real data volumes**: the primary user runs
~230 queued / ~300 completed items. Plain scrolling tables are unusable at
that scale — no filter, no sort, weak batch selection, no virtualization.
## 3. Decision: redesign in place, do NOT rewrite the stack
Evaluated and rejected: rewriting the frontend in Next.js (or Vite + React),
and rewriting the backend in Node.
Rationale:
- ~90% of the user-visible benefit comes from the **design**, not the
framework. Cards, thumbnails, filter/sort, batch actions, virtualization
are all achievable in the existing Angular + Bootstrap stack (Angular CDK
has virtual scrolling; Bootstrap renders cards fine).
- The rewrite's only unique benefit is code health / developer velocity,
which doesn't pay off for what is largely a one-off redesign.
- Regression risk: cookie-persisted prefs, folder tag-creation, socket
string-parsing quirks, PWA/service-worker setup would all need to be
rediscovered and re-tested.
- Next.js specifically is the wrong shape for this app: a LAN SPA with no
SEO/SSR needs. If a rewrite ever happens, Vite + React is the right-sized
tool — but it is not worth doing now.
- The backend must stay Python regardless: yt-dlp is Python-native, and the
subtle, battle-tested logic (precheck worker, filename-conflict resolution,
multiprocess lifecycle, playlist expansion, cookie wiring) lives in
`app/ytdl.py`. A Node port is 1.5–3 weeks of risk for zero user-visible gain.
If the frontend keeps needing substantial work after this redesign and Angular
becomes a friction point, revisit a rewrite then — with a proven design to
port, it becomes a mechanical ~2-day job.
## 4. Goals / Non-goals
Goals:
- Modern card-based, thumbnail-first UI for all three lists.
- Handle 200–500-item lists comfortably: filter, sort, folder grouping,
multi-select batch actions, virtualized rendering.
- Keep "now downloading" always visible.
- Replace `alert()` with toasts; add empty states and skeleton loading.
- Mobile-friendly (cards stack; no horizontal table scroll).
- Refactor the monolith into components as part of the work (only as much as
the redesign requires — no gratuitous re-architecture).
Non-goals:
- No framework/stack change (stays Angular 19 + Bootstrap 5.3).
- No backend queue/precheck/persistence changes, except the additive
thumbnail endpoint (§6.3).
- No new features beyond what the design requires (no new download options,
no playlist monitor — that's a separate planned effort).
## 5. Proposed design
### 5.1 Layout (desktop)
```
┌──────────────────────────────────────────────────────────────────────────┐
│ ▶ MeTube ⬇ 2 active · 12.4 MB/s 🌙 ⚙ │
├──────────────────────────────────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ 🔗 Paste video / playlist URL… [Add] │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ [Best][1080p][720p] [MP4▾][MP3][M4A] 📁 movies ⋯ Advanced │
├──────────────────────────────────────────────────────────────────────────┤
│ ── NOW DOWNLOADING (2) ───────────────────────────── 12.4 MB/s total ── │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ [thumb] Title ███████████████░░░░░ 62% 12.4MB/s ETA 0:43 ✕ │ │
│ │ [thumb] Title ████░░░░░░░░░░░░░░░ 18% 3.1MB/s ETA 4:12 ✕ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────────────────┤
│ [ Queued 230 ] [ Completed 303 ] 📁 All folders ▾ ▦ | ☰ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ ☑ Select 🔍 Filter titles… Sort: Date added ▾ Group: 📁 │ │
│ ├───────────────────────────────────────────────────────────────────┤ │
│ │ ▸ 📁 shows (86) ☐ all │ │
│ │ ▸ 📁 music (54) ☐ all │ │
│ │ ▾ 📁 movies (90) ☐ all │ │
│ │ ☐ [thumb] Movie title one 720p · queued 2d ago ✕ │ │
│ │ ☑ [thumb] Movie title two 720p · queued 2d ago ✕ │ │
│ │ ☐ [thumb] Movie title three 720p · queued 3d ago ✕ │ │
│ │ …(virtualized scroll)… │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ 2 selected ── [ ▶ Start ] [ ✕ Cancel ] [ 📁 Move to… ] │
└──────────────────────────────────────────────────────────────────────────┘
```
### 5.2 Advanced options slide-over
Replaces the inline collapsible card. Rarely-changed controls move here;
cookie-persisted defaults keep the main bar to one row.
```
┌────────────────────────────┐
│ Advanced options ✕ │
│ │
│ Auto-start [ toggle ]│
│ Playlist strict [ toggle ]│
│ Playlist limit [ 10 ] │
│ │
│ ── Batch ────────────── │
│ [Import URLs] [Export] │
│ [Copy all URLs] │
│ │
│ ── Cookies ──────────── │
│ domain.com ✓ saved │
│ [Paste cookie…] │
└────────────────────────────┘
```
### 5.3 Mobile
Cards stack vertically; the active panel, tabs, filter bar, and list reflow to
single-column. No horizontal scrolling anywhere. The ⋮ menu and slide-over
become bottom sheets.
```
┌─────────────────────────┐
│ ▶ MeTube 🌙 ⚙ │
│ ⬇2 · 12MB/s │
├─────────────────────────┤
│ ┌─────────────────────┐ │
│ │🔗 Paste URL… [Add]│ │
│ └─────────────────────┘ │
│ [Best][1080p][720p] │
│ 📁 movies ⋯ More │
│ │
│ NOW DOWNLOADING (2) │
│ ┌─────────────────────┐ │
│ │[thumb] │ │
│ │ Title goes here │ │
│ │ ████████░░░ 62% │ │
│ │ 12MB/s · ETA 0:43 ✕ │ │
│ └─────────────────────┘ │
│ │
│ [Queued 230][Done 303] │
│ ┌────────┐ ┌────────┐ │
│ │ thumb │ │ thumb │ │
│ │ title… │ │ title… │ │
│ │ 384MB ⋮│ │ 1.2GB ⋮│ │
│ └────────┘ └────────┘ │
└─────────────────────────┘
```
### 5.4 Key design decisions and rationale
1. **Tabs for the archives, pinned panel for active downloads.**
Stacked sections (all three lists on one scrolling page) were considered
and rejected: at 230/303 items the lists never fit on a screen, so the
"pipeline at a glance" argument fails. Tabs hide state, so each tab carries
a count badge. The active-downloads panel stays pinned above the tabs
because its size is bounded by the concurrency limit (~3) — it costs
nothing and keeps live progress always visible.
2. **Tables → thumbnail-first cards/rows.**
Recognition-by-image is far faster than recognition-by-text; it also
eliminates the 7-column mobile squeeze. Completed items get a grid/list
toggle (▦|☰) — the completed tab is a media library (browse, play, move,
delete), not an audit log.
3. **Command bar instead of a form.**
Usage is 95% two actions: paste URL, watch progress. Quality/format become
one-tap chips with cookie-persisted defaults; folder picker stays; prefix
and everything else moves to the slide-over.
4. **List tooling matched to volume** (the main volume-driven change):
- Per-tab filter box (title substring), sort (date added, title, size,
folder), and folder grouping.
- Folder groups are collapsible with per-group select-all.
- Multi-select with a batch action bar (start / cancel / delete / move to
folder). With 230 queued items the real operations are plural.
- Virtualized scrolling (Angular CDK `cdk-virtual-scroll-viewport`) — render
~30 rows instead of 533 DOM subtrees. Concrete perf win over the current
tables.
5. **Toasts replace `alert()`** — non-blocking, can carry actions (e.g.
"Duplicate skipped — view item"). The recent-events panel becomes a
collapsible activity drawer (diagnostic info: available, not permanently
occupying screen space).
6. **Theme**: keep the existing dark/light/auto Bootstrap `data-bs-theme`
mechanism; refine palette to one accent color. Header gets slim stat chips
(active count, total speed) replacing the current navbar counters.
### 5.5 Open question: per-folder navigation
If queue work is frequently per-folder ("start all the music ones"), folder
groups may deserve first-class status — e.g. a qBittorrent-style left sidebar
of folders with counts instead of group headers inside the list. Decide based
on actual usage before implementation; default is the group-header design
above (simpler, keeps single-column layout).
## 6. Implementation plan
### 6.1 Component refactor (Angular, in place)
Break the monolith into:
- `CommandBarComponent` — URL input, quality/format chips, folder picker,
slide-over trigger. Owns the add-form state and cookie-persisted defaults.
- `AdvancedOptionsComponent` — slide-over (auto-start, playlist options,
import/export/copy, cookie paste).
- `ActiveDownloadsComponent` — pinned now-downloading panel (rows + total
speed).
- `DownloadListComponent` — reusable list for Queued and Completed tabs:
filter, sort, folder grouping, multi-select, batch action bar,
`cdk-virtual-scroll-viewport`. Grid/list toggle applies to Completed.
- `DownloadRowComponent` — thumbnail, title, progress/status line, folder
badge, actions (cancel / start / move / delete / reveal).
- `ToastsComponent` / toast service — replaces `alert()`.
- Keep: `downloads.service.ts` (socket + HTTP state) largely as-is;
`downloads.pipe.ts` (eta/speed/fileSize); theme helpers. The triplicated
tab markup collapses into two instances of `DownloadListComponent`.
### 6.2 Frontend behaviors to preserve exactly
- Cookie-persisted prefs: quality, format, theme, active tab, sort, grouping.
- Folder picker with tag-creation (currently ng-select) and per-row folder
editing (calls `POST /set_folder`; moves completed files on disk).
- Socket.IO event handling: events arrive as JSON *strings* and are parsed
client-side; full state re-sync on connect via the `all` event.
- PWA/service-worker setup (`custom-service-worker.js`, `ngsw-config.json`).
- Import/Export/Copy URLs batch modal.
- Existing HTTP API usage: `POST /add`, `POST /delete`, `POST /start`,
`POST /set_folder`, `POST /cookie`, `GET /history`, `GET|POST /events`,
`GET /version`.
### 6.3 Backend addition: thumbnail endpoint (additive only)
Cards need thumbnails. yt-dlp metadata already contains `thumbnail` URLs, but
hot-linking remote thumbnails from a LAN app leaks requests to third-party
hosts and breaks for expired URLs. Add one endpoint:
- `GET /thumbnail?id=` — backend fetches/caches the thumbnail
(cache under `STATE_DIR/thumbnails/`, keyed by download id), returns the
image with long-lived cache headers. Fall back to a placeholder icon when
no thumbnail exists (e.g. audio-only downloads).
No other backend changes. Queue, precheck, conflict resolution, persistence,
socket events are untouched.
### 6.4 Phasing
1. **Phase 1 — component split** (no visual change): extract components from
the monolith, keep the tables. Establishes the structure the redesign
builds on; low regression risk, easily reviewable.
2. **Phase 2 — list redesign**: `DownloadListComponent` with cards, filter,
sort, folder groups, multi-select, virtualization; pinned active panel;
tabs with count badges.
3. **Phase 3 — command bar + slide-over**: new add flow, toasts, activity
drawer, empty states, skeletons.
4. **Phase 4 — thumbnails**: backend endpoint + wire into rows/grid.
5. **Phase 5 — polish**: palette/accent, mobile bottom sheets, cleanup of dead
tooling (tslint/codelyzer/protractor configs) if desired.
Phases 1–3 deliver the bulk of the UX gain; Phase 4 is the biggest visual
upgrade but is separable if backend changes need to wait.
### 6.5 Effort estimate
| Phase | Estimate |
|---|---|
| 1. Component split | 0.5–1 day |
| 2. List redesign | 1–1.5 days |
| 3. Command bar + toasts | 0.5–1 day |
| 4. Thumbnails (front + back) | 0.5 day |
| 5. Polish | 0.5 day |
| **Total** | **~3–4 days** |
(For reference, the rejected full rewrite in React was estimated at 3–5 days
for the frontend alone, plus regression risk on working behaviors.)
## 7. Risks
- **Virtual scroll + folder groups**: `cdk-virtual-scroll-viewport` works best
with flat lists; collapsible groups need flattening logic in the component.
Mitigation: flatten grouped data into a single array with group-header rows
(standard pattern).
- **Thumbnail fetching load**: 300 completed items loading thumbnails at once.
Mitigation: backend cache + lazy loading (`loading="lazy"` / viewport-based).
- **Bootstrap theming limits**: the refined palette may fight Bootstrap
defaults. Mitigation: CSS custom properties on top of `data-bs-theme`,
not a Bootstrap rebuild.
- **Regression in preserved behaviors** (§6.2): mitigate with manual test
checklist per phase; the project has no real frontend test suite, and
adding one is out of scope.