feat(downloads): a bare link is titled once, a missing picture shows a glyph

A job with no name yet is titled by its short link, as the link itself;
the channel line no longer falls back to the ref and the link line does
not repeat it. No picture at all draws a film (or note) glyph instead of
an empty box.
This commit is contained in:
2026-09-27 07:12:51 +02:00
parent f48f16830e
commit 098b8dbe43
3 changed files with 104 additions and 21 deletions
+39 -20
View File
@@ -1,6 +1,6 @@
import { useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { CalendarClock, File, Link2, Scissors } from "lucide-react";
import { CalendarClock, File, Film, Link2, Music, Scissors } from "lucide-react";
import clsx from "clsx";
import { qk } from "../lib/queryKeys";
import { useLiveQuery } from "../lib/useLiveQuery";
@@ -15,7 +15,7 @@ import {
} from "../lib/format";
import { navItemAttrs } from "../lib/listNav";
import { failCopyKeys } from "../lib/downloadFailure";
import { jobTitle } from "./downloadTitle";
import { displayUrl, hasOwnTitle, jobTitle } from "./downloadTitle";
import { IconButton } from "./ui/IconButton";
import { Pressable } from "./ui/Pressable";
import { SURFACE } from "./ui/surfaces";
@@ -113,19 +113,30 @@ function Thumb({ job }: { job: DownloadJob }) {
loading="lazy"
className="w-full h-full object-cover"
onError={() => {
if (remote && src !== remote) setFailedPrimary(primary);
if (src !== remote) setFailedPrimary(primary);
}}
/>
) : null}
) : (
// No picture at all (a link that failed before its details, a poster that never came): a
// quiet glyph of what it is, like the file glyph above — an empty box read as "still loading".
<div className="flex h-full items-center justify-center text-muted">
{job.asset?.media_kind === "audio" ? (
<Music className="w-7 h-7" aria-hidden="true" />
) : (
<Film className="w-7 h-7" aria-hidden="true" />
)}
</div>
)}
</div>
);
}
// The channel to show under the title: a user override wins over the auto-extracted asset value.
// `url` is null for sources without a channel link (e.g. reddit) — then it renders as plain text.
function jobChannel(job: DownloadJob): { name: string; url: string | null } {
// `url` is null for sources without a channel link (e.g. reddit) — then it renders as plain text. No
// channel known yet → no line: the ref it once fell back to repeated the title (the owner, 2026-09-27).
function jobChannel(job: DownloadJob): { name: string | null; url: string | null } {
return {
name: job.display_uploader || job.asset?.uploader || job.source_ref,
name: job.display_uploader || job.asset?.uploader || null,
url: job.display_uploader_url || job.asset?.uploader_url || null,
};
}
@@ -157,16 +168,6 @@ export function IconBtn({
);
}
// Compact display of a URL: drop the protocol + trailing slash so "youtube.com/watch?v=…" fits.
function displayUrl(url: string): string {
try {
const u = new URL(url);
return (u.host + u.pathname + u.search).replace(/\/$/, "");
} catch {
return url;
}
}
// A link inside the row keeps the BROWSER's own right-click menu (open in a new tab, copy the
// address): the row's menu answers everywhere else on it (E28 C1).
const nativeMenu = { onContextMenu: (e: React.MouseEvent) => e.stopPropagation() };
@@ -195,6 +196,7 @@ function SourceRef({ url }: { url: string }) {
// Channel under the title — a clickable link when a channel URL is known, plain text otherwise.
function ChannelLine({ job }: { job: DownloadJob }) {
const { name, url } = jobChannel(job);
if (!name) return null;
if (url) {
return (
<div className="text-sm mt-0.5 truncate">
@@ -234,6 +236,8 @@ export function DownloadRow({
}) {
const { t } = useTranslation();
const running = job.status === "running";
const linkTitle = !hasOwnTitle(job) && !!job.source_url;
const sourceLink = linkTitle ? null : job.source_url;
return (
<div
{...(navIndex !== undefined && navItemAttrs(navIndex, jobTitle(job)))}
@@ -255,7 +259,22 @@ export function DownloadRow({
<Scissors className="w-3 h-3" /> {t("downloads.clipBadge")}
</span>
)}
{jobTitle(job)}
{linkTitle ? (
// No name yet: the title IS the link, so the link line below leaves it out (it said the
// same URL a third time).
<a
href={job.source_url!}
target="_blank"
rel="noopener noreferrer"
title={`${t("downloads.source.open")} — ${job.source_url}`}
className="hover:text-accent hover:underline"
{...nativeMenu}
>
{jobTitle(job)}
</a>
) : (
jobTitle(job)
)}
</div>
<ChannelLine job={job} />
</div>
@@ -303,9 +322,9 @@ export function DownloadRow({
(at 375). On touch each is a 44px box, so separate lines stacked 44px gaps (the owner,
2026-09-27: "what are those huge empty parts?"). */}
<div className="contents @max-[40rem]/dlrow:[grid-area:foot] @max-[40rem]/dlrow:flex @max-[40rem]/dlrow:flex-wrap @max-[40rem]/dlrow:items-center @max-[40rem]/dlrow:gap-x-2">
{(!!job.source_url || !!job.extra_links?.length) && (
{(!!sourceLink || !!job.extra_links?.length) && (
<div className="[grid-area:src] min-w-0 @max-[40rem]/dlrow:min-w-32 @max-[40rem]/dlrow:flex-1">
{job.source_url && <SourceRef url={job.source_url} />}
{sourceLink && <SourceRef url={sourceLink} />}
{job.extra_links?.map((url) => (
<SourceRef key={url} url={url} />
))}