fix(files): review round 1, the page — honest states, the keyboard, the tray's words
- A library past the list's cap is searched and sorted on the server; the count says "the newest". - Each folder and each search opens on its first page (DataTable pageScope, useCardPager scope). - A file dropped where the page takes none never reaches the browser (which would unload the app). - The viewer's arrows wait while a layer opened from it is on top, and scroll a wide text line; a text shows its first 5000 lines and says where the rest is. - Confirms name the file (no more "your downloads"); the asking menu rows end in "…". - The tray names a running step beside its bar, shows a pause mark for paused work, polls slowly while only paused work waits, and forgets a finished file that was deleted. - An unchanged rename sends nothing; a paste opens the download dialog only with nothing on top; the link box never replaces what was typed. - Honest states: folders that failed to load, a file no longer there, the storage figure; the rail says "nothing downloaded" only when there is nothing. - Focus after deleting the open folder and when the format form opens; the format name's label. - ui/Meter (one danger line for both quota bars) and ui/RenameField in the platform layer. - Copy fixes, stale comments and the speculative hub key; a contract test for the list's parameters; e2e: the Files page and the paste intake.
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
FolderOpen,
|
||||
Link,
|
||||
Loader2,
|
||||
PauseCircle,
|
||||
RotateCw,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
@@ -17,6 +18,8 @@ import { quickShareLink } from "../lib/downloads";
|
||||
import { failCopyKeys, failTitleKey } from "../lib/downloadFailure";
|
||||
import {
|
||||
IN_FLIGHT,
|
||||
DOWNLOAD_PHASES,
|
||||
MOVING,
|
||||
clearFinished,
|
||||
doneRevealing,
|
||||
fetchQueue,
|
||||
@@ -66,22 +69,39 @@ const BYTE_PHASES = ["video", "audio", "editing", "converting"];
|
||||
|
||||
const keyOf = (job: QueueJob) => job.id;
|
||||
|
||||
// The step a running job is on is named beside its bar: a download fetches its video and its audio
|
||||
// as two streams, so the bar fills twice — without the name, the second start reads as a restart.
|
||||
function Progress({ job }: { job: QueueJob }) {
|
||||
if (job.phase && !BYTE_PHASES.includes(job.phase))
|
||||
return (
|
||||
<span className="block h-1 overflow-hidden rounded-full bg-fg/10">
|
||||
<span className="block h-full w-full animate-pulse bg-accent/60" />
|
||||
</span>
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
const bytes = !job.phase || BYTE_PHASES.includes(job.phase);
|
||||
const step =
|
||||
job.phase && DOWNLOAD_PHASES.includes(job.phase) ? t(`downloads.phase.${job.phase}`) : null;
|
||||
return (
|
||||
<span className="flex items-center gap-2 text-xs text-muted">
|
||||
<span className="h-1 flex-1 overflow-hidden rounded-full bg-fg/10">
|
||||
<span
|
||||
className="block h-full rounded-full bg-accent transition-all"
|
||||
style={{ width: `${job.progress}%` }}
|
||||
/>
|
||||
<span className="flex min-w-0 items-center gap-2 text-xs text-muted">
|
||||
{/* One column for the step, so every row's bar starts on the same line. */}
|
||||
{step && (
|
||||
<span className="w-36 shrink-0 truncate" title={step}>
|
||||
{step}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
role="progressbar"
|
||||
aria-label={step ?? t("downloads.status.running")}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={bytes ? job.progress : undefined}
|
||||
className="h-1 min-w-12 flex-1 overflow-hidden rounded-full bg-fg/10"
|
||||
>
|
||||
{bytes ? (
|
||||
<span
|
||||
className="block h-full rounded-full bg-accent transition-all"
|
||||
style={{ width: `${job.progress}%` }}
|
||||
/>
|
||||
) : (
|
||||
<span className="block h-full w-full animate-pulse bg-accent/60" />
|
||||
)}
|
||||
</span>
|
||||
<span className="w-8 text-right tabular-nums">{job.progress}%</span>
|
||||
{bytes && <span className="w-8 shrink-0 text-right tabular-nums">{job.progress}%</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -95,7 +115,7 @@ export default function DownloadTray() {
|
||||
// Every page polls it now, so the idle cadence is slow: a job started here refetches at once
|
||||
// (invalidateDownloads), and one started on another device shows within a minute.
|
||||
const queueQ = useLiveQuery(qk.downloads(), fetchQueue, {
|
||||
intervalMs: (data) => (data?.some((j) => IN_FLIGHT.has(j.status)) ? 2000 : 60000),
|
||||
intervalMs: (data) => (data?.some((j) => MOVING.has(j.status)) ? 2000 : 60000),
|
||||
});
|
||||
const finished = useFinished();
|
||||
const active = (queueQ.data ?? []).filter((j) => IN_FLIGHT.has(j.status) || j.status === "error");
|
||||
@@ -225,13 +245,26 @@ export default function DownloadTray() {
|
||||
const meanProgress = running.length
|
||||
? Math.round(running.reduce((sum, j) => sum + j.progress, 0) / running.length)
|
||||
: null;
|
||||
const StateIcon = counts.running ? Loader2 : counts.error ? AlertTriangle : CheckCircle2;
|
||||
// The tray's state, worst news first; a paused job is neither working nor done.
|
||||
const StateIcon = counts.running
|
||||
? Loader2
|
||||
: counts.error
|
||||
? AlertTriangle
|
||||
: counts.paused
|
||||
? PauseCircle
|
||||
: CheckCircle2;
|
||||
const stateIcon = (
|
||||
<StateIcon
|
||||
aria-hidden
|
||||
className={clsx(
|
||||
"size-4 shrink-0",
|
||||
counts.running ? "animate-spin text-accent" : counts.error ? "text-danger" : "text-success",
|
||||
counts.running
|
||||
? "animate-spin text-accent"
|
||||
: counts.error
|
||||
? "text-danger"
|
||||
: counts.paused
|
||||
? "text-warn"
|
||||
: "text-success",
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user