import { lazy, Suspense, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { useLatest, useStableCallback } from "../../lib/useLatest"; import { useTranslation } from "react-i18next"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { qk } from "../../lib/queryKeys"; import { addPlaylistItem, movePlaylistItem, removePlaylistItem, writeVideoState, } from "../../features/video/videoWrites"; 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 } from "lucide-react"; import { YouTubeIcon } from "../../lib/brandIcons"; import { api, type Playlist, type PlaylistDetail, type Video, type VideoStatus, } from "../../lib/api"; import { msg, notify, raw } from "../../lib/notifications"; import { notifyActionError } from "../../lib/actionErrors"; import { notifyYouTubeActionError } from "../../lib/youtubeErrors"; import { playlistName, playlistNameText } from "../../lib/playlistName"; import { reinsertAt } from "../../lib/reinsert"; import { useUndoable } from "../../lib/useUndoable"; import { cycleSort, type SortState } from "../../lib/columnSort"; import { useUrlMirror } from "../../lib/useUrlMirror"; import { isStoredSort, PLAYLIST_STORED_SORT, playlistViewFrom, playlistViewParamUpdates, } from "../../lib/playlistsUrlState"; import { formatRuntime } from "../../lib/format"; import { applyView, sameOrder } from "../../lib/playlistView"; import { useOpenLinkedPlayer } from "../../features/player/useOpenLinkedPlayer"; const PlayerModal = lazy(() => import("../../features/player/PlayerModal")); import { Button } from "../../components/ui/Button"; import { LoadingState, StateMessage } from "../../components/ui/QueryState"; import { type ActionMenuItem } from "../../components/ui/ActionMenu"; import { useComfortPin, pinPlaylist, pinVideo } from "../../features/comfort/comfortPin"; import { ContextMenu } from "../../components/ui/ContextMenu"; import { useContextMenu } from "../../components/ui/useContextMenu"; import { usePageMenu } from "../../components/ui/pageMenuSlot"; import { menuGroups } from "../../components/ui/menuGroups"; import UndoToolbar from "../../components/ui/UndoToolbar"; import NavGrid from "../../components/ui/NavGrid"; import { PageToolbar } from "../../components/ui/PageShell"; import { usePlaylistActions } from "./usePlaylistActions"; import { usePlaylists } from "./playlistsContext"; import { railDefaultId } from "./playlistsInit"; import { usePref } from "../../lib/usePref"; import { navItemAttrs } from "../../lib/listNav"; import { ColumnHead } from "../../components/ui/tableHeader"; import { alignClass } from "../../components/ui/tableAlign"; import { columnHideClass, filterRows, isActive, type Column, type FilterMap, } from "../../lib/tableColumns"; import { GRIP_KEY, playlistColumns } from "./playlistColumns"; import { useSortableA11y, useSortableItem } from "../../components/ui/sortableA11y"; import { DragHandle } from "../../components/ui/DragHandle"; import { useOrderItems } from "../../components/ui/useOrderItems"; import { Input } from "../../components/ui/Input"; import { CheckRow } from "../../components/ui/NativeControls"; import { useReturnFocus } from "../../components/ui/useReturnFocus"; import { MoreButton } from "../../components/ui/MoreButton"; import { HiddenFilterCue } from "../../components/ui/HiddenFilterCue"; import { PageHeading } from "../../components/ui/Headings"; import { useCopyItems } from "../../components/ui/copyItems"; import RemoteImage from "../../components/ui/RemoteImage"; import PlaylistsHeader from "./PlaylistsHeader"; // The playlist detail is a HYBRID table: 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. At first 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, the stored order) lives beside the address-bar codec of the view. const STORED_SORT = PLAYLIST_STORED_SORT; 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