feat(feed): resume progress bar, play/continue/restart, in-progress filter
Video cards show a resume progress bar for started-but-unfinished videos and a hover overlay: Play on every card, Continue + Restart on in-progress ones. The in-app player now resumes from (and checkpoints to) the server position instead of localStorage, accepts an explicit startAt (Restart -> 0), and refreshes the feed on close so the card bar reflects the session. Sidebar gains an 'In progress' show filter.
This commit is contained in:
parent
686c40cbb9
commit
04c971f623
5 changed files with 114 additions and 30 deletions
|
|
@ -1,5 +1,14 @@
|
|||
import { memo } from "react";
|
||||
import { Bookmark, Check, CheckCheck, Eye, EyeOff, ListFilter } from "lucide-react";
|
||||
import {
|
||||
Bookmark,
|
||||
Check,
|
||||
CheckCheck,
|
||||
Eye,
|
||||
EyeOff,
|
||||
ListFilter,
|
||||
Play,
|
||||
RotateCcw,
|
||||
} from "lucide-react";
|
||||
import Avatar from "./Avatar";
|
||||
import clsx from "clsx";
|
||||
import type { Video } from "../lib/api";
|
||||
|
|
@ -81,7 +90,7 @@ function Actions({
|
|||
function openInApp(
|
||||
e: React.MouseEvent,
|
||||
video: Video,
|
||||
onOpen?: (v: Video) => void
|
||||
onOpen?: (v: Video, startAt?: number | null) => void
|
||||
): void {
|
||||
if (!onOpen || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
|
|
@ -95,8 +104,23 @@ function Thumb({
|
|||
}: {
|
||||
video: Video;
|
||||
className?: string;
|
||||
onOpen?: (v: Video) => void;
|
||||
onOpen?: (v: Video, startAt?: number | null) => void;
|
||||
}) {
|
||||
// A non-zero saved position on an unfinished video = "in progress": show a resume
|
||||
// progress bar and offer Continue / Restart instead of a plain Play.
|
||||
const inProgress = video.position_seconds > 0 && video.status !== "watched";
|
||||
const pct =
|
||||
inProgress && video.duration_seconds
|
||||
? Math.min(99, Math.max(2, (video.position_seconds / video.duration_seconds) * 100))
|
||||
: 0;
|
||||
|
||||
// Open in the in-app player at an explicit position (null = resume from saved).
|
||||
const open = (startAt: number | null) => (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onOpen?.(video, startAt);
|
||||
};
|
||||
|
||||
return (
|
||||
<a
|
||||
href={video.watch_url}
|
||||
|
|
@ -133,6 +157,45 @@ function Thumb({
|
|||
<Bookmark className="w-3.5 h-3.5" />
|
||||
</span>
|
||||
)}
|
||||
{/* Hover overlay: Play everywhere; Continue + Restart once the video is in progress. */}
|
||||
{onOpen && (
|
||||
<div className="absolute inset-0 flex items-center justify-center gap-2 bg-black/45 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{inProgress ? (
|
||||
<>
|
||||
<button
|
||||
onClick={open(null)}
|
||||
title="Continue where you left off"
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-semibold bg-accent text-accent-fg shadow-lg hover:opacity-90 transition"
|
||||
>
|
||||
<Play className="w-4 h-4 fill-current" />
|
||||
Continue
|
||||
</button>
|
||||
<button
|
||||
onClick={open(0)}
|
||||
title="Play from the beginning"
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium bg-white/15 text-white backdrop-blur-sm hover:bg-white/25 transition"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Restart
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={open(null)}
|
||||
title="Play"
|
||||
className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-accent text-accent-fg shadow-lg hover:scale-105 transition"
|
||||
>
|
||||
<Play className="w-5 h-5 fill-current translate-x-[1px]" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Resume progress bar (sits at the very bottom, under the duration badge). */}
|
||||
{pct > 0 && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-1 bg-black/40">
|
||||
<div className="h-full bg-accent" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
|
@ -148,7 +211,7 @@ function VideoCard({
|
|||
view: "grid" | "list";
|
||||
onState: (id: string, status: string) => void;
|
||||
onChannelFilter?: (channelId: string, channelName: string) => void;
|
||||
onOpen?: (v: Video) => void;
|
||||
onOpen?: (v: Video, startAt?: number | null) => void;
|
||||
}) {
|
||||
const watched = video.status === "watched";
|
||||
const meta = (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue