Add confirmed offline-package removal while preserving downloaded files

This commit is contained in:
Sucukdeluxe
2026-09-04 21:34:06 +02:00
parent d84673da1d
commit 8b061cdbc1
12 changed files with 134 additions and 3 deletions
+6
View File
@@ -1338,6 +1338,12 @@ export class AppController {
this.manager.resetPackage(packageId);
}
public removeOfflinePackages(packageIds: string[]): number {
const removed = this.manager.removeOfflinePackages(packageIds);
this.audit("WARN", "Pakete mit Offline-Links aus der Queue entfernt", { removed });
return removed;
}
public cancelPackage(packageId: string): void {
this.audit("WARN", "Paket abgebrochen", { packageId });
this.manager.cancelPackage(packageId);
+12 -3
View File
@@ -1,4 +1,5 @@
import fs from "node:fs";
import { getPackagesWithOfflineLinks } from "../shared/offline-packages";
import path from "node:path";
import os from "node:os";
import { createHash } from "node:crypto";
@@ -6384,7 +6385,13 @@ export class DownloadManager extends EventEmitter {
});
}
public cancelPackage(packageId: string): void {
public removeOfflinePackages(packageIds: string[]): number {
const candidates = getPackagesWithOfflineLinks(packageIds, this.session.packages, this.session.items);
for (const packageId of candidates) this.cancelPackage(packageId, true);
return candidates.length;
}
public cancelPackage(packageId: string, preserveFiles = false): void {
const pkg = this.session.packages[packageId];
if (!pkg) {
return;
@@ -6408,8 +6415,8 @@ export class DownloadManager extends EventEmitter {
}
const active = this.activeTasks.get(itemId);
if (active) {
active.abortReason = "cancel";
active.abortController.abort("cancel");
active.abortReason = preserveFiles ? "stop" : "cancel";
active.abortController.abort(active.abortReason);
}
}
@@ -6419,6 +6426,8 @@ export class DownloadManager extends EventEmitter {
this.persistSoon();
this.emitState(true);
if (preserveFiles) return;
this.cleanupQueue = this.cleanupQueue
.then(async () => {
const removed = await cleanupCancelledPackageArtifactsAsync(outputDir, {
+3
View File
@@ -802,6 +802,9 @@ function registerIpcHandlers(): void {
validateString(packageId, "packageId");
return controller.cancelPackage(packageId);
});
handleTrusted(IPC_CHANNELS.REMOVE_OFFLINE_PACKAGES, (_event: IpcMainInvokeEvent, packageIds: string[]) => {
return controller.removeOfflinePackages(validateStringArray(packageIds, "packageIds"));
});
handleTrusted(IPC_CHANNELS.RENAME_PACKAGE, (_event: IpcMainInvokeEvent, packageId: string, newName: string) => {
validateString(packageId, "packageId");
validateString(newName, "newName");
+1
View File
@@ -83,6 +83,7 @@ const api: ElectronApi = {
stop: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.STOP),
togglePause: (): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.TOGGLE_PAUSE),
cancelPackage: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.CANCEL_PACKAGE, packageId),
removeOfflinePackages: (packageIds: string[]): Promise<number> => ipcRenderer.invoke(IPC_CHANNELS.REMOVE_OFFLINE_PACKAGES, packageIds),
renamePackage: (packageId: string, newName: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.RENAME_PACKAGE, packageId, newName),
reorderPackages: (packageIds: string[]): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.REORDER_PACKAGES, packageIds),
removeItem: (itemId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.REMOVE_ITEM, itemId),
+32
View File
@@ -41,6 +41,7 @@ import {
} from "../shared/provider-daily-limits";
import { preservePackageOrderForDisplay, sortPackageOrderByAvailability, sortPackageOrderByName } from "./package-order";
import { createPackageOrderState } from "./package-order-state";
import { getPackagesWithOfflineLinks } from "../shared/offline-packages";
import { pruneSelection, releaseAccountSelectionFocus, resolveEscapeSelectionScope, resolveSelectAllSelectionScope, shouldClearDownloadSelection } from "./selection";
import { buildConfiguredProviderOrder, createAccountToggleQueue, enqueueAccountToggleIntent, filterAccountDialogOptions, formatAccountOperationError, getAccountDialogSelectableOptions, getAvailableAccountOptions, mergeAccountToggleSettings, pruneAccountRowSelections, resolveAccountStatusState, resolveAccountToggleIntentEnabled, resolveAccountUsername, resolveVisibleAccountKind, sortAccountServices, updateAccountRowSelection, type AccountToggleTarget } from "./account-ui";
import { buildAccountDeleteCommand, buildAccountReplaceCommand, buildAccountSecretRequest, createAccountEditState, validateAccountEdit } from "./account-edit";
@@ -4850,6 +4851,31 @@ export function App(): ReactElement {
setSelectedIds(new Set());
}, []);
const requestRemoveOfflinePackages = useCallback((): void => {
setContextMenu(null);
void performQuickAction(async () => {
const { session, settings } = snapshotRef.current;
const candidates = getPackagesWithOfflineLinks(session.packageOrder, session.packages, session.items);
if (candidates.length === 0) return;
const english = normalizeLanguage(settings.language) === "en";
const count = candidates.length.toLocaleString(english ? "en-US" : "de-DE");
const packageLabel = english ? (candidates.length === 1 ? "package" : "packages") : (candidates.length === 1 ? "Paket" : "Pakete");
const confirmed = await askConfirmPrompt({
title: english ? "Remove packages with offline links" : "Pakete mit Offline-Links entfernen",
message: english
? `Remove ${count} ${packageLabel} with at least one offline link from the entire download queue?\n\nEach package is removed in full, including its online links. This applies regardless of filters or selection. Active downloads in these packages will stop. Downloaded files are kept.`
: `${count} ${packageLabel} mit mindestens einem Offline-Link aus der gesamten Downloadliste entfernen?\n\nDie Pakete werden vollständig entfernt, einschließlich ihrer Online-Links. Das gilt unabhängig von Filtern und Auswahl. Laufende Downloads in diesen Paketen werden gestoppt. Bereits heruntergeladene Dateien bleiben erhalten.`,
confirmLabel: english ? "Remove packages" : "Pakete entfernen",
cancelLabel: english ? "Cancel" : "Abbrechen",
danger: true
});
if (!confirmed) return;
const removed = await window.rd.removeOfflinePackages(candidates);
const removedCount = removed.toLocaleString(english ? "en-US" : "de-DE");
showToast(english ? `${removedCount} ${removed === 1 ? "package" : "packages"} removed. Downloaded files were kept.` : `${removedCount} ${removed === 1 ? "Paket" : "Pakete"} entfernt. Heruntergeladene Dateien wurden behalten.`, 3200);
});
}, [askConfirmPrompt, performQuickAction, showToast]);
const requestDeleteSelection = useCallback((): void => {
if (selectedIds.size === 0) return;
if (!settingsDraft.confirmDeleteSelection) {
@@ -7136,6 +7162,12 @@ export function App(): ReactElement {
else { executeDeleteSelection(ids); }
}}>{multi ? `Ausgewählte entfernen (${selectedPackageIds.length})` : "Paket entfernen"}</button>
)}
<div className="ctx-menu-sep" />
<button
className="ctx-menu-item ctx-danger"
disabled={actionBusy || getPackagesWithOfflineLinks(snapshot.session.packageOrder, snapshot.session.packages, snapshot.session.items).length === 0}
onClick={requestRemoveOfflinePackages}
>{normalizeLanguage(snapshot.settings.language) === "en" ? "Remove packages with offline links …" : "Pakete mit Offline-Links entfernen …"}</button>
</ContextMenu>
);
})() : null}
+1
View File
@@ -30,6 +30,7 @@ export const IPC_CHANNELS = {
STOP: "queue:stop",
TOGGLE_PAUSE: "queue:toggle-pause",
CANCEL_PACKAGE: "queue:cancel-package",
REMOVE_OFFLINE_PACKAGES: "queue:remove-offline-packages",
RENAME_PACKAGE: "queue:rename-package",
REORDER_PACKAGES: "queue:reorder-packages",
REMOVE_ITEM: "queue:remove-item",
+9
View File
@@ -0,0 +1,9 @@
import type { DownloadItem, PackageEntry } from "./types";
export function getPackagesWithOfflineLinks(
packageIds: readonly string[],
packages: Record<string, PackageEntry>,
items: Record<string, DownloadItem>
): string[] {
return [...new Set(packageIds)].filter((id) => packages[id]?.itemIds.some((itemId) => items[itemId]?.onlineStatus === "offline"));
}
+1
View File
@@ -103,6 +103,7 @@ export interface ElectronApi {
stop: () => Promise<void>;
togglePause: () => Promise<boolean>;
cancelPackage: (packageId: string) => Promise<void>;
removeOfflinePackages: (packageIds: string[]) => Promise<number>;
renamePackage: (packageId: string, newName: string) => Promise<void>;
reorderPackages: (packageIds: string[]) => Promise<void>;
removeItem: (itemId: string) => Promise<void>;