From e35fc1f31aa3828ce8a6b8b2a10782970c5ff161 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Mon, 2 Mar 2026 22:17:59 +0100 Subject: [PATCH] v1.5.28: Visuelle Fortschrittsanzeigen (JDownloader 2 Style) --- .claude/memory-bank.md | 126 ++++++++++++++++++++++++ .claude/plans/agile-watching-lampson.md | 66 +++++++++++++ .claude/settings.json | 6 ++ .claude/settings.local.json | 7 ++ .claude/worktrees/nifty-vaughan | 1 + CHANGELOG.md | 12 +++ _upload_release.mjs | 75 ++++++++++++++ package.json | 2 +- src/renderer/App.tsx | 16 ++- src/renderer/styles.css | 43 ++++++++ verify_remote.mjs | 58 +++++++++++ 11 files changed, 409 insertions(+), 3 deletions(-) create mode 100644 .claude/memory-bank.md create mode 100644 .claude/plans/agile-watching-lampson.md create mode 100644 .claude/settings.json create mode 100644 .claude/settings.local.json create mode 160000 .claude/worktrees/nifty-vaughan create mode 100644 _upload_release.mjs create mode 100644 verify_remote.mjs diff --git a/.claude/memory-bank.md b/.claude/memory-bank.md new file mode 100644 index 0000000..8720488 --- /dev/null +++ b/.claude/memory-bank.md @@ -0,0 +1,126 @@ +# Memory Bank - Multi Debrid Downloader + +## Projekt-Überblick + +**Name:** Multi Debrid Downloader (MDD) +**Typ:** Electron Desktop App für Windows 10/11 +**Repository:** +- Codeberg: https://codeberg.org/Sucukdeluxe/real-debrid-downloader.git +- GitHub: https://github.com/Sucukdeluxe/real-debrid-downloader.git + +## Technologie-Stack + +- **Runtime:** Electron 31.x +- **Frontend:** React 18.x + TypeScript 5.x +- **Build:** Vite (Renderer) + tsup (Main/Preload) +- **Tests:** Vitest (262+ Tests) +- **Installer:** NSIS via electron-builder + +## Unterstützte Debrid-Provider + +| Provider | Auth | Priorität | +|----------|------|-----------| +| Real-Debrid | API Token | Primär | +| Mega-Debrid | Login + Passwort | Fallback 1 | +| BestDebrid | API Token | Fallback 2 | +| AllDebrid | API Key | Fallback 3 | + +## Kernfeatures + +- **Queue-Management:** Package-basierte Organisation mit Drag & Drop +- **Auto-Extract:** RAR, ZIP, 7z mit Passwortliste +- **Auto-Rename:** Scene-Release Muster (4sf/4sj) → saubere Namen +- **Integritätsprüfung:** CRC32, MD5, SHA1 via SFV-Dateien +- **Provider-Fallback:** Automatischer Wechsel bei Fehlern/Fair-Use +- **Session-Persistenz:** Queue überlebt App-Neustart +- **Clipboard-Watcher:** Automatische Link-Erkennung +- **System-Tray:** Minimize to Tray +- **Speed-Limit:** Global oder per Download + Bandwidth-Schedules +- **MKV-Sammelordner:** Automatisches Verschieben nach Paketabschluss +- **Update-System:** Automatische Updates via Codeberg Releases + +## Projektstruktur + +``` +src/ +├── main/ # Electron Main Process +│ ├── main.ts # Entry Point, IPC Handler, Window Management +│ ├── app-controller.ts # Koordiniert DownloadManager + Settings +│ ├── download-manager.ts # Core: Queue, Downloads, Retry-Logic +│ ├── debrid.ts # Debrid-Service Abstraktion +│ ├── realdebrid.ts # Real-Debrid API Client +│ ├── extractor.ts # Archiv-Entpackung +│ ├── integrity.ts # CRC32/Hash-Validierung +│ ├── storage.ts # Session/Settings Persistenz +│ ├── update.ts # Update-Check & Installation +│ └── ... +├── renderer/ # React UI +│ ├── App.tsx # Hauptkomponente mit allen Tabs +│ └── styles.css # Styling +├── preload/ # Preload Script (IPC Bridge) +│ └── preload.ts +└── shared/ # Geteilte Types + ├── types.ts # Alle TypeScript Interfaces + ├── ipc.ts # IPC Channel Konstanten + └── preload-api.ts # window.rd API Definition +``` + +## Wichtige Types (src/shared/types.ts) + +- `DownloadItem`: Einzelner Download mit Status, Progress, Speed +- `PackageEntry`: Gruppe von Downloads mit OutputDir, ExtractDir +- `SessionState`: Gesamter Queue-Zustand (persistiert) +- `AppSettings`: Alle Einstellungen +- `UiSnapshot`: Kompletter UI-State für Renderer + +## IPC Channels (src/shared/ipc.ts) + +Hauptchannels für Renderer ↔ Main Kommunikation: +- `GET_SNAPSHOT`, `STATE_UPDATE`: State-Sync +- `ADD_LINKS`, `ADD_CONTAINERS`: Queue befüllen +- `START`, `STOP`, `TOGGLE_PAUSE`: Download-Kontrolle +- `UPDATE_SETTINGS`: Einstellungen ändern + +## Aktuelle Version + +**Version:** 1.5.27 +**Letztes Release:** 1.4.68 (2026-03-01) + +### Letzte Änderungen (CHANGELOG) +- Session-Backup für Queue-Zustand +- Start-Konflikt-Behandlung verbessert +- Mega-Web Unrestrict abort-fähig +- DLC-Import gehärtet +- Auto-Renamer erweitert + +## Offene Pläne + +1. **Native Menüleiste** (`.claude/plans/agile-watching-lampson.md`) + - JDownloader 2 Style Menü + - Electron Menu API nutzen + - Bestehende React Menu-Bar ersetzen + +## Coding-Conventions + +- TypeScript strict mode +- Async/Await über Promises +- Deutsche UI-Texte +- Ausführliche Error-Logs via `logger` +- Retry-Logic mit exponential backoff +- AbortController für abbrechbare Operationen + +## Build & Release + +```bash +npm run build # TypeScript + Vite Build +npm run dist # electron-builder (NSIS + Portable) +npm test # Vitest Tests +npm run self-check # Vollständiger Check (Typecheck + Tests) +``` + +## Wichtige Dateien + +- `CHANGELOG.md` - Detaillierte Versionshistorie +- `.claude/plans/` - Feature-Pläne +- `tests/` - Umfangreiche Test-Suite +- `installer/RealDebridDownloader.iss` - Inno Setup Script \ No newline at end of file diff --git a/.claude/plans/agile-watching-lampson.md b/.claude/plans/agile-watching-lampson.md new file mode 100644 index 0000000..e46a889 --- /dev/null +++ b/.claude/plans/agile-watching-lampson.md @@ -0,0 +1,66 @@ +# Native Menüleiste (JDownloader 2 Style) + +## Context +Die App hat aktuell keine native Menüleiste (nur ein Tray-Kontextmenü). Der User möchte eine Menüleiste oben links wie bei JDownloader 2 mit Datei-Menü, Shortcuts und Sicherungs-Funktion. + +## Features + +### "Datei"-Menü (oben links) +| Menüpunkt | Shortcut | Aktion | +|-----------|----------|--------| +| Text mit Links analysieren | Ctrl+L | Wechselt zum Linksammler-Tab | +| Linkcontainer laden | Ctrl+O | Öffnet DLC-Dateiauswahl (existiert bereits) | +| --- Separator --- | | | +| Sicherung → Backup erstellen | | Exportiert Queue als JSON (existiert: `exportQueue`) | +| Sicherung → Backup laden | | Importiert Queue-JSON (existiert: `importQueue`) | +| --- Separator --- | | | +| Neustart | Ctrl+Shift+R | `app.relaunch()` + `app.quit()` | +| Beenden | Ctrl+Q | `app.quit()` | + +## Implementation + +### Step 1: Neue IPC-Channels +**Datei:** `src/shared/ipc.ts` +- `NAVIGATE_TAB: "app:navigate-tab"` — Renderer wechselt Tab +- `RESTART: "app:restart"` — App neustarten +- `SAVE_BACKUP: "dialog:save-backup"` — Save-Dialog + Export +- `LOAD_BACKUP: "dialog:load-backup"` — Open-Dialog + Import + +### Step 2: Preload-API erweitern +**Datei:** `src/shared/preload-api.ts` + `src/preload/preload.ts` +- `onNavigateTab(callback)` — Event-Listener für Tab-Wechsel +- `saveBackup()` — Backup über nativen Save-Dialog speichern +- `loadBackup()` — Backup über nativen Open-Dialog laden + +### Step 3: Menüleiste erstellen +**Datei:** `src/main/main.ts` + +Neue Funktion `createApplicationMenu()` nach `createTray()`: +- Nutzt `Menu.buildFromTemplate()` + `Menu.setApplicationMenu()` +- "Datei"-Menü mit allen Punkten aus der Tabelle +- Accelerators für Shortcuts (Electron handelt die automatisch) +- Menü-Clicks senden IPC-Events an den Renderer oder rufen direkt Main-Process-Funktionen auf + +**Backup erstellen:** `dialog.showSaveDialog()` → `controller.exportQueue()` → `fs.writeFile()` +**Backup laden:** `dialog.showOpenDialog()` → `fs.readFile()` → `controller.importQueue()` +**Neustart:** `app.relaunch()` → `app.quit()` +**Beenden:** `app.quit()` +**Linksammler/DLC:** IPC-Event an Renderer senden + +### Step 4: Renderer reagiert auf Menü-Events +**Datei:** `src/renderer/App.tsx` +- `onNavigateTab` Listener registrieren im `useEffect` +- Bei `"collector"` → `setTab("collector")` +- DLC-Import: `pickContainers` + `addContainers` (bestehendes Pattern) + +## Dateien +- `src/shared/ipc.ts` — Neue Channels +- `src/shared/preload-api.ts` — Neue API-Methoden +- `src/preload/preload.ts` — IPC-Bridge +- `src/main/main.ts` — Menüleiste + IPC-Handler + Backup-Logik +- `src/renderer/App.tsx` — Tab-Navigation Listener + +## Verification +1. `npm run build` +2. `npx vitest run` (schnelle Tests) +3. Manuell: App starten, Datei-Menü prüfen, Shortcuts testen diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..b82dbf1 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,6 @@ +{ + "enabledPlugins": { + "frontend-design@claude-plugins-official": true, + "code-review@claude-plugins-official": true + } +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..e56d14e --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:github.com)" + ] + } +} diff --git a/.claude/worktrees/nifty-vaughan b/.claude/worktrees/nifty-vaughan new file mode 160000 index 0000000..f1e132b --- /dev/null +++ b/.claude/worktrees/nifty-vaughan @@ -0,0 +1 @@ +Subproject commit f1e132b2ed4717667fd7318ecab22e5ef52da0cc diff --git a/CHANGELOG.md b/CHANGELOG.md index dac185f..0c7c3e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ Alle nennenswerten Aenderungen werden in dieser Datei dokumentiert. +## 1.5.28 - 2026-03-02 + +UI-Verbesserung: Visuelle Fortschrittsanzeigen in der Download-Liste (JDownloader 2 Style). + +### Features + +- **Visuelle Fortschrittsbalken:** + - Paket-Fortschritt wird jetzt als grafische Progress-Bar in der Spalte "Fortschritt" angezeigt. + - Einzelne Items haben ebenfalls eine kleinere Progress-Bar. + - Grüner Gradient (#22c55e → #4ade80) für bessere Text-Lesbarkeit. + - Prozentanzeige als Overlay-Text auf der Bar. + ## 1.4.68 - 2026-03-01 Stabilitaets-Hotfix fuer Session-Verlust nach Update/Neustart: Session-Dateien haben jetzt ein robustes Backup-/Restore-Fallback. diff --git a/_upload_release.mjs b/_upload_release.mjs new file mode 100644 index 0000000..5fd2c28 --- /dev/null +++ b/_upload_release.mjs @@ -0,0 +1,75 @@ +import fs from "node:fs"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; + +const credResult = spawnSync("git", ["credential", "fill"], { + input: "protocol=https\nhost=codeberg.org\n\n", + encoding: "utf8", + stdio: ["pipe", "pipe", "pipe"] +}); +const creds = new Map(); +for (const line of credResult.stdout.split(/\r?\n/)) { + if (line.includes("=")) { + const [k, v] = line.split("=", 2); + creds.set(k, v); + } +} +const auth = "Basic " + Buffer.from(creds.get("username") + ":" + creds.get("password")).toString("base64"); +const owner = "Sucukdeluxe"; +const repo = "real-debrid-downloader"; +const tag = "v1.5.27"; +const baseApi = `https://codeberg.org/api/v1/repos/${owner}/${repo}`; + +async function main() { + await fetch(baseApi, { + method: "PATCH", + headers: { Authorization: auth, "Content-Type": "application/json" }, + body: JSON.stringify({ has_releases: true }) + }); + + const createRes = await fetch(`${baseApi}/releases`, { + method: "POST", + headers: { Authorization: auth, "Content-Type": "application/json", Accept: "application/json" }, + body: JSON.stringify({ + tag_name: tag, + target_commitish: "main", + name: tag, + body: "- Increase column spacing for Fortschritt/Größe/Geladen", + draft: false, + prerelease: false + }) + }); + const release = await createRes.json(); + if (!createRes.ok) { + console.error("Create failed:", JSON.stringify(release)); + process.exit(1); + } + console.log("Release created:", release.id); + + const files = [ + "Real-Debrid-Downloader Setup 1.5.27.exe", + "Real-Debrid-Downloader 1.5.27.exe", + "latest.yml", + "Real-Debrid-Downloader Setup 1.5.27.exe.blockmap" + ]; + for (const f of files) { + const filePath = path.join("release", f); + const data = fs.readFileSync(filePath); + const uploadUrl = `${baseApi}/releases/${release.id}/assets?name=${encodeURIComponent(f)}`; + const res = await fetch(uploadUrl, { + method: "POST", + headers: { Authorization: auth, "Content-Type": "application/octet-stream" }, + body: data + }); + if (res.ok) { + console.log("Uploaded:", f); + } else if (res.status === 409 || res.status === 422) { + console.log("Skipped existing:", f); + } else { + console.error("Upload failed for", f, ":", res.status); + } + } + console.log(`Done! https://codeberg.org/${owner}/${repo}/releases/tag/${tag}`); +} + +main().catch(e => { console.error(e.message); process.exit(1); }); diff --git a/package.json b/package.json index fbd0b49..c354e4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.5.27", + "version": "1.5.28", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index f1d835a..67a861c 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -2718,7 +2718,12 @@ const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, isFirs

