@burdenoff/microfe-hookmovies 2026.720.2 → 2026.726.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"PlayerPage.js","names":[],"sources":["../../../../src/hookmovies/pages/consumer/PlayerPage.tsx"],"sourcesContent":["/**\n * Player (Consumer)\n * @module hookmovies/pages/consumer\n *\n * A real, dependency-free HTML5 `<video>` player for a HookMovies title or a\n * specific episode. Routes:\n * /hookmovies/watch/:titleId\n * /hookmovies/watch/:titleId/episode/:episodeId\n *\n * Behaviour:\n * - Playback is GATED by `useCheckEntitlement(titleId)`. When the caller is not\n * entitled we render an honest \"where to watch\" panel that links back to the\n * title page — we never fabricate a player.\n * - When entitled, the `<video>` source is a presigned wspace-files URL resolved\n * from the title's `videoFileId` (movies) — see the note on episode sources\n * below. `.m3u8` sources rely on the browser's native HLS support (no player\n * library); everything else is a progressive MP4.\n * - Real subtitle / caption `<track>` elements are built from\n * `useLocalizations(titleId, episodeId)` (each track's `src` is a presigned\n * URL for its `fileId`); the default track is pre-selected.\n * - Custom, fully keyboard-accessible controls (no native chrome): play/pause,\n * a crimson (`action-primary`) scrubber, volume, a real subtitle/audio menu,\n * fullscreen and back. Cast / offline / quality are surfaced but disabled with\n * an honest tooltip because this player does not back them.\n * - Resume + heartbeat: on load we seek to `useMyWatchProgress(titleId)` and we\n * throttle `useRecordProgress` (~10s) while playing, marking `completed` near\n * the end and on `ended`, and saving once more on unmount / pause.\n *\n * NOTE on episode sources: the title-detail data layer (`useTitle`) exposes\n * episode summaries WITHOUT a media file reference, so a per-episode video URL\n * cannot be resolved from the current hooks. Rather than play the wrong asset,\n * the episode route shows the episode's context and an honest \"no video attached\"\n * state until the data layer surfaces an episode `fileId`.\n */\n\nimport { type FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n AlertCircle,\n ArrowLeft,\n Cast,\n Check,\n Download,\n Film,\n Loader2,\n Lock,\n Maximize,\n Minimize,\n Pause,\n Play,\n RotateCw,\n Settings,\n Subtitles,\n Volume1,\n Volume2,\n VolumeX,\n} from 'lucide-react';\n\nimport { useHookMovies } from '../../context/HookMoviesContext';\nimport {\n useCheckEntitlement,\n useLocalizations,\n useMyWatchProgress,\n useRecordProgress,\n useTitle,\n type HokLocalization,\n} from '../../hooks/useHookMoviesApi';\nimport { useFileUrl } from '../../hooks/useFiles';\n\n// ============================================================================\n// Small helpers\n// ============================================================================\n\n/** Seconds → `m:ss` or `h:mm:ss`. Non-finite (live / unknown) → `--:--`. */\nfunction formatTime(seconds: number): string {\n if (!Number.isFinite(seconds) || seconds < 0) return '--:--';\n const total = Math.floor(seconds);\n const h = Math.floor(total / 3600);\n const m = Math.floor((total % 3600) / 60);\n const s = total % 60;\n const two = (n: number) => n.toString().padStart(2, '0');\n return h > 0 ? `${h}:${two(m)}:${two(s)}` : `${m}:${two(s)}`;\n}\n\n/** ISO 639 code → human language name, with a graceful fallback. */\nfunction languageName(code: string): string {\n const trimmed = code.trim();\n if (!trimmed) return 'Unknown';\n try {\n const dn = new Intl.DisplayNames(['en'], { type: 'language' });\n return dn.of(trimmed) ?? trimmed.toUpperCase();\n } catch {\n return trimmed.toUpperCase();\n }\n}\n\nconst SEEK_STEP_SECONDS = 5;\nconst VOLUME_STEP = 0.1;\nconst SAVE_THROTTLE_MS = 10_000;\n/** Treat playback within this many seconds of the end as \"completed\". */\nconst COMPLETE_TAIL_SECONDS = 15;\n\n// ============================================================================\n// Subtitle track — resolves one localization's presigned URL into a <track>.\n// Rendered once per subtitle/caption localization (always present so the\n// video's `textTracks` indices stay aligned with `subtitleLocs`).\n// ============================================================================\n\nconst SubtitleTrack: FC<{ localization: HokLocalization }> = ({ localization }) => {\n const { data: url } = useFileUrl(localization.fileId);\n const label =\n localization.kind === 'CAPTION'\n ? `${languageName(localization.language)} (CC)`\n : languageName(localization.language);\n return (\n <track\n kind={localization.kind === 'CAPTION' ? 'captions' : 'subtitles'}\n label={label}\n srcLang={localization.language}\n src={url ?? undefined}\n default={localization.isDefault}\n />\n );\n};\n\n// ============================================================================\n// Player\n// ============================================================================\n\nexport const PlayerPage: FC = () => {\n const { titleId, episodeId } = useParams<{ titleId: string; episodeId?: string }>();\n const { navigateTo } = useHookMovies();\n\n // ── Data layer (all hooks unconditional; early returns happen after) ───────\n const { data: title, isLoading: titleLoading, isError: titleError, refetch } = useTitle(titleId);\n const entitlement = useCheckEntitlement(titleId);\n const localizations = useLocalizations(titleId, episodeId);\n const watchProgress = useMyWatchProgress(titleId);\n const recordProgress = useRecordProgress();\n\n const activeEpisode = useMemo(\n () => (episodeId ? (title?.episodes.find((e) => e.id === episodeId) ?? null) : null),\n [episodeId, title]\n );\n\n // Movies play the title's `videoFileId`. Episodes have no exposed media ref\n // (see file header) — so no source can be resolved for the episode route.\n const sourceFileId = episodeId ? undefined : (title?.videoFileId ?? undefined);\n const sourceUrlQuery = useFileUrl(sourceFileId);\n const posterUrlQuery = useFileUrl(title?.posterFileId ?? undefined);\n const backdropUrlQuery = useFileUrl(title?.backdropFileId ?? undefined);\n\n const subtitleLocs = useMemo(\n () => (localizations.data ?? []).filter((l) => l.kind === 'SUBTITLE' || l.kind === 'CAPTION'),\n [localizations.data]\n );\n const dubLocs = useMemo(\n () => (localizations.data ?? []).filter((l) => l.kind === 'DUB'),\n [localizations.data]\n );\n\n // ── Player element + UI state ──────────────────────────────────────────────\n const videoRef = useRef<HTMLVideoElement | null>(null);\n const containerRef = useRef<HTMLDivElement | null>(null);\n\n const [isPlaying, setIsPlaying] = useState(false);\n const [currentTime, setCurrentTime] = useState(0);\n const [duration, setDuration] = useState(0);\n const [volume, setVolume] = useState(1);\n const [muted, setMuted] = useState(false);\n const [buffering, setBuffering] = useState(false);\n const [metadataLoaded, setMetadataLoaded] = useState(false);\n const [playbackError, setPlaybackError] = useState(false);\n const [isFullscreen, setIsFullscreen] = useState(false);\n const [controlsVisible, setControlsVisible] = useState(true);\n const [openMenu, setOpenMenu] = useState<'none' | 'subtitles' | 'settings'>('none');\n const [selectedSubIdx, setSelectedSubIdx] = useState(-1);\n\n // ── Refs for values consumed inside stable callbacks / cleanup ─────────────\n const lastSaveRef = useRef(0);\n const resumeAppliedRef = useRef(false);\n const defaultSubAppliedRef = useRef(false);\n const hideTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const recordRef = useRef(recordProgress.mutate);\n recordRef.current = recordProgress.mutate;\n const isPlayingRef = useRef(isPlaying);\n isPlayingRef.current = isPlaying;\n\n const sourceUrl = sourceFileId ? (sourceUrlQuery.data ?? null) : null;\n const isHls = Boolean(sourceUrl && sourceUrl.split('?')[0]?.toLowerCase().endsWith('.m3u8'));\n const hasTextTracks = subtitleLocs.length > 0;\n const isLive = !Number.isFinite(duration) || duration === 0;\n\n // ── Progress persistence ───────────────────────────────────────────────────\n const saveProgress = useCallback(\n (completed: boolean) => {\n const v = videoRef.current;\n if (!v || !titleId) return;\n const pos = Math.floor(v.currentTime);\n if (pos <= 0 && !completed) return;\n recordRef.current({\n titleId,\n episodeId,\n positionSeconds: pos,\n completed,\n lastDevice: 'web',\n });\n lastSaveRef.current = Date.now();\n },\n [titleId, episodeId]\n );\n\n // Save once more when leaving the player (covers navigation / tab close).\n useEffect(() => {\n return () => {\n if (isPlayingRef.current) saveProgress(false);\n };\n }, [saveProgress]);\n\n // ── Resume: seek to the caller's saved position once metadata + progress are\n // both settled (runs a single time). ────────────────────────────────────\n useEffect(() => {\n const v = videoRef.current;\n if (!v || resumeAppliedRef.current || !metadataLoaded || watchProgress.isLoading) return;\n resumeAppliedRef.current = true;\n const match = (watchProgress.data ?? []).find((p) =>\n episodeId ? p.episodeId === episodeId : !p.episodeId\n );\n if (\n match &&\n !match.completed &&\n match.positionSeconds > SEEK_STEP_SECONDS &&\n Number.isFinite(v.duration) &&\n match.positionSeconds < v.duration - SEEK_STEP_SECONDS\n ) {\n v.currentTime = match.positionSeconds;\n setCurrentTime(match.positionSeconds);\n }\n }, [metadataLoaded, watchProgress.isLoading, watchProgress.data, episodeId]);\n\n // ── Pre-select the default subtitle track once localizations arrive. ───────\n useEffect(() => {\n if (defaultSubAppliedRef.current || subtitleLocs.length === 0) return;\n defaultSubAppliedRef.current = true;\n setSelectedSubIdx(subtitleLocs.findIndex((l) => l.isDefault));\n }, [subtitleLocs]);\n\n // ── Apply the selected text-track mode to the media element. ───────────────\n const applyTextTracks = useCallback(() => {\n const v = videoRef.current;\n if (!v) return;\n const tracks = v.textTracks;\n for (let i = 0; i < tracks.length; i += 1) {\n tracks[i].mode = i === selectedSubIdx ? 'showing' : 'disabled';\n }\n }, [selectedSubIdx]);\n\n useEffect(() => {\n applyTextTracks();\n }, [applyTextTracks, subtitleLocs.length, metadataLoaded]);\n\n // ── Fullscreen state sync ──────────────────────────────────────────────────\n useEffect(() => {\n const onChange = () => setIsFullscreen(Boolean(document.fullscreenElement));\n document.addEventListener('fullscreenchange', onChange);\n return () => document.removeEventListener('fullscreenchange', onChange);\n }, []);\n\n // ── Controls auto-hide ─────────────────────────────────────────────────────\n const revealControls = useCallback(() => {\n setControlsVisible(true);\n if (hideTimerRef.current) clearTimeout(hideTimerRef.current);\n hideTimerRef.current = setTimeout(() => {\n if (isPlayingRef.current && openMenu === 'none') setControlsVisible(false);\n }, 3000);\n }, [openMenu]);\n\n useEffect(() => {\n return () => {\n if (hideTimerRef.current) clearTimeout(hideTimerRef.current);\n };\n }, []);\n\n // ── Media element imperatives ──────────────────────────────────────────────\n const togglePlay = useCallback(() => {\n const v = videoRef.current;\n if (!v) return;\n if (v.paused) {\n void v.play().catch(() => setPlaybackError(true));\n } else {\n v.pause();\n }\n }, []);\n\n const seekBy = useCallback((delta: number) => {\n const v = videoRef.current;\n if (!v || !Number.isFinite(v.duration)) return;\n v.currentTime = Math.min(Math.max(v.currentTime + delta, 0), v.duration);\n setCurrentTime(v.currentTime);\n }, []);\n\n const seekTo = useCallback((value: number) => {\n const v = videoRef.current;\n if (!v) return;\n v.currentTime = value;\n setCurrentTime(value);\n }, []);\n\n const changeVolume = useCallback((value: number) => {\n const v = videoRef.current;\n if (!v) return;\n const clamped = Math.min(Math.max(value, 0), 1);\n v.volume = clamped;\n v.muted = clamped === 0;\n setVolume(clamped);\n setMuted(clamped === 0);\n }, []);\n\n const toggleMute = useCallback(() => {\n const v = videoRef.current;\n if (!v) return;\n const next = !v.muted;\n v.muted = next;\n setMuted(next);\n }, []);\n\n const toggleFullscreen = useCallback(() => {\n const el = containerRef.current;\n if (!el) return;\n if (!document.fullscreenElement) {\n void el.requestFullscreen?.().catch(() => undefined);\n } else {\n void document.exitFullscreen?.().catch(() => undefined);\n }\n }, []);\n\n // ── Keyboard controls (space / arrows / f / m) ─────────────────────────────\n const onKeyDown = useCallback(\n (e: React.KeyboardEvent<HTMLDivElement>) => {\n // Don't hijack typing in the (rare) focusable control.\n const target = e.target as HTMLElement;\n if (target.tagName === 'INPUT' || target.tagName === 'BUTTON') {\n if (e.key !== ' ' && e.key !== 'k') return;\n }\n switch (e.key) {\n case ' ':\n case 'k':\n e.preventDefault();\n togglePlay();\n revealControls();\n break;\n case 'ArrowRight':\n e.preventDefault();\n seekBy(SEEK_STEP_SECONDS);\n revealControls();\n break;\n case 'ArrowLeft':\n e.preventDefault();\n seekBy(-SEEK_STEP_SECONDS);\n revealControls();\n break;\n case 'ArrowUp':\n e.preventDefault();\n changeVolume(volume + VOLUME_STEP);\n revealControls();\n break;\n case 'ArrowDown':\n e.preventDefault();\n changeVolume(volume - VOLUME_STEP);\n revealControls();\n break;\n case 'f':\n case 'F':\n e.preventDefault();\n toggleFullscreen();\n break;\n case 'm':\n case 'M':\n e.preventDefault();\n toggleMute();\n revealControls();\n break;\n default:\n break;\n }\n },\n [togglePlay, seekBy, changeVolume, toggleFullscreen, toggleMute, revealControls, volume]\n );\n\n // ── Media element event handlers ───────────────────────────────────────────\n const handleTimeUpdate = useCallback(() => {\n const v = videoRef.current;\n if (!v) return;\n setCurrentTime(v.currentTime);\n if (!v.paused && Date.now() - lastSaveRef.current >= SAVE_THROTTLE_MS) {\n const nearEnd =\n Number.isFinite(v.duration) && v.currentTime >= v.duration - COMPLETE_TAIL_SECONDS;\n saveProgress(nearEnd);\n }\n }, [saveProgress]);\n\n const handleLoadedMetadata = useCallback(() => {\n const v = videoRef.current;\n if (!v) return;\n setDuration(v.duration);\n setMetadataLoaded(true);\n setPlaybackError(false);\n applyTextTracks();\n }, [applyTextTracks]);\n\n const handleEnded = useCallback(() => {\n setIsPlaying(false);\n saveProgress(true);\n }, [saveProgress]);\n\n // ── Early returns (after every hook above) ─────────────────────────────────\n const backToTitle = () => navigateTo(`title/${titleId ?? ''}`);\n\n if (titleLoading || entitlement.isLoading) {\n return (\n <PlayerStage>\n <div className=\"flex flex-col items-center gap-3 text-text-inverse\">\n <Loader2 className=\"h-8 w-8 animate-spin\" />\n <p className=\"text-sm opacity-80\">Loading…</p>\n </div>\n </PlayerStage>\n );\n }\n\n if (titleError || !title) {\n return (\n <PlayerStage>\n <StageMessage\n icon={<AlertCircle className=\"h-12 w-12\" />}\n title=\"Title unavailable\"\n message=\"We couldn't load this title. It may have been removed or you may not have access.\"\n primary={{ label: 'Retry', onClick: () => void refetch() }}\n />\n </PlayerStage>\n );\n }\n\n if (entitlement.isError) {\n return (\n <PlayerStage>\n <StageMessage\n icon={<AlertCircle className=\"h-12 w-12\" />}\n title=\"Couldn't verify access\"\n message=\"We couldn't check your entitlement for this title. Please try again.\"\n primary={{ label: 'Retry', onClick: () => void entitlement.refetch() }}\n secondary={{ label: 'Back to title', onClick: backToTitle }}\n />\n </PlayerStage>\n );\n }\n\n // Entitlement gate — no fake player when the viewer can't watch.\n if (!entitlement.data?.entitled) {\n return (\n <PlayerStage backdrop={backdropUrlQuery.data ?? posterUrlQuery.data ?? null}>\n <div className=\"mx-auto flex max-w-md flex-col items-center gap-5 text-center text-text-inverse\">\n {posterUrlQuery.data ? (\n <img\n src={posterUrlQuery.data}\n alt={`Poster for ${title.title}`}\n className=\"h-48 w-32 rounded-lg object-cover shadow-lg\"\n />\n ) : (\n <div className=\"flex h-48 w-32 items-center justify-center rounded-lg bg-bg-overlay\">\n <Film className=\"h-10 w-10 opacity-70\" />\n </div>\n )}\n <div className=\"flex items-center gap-2 rounded-full bg-bg-overlay px-3 py-1 text-xs font-medium\">\n <Lock className=\"h-3.5 w-3.5\" />\n Not available to watch yet\n </div>\n <div>\n <h1 className=\"text-xl font-semibold\">{title.title}</h1>\n <p className=\"mt-2 text-sm opacity-80\">\n You don&apos;t have an active entitlement for this title. Open the title page to see\n where and how to watch, or to acquire access.\n </p>\n </div>\n <div className=\"flex flex-wrap items-center justify-center gap-3\">\n <button\n type=\"button\"\n onClick={backToTitle}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 motion-safe:transition-opacity\"\n >\n Where to watch\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo('browse')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-text-inverse/30 px-4 py-2 text-sm font-medium text-text-inverse hover:bg-bg-overlay motion-safe:transition-colors\"\n >\n Browse catalog\n </button>\n </div>\n </div>\n </PlayerStage>\n );\n }\n\n const heading = activeEpisode\n ? `${title.title} — E${activeEpisode.number} · ${activeEpisode.name}`\n : title.title;\n\n const sourceResolving = Boolean(sourceFileId) && sourceUrlQuery.isLoading;\n const noSource = !sourceFileId || (!sourceUrlQuery.isLoading && !sourceUrl);\n\n const VolumeIcon = muted || volume === 0 ? VolumeX : volume < 0.5 ? Volume1 : Volume2;\n const progressPct = duration > 0 ? Math.min((currentTime / duration) * 100, 100) : 0;\n\n return (\n <div className=\"flex h-full flex-col bg-bg-inverse\">\n {/* Top bar */}\n <div className=\"flex items-center gap-3 px-4 py-3 sm:px-6\">\n <button\n type=\"button\"\n onClick={backToTitle}\n className=\"inline-flex items-center gap-2 rounded-lg px-2 py-1.5 text-sm font-medium text-text-inverse/90 hover:bg-bg-overlay motion-safe:transition-colors\"\n aria-label=\"Back to title\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n <span className=\"hidden sm:inline\">Back</span>\n </button>\n <h1 className=\"min-w-0 flex-1 truncate text-sm font-semibold text-text-inverse sm:text-base\">\n {heading}\n </h1>\n </div>\n\n {/* Stage */}\n <div className=\"flex flex-1 items-center justify-center px-0 sm:px-6 sm:pb-6\">\n <div\n ref={containerRef}\n className=\"group relative aspect-video w-full max-w-6xl overflow-hidden bg-bg-inverse sm:rounded-xl\"\n onMouseMove={revealControls}\n onKeyDown={onKeyDown}\n onClick={(e) => {\n if (e.target === videoRef.current) togglePlay();\n }}\n tabIndex={0}\n role=\"region\"\n aria-label={`Video player for ${heading}`}\n >\n {noSource ? (\n <div className=\"absolute inset-0 flex items-center justify-center p-6\">\n <StageMessage\n icon={<Film className=\"h-12 w-12\" />}\n title={activeEpisode ? 'Episode video not available' : 'No video attached'}\n message={\n activeEpisode\n ? 'This episode does not have a playable video file attached yet.'\n : 'This title does not have a playable video file attached yet.'\n }\n secondary={{ label: 'Back to title', onClick: backToTitle }}\n />\n </div>\n ) : (\n <>\n {/* Caption <track>s are added dynamically from localizations below. */}\n <video\n ref={videoRef}\n className=\"h-full w-full bg-bg-inverse\"\n src={sourceUrl ?? undefined}\n poster={posterUrlQuery.data ?? undefined}\n crossOrigin={hasTextTracks ? 'anonymous' : undefined}\n playsInline\n preload=\"metadata\"\n onLoadedMetadata={handleLoadedMetadata}\n onDurationChange={() => {\n const v = videoRef.current;\n if (v) setDuration(v.duration);\n }}\n onTimeUpdate={handleTimeUpdate}\n onPlay={() => {\n setIsPlaying(true);\n revealControls();\n }}\n onPause={() => {\n setIsPlaying(false);\n setControlsVisible(true);\n saveProgress(false);\n }}\n onEnded={handleEnded}\n onWaiting={() => setBuffering(true)}\n onPlaying={() => setBuffering(false)}\n onCanPlay={() => setBuffering(false)}\n onError={() => setPlaybackError(true)}\n >\n {subtitleLocs.map((loc) => (\n <SubtitleTrack key={loc.id} localization={loc} />\n ))}\n </video>\n\n {/* Buffering / resolving overlay */}\n {(buffering || sourceResolving) && !playbackError && (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center\">\n <Loader2 className=\"h-10 w-10 animate-spin text-text-inverse\" />\n </div>\n )}\n\n {/* Playback error overlay */}\n {playbackError && (\n <div className=\"absolute inset-0 flex items-center justify-center bg-overlay-scrim p-6\">\n <StageMessage\n icon={<AlertCircle className=\"h-12 w-12\" />}\n title=\"Playback failed\"\n message={\n isHls\n ? 'This stream is HLS (.m3u8) and your browser could not play it natively. Try a browser with native HLS support.'\n : 'We could not play this video. The link may have expired — reload to try again.'\n }\n primary={{\n label: 'Reload',\n onClick: () => {\n setPlaybackError(false);\n void sourceUrlQuery.refetch();\n },\n }}\n secondary={{ label: 'Back to title', onClick: backToTitle }}\n />\n </div>\n )}\n\n {/* Center play affordance when paused */}\n {!isPlaying && !buffering && !playbackError && metadataLoaded && (\n <button\n type=\"button\"\n onClick={togglePlay}\n aria-label=\"Play\"\n className=\"absolute inset-0 flex items-center justify-center\"\n >\n <span className=\"flex h-16 w-16 items-center justify-center rounded-full bg-action-primary-bg text-action-primary-text shadow-lg motion-safe:transition-transform group-hover:scale-105\">\n <Play className=\"ml-1 h-7 w-7\" />\n </span>\n </button>\n )}\n\n {/* Control bar */}\n <div\n className={`absolute inset-x-0 bottom-0 bg-overlay-scrim px-3 pb-3 pt-10 motion-safe:transition-opacity motion-safe:duration-300 sm:px-4 ${\n controlsVisible || !isPlaying ? 'opacity-100' : 'pointer-events-none opacity-0'\n }`}\n >\n {/* Scrubber */}\n <div className=\"mb-2 flex items-center gap-3\">\n <span className=\"w-12 shrink-0 text-right text-xs tabular-nums text-text-inverse/90\">\n {formatTime(currentTime)}\n </span>\n <div className=\"relative flex-1\">\n <div className=\"h-1.5 w-full overflow-hidden rounded-full bg-text-inverse/25\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${progressPct}%` }}\n />\n </div>\n <input\n type=\"range\"\n min={0}\n max={Number.isFinite(duration) && duration > 0 ? duration : 0}\n step=\"any\"\n value={currentTime}\n disabled={isLive}\n onChange={(e) => seekTo(Number(e.target.value))}\n aria-label=\"Seek\"\n className=\"absolute inset-0 h-full w-full cursor-pointer opacity-0 disabled:cursor-not-allowed\"\n style={{ accentColor: 'var(--color-action-primary-bg)' }}\n />\n </div>\n <span className=\"w-12 shrink-0 text-xs tabular-nums text-text-inverse/70\">\n {isLive ? 'LIVE' : formatTime(duration)}\n </span>\n </div>\n\n {/* Buttons */}\n <div className=\"flex items-center gap-1 sm:gap-2\">\n <ControlButton label={isPlaying ? 'Pause' : 'Play'} onClick={togglePlay}>\n {isPlaying ? <Pause className=\"h-5 w-5\" /> : <Play className=\"h-5 w-5\" />}\n </ControlButton>\n\n {/* Volume */}\n <div className=\"flex items-center gap-1.5\">\n <ControlButton label={muted ? 'Unmute' : 'Mute'} onClick={toggleMute}>\n <VolumeIcon className=\"h-5 w-5\" />\n </ControlButton>\n <input\n type=\"range\"\n min={0}\n max={1}\n step={0.05}\n value={muted ? 0 : volume}\n onChange={(e) => changeVolume(Number(e.target.value))}\n aria-label=\"Volume\"\n className=\"hidden h-1 w-20 cursor-pointer sm:block\"\n style={{ accentColor: 'var(--color-action-primary-bg)' }}\n />\n </div>\n\n <div className=\"ml-auto flex items-center gap-1 sm:gap-2\">\n {/* Subtitle / audio menu */}\n <div className=\"relative\">\n <ControlButton\n label=\"Subtitles & audio\"\n onClick={() =>\n setOpenMenu((m) => (m === 'subtitles' ? 'none' : 'subtitles'))\n }\n active={selectedSubIdx >= 0}\n expanded={openMenu === 'subtitles'}\n disabled={subtitleLocs.length === 0 && dubLocs.length === 0}\n disabledHint=\"No subtitle or audio tracks available for this title.\"\n >\n <Subtitles className=\"h-5 w-5\" />\n </ControlButton>\n {openMenu === 'subtitles' && (\n <TrackMenu\n subtitleLocs={subtitleLocs}\n dubLocs={dubLocs}\n selectedSubIdx={selectedSubIdx}\n onSelectSub={(idx) => {\n setSelectedSubIdx(idx);\n setOpenMenu('none');\n }}\n />\n )}\n </div>\n\n {/* Settings (quality) — honestly disabled */}\n <ControlButton\n label=\"Quality\"\n onClick={() => undefined}\n disabled\n disabledHint=\"Adaptive quality selection isn't available for this source.\"\n >\n <Settings className=\"h-5 w-5\" />\n </ControlButton>\n\n {/* Cast — honestly disabled */}\n <ControlButton\n label=\"Cast\"\n onClick={() => undefined}\n disabled\n disabledHint=\"Casting to external devices isn't supported in this player.\"\n >\n <Cast className=\"h-5 w-5\" />\n </ControlButton>\n\n {/* Offline / download — honestly disabled */}\n <ControlButton\n label=\"Download for offline\"\n onClick={() => undefined}\n disabled\n disabledHint=\"Offline downloads aren't available for this title.\"\n >\n <Download className=\"h-5 w-5\" />\n </ControlButton>\n\n <ControlButton\n label={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}\n onClick={toggleFullscreen}\n >\n {isFullscreen ? (\n <Minimize className=\"h-5 w-5\" />\n ) : (\n <Maximize className=\"h-5 w-5\" />\n )}\n </ControlButton>\n </div>\n </div>\n </div>\n </>\n )}\n </div>\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Presentational sub-components (kept local to the player surface)\n// ============================================================================\n\n/** Full-bleed dark stage used for loading / gated / error states. */\nconst PlayerStage: FC<{ children: React.ReactNode; backdrop?: string | null }> = ({\n children,\n backdrop,\n}) => (\n <div className=\"relative flex h-full min-h-[60vh] items-center justify-center overflow-hidden bg-bg-inverse p-6\">\n {backdrop && (\n <>\n <img\n src={backdrop}\n alt=\"\"\n aria-hidden\n className=\"absolute inset-0 h-full w-full object-cover opacity-30\"\n />\n <div className=\"absolute inset-0 bg-overlay-scrim\" />\n </>\n )}\n <div className=\"relative z-10 w-full\">{children}</div>\n </div>\n);\n\ninterface StageAction {\n label: string;\n onClick: () => void;\n}\n\n/** Centered icon + copy + up to two actions, styled for the dark stage. */\nconst StageMessage: FC<{\n icon: React.ReactNode;\n title: string;\n message: string;\n primary?: StageAction;\n secondary?: StageAction;\n}> = ({ icon, title, message, primary, secondary }) => (\n <div className=\"mx-auto flex max-w-md flex-col items-center gap-4 text-center text-text-inverse\">\n <span className=\"text-text-inverse/80\">{icon}</span>\n <div>\n <h2 className=\"text-lg font-semibold\">{title}</h2>\n <p className=\"mt-1 text-sm opacity-80\">{message}</p>\n </div>\n {(primary || secondary) && (\n <div className=\"flex flex-wrap items-center justify-center gap-3\">\n {primary && (\n <button\n type=\"button\"\n onClick={primary.onClick}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 motion-safe:transition-opacity\"\n >\n <RotateCw className=\"h-4 w-4\" />\n {primary.label}\n </button>\n )}\n {secondary && (\n <button\n type=\"button\"\n onClick={secondary.onClick}\n className=\"inline-flex items-center gap-2 rounded-lg border border-text-inverse/30 px-4 py-2 text-sm font-medium text-text-inverse hover:bg-bg-overlay motion-safe:transition-colors\"\n >\n {secondary.label}\n </button>\n )}\n </div>\n )}\n </div>\n);\n\n/** A single icon control on the dark control bar. */\nconst ControlButton: FC<{\n label: string;\n onClick: () => void;\n children: React.ReactNode;\n active?: boolean;\n disabled?: boolean;\n disabledHint?: string;\n expanded?: boolean;\n}> = ({ label, onClick, children, active, disabled, disabledHint, expanded }) => (\n <button\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n aria-label={label}\n aria-pressed={active}\n aria-expanded={expanded}\n title={disabled && disabledHint ? disabledHint : label}\n className={`relative rounded-lg p-2 motion-safe:transition-colors ${\n disabled ? 'cursor-not-allowed text-text-inverse/40' : 'text-text-inverse hover:bg-bg-overlay'\n }`}\n >\n {children}\n {active && !disabled && (\n <span className=\"absolute bottom-0.5 left-1/2 h-1 w-1 -translate-x-1/2 rounded-full bg-action-primary-bg\" />\n )}\n </button>\n);\n\n/** Popover listing real subtitle/caption tracks + (honest, disabled) dub tracks. */\nconst TrackMenu: FC<{\n subtitleLocs: HokLocalization[];\n dubLocs: HokLocalization[];\n selectedSubIdx: number;\n onSelectSub: (idx: number) => void;\n}> = ({ subtitleLocs, dubLocs, selectedSubIdx, onSelectSub }) => (\n <div\n role=\"menu\"\n aria-label=\"Subtitles and audio\"\n className=\"absolute bottom-full right-0 mb-2 max-h-72 w-56 overflow-y-auto rounded-lg border border-border-seam bg-bg-surface p-1.5 shadow-lg\"\n >\n <p className=\"px-2 py-1 text-[11px] font-semibold uppercase tracking-wide text-text-secondary\">\n Subtitles\n </p>\n <MenuRow label=\"Off\" selected={selectedSubIdx === -1} onClick={() => onSelectSub(-1)} />\n {subtitleLocs.map((loc, idx) => (\n <MenuRow\n key={loc.id}\n label={\n loc.kind === 'CAPTION' ? `${languageName(loc.language)} (CC)` : languageName(loc.language)\n }\n selected={selectedSubIdx === idx}\n onClick={() => onSelectSub(idx)}\n />\n ))}\n\n {dubLocs.length > 0 && (\n <>\n <p className=\"mt-1 px-2 py-1 text-[11px] font-semibold uppercase tracking-wide text-text-secondary\">\n Audio (dub)\n </p>\n {dubLocs.map((loc) => (\n <button\n key={loc.id}\n type=\"button\"\n role=\"menuitem\"\n disabled\n title=\"Alternate dub tracks are separate assets and can't be switched in this player.\"\n className=\"flex w-full cursor-not-allowed items-center justify-between rounded-md px-2 py-1.5 text-left text-sm text-text-disabled\"\n >\n <span>{languageName(loc.language)}</span>\n <Lock className=\"h-3.5 w-3.5\" />\n </button>\n ))}\n </>\n )}\n </div>\n);\n\nconst MenuRow: FC<{ label: string; selected: boolean; onClick: () => void }> = ({\n label,\n selected,\n onClick,\n}) => (\n <button\n type=\"button\"\n role=\"menuitemradio\"\n aria-checked={selected}\n onClick={onClick}\n className={`flex w-full items-center justify-between rounded-md px-2 py-1.5 text-left text-sm motion-safe:transition-colors ${\n selected\n ? 'bg-bg-sunken font-medium text-text-primary'\n : 'text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n }`}\n >\n <span className=\"truncate\">{label}</span>\n {selected && <Check className=\"h-4 w-4 shrink-0 text-action-primary-bg\" />}\n </button>\n);\n"],"mappings":"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"}
1
+ {"version":3,"file":"PlayerPage.js","names":[],"sources":["../../../../src/hookmovies/pages/consumer/PlayerPage.tsx"],"sourcesContent":["/**\n * Player (Consumer)\n * @module hookmovies/pages/consumer\n *\n * A real, dependency-free HTML5 `<video>` player for a HookMovies title or a\n * specific episode. Routes:\n * /hookmovies/watch/:titleId\n * /hookmovies/watch/:titleId/episode/:episodeId\n *\n * Behaviour:\n * - Playback is GATED by `useCheckEntitlement(titleId)`. When the caller is not\n * entitled we render an honest \"where to watch\" panel that links back to the\n * title page — we never fabricate a player.\n * - When entitled, the `<video>` source is a presigned wspace-files URL resolved\n * from the title's `videoFileId` (movies) — see the note on episode sources\n * below. `.m3u8` sources rely on the browser's native HLS support (no player\n * library); everything else is a progressive MP4.\n * - Real subtitle / caption `<track>` elements are built from\n * `useLocalizations(titleId, episodeId)` (each track's `src` is a presigned\n * URL for its `fileId`); the default track is pre-selected.\n * - Custom, fully keyboard-accessible controls (no native chrome): play/pause,\n * a crimson (`action-primary`) scrubber, volume, a real subtitle/audio menu,\n * fullscreen and back. Cast / offline / quality are surfaced but disabled with\n * an honest tooltip because this player does not back them.\n * - Resume + heartbeat: on load we seek to `useMyWatchProgress(titleId)` and we\n * throttle `useRecordProgress` (~10s) while playing, marking `completed` near\n * the end and on `ended`, and saving once more on unmount / pause.\n *\n * Movies resolve `title.videoFileId`; episodes resolve the active episode's own\n * `fileId` (from `HokEpisode.file`, surfaced by `useTitle`). If an episode has no\n * file attached yet, we show an honest \"no video attached\" state rather than\n * falling back to the movie/other-episode asset.\n */\n\nimport { type FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n AlertCircle,\n ArrowLeft,\n Cast,\n Check,\n Download,\n Film,\n Loader2,\n Lock,\n Maximize,\n Minimize,\n Pause,\n Play,\n RotateCw,\n Settings,\n Subtitles,\n Volume1,\n Volume2,\n VolumeX,\n} from 'lucide-react';\n\nimport { useHookMovies } from '../../context/HookMoviesContext';\nimport {\n useCheckEntitlement,\n useLocalizations,\n useMyWatchProgress,\n useRecordProgress,\n useTitle,\n type HokLocalization,\n} from '../../hooks/useHookMoviesApi';\nimport { useFileUrl } from '../../hooks/useFiles';\n\n// ============================================================================\n// Small helpers\n// ============================================================================\n\n/** Seconds → `m:ss` or `h:mm:ss`. Non-finite (live / unknown) → `--:--`. */\nfunction formatTime(seconds: number): string {\n if (!Number.isFinite(seconds) || seconds < 0) return '--:--';\n const total = Math.floor(seconds);\n const h = Math.floor(total / 3600);\n const m = Math.floor((total % 3600) / 60);\n const s = total % 60;\n const two = (n: number) => n.toString().padStart(2, '0');\n return h > 0 ? `${h}:${two(m)}:${two(s)}` : `${m}:${two(s)}`;\n}\n\n/** ISO 639 code → human language name, with a graceful fallback. */\nfunction languageName(code: string): string {\n const trimmed = code.trim();\n if (!trimmed) return 'Unknown';\n try {\n const dn = new Intl.DisplayNames(['en'], { type: 'language' });\n return dn.of(trimmed) ?? trimmed.toUpperCase();\n } catch {\n return trimmed.toUpperCase();\n }\n}\n\nconst SEEK_STEP_SECONDS = 5;\nconst VOLUME_STEP = 0.1;\nconst SAVE_THROTTLE_MS = 10_000;\n/** Treat playback within this many seconds of the end as \"completed\". */\nconst COMPLETE_TAIL_SECONDS = 15;\n\n// ============================================================================\n// Subtitle track — resolves one localization's presigned URL into a <track>.\n// Rendered once per subtitle/caption localization (always present so the\n// video's `textTracks` indices stay aligned with `subtitleLocs`).\n// ============================================================================\n\nconst SubtitleTrack: FC<{ localization: HokLocalization }> = ({ localization }) => {\n const { data: url } = useFileUrl(localization.fileId);\n const label =\n localization.kind === 'CAPTION'\n ? `${languageName(localization.language)} (CC)`\n : languageName(localization.language);\n return (\n <track\n kind={localization.kind === 'CAPTION' ? 'captions' : 'subtitles'}\n label={label}\n srcLang={localization.language}\n src={url ?? undefined}\n default={localization.isDefault}\n />\n );\n};\n\n// ============================================================================\n// Player\n// ============================================================================\n\nexport const PlayerPage: FC = () => {\n const { titleId, episodeId } = useParams<{ titleId: string; episodeId?: string }>();\n const { navigateTo } = useHookMovies();\n\n // ── Data layer (all hooks unconditional; early returns happen after) ───────\n const { data: title, isLoading: titleLoading, isError: titleError, refetch } = useTitle(titleId);\n const entitlement = useCheckEntitlement(titleId);\n const localizations = useLocalizations(titleId, episodeId);\n const watchProgress = useMyWatchProgress(titleId);\n const recordProgress = useRecordProgress();\n\n const activeEpisode = useMemo(\n () => (episodeId ? (title?.episodes.find((e) => e.id === episodeId) ?? null) : null),\n [episodeId, title]\n );\n\n // Movies play the title's `videoFileId`; episodes play their own `fileId`.\n const sourceFileId = episodeId\n ? (activeEpisode?.fileId ?? undefined)\n : (title?.videoFileId ?? undefined);\n const sourceUrlQuery = useFileUrl(sourceFileId);\n const posterUrlQuery = useFileUrl(title?.posterFileId ?? undefined);\n const backdropUrlQuery = useFileUrl(title?.backdropFileId ?? undefined);\n\n const subtitleLocs = useMemo(\n () => (localizations.data ?? []).filter((l) => l.kind === 'SUBTITLE' || l.kind === 'CAPTION'),\n [localizations.data]\n );\n const dubLocs = useMemo(\n () => (localizations.data ?? []).filter((l) => l.kind === 'DUB'),\n [localizations.data]\n );\n\n // ── Player element + UI state ──────────────────────────────────────────────\n const videoRef = useRef<HTMLVideoElement | null>(null);\n const containerRef = useRef<HTMLDivElement | null>(null);\n\n const [isPlaying, setIsPlaying] = useState(false);\n const [currentTime, setCurrentTime] = useState(0);\n const [duration, setDuration] = useState(0);\n const [volume, setVolume] = useState(1);\n const [muted, setMuted] = useState(false);\n const [buffering, setBuffering] = useState(false);\n const [metadataLoaded, setMetadataLoaded] = useState(false);\n const [playbackError, setPlaybackError] = useState(false);\n const [isFullscreen, setIsFullscreen] = useState(false);\n const [controlsVisible, setControlsVisible] = useState(true);\n const [openMenu, setOpenMenu] = useState<'none' | 'subtitles' | 'settings'>('none');\n const [selectedSubIdx, setSelectedSubIdx] = useState(-1);\n\n // ── Refs for values consumed inside stable callbacks / cleanup ─────────────\n const lastSaveRef = useRef(0);\n const resumeAppliedRef = useRef(false);\n const defaultSubAppliedRef = useRef(false);\n const hideTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const recordRef = useRef(recordProgress.mutate);\n recordRef.current = recordProgress.mutate;\n const isPlayingRef = useRef(isPlaying);\n isPlayingRef.current = isPlaying;\n\n const sourceUrl = sourceFileId ? (sourceUrlQuery.data ?? null) : null;\n const isHls = Boolean(sourceUrl && sourceUrl.split('?')[0]?.toLowerCase().endsWith('.m3u8'));\n const hasTextTracks = subtitleLocs.length > 0;\n const isLive = !Number.isFinite(duration) || duration === 0;\n\n // ── Progress persistence ───────────────────────────────────────────────────\n const saveProgress = useCallback(\n (completed: boolean) => {\n const v = videoRef.current;\n if (!v || !titleId) return;\n const pos = Math.floor(v.currentTime);\n if (pos <= 0 && !completed) return;\n recordRef.current({\n titleId,\n episodeId,\n positionSeconds: pos,\n completed,\n lastDevice: 'web',\n });\n lastSaveRef.current = Date.now();\n },\n [titleId, episodeId]\n );\n\n // Save once more when leaving the player (covers navigation / tab close).\n useEffect(() => {\n return () => {\n if (isPlayingRef.current) saveProgress(false);\n };\n }, [saveProgress]);\n\n // ── Resume: seek to the caller's saved position once metadata + progress are\n // both settled (runs a single time). ────────────────────────────────────\n useEffect(() => {\n const v = videoRef.current;\n if (!v || resumeAppliedRef.current || !metadataLoaded || watchProgress.isLoading) return;\n resumeAppliedRef.current = true;\n const match = (watchProgress.data ?? []).find((p) =>\n episodeId ? p.episodeId === episodeId : !p.episodeId\n );\n if (\n match &&\n !match.completed &&\n match.positionSeconds > SEEK_STEP_SECONDS &&\n Number.isFinite(v.duration) &&\n match.positionSeconds < v.duration - SEEK_STEP_SECONDS\n ) {\n v.currentTime = match.positionSeconds;\n setCurrentTime(match.positionSeconds);\n }\n }, [metadataLoaded, watchProgress.isLoading, watchProgress.data, episodeId]);\n\n // ── Pre-select the default subtitle track once localizations arrive. ───────\n useEffect(() => {\n if (defaultSubAppliedRef.current || subtitleLocs.length === 0) return;\n defaultSubAppliedRef.current = true;\n setSelectedSubIdx(subtitleLocs.findIndex((l) => l.isDefault));\n }, [subtitleLocs]);\n\n // ── Apply the selected text-track mode to the media element. ───────────────\n const applyTextTracks = useCallback(() => {\n const v = videoRef.current;\n if (!v) return;\n const tracks = v.textTracks;\n for (let i = 0; i < tracks.length; i += 1) {\n tracks[i].mode = i === selectedSubIdx ? 'showing' : 'disabled';\n }\n }, [selectedSubIdx]);\n\n useEffect(() => {\n applyTextTracks();\n }, [applyTextTracks, subtitleLocs.length, metadataLoaded]);\n\n // ── Fullscreen state sync ──────────────────────────────────────────────────\n useEffect(() => {\n const onChange = () => setIsFullscreen(Boolean(document.fullscreenElement));\n document.addEventListener('fullscreenchange', onChange);\n return () => document.removeEventListener('fullscreenchange', onChange);\n }, []);\n\n // ── Controls auto-hide ─────────────────────────────────────────────────────\n const revealControls = useCallback(() => {\n setControlsVisible(true);\n if (hideTimerRef.current) clearTimeout(hideTimerRef.current);\n hideTimerRef.current = setTimeout(() => {\n if (isPlayingRef.current && openMenu === 'none') setControlsVisible(false);\n }, 3000);\n }, [openMenu]);\n\n useEffect(() => {\n return () => {\n if (hideTimerRef.current) clearTimeout(hideTimerRef.current);\n };\n }, []);\n\n // ── Media element imperatives ──────────────────────────────────────────────\n const togglePlay = useCallback(() => {\n const v = videoRef.current;\n if (!v) return;\n if (v.paused) {\n void v.play().catch(() => setPlaybackError(true));\n } else {\n v.pause();\n }\n }, []);\n\n const seekBy = useCallback((delta: number) => {\n const v = videoRef.current;\n if (!v || !Number.isFinite(v.duration)) return;\n v.currentTime = Math.min(Math.max(v.currentTime + delta, 0), v.duration);\n setCurrentTime(v.currentTime);\n }, []);\n\n const seekTo = useCallback((value: number) => {\n const v = videoRef.current;\n if (!v) return;\n v.currentTime = value;\n setCurrentTime(value);\n }, []);\n\n const changeVolume = useCallback((value: number) => {\n const v = videoRef.current;\n if (!v) return;\n const clamped = Math.min(Math.max(value, 0), 1);\n v.volume = clamped;\n v.muted = clamped === 0;\n setVolume(clamped);\n setMuted(clamped === 0);\n }, []);\n\n const toggleMute = useCallback(() => {\n const v = videoRef.current;\n if (!v) return;\n const next = !v.muted;\n v.muted = next;\n setMuted(next);\n }, []);\n\n const toggleFullscreen = useCallback(() => {\n const el = containerRef.current;\n if (!el) return;\n if (!document.fullscreenElement) {\n void el.requestFullscreen?.().catch(() => undefined);\n } else {\n void document.exitFullscreen?.().catch(() => undefined);\n }\n }, []);\n\n // ── Keyboard controls (space / arrows / f / m) ─────────────────────────────\n const onKeyDown = useCallback(\n (e: React.KeyboardEvent<HTMLDivElement>) => {\n // Don't hijack typing in the (rare) focusable control.\n const target = e.target as HTMLElement;\n if (target.tagName === 'INPUT' || target.tagName === 'BUTTON') {\n if (e.key !== ' ' && e.key !== 'k') return;\n }\n switch (e.key) {\n case ' ':\n case 'k':\n e.preventDefault();\n togglePlay();\n revealControls();\n break;\n case 'ArrowRight':\n e.preventDefault();\n seekBy(SEEK_STEP_SECONDS);\n revealControls();\n break;\n case 'ArrowLeft':\n e.preventDefault();\n seekBy(-SEEK_STEP_SECONDS);\n revealControls();\n break;\n case 'ArrowUp':\n e.preventDefault();\n changeVolume(volume + VOLUME_STEP);\n revealControls();\n break;\n case 'ArrowDown':\n e.preventDefault();\n changeVolume(volume - VOLUME_STEP);\n revealControls();\n break;\n case 'f':\n case 'F':\n e.preventDefault();\n toggleFullscreen();\n break;\n case 'm':\n case 'M':\n e.preventDefault();\n toggleMute();\n revealControls();\n break;\n default:\n break;\n }\n },\n [togglePlay, seekBy, changeVolume, toggleFullscreen, toggleMute, revealControls, volume]\n );\n\n // ── Media element event handlers ───────────────────────────────────────────\n const handleTimeUpdate = useCallback(() => {\n const v = videoRef.current;\n if (!v) return;\n setCurrentTime(v.currentTime);\n if (!v.paused && Date.now() - lastSaveRef.current >= SAVE_THROTTLE_MS) {\n const nearEnd =\n Number.isFinite(v.duration) && v.currentTime >= v.duration - COMPLETE_TAIL_SECONDS;\n saveProgress(nearEnd);\n }\n }, [saveProgress]);\n\n const handleLoadedMetadata = useCallback(() => {\n const v = videoRef.current;\n if (!v) return;\n setDuration(v.duration);\n setMetadataLoaded(true);\n setPlaybackError(false);\n applyTextTracks();\n }, [applyTextTracks]);\n\n const handleEnded = useCallback(() => {\n setIsPlaying(false);\n saveProgress(true);\n }, [saveProgress]);\n\n // ── Early returns (after every hook above) ─────────────────────────────────\n const backToTitle = () => navigateTo(`title/${titleId ?? ''}`);\n\n if (titleLoading || entitlement.isLoading) {\n return (\n <PlayerStage>\n <div className=\"flex flex-col items-center gap-3 text-text-inverse\">\n <Loader2 className=\"h-8 w-8 animate-spin\" />\n <p className=\"text-sm opacity-80\">Loading…</p>\n </div>\n </PlayerStage>\n );\n }\n\n if (titleError || !title) {\n return (\n <PlayerStage>\n <StageMessage\n icon={<AlertCircle className=\"h-12 w-12\" />}\n title=\"Title unavailable\"\n message=\"We couldn't load this title. It may have been removed or you may not have access.\"\n primary={{ label: 'Retry', onClick: () => void refetch() }}\n />\n </PlayerStage>\n );\n }\n\n if (entitlement.isError) {\n return (\n <PlayerStage>\n <StageMessage\n icon={<AlertCircle className=\"h-12 w-12\" />}\n title=\"Couldn't verify access\"\n message=\"We couldn't check your entitlement for this title. Please try again.\"\n primary={{ label: 'Retry', onClick: () => void entitlement.refetch() }}\n secondary={{ label: 'Back to title', onClick: backToTitle }}\n />\n </PlayerStage>\n );\n }\n\n // Entitlement gate — no fake player when the viewer can't watch.\n if (!entitlement.data?.entitled) {\n return (\n <PlayerStage backdrop={backdropUrlQuery.data ?? posterUrlQuery.data ?? null}>\n <div className=\"mx-auto flex max-w-md flex-col items-center gap-5 text-center text-text-inverse\">\n {posterUrlQuery.data ? (\n <img\n src={posterUrlQuery.data}\n alt={`Poster for ${title.title}`}\n className=\"h-48 w-32 rounded-lg object-cover shadow-lg\"\n />\n ) : (\n <div className=\"flex h-48 w-32 items-center justify-center rounded-lg bg-bg-overlay\">\n <Film className=\"h-10 w-10 opacity-70\" />\n </div>\n )}\n <div className=\"flex items-center gap-2 rounded-full bg-bg-overlay px-3 py-1 text-xs font-medium\">\n <Lock className=\"h-3.5 w-3.5\" />\n Not available to watch yet\n </div>\n <div>\n <h1 className=\"text-xl font-semibold\">{title.title}</h1>\n <p className=\"mt-2 text-sm opacity-80\">\n You don&apos;t have an active entitlement for this title. Open the title page to see\n where and how to watch, or to acquire access.\n </p>\n </div>\n <div className=\"flex flex-wrap items-center justify-center gap-3\">\n <button\n type=\"button\"\n onClick={backToTitle}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 motion-safe:transition-opacity\"\n >\n Where to watch\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo('browse')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-text-inverse/30 px-4 py-2 text-sm font-medium text-text-inverse hover:bg-bg-overlay motion-safe:transition-colors\"\n >\n Browse catalog\n </button>\n </div>\n </div>\n </PlayerStage>\n );\n }\n\n const heading = activeEpisode\n ? `${title.title} — E${activeEpisode.number} · ${activeEpisode.name}`\n : title.title;\n\n const sourceResolving = Boolean(sourceFileId) && sourceUrlQuery.isLoading;\n const noSource = !sourceFileId || (!sourceUrlQuery.isLoading && !sourceUrl);\n\n const VolumeIcon = muted || volume === 0 ? VolumeX : volume < 0.5 ? Volume1 : Volume2;\n const progressPct = duration > 0 ? Math.min((currentTime / duration) * 100, 100) : 0;\n\n return (\n <div className=\"flex h-full flex-col bg-bg-inverse\">\n {/* Top bar */}\n <div className=\"flex items-center gap-3 px-4 py-3 sm:px-6\">\n <button\n type=\"button\"\n onClick={backToTitle}\n className=\"inline-flex items-center gap-2 rounded-lg px-2 py-1.5 text-sm font-medium text-text-inverse/90 hover:bg-bg-overlay motion-safe:transition-colors\"\n aria-label=\"Back to title\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n <span className=\"hidden sm:inline\">Back</span>\n </button>\n <h1 className=\"min-w-0 flex-1 truncate text-sm font-semibold text-text-inverse sm:text-base\">\n {heading}\n </h1>\n </div>\n\n {/* Stage */}\n <div className=\"flex flex-1 items-center justify-center px-0 sm:px-6 sm:pb-6\">\n <div\n ref={containerRef}\n className=\"group relative aspect-video w-full max-w-6xl overflow-hidden bg-bg-inverse sm:rounded-xl\"\n onMouseMove={revealControls}\n onKeyDown={onKeyDown}\n onClick={(e) => {\n if (e.target === videoRef.current) togglePlay();\n }}\n tabIndex={0}\n role=\"region\"\n aria-label={`Video player for ${heading}`}\n >\n {noSource ? (\n <div className=\"absolute inset-0 flex items-center justify-center p-6\">\n <StageMessage\n icon={<Film className=\"h-12 w-12\" />}\n title={activeEpisode ? 'Episode video not available' : 'No video attached'}\n message={\n activeEpisode\n ? 'This episode does not have a playable video file attached yet.'\n : 'This title does not have a playable video file attached yet.'\n }\n secondary={{ label: 'Back to title', onClick: backToTitle }}\n />\n </div>\n ) : (\n <>\n {/* Caption <track>s are added dynamically from localizations below. */}\n <video\n ref={videoRef}\n className=\"h-full w-full bg-bg-inverse\"\n src={sourceUrl ?? undefined}\n poster={posterUrlQuery.data ?? undefined}\n crossOrigin={hasTextTracks ? 'anonymous' : undefined}\n playsInline\n preload=\"metadata\"\n onLoadedMetadata={handleLoadedMetadata}\n onDurationChange={() => {\n const v = videoRef.current;\n if (v) setDuration(v.duration);\n }}\n onTimeUpdate={handleTimeUpdate}\n onPlay={() => {\n setIsPlaying(true);\n revealControls();\n }}\n onPause={() => {\n setIsPlaying(false);\n setControlsVisible(true);\n saveProgress(false);\n }}\n onEnded={handleEnded}\n onWaiting={() => setBuffering(true)}\n onPlaying={() => setBuffering(false)}\n onCanPlay={() => setBuffering(false)}\n onError={() => setPlaybackError(true)}\n >\n {subtitleLocs.map((loc) => (\n <SubtitleTrack key={loc.id} localization={loc} />\n ))}\n </video>\n\n {/* Buffering / resolving overlay */}\n {(buffering || sourceResolving) && !playbackError && (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center\">\n <Loader2 className=\"h-10 w-10 animate-spin text-text-inverse\" />\n </div>\n )}\n\n {/* Playback error overlay */}\n {playbackError && (\n <div className=\"absolute inset-0 flex items-center justify-center bg-overlay-scrim p-6\">\n <StageMessage\n icon={<AlertCircle className=\"h-12 w-12\" />}\n title=\"Playback failed\"\n message={\n isHls\n ? 'This stream is HLS (.m3u8) and your browser could not play it natively. Try a browser with native HLS support.'\n : 'We could not play this video. The link may have expired — reload to try again.'\n }\n primary={{\n label: 'Reload',\n onClick: () => {\n setPlaybackError(false);\n void sourceUrlQuery.refetch();\n },\n }}\n secondary={{ label: 'Back to title', onClick: backToTitle }}\n />\n </div>\n )}\n\n {/* Center play affordance when paused */}\n {!isPlaying && !buffering && !playbackError && metadataLoaded && (\n <button\n type=\"button\"\n onClick={togglePlay}\n aria-label=\"Play\"\n className=\"absolute inset-0 flex items-center justify-center\"\n >\n <span className=\"flex h-16 w-16 items-center justify-center rounded-full bg-action-primary-bg text-action-primary-text shadow-lg motion-safe:transition-transform group-hover:scale-105\">\n <Play className=\"ml-1 h-7 w-7\" />\n </span>\n </button>\n )}\n\n {/* Control bar */}\n <div\n className={`absolute inset-x-0 bottom-0 bg-overlay-scrim px-3 pb-3 pt-10 motion-safe:transition-opacity motion-safe:duration-300 sm:px-4 ${\n controlsVisible || !isPlaying ? 'opacity-100' : 'pointer-events-none opacity-0'\n }`}\n >\n {/* Scrubber */}\n <div className=\"mb-2 flex items-center gap-3\">\n <span className=\"w-12 shrink-0 text-right text-xs tabular-nums text-text-inverse/90\">\n {formatTime(currentTime)}\n </span>\n <div className=\"relative flex-1\">\n <div className=\"h-1.5 w-full overflow-hidden rounded-full bg-text-inverse/25\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${progressPct}%` }}\n />\n </div>\n <input\n type=\"range\"\n min={0}\n max={Number.isFinite(duration) && duration > 0 ? duration : 0}\n step=\"any\"\n value={currentTime}\n disabled={isLive}\n onChange={(e) => seekTo(Number(e.target.value))}\n aria-label=\"Seek\"\n className=\"absolute inset-0 h-full w-full cursor-pointer opacity-0 disabled:cursor-not-allowed\"\n style={{ accentColor: 'var(--color-action-primary-bg)' }}\n />\n </div>\n <span className=\"w-12 shrink-0 text-xs tabular-nums text-text-inverse/70\">\n {isLive ? 'LIVE' : formatTime(duration)}\n </span>\n </div>\n\n {/* Buttons */}\n <div className=\"flex items-center gap-1 sm:gap-2\">\n <ControlButton label={isPlaying ? 'Pause' : 'Play'} onClick={togglePlay}>\n {isPlaying ? <Pause className=\"h-5 w-5\" /> : <Play className=\"h-5 w-5\" />}\n </ControlButton>\n\n {/* Volume */}\n <div className=\"flex items-center gap-1.5\">\n <ControlButton label={muted ? 'Unmute' : 'Mute'} onClick={toggleMute}>\n <VolumeIcon className=\"h-5 w-5\" />\n </ControlButton>\n <input\n type=\"range\"\n min={0}\n max={1}\n step={0.05}\n value={muted ? 0 : volume}\n onChange={(e) => changeVolume(Number(e.target.value))}\n aria-label=\"Volume\"\n className=\"hidden h-1 w-20 cursor-pointer sm:block\"\n style={{ accentColor: 'var(--color-action-primary-bg)' }}\n />\n </div>\n\n <div className=\"ml-auto flex items-center gap-1 sm:gap-2\">\n {/* Subtitle / audio menu */}\n <div className=\"relative\">\n <ControlButton\n label=\"Subtitles & audio\"\n onClick={() =>\n setOpenMenu((m) => (m === 'subtitles' ? 'none' : 'subtitles'))\n }\n active={selectedSubIdx >= 0}\n expanded={openMenu === 'subtitles'}\n disabled={subtitleLocs.length === 0 && dubLocs.length === 0}\n disabledHint=\"No subtitle or audio tracks available for this title.\"\n >\n <Subtitles className=\"h-5 w-5\" />\n </ControlButton>\n {openMenu === 'subtitles' && (\n <TrackMenu\n subtitleLocs={subtitleLocs}\n dubLocs={dubLocs}\n selectedSubIdx={selectedSubIdx}\n onSelectSub={(idx) => {\n setSelectedSubIdx(idx);\n setOpenMenu('none');\n }}\n />\n )}\n </div>\n\n {/* Settings (quality) — honestly disabled */}\n <ControlButton\n label=\"Quality\"\n onClick={() => undefined}\n disabled\n disabledHint=\"Adaptive quality selection isn't available for this source.\"\n >\n <Settings className=\"h-5 w-5\" />\n </ControlButton>\n\n {/* Cast — honestly disabled */}\n <ControlButton\n label=\"Cast\"\n onClick={() => undefined}\n disabled\n disabledHint=\"Casting to external devices isn't supported in this player.\"\n >\n <Cast className=\"h-5 w-5\" />\n </ControlButton>\n\n {/* Offline / download — honestly disabled */}\n <ControlButton\n label=\"Download for offline\"\n onClick={() => undefined}\n disabled\n disabledHint=\"Offline downloads aren't available for this title.\"\n >\n <Download className=\"h-5 w-5\" />\n </ControlButton>\n\n <ControlButton\n label={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}\n onClick={toggleFullscreen}\n >\n {isFullscreen ? (\n <Minimize className=\"h-5 w-5\" />\n ) : (\n <Maximize className=\"h-5 w-5\" />\n )}\n </ControlButton>\n </div>\n </div>\n </div>\n </>\n )}\n </div>\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Presentational sub-components (kept local to the player surface)\n// ============================================================================\n\n/** Full-bleed dark stage used for loading / gated / error states. */\nconst PlayerStage: FC<{ children: React.ReactNode; backdrop?: string | null }> = ({\n children,\n backdrop,\n}) => (\n <div className=\"relative flex h-full min-h-[60vh] items-center justify-center overflow-hidden bg-bg-inverse p-6\">\n {backdrop && (\n <>\n <img\n src={backdrop}\n alt=\"\"\n aria-hidden\n className=\"absolute inset-0 h-full w-full object-cover opacity-30\"\n />\n <div className=\"absolute inset-0 bg-overlay-scrim\" />\n </>\n )}\n <div className=\"relative z-10 w-full\">{children}</div>\n </div>\n);\n\ninterface StageAction {\n label: string;\n onClick: () => void;\n}\n\n/** Centered icon + copy + up to two actions, styled for the dark stage. */\nconst StageMessage: FC<{\n icon: React.ReactNode;\n title: string;\n message: string;\n primary?: StageAction;\n secondary?: StageAction;\n}> = ({ icon, title, message, primary, secondary }) => (\n <div className=\"mx-auto flex max-w-md flex-col items-center gap-4 text-center text-text-inverse\">\n <span className=\"text-text-inverse/80\">{icon}</span>\n <div>\n <h2 className=\"text-lg font-semibold\">{title}</h2>\n <p className=\"mt-1 text-sm opacity-80\">{message}</p>\n </div>\n {(primary || secondary) && (\n <div className=\"flex flex-wrap items-center justify-center gap-3\">\n {primary && (\n <button\n type=\"button\"\n onClick={primary.onClick}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 motion-safe:transition-opacity\"\n >\n <RotateCw className=\"h-4 w-4\" />\n {primary.label}\n </button>\n )}\n {secondary && (\n <button\n type=\"button\"\n onClick={secondary.onClick}\n className=\"inline-flex items-center gap-2 rounded-lg border border-text-inverse/30 px-4 py-2 text-sm font-medium text-text-inverse hover:bg-bg-overlay motion-safe:transition-colors\"\n >\n {secondary.label}\n </button>\n )}\n </div>\n )}\n </div>\n);\n\n/** A single icon control on the dark control bar. */\nconst ControlButton: FC<{\n label: string;\n onClick: () => void;\n children: React.ReactNode;\n active?: boolean;\n disabled?: boolean;\n disabledHint?: string;\n expanded?: boolean;\n}> = ({ label, onClick, children, active, disabled, disabledHint, expanded }) => (\n <button\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n aria-label={label}\n aria-pressed={active}\n aria-expanded={expanded}\n title={disabled && disabledHint ? disabledHint : label}\n className={`relative rounded-lg p-2 motion-safe:transition-colors ${\n disabled ? 'cursor-not-allowed text-text-inverse/40' : 'text-text-inverse hover:bg-bg-overlay'\n }`}\n >\n {children}\n {active && !disabled && (\n <span className=\"absolute bottom-0.5 left-1/2 h-1 w-1 -translate-x-1/2 rounded-full bg-action-primary-bg\" />\n )}\n </button>\n);\n\n/** Popover listing real subtitle/caption tracks + (honest, disabled) dub tracks. */\nconst TrackMenu: FC<{\n subtitleLocs: HokLocalization[];\n dubLocs: HokLocalization[];\n selectedSubIdx: number;\n onSelectSub: (idx: number) => void;\n}> = ({ subtitleLocs, dubLocs, selectedSubIdx, onSelectSub }) => (\n <div\n role=\"menu\"\n aria-label=\"Subtitles and audio\"\n className=\"absolute bottom-full right-0 mb-2 max-h-72 w-56 overflow-y-auto rounded-lg border border-border-seam bg-bg-surface p-1.5 shadow-lg\"\n >\n <p className=\"px-2 py-1 text-[11px] font-semibold uppercase tracking-wide text-text-secondary\">\n Subtitles\n </p>\n <MenuRow label=\"Off\" selected={selectedSubIdx === -1} onClick={() => onSelectSub(-1)} />\n {subtitleLocs.map((loc, idx) => (\n <MenuRow\n key={loc.id}\n label={\n loc.kind === 'CAPTION' ? `${languageName(loc.language)} (CC)` : languageName(loc.language)\n }\n selected={selectedSubIdx === idx}\n onClick={() => onSelectSub(idx)}\n />\n ))}\n\n {dubLocs.length > 0 && (\n <>\n <p className=\"mt-1 px-2 py-1 text-[11px] font-semibold uppercase tracking-wide text-text-secondary\">\n Audio (dub)\n </p>\n {dubLocs.map((loc) => (\n <button\n key={loc.id}\n type=\"button\"\n role=\"menuitem\"\n disabled\n title=\"Alternate dub tracks are separate assets and can't be switched in this player.\"\n className=\"flex w-full cursor-not-allowed items-center justify-between rounded-md px-2 py-1.5 text-left text-sm text-text-disabled\"\n >\n <span>{languageName(loc.language)}</span>\n <Lock className=\"h-3.5 w-3.5\" />\n </button>\n ))}\n </>\n )}\n </div>\n);\n\nconst MenuRow: FC<{ label: string; selected: boolean; onClick: () => void }> = ({\n label,\n selected,\n onClick,\n}) => (\n <button\n type=\"button\"\n role=\"menuitemradio\"\n aria-checked={selected}\n onClick={onClick}\n className={`flex w-full items-center justify-between rounded-md px-2 py-1.5 text-left text-sm motion-safe:transition-colors ${\n selected\n ? 'bg-bg-sunken font-medium text-text-primary'\n : 'text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n }`}\n >\n <span className=\"truncate\">{label}</span>\n {selected && <Check className=\"h-4 w-4 shrink-0 text-action-primary-bg\" />}\n </button>\n);\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-hookmovies",
3
- "version": "2026.720.2",
3
+ "version": "2026.726.2",
4
4
  "description": "Burdenoff HookMovies Microfrontend — discovery, catalog, engagement, auditions, productions, distribution, and library surfaces for the global creative-media network.",
5
5
  "type": "module",
6
6
  "files": [