Files
Firelink/INTERACTION_REVIEW.md
T
NimBold 038f31b988 feat(settings): modernize download location settings
- Replace split 'Default Download Path' and 'All Categories Base' with a canonical baseDownloadFolder and categorySubfolders model
- Retain absolute category overrides only where required
- Update Add window to accurately reflect intended destination:
  - Single URL: specific category path
  - Multiple URLs: base folder with explanatory text
  - Manual Browse override: selected folder
- Centralize path resolution logic shared by Rust backend and UI
2026-06-20 19:22:20 +03:30

438 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Firelink — UI Interaction Inventory
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.**
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)
---
## 1. Main Window
### 1.1 Title bar / Toolbar (`DownloadTable.tsx`)
| # | 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 190260px | Clamped in handler |
⚠️ **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`).
### 1.2 Download row actions (`DownloadItem.tsx`, `DownloadTable.tsx`)
| # | 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'` |
### 1.3 Row context menu (`DownloadTable.tsx`, floating menu)
| # | 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.4 Column resize
| # | 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.5 Sidebar (`Sidebar.tsx`)
| # | 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 | — |
### 1.6 Global handlers (App-level `useEffect` listeners)
| # | 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` |
---
## 2. Add Download Window (`AddDownloadsModal.tsx`)
| # | 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` | — | — | — | 116 (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 | — |
**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)`.
| # | 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 |
⚠️ **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.
---
## 3. Download Properties Window (`PropertiesModal.tsx`)
| # | 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` (116) | — | — | — | 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 | — |
`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).
⚠️ **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.
---
## 4. Settings Window (`SettingsView.tsx`, tabbed)
| # | Action | Tab | Store setter | IPC command | Rust fn | Validation |
|---|--------|-----|--------------|-------------|---------|------------|
| T1 | Tab select | — | `setActiveSettingsTab` | — | — | enum |
| **Downloads** | | | | | | |
| SD1 | Default connections (116) | downloads | `setPerServerConnections` | — | — | onBlur clamp 116 |
| SD2 | Parallel downloads (112) | downloads | `setMaxConcurrentDownloads` | `set_concurrent_limit` (App effect) | `set_concurrent_limit` | onBlur clamp 112 |
| SD3 | Global speed limit | downloads | `setGlobalSpeedLimit` | `set_global_speed_limit` (App effect) | `set_global_speed_limit` | free text |
| SD4 | Automatic retries (010) | downloads | `setMaxAutomaticRetries` | — | — | onBlur clamp 010 |
| 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 (165535) | 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 |
---
## 5. Tools Views
### 5.1 Scheduler (`SchedulerView.tsx`)
| # | 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 |
### 5.2 Speed Limiter (`SpeedLimiterView.tsx`)
| # | 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→'' |
### 5.3 Diagnostics (`DiagnosticsView.tsx`)
| # | 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 |
---
## 6. Menus / Tray
### 6.1 Tray menu (built twice — see risk note)
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).
---
## 7. Delete Confirmation (`DeleteConfirmationModal.tsx`)
| # | 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 | — |
---
## 8. IPC Command → Rust function map
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` |
---
## 9. Highest-Risk Areas
Ranked by likelihood × impact:
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.**
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.**
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).
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.
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`.**
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.
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.
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.