{ e.stopPropagation(); onStartEdit(pkg.id, pkg.name); }} title="Klicken zum Umbenennen">{pkg.name}

)} - {dlProgress}% + + + + {dlProgress}% + + {humanSize(items.reduce((sum, item) => sum + (item.totalBytes || item.downloadedBytes || 0), 0))} {humanSize(items.reduce((sum, item) => sum + (item.downloadedBytes || 0), 0))} { @@ -2742,7 +2747,14 @@ const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, isFirs {!collapsed && items.map((item) => (
{ e.stopPropagation(); onSelect(item.id, e.ctrlKey); }} onMouseDown={(e) => { e.stopPropagation(); onSelectMouseDown(item.id, e); }} onMouseEnter={() => onSelectMouseEnter(item.id)} onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); onContextMenu(pkg.id, item.id, e.clientX, e.clientY); }}> {item.fileName} - {item.totalBytes > 0 ? `${item.progressPercent}%` : "-"} + + {item.totalBytes > 0 ? ( + + + {item.progressPercent}% + + ) : "-"} + {(item.totalBytes || item.downloadedBytes) ? humanSize(item.totalBytes || item.downloadedBytes || 0) : "-"} {item.downloadedBytes > 0 ? humanSize(item.downloadedBytes) : "-"} {formatHoster(item)} diff --git a/src/renderer/styles.css b/src/renderer/styles.css index 8fdb363..4c43752 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -641,6 +641,49 @@ body, font-variant-numeric: tabular-nums; } +.progress-inline { + display: block; + position: relative; + width: 100%; + height: 18px; + background: var(--progress-track); + border-radius: 4px; + overflow: hidden; +} + +.progress-inline-bar { + position: absolute; + top: 0; + left: 0; + height: 100%; + background: linear-gradient(90deg, #22c55e, #4ade80); + border-radius: 4px; + transition: width 0.15s ease; +} + +.progress-inline-text { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 600; + color: var(--text); + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); +} + +.progress-inline-small { + height: 14px; +} + +.progress-inline-small .progress-inline-text { + font-size: 10px; +} + .search-input { width: min(360px, 100%); background: var(--field); diff --git a/verify_remote.mjs b/verify_remote.mjs new file mode 100644 index 0000000..2d29acc --- /dev/null +++ b/verify_remote.mjs @@ -0,0 +1,58 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { Readable } from "node:stream"; +import { pipeline } from "node:stream/promises"; + +const localPath = "release/Real-Debrid-Downloader Setup 1.4.61.exe"; +const remoteUrl = "https://codeberg.org/Sucukdeluxe/real-debrid-downloader/releases/download/v1.4.61/Real-Debrid-Downloader%20Setup%201.4.61.exe"; +const tmpPath = path.join(os.tmpdir(), "rd-verify-1.4.61.exe"); + +// Local file info +const localSize = fs.statSync(localPath).size; +const localHash = crypto.createHash("sha512"); +localHash.update(fs.readFileSync(localPath)); +const localSha = localHash.digest("hex"); +console.log("Local file size:", localSize); +console.log("Local SHA512:", localSha.substring(0, 40) + "..."); + +// Download from Codeberg +console.log("\nDownloading from Codeberg..."); +const resp = await fetch(remoteUrl, { redirect: "follow" }); +console.log("Status:", resp.status); +console.log("Content-Length:", resp.headers.get("content-length")); + +const source = Readable.fromWeb(resp.body); +const target = fs.createWriteStream(tmpPath); +await pipeline(source, target); + +const remoteSize = fs.statSync(tmpPath).size; +const remoteHash = crypto.createHash("sha512"); +remoteHash.update(fs.readFileSync(tmpPath)); +const remoteSha = remoteHash.digest("hex"); +console.log("\nRemote file size:", remoteSize); +console.log("Remote SHA512:", remoteSha.substring(0, 40) + "..."); + +console.log("\nSize match:", localSize === remoteSize); +console.log("SHA512 match:", localSha === remoteSha); + +if (localSha !== remoteSha) { + console.log("\n!!! FILE ON CODEBERG IS CORRUPTED !!!"); + console.log("The upload to Codeberg damaged the file."); + + // Find first difference + const localBuf = fs.readFileSync(localPath); + const remoteBuf = fs.readFileSync(tmpPath); + for (let i = 0; i < Math.min(localBuf.length, remoteBuf.length); i++) { + if (localBuf[i] !== remoteBuf[i]) { + console.log(`First byte difference at offset ${i}: local=0x${localBuf[i].toString(16)} remote=0x${remoteBuf[i].toString(16)}`); + break; + } + } +} else { + console.log("\n>>> File on Codeberg is identical to local file <<<"); + console.log("The problem is on the user's server (network/proxy issue)."); +} + +fs.unlinkSync(tmpPath);