import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { qk } from "../lib/queryKeys"; import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors, type DragEndEvent, } from "@dnd-kit/core"; import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { AlertTriangle, Check, ExternalLink, GripVertical, ListPlus, Pencil, Play, RefreshCw, RotateCcw, Trash2, Upload, Youtube, } from "lucide-react"; import { api, HttpError, type Video } from "../lib/api"; import { notify } from "../lib/notifications"; import { notifyYouTubeActionError } from "../lib/youtubeErrors"; import { playlistName } from "../lib/playlistName"; import { useUndoable } from "../lib/useUndoable"; import { cycleSort, type SortState } from "../lib/columnSort"; import { formatRuntime } from "../lib/format"; import { applyView, sameOrder } from "../lib/playlistView"; const PlayerModal = lazy(() => import("./PlayerModal")); import UndoToolbar from "./UndoToolbar"; import { PageToolbar } from "./PageShell"; import { useChoice, useConfirm } from "./ConfirmProvider"; import { usePlaylists, usePlaylistsActions } from "./PlaylistsProvider"; import { useMe } from "../lib/useMe"; import { ColumnHead, alignClass, filterRows, isActive, type Column, type FilterMap, } from "./tableHeader"; import { GRIP_KEY, playlistColumns } from "./playlistColumns"; // The playlist detail is a HYBRID table (E4 S4): real sortable/filterable column headers over a // body that is still a drag-reorderable @dnd-kit list. // // The distinction that shapes the whole module: **the column sort and the filters are a VIEW**, // while the playlist's own order is stored data. Before S4 the sort control wrote straight through // to the server, so picking "sort by title" silently rewrote the playlist and marked a // YouTube-linked mirror dirty. Now nothing is written until the user presses "Save this order", // which still goes through the undoable path — the capability is intact, it just stopped being a // side effect of looking at the list. // // It composes `tableHeader`'s pieces instead of rendering a `DataTable`, because DataTable owns // its own filter/sort/page state and the stored order needs a single owner. // The neutral view: "#" ascending IS the stored order, so the header can show a real arrow for it // instead of the list looking unsorted (or, worse, looking sorted by whatever column the stored // order happens to correlate with — the user read a channel-grouped playlist as sorted by Channel). const STORED_SORT: SortState = { key: "pos", dir: "asc" }; const isStoredSort = (s: SortState) => s?.key === "pos" && s.dir === "asc"; const idsKey = (items: Video[]) => items .map((v) => v.id) .sort() .join(","); function Row({ video, columns, dragDisabled, dragHint, }: { video: Video; columns: Column