UNPKG

analytica-frontend-lib

Version:

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

1 lines 19.8 kB
{"version":3,"sources":["../src/components/DownloadButton/DownloadButton.tsx"],"sourcesContent":["import { useCallback, useState } from 'react';\nimport { DownloadSimpleIcon } from '@phosphor-icons/react/dist/csr/DownloadSimple';\nimport IconButton from '../IconButton/IconButton';\nimport ProgressModal from '../ProgressModal/ProgressModal';\nimport { cn } from '../../utils/utils';\n\n/**\n * Download content interface for lesson materials\n */\nexport interface DownloadContent {\n /** Initial frame image URL */\n urlInitialFrame?: string;\n /** Final frame image URL */\n urlFinalFrame?: string;\n /** Podcast audio URL */\n urlPodcast?: string;\n /** Video URL */\n urlVideo?: string;\n}\n\n/**\n * Props for DownloadButton component\n */\nexport interface DownloadButtonProps {\n /** Content URLs to download */\n content: DownloadContent;\n /** Additional CSS classes */\n className?: string;\n /** Callback fired when download starts */\n onDownloadStart?: (contentType: string) => void;\n /** Callback fired when download completes */\n onDownloadComplete?: (contentType: string) => void;\n /** Callback fired when download fails */\n onDownloadError?: (contentType: string, error: Error) => void;\n /** Lesson title for download file naming */\n lessonTitle?: string;\n /** Whether the button is disabled */\n disabled?: boolean;\n}\n\n/** A single downloadable asset resolved from the lesson content */\ninterface DownloadItem {\n type: string;\n url: string;\n label: string;\n}\n\n/**\n * Minimal shape of the File System Access API we rely on.\n *\n * Typed locally because `showDirectoryPicker` is not in the DOM lib yet, and\n * because only Chromium ships it — every use goes through `supportsDirectoryPicker`.\n */\ninterface FileSystemWritable {\n write: (chunk: Uint8Array) => Promise<void>;\n close: () => Promise<void>;\n abort?: () => Promise<void>;\n}\n\ninterface FileSystemFileHandleLike {\n createWritable: () => Promise<FileSystemWritable>;\n}\n\ninterface FileSystemDirectoryHandleLike {\n getFileHandle: (\n name: string,\n options?: { create?: boolean }\n ) => Promise<FileSystemFileHandleLike>;\n}\n\ntype DirectoryPicker = (options?: {\n mode?: string;\n id?: string;\n}) => Promise<FileSystemDirectoryHandleLike>;\n\n/**\n * Whether the browser can stream downloads straight to a folder on disk.\n *\n * Chromium-based browsers ship the File System Access API; Safari and Firefox\n * do not, and fall back to plain per-file downloads.\n *\n * @returns Whether `showDirectoryPicker` is available\n */\nconst supportsDirectoryPicker = (): boolean =>\n typeof (globalThis as { showDirectoryPicker?: DirectoryPicker })\n .showDirectoryPicker === 'function';\n\n/**\n * Get file extension from URL\n * @param url - URL to extract extension from\n * @returns File extension or default\n */\nconst getFileExtension = (url: string): string => {\n try {\n const u = new URL(url, globalThis.location?.origin || 'http://localhost');\n url = u.pathname;\n } catch {\n // keep original url (likely relative)\n }\n const path = url.split(/[?#]/)[0];\n const dot = path.lastIndexOf('.');\n return dot > -1 ? path.slice(dot + 1).toLowerCase() : 'file';\n};\n\n/**\n * Slugify a lesson title for safe use in filenames\n * @param lessonTitle - Title of the lesson\n * @returns Sanitized slug\n */\nconst slugifyTitle = (lessonTitle: string): string =>\n lessonTitle\n .toLowerCase()\n .replaceAll(/[^a-z0-9\\s]/g, '')\n .replaceAll(/\\s+/g, '-')\n .substring(0, 50);\n\n/**\n * Generate the filename an asset is saved as\n * @param contentType - Type of content being downloaded\n * @param url - URL to get extension from\n * @param lessonTitle - Title of the lesson\n * @returns Generated filename\n */\nconst generateFilename = (\n contentType: string,\n url: string,\n lessonTitle: string = 'aula'\n): string =>\n `${slugifyTitle(lessonTitle)}-${contentType}.${getFileExtension(url)}`;\n\n/**\n * Read the byte size of a remote file without downloading it.\n *\n * Weights per-file progress against the real payload — a lesson video can be\n * hundreds of MB while the board frames are ~1MB, so counting files would make\n * the bar jump. Returns 0 when the server omits `content-length` or the request\n * fails, and callers then fall back to counting files.\n *\n * @param url - URL to probe\n * @returns Size in bytes, or 0 when unknown\n */\nconst getRemoteFileSize = async (url: string): Promise<number> => {\n try {\n const response = await fetch(url, { method: 'HEAD', mode: 'cors' });\n if (!response.ok) return 0;\n return Number(response.headers.get('content-length')) || 0;\n } catch {\n return 0;\n }\n};\n\n/**\n * Stream a remote file straight into a file on disk.\n *\n * Nothing is buffered in memory: each chunk is written to the file handle as it\n * arrives. This is what makes a ~900MB lesson video downloadable — holding it\n * in a Blob (and zipping it) exhausts the tab's memory and silently drops the\n * assets queued behind it.\n *\n * @param url - URL to download\n * @param dirHandle - Directory the user granted write access to\n * @param filename - Name to save the file as\n * @param onBytes - Called with the byte length of each chunk written\n */\nconst streamToDisk = async (\n url: string,\n dirHandle: FileSystemDirectoryHandleLike,\n filename: string,\n onBytes: (chunkSize: number) => void\n): Promise<void> => {\n const response = await fetch(url, { mode: 'cors' });\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch file: ${response.status} ${response.statusText}`\n );\n }\n if (!response.body) {\n throw new Error('Response has no readable body');\n }\n\n const fileHandle = await dirHandle.getFileHandle(filename, { create: true });\n const writable = await fileHandle.createWritable();\n\n try {\n const reader = response.body.getReader();\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n await writable.write(value);\n onBytes(value.length);\n }\n\n await writable.close();\n } catch (error) {\n await writable.abort?.();\n throw error;\n }\n};\n\n/**\n * Save a file through a plain anchor click, letting the browser download it.\n *\n * Fallback for browsers without the File System Access API (Safari, Firefox).\n * Note the `download` attribute is ignored cross-origin, and the CDN does not\n * send `Content-Disposition: attachment`, so these browsers may open the asset\n * in a tab rather than save it. Fixing that properly means setting that header\n * on the CDN — it cannot be forced from the client.\n *\n * The browser streams the response to its own downloads folder, so memory is\n * not a concern — but it\n * cannot report progress, and firing several in a row is unreliable, which is\n * why the caller spaces them out.\n *\n * @param url - URL to download\n * @param filename - Suggested filename\n */\nconst triggerBrowserDownload = (url: string, filename: string): void => {\n const link = document.createElement('a');\n link.href = url;\n link.download = filename;\n link.rel = 'noopener noreferrer';\n\n document.body.appendChild(link);\n link.click();\n link.remove();\n};\n\n/**\n * DownloadButton component for downloading lesson content\n *\n * Downloads every asset of a lesson (video, podcast and board frames), matching\n * the mobile app's \"baixar aula completa\". Where the browser supports it, the\n * user picks a folder and each file is streamed straight to disk with real\n * progress; elsewhere the browser downloads them one by one.\n *\n * @param props - DownloadButton component props\n * @returns Download button element\n */\nconst DownloadButton = ({\n content,\n className,\n onDownloadStart,\n onDownloadComplete,\n onDownloadError,\n lessonTitle = 'aula',\n disabled = false,\n}: DownloadButtonProps) => {\n const [isDownloading, setIsDownloading] = useState(false);\n const [progress, setProgress] = useState(0);\n\n /**\n * Check if URL is valid and not empty\n * @param url - URL to validate\n * @returns Whether URL is valid\n */\n const isValidUrl = useCallback((url?: string): boolean => {\n return Boolean(\n url && url.trim() !== '' && url !== 'undefined' && url !== 'null'\n );\n }, []);\n\n /**\n * Get available download content\n * @returns Array of available download items\n */\n const getAvailableContent = useCallback((): DownloadItem[] => {\n const downloads: DownloadItem[] = [];\n\n if (isValidUrl(content.urlVideo)) {\n downloads.push({ type: 'video', url: content.urlVideo!, label: 'Vídeo' });\n }\n\n if (isValidUrl(content.urlPodcast)) {\n downloads.push({\n type: 'podcast',\n url: content.urlPodcast!,\n label: 'Podcast',\n });\n }\n\n if (isValidUrl(content.urlInitialFrame)) {\n downloads.push({\n type: 'quadro-inicial',\n url: content.urlInitialFrame!,\n label: 'Quadro Inicial',\n });\n }\n\n if (isValidUrl(content.urlFinalFrame)) {\n downloads.push({\n type: 'quadro-final',\n url: content.urlFinalFrame!,\n label: 'Quadro Final',\n });\n }\n\n return downloads;\n }, [content, isValidUrl]);\n\n /**\n * Download every asset by streaming it into the folder the user picked.\n *\n * @param items - Assets to download\n * @param dirHandle - Directory the user granted write access to\n */\n const downloadToFolder = useCallback(\n async (items: DownloadItem[], dirHandle: FileSystemDirectoryHandleLike) => {\n const sizes = await Promise.all(\n items.map((item) => getRemoteFileSize(item.url))\n );\n const totalBytes = sizes.reduce((sum, size) => sum + size, 0);\n // No content-length anywhere: weight each file equally instead of by bytes.\n const weighByFile = totalBytes === 0;\n\n let downloadedBytes = 0;\n let succeeded = 0;\n\n for (const [index, item] of items.entries()) {\n try {\n onDownloadStart?.(item.type);\n\n await streamToDisk(\n item.url,\n dirHandle,\n generateFilename(item.type, item.url, lessonTitle),\n (chunkSize) => {\n downloadedBytes += chunkSize;\n if (!weighByFile) {\n setProgress(\n Math.min(99, Math.round((downloadedBytes / totalBytes) * 100))\n );\n }\n }\n );\n\n succeeded++;\n onDownloadComplete?.(item.type);\n } catch (error) {\n onDownloadError?.(\n item.type,\n error instanceof Error\n ? error\n : new Error(`Falha ao baixar ${item.label}`)\n );\n } finally {\n if (weighByFile) {\n setProgress(\n Math.min(99, Math.round(((index + 1) / items.length) * 100))\n );\n }\n }\n }\n\n if (succeeded === 0) {\n throw new Error('Nenhum conteúdo da aula pôde ser baixado');\n }\n\n setProgress(100);\n },\n [lessonTitle, onDownloadStart, onDownloadComplete, onDownloadError]\n );\n\n /**\n * Hand each asset to the browser's own downloader, one at a time.\n *\n * Used where the File System Access API is missing. Downloads are spaced out\n * because browsers throttle several programmatic downloads fired at once.\n *\n * @param items - Assets to download\n */\n const downloadViaBrowser = useCallback(\n async (items: DownloadItem[]) => {\n for (const [index, item] of items.entries()) {\n onDownloadStart?.(item.type);\n\n triggerBrowserDownload(\n item.url,\n generateFilename(item.type, item.url, lessonTitle)\n );\n\n onDownloadComplete?.(item.type);\n setProgress(Math.round(((index + 1) / items.length) * 100));\n\n if (index < items.length - 1) {\n await new Promise((resolve) => setTimeout(resolve, 800));\n }\n }\n },\n [lessonTitle, onDownloadStart, onDownloadComplete]\n );\n\n /**\n * Download every asset of the lesson.\n *\n * Prefers streaming to a user-picked folder, which is the only approach that\n * survives a large lesson video. Falls back to the browser's downloader when\n * the File System Access API is unavailable.\n */\n const handleDownload = useCallback(async () => {\n if (disabled || isDownloading) return;\n\n const availableContent = getAvailableContent();\n if (availableContent.length === 0) return;\n\n let dirHandle: FileSystemDirectoryHandleLike | null = null;\n\n if (supportsDirectoryPicker()) {\n try {\n const picker = (globalThis as { showDirectoryPicker?: DirectoryPicker })\n .showDirectoryPicker!;\n dirHandle = await picker({ mode: 'readwrite', id: 'aulas' });\n } catch {\n // The user dismissed the folder picker: abort quietly, nothing to report.\n return;\n }\n }\n\n setIsDownloading(true);\n setProgress(0);\n\n try {\n if (dirHandle) {\n await downloadToFolder(availableContent, dirHandle);\n } else {\n await downloadViaBrowser(availableContent);\n }\n } catch (error) {\n onDownloadError?.(\n 'aula',\n error instanceof Error\n ? error\n : new Error('Falha ao baixar o conteúdo da aula')\n );\n } finally {\n setIsDownloading(false);\n setProgress(0);\n }\n }, [\n disabled,\n isDownloading,\n getAvailableContent,\n downloadToFolder,\n downloadViaBrowser,\n onDownloadError,\n ]);\n\n // Don't render if no content is available\n const availableCount = getAvailableContent().length;\n\n if (availableCount === 0) {\n return null;\n }\n\n const suffix = availableCount > 1 ? 's' : '';\n\n return (\n <div className={cn('flex items-center', className)}>\n <IconButton\n icon={<DownloadSimpleIcon size={24} />}\n onClick={handleDownload}\n disabled={disabled || isDownloading}\n aria-label={\n isDownloading\n ? 'Baixando conteúdo...'\n : `Baixar conteúdo da aula (${availableCount} arquivo${suffix})`\n }\n className={cn(\n '!bg-transparent hover:!bg-black/10 transition-colors',\n isDownloading && 'opacity-60 cursor-not-allowed'\n )}\n />\n\n <ProgressModal\n isOpen={isDownloading}\n onClose={() => {}}\n message=\"Baixando conteúdo da aula...\"\n progress={progress}\n />\n </div>\n );\n};\n\nexport default DownloadButton;\n"],"mappings":"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","names":[]}