Bundle optional Inter and Outfit families, preserve the platform system default, and provide script-aware fallbacks for every supported locale.
Refs #31
Keep custom Windows/Linux controls available during platform detection, preserve RTL table order while aligning filenames left, and persist sidebar placement overrides.
Refs #17
Remember manually selected Add-window folders during the current session, with a persisted Locations toggle and cross-platform path handling.\n\nRefs #23
Implement marker-based portable storage, portable WebView and log paths, secure queue and migration sanitization, and Windows portable ZIP validation while preserving the NSIS installer path.
Refs #15
Add a Settings > Locations toggle that disables automatic category subfolders while keeping the selected base download folder as the automatic destination.
Persist the setting across frontend and backend startup paths and disable the category subfolder controls when the feature is off.
Fixes#6
Update yt-dlp to 2026.07.04 and refresh the bundled macOS runtime plus engine locks.
Route every non-media download through aria2 by removing the native HTTP fallback path, native GID handling, and the old direct-download harness.
Retry transient aria2 startup/RPC failures before failing, then show the real last error in the download row and Properties modal so Windows failures are diagnosable instead of silent.
Refresh docs and tests around the aria2-only file-download contract, and advance the Firefox extension submodule to its published wording cleanup.
Modernize the Tools release surfaces for the cross-platform re-release checklist.
- make Speed Limiter presets unit-aware and persist custom removable quick presets
- keep diagnostic logging opt-in on clean installs and honor the saved log state on startup
- stage Scheduler enablement edits without discarding unsaved draft changes
- extend persisted settings and bindings for log opt-in and speed preset state
Verification:
- npm run build
- npm test -- --run src/store/useDownloadStore.test.ts
- cargo test --lib
- cargo test settings --lib
- Update 'fetch_metadata' to detect 'Accept-Ranges' HTTP header
- Persist 'resumable' flag in DownloadItem across backend and frontend
- Add confirmation dialog when pausing non-resumable downloads (single and bulk actions)
- Improve UI for start/pause/options buttons by adopting the application accent color on hover
The root cause was hydrate_extension_pairing_token accessing the
keychain when keychainAccessGranted was true in the DB. Any keychain
access on macOS triggers the system prompt when the binary signature
changes after an update.
Architecture change: two-store model.
- The SQLite settings DB is now the primary store for the token.
hydrate_extension_pairing_token reads from it exclusively -- it
never touches the OS keychain. No system prompt on startup.
- The OS keychain remains defence-in-depth: grant_keychain_access
still writes the token there, but it is only reached from the
explicit Grant Access button, so any system prompt is user-initiated.
DB helpers: load_pairing_token_from_settings / save_pairing_token_to_settings
hydrate_extension_pairing_token: reads from DB, skips keychain entirely
grant_keychain_access: syncs token to DB after keychain access
Frontend: extensionPairingToken included in partialize for auto-persist
- Implement scheduler hydration barrier to prevent premature triggers
- Track scheduler exact runs using keys to avoid false stops
- Use 'System Events' for accurate macOS automation permissions
- Prevent system-sleep via proper idle assertions
- Ensure download pauses use channel acknowledgements (PauseWithAck)
- Require Firelink ownership before replacing files in add/conflict UI
- Retain partial download assets when removing entries without deletion
- Clear progress state in store when downloads complete or pause to reduce churn
- Handle empty/invalid queue selections gracefully
- 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
- Add backendRegisteredIds and backendDispatchPromises for single dispatch enforcement
- Add detach_download_for_reconfigure to safely modify properties of active downloads
- Add applyProperties logic handling completed, failed, paused, and active queues
- Add extractValidDownloadUrls and fix multi-url paste handling
- Setup vitest and add useDownloadStore unit tests for backend registration lifecycle
- Strip password, cookies, and headers from download_queue before writing
to store.bin; secrets remain in-memory for the active session only.
- Move extension pairing token from PersistedSettings to the OS keychain,
rotating it on upgrade from versions that persisted it as plaintext.
- Add ignores_legacy_extension_pairing_token_field test to confirm serde
silently drops the old field so existing installs migrate cleanly.
- Document intentional retention of URLs (signed params are the download
source and cannot be redacted without breaking resume/retry).
Transient network drops and Wi-Fi timeouts no longer promote a download
straight to a hard Failed state. A shared retry engine classifies the
error, emits a transient `Retrying` state, and runs a 3-strike
exponential backoff (2s / 5s / 10s) while preserving the active download
allocation (semaphore permit / worker slot). Resumability primitives are
reused on every retry so no downloaded bytes are discarded.
Engine core (src-tauri/src/retry.rs, new):
- BACKOFF_SCHEDULE = [2s, 5s, 10s], MAX_RETRIES = 3
- backoff_for(strike): schedule index with graceful clamp beyond range
- is_transient_network_error(msg): string classifier covering reqwest,
yt-dlp, and aria2c phrasing; permanent conditions (HTTP 401/403/404/
410/451, not-found, permission denied, out-of-disk) checked first so
they always fail fast
- backoff_and_emit / backoff_and_emit_cancel: cancel-safe sleep helpers
driving a Retrying state emit before each delay
- 9 unit tests covering schedule math, clamping, transient vs permanent
classification, and permanent-wins-over-transient precedence
State model (src-tauri/src/ipc.rs):
- DownloadStatus::Retrying variant (serialized "retrying")
- DownloadStateEvent::retrying(id, reason) constructor
- Regenerated TypeScript binding: src/bindings/DownloadStatus.ts
Native reqwest backend (src-tauri/src/download.rs):
- DownloadEvent::Retrying variant (headless mirror)
- CoordinatorEventSink::emit_retrying() drives the production
download-state channel with status "retrying"
- download_file retry core rewritten: transient -> emit_retrying ->
backoff_for sleep inside a control_rx select (pause/cancel honored
mid-backoff) -> re-issue Range header; permanent errors or strike
exhaustion advance to the next URL then hard Failed
yt-dlp media backend (src-tauri/src/lib.rs):
- child spawn+stream loop wrapped in a 3-strike re-spawn loop; stderr
tail classified and, if transient, the process is re-spawned after
backoff (--continue resumes); --retry-wait=2 added to aria2c downloader
aria2c backend (src-tauri/src/queue.rs):
- retry-wait=2 option so aria2's internal retries are not rapid-fire
- handle_aria2_download_error: intercepts transient onDownloadError,
backs off, re-issues addUri, and rotates the stale gid -> id mapping
via rotate_aria2_gid (fresh addUri mints a new gid; not rotating would
detach subsequent WS events and leak the semaphore permit permanently)
- aria2_payloads / aria2_retry_strikes tracking with cleanup on terminal
outcomes
Verification: cargo build clean; cargo test --lib 37 passed / 0 failed.