feat: durations, clean subtitles, icon buttons
Subtitles rendered pinned to the left edge and clipped, in half-grey karaoke text: YouTube's auto-captions carry align:start position:0% on every cue plus inline <00:00:12.480><c>word</c> timing tags. Both are now stripped after download, and existing downloads are tidied the first time they are listed. Thumbnails show video length. The Atom feed carries no duration, so it is read from the watch page and cached — but only a trickle. The first version fetched 24 pages every 12 seconds at six concurrent, roughly two requests a second sustained, and YouTube answered by challenging the whole IP: 'Sign in to confirm you are not a bot', which broke streaming and downloads too. It is now four pages every five minutes, one at a time, and stops for the session the moment a batch is refused. A subtitle chosen in the player becomes the stored preference, so the next video matches without going back to Settings. The back and download buttons are square icon buttons; the back glyph was off-centre because px-2 beat the p-0 meant to clear it.
This commit is contained in:
@@ -20,6 +20,8 @@ interface Props {
|
||||
maxHeight: number | null;
|
||||
/** Preferred subtitle language, or "off". */
|
||||
subLang: string;
|
||||
/** Persists a subtitle choice made from the transport bar. */
|
||||
onSubLang: (l: string) => void;
|
||||
/** Position in the current feed, for the "3 of 180" readout. */
|
||||
index: number;
|
||||
total: number;
|
||||
@@ -99,7 +101,7 @@ const RESUME_EDGE_S = 5;
|
||||
*/
|
||||
export default function Player({
|
||||
item, path, onClose, onDelete, onPrev, onNext, onDownload, downloading,
|
||||
maxHeight, subLang, index, total, titleBarInset,
|
||||
maxHeight, subLang, onSubLang, index, total, titleBarInset,
|
||||
}: Props) {
|
||||
const streaming = path === null;
|
||||
const [src, setSrc] = useState<string | null>(path ? fileUrl(path) : null);
|
||||
@@ -220,8 +222,14 @@ export default function Player({
|
||||
return () => window.clearTimeout(hideTimer.current);
|
||||
}, [showChrome, src]);
|
||||
|
||||
const navIcon =
|
||||
"grid h-[30px] w-[30px] shrink-0 place-items-center rounded-lg border border-slate-300 " +
|
||||
"cursor-pointer hover:border-sky-500 hover:text-sky-600 disabled:opacity-30 " +
|
||||
"disabled:cursor-not-allowed dark:border-slate-700 dark:hover:border-sky-500 " +
|
||||
"dark:hover:text-sky-400";
|
||||
|
||||
const navBtn =
|
||||
"rounded-lg border border-slate-300 px-2 py-1.5 text-[11px] font-medium cursor-pointer " +
|
||||
"inline-flex h-[30px] items-center rounded-lg border border-slate-300 px-2.5 text-[12px] font-medium cursor-pointer " +
|
||||
"hover:border-sky-500 hover:text-sky-600 disabled:opacity-30 disabled:cursor-not-allowed " +
|
||||
"disabled:hover:border-slate-300 disabled:hover:text-inherit " +
|
||||
"dark:border-slate-700 dark:hover:border-sky-500 dark:hover:text-sky-400";
|
||||
@@ -234,7 +242,7 @@ export default function Player({
|
||||
dark:border-slate-800 dark:bg-slate-900"
|
||||
>
|
||||
<button onClick={leave} title="Back to the feed (Esc)" aria-label="Back"
|
||||
className={`${navBtn} w-[30px] p-0`}>
|
||||
className={navIcon}>
|
||||
<svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 6l-6 6 6 6" />
|
||||
</svg>
|
||||
@@ -360,6 +368,7 @@ export default function Player({
|
||||
stageRef={stageRef}
|
||||
onActivity={showChrome}
|
||||
subLang={subLang}
|
||||
onSubLang={onSubLang}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -383,9 +392,17 @@ export default function Player({
|
||||
<button
|
||||
onClick={onDownload}
|
||||
disabled={downloading}
|
||||
className={`${navBtn} whitespace-nowrap`}
|
||||
title={downloading ? "Downloading…" : "Download for offline"}
|
||||
aria-label="Download"
|
||||
className={navIcon}
|
||||
>
|
||||
{downloading ? "Downloading…" : "Download"}
|
||||
{downloading ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v12m0 0l-4-4m4 4l4-4M4 20h16" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{!streaming && (
|
||||
@@ -402,7 +419,7 @@ export default function Player({
|
||||
onClick={() => openExternal(`https://www.youtube.com/watch?v=${item.id}`)}
|
||||
title="Open on YouTube"
|
||||
aria-label="Open on YouTube"
|
||||
className={`${navBtn} w-[30px] p-0`}
|
||||
className={navIcon}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path strokeLinecap="round" strokeLinejoin="round"
|
||||
|
||||
Reference in New Issue
Block a user