Files
2026-05-26 22:46:00 +02:00

145 lines
6.0 KiB
TypeScript

"use client";
import { useEffect, useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { ListVideo, Eye, Trash2, X, Loader2 } from "lucide-react";
import { useWatchQueue } from "./WatchQueueProvider";
import { fetchQueueCovers } from "@/app/actions/queue";
import { ImageCard, type CardImage } from "@/components/grid/ImageCard";
import { bulkSetWatched } from "@/app/actions/bulk";
export function QueueView() {
const router = useRouter();
const queue = useWatchQueue();
const [covers, setCovers] = useState<CardImage[]>([]);
const [loading, setLoading] = useState(true);
const [pending, start] = useTransition();
// Re-fetch when the queue id list changes. Cheap — single query keyed by IN(...).
useEffect(() => {
let live = true;
setLoading(true);
fetchQueueCovers(queue.ids).then((c) => {
if (!live) return;
setCovers(c);
setLoading(false);
});
return () => { live = false; };
}, [queue.ids]);
// If a server-side delete or watched flip means a queued id no longer
// resolves to a cover, prune it locally so the count stays honest.
useEffect(() => {
if (loading) return;
const present = new Set(covers.map((c) => c.id));
const stale = queue.ids.filter((id) => !present.has(id));
if (stale.length > 0) queue.removeMany(stale);
}, [loading, covers, queue]);
function markAllWatched() {
if (covers.length === 0) return;
if (!confirm(`Mark all ${covers.length} covers as watched and clear the queue?`)) return;
const ids = covers.map((c) => c.id);
start(async () => {
await bulkSetWatched(ids, true);
queue.removeMany(ids);
router.refresh();
});
}
function clearQueue() {
if (queue.ids.length === 0) return;
if (!confirm("Clear the watch queue? Covers themselves are not affected.")) return;
queue.clear();
}
return (
<>
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-semibold tracking-tight flex items-center gap-2">
<ListVideo className="w-7 h-7 text-[var(--color-cyan)]" />
Watch queue
</h1>
<p className="text-[var(--color-fg-dim)] mt-1">
{queue.ids.length === 0
? "Empty — right-click any cover and choose “Add to queue”."
: `${covers.length} cover${covers.length === 1 ? "" : "s"} queued. Marking watched removes them automatically.`}
</p>
</div>
{queue.ids.length > 0 && (
<div className="flex items-center gap-2">
<button
onClick={markAllWatched}
disabled={pending}
className="inline-flex items-center gap-1.5 text-sm px-3 py-1.5 rounded-lg bg-[var(--color-mint)]/15 text-[var(--color-mint)] border border-[var(--color-mint)]/40 hover:bg-[var(--color-mint)]/25 disabled:opacity-50"
>
{pending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
Mark All Watched ({covers.length})
</button>
<button
onClick={clearQueue}
className="inline-flex items-center gap-1.5 text-sm px-3 py-1.5 rounded-lg glass glass-hover text-[var(--color-fg-dim)] hover:text-[var(--color-coral)]"
>
<X className="w-4 h-4" />
Clear Queue
</button>
</div>
)}
</div>
{loading ? (
<div className="glass rounded-2xl p-card text-center text-[var(--color-fg-dim)]">
<Loader2 className="w-6 h-6 mx-auto animate-spin mb-label" />
Loading queue
</div>
) : covers.length === 0 ? (
<div className="glass rounded-2xl p-card text-center">
<ListVideo className="w-8 h-8 mx-auto text-[var(--color-fg-dim)] mb-label" />
<p className="text-[var(--color-fg-dim)]">
{queue.ids.length > 0
? "Queue items couldn't be resolved (covers may have been deleted)."
: "Queue is empty."}
</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{covers.map((c) => (
<div key={c.id} className="relative group/queue">
<ImageCard image={c} />
<div className="absolute inset-0 z-20 rounded-2xl bg-black/80 opacity-0 group-hover/queue:opacity-100 transition-opacity grid place-items-center cursor-default">
<div className="grid grid-cols-2 rounded-full overflow-hidden shadow-2xl border border-white/10" style={{ width: "min(86%, 420px)" }}>
<button
type="button"
onClick={(e) => {
e.preventDefault(); e.stopPropagation();
start(async () => {
await bulkSetWatched([c.id], true);
queue.remove(c.id);
router.refresh();
});
}}
title="Mark as watched (removes from queue)"
className="inline-flex items-center justify-center gap-2 px-4 py-2.5 bg-[var(--color-mint)]/90 hover:bg-[var(--color-mint)] text-black font-semibold text-sm cursor-pointer"
>
<Eye className="w-4 h-4" />
Mark As Watched
</button>
<button
type="button"
onClick={(e) => { e.preventDefault(); e.stopPropagation(); queue.remove(c.id); }}
title="Remove from queue"
className="inline-flex items-center justify-center gap-2 px-4 py-2.5 bg-[var(--color-coral)]/90 hover:bg-[var(--color-coral)] text-black font-semibold text-sm cursor-pointer border-l border-black/20"
>
<Trash2 className="w-4 h-4" />
Remove From Queue
</button>
</div>
</div>
</div>
))}
</div>
)}
</>
);
}