Add quality indicator and re-download at quality to Watch page

- Quality selector now always visible when idle (not just pre-download)
- Saved chip shows actual downloaded resolution (e.g. "Saved · 1080p")
- Re-download chip deletes existing file and starts new download at selected quality

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mattias Tall
2026-05-26 17:37:03 +02:00
parent 623e82fb16
commit 744af7337b

View File

@@ -562,6 +562,7 @@ export default function Watch() {
const [queued, setQueued] = useState(null); const [queued, setQueued] = useState(null);
const [liked, setLiked] = useState(null); const [liked, setLiked] = useState(null);
const [disliked, setDisliked] = useState(null); const [disliked, setDisliked] = useState(null);
const [isRedownloading, setIsRedownloading] = useState(false);
const [selectedQuality, setSelectedQuality] = useState(null); const [selectedQuality, setSelectedQuality] = useState(null);
const [speed, setSpeed] = useState(1); const [speed, setSpeed] = useState(1);
const [autoplay, setAutoplay] = useState(false); const [autoplay, setAutoplay] = useState(false);
@@ -721,6 +722,22 @@ export default function Watch() {
setPlayRequested(true); setPlayRequested(true);
downloadMut.mutate(); downloadMut.mutate();
}, [downloadMut]); }, [downloadMut]);
const handleRedownload = useCallback(async () => {
const dlId = downloadId ?? allDownloads.find(
d => d.youtube_video_id === youtubeVideoId && d.status === "complete"
)?.id;
if (!dlId) return;
setIsRedownloading(true);
try { await deleteDownload(dlId); } catch (_) {}
setFileReady(false);
setConfirmedFileUrl(null);
setDownloadId(null);
setPlayRequested(false);
setIsRedownloading(false);
qc.invalidateQueries({ queryKey: ["downloads"] });
refetchVideo();
downloadMut.mutate();
}, [downloadId, allDownloads, youtubeVideoId, downloadMut, refetchVideo, qc]);
const saveProgress = useCallback((secs) => { const saveProgress = useCallback((secs) => {
if (!video?.id) return; if (!video?.id) return;
@@ -792,6 +809,7 @@ export default function Watch() {
const isLiked = liked ?? video?.liked ?? false; const isLiked = liked ?? video?.liked ?? false;
const isDisliked = disliked ?? (video?.rating === -1) ?? false; const isDisliked = disliked ?? (video?.rating === -1) ?? false;
const dlComplete = dlStatus?.status === "complete" || video?.is_downloaded; const dlComplete = dlStatus?.status === "complete" || video?.is_downloaded;
const downloadedResolution = dlStatus?.resolution ?? video?.download_resolution;
const isFollowed = followMut.isSuccess || video?.channel_followed; const isFollowed = followMut.isSuccess || video?.channel_followed;
const subs = formatSubs(channel?.subscriber_count); const subs = formatSubs(channel?.subscriber_count);
@@ -912,7 +930,7 @@ export default function Watch() {
{/* Actions */} {/* Actions */}
<div className="flex items-center gap-1.5 flex-wrap"> <div className="flex items-center gap-1.5 flex-wrap">
{!dlComplete && !isDownloading && !downloadMut.isPending && ( {!isDownloading && !downloadMut.isPending && !isRedownloading && (
<select <select
value={selectedQuality ?? "best"} value={selectedQuality ?? "best"}
onChange={(e) => setSelectedQuality(e.target.value)} onChange={(e) => setSelectedQuality(e.target.value)}
@@ -952,7 +970,7 @@ export default function Watch() {
onClick={() => !dlComplete && !isDownloading && downloadMut.mutate()} onClick={() => !dlComplete && !isDownloading && downloadMut.mutate()}
> >
{dlComplete ? ( {dlComplete ? (
<><svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M9 16.2l-3.5-3.5-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/></svg>Saved</> <><svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M9 16.2l-3.5-3.5-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/></svg>Saved{downloadedResolution ? ` · ${downloadedResolution}` : ""}</>
) : isDownloading || downloadMut.isPending ? ( ) : isDownloading || downloadMut.isPending ? (
<><svg className="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/></svg>Downloading</> <><svg className="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/></svg>Downloading</>
) : ( ) : (
@@ -969,6 +987,16 @@ export default function Watch() {
</Chip> </Chip>
)} )}
{dlComplete && (
<Chip onClick={handleRedownload} disabled={isRedownloading || downloadMut.isPending}>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
Re-download
</Chip>
)}
{video?.id && ( {video?.id && (
<> <>
<Chip active={isLiked} onClick={() => likeMut.mutate()} disabled={likeMut.isPending}> <Chip active={isLiked} onClick={() => likeMut.mutate()} disabled={likeMut.isPending}>