feat: dateien als reiter in downloads (breadcrumbs, redirect)
This commit is contained in:
1 parent
4d9912ff96
commit
36f36a131b
4 files changed
+136
-77
No files matched your search
@@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Entry = { name: string; type: string; size?: number | null; rel?: string };
|
||||
|
||||
function fmtSize(size?: number | null): string {
|
||||
if (typeof size !== "number") return "";
|
||||
if (size < 1024) return `${size} B`;
|
||||
if (size < 1024 * 1024) return `${Math.round(size / 1024)} KB`;
|
||||
return `${Math.round(size / 1024 / 1024)} MB`;
|
||||
}
|
||||
|
||||
export default function FilesBrowser() {
|
||||
const [path, setPath] = useState("");
|
||||
const [entries, setEntries] = useState<Entry[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [confirm, setConfirm] = useState<Entry | null>(null);
|
||||
|
||||
async function load(p: string) {
|
||||
setLoading(true);
|
||||
try {
|
||||
const r = await fetch(`/api/files?path=${encodeURIComponent(p)}`);
|
||||
if (!r.ok) return;
|
||||
const b = await r.json();
|
||||
setPath(b.path ?? "");
|
||||
setEntries(b.entries ?? []);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load("");
|
||||
}, []);
|
||||
|
||||
function up() {
|
||||
if (!path) return;
|
||||
const parts = path.split("/").filter(Boolean);
|
||||
parts.pop();
|
||||
load(parts.join("/"));
|
||||
}
|
||||
|
||||
async function remove(entry: Entry) {
|
||||
const r = await fetch(`/api/files?path=${encodeURIComponent(entry.rel ?? entry.name)}`, { method: "DELETE" });
|
||||
setConfirm(null);
|
||||
if (r.ok) load(path);
|
||||
else alert(`Löschen fehlgeschlagen (${r.status})`);
|
||||
}
|
||||
|
||||
const crumbs = path.split("/").filter(Boolean);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12, flexWrap: "wrap" }}>
|
||||
<button onClick={() => load("")} className="btn" style={{ padding: "4px 10px" }}>
|
||||
⌂ Root
|
||||
</button>
|
||||
{crumbs.map((c, i) => (
|
||||
<span key={i} style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span className="muted">/</span>
|
||||
<button
|
||||
onClick={() => load(crumbs.slice(0, i + 1).join("/"))}
|
||||
className="btn"
|
||||
style={{ padding: "4px 10px" }}
|
||||
>
|
||||
{c}
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
{path && (
|
||||
<button onClick={up} className="btn" style={{ padding: "4px 10px" }}>
|
||||
↑ Ebene hoch
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{loading && <p className="muted">Lädt …</p>}
|
||||
{!loading && entries.length === 0 && <p className="muted">Ordner ist leer.</p>}
|
||||
|
||||
<ol style={{ listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{entries.map((e) => (
|
||||
<li key={e.rel ?? e.name} className="card" style={{ display: "flex", alignItems: "center", gap: 12, padding: "10px 14px" }}>
|
||||
<span style={{ fontSize: 20 }} aria-hidden>{e.type === "dir" ? "📁" : "🎵"}</span>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
{e.type === "dir" ? (
|
||||
<button onClick={() => load(e.rel ?? e.name)} style={{ background: "none", border: "none", padding: 0, cursor: "pointer", color: "var(--text)", fontWeight: 600, fontSize: 15, textAlign: "left" }}>
|
||||
{e.name}
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{e.name}</div>
|
||||
<div className="muted" style={{ fontSize: 13 }}>{fmtSize(e.size)}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{e.type === "file" && (
|
||||
<>
|
||||
<a href={`/api/files/download?path=${encodeURIComponent(e.rel ?? e.name)}`} download className="btn" style={{ textDecoration: "none", whiteSpace: "nowrap" }}>
|
||||
↓ Laden
|
||||
</a>
|
||||
<button onClick={() => setConfirm(e)} className="btn" style={{ whiteSpace: "nowrap" }}>
|
||||
Löschen
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
{confirm && (
|
||||
<div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.6)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 100 }} onClick={() => setConfirm(null)}>
|
||||
<div className="card" style={{ padding: 20, maxWidth: 400, width: "90%", background: "var(--surface-2)" }} onClick={(ev) => ev.stopPropagation()}>
|
||||
<h3 style={{ marginTop: 0 }}>Wirklich löschen?</h3>
|
||||
<p><strong>{confirm.name}</strong>{confirm.size ? ` (${fmtSize(confirm.size)})` : ""}</p>
|
||||
<p className="muted">Endgültig — kein Papierkorb.</p>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<button onClick={() => remove(confirm)} className="btn btn-primary">Ja, löschen</button>
|
||||
<button onClick={() => setConfirm(null)} className="btn">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,6 @@ const NAV = [
|
||||
{ href: "/discover", label: "Entdecken" },
|
||||
{ href: "/downloads", label: "Downloads" },
|
||||
{ href: "/basket", label: "Korb" },
|
||||
{ href: "/files", label: "Dateien" },
|
||||
{ href: "/settings", label: "Einstellungen" },
|
||||
];
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import FilesBrowser from "../components/FilesBrowser";
|
||||
|
||||
type Job = { job_id: string; status: string; progress: number; params: Record<string, unknown>; logs: string[]; error?: string | null };
|
||||
type Entry = { id: string; source: string; media_type: string; title: string; artist: string; created_at: number };
|
||||
@@ -16,7 +17,7 @@ function jobTitle(j: Job): string {
|
||||
}
|
||||
|
||||
export default function DownloadsPage() {
|
||||
const [tab, setTab] = useState<"aktiv" | "verlauf">("aktiv");
|
||||
const [tab, setTab] = useState<"aktiv" | "verlauf" | "dateien">("aktiv");
|
||||
const [jobs, setJobs] = useState<Job[]>([]);
|
||||
const [entries, setEntries] = useState<Entry[]>([]);
|
||||
const [legacy, setLegacy] = useState<{ id: string }[]>([]);
|
||||
@@ -78,9 +79,14 @@ export default function DownloadsPage() {
|
||||
<button onClick={() => setTab("verlauf")} className={tab === "verlauf" ? "btn btn-primary" : "btn"}>
|
||||
Verlauf
|
||||
</button>
|
||||
<button onClick={() => setTab("dateien")} className={tab === "dateien" ? "btn btn-primary" : "btn"}>
|
||||
Dateien
|
||||
</button>
|
||||
</div>
|
||||
{msg && <p>{msg}</p>}
|
||||
|
||||
{tab === "dateien" && <FilesBrowser />}
|
||||
|
||||
{tab === "aktiv" && (
|
||||
<div>
|
||||
{jobs.length === 0 && <p className="muted">Keine Jobs — starte einen Download über die Suche.</p>}
|
||||
|
||||
@@ -1,77 +1,5 @@
|
||||
"use client";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Entry = { name: string; type: string; size?: number | null; rel?: string };
|
||||
|
||||
export default function FilesPage() {
|
||||
const [path, setPath] = useState("");
|
||||
const [entries, setEntries] = useState<Entry[]>([]);
|
||||
const [confirm, setConfirm] = useState<Entry | null>(null);
|
||||
|
||||
async function load(p: string) {
|
||||
const r = await fetch(`/api/files?path=${encodeURIComponent(p)}`);
|
||||
if (!r.ok) return;
|
||||
const b = await r.json();
|
||||
setPath(b.path ?? "");
|
||||
setEntries(b.entries ?? []);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load("");
|
||||
}, []);
|
||||
|
||||
async function remove(entry: Entry) {
|
||||
const r = await fetch(`/api/files?path=${encodeURIComponent(entry.rel ?? entry.name)}`, { method: "DELETE" });
|
||||
setConfirm(null);
|
||||
if (r.ok) load(path);
|
||||
else alert(`Löschen fehlgeschlagen (${r.status})`);
|
||||
}
|
||||
|
||||
function up() {
|
||||
if (!path) return;
|
||||
const parts = path.split("/").filter(Boolean);
|
||||
parts.pop();
|
||||
load(parts.join("/"));
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Dateien</h1>
|
||||
<p className="muted">/{path}</p>
|
||||
{path && <button onClick={up} className="btn" style={{ marginBottom: 10 }}>↑ Ebene hoch</button>}
|
||||
<ul>
|
||||
{entries.map((e) => (
|
||||
<li key={e.rel ?? e.name}>
|
||||
{e.type === "dir" ? (
|
||||
<button onClick={() => load(e.rel ?? e.name)} className="btn">📁 {e.name}</button>
|
||||
) : (
|
||||
<span>
|
||||
🎵 {e.name}{typeof e.size === "number" ? ` (${Math.round(e.size / 1024 / 1024)} MB)` : ""}{" "}
|
||||
<a href={`/api/files/download?path=${encodeURIComponent(e.rel ?? e.name)}`} download>
|
||||
Herunterladen ↓
|
||||
</a>{" "}
|
||||
<button onClick={() => setConfirm(e)} className="btn" style={{ padding: "2px 8px" }}>
|
||||
Löschen
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{confirm && (
|
||||
<div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.6)", display: "flex", alignItems: "center", justifyContent: "center" }} onClick={() => setConfirm(null)}>
|
||||
<div className="card" style={{ padding: 20, maxWidth: 400, background: "var(--surface-2)" }} onClick={(ev) => ev.stopPropagation()}>
|
||||
<h3 style={{ marginTop: 0 }}>Wirklich löschen?</h3>
|
||||
<p><strong>{confirm.name}</strong>{typeof confirm.size === "number" ? ` (${Math.round(confirm.size / 1024 / 1024)} MB)` : ""}</p>
|
||||
<p className="muted">Endgültig — kein Papierkorb.</p>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<button onClick={() => remove(confirm)} className="btn btn-primary">Ja, löschen</button>
|
||||
<button onClick={() => setConfirm(null)} className="btn">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
export default function FilesRedirect() {
|
||||
redirect("/downloads");
|
||||
}
|
||||
Reference in new issue
Block a user