mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-11 12:07:54 +00:00
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
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('@tauri-apps/api/path', () => ({
|
||||
join: vi.fn(async (...parts: string[]) =>
|
||||
parts
|
||||
.map((part, index) => index === 0 ? part.replace(/[\\/]+$/, '') : part.replace(/^[\\/]+|[\\/]+$/g, ''))
|
||||
.join('/')
|
||||
)
|
||||
}));
|
||||
|
||||
import {
|
||||
DEFAULT_CATEGORY_SUBFOLDERS,
|
||||
normalizeCategorySubfolder,
|
||||
normalizeDownloadLocationSettings,
|
||||
resolveCategoryDestination
|
||||
} from './downloadLocations';
|
||||
|
||||
describe('download locations', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('migrates legacy derived directories without creating overrides', () => {
|
||||
const settings = normalizeDownloadLocationSettings({
|
||||
defaultDownloadPath: '/Users/test/Downloads',
|
||||
downloadDirectories: {
|
||||
Movies: '/Users/test/Downloads/Movies',
|
||||
Documents: '/Users/test/Downloads/Documents'
|
||||
}
|
||||
});
|
||||
|
||||
expect(settings.baseDownloadFolder).toBe('/Users/test/Downloads');
|
||||
expect(settings.categorySubfolders).toEqual(DEFAULT_CATEGORY_SUBFOLDERS);
|
||||
expect(settings.categoryDirectoryOverrides).toEqual({});
|
||||
});
|
||||
|
||||
it('preserves legacy custom category directories as overrides', () => {
|
||||
const settings = normalizeDownloadLocationSettings({
|
||||
defaultDownloadPath: '/Users/test/Downloads',
|
||||
downloadDirectories: {
|
||||
Video: '/Volumes/Media/Movies'
|
||||
}
|
||||
});
|
||||
|
||||
expect(settings.categoryDirectoryOverrides.Movies).toBe('/Volumes/Media/Movies');
|
||||
});
|
||||
|
||||
it('resolves automatic and overridden category destinations', async () => {
|
||||
const automatic = normalizeDownloadLocationSettings({
|
||||
baseDownloadFolder: '/Users/test/Downloads',
|
||||
categorySubfolders: { Movies: 'Video Files' }
|
||||
});
|
||||
expect(await resolveCategoryDestination(automatic, 'Movies'))
|
||||
.toBe('/Users/test/Downloads/Video Files');
|
||||
|
||||
automatic.categoryDirectoryOverrides.Movies = '/Volumes/Media';
|
||||
expect(await resolveCategoryDestination(automatic, 'Movies')).toBe('/Volumes/Media');
|
||||
});
|
||||
|
||||
it('keeps category subfolders relative and permits nested folders', () => {
|
||||
expect(normalizeCategorySubfolder('../Media/./Movies', 'Movies')).toBe('Media/Movies');
|
||||
expect(normalizeCategorySubfolder('C:\\Media\\Movies', 'Movies')).toBe('Media/Movies');
|
||||
expect(normalizeCategorySubfolder('../../', 'Movies')).toBe('Movies');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import { join } from '@tauri-apps/api/path';
|
||||
import type { DownloadCategory } from '../bindings/DownloadCategory';
|
||||
|
||||
export const DOWNLOAD_CATEGORIES: DownloadCategory[] = [
|
||||
'Musics',
|
||||
'Movies',
|
||||
'Compressed',
|
||||
'Documents',
|
||||
'Pictures',
|
||||
'Applications',
|
||||
'Other'
|
||||
];
|
||||
|
||||
export const DEFAULT_CATEGORY_SUBFOLDERS: Record<DownloadCategory, string> = {
|
||||
Musics: 'Musics',
|
||||
Movies: 'Movies',
|
||||
Compressed: 'Compressed',
|
||||
Documents: 'Documents',
|
||||
Pictures: 'Pictures',
|
||||
Applications: 'Applications',
|
||||
Other: 'Other'
|
||||
};
|
||||
|
||||
export interface DownloadLocationSettings {
|
||||
baseDownloadFolder: string;
|
||||
categorySubfolders: Record<string, string>;
|
||||
categoryDirectoryOverrides: Record<string, string>;
|
||||
}
|
||||
|
||||
interface LegacyDownloadLocationSettings {
|
||||
baseDownloadFolder?: unknown;
|
||||
categorySubfolders?: unknown;
|
||||
categoryDirectoryOverrides?: unknown;
|
||||
defaultDownloadPath?: unknown;
|
||||
downloadDirectories?: unknown;
|
||||
}
|
||||
|
||||
const stringRecord = (value: unknown): Record<string, string> => {
|
||||
if (!value || typeof value !== 'object') return {};
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.filter((entry): entry is [string, string] => typeof entry[1] === 'string')
|
||||
.map(([key, path]) => [key, path.trim()])
|
||||
);
|
||||
};
|
||||
|
||||
const normalizedForComparison = (value: string): string =>
|
||||
value.replace(/\\/g, '/').replace(/\/+$/, '');
|
||||
|
||||
const legacyDerivedPath = (base: string, subfolder: string): string =>
|
||||
`${normalizedForComparison(base)}/${subfolder.replace(/^[\\/]+|[\\/]+$/g, '')}`;
|
||||
|
||||
export const normalizeCategorySubfolder = (
|
||||
value: string,
|
||||
fallback: string
|
||||
): string => {
|
||||
const parts = value
|
||||
.trim()
|
||||
.replace(/\\/g, '/')
|
||||
.split('/')
|
||||
.filter(part => part && part !== '.' && part !== '..' && !part.endsWith(':'));
|
||||
return parts.join('/') || fallback;
|
||||
};
|
||||
|
||||
export const normalizeDownloadLocationSettings = (
|
||||
value: LegacyDownloadLocationSettings
|
||||
): DownloadLocationSettings => {
|
||||
const baseDownloadFolder =
|
||||
(typeof value.baseDownloadFolder === 'string' && value.baseDownloadFolder.trim()) ||
|
||||
(typeof value.defaultDownloadPath === 'string' && value.defaultDownloadPath.trim()) ||
|
||||
'~/Downloads';
|
||||
const persistedSubfolders = stringRecord(value.categorySubfolders);
|
||||
const categorySubfolders = Object.fromEntries(
|
||||
DOWNLOAD_CATEGORIES.map(category => [
|
||||
category,
|
||||
normalizeCategorySubfolder(
|
||||
persistedSubfolders[category] || '',
|
||||
DEFAULT_CATEGORY_SUBFOLDERS[category]
|
||||
)
|
||||
])
|
||||
);
|
||||
const categoryDirectoryOverrides = stringRecord(value.categoryDirectoryOverrides);
|
||||
const legacyDirectories = stringRecord(value.downloadDirectories);
|
||||
const legacyAliases: Record<DownloadCategory, string> = {
|
||||
Musics: 'Audio',
|
||||
Movies: 'Video',
|
||||
Compressed: 'Archives',
|
||||
Documents: 'Documents',
|
||||
Pictures: 'Images',
|
||||
Applications: 'Apps',
|
||||
Other: 'Other'
|
||||
};
|
||||
|
||||
for (const category of DOWNLOAD_CATEGORIES) {
|
||||
const legacyDirectory =
|
||||
legacyDirectories[category] || legacyDirectories[legacyAliases[category]];
|
||||
if (categoryDirectoryOverrides[category] || !legacyDirectory) continue;
|
||||
const expected = legacyDerivedPath(baseDownloadFolder, categorySubfolders[category]);
|
||||
if (normalizedForComparison(legacyDirectory) !== expected) {
|
||||
categoryDirectoryOverrides[category] = legacyDirectory;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
baseDownloadFolder,
|
||||
categorySubfolders,
|
||||
categoryDirectoryOverrides
|
||||
};
|
||||
};
|
||||
|
||||
export const resolveCategoryDestination = async (
|
||||
settings: DownloadLocationSettings,
|
||||
category: DownloadCategory
|
||||
): Promise<string> => {
|
||||
const override = settings.categoryDirectoryOverrides[category]?.trim();
|
||||
if (override) return override;
|
||||
|
||||
const base = settings.baseDownloadFolder.trim() || '~/Downloads';
|
||||
const subfolder =
|
||||
normalizeCategorySubfolder(
|
||||
settings.categorySubfolders[category] || '',
|
||||
DEFAULT_CATEGORY_SUBFOLDERS[category]
|
||||
);
|
||||
return join(base, subfolder);
|
||||
};
|
||||
|
||||
export const resolveDownloadFilePath = async (
|
||||
destination: string,
|
||||
fileName: string
|
||||
): Promise<string> => join(destination, fileName);
|
||||
Reference in New Issue
Block a user