Initial public release v1.7.233

This commit is contained in:
Sucukdeluxe
2026-08-01 22:29:53 +02:00
commit dc367633f5
152 changed files with 85159 additions and 0 deletions
+7325
View File
File diff suppressed because it is too large Load Diff
+94
View File
@@ -0,0 +1,94 @@
import React from "react";
interface ErrorBoundaryProps {
children: React.ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
message: string;
}
// Catches render-time errors in the component tree so a crash shows a minimal
// recovery surface instead of a silent white screen, and forwards the error to
// the main process log. Kept deliberately dead-simple and state-independent: an
// error inside the error path is how you get a second white screen or a loop.
export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, message: "" };
}
static getDerivedStateFromError(error: unknown): ErrorBoundaryState {
return { hasError: true, message: error instanceof Error ? error.message : String(error) };
}
componentDidCatch(error: unknown, info: React.ErrorInfo): void {
try {
window.rd?.reportRendererError({
kind: "react",
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
componentStack: info?.componentStack || undefined
});
} catch {
}
}
private handleReload = (): void => {
window.location.reload();
};
render(): React.ReactNode {
if (!this.state.hasError) {
return this.props.children;
}
const overlay: React.CSSProperties = {
position: "fixed",
inset: 0,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 16,
padding: 32,
background: "#070b14",
color: "#e6edf6",
fontFamily: "Segoe UI, system-ui, sans-serif",
textAlign: "center"
};
const pre: React.CSSProperties = {
maxWidth: 640,
maxHeight: 200,
overflow: "auto",
padding: 12,
background: "#0d1422",
border: "1px solid #243049",
borderRadius: 6,
color: "#ff9a8c",
fontSize: 12,
whiteSpace: "pre-wrap",
textAlign: "left"
};
const button: React.CSSProperties = {
padding: "8px 20px",
background: "#2d5cff",
color: "#fff",
border: "none",
borderRadius: 6,
cursor: "pointer",
fontSize: 14
};
return (
<div style={overlay}>
<h1 style={{ margin: 0, fontSize: 20 }}>Die Oberfläche hat einen Fehler ausgelöst</h1>
<p style={{ margin: 0, maxWidth: 560, color: "#9aa7bd" }}>
Die Anzeige wurde gestoppt, um Datenverlust zu vermeiden. Die laufenden Downloads im
Hintergrund sind nicht betroffen. Der Fehler wurde ins Log geschrieben.
</p>
<pre style={pre}>{this.state.message}</pre>
<button type="button" style={button} onClick={this.handleReload}>Oberfläche neu laden</button>
</div>
);
}
}
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Multi Debrid Downloader</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
+48
View File
@@ -0,0 +1,48 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
import { ErrorBoundary } from "./error-boundary";
import "./styles.css";
// Forward otherwise-silent renderer failures (uncaught errors, unhandled promise
// rejections) to the main process log. Without this, a renderer crash leaves no
// trace anywhere on an unattended server.
function reportRendererError(report: Parameters<typeof window.rd.reportRendererError>[0]): void {
try {
window.rd?.reportRendererError(report);
} catch {
}
}
window.addEventListener("error", (event) => {
reportRendererError({
kind: "error",
message: event.message || String(event.error || "Unbekannter Fehler"),
stack: event.error instanceof Error ? event.error.stack : undefined,
source: event.filename || undefined,
line: typeof event.lineno === "number" ? event.lineno : undefined,
column: typeof event.colno === "number" ? event.colno : undefined
});
});
window.addEventListener("unhandledrejection", (event) => {
const reason = event.reason;
reportRendererError({
kind: "unhandledrejection",
message: reason instanceof Error ? reason.message : String(reason),
stack: reason instanceof Error ? reason.stack : undefined
});
});
const rootElement = document.getElementById("root");
if (!rootElement) {
throw new Error("Root element fehlt");
}
createRoot(rootElement).render(
<React.StrictMode>
<ErrorBoundary>
<App />
</ErrorBoundary>
</React.StrictMode>
);
+61
View File
@@ -0,0 +1,61 @@
import type { DownloadItem, DownloadStatus, PackageEntry } from "../shared/types";
const ACTIVE_PACKAGE_STATUSES = new Set<DownloadStatus>(["downloading", "validating", "integrity_check", "extracting"]);
export function reorderPackageOrderByDrop(order: string[], draggedPackageId: string, targetPackageId: string): string[] {
const fromIndex = order.indexOf(draggedPackageId);
const toIndex = order.indexOf(targetPackageId);
if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) {
return order;
}
const next = [...order];
const [dragged] = next.splice(fromIndex, 1);
const insertIndex = Math.max(0, Math.min(next.length, toIndex));
next.splice(insertIndex, 0, dragged);
return next;
}
export function sortPackageOrderByName(order: string[], packages: Record<string, PackageEntry>, descending: boolean): string[] {
const sorted = [...order];
sorted.sort((a, b) => {
const nameA = (packages[a]?.name ?? "").toLowerCase();
const nameB = (packages[b]?.name ?? "").toLowerCase();
const cmp = nameA.localeCompare(nameB, undefined, { numeric: true, sensitivity: "base" });
return descending ? -cmp : cmp;
});
return sorted;
}
export function sortPackagesForDisplay(
packages: PackageEntry[],
itemsById: Record<string, DownloadItem>,
running: boolean,
autoSortPackagesByProgress: boolean
): PackageEntry[] {
if (!running || !autoSortPackagesByProgress || packages.length <= 1) {
return packages;
}
const active: PackageEntry[] = [];
const rest: PackageEntry[] = [];
// Float packages that have an active item to the top, but keep BOTH groups in
// their original (queue) order. Earlier this sorted the active group by live
// completedRatio/downloadedBytes — which change on every progress tick (every
// 150-700ms), so active packages visibly reshuffled the whole time. A package
// entering/leaving the active bucket is a real, discrete event (start/finish);
// ranking *within* the bucket by live bytes was pure jitter nobody needs.
for (const pkg of packages) {
const hasActive = pkg.itemIds.some((id) => {
const item = itemsById[id];
return item != null && ACTIVE_PACKAGE_STATUSES.has(item.status);
});
(hasActive ? active : rest).push(pkg);
}
if (active.length === 0 || active.length === packages.length) {
return packages;
}
return [...active, ...rest];
}
+27
View File
@@ -0,0 +1,27 @@
import type { SessionState } from "../shared/types";
/**
* Drop selected ids whose package OR item no longer exists in the session.
* The selection set mixes package and item ids; when entries vanish (delta
* removal, backup-driven session swap, completed-cleanup) a stale id would
* otherwise inflate the selection count and the "(N)" action labels and keep
* "multi" styling alive for ghosts.
*
* Returns the SAME set instance when nothing changed, so callers can use it
* directly as a React state updater without forcing a re-render.
*/
export function pruneSelection(
selected: ReadonlySet<string>,
session: Pick<SessionState, "packages" | "items">
): Set<string> {
if (selected.size === 0) {
return selected as Set<string>;
}
const next = new Set<string>();
for (const id of selected) {
if (session.packages[id] || session.items[id]) {
next.add(id);
}
}
return next.size === selected.size ? (selected as Set<string>) : next;
}
File diff suppressed because it is too large Load Diff
+9
View File
@@ -0,0 +1,9 @@
import type { ElectronApi } from "../shared/preload-api";
declare global {
interface Window {
rd: ElectronApi;
}
}
export {};