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:
@@ -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} />
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user