UNPKG

analytica-frontend-lib

Version:

Repositório público dos componentes utilizados nas plataformas da Analytica Ensino

1,265 lines (1,258 loc) 45.4 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/components/VideoPlayer/VideoPlayer.tsx var VideoPlayer_exports = {}; __export(VideoPlayer_exports, { default: () => VideoPlayer_default }); module.exports = __toCommonJS(VideoPlayer_exports); var import_react4 = require("react"); var import_react_dom = require("react-dom"); var import_phosphor_react2 = require("phosphor-react"); // src/utils/utils.ts var import_clsx = require("clsx"); var import_tailwind_merge = require("tailwind-merge"); function cn(...inputs) { return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs)); } // src/components/IconButton/IconButton.tsx var import_react = require("react"); var import_jsx_runtime = require("react/jsx-runtime"); var IconButton = (0, import_react.forwardRef)( ({ icon, size = "md", active = false, className = "", disabled, ...props }, ref) => { const baseClasses = [ "inline-flex", "items-center", "justify-center", "rounded-lg", "font-medium", "bg-transparent", "text-text-950", "cursor-pointer", "hover:bg-primary-600", "hover:text-text", "focus-visible:outline-none", "focus-visible:ring-2", "focus-visible:ring-offset-0", "focus-visible:ring-indicator-info", "disabled:opacity-50", "disabled:cursor-not-allowed", "disabled:pointer-events-none" ]; const sizeClasses = { sm: ["w-6", "h-6", "text-sm"], md: ["w-10", "h-10", "text-base"] }; const activeClasses = active ? ["!bg-primary-50", "!text-primary-950", "hover:!bg-primary-100"] : []; const allClasses = [ ...baseClasses, ...sizeClasses[size], ...activeClasses ].join(" "); const ariaLabel = props["aria-label"] ?? "Bot\xE3o de a\xE7\xE3o"; return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { ref, type: "button", className: cn(allClasses, className), disabled, "aria-pressed": active, "aria-label": ariaLabel, ...props, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "flex items-center justify-center", children: icon }) } ); } ); IconButton.displayName = "IconButton"; var IconButton_default = IconButton; // src/components/Text/Text.tsx var import_jsx_runtime2 = require("react/jsx-runtime"); var Text = ({ children, size = "md", weight = "normal", color = "text-text-950", as, className = "", ...props }) => { let sizeClasses = ""; let weightClasses = ""; const sizeClassMap = { "2xs": "text-2xs", xs: "text-xs", sm: "text-sm", md: "text-md", lg: "text-lg", xl: "text-xl", "2xl": "text-2xl", "3xl": "text-3xl", "4xl": "text-4xl", "5xl": "text-5xl", "6xl": "text-6xl" }; sizeClasses = sizeClassMap[size] ?? sizeClassMap.md; const weightClassMap = { hairline: "font-hairline", light: "font-light", normal: "font-normal", medium: "font-medium", semibold: "font-semibold", bold: "font-bold", extrabold: "font-extrabold", black: "font-black" }; weightClasses = weightClassMap[weight] ?? weightClassMap.normal; const baseClasses = "font-primary"; const Component = as ?? "p"; return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( Component, { className: cn(baseClasses, sizeClasses, weightClasses, color, className), ...props, children } ); }; var Text_default = Text; // src/hooks/useMobile.ts var import_react2 = require("react"); var MOBILE_WIDTH = 500; var TABLET_WIDTH = 931; var SMALL_MOBILE_WIDTH = 425; var EXTRA_SMALL_MOBILE_WIDTH = 375; var ULTRA_SMALL_MOBILE_WIDTH = 375; var TINY_MOBILE_WIDTH = 320; var DEFAULT_WIDTH = 1200; var getWindowWidth = () => { if (typeof window === "undefined") { return DEFAULT_WIDTH; } return window.innerWidth; }; var getDeviceType = () => { const width = getWindowWidth(); return width < TABLET_WIDTH ? "responsive" : "desktop"; }; var useMobile = () => { const [isMobile, setIsMobile] = (0, import_react2.useState)(false); const [isTablet, setIsTablet] = (0, import_react2.useState)(false); const [isSmallMobile, setIsSmallMobile] = (0, import_react2.useState)(false); const [isExtraSmallMobile, setIsExtraSmallMobile] = (0, import_react2.useState)(false); const [isUltraSmallMobile, setIsUltraSmallMobile] = (0, import_react2.useState)(false); const [isTinyMobile, setIsTinyMobile] = (0, import_react2.useState)(false); (0, import_react2.useEffect)(() => { const checkScreenSize = () => { const width = getWindowWidth(); setIsMobile(width < MOBILE_WIDTH); setIsTablet(width < TABLET_WIDTH); setIsSmallMobile(width < SMALL_MOBILE_WIDTH); setIsExtraSmallMobile(width < EXTRA_SMALL_MOBILE_WIDTH); setIsUltraSmallMobile(width < ULTRA_SMALL_MOBILE_WIDTH); setIsTinyMobile(width < TINY_MOBILE_WIDTH); }; checkScreenSize(); window.addEventListener("resize", checkScreenSize); return () => window.removeEventListener("resize", checkScreenSize); }, []); const getFormContainerClasses = () => { if (isMobile) { return "w-full px-4"; } if (isTablet) { return "w-full px-6"; } return "w-full max-w-[992px] mx-auto px-0"; }; const getMobileHeaderClasses = () => { return "flex flex-col items-start gap-4 mb-6"; }; const getDesktopHeaderClasses = () => { return "flex flex-row justify-between items-center gap-6 mb-8"; }; const getHeaderClasses = () => { return isMobile ? getMobileHeaderClasses() : getDesktopHeaderClasses(); }; const getVideoContainerClasses = () => { if (isTinyMobile) return "aspect-square"; if (isExtraSmallMobile) return "aspect-[4/3]"; if (isSmallMobile) return "aspect-[16/12]"; return "aspect-video"; }; return { isMobile, isTablet, isSmallMobile, isExtraSmallMobile, isUltraSmallMobile, isTinyMobile, getFormContainerClasses, getHeaderClasses, getMobileHeaderClasses, getDesktopHeaderClasses, getVideoContainerClasses, getDeviceType }; }; // src/components/DownloadButton/DownloadButton.tsx var import_react3 = require("react"); var import_phosphor_react = require("phosphor-react"); var import_jsx_runtime3 = require("react/jsx-runtime"); var getMimeType = (url) => { const extension = getFileExtension(url); const mimeTypes = { pdf: "application/pdf", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", mp3: "audio/mpeg", mp4: "video/mp4", vtt: "text/vtt" }; return mimeTypes[extension] || "application/octet-stream"; }; var triggerDownload = async (url, filename) => { try { const response = await fetch(url, { mode: "cors", credentials: "same-origin" }); if (!response.ok) { throw new Error( `Failed to fetch file: ${response.status} ${response.statusText}` ); } const blob = await response.blob(); const mimeType = getMimeType(url); const typedBlob = new Blob([blob], { type: mimeType }); const blobUrl = URL.createObjectURL(typedBlob); const link = document.createElement("a"); link.href = blobUrl; link.download = filename; link.rel = "noopener noreferrer"; document.body.appendChild(link); link.click(); link.remove(); setTimeout(() => { URL.revokeObjectURL(blobUrl); }, 1e3); } catch (error) { console.warn("Fetch download failed, falling back to direct link:", error); const link = document.createElement("a"); link.href = url; link.download = filename; link.rel = "noopener noreferrer"; link.target = "_blank"; document.body.appendChild(link); link.click(); link.remove(); } }; var getFileExtension = (url) => { try { const u = new URL(url, globalThis.location?.origin || "http://localhost"); url = u.pathname; } catch { } const path = url.split(/[?#]/)[0]; const dot = path.lastIndexOf("."); return dot > -1 ? path.slice(dot + 1).toLowerCase() : "file"; }; var generateFilename = (contentType, url, lessonTitle = "aula") => { const sanitizedTitle = lessonTitle.toLowerCase().replaceAll(/[^a-z0-9\s]/g, "").replaceAll(/\s+/g, "-").substring(0, 50); const extension = getFileExtension(url); return `${sanitizedTitle}-${contentType}.${extension}`; }; var DownloadButton = ({ content, className, onDownloadStart, onDownloadComplete, onDownloadError, lessonTitle = "aula", disabled = false }) => { const [isDownloading, setIsDownloading] = (0, import_react3.useState)(false); const isValidUrl = (0, import_react3.useCallback)((url) => { return Boolean( url && url.trim() !== "" && url !== "undefined" && url !== "null" ); }, []); const getAvailableContent = (0, import_react3.useCallback)(() => { const downloads = []; if (isValidUrl(content.urlDoc)) { downloads.push({ type: "documento", url: content.urlDoc, label: "Documento" }); } if (isValidUrl(content.urlInitialFrame)) { downloads.push({ type: "quadro-inicial", url: content.urlInitialFrame, label: "Quadro Inicial" }); } if (isValidUrl(content.urlFinalFrame)) { downloads.push({ type: "quadro-final", url: content.urlFinalFrame, label: "Quadro Final" }); } if (isValidUrl(content.urlPodcast)) { downloads.push({ type: "podcast", url: content.urlPodcast, label: "Podcast" }); } if (isValidUrl(content.urlVideo)) { downloads.push({ type: "video", url: content.urlVideo, label: "V\xEDdeo" }); } return downloads; }, [content, isValidUrl]); const handleDownload = (0, import_react3.useCallback)(async () => { if (disabled || isDownloading) return; const availableContent = getAvailableContent(); if (availableContent.length === 0) { return; } setIsDownloading(true); try { for (let i = 0; i < availableContent.length; i++) { const item = availableContent[i]; try { onDownloadStart?.(item.type); const filename = generateFilename(item.type, item.url, lessonTitle); await triggerDownload(item.url, filename); onDownloadComplete?.(item.type); if (i < availableContent.length - 1) { await new Promise((resolve) => setTimeout(resolve, 200)); } } catch (error) { onDownloadError?.( item.type, error instanceof Error ? error : new Error(`Falha ao baixar ${item.label}`) ); } } } finally { setIsDownloading(false); } }, [ disabled, isDownloading, getAvailableContent, lessonTitle, onDownloadStart, onDownloadComplete, onDownloadError ]); const hasContent = getAvailableContent().length > 0; if (!hasContent) { return null; } return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: cn("flex items-center", className), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)( IconButton_default, { icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_phosphor_react.DownloadSimple, { size: 24 }), onClick: handleDownload, disabled: disabled || isDownloading, "aria-label": (() => { if (isDownloading) { return "Baixando conte\xFAdo..."; } const contentCount = getAvailableContent().length; const suffix = contentCount > 1 ? "s" : ""; return `Baixar conte\xFAdo da aula (${contentCount} arquivo${suffix})`; })(), className: cn( "!bg-transparent hover:!bg-black/10 transition-colors", isDownloading && "opacity-60 cursor-not-allowed" ) } ) }); }; var DownloadButton_default = DownloadButton; // src/components/VideoPlayer/VideoPlayer.tsx var import_jsx_runtime4 = require("react/jsx-runtime"); var CONTROLS_HIDE_TIMEOUT = 3e3; var LEAVE_HIDE_TIMEOUT = 1e3; var INIT_DELAY = 100; var formatTime = (seconds) => { if (!seconds || Number.isNaN(seconds)) return "0:00"; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, "0")}`; }; var ProgressBar = ({ currentTime, duration, progressPercentage, onSeek, className = "px-4 pb-2" }) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "input", { type: "range", min: 0, max: duration || 100, value: currentTime, onChange: (e) => onSeek(Number.parseFloat(e.target.value)), className: "w-full h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer slider:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500", "aria-label": "Video progress", style: { background: `linear-gradient(to right, var(--color-primary-700) ${progressPercentage}%, var(--color-secondary-300) ${progressPercentage}%)` } } ) }); var VolumeControls = ({ volume, isMuted, onVolumeChange, onToggleMute, iconSize = 24, showSlider = true }) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-2", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( IconButton_default, { icon: isMuted ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_phosphor_react2.SpeakerSlash, { size: iconSize }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_phosphor_react2.SpeakerHigh, { size: iconSize }), onClick: onToggleMute, "aria-label": isMuted ? "Unmute" : "Mute", className: "!bg-transparent !text-white hover:!bg-white/20" } ), showSlider && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "input", { type: "range", min: 0, max: 100, value: Math.round(volume * 100), onChange: (e) => onVolumeChange(Number.parseInt(e.target.value)), className: "w-20 h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-500", "aria-label": "Volume control", style: { background: `linear-gradient(to right, var(--color-primary-700) ${volume * 100}%, var(--color-secondary-300) ${volume * 100}%)` } } ) ] }); var SpeedMenu = ({ showSpeedMenu, playbackRate, onToggleMenu, onSpeedChange, isFullscreen, iconSize = 24, isTinyMobile = false }) => { const buttonRef = (0, import_react4.useRef)(null); const speedMenuContainerRef = (0, import_react4.useRef)(null); const speedMenuRef = (0, import_react4.useRef)(null); const getMenuPosition = () => { if (!buttonRef.current) return { top: 0, left: 0 }; const rect = buttonRef.current.getBoundingClientRect(); const menuHeight = isTinyMobile ? 150 : 180; const menuWidth = isTinyMobile ? 60 : 80; const padding = isTinyMobile ? 4 : 8; return { // Fixed coords are viewport-based — no scroll offsets. top: Math.max(padding, rect.top - menuHeight), left: Math.max(padding, rect.right - menuWidth) }; }; const position = getMenuPosition(); (0, import_react4.useEffect)(() => { const handleClickOutside = (event) => { const target = event.target; const isOutsideContainer = speedMenuContainerRef.current && !speedMenuContainerRef.current.contains(target); const isOutsideMenu = speedMenuRef.current && !speedMenuRef.current.contains(target); if (isOutsideContainer && isOutsideMenu) { onToggleMenu(); } }; if (showSpeedMenu) { document.addEventListener("mousedown", handleClickOutside); } return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [showSpeedMenu, onToggleMenu]); const menuContent = /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "div", { ref: speedMenuRef, role: "menu", "aria-label": "Playback speed", className: isFullscreen ? "absolute bottom-12 right-0 bg-background border border-border-100 rounded-lg shadow-lg p-2 min-w-24 z-[9999]" : "fixed bg-background border border-border-100 rounded-lg shadow-lg p-2 min-w-24 z-[9999]", style: isFullscreen ? void 0 : { top: `${position.top}px`, left: `${position.left}px` }, children: [0.5, 0.75, 1, 1.25, 1.5, 2].map((speed) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)( "button", { role: "menuitemradio", "aria-checked": playbackRate === speed, onClick: () => onSpeedChange(speed), className: `block w-full text-left px-3 py-1 text-sm rounded hover:bg-border-50 transition-colors ${playbackRate === speed ? "bg-primary-950 text-secondary-100 font-medium" : "text-text-950"}`, children: [ speed, "x" ] }, speed )) } ); const portalContent = showSpeedMenu && globalThis.window !== void 0 && globalThis.document !== void 0 && !!globalThis.document?.body ? (0, import_react_dom.createPortal)(menuContent, globalThis.document.body) : null; return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "relative", ref: speedMenuContainerRef, children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( IconButton_default, { ref: buttonRef, icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_phosphor_react2.DotsThreeVertical, { size: iconSize }), onClick: onToggleMenu, "aria-label": "Playback speed", "aria-haspopup": "menu", "aria-expanded": showSpeedMenu, className: "!bg-transparent !text-white hover:!bg-white/20" } ), showSpeedMenu && (isFullscreen ? menuContent : portalContent) ] }); }; var VideoPlayer = ({ src, poster, subtitles, title, subtitle: subtitleText, initialTime = 0, onTimeUpdate, onProgress, onVideoComplete, className, autoSave = true, storageKey = "video-progress", downloadContent, showDownloadButton = false, onDownloadStart, onDownloadComplete, onDownloadError }) => { const videoRef = (0, import_react4.useRef)(null); const { isUltraSmallMobile, isTinyMobile } = useMobile(); const [isPlaying, setIsPlaying] = (0, import_react4.useState)(false); const [currentTime, setCurrentTime] = (0, import_react4.useState)(0); const [duration, setDuration] = (0, import_react4.useState)(0); const [isMuted, setIsMuted] = (0, import_react4.useState)(false); const [volume, setVolume] = (0, import_react4.useState)(1); const [isFullscreen, setIsFullscreen] = (0, import_react4.useState)(false); const [showControls, setShowControls] = (0, import_react4.useState)(true); const [hasCompleted, setHasCompleted] = (0, import_react4.useState)(false); const [showCaptions, setShowCaptions] = (0, import_react4.useState)(false); const [subtitlesValidation, setSubtitlesValidation] = (0, import_react4.useState)("idle"); (0, import_react4.useEffect)(() => { setHasCompleted(false); }, [src]); const [playbackRate, setPlaybackRate] = (0, import_react4.useState)(1); const [showSpeedMenu, setShowSpeedMenu] = (0, import_react4.useState)(false); const lastSaveTimeRef = (0, import_react4.useRef)(0); const trackRef = (0, import_react4.useRef)(null); const controlsTimeoutRef = (0, import_react4.useRef)(null); const lastMousePositionRef = (0, import_react4.useRef)({ x: 0, y: 0 }); const isUserInteracting = (0, import_react4.useCallback)(() => { if (showSpeedMenu) { return true; } const activeElement = document.activeElement; const videoContainer = videoRef.current?.parentElement; if (activeElement && videoContainer?.contains(activeElement)) { if (activeElement === videoRef.current) { return false; } const isControl = activeElement.matches("button, input, [tabindex]"); if (isControl) { return true; } } return false; }, [showSpeedMenu]); const clearControlsTimeout = (0, import_react4.useCallback)(() => { if (controlsTimeoutRef.current) { clearTimeout(controlsTimeoutRef.current); controlsTimeoutRef.current = null; } }, []); const showControlsWithTimer = (0, import_react4.useCallback)(() => { setShowControls(true); clearControlsTimeout(); if (isFullscreen) { if (isPlaying) { controlsTimeoutRef.current = globalThis.setTimeout(() => { setShowControls(false); }, CONTROLS_HIDE_TIMEOUT); } } else { controlsTimeoutRef.current = globalThis.setTimeout(() => { setShowControls(false); }, CONTROLS_HIDE_TIMEOUT); } }, [isFullscreen, isPlaying, clearControlsTimeout]); const handleMouseMove = (0, import_react4.useCallback)( (event) => { const currentX = event.clientX; const currentY = event.clientY; const lastPos = lastMousePositionRef.current; const hasMoved = Math.abs(currentX - lastPos.x) > 5 || Math.abs(currentY - lastPos.y) > 5; if (hasMoved) { lastMousePositionRef.current = { x: currentX, y: currentY }; showControlsWithTimer(); } }, [showControlsWithTimer] ); const handleMouseEnter = (0, import_react4.useCallback)(() => { showControlsWithTimer(); }, [showControlsWithTimer]); const handleMouseLeave = (0, import_react4.useCallback)(() => { const userInteracting = isUserInteracting(); clearControlsTimeout(); if (!isFullscreen && !userInteracting) { controlsTimeoutRef.current = globalThis.setTimeout(() => { setShowControls(false); }, LEAVE_HIDE_TIMEOUT); } }, [isFullscreen, clearControlsTimeout, isUserInteracting]); (0, import_react4.useEffect)(() => { if (videoRef.current) { videoRef.current.volume = volume; videoRef.current.muted = isMuted; } }, [volume, isMuted]); (0, import_react4.useEffect)(() => { const video = videoRef.current; if (!video) return; const onPlay = () => setIsPlaying(true); const onPause = () => setIsPlaying(false); const onEnded = () => setIsPlaying(false); video.addEventListener("play", onPlay); video.addEventListener("pause", onPause); video.addEventListener("ended", onEnded); return () => { video.removeEventListener("play", onPlay); video.removeEventListener("pause", onPause); video.removeEventListener("ended", onEnded); }; }, []); (0, import_react4.useEffect)(() => { const video = videoRef.current; if (!video) return; video.setAttribute("playsinline", ""); video.setAttribute("webkit-playsinline", ""); }, []); (0, import_react4.useEffect)(() => { if (isPlaying) { showControlsWithTimer(); } else { clearControlsTimeout(); if (isFullscreen) { setShowControls(true); } else { showControlsWithTimer(); } } }, [isPlaying, isFullscreen, showControlsWithTimer, clearControlsTimeout]); (0, import_react4.useEffect)(() => { const video = videoRef.current; if (!video) return; const handleFullscreenChange = () => { const isCurrentlyFullscreen = !!document.fullscreenElement; setIsFullscreen(isCurrentlyFullscreen); if (isCurrentlyFullscreen) { showControlsWithTimer(); } }; const handleWebkitBeginFullscreen = () => { setIsFullscreen(true); showControlsWithTimer(); }; const handleWebkitEndFullscreen = () => { setIsFullscreen(false); }; document.addEventListener("fullscreenchange", handleFullscreenChange); video.addEventListener( "webkitbeginfullscreen", handleWebkitBeginFullscreen ); video.addEventListener("webkitendfullscreen", handleWebkitEndFullscreen); return () => { document.removeEventListener("fullscreenchange", handleFullscreenChange); video.removeEventListener( "webkitbeginfullscreen", handleWebkitBeginFullscreen ); video.removeEventListener( "webkitendfullscreen", handleWebkitEndFullscreen ); }; }, [showControlsWithTimer]); (0, import_react4.useEffect)(() => { const init = () => { if (!isFullscreen) { showControlsWithTimer(); } }; let raf1 = 0, raf2 = 0, tid; if (globalThis.requestAnimationFrame === void 0) { tid = globalThis.setTimeout(init, INIT_DELAY); return () => { if (tid) clearTimeout(tid); }; } else { raf1 = requestAnimationFrame(() => { raf2 = requestAnimationFrame(init); }); return () => { cancelAnimationFrame(raf1); cancelAnimationFrame(raf2); }; } }, []); const getInitialTime = (0, import_react4.useCallback)(() => { if (!autoSave || !storageKey) { return Number.isFinite(initialTime) && initialTime >= 0 ? initialTime : void 0; } const saved = Number( localStorage.getItem(`${storageKey}-${src}`) || Number.NaN ); const hasValidInitial = Number.isFinite(initialTime) && initialTime >= 0; const hasValidSaved = Number.isFinite(saved) && saved >= 0; if (hasValidInitial) return initialTime; if (hasValidSaved) return saved; return void 0; }, [autoSave, storageKey, src, initialTime]); (0, import_react4.useEffect)(() => { const start = getInitialTime(); if (start !== void 0 && videoRef.current) { videoRef.current.currentTime = start; setCurrentTime(start); } }, [getInitialTime]); const saveProgress = (0, import_react4.useCallback)( (time) => { if (!autoSave || !storageKey) return; const now = Date.now(); if (now - lastSaveTimeRef.current > 5e3) { localStorage.setItem(`${storageKey}-${src}`, time.toString()); lastSaveTimeRef.current = now; } }, [autoSave, storageKey, src] ); const togglePlayPause = (0, import_react4.useCallback)(async () => { const video = videoRef.current; if (!video) return; if (!video.paused) { video.pause(); return; } try { await video.play(); } catch { } }, []); const handleVolumeChange = (0, import_react4.useCallback)( (newVolume) => { const video = videoRef.current; if (!video) return; const volumeValue = newVolume / 100; video.volume = volumeValue; setVolume(volumeValue); const shouldMute = volumeValue === 0; const shouldUnmute = volumeValue > 0 && isMuted; if (shouldMute) { video.muted = true; setIsMuted(true); } else if (shouldUnmute) { video.muted = false; setIsMuted(false); } }, [isMuted] ); const toggleMute = (0, import_react4.useCallback)(() => { const video = videoRef.current; if (!video) return; if (isMuted) { const restoreVolume = volume > 0 ? volume : 0.5; video.volume = restoreVolume; video.muted = false; setVolume(restoreVolume); setIsMuted(false); } else { video.muted = true; setIsMuted(true); } }, [isMuted, volume]); const handleSeek = (0, import_react4.useCallback)((newTime) => { const video = videoRef.current; if (video) { video.currentTime = newTime; } }, []); const isSafariIOS = (0, import_react4.useCallback)(() => { const ua = navigator.userAgent; const isIOS = /iPad|iPhone|iPod/.test(ua); const isWebKit = /WebKit/.test(ua); const isNotChrome = !/CriOS|Chrome/.test(ua); return isIOS && isWebKit && isNotChrome; }, []); const toggleFullscreen = (0, import_react4.useCallback)(() => { const video = videoRef.current; const container = video?.parentElement; if (!video || !container) return; if (isSafariIOS()) { const videoElement = video; if (!isFullscreen && videoElement.webkitEnterFullscreen) { videoElement.webkitEnterFullscreen(); } else if (isFullscreen && videoElement.webkitExitFullscreen) { videoElement.webkitExitFullscreen(); } } else if (!isFullscreen && container.requestFullscreen) { container.requestFullscreen(); } else if (isFullscreen && document.exitFullscreen) { document.exitFullscreen(); } }, [isFullscreen, isSafariIOS]); const handleSpeedChange = (0, import_react4.useCallback)((speed) => { if (videoRef.current) { videoRef.current.playbackRate = speed; setPlaybackRate(speed); setShowSpeedMenu(false); } }, []); const toggleSpeedMenu = (0, import_react4.useCallback)(() => { setShowSpeedMenu(!showSpeedMenu); }, [showSpeedMenu]); const toggleCaptions = (0, import_react4.useCallback)(() => { if (!trackRef.current?.track || !subtitles || subtitlesValidation !== "valid") return; const newShowCaptions = !showCaptions; setShowCaptions(newShowCaptions); trackRef.current.track.mode = newShowCaptions ? "showing" : "hidden"; }, [showCaptions, subtitles, subtitlesValidation]); const checkVideoCompletion = (0, import_react4.useCallback)( (progressPercent) => { if (progressPercent >= 95 && !hasCompleted) { setHasCompleted(true); onVideoComplete?.(); } }, [hasCompleted, onVideoComplete] ); const handleTimeUpdate = (0, import_react4.useCallback)(() => { const video = videoRef.current; if (!video) return; const current = video.currentTime; setCurrentTime(current); saveProgress(current); onTimeUpdate?.(current); if (duration > 0) { const progressPercent = current / duration * 100; onProgress?.(progressPercent); checkVideoCompletion(progressPercent); } }, [duration, saveProgress, onTimeUpdate, onProgress, checkVideoCompletion]); const handleLoadedMetadata = (0, import_react4.useCallback)(() => { if (videoRef.current) { setDuration(videoRef.current.duration); } }, []); (0, import_react4.useEffect)(() => { const controller = new AbortController(); const validateSubtitles = async () => { if (!subtitles) { setSubtitlesValidation("idle"); return; } setSubtitlesValidation("validating"); try { if (subtitles.startsWith("data:")) { setSubtitlesValidation("valid"); return; } const response = await fetch(subtitles, { method: "HEAD", signal: controller.signal }); if (response.ok) { const contentType = response.headers.get("content-type"); const isValidType = !contentType || contentType.includes("text/vtt") || contentType.includes("text/plain") || contentType.includes("application/octet-stream"); if (isValidType) { setSubtitlesValidation("valid"); } else { setSubtitlesValidation("invalid"); console.warn( `Subtitles URL has invalid content type: ${contentType}` ); } } else { setSubtitlesValidation("invalid"); console.warn( `Subtitles URL returned status: ${response.status} ${response.statusText}` ); } } catch (error) { if (error instanceof Error && error.name === "AbortError") { return; } console.warn("Subtitles URL validation failed:", error); setSubtitlesValidation("invalid"); } }; validateSubtitles(); return () => { controller.abort(); }; }, [subtitles]); (0, import_react4.useEffect)(() => { if (trackRef.current?.track) { trackRef.current.track.mode = showCaptions && subtitles && subtitlesValidation === "valid" ? "showing" : "hidden"; } }, [subtitles, showCaptions, subtitlesValidation]); (0, import_react4.useEffect)(() => { const handleVisibilityChange = () => { if (document.hidden && isPlaying && videoRef.current) { videoRef.current.pause(); setIsPlaying(false); } }; const handleBlur = () => { if (isPlaying && videoRef.current) { videoRef.current.pause(); setIsPlaying(false); } }; document.addEventListener("visibilitychange", handleVisibilityChange); globalThis.addEventListener("blur", handleBlur); return () => { document.removeEventListener("visibilitychange", handleVisibilityChange); globalThis.removeEventListener("blur", handleBlur); clearControlsTimeout(); }; }, [isPlaying, clearControlsTimeout]); const progressPercentage = duration > 0 ? currentTime / duration * 100 : 0; const getIconSize = (0, import_react4.useCallback)(() => { if (isTinyMobile) return 18; if (isUltraSmallMobile) return 20; return 24; }, [isTinyMobile, isUltraSmallMobile]); const getControlsPadding = (0, import_react4.useCallback)(() => { if (isTinyMobile) return "px-2 pb-2 pt-1"; if (isUltraSmallMobile) return "px-3 pb-3 pt-1"; return "px-4 pb-4"; }, [isTinyMobile, isUltraSmallMobile]); const getControlsGap = (0, import_react4.useCallback)(() => { if (isTinyMobile) return "gap-1"; if (isUltraSmallMobile) return "gap-2"; return "gap-4"; }, [isTinyMobile, isUltraSmallMobile]); const getProgressBarPadding = (0, import_react4.useCallback)(() => { if (isTinyMobile) return "px-2 pb-1"; if (isUltraSmallMobile) return "px-3 pb-1"; return "px-4 pb-2"; }, [isTinyMobile, isUltraSmallMobile]); const getCenterPlayButtonPosition = (0, import_react4.useCallback)(() => { if (isTinyMobile) return "items-center justify-center -translate-y-12"; if (isUltraSmallMobile) return "items-center justify-center -translate-y-8"; return "items-center justify-center"; }, [isTinyMobile, isUltraSmallMobile]); const getTopControlsOpacity = (0, import_react4.useCallback)(() => { return showControls ? "opacity-100" : "opacity-0"; }, [showControls]); const getBottomControlsOpacity = (0, import_react4.useCallback)(() => { return showControls ? "opacity-100" : "opacity-0"; }, [showControls]); const seekBackward = (0, import_react4.useCallback)(() => { if (videoRef.current) { videoRef.current.currentTime -= 10; } }, []); const seekForward = (0, import_react4.useCallback)(() => { if (videoRef.current) { videoRef.current.currentTime += 10; } }, []); const increaseVolume = (0, import_react4.useCallback)(() => { handleVolumeChange(Math.min(100, volume * 100 + 10)); }, [handleVolumeChange, volume]); const decreaseVolume = (0, import_react4.useCallback)(() => { handleVolumeChange(Math.max(0, volume * 100 - 10)); }, [handleVolumeChange, volume]); const handleVideoKeyDown = (0, import_react4.useCallback)( (e) => { if (!e.key) return; e.stopPropagation(); showControlsWithTimer(); const keyHandlers = { " ": togglePlayPause, Enter: togglePlayPause, ArrowLeft: seekBackward, ArrowRight: seekForward, ArrowUp: increaseVolume, ArrowDown: decreaseVolume, m: toggleMute, M: toggleMute, f: toggleFullscreen, F: toggleFullscreen }; const handler = keyHandlers[e.key]; if (handler) { e.preventDefault(); handler(); } }, [ showControlsWithTimer, togglePlayPause, seekBackward, seekForward, increaseVolume, decreaseVolume, toggleMute, toggleFullscreen ] ); const groupedSubTitleValid = subtitles && subtitlesValidation === "valid"; return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: cn("flex flex-col", className), children: [ (title || subtitleText) && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "bg-subject-1 px-8 py-4 flex items-end justify-between min-h-20", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col gap-1", children: [ title && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( Text_default, { as: "h2", size: "lg", weight: "bold", color: "text-text-900", className: "leading-5 tracking-wide", children: title } ), subtitleText && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( Text_default, { as: "p", size: "sm", weight: "normal", color: "text-text-600", className: "leading-5", children: subtitleText } ) ] }), showDownloadButton && downloadContent && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( DownloadButton_default, { content: downloadContent, lessonTitle: title, onDownloadStart, onDownloadComplete, onDownloadError, className: "flex-shrink-0" } ) ] }), /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)( "section", { className: cn( "relative w-full bg-background overflow-hidden group", "rounded-b-xl", // Hide cursor when controls are hidden and video is playing isPlaying && !showControls ? "cursor-none group-hover:cursor-default" : "cursor-default" ), "aria-label": title ? `Video player: ${title}` : "Video player", onMouseMove: handleMouseMove, onMouseEnter: handleMouseEnter, onTouchStart: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "video", { ref: videoRef, src, poster, className: "w-full h-full object-contain analytica-video", controlsList: "nodownload", playsInline: true, onTimeUpdate: handleTimeUpdate, onLoadedMetadata: handleLoadedMetadata, onClick: togglePlayPause, onKeyDown: handleVideoKeyDown, tabIndex: 0, "aria-label": title ? `Video: ${title}` : "Video player", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "track", { ref: trackRef, kind: "captions", src: groupedSubTitleValid ? subtitles : "data:text/vtt;charset=utf-8,WEBVTT", srcLang: "pt-br", label: groupedSubTitleValid ? "Legendas em Portugu\xEAs" : "Sem legendas dispon\xEDveis", default: false } ) } ), !isPlaying && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "div", { className: cn( "absolute inset-0 flex bg-black/30 transition-opacity", getCenterPlayButtonPosition() ), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( IconButton_default, { icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_phosphor_react2.Play, { size: 32, weight: "regular", className: "ml-1" }), onClick: togglePlayPause, "aria-label": "Play video", className: "!bg-transparent !text-white !w-auto !h-auto hover:!bg-transparent hover:!text-gray-200" } ) } ), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "div", { className: cn( "absolute top-0 left-0 right-0 p-4 bg-gradient-to-b from-black/70 to-transparent transition-opacity", getTopControlsOpacity() ), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex justify-start", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( IconButton_default, { icon: isFullscreen ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_phosphor_react2.ArrowsInSimple, { size: 24 }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_phosphor_react2.ArrowsOutSimple, { size: 24 }), onClick: toggleFullscreen, "aria-label": isFullscreen ? "Exit fullscreen" : "Enter fullscreen", className: "!bg-transparent !text-white hover:!bg-white/20" } ) }) } ), /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)( "div", { className: cn( "absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 to-transparent transition-opacity", getBottomControlsOpacity() ), children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( ProgressBar, { currentTime, duration, progressPercentage, onSeek: handleSeek, className: getProgressBarPadding() } ), /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)( "div", { className: cn( "flex items-center justify-between", getControlsPadding() ), children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: cn("flex items-center", getControlsGap()), children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( IconButton_default, { icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_phosphor_react2.Pause, { size: getIconSize() }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_phosphor_react2.Play, { size: getIconSize() }), onClick: togglePlayPause, "aria-label": isPlaying ? "Pause" : "Play", className: "!bg-transparent !text-white hover:!bg-white/20" } ), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( VolumeControls, { volume, isMuted, onVolumeChange: handleVolumeChange, onToggleMute: toggleMute, iconSize: getIconSize(), showSlider: !isUltraSmallMobile } ), groupedSubTitleValid && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( IconButton_default, { icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_phosphor_react2.ClosedCaptioning, { size: getIconSize() }), onClick: toggleCaptions, "aria-label": showCaptions ? "Hide captions" : "Show captions", className: cn( "!bg-transparent hover:!bg-white/20", showCaptions ? "!text-primary-400" : "!text-white" ) } ), /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text_default, { size: "sm", weight: "medium", color: "text-white", children: [ formatTime(currentTime), " / ", formatTime(duration) ] }) ] }), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex items-center gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( SpeedMenu, { showSpeedMenu, playbackRate, onToggleMenu: toggleSpeedMenu, onSpeedChange: handleSpeedChange, iconSize: getIconSize(), isTinyMobile, isFullscreen } ) }) ] } ) ] } ) ] } ) ] }); }; var VideoPlayer_default = VideoPlayer; //# sourceMappingURL=index.js.map