From 160e6af94545cccbb216695bcc85ea526a43b36c Mon Sep 17 00:00:00 2001 From: NimBold Date: Sat, 18 Jul 2026 11:52:24 +0330 Subject: [PATCH] feat(locations): remember last Add-window directory Remember manually selected Add-window folders during the current session, with a persisted Locations toggle and cross-platform path handling.\n\nRefs #23 --- src-tauri/src/ipc.rs | 1 + src-tauri/src/settings.rs | 20 ++++++++ src/bindings/PersistedSettings.ts | 2 +- src/components/AddDownloadsModal.tsx | 76 +++++++++++++++++++++++++--- src/components/SettingsView.tsx | 15 ++++++ src/i18n/resources.ts | 2 + src/store/useSettingsStore.ts | 26 ++++++++++ src/utils/downloadLocations.test.ts | 20 ++++++++ src/utils/downloadLocations.ts | 20 ++++++++ 9 files changed, 173 insertions(+), 9 deletions(-) diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs index b909101..992b3cb 100644 --- a/src-tauri/src/ipc.rs +++ b/src-tauri/src/ipc.rs @@ -291,6 +291,7 @@ pub struct PersistedSettings { pub proxy_port: u16, pub custom_user_agent: String, pub ask_where_to_save_each_file: bool, + pub remember_last_used_download_directory: bool, pub prevents_sleep_while_downloading: bool, pub media_cookie_source: MediaCookieSource, pub site_logins: Vec, diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 43166fa..cca401d 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -451,6 +451,7 @@ fn default_settings() -> PersistedSettings { proxy_port: 8080, custom_user_agent: String::new(), ask_where_to_save_each_file: false, + remember_last_used_download_directory: true, prevents_sleep_while_downloading: true, media_cookie_source: MediaCookieSource::default(), site_logins: Vec::new(), @@ -702,6 +703,25 @@ mod tests { assert!(!default_settings().auto_add_clipboard_links); } + #[test] + fn remembers_last_used_download_directory_by_default() { + assert!(default_settings().remember_last_used_download_directory); + } + + #[test] + fn decodes_disabled_last_used_download_directory_setting() { + let stored = json!({ + "state": { + "rememberLastUsedDownloadDirectory": false + }, + "version": 3 + }); + + let settings = decode_stored_settings(&Value::String(stored.to_string())).unwrap(); + + assert!(!settings.remember_last_used_download_directory); + } + #[test] fn ignores_legacy_extension_pairing_token_field() { // Older standard installs persisted `extensionPairingToken` as diff --git a/src/bindings/PersistedSettings.ts b/src/bindings/PersistedSettings.ts index 62dfb02..5c5e8c7 100644 --- a/src/bindings/PersistedSettings.ts +++ b/src/bindings/PersistedSettings.ts @@ -8,4 +8,4 @@ import type { SettingsTab } from "./SettingsTab"; import type { SiteLogin } from "./SiteLogin"; import type { Theme } from "./Theme"; -export type PersistedSettings = { theme: Theme, baseDownloadFolder: string, categorySubfoldersEnabled: boolean, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array, maxConcurrentDownloads: number, globalSpeedLimit: string, speedLimitPresetValues: Array, logsEnabled: boolean, isSidebarVisible: boolean, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, customUserAgent: string, askWhereToSaveEachFile: boolean, preventsSleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array, autoCheckUpdates: boolean, keychainAccessGranted: boolean, }; +export type PersistedSettings = { theme: Theme, baseDownloadFolder: string, categorySubfoldersEnabled: boolean, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array, maxConcurrentDownloads: number, globalSpeedLimit: string, speedLimitPresetValues: Array, logsEnabled: boolean, isSidebarVisible: boolean, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, customUserAgent: string, askWhereToSaveEachFile: boolean, rememberLastUsedDownloadDirectory: boolean, preventsSleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array, autoCheckUpdates: boolean, keychainAccessGranted: boolean, }; diff --git a/src/components/AddDownloadsModal.tsx b/src/components/AddDownloadsModal.tsx index de91db3..882f8be 100644 --- a/src/components/AddDownloadsModal.tsx +++ b/src/components/AddDownloadsModal.tsx @@ -15,9 +15,11 @@ import { DuplicateResolutionModal, DuplicateConflict } from './DuplicateResoluti import { canonicalizeDownloadFileName, categoryForFileName } from '../utils/downloads'; import { fetchMediaMetadataDeduped, fetchMediaPlaylistMetadataDeduped } from '../utils/mediaMetadata'; import { + expandTilde, resolveCategoryDestination, resolveDownloadFilePath, - downloadLocationEquals + downloadLocationEquals, + resolveInitialAddWindowLocation } from '../utils/downloadLocations'; import { getPlatformInfo } from '../utils/platform'; import { isTransferLocked } from '../utils/downloadActions'; @@ -130,6 +132,8 @@ export const AddDownloadsModal = () => { } = useDownloadStore(); const { baseDownloadFolder, + rememberLastUsedDownloadDirectory, + lastUsedDownloadDirectory, perServerConnections, keychainAccessReady, keychainPromptDismissed, @@ -158,6 +162,9 @@ export const AddDownloadsModal = () => { // Right Form const [saveLocation, setSaveLocation] = useState(baseDownloadFolder); const [isSaveLocationManual, setIsSaveLocationManual] = useState(false); + const locationResolutionRequestRef = useRef(0); + const folderPickerRequestRef = useRef(0); + const pendingLastUsedDownloadDirectoryRef = useRef(null); const [connections, setConnections] = useState(perServerConnections); const [speedLimitEnabled, setSpeedLimitEnabled] = useState(false); const [speedLimit, setSpeedLimit] = useState('1024'); @@ -222,6 +229,8 @@ export const AddDownloadsModal = () => { useEffect(() => { if (!isAddModalOpen) { modalSessionRef.current = false; + ++folderPickerRequestRef.current; + pendingLastUsedDownloadDirectoryRef.current = null; setUrls(''); setPlaylistExpansions({}); playlistRequestsRef.current.clear(); @@ -238,8 +247,13 @@ export const AddDownloadsModal = () => { ? requestContextForUrl(initialUrlLines[0]) : undefined; - setSaveLocation(baseDownloadFolder); - setIsSaveLocationManual(false); + const initialLocation = resolveInitialAddWindowLocation( + baseDownloadFolder, + rememberLastUsedDownloadDirectory, + lastUsedDownloadDirectory + ); + setSaveLocation(initialLocation.path); + setIsSaveLocationManual(initialLocation.isManual); setUrls(initialUrls); setParsedItems([]); setPlaylistExpansions({}); @@ -279,6 +293,8 @@ export const AddDownloadsModal = () => { pendingAddCookies, pendingAddMediaUrls, baseDownloadFolder, + rememberLastUsedDownloadDirectory, + lastUsedDownloadDirectory, perServerConnections ]); @@ -609,6 +625,13 @@ export const AddDownloadsModal = () => { ]); useEffect(() => { + if (!rememberLastUsedDownloadDirectory) { + pendingLastUsedDownloadDirectoryRef.current = null; + } + }, [rememberLastUsedDownloadDirectory]); + + useEffect(() => { + const requestId = ++locationResolutionRequestRef.current; if (parsedItems.length === 0) { setSelectedItemIndex(null); return; @@ -618,7 +641,12 @@ export const AddDownloadsModal = () => { ); if (isSaveLocationManual) return; if (parsedItems.length > 1) { - setSaveLocation(useSettingsStore.getState().baseDownloadFolder || '~/Downloads'); + const baseFolder = useSettingsStore.getState().baseDownloadFolder || '~/Downloads'; + void expandTilde(baseFolder).then(location => { + if (requestId === locationResolutionRequestRef.current) { + setSaveLocation(location); + } + }); return; } const first = parsedItems[0]; @@ -626,22 +654,35 @@ export const AddDownloadsModal = () => { void resolveCategoryDestination( useSettingsStore.getState(), categoryForFileName(first.file) - ).then(setSaveLocation); + ).then(location => { + if (requestId === locationResolutionRequestRef.current) { + setSaveLocation(location); + } + }); }, [isSaveLocationManual, parsedItems]); if (!isAddModalOpen) return null; const handleBrowse = async () => { try { + const requestId = ++folderPickerRequestRef.current; + const defaultPath = await expandTilde(saveLocation); const selected = await open({ directory: true, multiple: false, - defaultPath: saveLocation.startsWith('~') ? undefined : saveLocation + defaultPath }); + if (requestId !== folderPickerRequestRef.current) return; if (selected && typeof selected === 'string') { + ++locationResolutionRequestRef.current; const approvedPath = await useSettingsStore.getState().approveDownloadRoot(selected); + if (requestId !== folderPickerRequestRef.current) return; setSaveLocation(approvedPath); setIsSaveLocationManual(true); + const settings = useSettingsStore.getState(); + if (settings.rememberLastUsedDownloadDirectory) { + pendingLastUsedDownloadDirectoryRef.current = approvedPath; + } } } catch (e) { console.error("Failed to select folder:", e); @@ -663,6 +704,7 @@ export const AddDownloadsModal = () => { } isSubmittingRef.current = true; setIsSubmitting(true); + ++folderPickerRequestRef.current; let finalLocation = saveLocation; let useSharedDestination = isSaveLocationManual; const destinationOverrides: Record = {}; @@ -679,18 +721,24 @@ export const AddDownloadsModal = () => { directory: true, multiple: false, title: `Choose a folder for ${item.file}`, - defaultPath: suggestedLocation.startsWith('~') ? undefined : suggestedLocation + defaultPath: await expandTilde(suggestedLocation) }); if (selected && typeof selected === 'string') { const approvedPath = await useSettingsStore.getState().approveDownloadRoot(selected); destinationOverrides[index] = approvedPath; + const currentSettings = useSettingsStore.getState(); + if (currentSettings.rememberLastUsedDownloadDirectory) { + pendingLastUsedDownloadDirectoryRef.current = approvedPath; + } } else { + pendingLastUsedDownloadDirectoryRef.current = null; isSubmittingRef.current = false; setIsSubmitting(false); return; } } catch (e) { console.error("Failed to select folder:", e); + pendingLastUsedDownloadDirectoryRef.current = null; isSubmittingRef.current = false; setIsSubmitting(false); return; @@ -981,6 +1029,17 @@ export const AddDownloadsModal = () => { failures.push(`${item.file}: ${e instanceof Error ? e.message : String(e)}`); } } + const currentSettings = useSettingsStore.getState(); + if ( + addedCount > 0 + && currentSettings.rememberLastUsedDownloadDirectory + && pendingLastUsedDownloadDirectoryRef.current + ) { + currentSettings.setLastUsedDownloadDirectory( + pendingLastUsedDownloadDirectoryRef.current + ); + } + pendingLastUsedDownloadDirectoryRef.current = null; toggleAddModal(false); if (failures.length > 0) { addToast({ @@ -1357,7 +1416,8 @@ export const AddDownloadsModal = () => { /> diff --git a/src/components/SettingsView.tsx b/src/components/SettingsView.tsx index 9398a0f..802bcd8 100644 --- a/src/components/SettingsView.tsx +++ b/src/components/SettingsView.tsx @@ -1069,6 +1069,21 @@ runEngineChecks(false); +
+ +
+