Fix Downloads search UI and clarify DS destination errors.
Clear results after starting a job, show one clean list layout, and map Synology 403 to a readable destination hint. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
12a99aec0f
commit
9843d20614
3 changed files with 91 additions and 57 deletions
|
|
@ -273,6 +273,9 @@ export default function DownloadsPage() {
|
|||
return;
|
||||
}
|
||||
setPick(null);
|
||||
setHits([]);
|
||||
setQuery("");
|
||||
setSearchErr(null);
|
||||
loadJobs();
|
||||
} finally {
|
||||
setJobBusy(false);
|
||||
|
|
@ -346,7 +349,7 @@ export default function DownloadsPage() {
|
|||
/>
|
||||
</label>
|
||||
<label>
|
||||
DS destination (share-relatief)
|
||||
DS destination (share-naam, géén /volume1)
|
||||
<input
|
||||
value={cfg.downloadDestination}
|
||||
onChange={(e) => setCfg({ ...cfg, downloadDestination: e.target.value })}
|
||||
|
|
@ -354,7 +357,7 @@ export default function DownloadsPage() {
|
|||
/>
|
||||
</label>
|
||||
<label>
|
||||
Download host-pad (node)
|
||||
Download host-pad (node, wél /volume1/…)
|
||||
<input
|
||||
value={cfg.downloadHostPath}
|
||||
onChange={(e) => setCfg({ ...cfg, downloadHostPath: e.target.value })}
|
||||
|
|
@ -401,58 +404,61 @@ export default function DownloadsPage() {
|
|||
{searchErr && <p className="danger-text">{searchErr}</p>}
|
||||
{!hasConfig && <p className="muted">Sla eerst de DS-configuratie op.</p>}
|
||||
|
||||
<div className="mobile-cards" style={{ marginTop: "1rem" }}>
|
||||
{hits.map((h) => (
|
||||
<article key={`${h.title}-${h.size}-${h.seeds}`} className="mobile-card">
|
||||
<div className="mobile-card-title">{h.title}</div>
|
||||
<div className="muted">
|
||||
{formatBytes(h.size)} · {h.seeds} seeds · {h.module || "module"}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!h.uri}
|
||||
onClick={() => void openMatch(h)}
|
||||
style={{ marginTop: "0.5rem" }}
|
||||
>
|
||||
Kiezen
|
||||
</button>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
{hits.length > 0 && (
|
||||
<>
|
||||
<div className="list-cards" style={{ marginTop: "1rem" }}>
|
||||
{hits.map((h) => (
|
||||
<article key={`${h.title}-${h.size}-${h.seeds}`} className="list-card">
|
||||
<div className="list-card-top">
|
||||
<div className="list-card-title">{h.title}</div>
|
||||
<button type="button" disabled={!h.uri} onClick={() => void openMatch(h)}>
|
||||
Kiezen
|
||||
</button>
|
||||
</div>
|
||||
<div className="list-card-meta muted">
|
||||
<span>{formatBytes(h.size)}</span>
|
||||
<span>
|
||||
{h.seeds}/{h.peers} seeds
|
||||
</span>
|
||||
<span>{h.module || "—"}</span>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="desktop-table" style={{ marginTop: "1rem" }}>
|
||||
{hits.length > 0 && (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Titel</th>
|
||||
<th>Grootte</th>
|
||||
<th>Seeds</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{hits.map((h) => (
|
||||
<tr key={`${h.title}-${h.size}-${h.seeds}`}>
|
||||
<td>
|
||||
<div>{h.title}</div>
|
||||
<div className="muted">{h.module}</div>
|
||||
</td>
|
||||
<td>{formatBytes(h.size)}</td>
|
||||
<td>
|
||||
{h.seeds}/{h.peers}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" disabled={!h.uri} onClick={() => void openMatch(h)}>
|
||||
Kiezen
|
||||
</button>
|
||||
</td>
|
||||
<div className="desktop-table" style={{ marginTop: "1rem" }}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Titel</th>
|
||||
<th>Grootte</th>
|
||||
<th>Seeds</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</thead>
|
||||
<tbody>
|
||||
{hits.map((h) => (
|
||||
<tr key={`${h.title}-${h.size}-${h.seeds}`}>
|
||||
<td>
|
||||
<div>{h.title}</div>
|
||||
<div className="muted">{h.module}</div>
|
||||
</td>
|
||||
<td>{formatBytes(h.size)}</td>
|
||||
<td>
|
||||
{h.seeds}/{h.peers}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" disabled={!h.uri} onClick={() => void openMatch(h)}>
|
||||
Kiezen
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -461,14 +467,14 @@ export default function DownloadsPage() {
|
|||
<h2 className="section-title">Jobs</h2>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="mobile-cards">
|
||||
<div className="list-cards">
|
||||
{jobs.map((j) => (
|
||||
<article key={j.id} className="mobile-card">
|
||||
<div className="mobile-card-head">
|
||||
<article key={j.id} className="list-card">
|
||||
<div className="list-card-top">
|
||||
<span className={statusClass(j.status)}>{j.status}</span>
|
||||
<span className="muted">{j.nodeName}</span>
|
||||
</div>
|
||||
<div className="mobile-card-title">
|
||||
<div className="list-card-title">
|
||||
{j.movieTitle}
|
||||
{j.movieYear ? ` (${j.movieYear})` : ""}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -779,6 +779,16 @@ tr.row-selected {
|
|||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.list-card-title {
|
||||
font-weight: 600;
|
||||
color: var(--text-bright);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.35;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.list-card-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ export class SynologyDownloadStation {
|
|||
const url = `${this.root()}/webapi/DownloadStation/task.cgi`;
|
||||
const res = (await requestJson(url, { method: "POST", body })) as DsApiResponse;
|
||||
if (!res.success) {
|
||||
throw new Error(`Download-taak aanmaken mislukt (code ${res.error?.code ?? "?"})`);
|
||||
throw new Error(dsTaskError(res.error?.code, "Download-taak aanmaken mislukt"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -294,6 +294,24 @@ function sleep(ms: number): Promise<void> {
|
|||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
/** Human-readable Synology Download Station Task API errors. */
|
||||
function dsTaskError(code: number | undefined, prefix: string): string {
|
||||
const hints: Record<number, string> = {
|
||||
400: "bestand-upload mislukt",
|
||||
401: "maximaal aantal taken bereikt",
|
||||
402: "geen schrijfrecht op destination",
|
||||
403: "destination bestaat niet — gebruik share-relatief pad, bv. Downloads of Downloads/complete (niet /volume1/...)",
|
||||
404: "ongeldige task-id",
|
||||
405: "ongeldige actie",
|
||||
406: "geen default destination in Download Station",
|
||||
407: "destination instellen mislukt",
|
||||
408: "bestand bestaat niet",
|
||||
};
|
||||
const hint = code != null ? hints[code] : undefined;
|
||||
if (hint) return `${prefix}: ${hint} (code ${code})`;
|
||||
return `${prefix} (code ${code ?? "?"})`;
|
||||
}
|
||||
|
||||
/** Strip common release tags so TMDB search works better. */
|
||||
export function cleanReleaseQuery(raw: string): string {
|
||||
let s = raw.trim();
|
||||
|
|
|
|||
Loading…
Reference in a new issue