mirror of
https://github.com/suitenumerique/drive.git
synced 2026-04-25 17:15:19 +02:00
♻️(frontend) rewrite preview viewers and FilesPreview
Reorganize all viewer components under a viewers/ directory and rewrite FilesPreview as the single orchestrator at the preview root. Add ZoomControls shared component for image and PDF viewers. The new layout centralizes the preview chrome (header, controls bar, navigation) in FilesPreview while each viewer only handles its own rendering.
This commit is contained in:
@@ -14,8 +14,8 @@ import {
|
||||
IconSize,
|
||||
} from "@gouvfr-lasuite/ui-kit";
|
||||
import { useMemo } from "react";
|
||||
import { FilePreviewType } from "@/features/ui/preview/files-preview/FilesPreview";
|
||||
import { getExtensionFromName } from "../../utils/utils";
|
||||
import { FilePreviewType } from "@/features/ui/preview/FilesPreview";
|
||||
|
||||
type ItemIconProps = {
|
||||
item: Item;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
WorkspaceType,
|
||||
} from "@/features/drivers/types";
|
||||
import i18n from "@/features/i18n/initI18n";
|
||||
import { FilePreviewType } from "@/features/ui/preview/files-preview/FilesPreview";
|
||||
import { FilePreviewType } from "@/features/ui/preview/FilesPreview";
|
||||
import { DefaultRoute } from "@/utils/defaultRoutes";
|
||||
|
||||
/**
|
||||
@@ -125,10 +125,7 @@ export const getExtensionFromName = (str: string) => {
|
||||
|
||||
const SIZE_UNIT_KEYS = ["B", "KB", "MB", "GB", "TB", "PB"] as const;
|
||||
|
||||
export const formatSize = (
|
||||
size: number,
|
||||
t?: (key: string) => string,
|
||||
) => {
|
||||
export const formatSize = (size: number, t?: (key: string) => string) => {
|
||||
let convertedSize = size;
|
||||
let unitIndex = 0;
|
||||
|
||||
@@ -138,9 +135,7 @@ export const formatSize = (
|
||||
}
|
||||
|
||||
const unitKey = SIZE_UNIT_KEYS[unitIndex];
|
||||
const unit = t
|
||||
? t(`explorer.grid.size_units.${unitKey}`)
|
||||
: unitKey;
|
||||
const unit = t ? t(`explorer.grid.size_units.${unitKey}`) : unitKey;
|
||||
|
||||
return `${
|
||||
convertedSize < 10
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.c__modal__backdrop:has(.file-preview-container) {
|
||||
.c__modal__backdrop:has(.file-preview__container) {
|
||||
background: var(--c--contextuals--background--semantic--contextual--primary);
|
||||
backdrop-filter: blur(8px);
|
||||
.c__modal {
|
||||
@@ -15,12 +15,12 @@
|
||||
* have a more opaque backdrop color.
|
||||
*/
|
||||
.ReactModalPortal:has(.explorer__search__modal)
|
||||
~ .ReactModalPortal:has(.file-preview-container)
|
||||
~ .ReactModalPortal:has(.file-preview__container)
|
||||
.c__modal__backdrop {
|
||||
background-color: var(--c--contextuals--background--surface--secondary);
|
||||
}
|
||||
|
||||
.file-preview-container {
|
||||
.file-preview__container {
|
||||
width: 100%;
|
||||
height: 100dvh;
|
||||
display: flex;
|
||||
@@ -28,7 +28,7 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-preview-header {
|
||||
.file-preview__header {
|
||||
height: 52px;
|
||||
border-bottom: 1px solid var(--c--contextuals--border--surface--primary);
|
||||
flex-shrink: 0;
|
||||
@@ -44,7 +44,7 @@
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
|
||||
&-left {
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--c--globals--spacings--3xs);
|
||||
@@ -52,20 +52,7 @@
|
||||
min-width: 0; // Allow shrinking
|
||||
}
|
||||
|
||||
&-center {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
color: red;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&-right {
|
||||
&__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--c--globals--spacings--3xs);
|
||||
@@ -76,7 +63,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.file-preview-title {
|
||||
.file-preview__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--c--globals--spacings--3xs);
|
||||
@@ -90,7 +77,7 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-preview-content {
|
||||
.file-preview__content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
max-width: 100dvw;
|
||||
@@ -111,7 +98,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.file-preview-main {
|
||||
.file-preview__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
transition: all 0.3s ease-in-out;
|
||||
@@ -126,7 +113,7 @@
|
||||
height: calc(100vh - 52px);
|
||||
|
||||
border-left: 1px solid var(--c--contextuals--border--surface--primary);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
transition: transform 0.3s;
|
||||
z-index: 9998;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
@@ -138,8 +125,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.file-preview-container.sidebar-open {
|
||||
.file-preview-main {
|
||||
.file-preview__container--sidebar-open {
|
||||
.file-preview__main {
|
||||
width: calc(100% - 300px);
|
||||
margin-right: 300px;
|
||||
}
|
||||
@@ -175,7 +162,7 @@
|
||||
}
|
||||
|
||||
// Styles for different viewer types
|
||||
.file-preview-viewer {
|
||||
.file-preview__viewer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
@@ -246,17 +233,58 @@
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// -- Navigation buttons --
|
||||
//
|
||||
|
||||
.file-preview__next-button,
|
||||
.file-preview__previous-button {
|
||||
position: absolute;
|
||||
top: calc(50% - 20px);
|
||||
background-color: white;
|
||||
transition: transform 0.3s;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--xs, 8px);
|
||||
border: 1px solid var(--c--contextuals--border--surface--primary);
|
||||
background: var(--c--contextuals--background--surface--primary);
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.file-preview__next-button {
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.file-preview__previous-button {
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
.file-preview__container--pdf-sidebar-open {
|
||||
.file-preview__previous-button {
|
||||
transform: translateX(150px);
|
||||
}
|
||||
}
|
||||
|
||||
.file-preview__container--sidebar-open {
|
||||
.file-preview__next-button {
|
||||
transform: translateX(-300px);
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.file-preview-header-content {
|
||||
.file-preview__header__content {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.file-preview-title {
|
||||
.file-preview__title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.file-preview-content > * {
|
||||
.file-preview__content > * {
|
||||
min-height: calc(100dvh - 84px);
|
||||
}
|
||||
|
||||
@@ -278,10 +306,56 @@
|
||||
}
|
||||
}
|
||||
|
||||
.file-preview-container.sidebar-open {
|
||||
.file-preview-main {
|
||||
.file-preview__container--sidebar-open {
|
||||
.file-preview__main {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.file-preview__next-button,
|
||||
.file-preview__previous-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.file-preview__controls {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--c--globals--spacings--3xs);
|
||||
padding: var(--c--globals--spacings--3xs);
|
||||
border-radius: var(--xs, 8px);
|
||||
border: 1px solid var(--c--contextuals--border--surface--primary);
|
||||
background: var(--c--contextuals--background--surface--primary);
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
|
||||
|
||||
&__separator {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
border-radius: 1px;
|
||||
overflow: hidden;
|
||||
background: var(--c--contextuals--border--surface--primary);
|
||||
}
|
||||
|
||||
&__zoom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--c--globals--spacings--3xs);
|
||||
|
||||
&__value {
|
||||
text-align: center;
|
||||
min-width: 35px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--c--contextuals--content--semantic--neutral--secondary);
|
||||
cursor: pointer;
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
419
src/frontend/apps/drive/src/features/ui/preview/FilesPreview.tsx
Normal file
419
src/frontend/apps/drive/src/features/ui/preview/FilesPreview.tsx
Normal file
@@ -0,0 +1,419 @@
|
||||
import {
|
||||
getMimeCategory,
|
||||
MimeCategory,
|
||||
removeFileExtension,
|
||||
} from "@/features/explorer/utils/mimeTypes";
|
||||
import { DropdownMenu, Icon, IconType } from "@gouvfr-lasuite/ui-kit";
|
||||
import {
|
||||
Button,
|
||||
ButtonProps,
|
||||
Modal,
|
||||
ModalSize,
|
||||
} from "@gouvfr-lasuite/cunningham-react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import posthog from "posthog-js";
|
||||
import dynamic from "next/dynamic";
|
||||
import clsx from "clsx";
|
||||
import { VideoPlayer } from "./viewers/video-player/VideoPlayer";
|
||||
import { AudioPlayer } from "./viewers/audio-player/AudioPlayer";
|
||||
import { ImageViewer } from "./viewers/image-viewer/ImageViewer";
|
||||
import { SuspiciousPreview } from "./viewers/suspicious/SuspiciousPreview";
|
||||
import { NotSupportedPreview } from "./viewers/not-supported/NotSupportedPreview";
|
||||
import { WopiEditor } from "./viewers/wopi/WopiEditor";
|
||||
import { OPEN_DELAY } from "./viewers/pdf-preview/pdfConsts";
|
||||
import { FileIcon } from "@/features/explorer/components/icons/ItemIcon";
|
||||
|
||||
const PreviewPdf = dynamic<{
|
||||
src: string;
|
||||
onThumbailSidebarOpen?: (isOpen: boolean) => void;
|
||||
}>(
|
||||
() =>
|
||||
import("./viewers/pdf-preview/PdfPreview")
|
||||
.then((mod) => mod.PdfPreview)
|
||||
.catch(() => {
|
||||
const {
|
||||
OutdatedBrowserPreview,
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
} = require("./viewers/pdf-preview/OutdatedBrowserPreview");
|
||||
return OutdatedBrowserPreview;
|
||||
}),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
export type FilePreviewType = {
|
||||
id: string;
|
||||
size: number;
|
||||
title: string;
|
||||
mimetype: string;
|
||||
is_wopi_supported?: boolean;
|
||||
url_preview: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type FilePreviewData = FilePreviewType & {
|
||||
category: MimeCategory;
|
||||
isSuspicious?: boolean;
|
||||
};
|
||||
|
||||
interface FilePreviewProps {
|
||||
isOpen: boolean;
|
||||
onClose?: () => void;
|
||||
title?: string;
|
||||
files?: FilePreviewType[];
|
||||
initialIndexFile?: number;
|
||||
openedFileId?: string;
|
||||
headerRightContent?: React.ReactNode;
|
||||
sidebarContent?: React.ReactNode;
|
||||
onChangeFile?: (file?: FilePreviewType) => void;
|
||||
handleDownloadFile?: (file?: FilePreviewType) => void;
|
||||
hideCloseButton?: boolean;
|
||||
onFileRename?: (file: FilePreviewType, newName: string) => void;
|
||||
}
|
||||
|
||||
export const FilePreview = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
title = "File Preview",
|
||||
files = [],
|
||||
initialIndexFile = -1,
|
||||
openedFileId,
|
||||
sidebarContent,
|
||||
headerRightContent,
|
||||
onChangeFile,
|
||||
handleDownloadFile,
|
||||
hideCloseButton,
|
||||
onFileRename,
|
||||
}: FilePreviewProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [currentIndex, setCurrentIndex] = useState(initialIndexFile);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
const [classNames, setClassNames] = useState<string[]>([]);
|
||||
const [pdfThumbnailSidebarOpen, setPdfThumbnailSidebarOpen] = useState(false);
|
||||
const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false);
|
||||
|
||||
const data: FilePreviewData[] = useMemo(() => {
|
||||
return files?.map((file) => ({
|
||||
...file,
|
||||
is_wopi_supported: file.is_wopi_supported ?? false,
|
||||
category: getMimeCategory(file.mimetype),
|
||||
}));
|
||||
}, [files]);
|
||||
|
||||
const currentFile: FilePreviewData | undefined =
|
||||
currentIndex > -1 ? data[currentIndex] : undefined;
|
||||
|
||||
const canGoNext = currentIndex < data.length - 1;
|
||||
const canGoPrevious = currentIndex > 0;
|
||||
|
||||
const goToNext = () => {
|
||||
if (canGoNext) setCurrentIndex(currentIndex + 1);
|
||||
};
|
||||
|
||||
const goToPrevious = () => {
|
||||
if (canGoPrevious) setCurrentIndex(currentIndex - 1);
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
handleDownloadFile?.(currentFile);
|
||||
};
|
||||
|
||||
const handlePrint = () => {
|
||||
if (!currentFile) return;
|
||||
window.open(currentFile.url_preview, "_blank");
|
||||
};
|
||||
|
||||
// Render the appropriate viewer based on file category
|
||||
const renderViewer = () => {
|
||||
if (!currentFile) {
|
||||
return <div>{t("file_preview.unsupported.title")}</div>;
|
||||
}
|
||||
|
||||
if (currentFile.isSuspicious) {
|
||||
return <SuspiciousPreview handleDownload={handleDownload} />;
|
||||
}
|
||||
if (currentFile.is_wopi_supported) {
|
||||
return <WopiEditor item={currentFile} onFileRename={onFileRename} />;
|
||||
}
|
||||
|
||||
switch (currentFile.category) {
|
||||
case MimeCategory.IMAGE:
|
||||
if (currentFile.mimetype.includes("heic")) {
|
||||
return (
|
||||
<NotSupportedPreview
|
||||
title={t("file_preview.unsupported.heic_title")}
|
||||
file={currentFile}
|
||||
onDownload={handleDownload}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ImageViewer
|
||||
src={currentFile.url_preview}
|
||||
alt={currentFile.title}
|
||||
className="file-preview__viewer"
|
||||
/>
|
||||
);
|
||||
case MimeCategory.VIDEO:
|
||||
return (
|
||||
<div className="video-preview-viewer-container">
|
||||
<div className="video-preview-viewer">
|
||||
<VideoPlayer
|
||||
src={currentFile.url_preview}
|
||||
className="file-preview__viewer"
|
||||
controls={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case MimeCategory.AUDIO:
|
||||
return (
|
||||
<div className="video-preview-viewer-container">
|
||||
<div className="video-preview-viewer">
|
||||
<AudioPlayer
|
||||
src={currentFile.url_preview}
|
||||
title={currentFile.title}
|
||||
className="file-preview__viewer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case MimeCategory.PDF:
|
||||
return (
|
||||
<PreviewPdf
|
||||
src={currentFile.url_preview}
|
||||
onThumbailSidebarOpen={(isOpen) => {
|
||||
setPdfThumbnailSidebarOpen(isOpen);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<NotSupportedPreview file={currentFile} onDownload={handleDownload} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Add a specific class name to the container when the PDF thumbnail sidebar is open.
|
||||
// So the navigation buttons can move in sync.
|
||||
useEffect(() => {
|
||||
const className = "file-preview__container--pdf-sidebar-open";
|
||||
const isPdf = currentFile?.category === MimeCategory.PDF;
|
||||
if (pdfThumbnailSidebarOpen && isPdf) {
|
||||
// The timeout is set so that the thumbnail sidebar and the button move in sync.
|
||||
setTimeout(() => {
|
||||
setClassNames((prev) => {
|
||||
if (prev.includes(className)) {
|
||||
return prev;
|
||||
}
|
||||
return [...prev, className];
|
||||
});
|
||||
}, OPEN_DELAY);
|
||||
} else {
|
||||
setClassNames((prev) => {
|
||||
return prev.filter((className) => className !== className);
|
||||
});
|
||||
}
|
||||
|
||||
// When switching from a PDF to a non-PDF file, we mark the thumbnail sidebar as closed.
|
||||
if (pdfThumbnailSidebarOpen && !isPdf) {
|
||||
setPdfThumbnailSidebarOpen(false);
|
||||
}
|
||||
}, [pdfThumbnailSidebarOpen, currentFile]);
|
||||
|
||||
useEffect(() => {
|
||||
if (openedFileId) {
|
||||
const index = data.findIndex((file) => file.id === openedFileId);
|
||||
const newIndex = index > -1 ? index : -1;
|
||||
setCurrentIndex(newIndex);
|
||||
} else {
|
||||
setCurrentIndex(-1);
|
||||
}
|
||||
}, [openedFileId]);
|
||||
|
||||
useEffect(() => {
|
||||
onChangeFile?.(currentFile);
|
||||
if (currentFile) {
|
||||
posthog.capture("file_preview_opened", {
|
||||
id: currentFile.id,
|
||||
size: currentFile.size,
|
||||
mimetype: currentFile.mimetype,
|
||||
});
|
||||
}
|
||||
}, [currentFile]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Prevent navigation if the target is a player timeline.
|
||||
const tag = (e.target as HTMLElement)?.tagName;
|
||||
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === "ArrowLeft") {
|
||||
e.preventDefault();
|
||||
goToPrevious();
|
||||
} else if (e.key === "ArrowRight") {
|
||||
e.preventDefault();
|
||||
goToNext();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [currentIndex, data.length, currentFile?.category]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !currentFile) return;
|
||||
const previousTitle = document.title;
|
||||
document.title = `${currentFile.title} - ${t("app_title")}`;
|
||||
return () => {
|
||||
document.title = previousTitle;
|
||||
};
|
||||
}, [isOpen, currentFile, t]);
|
||||
|
||||
if (!isOpen || !currentFile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={() => onClose?.()}
|
||||
size={ModalSize.FULL}
|
||||
hideCloseButton={true}
|
||||
>
|
||||
<div data-testid="file-preview">
|
||||
<div
|
||||
className={clsx(
|
||||
"file-preview__container",
|
||||
isSidebarOpen && "file-preview__container--sidebar-open",
|
||||
classNames,
|
||||
)}
|
||||
>
|
||||
<div className="file-preview__header">
|
||||
<div className="file-preview__header__content">
|
||||
<div className="file-preview__header__content__left">
|
||||
{!hideCloseButton && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
onClick={onClose}
|
||||
icon={<Icon name="close" />}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="file-preview__title">
|
||||
<FileIcon file={currentFile} type="mini" size="small" />
|
||||
<h1 className="file-preview__title">
|
||||
{removeFileExtension(currentFile?.title || title)}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-preview__header__content__right">
|
||||
{headerRightContent}
|
||||
{handleDownloadFile && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={handleDownload}
|
||||
icon={
|
||||
<Icon type={IconType.OUTLINED} name={"file_download"} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
icon={<Icon name={"info_outline"} />}
|
||||
/>
|
||||
{(currentFile?.category === MimeCategory.PDF ||
|
||||
currentFile?.category === MimeCategory.IMAGE) && (
|
||||
<DropdownMenu
|
||||
options={[
|
||||
...(handleDownloadFile
|
||||
? [
|
||||
{
|
||||
icon: (
|
||||
<Icon
|
||||
type={IconType.OUTLINED}
|
||||
name="file_download"
|
||||
/>
|
||||
),
|
||||
label: t("file_preview.actions.download"),
|
||||
value: "download",
|
||||
callback: handleDownload,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
icon: <Icon name="print" />,
|
||||
label: t("file_preview.actions.print"),
|
||||
value: "print",
|
||||
callback: handlePrint,
|
||||
},
|
||||
]}
|
||||
isOpen={isActionsMenuOpen}
|
||||
onOpenChange={setIsActionsMenuOpen}
|
||||
>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={() => setIsActionsMenuOpen(!isActionsMenuOpen)}
|
||||
icon={<Icon name="more_vert" />}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-preview__content">
|
||||
<div className="file-preview__main">
|
||||
{renderViewer()}
|
||||
<FilePreviewPreviousButton
|
||||
onClick={goToPrevious}
|
||||
disabled={!canGoPrevious}
|
||||
/>
|
||||
<FilePreviewNextButton onClick={goToNext} disabled={!canGoNext} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`file-preview-sidebar ${isSidebarOpen ? "open" : ""}`}
|
||||
>
|
||||
{sidebarContent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const FilePreviewNextButton = (props: Partial<ButtonProps>) => {
|
||||
return (
|
||||
<div className="file-preview__next-button">
|
||||
<Button
|
||||
{...props}
|
||||
icon={<Icon name="arrow_forward" />}
|
||||
color="brand"
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FilePreviewPreviousButton = (props: Partial<ButtonProps>) => {
|
||||
return (
|
||||
<div className="file-preview__previous-button">
|
||||
<Button
|
||||
{...props}
|
||||
icon={<Icon name="arrow_back" />}
|
||||
color="brand"
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ResetZoomIcon } from "@/features/ui/components/icon/ResetZoomIcon";
|
||||
import { ZoomMinusIcon } from "@/features/ui/components/icon/ZoomMinusIcon";
|
||||
import { ZoomPlusIcon } from "@/features/ui/components/icon/ZoomPlusIcon";
|
||||
import { Button } from "@gouvfr-lasuite/cunningham-react";
|
||||
|
||||
interface ZoomControlsProps {
|
||||
zoomOut: () => void;
|
||||
zoomIn: () => void;
|
||||
resetView: () => void;
|
||||
}
|
||||
|
||||
export const ZoomControls = ({
|
||||
zoomOut,
|
||||
zoomIn,
|
||||
resetView,
|
||||
}: ZoomControlsProps) => {
|
||||
return (
|
||||
<div className="file-preview__controls__zoom">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
color="neutral"
|
||||
size="small"
|
||||
onClick={zoomOut}
|
||||
icon={<ZoomMinusIcon />}
|
||||
/>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
color="neutral"
|
||||
size="small"
|
||||
onClick={resetView}
|
||||
icon={<ResetZoomIcon />}
|
||||
/>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
color="neutral"
|
||||
size="small"
|
||||
onClick={zoomIn}
|
||||
icon={<ZoomPlusIcon />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Item, ItemType } from "@/features/drivers/types";
|
||||
import { FilePreview, FilePreviewType } from "../files-preview/FilesPreview";
|
||||
import { FilePreview, FilePreviewType } from "../FilesPreview";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo } from "react";
|
||||
import { itemToPreviewFile } from "@/features/explorer/utils/utils";
|
||||
|
||||
@@ -1,302 +0,0 @@
|
||||
import {
|
||||
getMimeCategory,
|
||||
MimeCategory,
|
||||
removeFileExtension,
|
||||
} from "@/features/explorer/utils/mimeTypes";
|
||||
import { Icon, IconType } from "@gouvfr-lasuite/ui-kit";
|
||||
import { Button, Modal, ModalSize } from "@gouvfr-lasuite/cunningham-react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { ImageViewer } from "../image-viewer/ImageViewer";
|
||||
import { VideoPlayer } from "../video-player/VideoPlayer";
|
||||
import { AudioPlayer } from "../audio-player/AudioPlayer";
|
||||
|
||||
import { NotSupportedPreview } from "../not-supported/NotSupportedPreview";
|
||||
import { FileIcon } from "@/features/explorer/components/icons/ItemIcon";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SuspiciousPreview } from "../suspicious/SuspiciousPreview";
|
||||
import { WopiEditor } from "../wopi/WopiEditor";
|
||||
import posthog from "posthog-js";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const PreviewPdf = dynamic<{ src: string }>(
|
||||
() =>
|
||||
import("../pdf-preview/PreviewPdf")
|
||||
.then((mod) => mod.PreviewPdf)
|
||||
.catch(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { OutdatedBrowserPreview } = require("../pdf-preview/OutdatedBrowserPreview");
|
||||
return OutdatedBrowserPreview;
|
||||
}),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
export type FilePreviewType = {
|
||||
id: string;
|
||||
size: number;
|
||||
title: string;
|
||||
mimetype: string;
|
||||
is_wopi_supported?: boolean;
|
||||
url_preview: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type FilePreviewData = FilePreviewType & {
|
||||
category: MimeCategory;
|
||||
isSuspicious?: boolean;
|
||||
};
|
||||
|
||||
interface FilePreviewProps {
|
||||
isOpen: boolean;
|
||||
onClose?: () => void;
|
||||
title?: string;
|
||||
files?: FilePreviewType[];
|
||||
initialIndexFile?: number;
|
||||
openedFileId?: string;
|
||||
headerRightContent?: React.ReactNode;
|
||||
sidebarContent?: React.ReactNode;
|
||||
onChangeFile?: (file?: FilePreviewType) => void;
|
||||
handleDownloadFile?: (file?: FilePreviewType) => void;
|
||||
hideCloseButton?: boolean;
|
||||
hideNav?: boolean;
|
||||
onFileRename?: (file: FilePreviewType, newName: string) => void;
|
||||
}
|
||||
|
||||
export const FilePreview = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
title = "File Preview",
|
||||
files = [],
|
||||
initialIndexFile = -1,
|
||||
openedFileId,
|
||||
sidebarContent,
|
||||
headerRightContent,
|
||||
onChangeFile,
|
||||
handleDownloadFile,
|
||||
hideCloseButton,
|
||||
hideNav,
|
||||
onFileRename,
|
||||
}: FilePreviewProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [currentIndex, setCurrentIndex] = useState(initialIndexFile);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
const data: FilePreviewData[] = useMemo(() => {
|
||||
return files?.map((file) => ({
|
||||
...file,
|
||||
is_wopi_supported: file.is_wopi_supported ?? false,
|
||||
category: getMimeCategory(file.mimetype),
|
||||
}));
|
||||
}, [files]);
|
||||
|
||||
const currentFile: FilePreviewData | undefined =
|
||||
currentIndex > -1 ? data[currentIndex] : undefined;
|
||||
|
||||
const handleDownload = async () => {
|
||||
handleDownloadFile?.(currentFile);
|
||||
};
|
||||
|
||||
// Render the appropriate viewer based on file category
|
||||
const renderViewer = () => {
|
||||
if (!currentFile) {
|
||||
return <div>{t("file_preview.unsupported.title")}</div>;
|
||||
}
|
||||
|
||||
if (currentFile.isSuspicious) {
|
||||
return <SuspiciousPreview handleDownload={handleDownload} />;
|
||||
}
|
||||
if (currentFile.is_wopi_supported) {
|
||||
return <WopiEditor item={currentFile} onFileRename={onFileRename} />;
|
||||
}
|
||||
|
||||
switch (currentFile.category) {
|
||||
case MimeCategory.IMAGE:
|
||||
if (currentFile.mimetype.includes("heic")) {
|
||||
return (
|
||||
<NotSupportedPreview
|
||||
title={t("file_preview.unsupported.heic_title")}
|
||||
file={currentFile}
|
||||
onDownload={handleDownload}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ImageViewer
|
||||
src={currentFile.url_preview}
|
||||
alt={currentFile.title}
|
||||
className="file-preview-viewer"
|
||||
/>
|
||||
);
|
||||
case MimeCategory.VIDEO:
|
||||
return (
|
||||
<div className="video-preview-viewer-container">
|
||||
<div className="video-preview-viewer">
|
||||
<VideoPlayer
|
||||
src={currentFile.url_preview}
|
||||
className="file-preview-viewer"
|
||||
controls={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case MimeCategory.AUDIO:
|
||||
return (
|
||||
<div className="video-preview-viewer-container">
|
||||
<div className="video-preview-viewer">
|
||||
<AudioPlayer
|
||||
src={currentFile.url_preview}
|
||||
title={currentFile.title}
|
||||
className="file-preview-viewer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case MimeCategory.PDF:
|
||||
return <PreviewPdf src={currentFile.url_preview} />;
|
||||
|
||||
default:
|
||||
return (
|
||||
<NotSupportedPreview file={currentFile} onDownload={handleDownload} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (openedFileId) {
|
||||
const index = data.findIndex((file) => file.id === openedFileId);
|
||||
const newIndex = index > -1 ? index : -1;
|
||||
setCurrentIndex(newIndex);
|
||||
} else {
|
||||
setCurrentIndex(-1);
|
||||
}
|
||||
}, [openedFileId]);
|
||||
|
||||
useEffect(() => {
|
||||
onChangeFile?.(currentFile);
|
||||
if (currentFile) {
|
||||
posthog.capture("file_preview_opened", {
|
||||
id: currentFile.id,
|
||||
size: currentFile.size,
|
||||
mimetype: currentFile.mimetype,
|
||||
});
|
||||
}
|
||||
}, [currentFile]);
|
||||
|
||||
if (!isOpen || !currentFile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={() => onClose?.()}
|
||||
size={ModalSize.FULL}
|
||||
hideCloseButton={true}
|
||||
>
|
||||
<div data-testid="file-preview">
|
||||
<div
|
||||
className={`file-preview-container ${
|
||||
isSidebarOpen ? "sidebar-open" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="file-preview-header">
|
||||
<div className="file-preview-header__content">
|
||||
<div className="file-preview-header__content-left">
|
||||
{!hideCloseButton && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
onClick={onClose}
|
||||
icon={<Icon name="close" />}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="file-preview-title">
|
||||
<FileIcon file={currentFile} type="mini" size="small" />
|
||||
<h1 className="file-preview-title">
|
||||
{removeFileExtension(currentFile?.title || title)}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-preview-header__content-center">
|
||||
{!hideNav && (
|
||||
<FilePreviewNav
|
||||
currentIndex={currentIndex}
|
||||
totalFiles={data.length}
|
||||
onPrevious={() => setCurrentIndex(currentIndex - 1)}
|
||||
onNext={() => setCurrentIndex(currentIndex + 1)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="file-preview-header__content-right">
|
||||
{headerRightContent}
|
||||
{handleDownloadFile && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={handleDownload}
|
||||
icon={
|
||||
<Icon type={IconType.OUTLINED} name={"file_download"} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
icon={<Icon name={"info_outline"} />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-preview-content">
|
||||
<div className="file-preview-main">{renderViewer()}</div>
|
||||
|
||||
<div
|
||||
className={`file-preview-sidebar ${isSidebarOpen ? "open" : ""}`}
|
||||
>
|
||||
{sidebarContent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
interface FilePreviewNavProps {
|
||||
currentIndex: number;
|
||||
totalFiles: number;
|
||||
onPrevious: () => void;
|
||||
onNext: () => void;
|
||||
}
|
||||
|
||||
export const FilePreviewNav: React.FC<FilePreviewNavProps> = ({
|
||||
currentIndex,
|
||||
totalFiles,
|
||||
onPrevious,
|
||||
onNext,
|
||||
}) => {
|
||||
if (totalFiles === 1) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="file-preview-nav" data-testid="file-preview-nav">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={onPrevious}
|
||||
disabled={currentIndex === 0}
|
||||
icon={<Icon name="arrow_back" />}
|
||||
/>
|
||||
<span className="file-preview-nav__count">
|
||||
{currentIndex + 1} / {totalFiles}
|
||||
</span>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={onNext}
|
||||
disabled={currentIndex === totalFiles - 1}
|
||||
icon={<Icon name="arrow_forward" />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,12 +1,12 @@
|
||||
@use "./audio-player/audio-player.scss";
|
||||
@use "./files-preview/files-preview.scss";
|
||||
@use "./image-viewer/ImageViewer.scss";
|
||||
@use "./video-player/VideoPlayer.scss";
|
||||
@use "./FilesPreview.scss";
|
||||
@use "./components/duration-bar/DurationBar.scss";
|
||||
@use "./components/volume-bar/VolumeBar.scss";
|
||||
@use "./components/controls/PreviewControls.scss";
|
||||
@use "./not-supported/NotSupportedPreview.scss";
|
||||
@use "./suspicious/SuspiciousPreview.scss";
|
||||
@use "./wopi/WopiEditor.scss";
|
||||
@use "./error/ErrorPreview.scss";
|
||||
@use "./pdf-preview/PreviewPdf.scss";
|
||||
@use "./viewers/not-supported/NotSupportedPreview.scss";
|
||||
@use "./viewers/suspicious/SuspiciousPreview.scss";
|
||||
@use "./viewers/wopi/WopiEditor.scss";
|
||||
@use "./viewers/error/ErrorPreview.scss";
|
||||
@use "./viewers/audio-player/AudioPlayer.scss";
|
||||
@use "./viewers/image-viewer/ImageViewer.scss";
|
||||
@use "./viewers/video-player/VideoPlayer.scss";
|
||||
@use "./viewers/pdf-preview/PdfPreview.scss";
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import React, { useRef, useState, useEffect, useCallback } from "react";
|
||||
import { clsx } from "clsx";
|
||||
import { ProgressBar } from "../components/duration-bar/DurationBar";
|
||||
import { PreviewControls } from "../components/controls/PreviewControls";
|
||||
import { ProgressBar } from "../../components/duration-bar/DurationBar";
|
||||
import { PlayerPreviewControls } from "../../components/controls/PreviewControls";
|
||||
|
||||
interface AudioPlayerProps {
|
||||
src: string;
|
||||
@@ -168,7 +168,7 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
|
||||
handleSeek={handleSeek}
|
||||
/>
|
||||
|
||||
<PreviewControls
|
||||
<PlayerPreviewControls
|
||||
togglePlay={togglePlayPause}
|
||||
isPlaying={isPlaying}
|
||||
rewind10Seconds={handleRewind10Seconds}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FilePreviewType } from "../files-preview/FilesPreview";
|
||||
|
||||
import { Button } from "@gouvfr-lasuite/cunningham-react";
|
||||
import { Icon, IconType } from "@gouvfr-lasuite/ui-kit";
|
||||
import { FileIcon } from "@/features/explorer/components/icons/ItemIcon";
|
||||
import { downloadFile } from "@/features/items/utils";
|
||||
import { useCallback } from "react";
|
||||
import { FilePreviewType } from "../../FilesPreview";
|
||||
|
||||
interface ErrorPreviewProps {
|
||||
file: FilePreviewType;
|
||||
@@ -6,14 +6,6 @@
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&__controls {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&__container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
@@ -1,10 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@gouvfr-lasuite/cunningham-react";
|
||||
import React, { useState, useRef, useCallback, useEffect } from "react";
|
||||
import { ResetZoomIcon } from "../../components/icon/ResetZoomIcon";
|
||||
import { ZoomMinusIcon } from "../../components/icon/ZoomMinusIcon";
|
||||
import { ZoomPlusIcon } from "../../components/icon/ZoomPlusIcon";
|
||||
import { ZoomControls } from "../../components/ZoomControls/ZoomControls";
|
||||
|
||||
interface ImageViewerProps {
|
||||
src: string;
|
||||
@@ -405,41 +400,9 @@ export const ImageViewer: React.FC<ImageViewerProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="image-viewer__controls">
|
||||
<ZoomControl
|
||||
zoomOut={zoomOut}
|
||||
zoomIn={zoomIn}
|
||||
zoom={zoom}
|
||||
resetView={resetView}
|
||||
/>
|
||||
<div className="file-preview__controls">
|
||||
<ZoomControls zoomOut={zoomOut} zoomIn={zoomIn} resetView={resetView} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ZoomControlProps {
|
||||
zoomOut: () => void;
|
||||
zoomIn: () => void;
|
||||
zoom: number;
|
||||
resetView: () => void;
|
||||
}
|
||||
|
||||
export const ZoomControl = ({
|
||||
zoomOut,
|
||||
zoomIn,
|
||||
resetView,
|
||||
}: ZoomControlProps) => {
|
||||
return (
|
||||
<div className="zoom-control">
|
||||
<Button variant="tertiary" color="neutral" onClick={zoomOut}>
|
||||
<ZoomMinusIcon />
|
||||
</Button>
|
||||
<Button variant="tertiary" color="neutral" onClick={resetView}>
|
||||
<ResetZoomIcon />
|
||||
</Button>
|
||||
<Button variant="tertiary" color="neutral" onClick={zoomIn}>
|
||||
<ZoomPlusIcon />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FilePreviewType } from "../files-preview/FilesPreview";
|
||||
import { FilePreviewType } from "../../FilesPreview";
|
||||
|
||||
import { Button } from "@gouvfr-lasuite/cunningham-react";
|
||||
import { Icon, IconType } from "@gouvfr-lasuite/ui-kit";
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Button, Input } from "@gouvfr-lasuite/cunningham-react";
|
||||
import { LeftSidebarIcon } from "../../components/icon/LeftSidebarIcon";
|
||||
import { ZoomOut } from "../../components/icon/ZoomOut";
|
||||
import { ZoomIn } from "../../components/icon/ZoomIn";
|
||||
import { ZoomReset } from "../../components/icon/ZoomReset";
|
||||
import { LeftSidebarIcon } from "../../../components/icon/LeftSidebarIcon";
|
||||
import { ZoomControls } from "../../components/ZoomControls/ZoomControls";
|
||||
|
||||
interface PdfControlsProps {
|
||||
numPages: number;
|
||||
@@ -29,7 +27,7 @@ export function PdfControls({
|
||||
onZoomOut,
|
||||
}: PdfControlsProps) {
|
||||
return (
|
||||
<div className="pdf-preview__controls">
|
||||
<div className="file-preview__controls">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
color="neutral"
|
||||
@@ -38,7 +36,7 @@ export function PdfControls({
|
||||
size="small"
|
||||
icon={<LeftSidebarIcon />}
|
||||
/>
|
||||
<div className="controls-vertical-separator" />
|
||||
<div className="file-preview__controls__separator" />
|
||||
<div className="pdf-preview__page-nav">
|
||||
<div className="pdf-preview__page-indicator">
|
||||
<Input
|
||||
@@ -53,30 +51,12 @@ export function PdfControls({
|
||||
<span className="pdf-preview__page-total">/ {numPages}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="controls-vertical-separator" />
|
||||
<div className="pdf-preview__zoom-controls">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
color="neutral"
|
||||
onClick={onZoomOut}
|
||||
icon={<ZoomOut />}
|
||||
size="small"
|
||||
/>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
color="neutral"
|
||||
onClick={onZoomReset}
|
||||
icon={<ZoomReset />}
|
||||
size="small"
|
||||
/>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
color="neutral"
|
||||
onClick={onZoomIn}
|
||||
icon={<ZoomIn />}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<div className="file-preview__controls__separator" />
|
||||
<ZoomControls
|
||||
zoomOut={onZoomOut}
|
||||
zoomIn={onZoomIn}
|
||||
resetView={onZoomReset}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -158,23 +158,6 @@
|
||||
box-shadow: 0 3.649px 14.597px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&__controls {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--c--globals--spacings--xs);
|
||||
border-radius: var(--c--globals--spacings--3xs);
|
||||
padding: var(--c--globals--spacings--3xs);
|
||||
border-radius: var(--xs, 8px);
|
||||
border: 1px solid var(--c--contextuals--border--surface--primary);
|
||||
background: var(--c--contextuals--background--surface--primary);
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&__zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -185,6 +168,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--c--globals--spacings--3xs);
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
&__page-indicator {
|
||||
@@ -194,7 +178,6 @@
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--c--contextuals--content--semantic--neutral--secondary);
|
||||
padding-right: 4px;
|
||||
|
||||
.c__field {
|
||||
width: auto;
|
||||
@@ -230,31 +213,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.controls-vertical-separator {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
border-radius: 1px;
|
||||
overflow: hidden;
|
||||
background: var(--c--contextuals--border--surface--primary);
|
||||
}
|
||||
|
||||
.zoom-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--c--globals--spacings--3xs);
|
||||
|
||||
&__value {
|
||||
text-align: center;
|
||||
min-width: 35px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--c--contextuals--content--semantic--neutral--secondary);
|
||||
cursor: pointer;
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pdf-skeleton-pulse {
|
||||
0%,
|
||||
100% {
|
||||
@@ -20,7 +20,13 @@ import { pdfOptions } from "./pdfOptions";
|
||||
// Configure PDF.js worker source for PDF loading
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = "/pdf.worker.mjs";
|
||||
|
||||
export function PreviewPdf({ src }: { src: string }) {
|
||||
export function PdfPreview({
|
||||
src,
|
||||
onThumbailSidebarOpen,
|
||||
}: {
|
||||
src: string;
|
||||
onThumbailSidebarOpen?: (isOpen: boolean) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [numPages, setNumPages] = useState<number>(1);
|
||||
const [documentError, setDocumentError] = useState<
|
||||
@@ -43,7 +49,10 @@ export function PreviewPdf({ src }: { src: string }) {
|
||||
setZoom(1);
|
||||
}, []);
|
||||
const toggleSidebar = useCallback(() => {
|
||||
setIsSidebarOpen((prev) => !prev);
|
||||
setIsSidebarOpen((prev) => {
|
||||
onThumbailSidebarOpen?.(!prev);
|
||||
return !prev;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const scrollToPage = useCallback((page: number) => {
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { Thumbnail } from "react-pdf";
|
||||
import { AutoSizer, List } from "react-virtualized";
|
||||
import type { ListRowRenderer } from "react-virtualized";
|
||||
import { OPEN_DELAY, ROW_HEIGHT, THUMBNAIL_GAP, TRANSITION_DELAY } from "./pdfConsts";
|
||||
|
||||
interface PdfThumbnailSidebarProps {
|
||||
numPages: number;
|
||||
@@ -10,11 +11,6 @@ interface PdfThumbnailSidebarProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
const TRANSITION_DELAY = 300;
|
||||
const THUMBNAIL_HEIGHT = 178;
|
||||
const THUMBNAIL_GAP = 12;
|
||||
const ROW_HEIGHT = THUMBNAIL_HEIGHT + THUMBNAIL_GAP;
|
||||
|
||||
// Two-phase mount/unmount to allow CSS transitions to play out:
|
||||
// Opening: mount immediately (unmount=false), then defer isOpenProxy=true
|
||||
// so the DOM is present before the "open" class triggers the transition.
|
||||
@@ -30,7 +26,7 @@ export function PdfThumbnailSidebar(props: PdfThumbnailSidebarProps) {
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
if (props.isOpen) {
|
||||
setUnmount(false);
|
||||
timer = setTimeout(() => setIsOpenProxy(true), 100);
|
||||
timer = setTimeout(() => setIsOpenProxy(true), OPEN_DELAY);
|
||||
} else {
|
||||
setIsOpenProxy(false);
|
||||
// The 1.1 is to allow for the transition to finish.
|
||||
@@ -0,0 +1,5 @@
|
||||
export const TRANSITION_DELAY = 300;
|
||||
export const THUMBNAIL_HEIGHT = 178;
|
||||
export const THUMBNAIL_GAP = 12;
|
||||
export const ROW_HEIGHT = THUMBNAIL_HEIGHT + THUMBNAIL_GAP;
|
||||
export const OPEN_DELAY = 100;
|
||||
@@ -3,8 +3,8 @@
|
||||
import React, { useRef, useState, useEffect, useCallback } from "react";
|
||||
import clsx from "clsx";
|
||||
import { Icon } from "@gouvfr-lasuite/ui-kit";
|
||||
import { ProgressBar } from "../components/duration-bar/DurationBar";
|
||||
import { PreviewControls } from "../components/controls/PreviewControls";
|
||||
import { ProgressBar } from "../../components/duration-bar/DurationBar";
|
||||
import { PlayerPreviewControls } from "../../components/controls/PreviewControls";
|
||||
|
||||
interface VideoPlayerProps {
|
||||
src: string;
|
||||
@@ -269,16 +269,12 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
|
||||
{controls && !isFullscreen && (
|
||||
<div className="video-player__controls">
|
||||
{/* Progress bar */}
|
||||
|
||||
<ProgressBar
|
||||
duration={duration}
|
||||
currentTime={currentTime}
|
||||
handleSeek={handleSeek}
|
||||
/>
|
||||
|
||||
{/* Control buttons */}
|
||||
<PreviewControls
|
||||
<PlayerPreviewControls
|
||||
togglePlay={togglePlay}
|
||||
isPlaying={isPlaying}
|
||||
rewind10Seconds={rewind10Seconds}
|
||||
@@ -2,8 +2,8 @@ import { useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { getDriver } from "@/features/config/Config";
|
||||
import { FilePreviewType } from "../files-preview/FilesPreview";
|
||||
import { ErrorPreview } from "../error/ErrorPreview";
|
||||
import { FilePreviewType } from "../../FilesPreview";
|
||||
|
||||
interface WopiEditorProps {
|
||||
item: FilePreviewType;
|
||||
Reference in New Issue
Block a user