mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-27 19:17:13 +00:00
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
This commit is contained in:
@@ -291,6 +291,7 @@ pub struct PersistedSettings {
|
|||||||
pub proxy_port: u16,
|
pub proxy_port: u16,
|
||||||
pub custom_user_agent: String,
|
pub custom_user_agent: String,
|
||||||
pub ask_where_to_save_each_file: bool,
|
pub ask_where_to_save_each_file: bool,
|
||||||
|
pub remember_last_used_download_directory: bool,
|
||||||
pub prevents_sleep_while_downloading: bool,
|
pub prevents_sleep_while_downloading: bool,
|
||||||
pub media_cookie_source: MediaCookieSource,
|
pub media_cookie_source: MediaCookieSource,
|
||||||
pub site_logins: Vec<SiteLogin>,
|
pub site_logins: Vec<SiteLogin>,
|
||||||
|
|||||||
@@ -451,6 +451,7 @@ fn default_settings() -> PersistedSettings {
|
|||||||
proxy_port: 8080,
|
proxy_port: 8080,
|
||||||
custom_user_agent: String::new(),
|
custom_user_agent: String::new(),
|
||||||
ask_where_to_save_each_file: false,
|
ask_where_to_save_each_file: false,
|
||||||
|
remember_last_used_download_directory: true,
|
||||||
prevents_sleep_while_downloading: true,
|
prevents_sleep_while_downloading: true,
|
||||||
media_cookie_source: MediaCookieSource::default(),
|
media_cookie_source: MediaCookieSource::default(),
|
||||||
site_logins: Vec::new(),
|
site_logins: Vec::new(),
|
||||||
@@ -702,6 +703,25 @@ mod tests {
|
|||||||
assert!(!default_settings().auto_add_clipboard_links);
|
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]
|
#[test]
|
||||||
fn ignores_legacy_extension_pairing_token_field() {
|
fn ignores_legacy_extension_pairing_token_field() {
|
||||||
// Older standard installs persisted `extensionPairingToken` as
|
// Older standard installs persisted `extensionPairingToken` as
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ import type { SettingsTab } from "./SettingsTab";
|
|||||||
import type { SiteLogin } from "./SiteLogin";
|
import type { SiteLogin } from "./SiteLogin";
|
||||||
import type { Theme } from "./Theme";
|
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<string>, maxConcurrentDownloads: number, globalSpeedLimit: string, speedLimitPresetValues: Array<number>, logsEnabled: boolean, isSidebarVisible: boolean, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array<string>, 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<SiteLogin>, 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<string>, maxConcurrentDownloads: number, globalSpeedLimit: string, speedLimitPresetValues: Array<number>, logsEnabled: boolean, isSidebarVisible: boolean, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array<string>, 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<SiteLogin>, autoCheckUpdates: boolean, keychainAccessGranted: boolean, };
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ import { DuplicateResolutionModal, DuplicateConflict } from './DuplicateResoluti
|
|||||||
import { canonicalizeDownloadFileName, categoryForFileName } from '../utils/downloads';
|
import { canonicalizeDownloadFileName, categoryForFileName } from '../utils/downloads';
|
||||||
import { fetchMediaMetadataDeduped, fetchMediaPlaylistMetadataDeduped } from '../utils/mediaMetadata';
|
import { fetchMediaMetadataDeduped, fetchMediaPlaylistMetadataDeduped } from '../utils/mediaMetadata';
|
||||||
import {
|
import {
|
||||||
|
expandTilde,
|
||||||
resolveCategoryDestination,
|
resolveCategoryDestination,
|
||||||
resolveDownloadFilePath,
|
resolveDownloadFilePath,
|
||||||
downloadLocationEquals
|
downloadLocationEquals,
|
||||||
|
resolveInitialAddWindowLocation
|
||||||
} from '../utils/downloadLocations';
|
} from '../utils/downloadLocations';
|
||||||
import { getPlatformInfo } from '../utils/platform';
|
import { getPlatformInfo } from '../utils/platform';
|
||||||
import { isTransferLocked } from '../utils/downloadActions';
|
import { isTransferLocked } from '../utils/downloadActions';
|
||||||
@@ -130,6 +132,8 @@ export const AddDownloadsModal = () => {
|
|||||||
} = useDownloadStore();
|
} = useDownloadStore();
|
||||||
const {
|
const {
|
||||||
baseDownloadFolder,
|
baseDownloadFolder,
|
||||||
|
rememberLastUsedDownloadDirectory,
|
||||||
|
lastUsedDownloadDirectory,
|
||||||
perServerConnections,
|
perServerConnections,
|
||||||
keychainAccessReady,
|
keychainAccessReady,
|
||||||
keychainPromptDismissed,
|
keychainPromptDismissed,
|
||||||
@@ -158,6 +162,9 @@ export const AddDownloadsModal = () => {
|
|||||||
// Right Form
|
// Right Form
|
||||||
const [saveLocation, setSaveLocation] = useState(baseDownloadFolder);
|
const [saveLocation, setSaveLocation] = useState(baseDownloadFolder);
|
||||||
const [isSaveLocationManual, setIsSaveLocationManual] = useState(false);
|
const [isSaveLocationManual, setIsSaveLocationManual] = useState(false);
|
||||||
|
const locationResolutionRequestRef = useRef(0);
|
||||||
|
const folderPickerRequestRef = useRef(0);
|
||||||
|
const pendingLastUsedDownloadDirectoryRef = useRef<string | null>(null);
|
||||||
const [connections, setConnections] = useState(perServerConnections);
|
const [connections, setConnections] = useState(perServerConnections);
|
||||||
const [speedLimitEnabled, setSpeedLimitEnabled] = useState(false);
|
const [speedLimitEnabled, setSpeedLimitEnabled] = useState(false);
|
||||||
const [speedLimit, setSpeedLimit] = useState('1024');
|
const [speedLimit, setSpeedLimit] = useState('1024');
|
||||||
@@ -222,6 +229,8 @@ export const AddDownloadsModal = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAddModalOpen) {
|
if (!isAddModalOpen) {
|
||||||
modalSessionRef.current = false;
|
modalSessionRef.current = false;
|
||||||
|
++folderPickerRequestRef.current;
|
||||||
|
pendingLastUsedDownloadDirectoryRef.current = null;
|
||||||
setUrls('');
|
setUrls('');
|
||||||
setPlaylistExpansions({});
|
setPlaylistExpansions({});
|
||||||
playlistRequestsRef.current.clear();
|
playlistRequestsRef.current.clear();
|
||||||
@@ -238,8 +247,13 @@ export const AddDownloadsModal = () => {
|
|||||||
? requestContextForUrl(initialUrlLines[0])
|
? requestContextForUrl(initialUrlLines[0])
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
setSaveLocation(baseDownloadFolder);
|
const initialLocation = resolveInitialAddWindowLocation(
|
||||||
setIsSaveLocationManual(false);
|
baseDownloadFolder,
|
||||||
|
rememberLastUsedDownloadDirectory,
|
||||||
|
lastUsedDownloadDirectory
|
||||||
|
);
|
||||||
|
setSaveLocation(initialLocation.path);
|
||||||
|
setIsSaveLocationManual(initialLocation.isManual);
|
||||||
setUrls(initialUrls);
|
setUrls(initialUrls);
|
||||||
setParsedItems([]);
|
setParsedItems([]);
|
||||||
setPlaylistExpansions({});
|
setPlaylistExpansions({});
|
||||||
@@ -279,6 +293,8 @@ export const AddDownloadsModal = () => {
|
|||||||
pendingAddCookies,
|
pendingAddCookies,
|
||||||
pendingAddMediaUrls,
|
pendingAddMediaUrls,
|
||||||
baseDownloadFolder,
|
baseDownloadFolder,
|
||||||
|
rememberLastUsedDownloadDirectory,
|
||||||
|
lastUsedDownloadDirectory,
|
||||||
perServerConnections
|
perServerConnections
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -609,6 +625,13 @@ export const AddDownloadsModal = () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!rememberLastUsedDownloadDirectory) {
|
||||||
|
pendingLastUsedDownloadDirectoryRef.current = null;
|
||||||
|
}
|
||||||
|
}, [rememberLastUsedDownloadDirectory]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const requestId = ++locationResolutionRequestRef.current;
|
||||||
if (parsedItems.length === 0) {
|
if (parsedItems.length === 0) {
|
||||||
setSelectedItemIndex(null);
|
setSelectedItemIndex(null);
|
||||||
return;
|
return;
|
||||||
@@ -618,7 +641,12 @@ export const AddDownloadsModal = () => {
|
|||||||
);
|
);
|
||||||
if (isSaveLocationManual) return;
|
if (isSaveLocationManual) return;
|
||||||
if (parsedItems.length > 1) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const first = parsedItems[0];
|
const first = parsedItems[0];
|
||||||
@@ -626,22 +654,35 @@ export const AddDownloadsModal = () => {
|
|||||||
void resolveCategoryDestination(
|
void resolveCategoryDestination(
|
||||||
useSettingsStore.getState(),
|
useSettingsStore.getState(),
|
||||||
categoryForFileName(first.file)
|
categoryForFileName(first.file)
|
||||||
).then(setSaveLocation);
|
).then(location => {
|
||||||
|
if (requestId === locationResolutionRequestRef.current) {
|
||||||
|
setSaveLocation(location);
|
||||||
|
}
|
||||||
|
});
|
||||||
}, [isSaveLocationManual, parsedItems]);
|
}, [isSaveLocationManual, parsedItems]);
|
||||||
|
|
||||||
if (!isAddModalOpen) return null;
|
if (!isAddModalOpen) return null;
|
||||||
|
|
||||||
const handleBrowse = async () => {
|
const handleBrowse = async () => {
|
||||||
try {
|
try {
|
||||||
|
const requestId = ++folderPickerRequestRef.current;
|
||||||
|
const defaultPath = await expandTilde(saveLocation);
|
||||||
const selected = await open({
|
const selected = await open({
|
||||||
directory: true,
|
directory: true,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
defaultPath: saveLocation.startsWith('~') ? undefined : saveLocation
|
defaultPath
|
||||||
});
|
});
|
||||||
|
if (requestId !== folderPickerRequestRef.current) return;
|
||||||
if (selected && typeof selected === 'string') {
|
if (selected && typeof selected === 'string') {
|
||||||
|
++locationResolutionRequestRef.current;
|
||||||
const approvedPath = await useSettingsStore.getState().approveDownloadRoot(selected);
|
const approvedPath = await useSettingsStore.getState().approveDownloadRoot(selected);
|
||||||
|
if (requestId !== folderPickerRequestRef.current) return;
|
||||||
setSaveLocation(approvedPath);
|
setSaveLocation(approvedPath);
|
||||||
setIsSaveLocationManual(true);
|
setIsSaveLocationManual(true);
|
||||||
|
const settings = useSettingsStore.getState();
|
||||||
|
if (settings.rememberLastUsedDownloadDirectory) {
|
||||||
|
pendingLastUsedDownloadDirectoryRef.current = approvedPath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to select folder:", e);
|
console.error("Failed to select folder:", e);
|
||||||
@@ -663,6 +704,7 @@ export const AddDownloadsModal = () => {
|
|||||||
}
|
}
|
||||||
isSubmittingRef.current = true;
|
isSubmittingRef.current = true;
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
++folderPickerRequestRef.current;
|
||||||
let finalLocation = saveLocation;
|
let finalLocation = saveLocation;
|
||||||
let useSharedDestination = isSaveLocationManual;
|
let useSharedDestination = isSaveLocationManual;
|
||||||
const destinationOverrides: Record<number, string> = {};
|
const destinationOverrides: Record<number, string> = {};
|
||||||
@@ -679,18 +721,24 @@ export const AddDownloadsModal = () => {
|
|||||||
directory: true,
|
directory: true,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
title: `Choose a folder for ${item.file}`,
|
title: `Choose a folder for ${item.file}`,
|
||||||
defaultPath: suggestedLocation.startsWith('~') ? undefined : suggestedLocation
|
defaultPath: await expandTilde(suggestedLocation)
|
||||||
});
|
});
|
||||||
if (selected && typeof selected === 'string') {
|
if (selected && typeof selected === 'string') {
|
||||||
const approvedPath = await useSettingsStore.getState().approveDownloadRoot(selected);
|
const approvedPath = await useSettingsStore.getState().approveDownloadRoot(selected);
|
||||||
destinationOverrides[index] = approvedPath;
|
destinationOverrides[index] = approvedPath;
|
||||||
|
const currentSettings = useSettingsStore.getState();
|
||||||
|
if (currentSettings.rememberLastUsedDownloadDirectory) {
|
||||||
|
pendingLastUsedDownloadDirectoryRef.current = approvedPath;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
pendingLastUsedDownloadDirectoryRef.current = null;
|
||||||
isSubmittingRef.current = false;
|
isSubmittingRef.current = false;
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to select folder:", e);
|
console.error("Failed to select folder:", e);
|
||||||
|
pendingLastUsedDownloadDirectoryRef.current = null;
|
||||||
isSubmittingRef.current = false;
|
isSubmittingRef.current = false;
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
return;
|
return;
|
||||||
@@ -981,6 +1029,17 @@ export const AddDownloadsModal = () => {
|
|||||||
failures.push(`${item.file}: ${e instanceof Error ? e.message : String(e)}`);
|
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);
|
toggleAddModal(false);
|
||||||
if (failures.length > 0) {
|
if (failures.length > 0) {
|
||||||
addToast({
|
addToast({
|
||||||
@@ -1357,7 +1416,8 @@ export const AddDownloadsModal = () => {
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={handleBrowse}
|
onClick={handleBrowse}
|
||||||
className="add-download-button add-download-button-secondary px-3 text-xs font-medium"
|
disabled={isSubmitting}
|
||||||
|
className="add-download-button add-download-button-secondary px-3 text-xs font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{t($ => $.addDownloads.browse)}
|
{t($ => $.addDownloads.browse)}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1069,6 +1069,21 @@ runEngineChecks(false);
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mac-settings-group">
|
||||||
|
<label className="mac-settings-row cursor-default">
|
||||||
|
<div>
|
||||||
|
<span className="text-[13px] text-text-primary">{t($ => $.settings.locations.rememberLastUsedDownloadDirectory)}</span>
|
||||||
|
<p className="mt-0.5 text-[11px] text-text-muted">{t($ => $.settings.locations.rememberLastUsedDownloadDirectoryDescription)}</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.rememberLastUsedDownloadDirectory}
|
||||||
|
onChange={(e) => settings.setRememberLastUsedDownloadDirectory(e.target.checked)}
|
||||||
|
className="mac-switch"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mac-settings-group">
|
<div className="mac-settings-group">
|
||||||
<label className="mac-settings-row cursor-default">
|
<label className="mac-settings-row cursor-default">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -591,6 +591,8 @@ const common = {
|
|||||||
baseFolderCreateFailed: 'Base folder saved, but category folders could not be created: {{detail}}',
|
baseFolderCreateFailed: 'Base folder saved, but category folders could not be created: {{detail}}',
|
||||||
baseFolderUpdated: 'Base download folder updated',
|
baseFolderUpdated: 'Base download folder updated',
|
||||||
askWhereToSave: 'Ask where to save when adding downloads',
|
askWhereToSave: 'Ask where to save when adding downloads',
|
||||||
|
rememberLastUsedDownloadDirectory: 'Remember the last used download directory',
|
||||||
|
rememberLastUsedDownloadDirectoryDescription: 'Use the last folder selected in the Add window for the next download until the app restarts.',
|
||||||
automaticCategorySubfolders: 'Automatically save files to category subfolders',
|
automaticCategorySubfolders: 'Automatically save files to category subfolders',
|
||||||
automaticCategorySubfoldersDescription: 'When off, automatic downloads use the base folder.',
|
automaticCategorySubfoldersDescription: 'When off, automatic downloads use the base folder.',
|
||||||
categorySubfolders: 'Category Subfolders',
|
categorySubfolders: 'Category Subfolders',
|
||||||
|
|||||||
@@ -168,6 +168,9 @@ export interface SettingsState {
|
|||||||
categorySubfoldersEnabled: boolean;
|
categorySubfoldersEnabled: boolean;
|
||||||
categorySubfolders: Record<string, string>;
|
categorySubfolders: Record<string, string>;
|
||||||
categoryDirectoryOverrides: Record<string, string>;
|
categoryDirectoryOverrides: Record<string, string>;
|
||||||
|
rememberLastUsedDownloadDirectory: boolean;
|
||||||
|
/** Session-only path selected in the Add window; intentionally not persisted. */
|
||||||
|
lastUsedDownloadDirectory: string | null;
|
||||||
approvedDownloadRoots: string[];
|
approvedDownloadRoots: string[];
|
||||||
maxConcurrentDownloads: number;
|
maxConcurrentDownloads: number;
|
||||||
globalSpeedLimit: string;
|
globalSpeedLimit: string;
|
||||||
@@ -245,6 +248,8 @@ export interface SettingsState {
|
|||||||
setAskWhereToSaveEachFile: (ask: boolean) => void;
|
setAskWhereToSaveEachFile: (ask: boolean) => void;
|
||||||
setPreventsSleepWhileDownloading: (prevent: boolean) => void;
|
setPreventsSleepWhileDownloading: (prevent: boolean) => void;
|
||||||
setMediaCookieSource: (source: MediaCookieSource) => void;
|
setMediaCookieSource: (source: MediaCookieSource) => void;
|
||||||
|
setRememberLastUsedDownloadDirectory: (enabled: boolean) => void;
|
||||||
|
setLastUsedDownloadDirectory: (path: string) => void;
|
||||||
setCategorySubfoldersEnabled: (enabled: boolean) => void;
|
setCategorySubfoldersEnabled: (enabled: boolean) => void;
|
||||||
setCategorySubfolder: (category: string, subfolder: string) => void;
|
setCategorySubfolder: (category: string, subfolder: string) => void;
|
||||||
setCategoryDirectoryOverride: (category: string, path?: string) => void;
|
setCategoryDirectoryOverride: (category: string, path?: string) => void;
|
||||||
@@ -268,6 +273,8 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
categorySubfoldersEnabled: true,
|
categorySubfoldersEnabled: true,
|
||||||
categorySubfolders: { ...DEFAULT_CATEGORY_SUBFOLDERS },
|
categorySubfolders: { ...DEFAULT_CATEGORY_SUBFOLDERS },
|
||||||
categoryDirectoryOverrides: {},
|
categoryDirectoryOverrides: {},
|
||||||
|
rememberLastUsedDownloadDirectory: true,
|
||||||
|
lastUsedDownloadDirectory: null,
|
||||||
approvedDownloadRoots: [],
|
approvedDownloadRoots: [],
|
||||||
maxConcurrentDownloads: 3,
|
maxConcurrentDownloads: 3,
|
||||||
globalSpeedLimit: '',
|
globalSpeedLimit: '',
|
||||||
@@ -390,6 +397,18 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
set({ preventsSleepWhileDownloading });
|
set({ preventsSleepWhileDownloading });
|
||||||
},
|
},
|
||||||
setMediaCookieSource: (mediaCookieSource) => { info('Settings updated: mediaCookieSource'); set({ mediaCookieSource }); },
|
setMediaCookieSource: (mediaCookieSource) => { info('Settings updated: mediaCookieSource'); set({ mediaCookieSource }); },
|
||||||
|
setRememberLastUsedDownloadDirectory: (rememberLastUsedDownloadDirectory) => {
|
||||||
|
info('Settings updated: rememberLastUsedDownloadDirectory');
|
||||||
|
set({
|
||||||
|
rememberLastUsedDownloadDirectory,
|
||||||
|
...(rememberLastUsedDownloadDirectory ? {} : { lastUsedDownloadDirectory: null })
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setLastUsedDownloadDirectory: (path) => {
|
||||||
|
const trimmedPath = path.trim();
|
||||||
|
if (!trimmedPath) return;
|
||||||
|
set({ lastUsedDownloadDirectory: trimmedPath });
|
||||||
|
},
|
||||||
setCategorySubfoldersEnabled: (categorySubfoldersEnabled) => {
|
setCategorySubfoldersEnabled: (categorySubfoldersEnabled) => {
|
||||||
info('Settings updated: categorySubfoldersEnabled');
|
info('Settings updated: categorySubfoldersEnabled');
|
||||||
set({ categorySubfoldersEnabled });
|
set({ categorySubfoldersEnabled });
|
||||||
@@ -515,6 +534,7 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
categorySubfoldersEnabled: state.categorySubfoldersEnabled,
|
categorySubfoldersEnabled: state.categorySubfoldersEnabled,
|
||||||
categorySubfolders: state.categorySubfolders,
|
categorySubfolders: state.categorySubfolders,
|
||||||
categoryDirectoryOverrides: state.categoryDirectoryOverrides,
|
categoryDirectoryOverrides: state.categoryDirectoryOverrides,
|
||||||
|
rememberLastUsedDownloadDirectory: state.rememberLastUsedDownloadDirectory,
|
||||||
approvedDownloadRoots: state.approvedDownloadRoots,
|
approvedDownloadRoots: state.approvedDownloadRoots,
|
||||||
maxConcurrentDownloads: state.maxConcurrentDownloads,
|
maxConcurrentDownloads: state.maxConcurrentDownloads,
|
||||||
globalSpeedLimit: state.globalSpeedLimit,
|
globalSpeedLimit: state.globalSpeedLimit,
|
||||||
@@ -562,6 +582,8 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
...persisted,
|
...persisted,
|
||||||
...locations,
|
...locations,
|
||||||
extensionPairingToken: currentState.extensionPairingToken,
|
extensionPairingToken: currentState.extensionPairingToken,
|
||||||
|
// Never hydrate the remembered Add-window path from persisted data.
|
||||||
|
lastUsedDownloadDirectory: currentState.lastUsedDownloadDirectory,
|
||||||
keychainAccessReady: currentState.keychainAccessReady,
|
keychainAccessReady: currentState.keychainAccessReady,
|
||||||
theme: isAllowedSetting(THEME_VALUES, persisted.theme)
|
theme: isAllowedSetting(THEME_VALUES, persisted.theme)
|
||||||
? persisted.theme
|
? persisted.theme
|
||||||
@@ -587,6 +609,10 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
persisted.autoAddClipboardLinks,
|
persisted.autoAddClipboardLinks,
|
||||||
currentState.autoAddClipboardLinks
|
currentState.autoAddClipboardLinks
|
||||||
),
|
),
|
||||||
|
rememberLastUsedDownloadDirectory: persistedBoolean(
|
||||||
|
persisted.rememberLastUsedDownloadDirectory,
|
||||||
|
currentState.rememberLastUsedDownloadDirectory
|
||||||
|
),
|
||||||
showDockBadge: persistedBoolean(persisted.showDockBadge, currentState.showDockBadge),
|
showDockBadge: persistedBoolean(persisted.showDockBadge, currentState.showDockBadge),
|
||||||
showMenuBarIcon: persistedBoolean(persisted.showMenuBarIcon, currentState.showMenuBarIcon),
|
showMenuBarIcon: persistedBoolean(persisted.showMenuBarIcon, currentState.showMenuBarIcon),
|
||||||
askWhereToSaveEachFile: persistedBoolean(
|
askWhereToSaveEachFile: persistedBoolean(
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
formatDerivedCategoryPath,
|
formatDerivedCategoryPath,
|
||||||
normalizeCategorySubfolder,
|
normalizeCategorySubfolder,
|
||||||
normalizeDownloadLocationSettings,
|
normalizeDownloadLocationSettings,
|
||||||
|
resolveInitialAddWindowLocation,
|
||||||
resolveCategoryDestination,
|
resolveCategoryDestination,
|
||||||
subfolderFromDerivedCategoryPath
|
subfolderFromDerivedCategoryPath
|
||||||
} from './downloadLocations';
|
} from './downloadLocations';
|
||||||
@@ -24,6 +25,25 @@ describe('download locations', () => {
|
|||||||
expect(downloadLocationEquals('/Users/Test', 'Movie.MP4', '/users/test', 'movie.mp4', 'macos')).toBe(false);
|
expect(downloadLocationEquals('/Users/Test', 'Movie.MP4', '/users/test', 'movie.mp4', 'macos')).toBe(false);
|
||||||
expect(downloadLocationEquals('/home/Test', 'Movie.MP4', '/home/test', 'movie.mp4', 'linux')).toBe(false);
|
expect(downloadLocationEquals('/home/Test', 'Movie.MP4', '/home/test', 'movie.mp4', 'linux')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses a remembered Add-window directory only when the setting is enabled', () => {
|
||||||
|
expect(resolveInitialAddWindowLocation(
|
||||||
|
'D:\\Downloads',
|
||||||
|
true,
|
||||||
|
'D:\\Course_Videos'
|
||||||
|
)).toEqual({ path: 'D:\\Course_Videos', isManual: true });
|
||||||
|
|
||||||
|
expect(resolveInitialAddWindowLocation(
|
||||||
|
'D:\\Downloads',
|
||||||
|
false,
|
||||||
|
'D:\\Course_Videos'
|
||||||
|
)).toEqual({ path: 'D:\\Downloads', isManual: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to the normalized base folder when no directory was remembered', () => {
|
||||||
|
expect(resolveInitialAddWindowLocation(' ', true, null))
|
||||||
|
.toEqual({ path: '~/Downloads', isManual: false });
|
||||||
|
});
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,6 +48,26 @@ interface LegacyDownloadLocationSettings {
|
|||||||
downloadDirectories?: unknown;
|
downloadDirectories?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AddWindowLocationSuggestion {
|
||||||
|
path: string;
|
||||||
|
isManual: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resolveInitialAddWindowLocation = (
|
||||||
|
baseDownloadFolder: string,
|
||||||
|
rememberLastUsedDownloadDirectory: boolean,
|
||||||
|
lastUsedDownloadDirectory: string | null
|
||||||
|
): AddWindowLocationSuggestion => {
|
||||||
|
const rememberedPath = rememberLastUsedDownloadDirectory
|
||||||
|
? lastUsedDownloadDirectory?.trim()
|
||||||
|
: undefined;
|
||||||
|
const basePath = baseDownloadFolder.trim() || '~/Downloads';
|
||||||
|
return {
|
||||||
|
path: rememberedPath || basePath,
|
||||||
|
isManual: Boolean(rememberedPath)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const stringRecord = (value: unknown): Record<string, string> => {
|
const stringRecord = (value: unknown): Record<string, string> => {
|
||||||
if (!value || typeof value !== 'object') return {};
|
if (!value || typeof value !== 'object') return {};
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
|
|||||||
Reference in New Issue
Block a user