diff --git a/.gsd/milestones/M002/M002-ROADMAP.md b/.gsd/milestones/M002/M002-ROADMAP.md index e58d90f..a5a46b3 100644 --- a/.gsd/milestones/M002/M002-ROADMAP.md +++ b/.gsd/milestones/M002/M002-ROADMAP.md @@ -53,10 +53,10 @@ This milestone is complete only when all are true: ## Slices -- [ ] **S01: Bug Fixes + Header/Footer Rework** `risk:high` `depends:[]` +- [x] **S01: Bug Fixes + Header/Footer Rework** `risk:high` `depends:[]` > After this: Cancel button works, header has no tabs, footer shows version info, welcome message block is visible with default text, theme is sun/moon toggle -- [ ] **S02: Download Flow + Queue Redesign** `risk:medium` `depends:[S01]` +- [x] **S02: Download Flow + Queue Redesign** `risk:medium` `depends:[S01]` > After this: Single "Download" button with optional format picker, audio/video toggle, queue displays as styled table with sorting, completed items show download/copy/clear glyphs - [ ] **S03: Mobile + Integration Polish** `risk:low` `depends:[S02]` diff --git a/.gsd/milestones/M002/slices/S02/S02-PLAN.md b/.gsd/milestones/M002/slices/S02/S02-PLAN.md new file mode 100644 index 0000000..ebf3635 --- /dev/null +++ b/.gsd/milestones/M002/slices/S02/S02-PLAN.md @@ -0,0 +1,68 @@ +# S02: Download Flow + Queue Redesign + +**Goal:** Simplify the download flow to a single "Download" button (with optional format picker), add an audio/video quick-toggle, convert the queue from cards to a sortable table, and add action glyphs (download file, copy link, clear) for completed items. +**Demo:** User pastes URL → clicks "Download" → item appears in the table queue → completes → user clicks download icon to save file or copy icon to copy the download link. Table columns are sortable by status, name, progress, ETA. + +## Must-Haves + +- "Download" is the primary button (not "Get Formats") — one-click download with best quality +- Optional format picker accessible via a secondary "⚙ Options" toggle +- Audio/video quick-toggle (video default) that sets appropriate format flags +- Queue rendered as a styled table with columns: Name, Status, Progress, Speed, ETA, Actions +- Table headers are clickable to sort (ascending/descending) +- Completed items show download (⬇), copy-link (🔗), clear (✕) action icons +- Active items show cancel (✕) icon +- Failed items show error message and clear (✕) icon +- Mobile: table degrades gracefully (horizontal scroll or card fallback below 640px) + +## Verification + +- `cd frontend && npx vitest run` — all tests pass +- `cd backend && source .venv/Scripts/activate && python -m pytest tests/ -q -m "not integration"` — no regressions +- Browser: paste URL → click Download → job appears in table → progresses → completes +- Browser: click download icon on completed item → file downloads +- Browser: click copy-link icon → link copied (or tooltip confirms) +- Browser: sort table by each column header +- Browser: mobile viewport shows readable queue + +## Tasks + +- [x] **T01: Rework UrlInput — Download-first flow with collapsible options** `est:45m` + - Why: Current flow forces "Get Formats" before downloading. Most users just want to paste and go. + - Files: `frontend/src/components/UrlInput.vue` + - Do: Make "Download" the primary action button. Add a "⚙" toggle button that expands/collapses the format picker section below. Add audio/video toggle pills (Video | Audio) that set a `mediaType` ref. When `mediaType` is "audio", pass `quality: "bestaudio"` to the submit payload. When format picker is open and user selects a format, use that instead. Keep the paste-to-auto-extract behavior but make it extract silently in the background (populate formats without showing picker). "Download" works immediately with or without format selection. + - Verify: Paste URL → click Download → job starts without format selection. Toggle audio → Download → job starts with audio quality. Click ⚙ → format picker opens → select format → Download. + - Done when: Download is the primary one-click action, format picker is optional + +- [x] **T02: Convert queue to sortable table** `est:60m` + - Why: Card-based queue doesn't scan well with many items. Table with sorting is standard for download managers. + - Files: `frontend/src/components/DownloadQueue.vue`, `frontend/src/components/DownloadTable.vue` (new), `frontend/src/components/DownloadItem.vue` (remove or repurpose) + - Do: Create DownloadTable component with `` markup. Columns: Name (truncated, title=full URL), Status (badge), Progress (inline bar), Speed, ETA, Actions. Add a `sortBy` ref and `sortDir` ref. Clicking a column header toggles sort. Computed `sortedJobs` applies sort. Keep the filter buttons (All/Active/Completed/Failed) above the table. Style the table with theme CSS variables. On mobile (< 640px), hide Speed and ETA columns, or use a responsive approach. + - Verify: Jobs render as table rows. Click column headers to sort. Filter buttons still work. Mobile view is usable. + - Done when: Queue is a sortable table with all columns rendering correctly + +- [x] **T03: Action glyphs for completed/active/failed items** `est:30m` + - Why: Users need to download completed files, copy links, and clear items from the queue. + - Files: `frontend/src/components/DownloadTable.vue`, `frontend/src/stores/downloads.ts`, `frontend/src/api/client.ts` + - Do: Add action icons in the Actions column. Completed: download file (anchor to `/api/downloads/{filename}`), copy download link (clipboard API), clear from queue (DELETE + remove from store). Active: cancel (existing logic). Failed: clear from queue. Style icons as small inline buttons with hover effects. Add `clearJob(id)` to downloads store that calls DELETE and removes locally. + - Verify: Click download icon on completed item → browser downloads file. Click copy icon → link in clipboard. Click clear → item removed. Click cancel on active → cancelled. + - Done when: All action icons work for each status type + +- [x] **T04: Update tests** `est:20m` + - Why: New components and store changes need test coverage. Old DownloadItem tests may need removal/update. + - Files: `frontend/src/tests/stores/downloads.test.ts`, `frontend/src/tests/components/` (if any) + - Do: Add tests for the new sort logic (sortBy, sortDir). Test clearJob action in downloads store. Verify existing download store tests still pass. Run full frontend and backend test suites. + - Verify: `npx vitest run` all pass, `python -m pytest tests/ -q -m "not integration"` all pass + - Done when: All tests green, no regressions + +## Files Likely Touched + +- `frontend/src/components/UrlInput.vue` (reworked) +- `frontend/src/components/DownloadQueue.vue` (reworked to use table) +- `frontend/src/components/DownloadTable.vue` (new) +- `frontend/src/components/DownloadItem.vue` (may be removed — logic moves to table rows) +- `frontend/src/components/FormatPicker.vue` (minor — toggled visibility) +- `frontend/src/components/ProgressBar.vue` (minor — may need inline variant) +- `frontend/src/stores/downloads.ts` (add clearJob, sort helpers) +- `frontend/src/api/client.ts` (no changes expected — DELETE already exists) +- `frontend/src/api/types.ts` (no changes expected) diff --git a/frontend/src/components/DownloadQueue.vue b/frontend/src/components/DownloadQueue.vue index f878040..cf01fde 100644 --- a/frontend/src/components/DownloadQueue.vue +++ b/frontend/src/components/DownloadQueue.vue @@ -1,7 +1,7 @@ + + + + diff --git a/frontend/src/components/UrlInput.vue b/frontend/src/components/UrlInput.vue index 91baec2..b211346 100644 --- a/frontend/src/components/UrlInput.vue +++ b/frontend/src/components/UrlInput.vue @@ -1,5 +1,5 @@ @@ -137,24 +182,10 @@ function handlePaste(): void { font-size: var(--font-size-base); } -.btn-extract, .btn-download { white-space: nowrap; padding: var(--space-sm) var(--space-lg); font-weight: 600; -} - -.btn-extract { - background: var(--color-surface); - color: var(--color-accent); - border: 1px solid var(--color-accent); -} - -.btn-extract:hover:not(:disabled) { - background: color-mix(in srgb, var(--color-accent) 15%, transparent); -} - -.btn-download { background: var(--color-accent); color: var(--color-bg); } @@ -168,6 +199,76 @@ button:disabled { cursor: not-allowed; } +/* Controls row */ +.controls-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-sm); +} + +.media-toggle { + display: flex; + gap: 0; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + overflow: hidden; +} + +.toggle-pill { + display: flex; + align-items: center; + gap: var(--space-xs); + padding: var(--space-xs) var(--space-md); + background: var(--color-surface); + color: var(--color-text-muted); + border: none; + border-radius: 0; + font-size: var(--font-size-sm); + min-height: 32px; + transition: all 0.15s ease; +} + +.toggle-pill:not(:last-child) { + border-right: 1px solid var(--color-border); +} + +.toggle-pill:hover { + background: var(--color-surface-hover); + color: var(--color-text); +} + +.toggle-pill.active { + background: color-mix(in srgb, var(--color-accent) 15%, transparent); + color: var(--color-accent); +} + +.btn-options { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + background: var(--color-surface); + color: var(--color-text-muted); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + transition: all 0.15s ease; +} + +.btn-options:hover:not(:disabled) { + color: var(--color-accent); + border-color: var(--color-accent); +} + +.btn-options.active { + color: var(--color-accent); + border-color: var(--color-accent); + background: color-mix(in srgb, var(--color-accent) 10%, transparent); +} + +/* Loading / errors */ .extract-loading { display: flex; align-items: center; @@ -197,13 +298,41 @@ button:disabled { padding: var(--space-sm); } +/* Options panel transition */ +.options-panel { + overflow: hidden; +} + +.options-slide-enter-active, +.options-slide-leave-active { + transition: all 0.25s ease; +} + +.options-slide-enter-from, +.options-slide-leave-to { + opacity: 0; + max-height: 0; +} + +.options-slide-enter-to, +.options-slide-leave-from { + opacity: 1; + max-height: 400px; +} + +.options-hint { + padding: var(--space-md); + text-align: center; + color: var(--color-text-muted); + font-size: var(--font-size-sm); +} + /* Mobile: stack vertically */ @media (max-width: 767px) { .input-row { flex-direction: column; } - .btn-extract, .btn-download { width: 100%; }