feat: Introduce MediaViewer component to centralize media rendering and refactor document preview and zoom overlay to use it, along with new CSS padding variables.

This commit is contained in:
2025-11-28 09:08:28 +01:00
parent 0b4a9441d0
commit 65ad1f6cfa
7 changed files with 151 additions and 87 deletions
+7 -69
View File
@@ -1,8 +1,8 @@
import { useCallback, useMemo, useRef } from 'react';
import type { JSX } from 'react';
import DocumentInfoPanel from '../documents/DocumentInfoPanel';
import { DownloadIcon } from '../ui/icons';
import PdfViewer from './PdfViewer';
import MediaViewer from './MediaViewer';
import { AUDIO_EXTENSIONS, VIDEO_EXTENSIONS } from '../constants/preview';
import type { Document } from '../types/documents';
@@ -79,28 +79,8 @@ const DocumentViewerLayout = ({
|| document.filename
|| document.original_name
|| '';
const fileExtension = getFileExtension(normalizedFilename);
const isImage = normalizedMimeType.startsWith('image/');
const isPdf = normalizedMimeType === 'application/pdf'
|| normalizedMimeType === 'application/x-pdf';
const isAudio = normalizedMimeType.startsWith('audio/')
|| AUDIO_EXTENSIONS.has(fileExtension);
const isVideo = normalizedMimeType.startsWith('video/')
|| VIDEO_EXTENSIONS.has(fileExtension);
const mediaLabel = document.title
|| normalizedFilename
|| 'Document preview';
if (isImage) {
return (
<img
src={documentLink.url}
alt={`Preview of ${document.title}`}
className="document-viewer__object document-viewer__object--image"
draggable={false}
/>
);
}
if (isPdf) {
const documentTitle = document.title
@@ -115,55 +95,13 @@ const DocumentViewerLayout = ({
);
}
if (isAudio) {
return (
<audio
className="document-viewer__object document-viewer__object--audio"
controls
preload="metadata"
src={documentLink.url}
aria-label={`Audio preview of ${mediaLabel}`}
>
</audio>
);
}
if (isVideo) {
return (
<video
className="document-viewer__object document-viewer__object--video"
controls
preload="metadata"
src={documentLink.url}
aria-label={`Video preview of ${mediaLabel}`}
>
</video>
);
}
const displayMimeType = document.mime_type || documentLink.mimeType || 'this file type';
const displayFilename = documentLink.filename
|| document.filename
|| document.original_name
|| 'download';
return (
<div className="document-viewer__unsupported">
<div className="document-viewer__unsupported-message">
Preview is not available for {displayMimeType} files.
</div>
<div className="document-viewer__unsupported-filename">{displayFilename}</div>
<a
className="button-link document-viewer__unsupported-download"
href={documentLink.url}
download={displayFilename}
target="_blank"
rel="noopener noreferrer"
>
<DownloadIcon />
<span>Download</span>
</a>
</div>
<MediaViewer
src={documentLink.url}
mimeType={normalizedMimeType}
filename={normalizedFilename}
alt={`Preview of ${document.title}`}
/>
);
}, [document, documentLink, viewportRef]);
+117
View File
@@ -0,0 +1,117 @@
import React from 'react';
import { DownloadIcon } from '../ui/icons';
import { AUDIO_EXTENSIONS, VIDEO_EXTENSIONS } from '../constants/preview';
export interface MediaViewerProps {
src: string;
mimeType?: string;
filename?: string;
alt?: string;
className?: string;
style?: React.CSSProperties;
onLoad?: (event: React.SyntheticEvent<HTMLElement>) => void;
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
mediaRef?: React.Ref<any>;
draggable?: boolean;
}
const getFileExtension = (filename?: string | null) => {
if (!filename) {
return '';
}
const match = filename.toLowerCase().match(/\.([a-z0-9]+)$/);
return match ? match[1] : '';
};
const MediaViewer: React.FC<MediaViewerProps> = ({
src,
mimeType = '',
filename = '',
alt = 'Media preview',
className,
style,
onLoad,
onClick,
mediaRef,
draggable = false,
}) => {
const normalizedMimeType = mimeType.toLowerCase();
const fileExtension = getFileExtension(filename);
const isImage = normalizedMimeType.startsWith('image/');
const isAudio = normalizedMimeType.startsWith('audio/') || AUDIO_EXTENSIONS.has(fileExtension);
const isVideo = normalizedMimeType.startsWith('video/') || VIDEO_EXTENSIONS.has(fileExtension);
const viewerClasses = ['document-viewer__object', className].filter(Boolean).join(' ');
if (isImage) {
return (
<img
ref={mediaRef}
src={src}
alt={alt}
className={`${viewerClasses} document-viewer__object--image`}
style={style}
onLoad={onLoad}
onClick={onClick}
draggable={draggable}
/>
);
}
if (isAudio) {
return (
<audio
ref={mediaRef}
className={`${viewerClasses} document-viewer__object--audio`}
controls
preload="metadata"
src={src}
aria-label={`Audio preview of ${alt}`}
style={style}
onLoadedMetadata={onLoad}
onClick={onClick}
/>
);
}
if (isVideo) {
return (
<video
ref={mediaRef}
className={`${viewerClasses} document-viewer__object--video`}
controls
preload="metadata"
src={src}
aria-label={`Video preview of ${alt}`}
style={style}
onLoadedMetadata={onLoad}
onClick={onClick}
/>
);
}
const displayMimeType = mimeType || 'this file type';
const displayFilename = filename || 'download';
return (
<div className="document-viewer__unsupported" style={style}>
<div className="document-viewer__unsupported-message">
Preview is not available for {displayMimeType} files.
</div>
<div className="document-viewer__unsupported-filename">{displayFilename}</div>
<a
className="button-link document-viewer__unsupported-download"
href={src}
download={displayFilename}
target="_blank"
rel="noopener noreferrer"
>
<DownloadIcon />
<span>Download</span>
</a>
</div>
);
};
export default MediaViewer;
+8 -4
View File
@@ -172,13 +172,15 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
const attach = () => {
const stackElement = containerRef.current;
const viewportElement = viewportRef?.current;
if (!stackElement || !viewportElement) {
const sizingElement = stackElement?.parentElement;
if (!stackElement || !sizingElement) {
frameId = requestAnimationFrame(attach);
return;
}
const updateBounds = () => {
const { width, height } = getAvailableViewportSize(viewportElement, stackElement);
const { width, height } = getAvailableViewportSize(sizingElement, stackElement);
const nextWidth = Math.max(1, Math.round(width || 0));
const nextHeight = Math.max(1, Math.round(height || 0));
setViewportWidth((prev) => (prev === nextWidth ? prev : nextWidth));
@@ -201,8 +203,10 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
};
observeNode(stackElement);
observeNode(stackElement.parentElement);
observeNode(viewportElement);
observeNode(sizingElement);
if (viewportElement) {
observeNode(viewportElement);
}
observeNode(stackElement.closest('.document-viewer__viewport'));
};