import { lazy, Suspense, useCallback, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore, } from "react"; import { qk } from "../lib/queryKeys"; import { createPortal } from "react-dom"; import { useTranslation } from "react-i18next"; import { plexLang } from "../i18n"; import { exposeVideoToBackgroundPlay } from "../lib/browserBackgroundPlay"; import { useFocusKeeper } from "../lib/useFocusKeeper"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { ArrowLeft, Clock, Download, Gauge, Info, Keyboard, Languages, Loader2, Maximize, Minimize, MoreHorizontal, Pause, Play, Settings, SkipBack, SkipForward, Volume2, VolumeX, } from "lucide-react"; import { api, HttpError, type PlexTrackNaming } from "../lib/api"; import { notifyActionError } from "../lib/actionErrors"; import { useLatest } from "../lib/useLatest"; import { seekableEndOf } from "../lib/seekableEnd"; import { audioOrdForLang, clockParts, DEFAULT_PREFS, fmt, subOrdForLang, subShadowCss, type PlexPlayerPrefs, } from "../lib/plexPlayerPrefs"; import { Ctrl, PanelHead } from "./plexPlayerControls"; import { PlexPlayerSettings } from "./PlexPlayerSettings"; import { PlexStatsOverlay } from "./PlexStatsOverlay"; import { createFragMeter } from "../lib/plexStats"; import { HlsEngine } from "../lib/hlsEngine"; import { createHlsPort } from "../lib/hlsPort"; import { enabled, log } from "../lib/log"; import { logMediaElement } from "../lib/mediaLog"; import { useMediaSession } from "../lib/mediaSession"; import { UpNextCard } from "./UpNextCard"; import Toaster from "./ui/Toaster"; import { useOverlayOrder } from "./ui/overlayOrder"; import { useAutoSkip, useControlsVisibility, useFullscreen, usePlayerVolume, usePlexProgress, usePlexTracks, useWallClock, } from "../lib/plexPlayerHooks"; import { fullscreenElement } from "../lib/fullscreen"; import { dispatch, isActivationTarget, isTypingTarget, toggleKeyboardHelp, useHelpConditions, useKeyboardHelpOpen, useKeyScope, } from "../lib/keymap"; import { LS, useAccountPersistedObject } from "../lib/storage"; import { useDismiss } from "../lib/useDismiss"; import { useScrollFade } from "../lib/useScrollFade"; import { useBackToClose } from "../lib/history"; import { trapTab } from "./ui/dialogFocus"; import { pushLayer, removeLayer, anyLayerAbove, anyBlockingLayerAbove, escapeHandled, } from "../lib/layerStack"; import { Button } from "./ui/Button"; import { IconButton } from "./ui/IconButton"; import { MenuCheck } from "./ui/MenuCheck"; import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover"; import { closeThenAct, useAnchoredPopover } from "./ui/anchoredPopoverHooks"; import { Slider } from "./ui/NativeControls"; import { formatUnit } from "../lib/format"; import { trackLabel } from "../lib/vocab"; // The rich info overlay (poster/cast/ratings) reuses the same component as the card's info page. const PlexInfo = lazy(() => import("./PlexInfo")); // C5: Back restarts the item after this many seconds in, and steps to the previous one before it. const BACK_TO_PREV_S = 3; // C5: a touch double-tap on the picture's left or right third skips this far; each further tap // inside the window skips again (the keyboard keeps ← / → at the viewer's own step). // The player's menus sit on the always-dark media chrome, under white text, in every theme. const PLAYER_MENU = "rounded-xl border border-white/15 bg-media-chrome/95 text-sm text-white shadow-2xl"; const PLAYER_ROW = "block w-full rounded-lg px-2 py-1.5 text-left hover:bg-white/10"; // A track choice: the chosen one wears the tick in the leading slot (DESIGN "one of several"), which // the highlight's white text cannot wash out the way it did the accent-coloured label. const TRACK_ROW = "flex w-full min-h-9 items-center gap-2 rounded-lg px-2 py-1.5 text-left hover:bg-white/10"; const TAP_SKIP_S = 10; const DOUBLE_TAP_MS = 300; // The Plex module's rich, full-page player. Plays the LOCAL file: direct-playable files stream raw // (native