import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { DndContext, PointerSensor, closestCenter, useSensor, useSensors, type DragEndEvent, } from "@dnd-kit/core"; import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { Check, GripVertical, ListPlus, Pencil, Play, Plus, Trash2, X, } from "lucide-react"; import { api, type Playlist, type Video } from "../lib/api"; import { formatDuration } from "../lib/format"; import PlayerModal from "./PlayerModal"; function Row({ video, index, onPlay, onRemove, }: { video: Video; index: number; onPlay: () => void; onRemove: () => void; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: video.id }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.6 : 1, }; return (
{index + 1} {video.duration_seconds != null && ( {formatDuration(video.duration_seconds)} )}
); } export default function Playlists() { const { t } = useTranslation(); const qc = useQueryClient(); const [selectedId, setSelectedId] = useState(null); const [newName, setNewName] = useState(""); const [renaming, setRenaming] = useState(false); const [renameValue, setRenameValue] = useState(""); const [items, setItems] = useState([]); const [active, setActive] = useState<{ video: Video; startAt: number | null } | null>(null); const listQuery = useQuery({ queryKey: ["playlists"], queryFn: () => api.playlists() }); const playlists = listQuery.data ?? []; // Default the selection to the first playlist once the list loads. useEffect(() => { if (selectedId == null && playlists.length) setSelectedId(playlists[0].id); }, [playlists, selectedId]); const detailQuery = useQuery({ queryKey: ["playlist", selectedId], queryFn: () => api.playlist(selectedId as number), enabled: selectedId != null, }); const detail = detailQuery.data; useEffect(() => { if (detail) setItems(detail.items); }, [detail]); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 4 } }) ); const createMut = useMutation({ mutationFn: (name: string) => api.createPlaylist(name), onSuccess: (pl: Playlist) => { setNewName(""); qc.invalidateQueries({ queryKey: ["playlists"] }); setSelectedId(pl.id); }, }); function refreshAll() { qc.invalidateQueries({ queryKey: ["playlists"] }); qc.invalidateQueries({ queryKey: ["playlist", selectedId] }); } async function onDragEnd(e: DragEndEvent) { const { active: a, over } = e; if (!over || a.id === over.id || selectedId == null) return; const oldIndex = items.findIndex((v) => v.id === a.id); const newIndex = items.findIndex((v) => v.id === over.id); if (oldIndex < 0 || newIndex < 0) return; const next = arrayMove(items, oldIndex, newIndex); setItems(next); await api.reorderPlaylist(selectedId, next.map((v) => v.id)); qc.invalidateQueries({ queryKey: ["playlist", selectedId] }); } async function removeItem(videoId: string) { if (selectedId == null) return; setItems((cur) => cur.filter((v) => v.id !== videoId)); await api.removeFromPlaylist(selectedId, videoId); refreshAll(); } async function saveRename() { if (selectedId == null) return; const name = renameValue.trim(); setRenaming(false); if (name && name !== detail?.name) { await api.renamePlaylist(selectedId, name); refreshAll(); } } async function deletePlaylist() { if (selectedId == null || !detail) return; if (!window.confirm(t("playlists.confirmDelete", { name: detail.name }))) return; await api.deletePlaylist(selectedId); setSelectedId(null); qc.invalidateQueries({ queryKey: ["playlists"] }); } function playerState(id: string, status: string) { api.setState(id, status).then(() => { qc.invalidateQueries({ queryKey: ["playlist", selectedId] }); qc.invalidateQueries({ queryKey: ["feed"] }); }); } return (
{selectedId == null || !detail ? (
{selectedId == null ? t("playlists.pickOne") : t("playlists.loading")}
) : ( <>
{items[0]?.thumbnail_url && ( )}
{renaming ? ( setRenameValue(e.target.value)} onBlur={saveRename} onKeyDown={(e) => { if (e.key === "Enter") saveRename(); if (e.key === "Escape") setRenaming(false); }} className="text-lg font-semibold bg-card border border-border rounded-md px-2 py-0.5 outline-none focus:border-accent" /> ) : (

{detail.name}

)}
{t("playlists.itemCount", { count: items.length })}
{items.length === 0 ? (
{t("playlists.empty")}
) : ( v.id)} strategy={verticalListSortingStrategy} >
{items.map((v, i) => ( setActive({ video: v, startAt: null })} onRemove={() => removeItem(v.id)} /> ))}
)} )}
{active && ( setActive(null)} onState={playerState} /> )}
); }