diff --git a/CHANGELOG.md b/CHANGELOG.md index 01eacad..05705f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to Multi-Debrid Downloader are documented in this file. ## [Unreleased] +## [2.0.55] - 2026-08-22 + ### 1Fichier imports - Resolve original 1Fichier filenames, exact sizes, and availability in batches before downloads start. @@ -23,6 +25,8 @@ All notable changes to Multi-Debrid Downloader are documented in this file. - Rebuilt the link collector as a package-oriented preview with expandable file rows, resolved metadata, availability filters, stable selection, and selected or complete transfer to Downloads. - Route pasted links, clipboard detections, text files, drag-and-drop, and DLC containers through inspection before they enter the download queue. - Show known hosters as icons with full-name tooltips and a text fallback when an icon cannot be loaded. +- Restore exported queue JSON through the structured queue importer while keeping text files in the link collector preview. +- Open the link analysis dialog directly from Ctrl+L and the File menu. ## [2.0.54] - 2026-08-21 diff --git a/package-lock.json b/package-lock.json index 7630715..8fb787a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "multi-debrid-downloader", - "version": "2.0.54", + "version": "2.0.55", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "multi-debrid-downloader", - "version": "2.0.54", + "version": "2.0.55", "license": "MIT", "dependencies": { "adm-zip": "0.6.0", diff --git a/package.json b/package.json index 749d6c5..1d44f7e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "multi-debrid-downloader", - "version": "2.0.54", + "version": "2.0.55", "description": "Desktop downloader", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 93558a2..0d44ee8 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -837,7 +837,29 @@ export function getSnapshotRenderDelay(itemCount: number, running: boolean, acti if (activeTab !== "downloads") delay = Math.max(delay, 800); return delay; } - + +export function classifyCollectorImportFileName(fileName: string): "queue" | "links" | "unsupported" { + const normalized = String(fileName || "").trim().toLowerCase(); + if (normalized.endsWith(".json")) return "queue"; + if (normalized.endsWith(".txt")) return "links"; + return "unsupported"; +} + +export async function readCollectorImportFiles( + files: ReadonlyArray<{ name: string; text: () => Promise }> +): Promise<{ queueJson: string[]; linkText: string }> { + const queueJson: string[] = []; + const linkText: string[] = []; + for (const file of files) { + const kind = classifyCollectorImportFileName(file.name); + if (kind === "unsupported") continue; + const text = await file.text(); + if (kind === "queue") queueJson.push(text); + else linkText.push(text); + } + return { queueJson, linkText: linkText.join("\n") }; +} + const KNOWN_HOSTERS: { id: string; label: string }[] = [ { id: "rapidgator", label: "Rapidgator" }, { id: "uploaded", label: "Uploaded" }, @@ -3498,9 +3520,35 @@ export function App(): ReactElement { }); }; - onImportDlcRef.current = onImportDlc; - - const onDrop = async (event: DragEvent): Promise => { + onImportDlcRef.current = onImportDlc; + + const importQueueJsonTexts = async (queueJson: string[]): Promise => { + if (queueJson.length === 0) return; + setCollectorError(""); + await performQuickAction(async () => { + await persistDraftSettings(); + const existingIds = new Set(Object.keys(snapshotRef.current.session.packages)); + let addedPackages = 0; + let addedLinks = 0; + for (const json of queueJson) { + const result = await window.rd.importQueue(json); + addedPackages += result.addedPackages; + addedLinks += result.addedLinks; + } + if (addedLinks === 0) { + setCollectorError("Keine gültigen Links in der Queue-Datei gefunden"); + showToast("Keine gültigen Links in der Queue-Datei gefunden", 3000); + return; + } + showToast(`Importiert: ${addedPackages} Paket(e), ${addedLinks} Link(s)`); + if (snapshotRef.current.settings.collapseNewPackages) await collapseNewPackages(existingIds); + }, (error) => { + setCollectorError(`Import fehlgeschlagen: ${String(error)}`); + showToast(`Import fehlgeschlagen: ${String(error)}`, 2600); + }); + }; + + const onDrop = async (event: DragEvent): Promise => { event.preventDefault(); dragDepthRef.current = 0; dragOverRef.current = false; @@ -3515,8 +3563,9 @@ export function App(): ReactElement { if (dlc.length > 0) { await enqueueCollectorInspection(() => window.rd.inspectCollectorContainers(dlc, Date.now())); } else if (importFiles.length > 0) { - const importedText = (await Promise.all(importFiles.map((file) => file.text()))).join("\n"); - await inspectCollectorRawText(importedText); + const { queueJson, linkText } = await readCollectorImportFiles(importFiles); + await importQueueJsonTexts(queueJson); + if (linkText.trim()) await inspectCollectorRawText(linkText); } else if (droppedText.trim()) { await inspectCollectorRawText(droppedText); } @@ -3566,7 +3615,9 @@ export function App(): ReactElement { return; } releasePickerBusy(); - await inspectCollectorRawText(await file.text()); + const { queueJson, linkText } = await readCollectorImportFiles([file]); + await importQueueJsonTexts(queueJson); + if (linkText.trim()) await inspectCollectorRawText(linkText); }; clearImportQueueFocusListener(); @@ -3673,6 +3724,7 @@ export function App(): ReactElement { const openCollectorInput = (): void => { setCollectorError(""); + setTab("collector"); setCollectorInput({ draft: "" }); }; @@ -4478,11 +4530,11 @@ export function App(): ReactElement { void window.rd.quit(); return; } - if (!e.shiftKey && e.key.toLowerCase() === "l") { - if (inInput) return; - e.preventDefault(); - setTab("collector"); - setOpenMenu(null); + if (!e.shiftKey && e.key.toLowerCase() === "l") { + if (inInput) return; + e.preventDefault(); + openCollectorInput(); + setOpenMenu(null); return; } if (!e.shiftKey && e.key.toLowerCase() === "p") { @@ -5613,7 +5665,7 @@ export function App(): ReactElement { Datei
- diff --git a/tests/app-shell.test.tsx b/tests/app-shell.test.tsx index 4f8e2e8..3121700 100644 --- a/tests/app-shell.test.tsx +++ b/tests/app-shell.test.tsx @@ -5,7 +5,7 @@ import { AvatarMenu, getAvatarMenuKeyboardAction } from "../src/renderer/shell/A import { AppHeader } from "../src/renderer/shell/AppHeader"; import { AppShell } from "../src/renderer/shell/AppShell"; import { buildMainNavigation } from "../src/renderer/shell/shell-model"; -import { getSnapshotRenderDelay } from "../src/renderer/App"; +import { classifyCollectorImportFileName, getSnapshotRenderDelay, readCollectorImportFiles } from "../src/renderer/App"; describe("desktop shell", () => { it("uses keyboard-focusable controls for every copy target", () => { @@ -37,6 +37,43 @@ describe("desktop shell", () => { expect(removal).toContain('title: "Ausgewählte Links löschen"'); }); + it("routes exported queue JSON back through the queue importer while text files stay in the collector", async () => { + expect(classifyCollectorImportFileName("queue.json")).toBe("queue"); + expect(classifyCollectorImportFileName("LINKS.TXT")).toBe("links"); + expect(classifyCollectorImportFileName("archive.zip")).toBe("unsupported"); + + const files = await readCollectorImportFiles([ + { name: "queue.json", text: async () => '{"version":1,"packages":[]}' }, + { name: "links.txt", text: async () => "https://example.test/file" }, + { name: "ignored.zip", text: async () => "not-read" } + ]); + expect(files).toEqual({ + queueJson: ['{"version":1,"packages":[]}'], + linkText: "https://example.test/file" + }); + + const source = readFileSync(new URL("../src/renderer/App.tsx", import.meta.url), "utf8"); + const picker = source.slice(source.indexOf("const onImportQueue ="), source.indexOf("const setBool =")); + const drop = source.slice(source.indexOf("const onDrop ="), source.indexOf("const onExportQueue =")); + + expect(picker).toContain("readCollectorImportFiles([file])"); + expect(picker).toContain("importQueueJsonTexts(queueJson)"); + expect(drop).toContain("readCollectorImportFiles(importFiles)"); + expect(drop).toContain("importQueueJsonTexts(queueJson)"); + }); + + it("opens the link analysis dialog from Ctrl+L and the File menu", () => { + const source = readFileSync(new URL("../src/renderer/App.tsx", import.meta.url), "utf8"); + const shortcutStart = source.indexOf('e.key.toLowerCase() === "l"'); + const shortcut = source.slice(shortcutStart, source.indexOf('e.key.toLowerCase() === "p"', shortcutStart)); + const menuStart = source.indexOf("Text mit Links analysieren"); + const menu = source.slice(source.lastIndexOf("", menuStart)); + + expect(shortcut).toContain("openCollectorInput()"); + expect(shortcut).not.toContain('setTab("collector")'); + expect(menu).toContain("openCollectorInput()"); + }); + it("places the delete confirmation opt-out below the right-aligned actions", () => { const source = readFileSync(new URL("../src/renderer/views/downloads/DeleteConfirmationDialog.tsx", import.meta.url), "utf8"); const css = readFileSync(new URL("../src/renderer/styles.css", import.meta.url), "utf8");