mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-12 12:37:04 +00:00
feat(ui): modernize desktop interactions
This commit is contained in:
+497
-366
@@ -1,437 +1,568 @@
|
||||
# Firelink — UI Interaction Inventory
|
||||
# Firelink Interaction Inventory and Modernization Review
|
||||
|
||||
A read-only audit of every user-facing action in Firelink, mapping each to the
|
||||
component/handler, Zustand store action, IPC command, Rust function, expected
|
||||
behavior, and the likely validation method. **No code was changed.**
|
||||
Static, inventory-only review of the current React/Tauri code. No application
|
||||
click-through or manual UI testing was performed.
|
||||
|
||||
Legend:
|
||||
- ✅ = wired end-to-end (UI → store → IPC → Rust)
|
||||
- ⚠️ = present in UI/store but with a gap, mismatch, or risk (see notes)
|
||||
- ❌ = declared/wired in one layer but missing in another (dead path)
|
||||
## Implementation update — June 21, 2026
|
||||
|
||||
The following recommendations from this review have now been implemented:
|
||||
|
||||
- all new and migrated downloads receive an explicit Main Queue identity unless
|
||||
the user chooses another queue;
|
||||
- shared action-policy helpers now drive Start, Resume, Pause, Redownload, and
|
||||
Properties lock behavior;
|
||||
- tray Pause/Resume All uses centralized global store actions, and tray
|
||||
reconstruction retains the complete menu;
|
||||
- queued, processing, and retrying lifecycle actions are handled consistently;
|
||||
- Show in Finder now sends the exact owned output path for completed and partial
|
||||
downloads;
|
||||
- duplicate replacement no longer suppresses deletion errors, blocks replacement
|
||||
of active transfers, reports batch failures, and fails explicitly when no
|
||||
rename candidate is available;
|
||||
- bulk removal reports partial failures instead of failing an opaque concurrent
|
||||
batch;
|
||||
- List Row Density and Automatically Check for Updates are functional;
|
||||
- update notifications include an action to open the release;
|
||||
- the duplicate global-speed editor was replaced with a single Speed Limiter
|
||||
configuration path and its capability copy is now accurate;
|
||||
- extension server status reports the actual bound port;
|
||||
- queue assignment, IPC event listening, path resolution, and editable-state
|
||||
policy are centralized;
|
||||
- the enqueue payload now uses a Rust-generated TypeScript binding;
|
||||
- column widths persist, menus close with Escape, async clipboard/keychain/log
|
||||
export failures are surfaced, and fatal render errors provide recovery;
|
||||
- the confirmed-dead legacy `QualityModal` and its unused metadata store state
|
||||
were removed. The active Add Downloads media-format flow and all backend media
|
||||
format/progress/size logic were preserved.
|
||||
|
||||
The following larger architectural items remain recommendations rather than
|
||||
being forced into this change:
|
||||
|
||||
- generating the complete command/event client instead of maintaining command
|
||||
names in `src/ipc.ts`;
|
||||
- moving all settings migration logic to one generated cross-language schema;
|
||||
- replacing custom menus with a complete focus-managed menu primitive;
|
||||
- making scheduler runs backend-owned objects with explicit run membership;
|
||||
- adding request-level acknowledgement from the app UI back to automatic
|
||||
browser-download capture.
|
||||
|
||||
## Status and validation legend
|
||||
|
||||
- **wired**: the visible control reaches the intended state/IPC/backend path.
|
||||
- **partially wired**: the main path exists, but a setting, branch, result, or
|
||||
failure state is not fully applied.
|
||||
- **unwired/dead**: code or a visible control exists without an effective
|
||||
consumer.
|
||||
- **fragile**: behavior depends on duplicated state, timing, implicit status
|
||||
rules, swallowed failures, or incomplete synchronization.
|
||||
- **duplicated**: materially similar behavior exists in multiple handlers.
|
||||
- **outdated**: the implementation works or persists, but no longer matches the
|
||||
current interaction architecture or expected desktop behavior.
|
||||
- **unsafe**: destructive or privileged behavior lacks a sufficiently strong
|
||||
confirmation/error boundary.
|
||||
- **Manual QA needed**: static inspection cannot confirm native dialog,
|
||||
notification, menu, focus, accessibility, or operating-system behavior.
|
||||
|
||||
Validation methods named below are non-UI methods: **code trace**, **type
|
||||
check**, **unit test**, **integration test**, **IPC contract check**, and
|
||||
**build check**.
|
||||
|
||||
## Executive risk inventory
|
||||
|
||||
1. **Queue identity and global controls are inconsistent.** `start-now` and
|
||||
`add-to-list` downloads have no `queueId`, while tray Pause/Resume All and
|
||||
Scheduler Run Now operate by queue IDs. Those downloads can be omitted from
|
||||
global actions. Status: **fragile / partially wired**.
|
||||
2. **Tray menu rebuilding changes its action set.** Startup creates Show,
|
||||
Pause All, Resume All, Quit; re-enabling the menu-bar icon creates only Show
|
||||
and Quit. Status: **duplicated / partially wired**.
|
||||
3. **Several settings advertise behavior that is not consumed.** List Row
|
||||
Density and Automatically Check for Updates are persisted but have no
|
||||
runtime consumer. Status: **unwired/dead**.
|
||||
4. **Destructive duplicate replacement suppresses deletion failure.** The
|
||||
existing list item may be removed before `delete_file` fails, and the
|
||||
replacement continues. Status: **unsafe / fragile**.
|
||||
5. **Action availability is implemented separately in rows, single-select
|
||||
menus, multi-select menus, toolbar controls, queue controls, tray handlers,
|
||||
and scheduler handlers.** Their status sets already differ, especially for
|
||||
`processing` and `retrying`. Status: **duplicated / fragile**.
|
||||
6. **The frontend IPC wrapper is incomplete and handwritten.** Rust-generated
|
||||
data types exist, but command/event names and payloads are manually mapped;
|
||||
some live calls bypass the wrapper and some mapped commands are legacy.
|
||||
Status: **outdated / fragile**.
|
||||
7. **Legacy media-quality state and component are dead.** `QualityModal`,
|
||||
`activeMetadata`, `fetchMetadataAction`, and `activeDownloadId` are not
|
||||
connected to the rendered app; the active add flow has its own media
|
||||
selection implementation. Status: **unwired/dead / duplicated**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Main Window
|
||||
## 1. Main window
|
||||
|
||||
### 1.1 Title bar / Toolbar (`DownloadTable.tsx`)
|
||||
### 1.1 Window shell, toolbar, and status bar
|
||||
|
||||
| # | Action | Component / fn | Store action | IPC command | Rust fn | Expected behavior | Validation |
|
||||
|---|--------|----------------|--------------|-------------|---------|-------------------|------------|
|
||||
| M1 | Add Download (+) | `DownloadTable` → `toggleAddModal(true)` | `useDownloadStore.toggleAddModal` | — | — | Opens Add Downloads modal | None |
|
||||
| M2 | Resume All (▶) | `DownloadTable` maps over `paused` items → `handleResume` | `useDownloadStore.resumeDownload` (per item) | `resume_download` | `lib.rs:2211 resume_download` | Resumes every paused download in the current filter | Per-item status check |
|
||||
| M3 | Pause All (⏸) | `DownloadTable` maps over `downloading` items → `handlePause` | — (direct) | `pause_download` | `lib.rs:2137 pause_download` | Pauses every downloading item in the current filter | Per-item aria2 status |
|
||||
| M4 | Show Sidebar (PanelLeft, when hidden) | `DownloadTable` → `toggleSidebar` | `useSettingsStore.toggleSidebar` | — | — | Reveals sidebar; persisted | None |
|
||||
| M5 | Drag window | `WindowDragRegion` / `data-tauri-drag-region` divs → `getCurrentWindow().startDragging()` | — | core window | Tauri core | Moves window | Native |
|
||||
| M6 | Sidebar resize handle | `App.startSidebarResize` (pointer drag) | `setSidebarWidth` (local) → `localStorage` | — | — | Resizes sidebar 190–260px | Clamped in handler |
|
||||
| ID | UI action | Component / function | Store action | IPC command | Rust function | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| MW-01 | Drag window | `WindowDragRegion.onPointerDown`; title-bar drag regions | none | Tauri window API | Tauri runtime | Move the native window | wired; Manual QA needed | code trace, build check |
|
||||
| MW-02 | Resize sidebar | `App.startSidebarResize` | local `sidebarWidth`, `localStorage` | none | none | Resize sidebar from 190–260 px and persist width | wired; fragile pointer-only interaction; Manual QA needed | code trace |
|
||||
| MW-03 | Hide sidebar | `Sidebar` title control | `toggleSidebar` | none | none | Collapse sidebar | wired | code trace |
|
||||
| MW-04 | Show sidebar | `DownloadTable` title control | `toggleSidebar` | none | none | Restore sidebar | wired | code trace |
|
||||
| MW-05 | Add Download | `DownloadTable` plus button | `toggleAddModal(true)` | none | none | Open Add Downloads | wired | code trace |
|
||||
| MW-06 | Resume All in current view | `DownloadTable` toolbar loop | `resumeDownload` per matching row | `resume_download`, sometimes `enqueue_download` | `resume_download`, `enqueue_download` | Start/resume ready or paused items visible in the current filter | wired but fragile: label sounds global and excludes failed/retrying | unit test, IPC contract check |
|
||||
| MW-07 | Pause All in current view | `DownloadTable` toolbar loop | none; direct handler | `pause_download` per downloading row | `pause_download` | Pause downloading items visible in the current filter | wired but fragile: excludes processing/retrying and is not global | unit test, integration test |
|
||||
| MW-08 | Read active/queued/done counts | `App` status bar | derived store state | none | none | Show application totals | wired; `queued` excludes `ready` | code trace |
|
||||
|
||||
⚠️ **M2/M3**: "Resume All"/"Pause All" only act on items in the **current sidebar
|
||||
filter** (e.g. active vs. all). A user on the "Active" filter pausing all won't
|
||||
touch paused items elsewhere — could surprise users who read the button as global.
|
||||
Contrast with the tray's true global Pause/Resume All (`downloadStore.ts:79`).
|
||||
Recommendation: replace the three toolbar buttons with a macOS-style primary
|
||||
Add split button plus an overflow menu for view-scoped Start/Pause actions.
|
||||
Name scope explicitly, for example “Pause Visible Downloads.” Route all status
|
||||
eligibility through shared action selectors.
|
||||
|
||||
### 1.2 Download row actions (`DownloadItem.tsx`, `DownloadTable.tsx`)
|
||||
### 1.2 Download list selection and row actions
|
||||
|
||||
| # | Action | Component / fn | Store action | IPC command | Rust fn | Expected behavior | Validation |
|
||||
|---|--------|----------------|--------------|-------------|---------|-------------------|------------|
|
||||
| M7 | Move Up / Move Down (queued) | `DownloadItem` → `moveInQueue(id,'up'\|'down')` | `useDownloadStore.moveInQueue` | `move_in_queue` | `lib.rs:2613 move_in_queue` | Reorders pending queue; disabled at bounds | `queueIndex === 0` / `length-1` |
|
||||
| M8 | Pause (row, active) | `DownloadItem.handlePause` | — | `pause_download` | `pause_download` | Pauses aria2/native/media task | aria2 status check |
|
||||
| M9 | Resume (row, paused) | `DownloadItem.handleResume` | `resumeDownload` | `resume_download` | `resume_download` | Resumes; re-enqueues if no gid | gid presence |
|
||||
| M10 | Options menu (⋮) / right-click | `DownloadItem` → `setContextMenu` | — | — | — | Opens context menu | — |
|
||||
| M11 | Double-click row | `handleDownloadDoubleClick` | — | `open_downloaded_file` | `commands.rs:27 open_downloaded_file` | Completed → open file; else → Properties | `item.status === 'completed'` |
|
||||
| ID | UI action | Component / function | Store action | IPC command | Rust function | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| MW-09 | Select row | `DownloadTable.handleItemClick` | local `selectedIds` | none | none | Select one row | wired | unit test |
|
||||
| MW-10 | Toggle multi-selection | same; Cmd/Ctrl click | local `selectedIds` | none | none | Add/remove row from selection | wired; Manual QA needed for platform modifiers | unit test |
|
||||
| MW-11 | Range selection | same; Shift click | local `selectedIds`, `lastSelectedId` | none | none | Select visible range | wired; fragile when filtering/reordering changes anchor | unit test |
|
||||
| MW-12 | Right-click row | `DownloadItem.onContextMenu` | local context-menu state | none | none | Select row if needed and open menu | wired; Manual QA needed | code trace |
|
||||
| MW-13 | Options button | `DownloadItem` overflow button | local context-menu state | none | none | Open same menu | wired | code trace |
|
||||
| MW-14 | Double-click completed row | `handleDownloadDoubleClick` → `openDownloadFile` | none | `open_downloaded_file` | `commands::open_downloaded_file` | Open owned downloaded file | wired; Manual QA needed | IPC contract check, integration test |
|
||||
| MW-15 | Double-click unfinished row | `handleDownloadDoubleClick` → `openProperties` | `setSelectedPropertiesDownloadId` | none | none | Open Properties | wired | code trace |
|
||||
| MW-16 | Move queued item up/down | `DownloadItem` hover controls | `moveInQueue` | `move_in_queue` | `QueueManager::move_in_queue` | Reorder backend pending queue | wired; visible order remains download-list order rather than pending order | integration test |
|
||||
| MW-17 | Pause row | `DownloadItem` hover control | none | `pause_download` | `pause_download` | Pause downloading, processing, or retrying item | wired; errors only reach console | integration test |
|
||||
| MW-18 | Start/Resume row | `DownloadItem` hover control | `resumeDownload` | `resume_download`, fallback `enqueue_download` | `resume_download`, `enqueue_download` | Start ready item or resume paused item | wired; errors only reach console | unit test, integration test |
|
||||
| MW-19 | Resize table column | `DownloadTable.startColumnResize` | local `columnWidths` | none | none | Resize column until view remount | wired; outdated because widths are not persisted and pointer-only; Manual QA needed | code trace |
|
||||
|
||||
### 1.3 Row context menu (`DownloadTable.tsx`, floating menu)
|
||||
Recommendation: introduce a `DownloadAction` enum and a single
|
||||
`getAvailableActions(download, selectionContext)` function. Row buttons,
|
||||
context menus, toolbar actions, keyboard commands, tray actions, and queue
|
||||
actions should call the same handlers and eligibility rules.
|
||||
|
||||
| # | Action | Component / fn | Store action | IPC command | Rust fn | Expected behavior | Validation |
|
||||
|---|--------|----------------|--------------|-------------|---------|-------------------|------------|
|
||||
| C1 | Open | `openDownloadFile` | — | `open_downloaded_file` | `open_downloaded_file` | Opens completed file | `status==='completed'`; path authorization |
|
||||
| C2 | Show in Finder | `revealDownloadFile` | — | `reveal_in_file_manager` | `commands.rs:6 reveal_in_file_manager` | Reveals file/part in Finder | `status==='completed'`; path authorization |
|
||||
| C3 | Pause | `handlePause` | — | `pause_download` | `pause_download` | Pauses | status in `{downloading,queued,retrying}` |
|
||||
| C4 | Resume | `handleResume` | `resumeDownload` | `resume_download` | `resume_download` | Resumes | status in `{paused,failed,retrying}` |
|
||||
| C5 | Redownload | `redownload(contextItem.id)` | `useDownloadStore.redownload` | `enqueue_download` (via `dispatchItem`) | `enqueue_download` | Creates a new queued copy | status in `{completed,failed,paused}` |
|
||||
| C6 | Copy Address | `navigator.clipboard.writeText(url)` | — | — | — | Copies URL | try/catch toast |
|
||||
| C7 | Copy File Path | clipboard write | — | — | — | Copies resolved path | `status==='completed'`; filename present |
|
||||
| C8 | Remove | `handleDelete` → `openDeleteModal(id)` | `useDownloadStore.openDeleteModal` | — | — | Opens Delete modal | — |
|
||||
| C9 | Properties | `openProperties` → `setSelectedPropertiesDownloadId` | `useDownloadStore.setSelectedPropertiesDownloadId` | — | — | Opens Properties modal | — |
|
||||
### 1.3 Single-selection context menu
|
||||
|
||||
### 1.4 Column resize
|
||||
| ID | Action | Component / function | Store action | IPC / Rust | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| CM-01 | Open | `openDownloadFile` | none | `open_downloaded_file` / `commands::open_downloaded_file` | Open completed owned file | wired; Manual QA needed | integration test |
|
||||
| CM-02 | Show in Finder | `revealDownloadFile` | none | `reveal_in_file_manager` / `commands::reveal_in_file_manager` | Reveal completed file, partial file, or known destination | partially wired: backend requires an owned file path, but unfinished UI may send only a directory | integration test |
|
||||
| CM-03 | Pause | `handlePause` | none | `pause_download` / `pause_download` | Pause queued/downloading/retrying item | wired, but menu omits `processing` although row action includes it | unit test |
|
||||
| CM-04 | Start/Resume | `handleResume` | `resumeDownload` | `resume_download`, `enqueue_download` | Start ready or resume paused/failed/retrying | fragile: retrying exposes both Pause and Resume in the same menu | unit test, integration test |
|
||||
| CM-05 | Redownload | `redownload` | `redownload` | `enqueue_download` / `enqueue_download` | Create and immediately enqueue a copy | wired; status is initialized as queued and queue identity may be absent | unit test |
|
||||
| CM-06 | Add to Queue | inline menu handler | `updateDownload({queueId})` | none | Reassign logical queue | partially wired: frontend grouping changes but no explicit backend queue/order reconciliation | integration test |
|
||||
| CM-07 | Copy Address | clipboard handler | none | browser clipboard | Copy URL | wired; Manual QA needed | code trace |
|
||||
| CM-08 | Copy File Path | clipboard handler | none | browser clipboard | Copy resolved completed path | wired; Manual QA needed | unit test |
|
||||
| CM-09 | Remove | `handleDelete` | `openDeleteModal` | none | Open removal confirmation | wired | code trace |
|
||||
| CM-10 | Properties | `openProperties` | `setSelectedPropertiesDownloadId` | none | Open Properties | wired | code trace |
|
||||
|
||||
| # | Action | Component / fn | Store action | IPC command | Rust fn | Expected behavior | Validation |
|
||||
|---|--------|----------------|--------------|-------------|---------|-------------------|------------|
|
||||
| M12 | Resize column handle | `DownloadTable.startColumnResize` | `setColumnWidths` (local) | — | — | Resizes table columns | per-column minimums |
|
||||
### 1.4 Multi-selection context menu
|
||||
|
||||
### 1.5 Sidebar (`Sidebar.tsx`)
|
||||
| ID | Action | Component / function | Store action | IPC / Rust | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| CM-11 | Start/Resume selected | inline loop | `resumeDownload` | resume/enqueue commands | Start eligible selected items | wired but duplicated; eligibility differs from toolbar and single menu | unit test |
|
||||
| CM-12 | Add selected to Queue | inline loop | `updateDownload({queueId})` | none | Reassign non-completed items | partially wired / duplicated | integration test |
|
||||
| CM-13 | Copy selected addresses | inline clipboard handler | none | browser clipboard | Copy URLs separated by newlines | wired; no success feedback | code trace |
|
||||
| CM-14 | Remove selected | `openDeleteModal(ids)` | modal state | none | Confirm bulk removal/deletion | wired; destructive operations execute concurrently | integration test |
|
||||
|
||||
| # | Action | Component / fn | Store action | IPC command | Rust fn | Expected behavior | Validation |
|
||||
|---|--------|----------------|--------------|-------------|---------|-------------------|------------|
|
||||
| S1 | Library filters (All/Active/Completed/Unfinished) | `NavItem` → `onSelectFilter` | `App.setFilter` + `setActiveView('downloads')` | — | — | Filters download list | — |
|
||||
| S2 | Folder filters (Musics/Movies/…/Other) | `NavItem` | as above | — | — | Filters by `category` | — |
|
||||
| S3 | Queue item select | `QueueItem` → `onSelectFilter('queue:<id>')` | as above | — | — | Filters by `queueId` | — |
|
||||
| S4 | Add new queue | "Add new queue" button → input → `handleAddQueueSubmit` | `useDownloadStore.addQueue` | — | — | Adds queue (persisted to store.bin) | name trimmed non-empty |
|
||||
| S5 | Queue rename | context → Rename → `handleRenameQueueSubmit` | `useDownloadStore.renameQueue` | — | — | Renames queue | name trimmed non-empty |
|
||||
| S6 | Queue delete | context → Delete | `useDownloadStore.removeQueue` | — | — | Deletes queue; reassigns downloads to Main | `id !== MAIN_QUEUE_ID` |
|
||||
| S7 | Start Queue | context → Start Queue | `useDownloadStore.startQueue` | `enqueue_download`/`resume_download` | dispatchItem | Dispatches/resumes queue's runnable items | queued/paused/failed |
|
||||
| S8 | Pause Queue | context → Pause Queue | `useDownloadStore.pauseQueue` | `pause_download` | `pause_download` | Pauses all downloading in queue | status `downloading` |
|
||||
| S9 | Tools: Scheduler | `ToolItem view='scheduler'` | `setActiveView('scheduler')` | — | — | Shows Scheduler view | — |
|
||||
| S10 | Tools: Speed Limiter | `ToolItem view='speedLimiter'` | `setActiveView('speedLimiter')` | — | — | Shows Speed Limiter view | — |
|
||||
| S11 | Tools: Diagnostics | `ToolItem view='diagnostics'` | `setActiveView('diagnostics')` | — | — | Shows Diagnostics console | — |
|
||||
| S12 | Settings | footer button | `setActiveView('settings')` | — | — | Shows Settings view | — |
|
||||
| S13 | Hide Sidebar | `toggleSidebar` | `useSettingsStore.toggleSidebar` | — | — | Collapses sidebar | — |
|
||||
Recommendation: use an accessible menu primitive with roving focus, Escape,
|
||||
arrow-key navigation, viewport collision handling, and one action registry.
|
||||
The current hover-only nested queue submenu is fragile for keyboard and trackpad
|
||||
users. Manual QA is needed to confirm current focus behavior.
|
||||
|
||||
### 1.6 Global handlers (App-level `useEffect` listeners)
|
||||
### 1.5 Sidebar navigation and queues
|
||||
|
||||
| # | Action | Trigger | Handler | Store/IPC | Rust fn | Expected behavior | Validation |
|
||||
|---|--------|---------|---------|-----------|---------|-------------------|------------|
|
||||
| G1 | Paste URLs | window `paste` (non-input) | `extractValidDownloadUrls` → `openAddModalWithUrls` | store | — | Opens Add modal with pasted URLs | URL scheme allowlist |
|
||||
| G2 | Deep link | `deep-link-add-download` event | `openAddModalWithUrls(payload)` | store | `dispatch_deep_links` → `parse_firelink_urls` | Opens Add modal | scheme/host/length caps |
|
||||
| G3 | Extension add | `extension-add-download` event | `handleExtensionDownload` | store | extension_server | Opens Add modal | dedupe URLs |
|
||||
| G4 | Extension queued batch | `extension-downloads-queued` event | merges into `downloads`/`pendingOrder` | setState | extension_server | Appends new items | id dedupe |
|
||||
| G5 | Schedule trigger | `schedule-trigger` event ('start'/'stop') | `startQueue`/`pauseQueue` + `setSchedulerRunning` | store+IPC | scheduler.rs | Starts/stops main queue | — |
|
||||
| G6 | Post-queue system action | scheduler end + `postQueueAction !== 'none'` | `invoke('perform_system_action')` | — | `lib.rs:2555 perform_system_action` | Sleep/restart/shutdown | automation permission |
|
||||
| G7 | Notification | `download-state` terminal | `sendNotification` | — | — | OS notification | permission; `showNotifications` |
|
||||
| ID | Action | Component / function | Store action | IPC / Rust | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| SB-01 | Select Library filter | `Sidebar.NavItem` | `App.setFilter`, `setActiveView('downloads')` | none | Filter All/Active/Completed/Unfinished | wired | unit test |
|
||||
| SB-02 | Select category folder | `Sidebar.NavItem` | same | none | Filter by category | wired | unit test |
|
||||
| SB-03 | Select queue | `QueueItem` | same | none | Filter by queue ID | wired | unit test |
|
||||
| SB-04 | Add queue | add input, Enter/blur | `addQueue` | DB persistence subscription | DB replace functions | Create a persisted logical queue | wired; duplicate/blank-after-trim names are not explicitly reported | unit test |
|
||||
| SB-05 | Cancel add queue | Escape | local state | none | Close input without adding | wired; blur after Escape can still run submit with current value depending event order; Manual QA needed | unit test |
|
||||
| SB-06 | Rename queue | context action, input Enter/blur | `renameQueue` | DB persistence subscription | DB replace functions | Rename queue | wired; same blur/keyboard fragility | unit test |
|
||||
| SB-07 | Start Queue | queue context menu | `startQueue` | resume/enqueue commands | Start queued/paused/failed items with this queue ID | wired but fragile around `hasBeenDispatched` and absent queue IDs | unit test, integration test |
|
||||
| SB-08 | Pause Queue | queue context menu | `pauseQueue` | `pause_download` | Pause active items in queue | partially wired: only `downloading`, not `processing` or `retrying` | unit test |
|
||||
| SB-09 | Delete Queue | queue context menu | `removeQueue` | DB persistence subscription | Reassign items to Main Queue and delete custom queue | wired; no confirmation | unit test |
|
||||
| SB-10 | Open Scheduler | `ToolItem` | `setActiveView` | none | Show Scheduler | wired | code trace |
|
||||
| SB-11 | Open Speed Limiter | `ToolItem` | `setActiveView` | none | Show Speed Limiter | wired | code trace |
|
||||
| SB-12 | Open Diagnostics | `ToolItem` | `setActiveView` | none | Show Diagnostics | wired | code trace |
|
||||
| SB-13 | Open Settings | footer button | `setActiveView` | none | Show Settings | wired | code trace |
|
||||
|
||||
Recommendation: make queue membership explicit and total. Every non-completed
|
||||
download should either have a queue ID or an explicit `unassigned` state that
|
||||
global commands intentionally include/exclude. Do not use optional `queueId`
|
||||
as an implicit behavior switch.
|
||||
|
||||
---
|
||||
|
||||
## 2. Add Download Window (`AddDownloadsModal.tsx`)
|
||||
## 2. Add Downloads window
|
||||
|
||||
| # | Action | Component / fn | Store action | IPC command | Rust fn | Expected behavior | Validation |
|
||||
|---|--------|----------------|--------------|-------------|---------|-------------------|------------|
|
||||
| A1 | Paste/edit URLs textarea | `setUrls` (local) → debounce parse | — | `fetch_metadata` / `fetch_media_metadata` (via `fetchMediaMetadataDeduped`) | `lib.rs:616 fetch_metadata`, `lib.rs:784 fetch_media_metadata` | Resolves filename/size/formats | `new URL()`; isMediaUrl |
|
||||
| A2 | Refresh Metadata | `setMetadataRefreshNonce(n+1)` (re-runs effect) | — | as above | as above | Re-fetches metadata | active guard |
|
||||
| A3 | Select preview row | `setSelectedItemIndex(i)` | — | — | — | Selects item for media-format panel | bounds |
|
||||
| A4 | Select media format | `selectMediaFormat(idx)` | — (local parsedItems) | — | — | Sets chosen stream; updates filename/size | selectedFormat bounds |
|
||||
| A5 | Browse Save Location | `handleBrowse` → `open({directory:true})` | — | — | — | Sets save location; marks manual | dialog result |
|
||||
| A6 | Target Queue select | `setSelectedQueueId` | — | — | — | Sets target queue | queue exists |
|
||||
| A7 | Connections slider | `setConnections` | — | — | — | 1–16 (disabled for media) | min/max |
|
||||
| A8 | Limit speed per file (checkbox+input) | `setSpeedLimitEnabled`/`setSpeedLimit` | — | — | — | Per-file KiB/s cap | numeric |
|
||||
| A9 | Use authorization (checkbox+user/pass) | `setUseAuth`/`setUsername`/`setPassword` | — | — | — | Sends basic auth | useAuth flag |
|
||||
| A10 | Advanced toggle | `setAdvancedExpanded` | — | — | — | Expands advanced fields | — |
|
||||
| A11 | Verify Checksum (algo+digest) | `setChecksumEnabled`/`setChecksumAlgo`/`setChecksumValue` | — | — | — | Sends `algo=digest` | checksumEnabled |
|
||||
| A12 | Headers / Cookies / Mirrors | `setHeaders`/`setCookies`/`setMirrors` | — | — | — | Raw strings forwarded | trim/empty |
|
||||
| A13 | Add to Queue | `handleStart(false)` → `executeAddDownloads(false,…)` | `addDownload` (status `'paused'`) | `enqueue_download` (via dispatchItem) | `enqueue_download` | Adds paused items | duplicate detection |
|
||||
| A14 | Start Downloads | `handleStart(true)` → `executeAddDownloads(true,…)` | `addDownload` (status `'queued'`) + immediate dispatch | `enqueue_download` | `enqueue_download` | Adds + starts items | duplicate detection |
|
||||
| A15 | Cancel | `toggleAddModal(false)` | `useDownloadStore.toggleAddModal` | — | — | Closes modal | — |
|
||||
### 2.1 Input, metadata, preview, and destination
|
||||
|
||||
**Duplicate detection** (`handleStart` → `DuplicateResolutionModal`):
|
||||
- Checks URL already in queue (`status !== failed/completed`) and file exists on
|
||||
disk via `check_file_exists` and in-store path match.
|
||||
- `DuplicateResolutionModal`: per-conflict **Rename** / **Replace** (file only) /
|
||||
**Skip** select → on Confirm calls `executeAddDownloads(…,resolutions)`.
|
||||
| ID | Action | Component / function | Store action | IPC / Rust | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| AD-01 | Enter/paste URL lines | URL textarea and metadata effect | local state | `fetch_metadata` or `fetch_media_metadata` | Parse each line and show metadata | wired but fragile: any non-empty line is counted as “valid”; metadata requests are sequential | unit test, integration test |
|
||||
| AD-02 | Refresh Metadata | refresh button | nonce increment | same metadata commands | Re-run metadata lookup | wired | unit test |
|
||||
| AD-03 | Select preview item | preview row click/Enter/Space | local selected index | none | Select item for media format details | wired | unit test |
|
||||
| AD-04 | Select media stream | `selectMediaFormat` | local parsed item | none | Change format, extension, and estimated size | wired; duplicated with dead `QualityModal` implementation | unit test |
|
||||
| AD-05 | Browse save location | `handleBrowse` | local manual destination | dialog plugin | Select one shared folder | wired; Manual QA needed | code trace |
|
||||
| AD-06 | Ask where to save on action | `handleAction` | reads setting | dialog plugin | Prompt before committing additions | wired; name says “each file” but one folder is selected for the whole batch | unit test, Manual QA needed |
|
||||
| AD-07 | Read free space | save-location effect | local `freeSpace` | `get_free_space` / `get_free_space` | Show available space for destination | wired; path is not constrained because this is read-only | IPC contract check |
|
||||
|
||||
| # | Action | Component / fn | Store/IPC | Rust fn | Expected behavior | Validation |
|
||||
|---|--------|----------------|-----------|---------|-------------------|------------|
|
||||
| A16 | Pick resolution per conflict | `updateResolution` (local) | — | — | rename/replace/skip | reason.type gates replace |
|
||||
| A17 | Continue | `onConfirm(resolutions)` → `executeAddDownloads` | store+IPC | `enqueue_download`; `delete_file` (replace) | Applies resolutions | loop bounds |
|
||||
Recommendation: separate URL parsing from metadata loading. Build a validated
|
||||
draft list first, reject unsupported schemes immediately, then run metadata
|
||||
lookups concurrently with bounded concurrency and per-item retry/error actions.
|
||||
|
||||
⚠️ **A14 "Start Downloads"** does **not** show any progress/confirmation that
|
||||
duplicate conflicts were resolved; resolution loop uses `check_file_exists` only
|
||||
to gate 'rename', but the rename loop caps at count < 1000 and silently keeps
|
||||
the last tried name if exceeded.
|
||||
### 2.2 Transfer and authentication controls
|
||||
|
||||
| ID | Action | Component / function | Store effect | Backend payload | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| AD-08 | Connections slider | local state | passed to `addDownload` | `connections` | Set 1–16 connections for non-media | wired; defaults to 16 instead of current setting; disabled if any batch item is media | unit test |
|
||||
| AD-09 | Per-file speed toggle/value | local state | passed to `addDownload` | `speed_limit` | Apply per-download limit | wired; accepts invalid/zero text until backend normalization/use | unit test |
|
||||
| AD-10 | Authorization toggle | local state | passed to `addDownload` | username/password | Use ad-hoc credentials | wired; session-only secrets are intentionally not persisted | unit test |
|
||||
| AD-11 | Username/password | local state | passed to store | username/password | Override matching site login | wired | integration test |
|
||||
| AD-12 | Advanced disclosure | local state | none | none | Show advanced fields | wired | code trace |
|
||||
| AD-13 | Checksum toggle/algorithm/digest | local state | passed to store | checksum | Verify checksum | wired; digest format is not validated in UI | unit test |
|
||||
| AD-14 | Headers | local state | passed to store | headers | Add request headers, including extension referer | wired; raw multiline input | integration test |
|
||||
| AD-15 | Cookies | local state | passed to store | cookies | Add Cookie header | wired; raw secret input | integration test |
|
||||
| AD-16 | Mirrors | local state | passed to store | mirrors | Add alternate URIs | wired; scheme/value validation deferred | integration test |
|
||||
|
||||
### 2.3 Commit actions and duplicate modal
|
||||
|
||||
| ID | Action | Component / function | Store action | IPC / Rust | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| AD-17 | Start Downloads | `handleAction({type:'start-now'})` | `addDownload` → `dispatchItem` | `enqueue_download` | Add and immediately dispatch each item | wired; per-item failures are logged and modal still closes | integration test |
|
||||
| AD-18 | Add to List | action menu | `addDownload({type:'add-to-list'})` | DB persistence only | Add as ready without dispatch | wired; omitted from queue-based global actions | unit test |
|
||||
| AD-19 | Add to Queue | nested action menu | `addDownload({type:'add-to-queue'})` | no immediate backend enqueue | Add as queued for selected logical queue | wired; local pending order is updated before backend registration | unit test |
|
||||
| AD-20 | Cancel | footer button | `toggleAddModal(false)` | none | Close and clear pending extension/deep-link fields | wired | code trace |
|
||||
| AD-21 | Choose Rename/Replace/Skip | `DuplicateResolutionModal` | local conflict state | none | Select resolution per conflict | wired; URL duplicates default to Rename although renaming does not resolve URL duplication | unit test |
|
||||
| AD-22 | Continue duplicate resolution | `executeAddDownloads` | remove/add actions | `check_file_exists`, `delete_file`, enqueue commands | Apply selected resolutions and add items | unsafe / fragile: replacement delete failures are swallowed; rename loop silently stops at 999 | integration test |
|
||||
| AD-23 | Cancel duplicate resolution | duplicate modal Cancel | local state | none | Return to Add Downloads without committing | wired | code trace |
|
||||
|
||||
Recommendation: move duplicate resolution into a transactional service that
|
||||
returns typed outcomes. Never remove the existing list item until filesystem
|
||||
replacement succeeds or a backend-owned overwrite operation has accepted the
|
||||
request. Show a batch result summary when some additions fail.
|
||||
|
||||
---
|
||||
|
||||
## 3. Download Properties Window (`PropertiesModal.tsx`)
|
||||
## 3. Download Properties window
|
||||
|
||||
| # | Action | Component / fn | Store action | IPC command | Rust fn | Expected behavior | Validation |
|
||||
|---|--------|----------------|--------------|-------------|---------|-------------------|------------|
|
||||
| P1 | Edit URL | `setUrl` | — | — | — | Saves on Save | non-empty on save |
|
||||
| P2 | Edit File name | `setFileName` | — | — | — | Saves on Save | non-empty on save |
|
||||
| P3 | Select Save Location | `handleBrowse` → `open({directory})` | — | — | — | Sets destination | dialog result |
|
||||
| P4 | Connections | `setConnections` (1–16) | — | — | — | per-file connections | disabled if transferLocked |
|
||||
| P5 | Limit speed (checkbox+value) | `setSpeedLimitEnabled`/`setSpeedLimitValue` | — | — | — | per-file KiB/s | disabled if transferLocked |
|
||||
| P6 | Login mode toggle (matching/custom/none) | `setLoginMode` | — | — | — | Credentials source | disabled if transferLocked |
|
||||
| P7 | Custom username/password | `setUsername`/`setPassword` | — | — | — | Override login | custom mode only |
|
||||
| P8 | Checksum (verify+algo+digest) | `setChecksumEnabled`/`setChecksumAlgorithm`/`setChecksumValue` | — | — | — | `algo=digest` | disabled if transferLocked |
|
||||
| P9 | Cookies / Headers / Mirrors | `setCookies`/`setHeaders`/`setMirrors` | — | — | — | Advanced fields | disabled if transferLocked |
|
||||
| P10 | Advanced toggle | `setAdvancedExpanded` | — | — | — | Expand/collapse | — |
|
||||
| P11 | Save | `handleSave` → `applyProperties(id, updates)` | `useDownloadStore.applyProperties` | `remove_from_queue`/`detach_download_for_reconfigure`/`enqueue_download` (re-dispatch) | `remove_from_queue`, `detach_download_for_reconfigure`, `enqueue_download` | Updates item; re-attaches backend | status guard (throws if active) |
|
||||
| P12 | Cancel | `setSelectedPropertiesDownloadId(null)` | as above | — | — | Closes modal | — |
|
||||
| ID | Action | Component / function | Store action | IPC / Rust | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| PR-01 | Edit URL | local state | applied by `applyProperties` | may re-enqueue later | Change source for eligible item | wired | unit test |
|
||||
| PR-02 | Edit filename | local state | `applyProperties` | may re-enqueue later | Change output name | wired | unit test |
|
||||
| PR-03 | Select save location | `handleBrowse` | local state | dialog plugin | Change destination | wired; Manual QA needed | code trace |
|
||||
| PR-04 | Edit connections | local state | `applyProperties` | enqueue payload | Change future/reconfigured transfer | wired; no on-blur clamp | unit test |
|
||||
| PR-05 | Edit speed limit | local state | `applyProperties` | enqueue payload | Change future/reconfigured transfer | wired | unit test |
|
||||
| PR-06 | Matching/custom/no-login mode | local state | `applyProperties` | dispatch credential resolution | Choose credential source | wired | unit test |
|
||||
| PR-07 | Custom credentials | local state | `applyProperties` | enqueue payload | Use ad-hoc credentials | wired; secret is not persisted | integration test |
|
||||
| PR-08 | Advanced transfer fields | local state | `applyProperties` | enqueue payload | Change checksum/cookies/headers/mirrors | wired | unit test |
|
||||
| PR-09 | Save | `handleSave` | `applyProperties` | `remove_from_queue`, `detach_download_for_reconfigure`, `enqueue_download` depending status | Apply safe changes and close | wired but fragile: behavior is status-dependent and rollback is incomplete after re-dispatch failure | unit test, integration test |
|
||||
| PR-10 | Cancel | footer button | clear selected ID | none | Close without applying | wired | code trace |
|
||||
|
||||
`applyProperties` rules (`useDownloadStore.ts:313`):
|
||||
- **downloading/processing/retrying** → throws "Cannot change properties while
|
||||
transfer is active."
|
||||
- **completed/failed** → updates store only (read-only identity; settings saved
|
||||
for redownload).
|
||||
- **queued** → removes from queue, updates, re-dispatches; on dispatch failure
|
||||
removes from queue.
|
||||
- **paused** → `detach_download_for_reconfigure`, unregister, update (no
|
||||
re-dispatch; resumes on next resume).
|
||||
Status rules:
|
||||
|
||||
⚠️ **P5/P8/P9** use `isTransferLocked` (downloading/processing/retrying) to
|
||||
disable inputs, but `isLocked` also includes `completed`. For a **completed**
|
||||
item the URL/filename/destination are disabled (correct), yet speed-limit,
|
||||
checksum, cookies, headers, mirrors are **editable** — these only matter for
|
||||
redownload, which is the documented intent, but the disable logic is split
|
||||
across two booleans (`isLocked` vs `isTransferLocked`) and is easy to misread.
|
||||
- active `downloading`/`processing`/`retrying`: Save disabled and store rejects.
|
||||
- `ready`/`completed`/`failed`: frontend store update only.
|
||||
- backend-registered `queued`: remove, update, re-dispatch.
|
||||
- backend-registered `paused`: detach with acknowledgement, update, re-dispatch on resume.
|
||||
|
||||
Recommendation: model Properties as a typed edit session with explicit modes:
|
||||
`identity-editable`, `transfer-options-only`, `requires-detach`, and
|
||||
`read-only`. A single policy function should drive disabled fields, copy, save,
|
||||
and backend transition behavior. The current `isLocked` and
|
||||
`isTransferLocked` booleans are easy to diverge.
|
||||
|
||||
---
|
||||
|
||||
## 4. Settings Window (`SettingsView.tsx`, tabbed)
|
||||
## 4. Removal confirmation modal
|
||||
|
||||
| # | Action | Tab | Store setter | IPC command | Rust fn | Validation |
|
||||
|---|--------|-----|--------------|-------------|---------|------------|
|
||||
| T1 | Tab select | — | `setActiveSettingsTab` | — | — | enum |
|
||||
| **Downloads** | | | | | | |
|
||||
| SD1 | Default connections (1–16) | downloads | `setPerServerConnections` | — | — | onBlur clamp 1–16 |
|
||||
| SD2 | Parallel downloads (1–12) | downloads | `setMaxConcurrentDownloads` | `set_concurrent_limit` (App effect) | `set_concurrent_limit` | onBlur clamp 1–12 |
|
||||
| SD3 | Global speed limit | downloads | `setGlobalSpeedLimit` | `set_global_speed_limit` (App effect) | `set_global_speed_limit` | free text |
|
||||
| SD4 | Automatic retries (0–10) | downloads | `setMaxAutomaticRetries` | — | — | onBlur clamp 0–10 |
|
||||
| SD5 | Show notification on completion | downloads | `setShowNotifications` | — | — | toggle |
|
||||
| SD6 | Play sound on completion | downloads | `setPlayCompletionSound` | — | — | requires SD5 |
|
||||
| **Look & Feel** | | | | | | |
|
||||
| SL1 | Theme (system/light/dark/dracula/nord) | lookandfeel | `setTheme` | — | — | App effect applies classes |
|
||||
| SL2 | Font size | lookandfeel | `setAppFontSize` | — | — | `data-font-size` attr |
|
||||
| SL3 | List row density | lookandfeel | `setListRowDensity` | — | — | enum |
|
||||
| SL4 | Dock badge | lookandfeel | `setShowDockBadge` | `update_dock_badge` (setter + effect) | `update_dock_badge` | toggle; clears badge when off |
|
||||
| SL5 | Menu bar icon | lookandfeel | `setShowMenuBarIcon` | `toggle_tray_icon` (App effect) | `toggle_tray_icon` | toggle |
|
||||
| **Network** | | | | | | |
|
||||
| SN1 | Proxy mode (none/system/custom) | network | `setProxyMode` | — | — | radio |
|
||||
| SN2 | Proxy host | network | `setProxyHost` | — | — | shown if custom |
|
||||
| SN3 | Proxy port (1–65535) | network | `setProxyPort` | — | — | onBlur clamp |
|
||||
| SN4 | Custom User Agent | network | `setCustomUserAgent` | — | — | free text + datalist |
|
||||
| **Locations** | | | | | | |
|
||||
| SO1 | Default download path (text+Browse) | locations | `setDefaultDownloadPath` | — | — | `pickDirectory` |
|
||||
| SO2 | Ask where to save each file | locations | `setAskWhereToSaveEachFile` | — | — | toggle |
|
||||
| SO3 | All Categories Base Browse | locations | `setCategoryDirectory` ×7 | `create_category_directories` | `parity::create_category_directories` | creates dirs on disk |
|
||||
| SO4 | Per-category path (text+Browse) | locations | `setCategoryDirectory` | — | — | `handleBrowseCategory` |
|
||||
| SO5 | Reset Defaults | locations | `resetCategoryDirectories` | — | — | restores defaults |
|
||||
| **Site Logins** | | | | | | |
|
||||
| SLg1 | Delete login | sitelogins | `removeSiteLogin` | `delete_keychain_password` | `delete_keychain_password` | removes keychain entry |
|
||||
| SLg2 | Add login (pattern/user/pass) | sitelogins | `addSiteLogin` | `set_keychain_password` | `set_keychain_password` | pattern+user non-empty |
|
||||
| **Power** | | | | | | |
|
||||
| SP1 | Prevent sleep while downloading | power | `setPreventsSleepWhileDownloading` | `set_prevent_sleep` (syncSystemIntegrations + setter) | `set_prevent_sleep` | keepawake on/off |
|
||||
| **Engine** | | | | | | |
|
||||
| SE1 | Recheck engines | engine | — | `get_*_engine_status` ×4 | `check_*` | force re-run; cached otherwise |
|
||||
| SE2 | Show/hide technical details | engine | — (local) | — | — | expand card |
|
||||
| SE3 | Browser Cookies Source | engine | `setMediaCookieSource` | — | — | enum (none/safari/chrome/firefox/edge/brave) |
|
||||
| **Integrations** | | | | | | |
|
||||
| SI1 | Copy Token | integrations | — (clipboard) | — | — | copies pairing token |
|
||||
| SI2 | Regenerate token | integrations | `regeneratePairingToken` | `set_keychain_password` + `set_extension_pairing_token` (App effect) | both | mints new token |
|
||||
| SI3 | Get Extension links | integrations | — (anchor) | — | — | opens external URLs |
|
||||
| **About** | | | | | | |
|
||||
| SA1 | Check for Updates | about | — | `check_for_updates` | `parity::check_for_updates` | toast result |
|
||||
| SA2 | Auto-check updates toggle | about | `setAutoCheckUpdates` | — | — | switch |
|
||||
| ID | Action | Component / function | Store action | IPC / Rust | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| RM-01 | Cancel | `handleCancel` | `closeDeleteModal` | none | Close without changes | wired | code trace |
|
||||
| RM-02 | Remove from list | `handleRemoveFromList` | `removeDownload(id,false)` | `remove_download` | Stop backend work, remove record, retain file | wired; bulk operations run concurrently and partially completed batches remain if one fails | integration test |
|
||||
| RM-03 | Delete file | `handleDeleteFile` | `removeDownload(id,true)` | `trash_download_assets`, then `remove_download` | Trash owned primary/partial assets and remove record | wired with strong ownership checks; bulk partial-failure UX is fragile | integration test |
|
||||
|
||||
Recommendation: use a batch command returning per-item results, then display a
|
||||
clear partial-success summary. Keep the exact owned-path authorization model in
|
||||
`commands.rs`; do not replace it with broad folder-prefix authorization.
|
||||
|
||||
---
|
||||
|
||||
## 5. Tools Views
|
||||
## 5. Settings window
|
||||
|
||||
### 5.1 Scheduler (`SchedulerView.tsx`)
|
||||
### 5.1 Navigation and Downloads
|
||||
|
||||
| # | Action | Component / fn | Store action | IPC command | Rust fn | Expected behavior | Validation |
|
||||
|---|--------|----------------|--------------|-------------|---------|-------------------|------------|
|
||||
| SC1 | Enable Scheduler | `updateDraft('enabled')` | — (draft) | — | — | Toggles draft (saved on Save) | — |
|
||||
| SC2 | Start/Stop time | `updateDraft('startTime'/'stopTime'/'stopTimeEnabled')` | — | — | — | Sets schedule window | time input |
|
||||
| SC3 | Run Every Day | `updateDraft('everyday')` | — | — | — | All days | — |
|
||||
| SC4 | Day toggles | `toggleDay` | — | — | — | Selected days | — |
|
||||
| SC5 | Post-queue action radio | `updateDraft('postQueueAction')` | — | — | — | none/sleep/restart/shutdown | — |
|
||||
| SC6 | Run Now | `runNow` → `startQueue(MAIN_QUEUE_ID)` | `setSchedulerRunning(true)` | enqueue/resume | dispatchItem | Starts main queue now | count>0 |
|
||||
| SC7 | Pause | `pauseNow` → `pauseQueue(MAIN_QUEUE_ID)` | `setSchedulerRunning(false)` | `pause_download` | `pause_download` | Pauses main queue | — |
|
||||
| SC8 | Save Settings | `save` → `setScheduler(normalized)` | `setScheduler` | — | — | Persists scheduler; registered by scheduler.rs | selectedDays fallback |
|
||||
| SC9 | Grant/Revoke Automation | `handlePermissionAction` | — | `request_automation_permission` / `open_automation_settings` | both | macOS Automation for Finder | platform isMac |
|
||||
| ID | Action | Store / IPC | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|
|
||||
| ST-01 | Select one of nine settings tabs | `setActiveSettingsTab` | Switch pane and persist selected tab | wired | unit test |
|
||||
| ST-02 | Default connections | `setPerServerConnections` | Set default 1–16 | wired; only clamped on blur | unit test |
|
||||
| ST-03 | Parallel downloads | `setMaxConcurrentDownloads` → App effect `set_concurrent_limit` | Resize backend concurrency | wired; transient invalid values can reach backend before blur | integration test |
|
||||
| ST-04 | Global speed limit text | `setGlobalSpeedLimit` → App effect `set_global_speed_limit` | Apply global limit | wired but duplicated with Speed Limiter; invalid input silently becomes unlimited in backend normalization | unit test |
|
||||
| ST-05 | Automatic retries | `setMaxAutomaticRetries` | Set future enqueue retry count | wired; current active tasks are unchanged | unit test |
|
||||
| ST-06 | Completion notifications | `setShowNotifications` | Gate terminal OS notifications | wired; permission is requested at startup regardless of setting | integration test, Manual QA needed |
|
||||
| ST-07 | Completion sound | `setPlayCompletionSound` | Add sound to completion notification | wired; failed notifications never use sound | code trace, Manual QA needed |
|
||||
|
||||
### 5.2 Speed Limiter (`SpeedLimiterView.tsx`)
|
||||
### 5.2 Look and Feel
|
||||
|
||||
| # | Action | Component / fn | Store action | IPC command | Rust fn | Expected behavior | Validation |
|
||||
|---|--------|----------------|--------------|-------------|---------|-------------------|------------|
|
||||
| VL1 | Enable toggle | `setEnabled` | — (local) | — | — | Enables draft | — |
|
||||
| VL2 | Value input | `setValue` | — | — | — | numeric | min 1; clamp |
|
||||
| VL3 | Unit KB/s · MB/s | `setUnit` | — | — | — | unit select | — |
|
||||
| VL4 | Quick presets (1/5/10 MB/s) | `preset(n)` | — | — | — | sets value+unit | — |
|
||||
| VL5 | Save Limit | `save` → `setGlobalSpeedLimit` + `setLastCustomSpeedLimitKiB` | both | `set_global_speed_limit` (App effect) | `set_global_speed_limit` | Applies global cap | clamp; disabled→'' |
|
||||
| ID | Action | Store / IPC | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|
|
||||
| ST-08 | Theme | `setTheme`; App root-class effect | Apply system/light/dark/Dracula/Nord | wired; Manual QA needed | code trace |
|
||||
| ST-09 | Font size | `setAppFontSize`; App data attribute | Apply small/standard/large | wired; Manual QA needed | code trace |
|
||||
| ST-10 | List Row Density | `setListRowDensity` | Change download-row density | **unwired/dead**: no consumer outside settings persistence | code trace, unit test |
|
||||
| ST-11 | Dock badge | `setShowDockBadge`; `update_dock_badge` | Show active count | wired but duplicated between setter, App effect, and download-store sync | integration test, Manual QA needed |
|
||||
| ST-12 | Menu bar icon | `setShowMenuBarIcon`; `toggle_tray_icon` | Show/hide tray | partially wired: re-created tray loses Pause/Resume All | integration test, Manual QA needed |
|
||||
|
||||
### 5.3 Diagnostics (`DiagnosticsView.tsx`)
|
||||
### 5.3 Network
|
||||
|
||||
| # | Action | Component / fn | Store action | IPC command | Rust fn | Expected behavior | Validation |
|
||||
|---|--------|----------------|--------------|-------------|---------|-------------------|------------|
|
||||
| DG1 | Clear console | `handleClear` → `setLogs([])` | — (local) | — | — | Empties log view | — |
|
||||
| DG2 | Export Logs | `handleExport` → `save({…})` | — | `export_logs` | `lib.rs:2800 export_logs` | Writes log to chosen path | path from save dialog |
|
||||
| ID | Action | Store / IPC | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|
|
||||
| ST-13 | Proxy mode | `setProxyMode`; dispatch-time `getProxyArgs` | None/system/custom proxy for new dispatches | wired | unit test |
|
||||
| ST-14 | Proxy host/port | setters | Build custom HTTP proxy URL | wired; host/scheme/auth validation is minimal | unit test |
|
||||
| ST-15 | Custom User Agent | `setCustomUserAgent` | Apply to metadata and downloads | wired; preset strings are dated static examples | integration test |
|
||||
|
||||
### 5.4 Locations
|
||||
|
||||
| ID | Action | Store / IPC | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|
|
||||
| ST-16 | Edit base folder text | `setBaseDownloadFolder` | Change automatic category base | wired; does not create folders or verify access until used | unit test |
|
||||
| ST-17 | Browse base folder | `handleBrowseBase`; `create_category_directories` | Select base and create normalized category folders | wired; directory creation warnings do not reach UI | integration test, Manual QA needed |
|
||||
| ST-18 | Ask where to save each file | `setAskWhereToSaveEachFile` | Prompt during Add action | partially wired: one prompt per batch, not per file | unit test |
|
||||
| ST-19 | Edit category path/subfolder | `CategoryFolderInput` | Use relative automatic subfolder or absolute override | wired but fragile: writes persisted settings on each keystroke and infers mode from string prefix | unit test |
|
||||
| ST-20 | Custom folder | `handleBrowseCategory` | Set absolute category override | wired; Manual QA needed | code trace |
|
||||
| ST-21 | Use automatic | clear override | Return to base/subfolder resolution | wired | unit test |
|
||||
| ST-22 | Reset Defaults | `resetCategoryLocations` | Reset subfolders and overrides | wired; no confirmation | unit test |
|
||||
|
||||
The project already has shared frontend location normalization and a backend
|
||||
settings decoder/migration layer, but equivalent migration and normalization
|
||||
logic still exists in both TypeScript and Rust. Recommendation: define one
|
||||
versioned persisted-settings schema and generate both bindings and migrations,
|
||||
or make Rust the authoritative migration service and return normalized settings
|
||||
to the frontend.
|
||||
|
||||
### 5.5 Site Logins
|
||||
|
||||
| ID | Action | Store / IPC | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|
|
||||
| ST-23 | Add login | `handleAddLogin`; `set_keychain_password`; `addSiteLogin` | Store username/pattern and password in keychain | wired; pattern validation is deferred to matching logic | integration test |
|
||||
| ST-24 | Delete login | inline delete; `delete_keychain_password`; `removeSiteLogin` | Remove keychain secret and persisted metadata | fragile: keychain deletion failure is logged but metadata is still removed | integration test |
|
||||
|
||||
### 5.6 Power, Engines, Integrations, About
|
||||
|
||||
| ID | Action | Store / IPC | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|
|
||||
| ST-25 | Prevent system sleep | setter plus download-state sync; `set_prevent_sleep` | Keep system awake during active downloads | wired but duplicated in setter/store synchronization | integration test, Manual QA needed |
|
||||
| ST-26 | Recheck engines | four engine status commands | Validate packaged sidecars | wired | integration test, build check |
|
||||
| ST-27 | Show/hide engine details | local expanded state | Reveal diagnostics | wired | code trace |
|
||||
| ST-28 | Browser Cookies Source | `setMediaCookieSource` | Pass selected browser to media metadata/downloads | wired; Manual QA needed for browser permissions | integration test |
|
||||
| ST-29 | Copy pairing token | clipboard handler | Copy keychain-hydrated token | wired; success toast is shown without awaiting clipboard result | code trace, Manual QA needed |
|
||||
| ST-30 | Regenerate pairing token | `regeneratePairingToken`; keychain + App effect | Rotate token and reconfigure local server | wired but fragile: UI reports success before keychain/server calls confirm | integration test |
|
||||
| ST-31 | Open extension links | external anchors | Open Firefox store or GitHub releases | wired; Manual QA needed | code trace |
|
||||
| ST-32 | Check Now | `check_for_updates` | Compare stable GitHub release version and show toast | wired; available update toast has no action to open release/download | integration test |
|
||||
| ST-33 | Automatically check for updates | `setAutoCheckUpdates` | Check on startup/periodically | **unwired/dead**: persisted only | code trace |
|
||||
| ST-34 | Open source/license links | external anchors | Open project pages | wired; Manual QA needed | code trace |
|
||||
|
||||
The Integrations status text is hard-coded to “Active” and the full port range;
|
||||
it is not backed by server state. Status: **outdated / partially wired**.
|
||||
|
||||
---
|
||||
|
||||
## 6. Menus / Tray
|
||||
## 6. Scheduler
|
||||
|
||||
### 6.1 Tray menu (built twice — see risk note)
|
||||
| ID | Action | Component / function | Store / IPC / Rust | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|---|
|
||||
| SC-01 | Enable Scheduler | draft checkbox | local draft then `setScheduler` on Save | Enable timed triggers | wired | unit test |
|
||||
| SC-02 | Set start/stop time | draft inputs | persisted scheduler; `scheduler::spawn_scheduler` | Trigger start/stop at local time | wired; one-second polling and string-key persistence are fragile | unit test, integration test |
|
||||
| SC-03 | Run Every Day / day buttons | draft controls | persisted selected days | Select schedule days | wired | unit test |
|
||||
| SC-04 | Select post-queue action | radio cards | persisted enum; `perform_system_action` | Do nothing/sleep/restart/shutdown after scheduled work | wired but high-impact; no final confirmation at execution | integration test, Manual QA needed |
|
||||
| SC-05 | Run Now | `runNow` | `startQueue(MAIN_QUEUE_ID)` | Start Main Queue | partially wired: misses unassigned ready/paused/failed downloads despite UI text saying all paused/failed | unit test |
|
||||
| SC-06 | Pause | `pauseNow` | `pauseQueue(MAIN_QUEUE_ID)` | Pause Main Queue | partially wired: only downloading items with Main Queue ID | unit test |
|
||||
| SC-07 | Save Settings | `save` | `setScheduler` | Persist normalized draft | wired | unit test |
|
||||
| SC-08 | Grant permission | `handlePermissionAction` | `request_automation_permission` | Prompt/check macOS Automation | wired; command is also used as a status probe and can prompt during passive view entry; Manual QA needed | integration test |
|
||||
| SC-09 | “Revoke permission” | same handler | `open_automation_settings` | Explain and open System Settings | partially wired/outdated label: app cannot revoke directly | code trace, Manual QA needed |
|
||||
|
||||
Primary tray built in `setup()` (`lib.rs:3192`) with items: **Show Firelink,
|
||||
Pause All, Resume All, Quit**. Menu events emit `tray-action` events handled by
|
||||
`downloadStore.ts:79` (true global pause/resume across all queues). Left-click
|
||||
tray icon restores main window.
|
||||
|
||||
`toggle_tray_icon` (`lib.rs:2822`) **rebuilds** the tray when "Menu bar icon" is
|
||||
toggled, but only registers **Show / Quit** — it omits the Pause All / Resume All
|
||||
items. Because `toggle_tray_icon` checks `tray_by_id("main")` and the setup tray
|
||||
already used id `"main"`, toggling on is a no-op while the setup tray exists.
|
||||
|
||||
| # | Action | Trigger | Handler | IPC / Event | Rust fn | Expected behavior |
|
||||
|---|--------|---------|---------|-------------|---------|-------------------|
|
||||
| TR1 | Show Firelink | tray menu | `restore_main_window` | — | `restore_main_window` | Unminimize/show/focus |
|
||||
| TR2 | Pause All | tray menu | emits `tray-action` 'pause-all' | event | — | Pauses all queues |
|
||||
| TR3 | Resume All | tray menu | emits `tray-action` 'resume-all' | event | — | Starts all queues |
|
||||
| TR4 | Quit | tray menu | `app.exit(0)` | — | — | Exits app |
|
||||
| TR5 | Tray left-click | icon click | `restore_main_window` | — | — | Show window |
|
||||
|
||||
⚠️ There is **no native macOS app menu** (no Edit menu with Cut/Copy/Paste /
|
||||
Cmd+Q, no Window menu). Standard keyboard shortcuts and clipboard editing rely
|
||||
on the webview default. This is a UX gap on macOS.
|
||||
|
||||
### 6.2 Window controls
|
||||
|
||||
Close/minimize/maximize are the native Tauri decorations (title bar hidden;
|
||||
`WindowDragRegion` provides drag only). Close behavior default (no
|
||||
minimize-to-tray override observed).
|
||||
Recommendation: represent scheduler execution as a backend-owned state machine
|
||||
with a specific run ID and set of download IDs. The frontend should observe
|
||||
that run, not infer completion from all globally active statuses.
|
||||
|
||||
---
|
||||
|
||||
## 7. Delete Confirmation (`DeleteConfirmationModal.tsx`)
|
||||
## 7. Speed Limiter
|
||||
|
||||
| # | Action | Component / fn | Store action | IPC command | Rust fn | Expected behavior | Validation |
|
||||
|---|--------|----------------|--------------|-------------|---------|-------------------|------------|
|
||||
| D1 | Remove (from list only) | `handleRemoveFromList` → `removeDownload(id,false)` | `useDownloadStore.removeDownload` | `remove_download` (filepath null) | `remove_download` | Removes item; keeps file | isRemoving flag |
|
||||
| D2 | Delete file | `handleDeleteFile` → `removeDownload(id,true)` | `removeDownload` | `trash_download_assets` + `remove_download` | `trash_download_assets`, `remove_download` | Trashes file+.aria2+.part, removes item | path resolve |
|
||||
| D3 | Cancel | `handleCancel` → `closeDeleteModal` | `closeDeleteModal` | — | — | Closes modal | — |
|
||||
| ID | Action | Store / IPC | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|
|
||||
| SP-01 | Enable/disable draft | local state | Prepare limit change | wired | unit test |
|
||||
| SP-02 | Enter value | local state | Set numeric limit | wired | unit test |
|
||||
| SP-03 | Select KB/s or MB/s | local state | Change unit | wired | unit test |
|
||||
| SP-04 | Select 1/5/10 MB/s preset | local state | Fill common limit | wired | unit test |
|
||||
| SP-05 | Save Limit | settings setters → App effect `set_global_speed_limit` | Apply backend global cap | wired; duplicated with Settings text field; UI claims active jobs are gracefully restarted, but backend only changes aria2 global option | IPC contract check, integration test |
|
||||
|
||||
Recommendation: keep one global speed-limit editor and one parser/formatter.
|
||||
Expose backend capability/result so copy does not promise behavior for media or
|
||||
native transfers that the command does not implement.
|
||||
|
||||
---
|
||||
|
||||
## 8. IPC Command → Rust function map
|
||||
## 8. Diagnostics
|
||||
|
||||
All commands declared in `src/ipc.ts` `CommandMap`. Registered handlers:
|
||||
`src-tauri/src/lib.rs:3550 generate_handler![…]`.
|
||||
|
||||
| IPC command | Rust fn | File:line | Status |
|
||||
|-------------|---------|-----------|--------|
|
||||
| `fetch_metadata` | `fetch_metadata` | lib.rs:616 | ✅ |
|
||||
| `fetch_media_metadata` | `fetch_media_metadata` | lib.rs:784 | ✅ |
|
||||
| `get_engine_status` | `get_engine_status` | lib.rs:1674 | ✅ |
|
||||
| `get_aria2_engine_status` | `get_aria2_engine_status` | lib.rs:1694 | ✅ |
|
||||
| `get_ytdlp_engine_status` | `get_ytdlp_engine_status` | lib.rs:1702 | ✅ |
|
||||
| `get_ffmpeg_engine_status` | `get_ffmpeg_engine_status` | lib.rs:1707 | ✅ |
|
||||
| `get_deno_engine_status` | `get_deno_engine_status` | lib.rs:1712 | ✅ |
|
||||
| `open_file` | `open_file` | lib.rs:1018 | ✅ (unused by UI; UI uses open_downloaded_file) |
|
||||
| `show_in_folder` | `show_in_folder` | lib.rs:1031 | ✅ (unused by UI; UI uses reveal_in_file_manager) |
|
||||
| `reveal_in_file_manager` | `commands::reveal_in_file_manager` | commands.rs:6 | ✅ |
|
||||
| `open_downloaded_file` | `commands::open_downloaded_file` | commands.rs:27 | ✅ |
|
||||
| `trash_download_assets` | `commands::trash_download_assets` | commands.rs:45 | ✅ |
|
||||
| `pause_download` | `pause_download` | lib.rs:2137 | ✅ |
|
||||
| `resume_download` | `resume_download` | lib.rs:2211 | ✅ |
|
||||
| `remove_download` | `remove_download` | lib.rs:2292 | ✅ |
|
||||
| `detach_download_for_reconfigure` | `detach_download_for_reconfigure` | lib.rs:2364 | ✅ |
|
||||
| `enqueue_download` | `enqueue_download` | lib.rs:2565 | ✅ |
|
||||
| `enqueue_many` | `enqueue_many` | lib.rs:2586 | ✅ |
|
||||
| `move_in_queue` | `move_in_queue` | lib.rs:2613 | ✅ |
|
||||
| `remove_from_queue` | `remove_from_queue` | lib.rs:2622 | ✅ |
|
||||
| `get_pending_order` | `get_pending_order` | lib.rs:2560 | ✅ |
|
||||
| `set_concurrent_limit` | `set_concurrent_limit` | lib.rs:2636 | ✅ |
|
||||
| `set_global_speed_limit` | `set_global_speed_limit` | lib.rs:2663 | ✅ |
|
||||
| `update_dock_badge` | `update_dock_badge` | lib.rs:2507 | ✅ |
|
||||
| `set_prevent_sleep` | `set_prevent_sleep` | lib.rs:2526 | ✅ |
|
||||
| `perform_system_action` | `perform_system_action` | lib.rs:2555 | ✅ |
|
||||
| `request_automation_permission` | `request_automation_permission` | lib.rs:2680 | ✅ |
|
||||
| `open_automation_settings` | `open_automation_settings` | lib.rs:2707 | ✅ |
|
||||
| `get_free_space` | `get_free_space` | lib.rs:2721 | ✅ |
|
||||
| `set_keychain_password` | `set_keychain_password` | lib.rs:2758 | ✅ |
|
||||
| `get_keychain_password` | `get_keychain_password` | lib.rs:2765 | ✅ |
|
||||
| `delete_keychain_password` | `delete_keychain_password` | lib.rs:2771 | ✅ |
|
||||
| `check_file_exists` | `check_file_exists` | lib.rs:2778 | ✅ |
|
||||
| `delete_file` | `delete_file` | lib.rs:2787 | ✅ |
|
||||
| `toggle_tray_icon` | `toggle_tray_icon` | lib.rs:2823 | ⚠️ see TR note |
|
||||
| `set_extension_pairing_token` | `set_extension_pairing_token` | lib.rs:2875 | ✅ |
|
||||
| `set_extension_frontend_ready` | `set_extension_frontend_ready` | lib.rs:2892 | ✅ |
|
||||
| `get_system_proxy` | `parity::get_system_proxy` | parity.rs:7 | ✅ |
|
||||
| `get_file_category` | `parity::get_file_category` | parity.rs:22 | ✅ (UI uses TS `categoryForFileName` instead) |
|
||||
| `check_for_updates` | `parity::check_for_updates` | parity.rs:84 | ✅ |
|
||||
| `is_supported_media` | `parity::is_supported_media` | parity.rs:180 | ✅ (declared in ipc.ts but UI uses TS `isMediaUrl`/`get_supported_media_domains`) |
|
||||
| `create_category_directories` | `parity::create_category_directories` | parity.rs:144 | ✅ |
|
||||
| `export_logs` | `export_logs` | lib.rs:2800 | ✅ |
|
||||
| `db_save_settings` | — | — | ❌ declared in `ipc.ts` but **not registered** in Rust |
|
||||
| `db_load_settings` | — | — | ❌ declared in `ipc.ts` but **not registered** in Rust |
|
||||
| `db_get_all_downloads` | — | — | ❌ declared in `ipc.ts` but **not registered** in Rust |
|
||||
| `db_save_download` | — | — | ❌ declared in `ipc.ts` but **not registered** in Rust |
|
||||
| `db_delete_download` | — | — | ❌ declared in `ipc.ts` but **not registered** in Rust |
|
||||
| `start_download` | — | — | ❌ declared in `ipc.ts` (`StartDownloadArgs`) but **not registered** in Rust |
|
||||
| `start_media_download` | — | — | ❌ declared in `ipc.ts` (`StartMediaDownloadArgs`) but **not registered** in Rust |
|
||||
| `get_supported_media_domains` | `parity::get_supported_media_domains` | parity.rs:175 | ✅ (called directly via `invoke` in `downloads.ts`, not in typed `CommandMap`) |
|
||||
|
||||
> Persistence actually runs through the Tauri `plugin-store` (`LazyStore` on
|
||||
> `store.bin`) in the Zustand subscriber (`useDownloadStore.ts:646`), not the
|
||||
> `db_*` commands — those are dead declarations. Likewise `start_download` /
|
||||
> `start_media_download` are superseded by `enqueue_download`/`enqueue_many` via
|
||||
> the `dispatchItem` flow.
|
||||
|
||||
### Backend events (Rust → Frontend)
|
||||
|
||||
| Event | Payload | Emitted by | Listened by |
|
||||
|-------|---------|------------|-------------|
|
||||
| `download-progress` | `DownloadProgressEvent` | download/queue tasks | `downloadStore.ts:31` |
|
||||
| `download-state` | `DownloadStateEvent` | pause/resume/remove + tasks | `downloadStore.ts:49`, `App.tsx:210` |
|
||||
| `download-complete` / `download-failed` | string | (declared; not emitted in read code) | — |
|
||||
| `schedule-trigger` | 'start'\|'stop' | scheduler.rs | `App.tsx:113` |
|
||||
| `tray-action` | 'pause-all'\|'resume-all' | tray menu | `downloadStore.ts:80` |
|
||||
| `extension-add-download` | `ExtensionDownload` | extension_server | `App.tsx:231` |
|
||||
| `extension-downloads-queued` | `DownloadItem[]` | extension_server | `App.tsx:234` |
|
||||
| `deep-link-add-download` | string | deep-link dispatch | `App.tsx:251` |
|
||||
| `log` | `{level,message}` | tauri-plugin-log | `DiagnosticsView.tsx:19` |
|
||||
| ID | Action | Component / function | IPC / Rust | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|---|
|
||||
| DG-01 | Filter severity | local `levelFilter` | none | Filter visible attached logs | wired | unit test |
|
||||
| DG-02 | Clear console | `handleClear` | none | Clear only current in-memory view | wired; label could clarify that log file remains | unit test |
|
||||
| DG-03 | Export Logs | `handleExport` | `export_logs` / `export_logs` | Copy current log file to selected path | wired; no success/error toast; Manual QA needed | integration test |
|
||||
|
||||
---
|
||||
|
||||
## 9. Highest-Risk Areas
|
||||
## 9. Toasts, notifications, and error surfaces
|
||||
|
||||
Ranked by likelihood × impact:
|
||||
| ID | User-facing action/event | Component / function | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|
|
||||
| NT-01 | Dismiss toast | `ToastItem` close button | Remove toast with exit animation | wired | unit test |
|
||||
| NT-02 | Hover/focus toast | `ToastItem` timer logic | Pause auto-dismiss | wired | unit test |
|
||||
| NT-03 | Pairing-token migration: Copy token | actionable toast in `App` | Copy token and acknowledge notice | partially wired: clipboard failure is ignored; toast is not explicitly dismissed | unit test |
|
||||
| NT-04 | Pairing-token migration: Integrations | actionable toast in `App` | Open Integrations and acknowledge | wired | unit test |
|
||||
| NT-05 | Download complete/failed OS notification | `App` terminal-state listener | Show native notification when enabled | wired; permission request result is ignored; Manual QA needed | integration test |
|
||||
| NT-06 | Root render failure | `ErrorBoundary` | Show fatal error and component stack | wired but outdated/unsafe for production: exposes technical stack and offers no recovery action | build check |
|
||||
|
||||
1. **Tray menu divergence / duplicate tray builder** (`lib.rs:3192` vs
|
||||
`lib.rs:2822`). The setup tray registers Pause All / Resume All; the toggle
|
||||
builder does not and silently no-ops when the setup tray already exists.
|
||||
Toggling "Menu bar icon" off then on can leave the user with a tray that has
|
||||
no pause/resume items, or no tray at all depending on ordering. **Risk: lost
|
||||
global queue control + confusing state.**
|
||||
The root toast provider is already the correct architectural direction.
|
||||
Recommendation: standardize async user actions around a shared result-to-toast
|
||||
helper, and ensure success is shown only after clipboard, keychain, filesystem,
|
||||
or IPC completion.
|
||||
|
||||
2. **Resume/Pause All scope mismatch** (M2/M3 vs TR2/TR3). Toolbar Resume/Pause
|
||||
All only act on the **current filter**, while tray Resume/Pause All act on
|
||||
**all queues**. Same icon, different semantics — users will mis-pause and
|
||||
leave downloads running, or fail to resume paused items.
|
||||
---
|
||||
|
||||
3. **`perform_system_action` (Sleep/Restart/Shutdown)** is invoked with no
|
||||
confirmation dialog from the scheduler path (`App.tsx:139`) once the queue
|
||||
drains. A user who enables "Shut down" and walks away loses unsaved work in
|
||||
other apps. Permission is checked, but no undo. **Risk: destructive,
|
||||
cross-application.**
|
||||
## 10. Tray and native menus
|
||||
|
||||
4. **`applyProperties` state machine** (`useDownloadStore.ts:313`). Five
|
||||
branches (active throws, completed/failed store-only, queued re-dispatch,
|
||||
paused detach) with backend registration tracked in a separate `Set`. A
|
||||
failed `detach_download_for_reconfigure` throws and preserves old props, but
|
||||
a failed re-dispatch on queued silently removes from queue. Easy to reach a
|
||||
state where the UI shows "queued" but the backend has no task
|
||||
(`backendRegisteredIds` desync).
|
||||
No custom main-window application menu actions were found. The native
|
||||
user-facing menu is the menu-bar/tray menu.
|
||||
|
||||
5. **`dispatchItem` in-flight dedupe + failure → 'failed'**
|
||||
(`useDownloadStore.ts:21`). On enqueue failure the item is marked `failed`
|
||||
without surfacing the error to the user (only `console.error`). Users see a
|
||||
mysteriously failed download with no diagnostic.
|
||||
| ID | Action | Rust location | Frontend/store path | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|---|
|
||||
| TR-01 | Left-click tray icon | startup and rebuilt tray handlers | `restore_main_window` | Show/focus main window | wired; Manual QA needed | integration test |
|
||||
| TR-02 | Show Firelink | tray menu | `restore_main_window` | Show/focus main window | wired | integration test |
|
||||
| TR-03 | Pause All | startup tray emits `tray-action` | `initDownloadListener` → `pauseQueue` per distinct queue ID | Pause all downloads | partially wired: excludes unassigned items and queue pause ignores processing/retrying | integration test |
|
||||
| TR-04 | Resume All | startup tray emits `tray-action` | `startQueue` per distinct queue ID | Resume all downloads | partially wired: excludes unassigned items | integration test |
|
||||
| TR-05 | Quit | tray handler | `app.exit(0)` | Exit application | wired; Manual QA needed | integration test |
|
||||
| TR-06 | Close main window | `on_window_event` | hide instead of exit | Keep app running in background | wired; discoverability depends on Dock/tray state; Manual QA needed | integration test |
|
||||
|
||||
6. **Path authorization surface** (`commands.rs`). `open_downloaded_file`,
|
||||
`reveal_in_file_manager`, `trash_download_assets` all gate on
|
||||
`known_download_paths` (registered via `download_ownership`). If an item is
|
||||
redownloaded or its destination changes, stale registration could either
|
||||
block a legitimate open or — worse — the `delete_file` path used by
|
||||
duplicate "Replace" (`AddDownloadsModal` A17) bypasses ownership checks and
|
||||
uses `is_safe_path` only. **Risk: deleting the wrong file if a crafted path
|
||||
passes `is_safe_path`.**
|
||||
Recommendation: define one tray menu builder and update visibility in place.
|
||||
Use backend commands for true global pause/resume rather than reconstructing
|
||||
global behavior from optional frontend queue IDs.
|
||||
|
||||
7. **Dead IPC declarations** (`start_download`, `start_media_download`, all
|
||||
`db_*`). Not exploitable, but a maintenance trap: any future caller using the
|
||||
typed `invokeCommand` for these will get a runtime "command not found" with
|
||||
no compile-time signal.
|
||||
---
|
||||
|
||||
8. **No native macOS app menu.** No Edit menu (cut/copy/paste/select-all),
|
||||
Cmd+Q, or Window menu. Clipboard paste into the Add-URLs textarea works only
|
||||
via the webview default; the global paste handler (G1) intentionally ignores
|
||||
inputs, so Cmd+V in the textarea is the only path — fine, but no menu affordance.
|
||||
## 11. Extension-triggered UI actions
|
||||
|
||||
9. **Duplicate-resolution rename loop** (A17) caps at 1000 attempts and on
|
||||
exhaustion keeps the last tried name without warning, which could collide
|
||||
and overwrite via the subsequent `addDownload` if the backend doesn't guard.
|
||||
### 11.1 Firefox extension popup and browser menus
|
||||
|
||||
10. **`SpeedLimiterView` Save applies via effect** (`App.tsx:101`), not directly.
|
||||
The `previousSpeedLimit` ref dedupes, but the indirection means the visible
|
||||
toast ("Global limit saved…") can fire before the backend actually accepts
|
||||
the limit, and a failed `set_global_speed_limit` is only logged.
|
||||
| ID | Extension action | JS function/listener | App effect | Status | Validation |
|
||||
|---|---|---|---|---|---|
|
||||
| EX-01 | Toggle Capture All Downloads | popup `globalToggle` | Browser download interception on/off | wired | extension integration test |
|
||||
| EX-02 | Disable capture on current site | popup `siteToggle` | Host-specific interception exclusion | wired; naming stores inverse boolean | unit test |
|
||||
| EX-03 | Save pairing token | popup `saveTokenBtn` | Store token and probe `/ping` | wired; token remains extension-local plaintext storage by browser design | integration test |
|
||||
| EX-04 | Expand/collapse token field | popup `pairingToggleBtn` | Show/hide pairing controls | wired | code trace |
|
||||
| EX-05 | Toggle popup theme | popup `themeToggleBtn` | Persist popup light/dark theme | wired | code trace |
|
||||
| EX-06 | Download link with Firelink | browser context menu | authenticated `/download` → app Add Downloads | wired | integration test |
|
||||
| EX-07 | Download selected with Firelink | context menu + injected `content.js` | Extract selected links and open Add Downloads | wired; fallback paths are complex and duplicated | integration test |
|
||||
| EX-08 | Automatic browser download capture | `chrome.downloads.onCreated` | Forward, then cancel/erase browser download after accepted response | wired but high-risk: acceptance means UI event emission, not confirmed app enqueue | integration test |
|
||||
| EX-09 | Deep-link fallback | `sendToFirelink.triggerDeepLink` | `firelink://add` → `deep-link-add-download` | Open Add Downloads when local server unavailable | wired; browser fallback behavior needs Manual QA | integration test |
|
||||
|
||||
### 11.2 Native extension server to UI
|
||||
|
||||
| ID | Trigger | Backend / frontend path | Expected behavior | Status | Validation |
|
||||
|---|---|---|---|---|---|
|
||||
| EX-10 | Authenticated `/download` | `extension_server::download_handler` → `extension-add-download` → `handleExtensionDownload` | Wake/focus app and merge sanitized URLs into Add Downloads | wired | integration test |
|
||||
| EX-11 | Deep link | deep-link parser/event → `openAddModalWithUrls` | Open Add Downloads with URLs | wired | integration test |
|
||||
| EX-12 | Extension connection status shown in Settings | static Settings text | Report live server state | **unwired/dead**: text is hard-coded | code trace |
|
||||
|
||||
Recommendation: return a request ID from `/download`, acknowledge only after
|
||||
the frontend has created a draft, and optionally return a second “queued”
|
||||
result. That would let automatic browser capture cancel the browser download
|
||||
only after Firelink has actually accepted responsibility.
|
||||
|
||||
---
|
||||
|
||||
## 12. IPC-backed background actions affecting UI
|
||||
|
||||
| ID | Background action | Frontend path | IPC / Rust | User-facing result | Status | Validation |
|
||||
|---|---|---|---|---|---|---|
|
||||
| BG-01 | Database hydration | `initDB` | DB load commands | Restore downloads/queues and auto-enqueue queued items | fragile: `enqueue_many` per-item results are ignored and frontend backend-registration state is not rebuilt directly | integration test |
|
||||
| BG-02 | Progress events | `initDownloadListener` | `download-progress` | Update progress, speed, ETA, size | wired | integration test |
|
||||
| BG-03 | State events | `initDownloadListener` | `download-state` | Update lifecycle, pending order, registration state | wired but event errors/retry reasons are not surfaced in row UI | integration test |
|
||||
| BG-04 | Scheduler trigger | `App` listener | `schedule-trigger` | Start/pause Main Queue | wired with queue-scope gaps | integration test |
|
||||
| BG-05 | Pairing token hydration | App startup | keychain hydration commands | Configure extension authentication and migration toast | wired | integration test |
|
||||
| BG-06 | Theme system change | App media-query listener | none | Follow macOS appearance | wired; Manual QA needed | code trace |
|
||||
| BG-07 | Paste outside inputs | App paste listener | none | Extract supported URLs and open Add Downloads | wired | unit test |
|
||||
|
||||
---
|
||||
|
||||
## 13. Dead, duplicated, outdated, and drift-prone implementation inventory
|
||||
|
||||
| Area | Evidence | Status | Practical replacement |
|
||||
|---|---|---|---|
|
||||
| Legacy media quality flow | `QualityModal.tsx`, `activeMetadata`, `fetchMetadataAction`, `activeDownloadId` have no rendered caller | unwired/dead, duplicated | Remove after confirming no external import; keep the Add Downloads media-format flow |
|
||||
| IPC command typing | `src/ipc.ts` manually lists commands; `utils/downloads.ts` and tray listener bypass it; several listed commands are legacy UI-unused | outdated, fragile | Generate command and event bindings from Rust, including argument/result types |
|
||||
| Action rules | Status checks repeated across row, toolbar, menus, queues, tray, scheduler | duplicated, fragile | Shared action enum, selectors, and command handlers |
|
||||
| Tray construction | Startup and `toggle_tray_icon` build different menus | duplicated, partially wired | One builder/update function |
|
||||
| Global speed editor | Settings and Speed Limiter use different input models/copy | duplicated | One shared control and parser |
|
||||
| Settings decoding/migration | TypeScript normalization plus Rust decoder/migration | duplicated, fragile | One authoritative versioned schema/migration boundary |
|
||||
| Category classification | TypeScript and Rust maintain separate extension lists | duplicated, drift-prone | Generate/shared data table or backend-owned classification |
|
||||
| Supported media domains | Rust list copied into frontend fallback, then asynchronously refreshed | duplicated, drift-prone | Generated/static shared binding or required startup capability payload |
|
||||
| Path resolution | Shared frontend resolver exists, but store also has a separate tilde resolver and backend has additional resolution/authorization paths | duplicated, fragile | One frontend resolver plus backend canonical ownership API; avoid new scattered joins |
|
||||
| Boolean lock policy | `isLocked`, `isTransferLocked`, many status condition arrays | fragile | Explicit edit/action policy enums |
|
||||
| Update preference | `autoCheckUpdates` persisted without consumer | unwired/dead | Startup update-check service with throttling and actionable result |
|
||||
| Row density preference | `listRowDensity` persisted without consumer | unwired/dead | Root data attribute/CSS variable or remove setting |
|
||||
| Integration status | Static “Active” server label | outdated | Query actual bound port/readiness and render truthful state |
|
||||
| Error handling | Many clipboard, pause, folder creation, export, metadata, and deletion errors go only to console or are swallowed | fragile | Standard async action result and root toast reporting |
|
||||
| Custom menus | Hand-built floating div/button menus and hover submenus | outdated/fragile | Accessible menu/split-button primitive with keyboard and collision support |
|
||||
| Scheduler lifecycle | Frontend boolean plus global download scan | fragile | Backend-owned run state machine with run membership |
|
||||
|
||||
---
|
||||
|
||||
## 14. Recommended modernization sequence
|
||||
|
||||
### Priority 1 — behavior correctness
|
||||
|
||||
1. Define total queue ownership and make true global Pause/Resume backend
|
||||
commands.
|
||||
2. Centralize action eligibility and execution for row/menu/toolbar/queue/tray.
|
||||
3. Make duplicate replacement transactional and surface batch failures.
|
||||
4. Fix tray reconstruction so the same menu is retained after hide/show.
|
||||
5. Wire or remove Row Density and Automatic Update Check.
|
||||
|
||||
### Priority 2 — contract and lifecycle reliability
|
||||
|
||||
1. Generate IPC command/event bindings from Rust and prohibit raw command-name
|
||||
strings outside the generated client.
|
||||
2. Reconcile `enqueue_many` results into frontend registration/status state.
|
||||
3. Replace scheduler completion inference with an explicit backend run model.
|
||||
4. Consolidate settings migration and location normalization around one
|
||||
authoritative schema.
|
||||
|
||||
### Priority 3 — modern desktop interaction
|
||||
|
||||
1. Use an accessible Add split button and accessible context/overflow menus.
|
||||
2. Add keyboard commands for Add, Start/Resume, Pause, Remove, Properties, and
|
||||
search/filter.
|
||||
3. Persist column widths and support reset-to-default.
|
||||
4. Replace hard-coded extension status with live connection state.
|
||||
5. Add actionable update notifications with a safe external release link.
|
||||
|
||||
---
|
||||
|
||||
## 15. Static verification matrix
|
||||
|
||||
Recommended non-UI checks for this inventory:
|
||||
|
||||
- `npm run build` — TypeScript and production frontend build.
|
||||
- `npm run test -- --run` — store and location unit tests.
|
||||
- `cargo check --manifest-path src-tauri/Cargo.toml` — Rust command/type check.
|
||||
- `cargo test --manifest-path src-tauri/Cargo.toml --all-targets` — queue,
|
||||
ownership, settings, extension, and engine tests.
|
||||
- `npm run bindings` followed by a clean diff check — generated Rust data
|
||||
binding drift.
|
||||
- Add a command-registration/TypeScript-command-map contract test until the IPC
|
||||
client is generated.
|
||||
|
||||
Manual QA remains needed for native dialogs, Finder/open behavior, tray
|
||||
visibility and focus, notification permission/sound, macOS Automation prompts,
|
||||
external links, browser extension fallback behavior, menu keyboard navigation,
|
||||
and pointer resizing.
|
||||
|
||||
Reference in New Issue
Block a user