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
+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) && (