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
JavaScript
;
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