Redesign history tab with package-card style, collapsible details, right-align remove button
Some checks are pending
Build and Release / build (push) Waiting to run

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-02 23:05:23 +01:00
parent e35fc1f31a
commit cb66661d9b
3 changed files with 138 additions and 2 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.5.28", "version": "1.5.29",
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
"main": "build/main/main/main.js", "main": "build/main/main/main.js",
"author": "Sucukdeluxe", "author": "Sucukdeluxe",

View File

@ -8,6 +8,7 @@ import type {
DownloadItem, DownloadItem,
DownloadStats, DownloadStats,
DuplicatePolicy, DuplicatePolicy,
HistoryEntry,
PackageEntry, PackageEntry,
StartConflictEntry, StartConflictEntry,
UiSnapshot, UiSnapshot,
@ -15,7 +16,7 @@ import type {
UpdateInstallProgress UpdateInstallProgress
} from "../shared/types"; } from "../shared/types";
type Tab = "collector" | "downloads" | "statistics" | "settings"; type Tab = "collector" | "downloads" | "history" | "statistics" | "settings";
type SettingsSubTab = "allgemein" | "accounts" | "entpacken" | "geschwindigkeit" | "bereinigung" | "updates"; type SettingsSubTab = "allgemein" | "accounts" | "entpacken" | "geschwindigkeit" | "bereinigung" | "updates";
interface CollectorTab { interface CollectorTab {
@ -480,6 +481,25 @@ export function App(): ReactElement {
const [linkPopup, setLinkPopup] = useState<LinkPopupState | null>(null); const [linkPopup, setLinkPopup] = useState<LinkPopupState | null>(null);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set()); const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [deleteConfirm, setDeleteConfirm] = useState<{ ids: Set<string>; dontAsk: boolean } | null>(null); const [deleteConfirm, setDeleteConfirm] = useState<{ ids: Set<string>; dontAsk: boolean } | null>(null);
const [historyEntries, setHistoryEntries] = useState<HistoryEntry[]>([]);
const [historyCollapsed, setHistoryCollapsed] = useState<Record<string, boolean>>({});
// Load history when tab changes to history
useEffect(() => {
if (tab !== "history") return;
const loadHistory = async (): Promise<void> => {
try {
const entries = await window.rd.getHistory();
console.log("History loaded:", entries);
if (mountedRef.current && entries) {
setHistoryEntries(entries);
}
} catch (err) {
console.error("Failed to load history:", err);
}
};
void loadHistory();
}, [tab]);
const currentCollectorTab = collectorTabs.find((t) => t.id === activeCollectorTab) ?? collectorTabs[0]; const currentCollectorTab = collectorTabs.find((t) => t.id === activeCollectorTab) ?? collectorTabs[0];
@ -1960,6 +1980,7 @@ export function App(): ReactElement {
<button className={tab === "downloads" ? "tab active" : "tab"} onClick={() => setTab("downloads")}>Downloads</button> <button className={tab === "downloads" ? "tab active" : "tab"} onClick={() => setTab("downloads")}>Downloads</button>
<button className={tab === "collector" ? "tab active" : "tab"} onClick={() => setTab("collector")}>Linksammler</button> <button className={tab === "collector" ? "tab active" : "tab"} onClick={() => setTab("collector")}>Linksammler</button>
<button className={tab === "settings" ? "tab active" : "tab"} onClick={() => setTab("settings")}>Einstellungen</button> <button className={tab === "settings" ? "tab active" : "tab"} onClick={() => setTab("settings")}>Einstellungen</button>
<button className={tab === "history" ? "tab active" : "tab"} onClick={() => setTab("history")}>Verlauf</button>
<button className={tab === "statistics" ? "tab active" : "tab"} onClick={() => setTab("statistics")}>Statistiken</button> <button className={tab === "statistics" ? "tab active" : "tab"} onClick={() => setTab("statistics")}>Statistiken</button>
<div className="tab-actions"> <div className="tab-actions">
{tab === "downloads" && ( {tab === "downloads" && (
@ -2138,6 +2159,67 @@ export function App(): ReactElement {
</section> </section>
)} )}
{tab === "history" && (
<section className="history-view">
<div className="history-toolbar">
<span className="history-count">{historyEntries.length} Paket{historyEntries.length !== 1 ? "e" : ""} im Verlauf</span>
{historyEntries.length > 0 && (
<button className="btn btn-danger" onClick={() => { void window.rd.clearHistory().then(() => setHistoryEntries([])); }}>Verlauf leeren</button>
)}
</div>
{historyEntries.length === 0 && <div className="empty">Noch keine abgeschlossenen Pakete im Verlauf.</div>}
{historyEntries.map((entry) => {
const collapsed = historyCollapsed[entry.id] ?? true;
return (
<article key={entry.id} className="package-card history-card">
<header onClick={() => setHistoryCollapsed((prev) => ({ ...prev, [entry.id]: !collapsed }))} style={{ cursor: "pointer" }}>
<div className="pkg-columns">
<div className="pkg-col pkg-col-name">
<button className="pkg-toggle" title={collapsed ? "Ausklappen" : "Einklappen"}>{collapsed ? "+" : "\u2212"}</button>
<h4>{entry.name}</h4>
</div>
<span className="pkg-col pkg-col-progress">{entry.status === "completed" ? "100%" : "-"}</span>
<span className="pkg-col pkg-col-size">{humanSize(entry.totalBytes)}</span>
<span className="pkg-col pkg-col-downloaded">{humanSize(entry.downloadedBytes)}</span>
<span className="pkg-col pkg-col-hoster">{entry.provider ? providerLabels[entry.provider] : "-"}</span>
<span className="pkg-col pkg-col-status">{entry.status === "completed" ? "Abgeschlossen" : "Gelöscht"}</span>
<span className="pkg-col pkg-col-speed">-</span>
</div>
</header>
<div className="progress"><div className="progress-dl" style={{ width: entry.status === "completed" ? "100%" : "0%" }} /></div>
{!collapsed && (
<div className="history-details">
<div className="history-detail-grid">
<span className="history-label">Abgeschlossen am</span>
<span>{new Date(entry.completedAt).toLocaleString("de-DE")}</span>
<span className="history-label">Dateien</span>
<span>{entry.fileCount} Datei{entry.fileCount !== 1 ? "en" : ""}</span>
<span className="history-label">Gesamtgröße</span>
<span>{humanSize(entry.totalBytes)}</span>
<span className="history-label">Heruntergeladen</span>
<span>{humanSize(entry.downloadedBytes)}</span>
<span className="history-label">Dauer</span>
<span>{entry.durationSeconds >= 3600 ? `${Math.floor(entry.durationSeconds / 3600)}h ${Math.floor((entry.durationSeconds % 3600) / 60)}min` : entry.durationSeconds >= 60 ? `${Math.floor(entry.durationSeconds / 60)}min ${entry.durationSeconds % 60}s` : `${entry.durationSeconds}s`}</span>
<span className="history-label">Durchschnitt</span>
<span>{entry.durationSeconds > 0 ? formatSpeedMbps(Math.round(entry.downloadedBytes / entry.durationSeconds)) : "-"}</span>
<span className="history-label">Provider</span>
<span>{entry.provider ? providerLabels[entry.provider] : "-"}</span>
<span className="history-label">Zielordner</span>
<span className="history-path" title={entry.outputDir}>{entry.outputDir || "-"}</span>
<span className="history-label">Status</span>
<span>{entry.status === "completed" ? "Abgeschlossen" : "Gelöscht"}</span>
</div>
<div className="history-actions">
<button className="btn" onClick={() => { void window.rd.removeHistoryEntry(entry.id).then(() => setHistoryEntries((prev) => prev.filter((e) => e.id !== entry.id))); }}>Eintrag entfernen</button>
</div>
</div>
)}
</article>
);
})}
</section>
)}
{tab === "statistics" && ( {tab === "statistics" && (
<section className="statistics-view"> <section className="statistics-view">
<article className="card stats-overview"> <article className="card stats-overview">

View File

@ -1102,6 +1102,60 @@ body,
background: linear-gradient(90deg, #22c55e, #4ade80); background: linear-gradient(90deg, #22c55e, #4ade80);
} }
/* History Tab */
.history-view {
display: flex;
flex-direction: column;
gap: 6px;
}
.history-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 12px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 8px;
font-size: 12px;
font-weight: 700;
color: var(--muted);
}
.history-card {
cursor: default;
}
.history-details {
padding: 10px 12px;
border-top: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
}
.history-detail-grid {
display: grid;
grid-template-columns: 140px 1fr;
gap: 4px 16px;
font-size: 13px;
}
.history-label {
color: var(--muted);
font-weight: 600;
}
.history-path {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.history-actions {
margin-top: 10px;
display: flex;
justify-content: flex-end;
gap: 8px;
}
table { table {
width: 100%; width: 100%;
table-layout: fixed; table-layout: fixed;