Add confirmed offline-package removal while preserving downloaded files
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user