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, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { Check, FolderInput, History, ListPlus, Play, X, Youtube } from "lucide-react"; import { api, type Playlist, type PlaylistDetail, type Video } from "../lib/api"; import { notify } from "../lib/notifications"; import { notifyActionError } from "../lib/actionErrors"; import { notifyYouTubeActionError } from "../lib/youtubeErrors"; import { playlistName } from "../lib/playlistName"; import { reinsertAt } from "../lib/reinsert"; 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 { Button } from "./ui/Button"; import { LoadingState, StateMessage } from "./ui/QueryState"; import { type ActionMenuItem } from "./ui/ActionMenu"; import { useComfortPin, pinPlaylist, pinVideo } from "./comfortPin"; import { ContextMenu } from "./ui/ContextMenu"; import { useContextMenu } from "./ui/useContextMenu"; import { usePageMenu } from "./ui/pageMenuSlot"; import { menuGroups } from "./ui/menuGroups"; import UndoToolbar from "./ui/UndoToolbar"; import NavGrid from "./ui/NavGrid"; import { PageToolbar } from "./ui/PageShell"; import { usePlaylistActions } from "./usePlaylistActions"; import { usePlaylists } from "./playlistsContext"; import { usePref } from "../lib/usePref"; import { navItemAttrs } from "../lib/listNav"; import { ColumnHead } from "./ui/tableHeader"; import { alignClass } from "./ui/tableAlign"; import { columnHideClass, filterRows, isActive, type Column, type FilterMap, } from "../lib/tableColumns"; import { GRIP_KEY, playlistColumns } from "./playlistColumns"; import { useSortableA11y, useSortableItem } from "./ui/sortableA11y"; import { DragHandle } from "./ui/DragHandle"; import { useOrderItems } from "./ui/useOrderItems"; import { Input } from "./ui/Input"; import { CheckRow } from "./ui/NativeControls"; import { useReturnFocus } from "./ui/useReturnFocus"; import { MoreButton } from "./ui/MoreButton"; import { HiddenFilterCue } from "./ui/HiddenFilterCue"; import { useCopyItems } from "./copyItems"; // 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, index, columns, dragDisabled, dragHint, menuProps, }: { video: Video; /** Its position among the rows shown — the row is a keyboard item (lib/listNav). */ index: number; columns: Column