import React from "react"; import _ from "lodash"; import {TagsSelector} from "../Tags"; import {encodeMediaPath, isSupportedArchive, useLocalStorage} from "./Common"; import {getFile, tagFileGroup, untagFileGroup} from "../api"; import {ArchivePreviewContent} from "./ArchivePreview"; import {CbzViewer} from "./CbzViewer"; import {StlViewer} from "react-stl-viewer"; import {Button, ButtonGroup, Header, Icon, IconButton, MediaGate, Menu, Modal, toast} from "./ui"; import {useOneQuery} from "../hooks/customHooks"; import {ShareButton} from "./Share"; import {AddToPlaylistButton} from "./AddToPlaylist"; import {pathDirectory} from "./FileBrowser"; import {InlineErrorBoundary} from "./ErrorBoundary"; import {useLocation} from "react-router"; import {ThemeContext} from "../contexts/contexts"; // 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'])}`; } } // PrusaSlicer is omitted because it only downloads from domains whitelisted by Prusa // (printables.com, thingiverse.com, cults3d.com), so it cannot fetch from a WROLPi. // Bambu Studio asks the user to confirm because a WROLPi is not one of its trusted sites. export const SLICERS = [ {key: 'orcaslicer', text: 'Orca Slicer', scheme: 'orcaslicer'}, {key: 'bambustudio', text: 'Bambu Studio', scheme: 'bambustudio'}, {key: 'cura', text: 'Cura', scheme: 'cura'}, ]; export function getSlicerURL(previewFile, scheme) { // Slicers register their own URL scheme and download the file themselves, so the // file parameter must be an absolute URL reachable from the user's computer. const absoluteURL = new URL(getMediaPathURL(previewFile), window.location.origin).href; return `${scheme}://open?file=${encodeURIComponent(absoluteURL)}`; } function OpenInSlicerButton({previewFile}) { // Browsers cannot report which slicers are installed, so remember the last one used. const [slicerKey, setSlicerKey] = useLocalStorage('preferredSlicer', SLICERS[0].key); const slicer = SLICERS.find(s => s.key === slicerKey) || SLICERS[0]; return {SLICERS.map(s => { // Navigate programmatically so keyboard activation (which may not // follow the href) still launches the slicer. if (e) e.preventDefault(); setSlicerKey(s.key); window.location.assign(getSlicerURL(previewFile, s.scheme)); }} > Open in {s.text} )} } function getEpubViewerURL(previewFile) { let url = previewFile['primary_path'] ?? previewFile['path']; return `/epub/epub.html?url=/media/${encodeMediaPath(url)}` } /* * The iframe body of a preview. * * A component rather than part of `getIframePreviewModal` because it reads the theme: the * modal builders are plain functions called from an event handler, where hooks cannot run. */ export function IframePreview({url, gatePdf}) { const {isDark, mediaFilterEnabled} = React.useContext(ThemeContext); /* * `gatePdf` only for PDFs. Text and the EPUB viewer are documents of our own, which the * theme's media filter tints; Chrome's PDF viewer is composited out of process and arrives * in full color however the iframe is styled. * * Keyed by `url`, which is load-bearing rather than tidy. The effect that rebuilds a * preview sets the modal to null and then to the new content in one pass, so React 18 * batches both and reconciles instead of unmounting: the MediaGate fiber survives, and a * reader who revealed one PDF would get the next one in full color with no prompt -- the * exact flash this exists to prevent. The key forces a remount per document. */ return