import { DragEvent, ReactElement, useEffect, useMemo, useRef, useState } from "react"; import type { AppSettings, DebridFallbackProvider, DebridProvider, DownloadItem, PackageEntry, UiSnapshot, UpdateCheckResult } from "../shared/types"; type Tab = "collector" | "downloads" | "settings"; const emptySnapshot = (): UiSnapshot => ({ settings: { token: "", megaLogin: "", megaPassword: "", bestToken: "", allDebridToken: "", rememberToken: true, providerPrimary: "realdebrid", providerSecondary: "megadebrid", providerTertiary: "bestdebrid", autoProviderFallback: true, outputDir: "", packageName: "", autoExtract: true, extractDir: "", createExtractSubfolder: true, hybridExtract: true, cleanupMode: "none", extractConflictMode: "overwrite", removeLinkFilesAfterExtract: false, removeSamplesAfterExtract: false, enableIntegrityCheck: true, autoResumeOnStart: true, autoReconnect: false, reconnectWaitSeconds: 45, completedCleanupPolicy: "never", maxParallel: 4, speedLimitEnabled: false, speedLimitKbps: 0, speedLimitMode: "global", updateRepo: "", autoUpdateCheck: true }, session: { version: 2, packageOrder: [], packages: {}, items: {}, runStartedAt: 0, totalDownloadedBytes: 0, summaryText: "", reconnectUntil: 0, reconnectReason: "", paused: false, running: false, updatedAt: Date.now() }, summary: null, speedText: "Geschwindigkeit: 0 B/s", etaText: "ETA: --", canStart: true, canStop: false, canPause: false }); const cleanupLabels: Record = { never: "Nie", immediate: "Sofort", on_start: "Beim App-Start", package_done: "Sobald Paket fertig ist" }; const providerLabels: Record = { realdebrid: "Real-Debrid", megadebrid: "Mega-Debrid", bestdebrid: "BestDebrid", alldebrid: "AllDebrid" }; const fallbackProviderOptions: Array<{ value: DebridFallbackProvider; label: string }> = [ { value: "none", label: "Kein Fallback" }, { value: "realdebrid", label: providerLabels.realdebrid }, { value: "megadebrid", label: providerLabels.megadebrid }, { value: "bestdebrid", label: providerLabels.bestdebrid }, { value: "alldebrid", label: providerLabels.alldebrid } ]; function formatSpeedMbps(speedBps: number): string { const mbps = Math.max(0, speedBps) / (1024 * 1024); return `${mbps.toFixed(2)} MB/s`; } export function App(): ReactElement { const [snapshot, setSnapshot] = useState(emptySnapshot); const [tab, setTab] = useState("collector"); const [linksRaw, setLinksRaw] = useState(""); const [statusToast, setStatusToast] = useState(""); const [settingsDraft, setSettingsDraft] = useState(emptySnapshot().settings); const latestStateRef = useRef(null); const stateFlushTimerRef = useRef | null>(null); const toastTimerRef = useRef | null>(null); const showToast = (message: string, timeoutMs = 2200): void => { setStatusToast(message); if (toastTimerRef.current) { clearTimeout(toastTimerRef.current); } toastTimerRef.current = setTimeout(() => { setStatusToast(""); toastTimerRef.current = null; }, timeoutMs); }; useEffect(() => { let unsubscribe: (() => void) | null = null; void window.rd.getSnapshot().then((state) => { setSnapshot(state); setSettingsDraft(state.settings); if (state.settings.autoUpdateCheck) { void window.rd.checkUpdates().then((result) => { void handleUpdateResult(result, "startup"); }).catch(() => undefined); } }).catch((error) => { showToast(`Snapshot konnte nicht geladen werden: ${String(error)}`, 2800); }); unsubscribe = window.rd.onStateUpdate((state) => { latestStateRef.current = state; if (stateFlushTimerRef.current) { return; } stateFlushTimerRef.current = setTimeout(() => { stateFlushTimerRef.current = null; if (latestStateRef.current) { setSnapshot(latestStateRef.current); latestStateRef.current = null; } }, 220); }); return () => { if (stateFlushTimerRef.current) { clearTimeout(stateFlushTimerRef.current); stateFlushTimerRef.current = null; } if (toastTimerRef.current) { clearTimeout(toastTimerRef.current); toastTimerRef.current = null; } if (unsubscribe) { unsubscribe(); } }; }, []); const packages = useMemo(() => snapshot.session.packageOrder .map((id: string) => snapshot.session.packages[id]) .filter(Boolean), [snapshot]); const handleUpdateResult = async (result: UpdateCheckResult, source: "manual" | "startup"): Promise => { if (result.error) { if (source === "manual") { showToast(`Update-Check fehlgeschlagen: ${result.error}`, 2800); } return; } if (!result.updateAvailable) { if (source === "manual") { showToast(`Kein Update verfügbar (v${result.currentVersion})`, 2000); } return; } const approved = window.confirm( `Update verfügbar: ${result.latestTag} (aktuell v${result.currentVersion})\n\nJetzt automatisch herunterladen und installieren?` ); if (!approved) { showToast(`Update verfügbar: ${result.latestTag}`, 2600); return; } const install = await window.rd.installUpdate(); if (install.started) { showToast("Updater gestartet - App wird geschlossen", 2600); return; } showToast(`Auto-Update fehlgeschlagen: ${install.message}`, 3200); }; const onSaveSettings = async (): Promise => { try { await window.rd.updateSettings(settingsDraft); showToast("Settings gespeichert", 1800); } catch (error) { showToast(`Settings konnten nicht gespeichert werden: ${String(error)}`, 2800); } }; const onCheckUpdates = async (): Promise => { try { const result = await window.rd.checkUpdates(); await handleUpdateResult(result, "manual"); } catch (error) { showToast(`Update-Check fehlgeschlagen: ${String(error)}`, 2800); } }; const onAddLinks = async (): Promise => { try { await window.rd.updateSettings(settingsDraft); const result = await window.rd.addLinks({ rawText: linksRaw, packageName: settingsDraft.packageName }); if (result.addedLinks > 0) { showToast(`${result.addedPackages} Paket(e), ${result.addedLinks} Link(s) hinzugefügt`); setLinksRaw(""); } else { showToast("Keine gültigen Links gefunden"); } } catch (error) { showToast(`Fehler beim Hinzufügen: ${String(error)}`, 2600); } }; const onImportDlc = async (): Promise => { try { const files = await window.rd.pickContainers(); if (files.length === 0) { return; } await window.rd.updateSettings(settingsDraft); const result = await window.rd.addContainers(files); showToast(`DLC importiert: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`); } catch (error) { showToast(`Fehler beim DLC-Import: ${String(error)}`, 2600); } }; const onDrop = async (event: DragEvent): Promise => { event.preventDefault(); const files = Array.from(event.dataTransfer.files ?? []) as File[]; const dlc = files .filter((file) => file.name.toLowerCase().endsWith(".dlc")) .map((file) => (file as unknown as { path?: string }).path) .filter((value): value is string => !!value); if (dlc.length === 0) { return; } try { await window.rd.updateSettings(settingsDraft); const result = await window.rd.addContainers(dlc); showToast(`Drag-and-Drop: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`); } catch (error) { showToast(`Fehler bei Drag-and-Drop: ${String(error)}`, 2600); } }; const setBool = (key: keyof AppSettings, value: boolean): void => { setSettingsDraft((prev: AppSettings) => ({ ...prev, [key]: value })); }; const setText = (key: keyof AppSettings, value: string): void => { setSettingsDraft((prev: AppSettings) => ({ ...prev, [key]: value })); }; const setNum = (key: keyof AppSettings, value: number): void => { setSettingsDraft((prev: AppSettings) => ({ ...prev, [key]: value })); }; const performQuickAction = async (action: () => Promise): Promise => { try { await action(); } catch (error) { showToast(`Fehler: ${String(error)}`, 2600); } }; return (

Debrid Download Manager

Multi-Provider Workflow
{snapshot.speedText}
{snapshot.etaText}
setNum("speedLimitKbps", Number(event.target.value) || 0)} /> KB/s
{tab === "collector" && (

Linksammler