mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Semantic UI may activate a Dropdown.Item from the keyboard without following its href, which would save the slicer preference without launching the slicer. Prevent the default anchor navigation and call window.location.assign() so every activation path behaves the same. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
635 lines
27 KiB
JavaScript
635 lines
27 KiB
JavaScript
import React from "react";
|
|
import _ from "lodash";
|
|
import {Button as SButton, Dropdown, Header as SHeader, Icon, Image,} from "semantic-ui-react";
|
|
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, Modal} from "./Theme";
|
|
import {toast} from "react-semantic-toasts-2";
|
|
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";
|
|
|
|
// 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 <SButton.Group color='violet' size='small'>
|
|
<Button color='violet' as='a' href={getSlicerURL(previewFile, slicer.scheme)} icon labelPosition='left'>
|
|
<Icon name='cube'/>Open in {slicer.text}
|
|
</Button>
|
|
<Dropdown className='button icon' floating trigger={<></>}>
|
|
<Dropdown.Menu>
|
|
{SLICERS.map(s =>
|
|
<Dropdown.Item key={s.key} text={`Open in ${s.text}`}
|
|
as='a' href={getSlicerURL(previewFile, s.scheme)}
|
|
onClick={(e) => {
|
|
// 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));
|
|
}}/>)}
|
|
</Dropdown.Menu>
|
|
</Dropdown>
|
|
</SButton.Group>
|
|
}
|
|
|
|
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 <Modal.Content>
|
|
<div className='preview-fit'>
|
|
<iframe title='textModal' src={url}
|
|
style={{
|
|
height: '100%', width: '100%', border: 'none',
|
|
// Use white to avoid iframe displaying with dark-theme.
|
|
backgroundColor: '#ffffff',
|
|
}}/>
|
|
</div>
|
|
</Modal.Content>
|
|
}
|
|
|
|
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 <React.Fragment>
|
|
<Modal.Header>
|
|
{name}
|
|
</Modal.Header>
|
|
<Modal.Content>
|
|
<div className='preview-fit'>
|
|
<a href={url}>
|
|
<Image src={url} style={{maxHeight: '100%', maxWidth: '100%', objectFit: 'contain'}}/>
|
|
</a>
|
|
</div>
|
|
</Modal.Content>
|
|
</React.Fragment>
|
|
}
|
|
|
|
function getVideoPreviewModal(previewFile) {
|
|
const url = getMediaPathURL(previewFile);
|
|
const path = previewFile.primary_path ?? previewFile.path;
|
|
const name = path.replace(/^.*[\\\/]/, '');
|
|
return <React.Fragment>
|
|
<Modal.Header>
|
|
{name}
|
|
</Modal.Header>
|
|
<Modal.Content>
|
|
<SHeader as='h5'>{path['path']}</SHeader>
|
|
<div className='preview-fit'>
|
|
<video controls
|
|
autoPlay={true}
|
|
id="player"
|
|
playsInline={true}
|
|
style={{maxHeight: '100%', maxWidth: '100%'}}
|
|
>
|
|
<source src={url}/>
|
|
</video>
|
|
</div>
|
|
</Modal.Content>
|
|
</React.Fragment>
|
|
}
|
|
|
|
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 <React.Fragment>
|
|
<Modal.Header>
|
|
{name}
|
|
</Modal.Header>
|
|
<Modal.Content>
|
|
<audio controls
|
|
autoPlay={true}
|
|
id="player"
|
|
playsInline={true}
|
|
style={{width: '90%', maxWidth: '95%'}}
|
|
>
|
|
<source src={url} type={type}/>
|
|
</audio>
|
|
</Modal.Content>
|
|
</React.Fragment>
|
|
}
|
|
|
|
function ThreeMFPreviewModal({previewFile}) {
|
|
const containerRef = React.useRef(null);
|
|
const [dimensions, setDimensions] = React.useState(null);
|
|
const [error, setError] = React.useState(null);
|
|
const url = getMediaPathURL(previewFile);
|
|
const path = previewFile.primary_path ?? previewFile.path;
|
|
const name = path.replace(/^.*[\\\/]/, '');
|
|
|
|
React.useEffect(() => {
|
|
// Imperative three.js viewer — mirrors react-stl-viewer behavior for .3mf files.
|
|
let cancelled = false;
|
|
const container = containerRef.current;
|
|
if (!container) return;
|
|
|
|
let renderer, scene, camera, controls, animationId, resizeObserver, loadedGroup;
|
|
let THREE;
|
|
|
|
(async () => {
|
|
try {
|
|
THREE = await import('three');
|
|
const {OrbitControls} = await import('three/examples/jsm/controls/OrbitControls.js');
|
|
const {ThreeMFLoader} = await import('three/examples/jsm/loaders/3MFLoader.js');
|
|
if (cancelled) return;
|
|
|
|
scene = new THREE.Scene();
|
|
// Transparent background so the modal/theme color shows through,
|
|
// matching react-stl-viewer's behavior.
|
|
|
|
const width = container.clientWidth || 1;
|
|
const height = container.clientHeight || 1;
|
|
|
|
camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 10000);
|
|
camera.position.set(0, 0, 100);
|
|
|
|
renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});
|
|
renderer.setClearColor(0x000000, 0);
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
renderer.setSize(width, height);
|
|
container.appendChild(renderer.domElement);
|
|
|
|
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
|
|
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
dirLight.position.set(1, 1, 1);
|
|
scene.add(dirLight);
|
|
const dirLight2 = new THREE.DirectionalLight(0xffffff, 0.4);
|
|
dirLight2.position.set(-1, -1, -1);
|
|
scene.add(dirLight2);
|
|
|
|
controls = new OrbitControls(camera, renderer.domElement);
|
|
controls.enableDamping = true;
|
|
|
|
const loader = new ThreeMFLoader();
|
|
loader.load(
|
|
url,
|
|
(group) => {
|
|
if (cancelled) return;
|
|
loadedGroup = group;
|
|
|
|
// Match react-stl-viewer's violet so 3MFs without authored
|
|
// materials are visible against either theme's background.
|
|
// 3MF geometry often lacks vertex normals — compute them so
|
|
// MeshPhongMaterial can shade faces instead of rendering flat black.
|
|
group.traverse((obj) => {
|
|
if (obj.isMesh) {
|
|
if (obj.geometry && !obj.geometry.attributes.normal) {
|
|
obj.geometry.computeVertexNormals();
|
|
}
|
|
obj.material = new THREE.MeshPhongMaterial({
|
|
color: 0x7353a8,
|
|
flatShading: false,
|
|
});
|
|
}
|
|
});
|
|
|
|
scene.add(group);
|
|
|
|
// Frame the model: center it, then pull camera back along Z by bounding radius.
|
|
const box = new THREE.Box3().setFromObject(group);
|
|
const size = new THREE.Vector3();
|
|
const center = new THREE.Vector3();
|
|
box.getSize(size);
|
|
box.getCenter(center);
|
|
group.position.sub(center);
|
|
|
|
const boundingRadius = Math.max(size.x, size.y, size.z) / 2 || 1;
|
|
const fitDistance = boundingRadius / Math.tan((Math.PI * camera.fov) / 360);
|
|
camera.position.set(0, 0, fitDistance * 2.2);
|
|
camera.near = Math.max(0.1, fitDistance / 1000);
|
|
camera.far = fitDistance * 100;
|
|
camera.updateProjectionMatrix();
|
|
controls.target.set(0, 0, 0);
|
|
controls.update();
|
|
|
|
setDimensions({
|
|
width: size.x,
|
|
height: size.y,
|
|
length: size.z,
|
|
boundingRadius: size.length() / 2,
|
|
});
|
|
},
|
|
undefined,
|
|
(err) => {
|
|
console.error('Failed to load 3MF', err);
|
|
if (!cancelled) setError('Failed to load 3MF file.');
|
|
},
|
|
);
|
|
|
|
const animate = () => {
|
|
animationId = requestAnimationFrame(animate);
|
|
controls.update();
|
|
renderer.render(scene, camera);
|
|
};
|
|
animate();
|
|
|
|
resizeObserver = new ResizeObserver(() => {
|
|
const w = container.clientWidth || 1;
|
|
const h = container.clientHeight || 1;
|
|
camera.aspect = w / h;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(w, h);
|
|
});
|
|
resizeObserver.observe(container);
|
|
} catch (err) {
|
|
console.error('3MF viewer init failed', err);
|
|
if (!cancelled) setError('Failed to initialize 3MF viewer.');
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
if (animationId) cancelAnimationFrame(animationId);
|
|
if (resizeObserver) resizeObserver.disconnect();
|
|
if (controls) controls.dispose();
|
|
if (loadedGroup && THREE) {
|
|
loadedGroup.traverse((obj) => {
|
|
if (obj.geometry) obj.geometry.dispose();
|
|
if (obj.material) {
|
|
const materials = Array.isArray(obj.material) ? obj.material : [obj.material];
|
|
materials.forEach((m) => {
|
|
if (m.map) m.map.dispose();
|
|
m.dispose();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
if (renderer) {
|
|
renderer.dispose();
|
|
if (renderer.domElement && renderer.domElement.parentNode) {
|
|
renderer.domElement.parentNode.removeChild(renderer.domElement);
|
|
}
|
|
}
|
|
};
|
|
}, [url]);
|
|
|
|
return <React.Fragment>
|
|
<Modal.Header>{name}</Modal.Header>
|
|
<Modal.Content>
|
|
{dimensions && <div style={{marginBottom: '1em', textAlign: 'center'}}>
|
|
<b>Width:</b> {dimensions.width.toFixed(2)}
|
|
{' | '}
|
|
<b>Height:</b> {dimensions.height.toFixed(2)}
|
|
{' | '}
|
|
<b>Length:</b> {dimensions.length.toFixed(2)}
|
|
{' | '}
|
|
<b>Bounding Radius:</b> {dimensions.boundingRadius.toFixed(2)}
|
|
{' (model units)'}
|
|
</div>}
|
|
{error && <div style={{color: 'red', textAlign: 'center'}}>{error}</div>}
|
|
<InlineErrorBoundary>
|
|
<div className='preview-fit'>
|
|
<div ref={containerRef} style={{height: '100%', width: '100%'}}/>
|
|
</div>
|
|
</InlineErrorBoundary>
|
|
</Modal.Content>
|
|
</React.Fragment>
|
|
}
|
|
|
|
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 <React.Fragment>
|
|
<Modal.Header>{name}</Modal.Header>
|
|
<Modal.Content>
|
|
{dimensions && <div style={{marginBottom: '1em', textAlign: 'center'}}>
|
|
<b>Width:</b> {dimensions.width.toFixed(2)}
|
|
{' | '}
|
|
<b>Height:</b> {dimensions.height.toFixed(2)}
|
|
{' | '}
|
|
<b>Length:</b> {dimensions.length.toFixed(2)}
|
|
{' | '}
|
|
<b>Bounding Radius:</b> {dimensions.boundingRadius.toFixed(2)}
|
|
</div>}
|
|
<InlineErrorBoundary>
|
|
<div className='preview-fit'>
|
|
<StlViewer orbitControls shadows
|
|
url={url}
|
|
style={{height: '100%', width: '100%'}}
|
|
modelProps={{color: '#7353a8'}}
|
|
onFinishLoading={(dims) => setDimensions(dims)}
|
|
/>
|
|
</div>
|
|
</InlineErrorBoundary>
|
|
</Modal.Content>
|
|
</React.Fragment>
|
|
}
|
|
|
|
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 <React.Fragment>
|
|
<Modal.Header>{name}</Modal.Header>
|
|
</React.Fragment>
|
|
}
|
|
|
|
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 = <Modal closeIcon
|
|
open={errorModalOpen}
|
|
onClose={handleErrorModalClose}
|
|
>
|
|
<Modal.Header>Unknown File</Modal.Header>
|
|
<Modal.Content>Cannot display preview, no such file exists: {previewQuery}</Modal.Content>
|
|
<Modal.Actions>
|
|
<Button onClick={handleErrorModalClose}>Close</Button>
|
|
</Modal.Actions>
|
|
</Modal>
|
|
|
|
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, extraButtons = null) {
|
|
// All action buttons share `size='small'` and `icon` for consistent height across viewports.
|
|
// Close is handled by the Modal's `closeIcon` (X in the top-right corner) — no dedicated button.
|
|
const openButton = url
|
|
? <Button color='blue' as='a' href={url} size='small' icon labelPosition='left'>
|
|
<Icon name='external'/>Open
|
|
</Button>
|
|
: null;
|
|
const tagsDisplay = taggable ?
|
|
<TagsSelector selectedTagNames={previewFile['tags']} onAdd={localAddTag}
|
|
onRemove={localRemoveTag}/>
|
|
: null;
|
|
const downloadButton = downloadURL
|
|
? <Button color='yellow' as='a' href={downloadURL} size='small' icon='download'/>
|
|
: null;
|
|
const directoryURL = path ? `/files?folders=${encodeURIComponent(pathDirectory(path))}` : null;
|
|
const directoryButton = path ? <Button as='a' href={directoryURL} size='small' icon='folder'/> : null;
|
|
const pathContent = path ? <Modal.Content>
|
|
<pre>{path}</pre>
|
|
</Modal.Content>
|
|
: null;
|
|
console.log('Previewing', path);
|
|
console.log('Preview Download URL', downloadURL);
|
|
console.log('Preview Open URL', url);
|
|
|
|
// Single flex toolbar — same button order at every viewport size.
|
|
// `flex-wrap: wrap` lets the tags selector drop to a second line on narrow screens
|
|
// while the button groups stay together and in order: [file actions] [tags] [open/close].
|
|
setPreviewModal(<Modal closeIcon
|
|
size='fullscreen'
|
|
open={true}
|
|
onClose={e => handleClose(e)}
|
|
>
|
|
{pathContent}
|
|
<Modal.Actions>
|
|
<div className='preview-toolbar'>
|
|
<div className='preview-toolbar-group'>
|
|
{downloadButton}
|
|
{directoryButton}
|
|
<ShareButton/>
|
|
{extraButtons}
|
|
{previewFile && previewFile['id'] &&
|
|
<AddToPlaylistButton fileGroupId={previewFile['id']} content={null} size='small'
|
|
title='Add to Playlist'/>}
|
|
</div>
|
|
<div className='preview-toolbar-tags'>{tagsDisplay}</div>
|
|
<div className='preview-toolbar-group'>
|
|
{openButton}
|
|
</div>
|
|
</div>
|
|
</Modal.Actions>
|
|
{content}
|
|
</Modal>);
|
|
}
|
|
|
|
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(<STLPreviewModal previewFile={previewFile}/>, null, downloadURL, path, taggable,
|
|
<OpenInSlicerButton previewFile={previewFile}/>);
|
|
} else if (mimetype.startsWith('model/3mf')) {
|
|
setModalContent(<ThreeMFPreviewModal previewFile={previewFile}/>, null, downloadURL, path, taggable,
|
|
<OpenInSlicerButton previewFile={previewFile}/>);
|
|
} 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(<STLPreviewModal previewFile={previewFile}/>, null, downloadURL, path, taggable,
|
|
<OpenInSlicerButton previewFile={previewFile}/>);
|
|
} else if ((mimetype.startsWith('application/octet-stream') || mimetype === 'application/zip')
|
|
&& lowerPath.endsWith('.3mf')) {
|
|
setModalContent(<ThreeMFPreviewModal previewFile={previewFile}/>, null, downloadURL, path, taggable,
|
|
<OpenInSlicerButton previewFile={previewFile}/>);
|
|
} 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(<Modal.Content><CbzViewer path={path}/></Modal.Content>, null, downloadURL, path, taggable);
|
|
} else if (isSupportedArchive(mimetype, lowerPath)) {
|
|
setModalContent(<ArchivePreviewContent previewFile={previewFile}/>, 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 <FilePreviewContext.Provider value={value}>
|
|
{children}
|
|
{previewModal}
|
|
{errorModal}
|
|
</FilePreviewContext.Provider>
|
|
}
|