Files
FlightTube/src/components/DownloadButton.tsx
T
vincent 9bb7b71225 feat: uniform control height, chrome auto-hide, streaming quality
Every control in the app now shares one height (CONTROL_H), with width
still growing to fit its label. The player transport is larger, Back is
an icon, and the footer buttons match the prev/next size.

Player chrome — transport and edge arrows — fades after 2.6s of
inactivity and never hides while paused.

The title-bar strip collapses in macOS window fullscreen, where the
traffic lights are gone and it was just a blank white bar.

Streaming quality is now selectable alongside download quality. A cap is
applied by rewriting YouTube's HLS master playlist down to the best
variant at or below the chosen height, keeping its audio group. That
playlist is served from a small loopback HTTP server: Safari's native
HLS is backed by AVFoundation, which cannot read blob: or custom-scheme
URLs, so a Blob URL silently fails to play.

Settings closes with an icon button and its selects match the shared
control height.
2026-08-29 11:28:05 +02:00

80 lines
3.1 KiB
TypeScript

import type { LiveDownload } from "../hooks/useDownloads";
import type { FeedItem } from "../types";
import { humanEta } from "./format";
import { CONTROL_H } from "./ui";
interface Props {
item: FeedItem;
live?: LiveDownload;
online: boolean;
onDownload: () => void;
onCancel: () => void;
onDelete: () => void;
}
const CHIP =
`inline-flex ${CONTROL_H} shrink-0 cursor-pointer items-center justify-center rounded-lg ` +
"px-2.5 text-[12px] font-medium";
export default function DownloadButton({
item, live, online, onDownload, onCancel, onDelete,
}: Props) {
const state = live?.state ?? item.state ?? null;
const pct = live?.pct ?? item.pct ?? null;
const error = live?.error ?? item.error ?? null;
if (state === "done") {
// Neutral until hovered, then reveals red — destruction is never
// pre-coloured on a control that is not currently destructive.
return (
<button onClick={onDelete} title="Delete the downloaded file"
className={`${CHIP} group border border-slate-300 text-slate-500
hover:border-red-500 hover:text-red-600
dark:border-slate-700 dark:text-slate-400 dark:hover:border-red-500 dark:hover:text-red-400`}>
<span className="group-hover:hidden">Saved</span>
<span className="hidden group-hover:inline">Delete</span>
</button>
);
}
if (state === "running" || state === "queued") {
const known = state === "running" && pct != null;
return (
<button onClick={onCancel} title={humanEta(live?.eta ?? null) || "Cancel download"}
className={`${CHIP} group relative w-[104px] overflow-hidden border border-slate-300
text-slate-500 hover:border-red-500 hover:text-red-600
dark:border-slate-700 dark:text-slate-400 dark:hover:border-red-500
dark:hover:text-red-400`}>
{/* Progress fills the chip itself, so the control stays one line tall. */}
<span
className="absolute inset-y-0 left-0 bg-sky-500/15 transition-[width] duration-100"
style={{ width: known ? `${pct}%` : "35%" }}
/>
<span className="relative hidden group-hover:inline">Cancel</span>
<span className="relative inline font-mono tabular-nums group-hover:hidden">
{known ? `${pct!.toFixed(0)}%` : state === "queued" ? "Queued" : "Starting"}
</span>
</button>
);
}
const failed = state === "failed";
return (
<button onClick={onDownload} disabled={!online}
title={
!online
? "You are offline — downloading needs a connection"
: failed && error
? error
: "Download for offline viewing"
}
className={`${CHIP} border disabled:cursor-not-allowed disabled:opacity-40 ${
failed
? "border-red-500/60 text-red-600 hover:bg-red-500/5 dark:text-red-400"
: "border-slate-300 hover:border-sky-500 hover:text-sky-600 dark:border-slate-700 dark:hover:border-sky-500 dark:hover:text-sky-400"
}`}>
{failed ? "Retry" : "Download"}
</button>
);
}