feat: queue UX (Phase M) + media editing (split-chapters, trim)
Phase L (media editing): - Split by chapters (--split-chapters) as a DownloadOptions toggle - Trim/cut by time range: parseTrimSections -> --download-sections "*A-B" + --force-keyframes-at-cuts (deduped vs SponsorBlock), per-download Trim panel Phase M (queue & daily-use UX), all 7: - Pause/resume: main-side killTree + paused flag (silent close), download:pause IPC, store pause/resume + paused status, QueueItem controls - Reorder via "Download next" (prioritize) - Aggregate progress strip (pure summarizeQueue) + combined speed/ETA - Drag-and-drop links / .url files onto the download card - Retry all failed - Duplicate detection (sameVideo) with "Download anyway" guard - Per-download scheduling (datetime-local) + save-for-later (saved status, 15s promotion ticker); session-only (queue not persisted) New pure modules unit-tested (queueStats); buildArgs trim/split tested. typecheck + test green (178 passed). Roadmap updated: Phase M COMPLETE. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+63
-39
@@ -166,7 +166,7 @@ parallel storage. All items verified in the UI preview (typecheck + `npm run tes
|
||||
non-zero yt-dlp exit); **Settings → Diagnostics** lists recent entries with
|
||||
**Copy full report** and **Clear log**.
|
||||
|
||||
## Phase E — Theming & platform polish
|
||||
## Phase E — Theming & platform polish ✅ COMPLETE
|
||||
|
||||
Some items are Android-specific in Seal and adapted to Windows here.
|
||||
|
||||
@@ -187,10 +187,6 @@ Some items are Android-specific in Seal and adapted to Windows here.
|
||||
auto. Verified in the UI preview (typecheck + `npm run test` clean): default Toffee,
|
||||
switching to Slate/Evergreen/Lavender, Light/Dark/Follow-system, and the sidebar
|
||||
toggle's break-out-of-system behavior all checked visually.
|
||||
- [ ] **i18n / language setting** (Seal ships ~40 languages). Large but optional — deferred,
|
||||
since shipping ~40 real translations isn't something to fake; doing this properly means
|
||||
wiring up an i18n library + extracting every string now, with translation content
|
||||
arriving incrementally from contributors.
|
||||
- [x] **Windows "open with" / share integration.** Both mechanisms named above, since a Win32
|
||||
(non-MSIX) app can't register as an actual Share Target:
|
||||
- **`aerofetch://` protocol** (`aerofetch://download?url=<encoded>`) — registered at
|
||||
@@ -274,49 +270,77 @@ YTDLnis's signature differentiator. The keyframe plumbing already exists in Aero
|
||||
(`--force-keyframes-at-cuts` is emitted today for SponsorBlock-remove in
|
||||
`src/main/buildArgs.ts`), so this is cheaper than it looks.
|
||||
|
||||
- [ ] **Trim / cut downloads.** Download only part of a video, or remove segments — by
|
||||
**timestamp** or by **chapter**, with multiple cuts allowed. → yt-dlp
|
||||
`--download-sections "*START-END"` (repeatable) + `--force-keyframes-at-cuts`.
|
||||
Chapters come free from the probe's `yt-dlp -J` `chapters[]` array (`src/main/probe.ts`).
|
||||
New fields on `DownloadOptions` (or a per-download override), surfaced as a **Trim**
|
||||
panel in `src/renderer/src/components/DownloadBar.tsx`. *v1 can skip a scrubber/preview
|
||||
player — timestamp inputs + checkboxes of the probed chapter list is enough. Smoke-test
|
||||
the exact multi-section output behaviour (one file vs. per-section) the same way
|
||||
Phases A–C smoke-tested live yt-dlp.*
|
||||
- [ ] **Split by chapters into separate files.** → `--split-chapters` (already flagged as a
|
||||
- [x] **Trim / cut downloads.** Keep only chosen time ranges (multiple allowed). →
|
||||
`parseTrimSections` (`src/main/buildArgs.ts`) normalises raw user text into yt-dlp
|
||||
`--download-sections "*START-END"` specs (+ `--force-keyframes-at-cuts`, deduped against
|
||||
SponsorBlock-remove). Threaded as `StartDownloadOptions.trim` through
|
||||
`store/downloads.ts`, surfaced as a collapsible **Trim** panel in `DownloadBar.tsx`
|
||||
(single downloads only). *Implemented + unit-tested (`buildArgs.test.ts` — 7 new tests;
|
||||
typecheck + test green). Live smoke test pending, plus two follow-ups: timestamp-range
|
||||
only so far (chapter-based cuts via the probe `chapters[]` array, and a scrubber/preview
|
||||
player, still TODO).*
|
||||
- [x] **Split by chapters into separate files.** → `--split-chapters` (already flagged as a
|
||||
TODO in Phase A above). One toggle on `DownloadOptions` +
|
||||
`src/renderer/src/components/DownloadOptionsForm.tsx`; pairs naturally with trim. Uses an
|
||||
`--output chapter:` template for the per-chapter filenames.
|
||||
`src/renderer/src/components/DownloadOptionsForm.tsx`; pairs naturally with trim. yt-dlp
|
||||
keeps the full file and writes per-chapter files via its default `chapter:` template.
|
||||
*Implemented + unit-tested (`buildArgs.test.ts`, typecheck + test green); live smoke test
|
||||
still pending.*
|
||||
- [ ] **Metadata editing before download.** Change title / author / artist pre-download
|
||||
(YTDLnis: "modify metadata such as title and author"). → `--parse-metadata` /
|
||||
`--ppa "Metadata:-metadata title=…"`; fits the audio path of `DownloadOptions`. Good for
|
||||
building a clean music library where the source title is messy.
|
||||
`--replace-in-metadata`; fits the audio path. Good for building a clean music library
|
||||
where the source title is messy. *Held: the exact set-a-literal recipe is fragile
|
||||
(`--parse-metadata FROM:TO` splits on a colon that titles often contain;
|
||||
`--replace-in-metadata FIELD REGEX REPLACE` has empty-field + regex-replacement edge
|
||||
cases) — confirm against a live yt-dlp run before wiring it, rather than guessing the
|
||||
flag.*
|
||||
|
||||
## Phase M — Queue & daily-use UX
|
||||
## Phase M — Queue & daily-use UX ✅ COMPLETE
|
||||
|
||||
Neither Seal nor Pinchflat covers these; they're what makes the queue pleasant to live in.
|
||||
Today `src/renderer/src/store/downloads.ts` has cancel + retry only.
|
||||
|
||||
- [ ] **Pause / resume individual downloads.** → yt-dlp resumes `.part` files via `--continue`
|
||||
(already the default). Pause = kill the child (existing `taskkill /pid /T /F` path) but
|
||||
keep the partial + mark the item `paused`; resume = relaunch the same item, which
|
||||
continues. New `paused` state in the queue store.
|
||||
- [ ] **Reorder / prioritize the queue.** Drag a queued item up. The renderer scheduler
|
||||
(`pump()` promotes oldest-queued-first) already treats the queued array as the order, so
|
||||
this is reordering that array.
|
||||
- [ ] **Aggregate progress + total ETA + combined speed.** A header strip on the Downloads
|
||||
- [x] **Pause / resume individual downloads.** → yt-dlp resumes `.part` files via `--continue`
|
||||
(already the default). *Done: `pauseDownload()` (`src/main/download.ts`) reuses the
|
||||
`taskkill /T /F` tree-kill via a new `killTree` helper but flags the record `paused` so
|
||||
the close/error handlers stay silent (no error event/log/notification); new
|
||||
`download:pause` IPC + preload bridge. Renderer `pause`/`resume` (`store/downloads.ts`) +
|
||||
a `paused` status: pause frees a concurrency slot, resume re-queues WITHOUT resetting
|
||||
progress so the relaunch continues the partial. Pause/Resume buttons + a frozen progress
|
||||
row in `QueueItem.tsx`; paused items survive "Clear finished". typecheck + test green;
|
||||
live resume-continues-the-.part still wants a smoke test.*
|
||||
- [x] **Reorder / prioritize the queue.** *Done as a **"Download next"** action rather than
|
||||
drag: since the queue renders newest-first while `pump()` promotes oldest-first, a
|
||||
reposition primitive is clearer than drag. `prioritize(id)` (`store/downloads.ts`) moves a
|
||||
queued item to the front of the promotion order; an up-arrow button on queued rows
|
||||
(`QueueItem.tsx`).*
|
||||
- [x] **Aggregate progress + total ETA + combined speed.** A header strip on the Downloads
|
||||
view summing bytes/speed across active items. Feeds straight into Phase O's taskbar
|
||||
progress bar.
|
||||
- [ ] **Drag-and-drop a link or `.url` file onto the window.** A window `drop` handler that
|
||||
accepts `text/uri-list` and Internet-Shortcut files, reusing `extractIncomingUrl()`
|
||||
from `src/main/deeplink.ts` (already validates http(s)-only).
|
||||
- [ ] **Retry all failed.** One action that re-enqueues every entry in `src/main/errorlog.ts`.
|
||||
- [ ] **Duplicate detection.** Warn ("you already have this") when a URL/video-id is already in
|
||||
history or the active queue before enqueuing.
|
||||
- [ ] **Per-download scheduling + "save for later."** YTDLnis lets you "schedule by date and
|
||||
time" and park items. Add `scheduledFor?: number` + a `saved` state to `DownloadItem`;
|
||||
`pump()` gains a time gate before promotion. Distinct from the existing background
|
||||
`--sync` schedule (Phase J), which is for *sources*, not one-off items.
|
||||
progress bar. *Done: pure `summarizeQueue` (`store/queueStats.ts`) → a strip in
|
||||
`DownloadsView.tsx` (overall %, combined speed, longest-ETA "time left"); unit-tested
|
||||
(`test/queueStats.test.ts`). A true remaining-bytes combined ETA is still out of reach —
|
||||
we don't track per-item sizes — so "time left" is the longest single ETA.*
|
||||
- [x] **Drag-and-drop a link or `.url` file onto the window.** *Done: the download card
|
||||
(`DownloadBar.tsx`) handles `onDragOver`/`onDrop` with a dashed-outline drag highlight,
|
||||
accepting `text/uri-list`/`text/plain` (`firstUrl`) and dropped Windows `.url`
|
||||
Internet-Shortcut files (`parseUrlFile`, read via `File.text()`); both http(s)-validated by
|
||||
the existing `looksLikeUrl`. A dropped link fills the URL field.*
|
||||
- [x] **Retry all failed.** One action that re-enqueues every failed item. *Done:
|
||||
`retryAll()` in `store/downloads.ts` re-queues all `error` items + a "Retry all failed (N)"
|
||||
button in `DownloadsView.tsx`. (Re-queues live queue items rather than reading
|
||||
`errorlog.json` — same effect for the on-screen failures.)*
|
||||
- [x] **Duplicate detection.** Warn ("you already have this") before enqueuing a URL already in
|
||||
the active queue. *Done: pure `sameVideo()` (`store/queueStats.ts`, YouTube-id aware +
|
||||
normalised-URL fallback) drives a "Already in your queue — Download anyway?" banner in
|
||||
`DownloadBar.tsx`; unit-tested. Checks the live queue (which still holds recently-completed
|
||||
items); cross-checking `history.json` for older downloads is a possible extension.*
|
||||
- [x] **Per-download scheduling + "save for later."** *Done: a `saved` status + optional
|
||||
`scheduledFor` (`store/downloads.ts`); `pump()` ignores `saved` (the time gate), and a 15s
|
||||
ticker promotes due scheduled items to `queued`. UI: a **Schedule** panel with a native
|
||||
`datetime-local` input in `DownloadBar.tsx` (future time parks the new download as
|
||||
scheduled), plus per-row **Save for later** / **Add to queue** actions in `QueueItem.tsx`;
|
||||
saved items survive "Clear finished". Distinct from the Phase J `--sync` schedule (which is
|
||||
for *sources*). **Limitation: the queue isn't persisted, so a schedule only fires while
|
||||
AeroFetch is running** — noted in the UI hint and the code.*
|
||||
|
||||
## Phase N — Power-user surface
|
||||
|
||||
|
||||
Reference in New Issue
Block a user