diff --git a/CHANGELOG.md b/CHANGELOG.md index c9ab46c..8bd1e63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Keyboard-accessible move-download-between-packages** (issue #134, PR #133 follow-up): the pointer-only HTML5 drag-and-drop introduced for moving downloads between packages now has a fully keyboard-driven and screen-reader-friendly alternative. Each `PackageDownloadRow` exposes a single toggle button (`Move…` ↔ `Cancel move`) that marks the download as pending-move; the button carries `aria-pressed` so screen readers announce the toggle state, replacing the deprecated `aria-grabbed` named in the original issue. Once a download is selected, every other `PackageRow` (the source package is intentionally excluded) surfaces a `Move selected here` action with an explicit `aria-label="Move selected download to {package}"`; the deprecated `aria-dropeffect` is intentionally not used because the descriptive button label plus the live region cover the same intent without the deprecated attribute. Activating the target reuses the same `package_remove_download` + `package_add_download` IPC pair as the drag-and-drop path (now factored into a shared `moveDownloadBetweenPackages` helper so the rollback logic stays in one place), surfaces start / success / cancel / error transitions through a new visually-hidden `role="status" aria-live="polite"` live region (`packages-move-live-region`), and matches the pointer path's visual feedback by raising `toast.success(packages.toast.moveDownloadSuccess)` on success and `toast.error(packages.toast.moveDownloadError)` in the catch (`toast.error(packages.toast.moveDownloadRollbackError)` still fires when both the move and the rollback fail). The `executeMove` finally block guards against an in-flight move clobbering a newer pending selection: `setPendingMove` is cleared only when the current state still references the move that just completed. The drag-and-drop path is unchanged. New i18n keys `packages.move.{start,startAriaLabel,cancel,cancelAriaLabel,target,targetAriaLabel,liveRegionLabel,announce.{selected,cancelled,started,success,error}}` ship in EN + FR. Six new Vitest tests cover the four issue acceptance criteria: keyboard initiation without a pointer (`Move` button → `aria-pressed` flips), live region announcements (selected / success / cancelled / error wording), unchanged pointer drag-and-drop (existing test still green), error-path coverage of the "target add fails, rollback succeeds" branch with explicit call-order assertions, and a race-condition guard that proves a stale `executeMove` does not erase a newer selection when its `package_remove_download` resolves late. + - **Auto-grouping playlist → package** (PRD §6.3, PRD-v2 §P1.11, task 30): when the Link Grabber resolves a YouTube or SoundCloud playlist URL, every selected item is now auto-attached to a single `Package` per playlist. New `PlaylistGrouper` application service (`src-tauri/src/application/services/playlist_grouper.rs`) finds-or-creates a `Package` keyed by the new `external_id` natural key (the playlist URL acts as the natural id), so re-resolving the same playlist reuses the existing package instead of creating a duplicate. New `GroupPlaylistsCommand` handler (`group_playlists.rs`) routes the request through the grouper; new `link_group_playlists` Tauri IPC command (`PlaylistGroupInputDto` → `Vec`) lets the frontend orchestrate the group-then-attach flow. Migration `m20260430_000008_add_package_external_id` adds `packages.external_id TEXT NULL` plus the `idx_packages_external_id` index; the column is intentionally nullable (manual packages keep `NULL`) and the application layer enforces uniqueness through `find_by_external_id` rather than a DB UNIQUE constraint that would forbid more than one manual row. Domain `Package` aggregate gains an `external_id: Option` field with `set_external_id` and `external_id()` accessors; `PackageRepository::find_by_external_id` is added to the trait, returning the oldest row by `(created_at, id)` so repeated reuses pick the same package deterministically. The `MediaGrabberDialog` now shows a `PlaylistPackageBanner` ("Will create package: {name} with {N} items" / "Will reuse existing package …") above the playlist item list once `metadata.isPlaylist` is true, with EN + FR plural translations under `mediaGrabber.playlistBanner.*`. `LinkGrabberView.handleMediaGrabberConfirm` now calls `link_group_playlists` first when the user has selected playlist items, then `download_media_start`, then attaches every returned `downloadId` to the package via `package_add_download` (failures on attach are non-fatal — downloads still run, only the grouping is missed). Visibility of `application::commands::tests_support` is bumped to `pub(crate)` so the new `playlist_grouper` and `group_playlists` tests can reuse the existing `InMemoryPackageRepo` / `CapturingEventBus` mocks. 13 new tests cover the four acceptance criteria: 7 unit tests on `PlaylistGrouper` (create / reuse / blank id / fallback name / batch / partial failure / trim), 5 SQLite tests on `find_by_external_id` (round-trip / match / not-found / no NULL match / oldest-on-duplicates / clear-on-upsert), 3 handler tests, 4 frontend tests on `PlaylistPackageBanner`. cargo test 1327 passing, vitest 626 passing, clippy + fmt clean. Unblocks the Link Grabber's playlist UX. - **Packages view** (PRD §6.3, PRD-v2 §P1.10, task 29): full Packages management UI replacing the previous `PlaceholderView`. New `src/views/PackagesView/` folder with `PackagesView` (root), `PackageToolbar` (filter chips `All / Container / Playlist / Manual / Split archive` + debounced search input + "New package" trigger), `PackageTree` (empty state + list of `PackageRow`), `PackageRow` (chevron toggle, inline rename trigger, source-type badge, file count via `_one`/`_other` plural keys, total bytes via `formatBytes`, aggregated `Progress` bar, folder browse button, `Key` password trigger, `Switch` for auto-extract, native `