feat(torrents): safely remove unselected files

This commit is contained in:
NimBold
2026-08-02 09:49:57 +03:30
parent 67023d3f0d
commit b4da68655a
19 changed files with 695 additions and 49 deletions
+1 -1
View File
@@ -2,4 +2,4 @@
import type { DownloadCategory } from "./DownloadCategory";
import type { DownloadStatus } from "./DownloadStatus";
export type DownloadItem = { id: string, url: string, fileName: string, status: DownloadStatus, fraction?: number, speed?: string, eta?: string, size?: string, downloadedBytes?: number, totalBytes?: number, totalIsEstimate?: boolean, category: DownloadCategory, dateAdded: string, resumable?: boolean, connections?: number, speedLimit?: string, username?: string, password?: string, headers?: string, checksum?: string, cookies?: string, mirrors?: string, destination?: string, isMedia?: boolean, mediaFormatSelector?: string, mediaQuality?: string, queueId?: string, queuePosition?: number, hasBeenDispatched?: boolean, lastError?: string, lastTry?: string, isTorrent?: boolean, torrentPath?: string, torrentFileIndices?: Array<number>, torrentInfoHash?: string, torrentSeedTime?: number, torrentSeedRatio?: number, torrentUploadLimit?: string, torrentMaxPeers?: number, torrentPeerSpeedLimit?: string, torrentCheckIntegrity?: boolean, torrentTrackers?: string, torrentExcludeTrackers?: string, torrentStopTimeout?: number, torrentPrioritizePiece?: string, };
export type DownloadItem = { id: string, url: string, fileName: string, status: DownloadStatus, fraction?: number, speed?: string, eta?: string, size?: string, downloadedBytes?: number, totalBytes?: number, totalIsEstimate?: boolean, category: DownloadCategory, dateAdded: string, resumable?: boolean, connections?: number, speedLimit?: string, username?: string, password?: string, headers?: string, checksum?: string, cookies?: string, mirrors?: string, destination?: string, isMedia?: boolean, mediaFormatSelector?: string, mediaQuality?: string, queueId?: string, queuePosition?: number, hasBeenDispatched?: boolean, lastError?: string, lastTry?: string, isTorrent?: boolean, torrentPath?: string, torrentFileIndices?: Array<number>, torrentInfoHash?: string, torrentSeedTime?: number, torrentSeedRatio?: number, torrentUploadLimit?: string, torrentMaxPeers?: number, torrentPeerSpeedLimit?: string, torrentCheckIntegrity?: boolean, torrentTrackers?: string, torrentExcludeTrackers?: string, torrentStopTimeout?: number, torrentPrioritizePiece?: string, torrentRemoveUnselectedFile?: boolean, };
+1 -1
View File
@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type EnqueueItem = { id: string, queue_id: string, url: string, destination: string, filename: string, connections: number | null, speed_limit: string | null, username: string | null, password: string | null, headers: string | null, checksum: string | null, cookies: string | null, mirrors: string | null, user_agent: string | null, max_tries: number | null, proxy: string | null, format_selector: string | null, cookie_source: string | null, is_media: boolean | null, is_torrent?: boolean, torrent_path?: string, torrent_file_indices?: Array<number>, torrent_info_hash?: string, torrent_seed_time?: number, torrent_seed_ratio?: number, torrent_upload_limit?: string, torrent_max_peers?: number, torrent_peer_speed_limit?: string, torrent_check_integrity?: boolean, torrent_trackers?: string, torrent_exclude_trackers?: string, torrent_stop_timeout?: number, torrent_prioritize_piece?: string, lifecycle_generation?: string, };
export type EnqueueItem = { id: string, queue_id: string, url: string, destination: string, filename: string, connections: number | null, speed_limit: string | null, username: string | null, password: string | null, headers: string | null, checksum: string | null, cookies: string | null, mirrors: string | null, user_agent: string | null, max_tries: number | null, proxy: string | null, format_selector: string | null, cookie_source: string | null, is_media: boolean | null, is_torrent?: boolean, torrent_path?: string, torrent_file_indices?: Array<number>, torrent_info_hash?: string, torrent_seed_time?: number, torrent_seed_ratio?: number, torrent_upload_limit?: string, torrent_max_peers?: number, torrent_peer_speed_limit?: string, torrent_check_integrity?: boolean, torrent_trackers?: string, torrent_exclude_trackers?: string, torrent_stop_timeout?: number, torrent_prioritize_piece?: string, torrent_remove_unselected_file?: boolean, lifecycle_generation?: string, };
+39
View File
@@ -233,6 +233,7 @@ export const AddDownloadsModal = () => {
const [torrentMaxPeers, setTorrentMaxPeers] = useState('');
const [torrentPeerSpeedLimit, setTorrentPeerSpeedLimit] = useState('');
const [torrentCheckIntegrity, setTorrentCheckIntegrity] = useState(false);
const [torrentRemoveUnselectedFile, setTorrentRemoveUnselectedFile] = useState(false);
const [torrentTrackers, setTorrentTrackers] = useState('');
const [torrentExcludeTrackers, setTorrentExcludeTrackers] = useState('');
const [torrentStopTimeout, setTorrentStopTimeout] = useState('0');
@@ -997,6 +998,21 @@ export const AddDownloadsModal = () => {
addToast({ message: t($ => $.addDownloads.torrentStopTimeoutInvalid), variant: 'error', isActionable: true });
return;
}
const removableTorrentFileCount = parsedItems.reduce((total, item) => {
if (item.selected === false || !item.isTorrent || !item.torrentFiles?.length) return total;
const selected = item.selectedTorrentFileIndices;
if (!selected || selected.length === 0 || selected.length >= item.torrentFiles.length) return total;
return total + item.torrentFiles.length - selected.length;
}, 0);
if (
torrentRemoveUnselectedFile
&& removableTorrentFileCount > 0
&& !window.confirm(t($ => $.addDownloads.torrentRemoveUnselectedFileConfirm, {
count: removableTorrentFileCount
}))
) {
return;
}
if (saveInDedicatedFolder && !sanitizeBatchFolderName(dedicatedFolderName)) {
addToast({
message: t($ => $.addDownloads.dedicatedFolderNameRequired),
@@ -1476,6 +1492,9 @@ export const AddDownloadsModal = () => {
? normalizeSpeedLimitForBackend(torrentPeerSpeedLimit) || undefined
: undefined,
torrentCheckIntegrity: item.isTorrent ? torrentCheckIntegrity : undefined,
torrentRemoveUnselectedFile: item.isTorrent && torrentRemoveUnselectedFile && hasPartialTorrentSelection(item)
? true
: undefined,
torrentTrackers: item.isTorrent ? torrentTrackers.trim() || undefined : undefined,
torrentExcludeTrackers: item.isTorrent ? torrentExcludeTrackers.trim() || undefined : undefined,
torrentStopTimeout: item.isTorrent && torrentStopTimeout.trim() ? Number(torrentStopTimeout) : undefined,
@@ -1612,6 +1631,11 @@ export const AddDownloadsModal = () => {
};
const selectedItems = parsedItems.filter(item => item.selected !== false);
const hasPartialTorrentSelection = (item: AddDownloadDraftRow): boolean => {
if (!item.isTorrent || !item.torrentFiles?.length) return false;
const selected = item.selectedTorrentFileIndices;
return Boolean(selected && selected.length > 0 && selected.length < item.torrentFiles.length);
};
const selectedItem = selectedItemIndex === null ? undefined : parsedItems[selectedItemIndex];
const selectedPlaylistSourceUrl = selectedItem?.playlistSourceUrl;
const selectedPlaylistRows = selectedPlaylistSourceUrl
@@ -2140,6 +2164,21 @@ export const AddDownloadsModal = () => {
</span>
</span>
</label>
<label className="flex items-start gap-2 text-text-primary pt-2 border-t border-border-modal/50">
<input
type="checkbox"
checked={torrentRemoveUnselectedFile}
onChange={event => setTorrentRemoveUnselectedFile(event.target.checked)}
disabled={parsedItems.every(item => !hasPartialTorrentSelection(item))}
className="accent-red-500 mt-0.5 disabled:opacity-50"
/>
<span>
<span className="block">{t($ => $.addDownloads.torrentRemoveUnselectedFile)}</span>
<span className="block text-[10px] text-text-muted">
{t($ => $.addDownloads.torrentRemoveUnselectedFileHint)}
</span>
</span>
</label>
<div className="pt-2 border-t border-border-modal/50">
<label htmlFor="torrent-trackers" className="block text-text-muted">
{t($ => $.addDownloads.torrentTrackers)}
+36
View File
@@ -87,6 +87,7 @@ export const PropertiesModal = () => {
const [liveTorrentMaxPeersValue, setLiveTorrentMaxPeersValue] = useState('');
const [liveTorrentPeerSpeedLimitValue, setLiveTorrentPeerSpeedLimitValue] = useState('');
const [torrentCheckIntegrity, setTorrentCheckIntegrity] = useState(false);
const [torrentRemoveUnselectedFile, setTorrentRemoveUnselectedFile] = useState(false);
const [torrentTrackers, setTorrentTrackers] = useState('');
const [torrentExcludeTrackers, setTorrentExcludeTrackers] = useState('');
const [torrentStopTimeout, setTorrentStopTimeout] = useState('0');
@@ -191,6 +192,7 @@ export const PropertiesModal = () => {
);
setLiveTorrentPeerSpeedLimitValue(activeItem.torrentPeerSpeedLimit || '');
setTorrentCheckIntegrity(activeItem.torrentCheckIntegrity === true);
setTorrentRemoveUnselectedFile(activeItem.torrentRemoveUnselectedFile === true);
setTorrentTrackers(activeItem.torrentTrackers || '');
setTorrentExcludeTrackers(activeItem.torrentExcludeTrackers || '');
setTorrentStopTimeout(activeItem.torrentStopTimeout === undefined ? '0' : String(activeItem.torrentStopTimeout));
@@ -360,6 +362,18 @@ export const PropertiesModal = () => {
setErrorMessage(t($ => $.properties.torrentStopTimeoutInvalid));
return;
}
if (item.isTorrent && torrentRemoveUnselectedFile && !item.torrentFileIndices?.length) {
setErrorMessage(t($ => $.properties.torrentRemoveUnselectedFileSelectionRequired));
return;
}
if (
item.isTorrent
&& torrentRemoveUnselectedFile
&& !item.torrentRemoveUnselectedFile
&& !window.confirm(t($ => $.properties.torrentRemoveUnselectedFileConfirm))
) {
return;
}
const updates: Partial<DownloadItem> = {
url,
@@ -381,6 +395,9 @@ export const PropertiesModal = () => {
torrentExcludeTrackers: torrentExcludeTrackers.trim() || undefined,
torrentStopTimeout: normalizedStopTimeout,
torrentPrioritizePiece: normalizeTorrentPrioritizePiece(torrentPrioritizePiece) || undefined,
torrentRemoveUnselectedFile: item.torrentFileIndices !== undefined
? torrentRemoveUnselectedFile
: undefined,
}
: {}),
...(connectionsDirty
@@ -972,6 +989,25 @@ export const PropertiesModal = () => {
{t($ => $.properties.torrentVerifyIntegrityHint)}
</span>
</label>
<label className="text-xs text-text-muted text-right" htmlFor="torrent-remove-unselected-file">
{t($ => $.properties.torrentRemoveUnselectedFile)}
</label>
<label className="flex items-start gap-2 text-xs text-text-primary">
<input
id="torrent-remove-unselected-file"
type="checkbox"
checked={torrentRemoveUnselectedFile}
onChange={event => setTorrentRemoveUnselectedFile(event.currentTarget.checked)}
disabled={transferLocked || !item.torrentFileIndices?.length}
className="accent-red-500 mt-0.5 disabled:opacity-50"
aria-describedby="torrent-remove-unselected-file-hint"
/>
<span id="torrent-remove-unselected-file-hint" className="text-[11px] text-text-muted">
{item.torrentFileIndices?.length
? t($ => $.properties.torrentRemoveUnselectedFileHint)
: t($ => $.properties.torrentRemoveUnselectedFileSelectionRequired)}
</span>
</label>
</>
)}
{(liveSpeedLimitAvailable || liveSpeedLimitUnavailable) && (
+8
View File
@@ -268,6 +268,10 @@ const common = {
torrentPrioritizePiece: 'Prioritize Torrent pieces',
torrentPrioritizePieceHint: 'Optional Aria2 preview policy: head, tail, or both; each may use a size such as 1M. Changes apply when the Torrent starts or retries.',
torrentPrioritizePieceInvalid: 'Torrent piece priority must use head and/or tail with optional K or M sizes between 1K and 1024M',
torrentRemoveUnselectedFile: 'Delete unselected Torrent files after completion',
torrentRemoveUnselectedFileHint: 'Only applies when a subset of files is selected. Aria2 permanently deletes the other files after the Torrent completes.',
torrentRemoveUnselectedFileConfirm: 'Delete {{count}} unselected Torrent files after completion? This cannot be undone.',
torrentRemoveUnselectedFileSelectionRequired: 'Select a subset of Torrent files before enabling unselected-file removal.',
liveTorrentPeerOptionsFailed: 'Could not update live Torrent peer controls: {{detail}}',
category: 'Category',
lastTry: 'Last try',
@@ -530,6 +534,10 @@ const common = {
torrentPrioritizePiece: 'Prioritize Torrent pieces',
torrentPrioritizePieceHint: 'Saved with this Torrent and applied on its next start or retry. Use head, tail, or both with optional K or M sizes.',
torrentPrioritizePieceInvalid: 'Torrent piece priority must use head and/or tail with optional K or M sizes between 1K and 1024M',
torrentRemoveUnselectedFile: 'Delete unselected Torrent files after completion',
torrentRemoveUnselectedFileHint: 'Only applies when a selected subset is configured. The unselected files are not Firelink-owned and are permanently removed when the Torrent completes.',
torrentRemoveUnselectedFileConfirm: 'Enable permanent deletion of unselected Torrent files after completion? This cannot be undone.',
torrentRemoveUnselectedFileSelectionRequired: 'Select a subset of Torrent files before enabling unselected-file removal.',
required: 'Required',
free: 'Free',
preview: 'Preview',
+8
View File
@@ -268,6 +268,10 @@ const fa = {
torrentPrioritizePiece: 'اولویت‌بندی قطعه‌های تورنت',
torrentPrioritizePieceHint: 'سیاست اختیاری پیش‌نمایش آریا۲: ابتدا، انتها یا هر دو؛ برای هرکدام می‌توان اندازه‌ای مثل 1M نوشت. تغییرات هنگام شروع یا تلاش مجدد اعمال می‌شوند.',
torrentPrioritizePieceInvalid: 'اولویت قطعه‌های تورنت باید شامل ابتدا یا انتها، با اندازه اختیاری بین 1K و 1024M باشد',
torrentRemoveUnselectedFile: 'حذف فایل‌های انتخاب‌نشده تورنت پس از تکمیل',
torrentRemoveUnselectedFileHint: 'فقط وقتی اعمال می‌شود که زیرمجموعه‌ای از فایل‌ها انتخاب شده باشد. آریا۲ فایل‌های دیگر را پس از تکمیل تورنت برای همیشه حذف می‌کند.',
torrentRemoveUnselectedFileConfirm: '{{count}} فایل انتخاب‌نشده تورنت پس از تکمیل حذف شوند؟ این کار قابل بازگشت نیست.',
torrentRemoveUnselectedFileSelectionRequired: 'پیش از فعال‌کردن حذف فایل‌های انتخاب‌نشده، زیرمجموعه‌ای از فایل‌های تورنت را انتخاب کنید.',
liveTorrentPeerOptionsFailed: 'کنترل زنده همتاهای تورنت به‌روزرسانی نشد: {{detail}}',
category: 'دسته',
lastTry: 'آخرین تلاش',
@@ -530,6 +534,10 @@ const fa = {
torrentPrioritizePiece: 'اولویت‌بندی قطعه‌های تورنت',
torrentPrioritizePieceHint: 'با این تورنت ذخیره و در شروع یا تلاش مجدد بعدی اعمال می‌شود. ابتدا، انتها یا هر دو را با اندازه اختیاری K یا M وارد کنید.',
torrentPrioritizePieceInvalid: 'اولویت قطعه‌های تورنت باید شامل ابتدا یا انتها، با اندازه اختیاری بین 1K و 1024M باشد',
torrentRemoveUnselectedFile: 'حذف فایل‌های انتخاب‌نشده تورنت پس از تکمیل',
torrentRemoveUnselectedFileHint: 'فقط برای زیرمجموعه انتخاب‌شده اعمال می‌شود. فایل‌های انتخاب‌نشده متعلق به Firelink نیستند و هنگام تکمیل تورنت برای همیشه حذف می‌شوند.',
torrentRemoveUnselectedFileConfirm: 'حذف دائمی فایل‌های انتخاب‌نشده تورنت پس از تکمیل فعال شود؟ این کار قابل بازگشت نیست.',
torrentRemoveUnselectedFileSelectionRequired: 'پیش از فعال‌کردن حذف فایل‌های انتخاب‌نشده، زیرمجموعه‌ای از فایل‌های تورنت را انتخاب کنید.',
required: 'الزامی',
free: 'فضای آزاد',
preview: 'پیش‌نمایش',
+8
View File
@@ -268,6 +268,10 @@ const he = {
torrentPrioritizePiece: 'תעדוף חלקי טורנט',
torrentPrioritizePieceHint: 'מדיניות תצוגה מקדימה אופציונלית של Aria2: התחלה, סוף או שניהם; לכל אחד אפשר לציין גודל כמו 1M. השינוי חל בהפעלה או בניסיון חוזר.',
torrentPrioritizePieceInvalid: 'תעדוף חלקי טורנט חייב לכלול התחלה ו/או סוף, עם גודל אופציונלי בין 1K ל-1024M',
torrentRemoveUnselectedFile: 'מחיקת קבצי Torrent שלא נבחרו לאחר השלמה',
torrentRemoveUnselectedFileHint: 'חל רק כאשר נבחרה קבוצת קבצים חלקית. Aria2 מוחק לצמיתות את שאר הקבצים לאחר השלמת ה-Torrent.',
torrentRemoveUnselectedFileConfirm: 'למחוק {{count}} קבצי Torrent שלא נבחרו לאחר השלמה? אי אפשר לבטל פעולה זו.',
torrentRemoveUnselectedFileSelectionRequired: 'בחרו קבוצת קבצים חלקית לפני הפעלת מחיקת הקבצים שלא נבחרו.',
liveTorrentPeerOptionsFailed: 'לא ניתן לעדכן את בקרות עמיתי הטורנט בזמן אמת: {{detail}}',
category: 'קטגוריה',
lastTry: 'ניסיון אחרון',
@@ -530,6 +534,10 @@ const he = {
torrentPrioritizePiece: 'תעדוף חלקי טורנט',
torrentPrioritizePieceHint: 'נשמר עם הטורנט ומוחל בהפעלה או בניסיון חוזר. יש להזין התחלה, סוף או שניהם עם גודל K או M אופציונלי.',
torrentPrioritizePieceInvalid: 'תעדוף חלקי טורנט חייב לכלול התחלה ו/או סוף, עם גודל אופציונלי בין 1K ל-1024M',
torrentRemoveUnselectedFile: 'מחיקת קבצי Torrent שלא נבחרו לאחר השלמה',
torrentRemoveUnselectedFileHint: 'חל רק כאשר מוגדרת קבוצת קבצים חלקית. הקבצים שלא נבחרו אינם בבעלות Firelink ונמחקים לצמיתות כשה-Torrent מסתיים.',
torrentRemoveUnselectedFileConfirm: 'להפעיל מחיקה לצמיתות של קבצי Torrent שלא נבחרו לאחר השלמה? אי אפשר לבטל פעולה זו.',
torrentRemoveUnselectedFileSelectionRequired: 'בחרו קבוצת קבצים חלקית לפני הפעלת מחיקת הקבצים שלא נבחרו.',
required: 'נדרש',
free: 'פנוי',
preview: 'תצוגה מקדימה',
+8
View File
@@ -268,6 +268,10 @@ const ru = {
torrentPrioritizePiece: 'Приоритет частей торрента',
torrentPrioritizePieceHint: 'Необязательная политика предпросмотра Aria2: начало, конец или оба варианта; для каждого можно указать размер, например 1M. Применяется при запуске или повторной попытке.',
torrentPrioritizePieceInvalid: 'Приоритет частей торрента должен содержать начало и/или конец с необязательным размером от 1K до 1024M',
torrentRemoveUnselectedFile: 'Удалять невыбранные файлы Torrent после завершения',
torrentRemoveUnselectedFileHint: 'Применяется только при выборе части файлов. Aria2 навсегда удалит остальные файлы после завершения Torrent.',
torrentRemoveUnselectedFileConfirm: 'Удалить {{count}} невыбранных файлов Torrent после завершения? Это действие нельзя отменить.',
torrentRemoveUnselectedFileSelectionRequired: 'Выберите часть файлов Torrent перед включением удаления невыбранных файлов.',
liveTorrentPeerOptionsFailed: 'Не удалось обновить текущие настройки пиров торрента: {{detail}}',
category: 'Категория',
lastTry: 'Последняя попытка',
@@ -530,6 +534,10 @@ const ru = {
torrentPrioritizePiece: 'Приоритет частей торрента',
torrentPrioritizePieceHint: 'Сохраняется с торрентом и применяется при следующем запуске или повторной попытке. Укажите начало, конец или оба варианта с размером K или M.',
torrentPrioritizePieceInvalid: 'Приоритет частей торрента должен содержать начало и/или конец с необязательным размером от 1K до 1024M',
torrentRemoveUnselectedFile: 'Удалять невыбранные файлы Torrent после завершения',
torrentRemoveUnselectedFileHint: 'Применяется при настроенном выборе части файлов. Невыбранные файлы не принадлежат Firelink и навсегда удаляются после завершения Torrent.',
torrentRemoveUnselectedFileConfirm: 'Включить безвозвратное удаление невыбранных файлов Torrent после завершения? Это действие нельзя отменить.',
torrentRemoveUnselectedFileSelectionRequired: 'Выберите часть файлов Torrent перед включением удаления невыбранных файлов.',
required: 'Требуется',
free: 'Свободно',
preview: 'Предпросмотр',
+8
View File
@@ -268,6 +268,10 @@ const uk = {
torrentPrioritizePiece: 'Пріоритет частин торрента',
torrentPrioritizePieceHint: 'Необов’язкова політика попереднього перегляду Aria2: початок, кінець або обидва варіанти; для кожного можна вказати розмір, наприклад 1M. Застосовується під час запуску або повторної спроби.',
torrentPrioritizePieceInvalid: 'Пріоритет частин торрента має містити початок і/або кінець із необов’язковим розміром від 1K до 1024M',
torrentRemoveUnselectedFile: 'Видаляти невибрані файли Torrent після завершення',
torrentRemoveUnselectedFileHint: 'Застосовується лише після вибору частини файлів. Aria2 назавжди видалить решту файлів після завершення Torrent.',
torrentRemoveUnselectedFileConfirm: 'Видалити {{count}} невибраних файлів Torrent після завершення? Цю дію не можна скасувати.',
torrentRemoveUnselectedFileSelectionRequired: 'Виберіть частину файлів Torrent перед увімкненням видалення невибраних файлів.',
liveTorrentPeerOptionsFailed: 'Не вдалося оновити поточні налаштування пірів торрента: {{detail}}',
category: 'Категорія',
lastTry: 'Остання спроба',
@@ -530,6 +534,10 @@ const uk = {
torrentPrioritizePiece: 'Пріоритет частин торрента',
torrentPrioritizePieceHint: 'Зберігається разом із торрентом і застосовується під час наступного запуску або повторної спроби. Укажіть початок, кінець або обидва варіанти з розміром K чи M.',
torrentPrioritizePieceInvalid: 'Пріоритет частин торрента має містити початок і/або кінець із необов’язковим розміром від 1K до 1024M',
torrentRemoveUnselectedFile: 'Видаляти невибрані файли Torrent після завершення',
torrentRemoveUnselectedFileHint: 'Застосовується для налаштованого вибору частини файлів. Невибрані файли не належать Firelink і назавжди видаляються після завершення Torrent.',
torrentRemoveUnselectedFileConfirm: 'Увімкнути незворотне видалення невибраних файлів Torrent після завершення? Цю дію не можна скасувати.',
torrentRemoveUnselectedFileSelectionRequired: 'Виберіть частину файлів Torrent перед увімкненням видалення невибраних файлів.',
required: 'Обов\'язково',
free: 'Вільно',
preview: 'Попередній перегляд',
+8
View File
@@ -268,6 +268,10 @@ const zhCN = {
torrentPrioritizePiece: '优先下载 Torrent 片段',
torrentPrioritizePieceHint: '可选的 Aria2 预览策略:开头、结尾或两者;每项可使用 1M 等大小。Torrent 启动或重试时应用。',
torrentPrioritizePieceInvalid: 'Torrent 片段优先级必须使用开头和/或结尾,并可选 1K 到 1024M 的大小',
torrentRemoveUnselectedFile: '完成后删除未选中的 Torrent 文件',
torrentRemoveUnselectedFileHint: '仅在选择了部分文件时生效。Torrent 完成后,Aria2 会永久删除其余文件。',
torrentRemoveUnselectedFileConfirm: '完成后删除 {{count}} 个未选中的 Torrent 文件?此操作无法撤销。',
torrentRemoveUnselectedFileSelectionRequired: '请先选择部分 Torrent 文件,再启用未选中文件删除功能。',
liveTorrentPeerOptionsFailed: '无法更新 Torrent 实时对等节点控制:{{detail}}',
category: '类别',
lastTry: '上次尝试',
@@ -530,6 +534,10 @@ const zhCN = {
torrentPrioritizePiece: '优先下载 Torrent 片段',
torrentPrioritizePieceHint: '随 Torrent 保存,并在下次启动或重试时应用。可使用开头、结尾或两者,并可选 K 或 M 大小。',
torrentPrioritizePieceInvalid: 'Torrent 片段优先级必须使用开头和/或结尾,并可选 1K 到 1024M 的大小',
torrentRemoveUnselectedFile: '完成后删除未选中的 Torrent 文件',
torrentRemoveUnselectedFileHint: '仅适用于配置了部分文件选择的 Torrent。未选中的文件不属于 Firelink,并会在 Torrent 完成后永久删除。',
torrentRemoveUnselectedFileConfirm: '启用完成后永久删除未选中的 Torrent 文件?此操作无法撤销。',
torrentRemoveUnselectedFileSelectionRequired: '请先选择部分 Torrent 文件,再启用未选中文件删除功能。',
required: '必需',
free: '可用空间',
preview: '预览',
+9 -3
View File
@@ -857,7 +857,8 @@ describe('useDownloadStore', () => {
torrentTrackers: 123 as unknown as string,
torrentExcludeTrackers: 123 as unknown as string,
torrentStopTimeout: 604801,
torrentPrioritizePiece: 'head=1G'
torrentPrioritizePiece: 'head=1G',
torrentRemoveUnselectedFile: 'yes' as unknown as boolean
});
expect(normalized.torrentMaxPeers).toBeUndefined();
@@ -867,6 +868,7 @@ describe('useDownloadStore', () => {
expect(normalized.torrentExcludeTrackers).toBeUndefined();
expect(normalized.torrentStopTimeout).toBeUndefined();
expect(normalized.torrentPrioritizePiece).toBeUndefined();
expect(normalized.torrentRemoveUnselectedFile).toBeUndefined();
});
it('normalizes proxy settings for download dispatch', async () => {
@@ -1521,7 +1523,9 @@ describe('useDownloadStore', () => {
torrentTrackers: 'https://tracker.example/announce',
torrentExcludeTrackers: '*',
torrentStopTimeout: 300,
torrentPrioritizePiece: 'head=1M,tail=1M'
torrentPrioritizePiece: 'head=1M,tail=1M',
torrentFileIndices: [1],
torrentRemoveUnselectedFile: true
}, { type: 'start-now' });
const item = useDownloadStore.getState().downloads[0];
@@ -1536,7 +1540,9 @@ describe('useDownloadStore', () => {
torrent_trackers: 'https://tracker.example/announce',
torrent_exclude_trackers: '*',
torrent_stop_timeout: 300,
torrent_prioritize_piece: 'head=1M,tail=1M'
torrent_prioritize_piece: 'head=1M,tail=1M',
torrent_file_indices: [1],
torrent_remove_unselected_file: true
})
})
);
+10 -2
View File
@@ -355,6 +355,7 @@ async function dispatchItemInternal(id: string, proxyOverride?: string | null):
torrent_exclude_trackers: item.torrentExcludeTrackers || undefined,
torrent_stop_timeout: item.torrentStopTimeout,
torrent_prioritize_piece: item.torrentPrioritizePiece || undefined,
torrent_remove_unselected_file: item.torrentRemoveUnselectedFile,
lifecycle_generation: lifecycleGeneration.toString(),
};
@@ -654,13 +655,18 @@ export const normalizePersistedDownloadProgress = (download: DownloadItem): Down
const normalizedPrioritizePiece = typeof rawPrioritizePiece === 'string'
? normalizeTorrentPrioritizePiece(rawPrioritizePiece) || undefined
: undefined;
const rawRemoveUnselectedFile = download.torrentRemoveUnselectedFile as unknown;
const normalizedRemoveUnselectedFile = typeof rawRemoveUnselectedFile === 'boolean'
? rawRemoveUnselectedFile
: undefined;
const normalizedOptions = rawMaxPeers !== normalizedMaxPeers ||
rawPeerSpeedLimit !== normalizedPeerSpeedLimit ||
rawCheckIntegrity !== normalizedCheckIntegrity ||
rawTrackers !== normalizedTrackers ||
rawExcludeTrackers !== normalizedExcludeTrackers ||
rawStopTimeout !== normalizedStopTimeout ||
rawPrioritizePiece !== normalizedPrioritizePiece
rawPrioritizePiece !== normalizedPrioritizePiece ||
rawRemoveUnselectedFile !== normalizedRemoveUnselectedFile
? {
...download,
torrentMaxPeers: normalizedMaxPeers,
@@ -669,7 +675,8 @@ export const normalizePersistedDownloadProgress = (download: DownloadItem): Down
torrentTrackers: normalizedTrackers,
torrentExcludeTrackers: normalizedExcludeTrackers,
torrentStopTimeout: normalizedStopTimeout,
torrentPrioritizePiece: normalizedPrioritizePiece
torrentPrioritizePiece: normalizedPrioritizePiece,
torrentRemoveUnselectedFile: normalizedRemoveUnselectedFile
}
: download;
@@ -2205,6 +2212,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
torrent_exclude_trackers: item.torrentExcludeTrackers || undefined,
torrent_stop_timeout: item.torrentStopTimeout,
torrent_prioritize_piece: item.torrentPrioritizePiece || undefined,
torrent_remove_unselected_file: item.torrentRemoveUnselectedFile,
lifecycle_generation: currentDownloadLifecycle(item.id).toString(),
});
}