import React from "react"; import _ from "lodash"; import {Grid, Header as SHeader, Image,} from "semantic-ui-react"; import {TagsSelector} from "../Tags"; import {Media} from "../contexts/contexts"; import {encodeMediaPath, isSupportedArchive} from "./Common"; import {getFile, tagFileGroup, untagFileGroup} from "../api"; import {ArchivePreviewContent} from "./ArchivePreview"; import {CbzViewer} from "./CbzViewer"; import {StlViewer} from "react-stl-viewer"; import {Button, Modal} from "./Theme"; import {toast} from "react-semantic-toasts-2"; import {useOneQuery} from "../hooks/customHooks"; import {ShareButton} from "./Share"; import {pathDirectory} from "./FileBrowser"; import {InlineErrorBoundary} from "./ErrorBoundary"; import {useLocation} from "react-router"; // Routes where file views should not be tracked const EXCLUDED_TRACKING_ROUTES = [ /^\/videos\/video\//, /^\/videos\/channel\/[^/]+\/video\//, /^\/archive\/\d+/ ]; function shouldSkipTracking(pathname) { return EXCLUDED_TRACKING_ROUTES.some(pattern => pattern.test(pathname)); } function getMediaPathURL(previewFile) { if (previewFile['primary_path']) { return `/media/${encodeMediaPath(previewFile['primary_path'])}`; } else { return `/media/${encodeMediaPath(previewFile['path'])}`; } } function getDownloadPathURL(previewFile) { if (previewFile['primary_path']) { return `/download/${encodeMediaPath(previewFile['primary_path'])}`; } else { return `/download/${encodeMediaPath(previewFile['path'])}`; } } function getEpubViewerURL(previewFile) { let url = previewFile['primary_path'] ?? previewFile['path']; return `/epub/epub.html?url=/media/${encodeMediaPath(url)}` } function getIframePreviewModal(previewFile, url) { url = url || getMediaPathURL(previewFile); return } function getImagePreviewModal(previewFile) { const path = previewFile.primary_path ?? previewFile.path; const url = getMediaPathURL(previewFile); // Get the file name from the absolute path. const name = path.replace(/^.*[\\\/]/, ''); return {name} } function getVideoPreviewModal(previewFile) { const url = getMediaPathURL(previewFile); const path = previewFile.primary_path ?? previewFile.path; const name = path.replace(/^.*[\\\/]/, ''); return {name} {path['path']} } function getAudioPreviewModal(previewFile) { const url = getMediaPathURL(previewFile); const path = previewFile.primary_path ?? previewFile.path; const name = path.replace(/^.*[\\\/]/, ''); // Some audio files may not have the correct mimetype. Trust the suffix identifies an audio file. const type = previewFile['mimetype'] !== 'application/octet-stream' ? previewFile['mimetype'] : 'audio/mpeg'; return {name} } function STLPreviewModal({previewFile}) { const [dimensions, setDimensions] = React.useState(null); const url = getMediaPathURL(previewFile); const path = previewFile.primary_path ?? previewFile.path; const name = path.replace(/^.*[\\\/]/, ''); return {name} {dimensions && Width: {dimensions.width.toFixed(2)} {' | '} Height: {dimensions.height.toFixed(2)} {' | '} Length: {dimensions.length.toFixed(2)} {' | '} Bounding Radius: {dimensions.boundingRadius.toFixed(2)} } setDimensions(dims)} /> } function getGenericPreviewModal(previewFile) { // No special handler for this file type, just open it. const path = previewFile.primary_path ?? previewFile.path; const name = path.replace(/^.*[\\\/]/, ''); return {name} } export const FilePreviewContext = React.createContext({ previewFile: null, setPreviewFile: null, previewModal: null, setPreviewModal: null, setCallbacks: null, }); const MAXIMUM_TEXT_SIZE = 5_000_000; export function FilePreviewProvider({children}) { // Data about the file that is being previewed. const [previewFile, setPreviewFile] = React.useState(null); // A modal is only displayed once we have data about the file. const [previewModal, setPreviewModal] = React.useState(null); const [callbacks, setCallbacks] = React.useState(null); // The file path from the `preview` URL query. const [previewQuery, setPreviewQuery] = useOneQuery('preview'); const [errorModalOpen, setErrorModalOpen] = React.useState(false); const location = useLocation(); // Determine if tracking should be skipped based on current route const skipTracking = shouldSkipTracking(location.pathname); const handleErrorModalClose = () => { setPreviewQuery(null); setErrorModalOpen(false); } const errorModal = Unknown File Cannot display preview, no such file exists: {previewQuery} Close React.useEffect(() => { if (!_.isEmpty(previewFile)) { // Change URL to match the file that is being previewed. setPreviewQuery(previewFile['primary_path'] || previewFile['path']); } }, [previewFile]); const initPreviewFile = async () => { // Get simple information about the file, preview the file. try { const file = await getFile(previewQuery, skipTracking); setPreviewFile(file); } catch (e) { console.error(e); setPreviewModal(errorModal); setErrorModalOpen(true); } } React.useEffect(() => { // Navigated to a page which has a file preview active, fetch the information about the file, then display it. if (previewQuery && !previewFile) { initPreviewFile(); } else if (!previewQuery && (previewFile || previewModal)) { // User navigated back/forward and preview query is gone, close the preview. // Only close if there's actually a preview open (previewFile or previewModal exists). setPreviewFile(null); setPreviewModal(null); } }, [previewQuery]); const localFetchFile = async () => { // Get the file again with its Tags. const {path, primary_path} = previewFile; try { const file = await getFile(primary_path ?? path, skipTracking); setPreviewFile(file); } finally { await handleCallbacks(); } }; const handleClose = (e) => { if (e) { e.preventDefault(); } setPreviewModal(null); setPreviewFile(null); setPreviewQuery(null); } const handleCallbacks = async () => { for (const callbacksKey in callbacks) { try { await callbacks[callbacksKey](); } catch (e) { console.error(e); } } } const localAddTag = async (name) => { await tagFileGroup(previewFile, name); await localFetchFile(); } const localRemoveTag = async (name) => { await untagFileGroup(previewFile, name); await localFetchFile(); } function setModalContent(content, url, downloadURL, path, taggable = true) { const openButton = url ? Open : null; const closeButton = Close; const tagsDisplay = taggable ? : null; const downloadButton = downloadURL ? : null; const directoryURL = path ? `/files?folders=${encodeURIComponent(pathDirectory(path))}` : null; const directoryButton = path ? : null; const pathContent = path ? {path} : null; console.log('Previewing', path); console.log('Preview Download URL', downloadURL); console.log('Preview Open URL', url); setPreviewModal( handleClose(e)} > {pathContent} {downloadButton}{directoryButton} {openButton} {closeButton} {tagsDisplay} {downloadButton}{directoryButton} {tagsDisplay} {openButton} {closeButton} {content} ); } React.useEffect(() => { setPreviewModal(null); if (previewFile && !_.isObject(previewFile)) { console.error(`Unknown previewFile type: ${typeof previewFile}`); toast({ type: 'error', title: 'Cannot preview file', description: 'Cannot preview file', time: 5000, }); return; } if (previewFile && !_.isEmpty(previewFile)) { const {mimetype, size, taggable} = previewFile; const path = previewFile['primary_path'] || previewFile['path']; console.debug('Previewing file', previewFile); const lowerPath = path.toLowerCase(); const url = getMediaPathURL(previewFile); const downloadURL = getDownloadPathURL(previewFile); if (mimetype.startsWith('text/') && size > MAXIMUM_TEXT_SIZE) { // Large text files should be downloaded. window.open(downloadURL); } else if (mimetype.startsWith('text/') || mimetype.startsWith('application/json')) { setModalContent(getIframePreviewModal(previewFile), url, downloadURL, path, taggable); } else if (mimetype.startsWith('video/')) { setModalContent(getVideoPreviewModal(previewFile), url, downloadURL, path, taggable); } else if (mimetype.startsWith('audio/')) { setModalContent(getAudioPreviewModal(previewFile), url, downloadURL, path, taggable); } else if (mimetype.startsWith('application/epub')) { const viewerURL = getEpubViewerURL(previewFile); setModalContent(getIframePreviewModal(previewFile, viewerURL), viewerURL, downloadURL, path, taggable); } else if (mimetype.startsWith('application/pdf')) { setModalContent(getIframePreviewModal(previewFile), url, downloadURL, path, taggable); } else if (mimetype.startsWith('image/')) { setModalContent(getImagePreviewModal(previewFile), url, null, path, taggable); } else if (mimetype.startsWith('model/stl')) { setModalContent(, null, downloadURL, path, taggable); } else if (mimetype.startsWith('application/octet-stream') && lowerPath.endsWith('.mp3')) { setModalContent(getAudioPreviewModal(previewFile), url, downloadURL, path, taggable); } else if (mimetype.startsWith('application/octet-stream') && lowerPath.endsWith('.stl')) { setModalContent(, null, downloadURL, path, taggable); } else if (mimetype.includes('cbz') || mimetype.includes('cbr') || mimetype.includes('comicbook+zip') || mimetype.includes('comicbook-rar') || lowerPath.endsWith('.cbz') || lowerPath.endsWith('.cbr') || lowerPath.endsWith('.cbt') || lowerPath.endsWith('.cb7')) { setModalContent(, null, downloadURL, path, taggable); } else if (isSupportedArchive(mimetype, lowerPath)) { setModalContent(, null, downloadURL, path, taggable); } else { // No special handler for this file type, just open it. setModalContent(getGenericPreviewModal(previewFile), url, downloadURL, path, taggable); } // Trigger tracking for the file view (unless on an excluded route) if (!skipTracking) { getFile(path).catch(e => { console.error(e); console.error('Failed to get file to set FileGroup.viewed'); }); } } }, [previewFile, skipTracking]); const value = {previewFile, setPreviewFile, previewModal, setPreviewModal, setCallbacks}; return {children} {previewModal} {errorModal} }
{path}