DocSpace-client/packages/common/components/MediaViewer/index.tsx

460 lines
11 KiB
TypeScript

import { isMobile } from "react-device-detect";
import React, {
useState,
useCallback,
useMemo,
useEffect,
useRef,
} from "react";
import ViewerWrapper from "./sub-components/ViewerWrapper";
import { MediaViewerProps } from "./MediaViewer.props";
import { FileStatus } from "@docspace/common/constants";
import {
isNullOrUndefined,
KeyboardEventKeys,
mapSupplied,
mediaTypes,
} from "./helpers";
import { getFileExtension } from "@docspace/common/utils";
import {
getDesktopMediaContextModel,
getMobileMediaContextModel,
getPDFContextModel,
} from "./helpers/contextModel";
import { checkDialogsOpen } from "../../utils/checkDialogsOpen";
function MediaViewer({
playlistPos,
nextMedia,
prevMedia,
pluginContextMenuItems,
setActiveFiles,
...props
}: MediaViewerProps): JSX.Element {
const TiffXMLHttpRequestRef = useRef<XMLHttpRequest>();
const [title, setTitle] = useState<string>("");
const [fileUrl, setFileUrl] = useState<string | undefined>(() => {
const { playlist, currentFileId } = props;
const item = playlist.find(
(file) => file.fileId.toString() === currentFileId.toString()
);
return item?.src;
});
const [targetFile, setTargetFile] = useState(() => {
const { files, currentFileId } = props;
return files.find((item) => item.id === currentFileId);
});
const [isFavorite, setIsFavorite] = useState<boolean>(() => {
const { playlist } = props;
return (
(playlist[playlistPos].fileStatus & FileStatus.IsFavorite) ===
FileStatus.IsFavorite
);
});
useEffect(() => {
const fileId = props.playlist[playlistPos]?.fileId;
if (!isNullOrUndefined(fileId) && props.currentFileId !== fileId) {
props.onChangeUrl(fileId);
}
}, [props.playlist.length]);
useEffect(() => {
return () => {
props.onClose();
};
}, []);
useEffect(() => {
const { playlist, files, setBufferSelection } = props;
const currentFile = playlist[playlistPos];
const currentFileId =
playlist.length > 0
? playlist.find((file) => file.id === playlistPos)?.fileId
: 0;
const targetFile = files.find((item) => item.id === currentFileId);
if (targetFile) setBufferSelection(targetFile);
const { src, title } = currentFile;
const ext = getFileExtension(title);
if (ext === ".tiff" || ext === ".tif") {
fetchAndSetTiffDataURL(src);
}
}, []);
useEffect(() => {
const { playlist, onEmptyPlaylistError, files, setBufferSelection } = props;
const { src, title, fileId } = playlist[playlistPos];
const ext = getFileExtension(title);
if (!src) return onEmptyPlaylistError();
if (ext !== ".tif" && ext !== ".tiff" && src !== fileUrl) {
TiffXMLHttpRequestRef.current?.abort();
setFileUrl(src);
}
if (ext === ".tiff" || ext === ".tif") {
setFileUrl(undefined);
fetchAndSetTiffDataURL(src);
}
const foundFile = files.find((file) => file.id === fileId);
if (!isNullOrUndefined(foundFile)) {
setTargetFile(foundFile);
setBufferSelection(foundFile);
}
setTitle(title);
setIsFavorite(
(playlist[playlistPos].fileStatus & FileStatus.IsFavorite) ===
FileStatus.IsFavorite
);
}, [props.playlist, props.files.length, props.currentFileId, playlistPos]);
useEffect(() => {
document.addEventListener("keydown", onKeydown);
return () => {
document.removeEventListener("keydown", onKeydown);
TiffXMLHttpRequestRef.current?.abort();
};
}, [
props.playlist.length,
props.files.length,
playlistPos,
props.deleteDialogVisible,
]);
const getContextModel = () => {
const {
t,
onClickDownloadAs,
onClickLinkEdit,
onClickDownload,
onPreviewClick,
onClickRename,
onClickDelete,
onShowInfoPanel,
onMoveAction,
onCopyAction,
onDuplicate,
onCopyLink,
} = props;
if (!targetFile) return [];
const desktopModel = getDesktopMediaContextModel(
t,
targetFile,
archiveRoom,
{
onClickDownload,
onClickRename,
onClickDelete,
}
);
const model = getMobileMediaContextModel(t, targetFile, {
onShowInfoPanel,
onClickDownload,
onMoveAction,
onCopyAction,
onDuplicate,
onClickRename,
onClickDelete,
});
if (isPdf)
return getPDFContextModel(t, targetFile, {
onClickDownloadAs,
onMoveAction,
onCopyAction,
onClickRename,
onDuplicate,
onClickDelete,
onClickDownload,
onClickLinkEdit,
onPreviewClick,
onCopyLink,
});
if (pluginContextMenuItems && pluginContextMenuItems.length > 0) {
model.unshift({
key: "separator-plugin",
isSeparator: true,
disabled: false,
});
pluginContextMenuItems.forEach((item) => {
const onClick = async (): Promise<void> => {
props.onClose();
if (item.value.withActiveItem) setActiveFiles([targetFile.id]);
await item.value.onClick(targetFile.id);
if (item.value.withActiveItem) setActiveFiles([]);
};
if (
item.value.fileType &&
item.value.fileType.includes("image") &&
!targetFile.viewAccessability.ImageView
)
return;
if (
item.value.fileType &&
item.value.fileType.includes("video") &&
!targetFile.viewAccessability.MediaView
)
return;
model.unshift({
id: item.key,
key: item.key,
disabled: false,
...item.value,
onClick,
});
desktopModel.unshift({
key: item.key,
disabled: false,
...item.value,
onClick,
});
});
}
return isMobile
? model
: isImage && !isMobile
? desktopModel.filter((el) => el.key !== "download")
: desktopModel;
};
const canImageView = useCallback(
(ext: string) => {
const { extsImagePreviewed } = props;
return extsImagePreviewed.indexOf(ext) != -1;
},
[props.extsImagePreviewed]
);
const canPlay = useCallback(
(fileTitle: string) => {
const { extsMediaPreviewed } = props;
const ext =
fileTitle[0] === "." ? fileTitle : getFileExtension(fileTitle);
const supply = mapSupplied[ext];
return !!supply && extsMediaPreviewed.indexOf(ext) != -1;
},
[props.extsMediaPreviewed]
);
let lastRemovedFileId: null | number = null;
const onDelete = () => {
const { playlist, onDelete } = props;
let currentFileId = playlist.find(
(file) => file.id === playlistPos
)?.fileId;
if (currentFileId === lastRemovedFileId) return;
const canDelete = targetFile?.security?.Delete;
if (!canDelete) return;
if (!isNullOrUndefined(currentFileId)) {
onDelete(currentFileId);
lastRemovedFileId = currentFileId;
}
};
const onDownload = () => {
const { playlist, onDownload } = props;
if (!targetFile?.security.Download) return;
let currentFileId = playlist.find(
(file) => file.id === playlistPos
)?.fileId;
if (!isNullOrUndefined(currentFileId)) onDownload(currentFileId);
};
const onKeydown = (event: KeyboardEvent) => {
const { code, ctrlKey } = event;
const someDialogIsOpen = checkDialogsOpen();
if (props.deleteDialogVisible || someDialogIsOpen) return;
if (code in KeyboardEventKeys) {
const includesKeyboardCode = [
KeyboardEventKeys.KeyS,
KeyboardEventKeys.Numpad1,
KeyboardEventKeys.Digit1,
KeyboardEventKeys.Space,
].includes(code as KeyboardEventKeys);
if (!includesKeyboardCode || ctrlKey) event.preventDefault();
}
switch (code) {
case KeyboardEventKeys.ArrowLeft:
if (document.fullscreenElement) return;
if (!ctrlKey) prevMedia();
break;
case KeyboardEventKeys.ArrowRight:
if (document.fullscreenElement) return;
if (!ctrlKey) nextMedia();
break;
case KeyboardEventKeys.Escape:
if (!props.deleteDialogVisible) props.onClose();
break;
case KeyboardEventKeys.KeyS:
if (ctrlKey) onDownload();
break;
case KeyboardEventKeys.Delete:
onDelete();
break;
default:
break;
}
};
const onClose = useCallback(() => {
props.onClose();
}, [props.onClose]);
const fetchAndSetTiffDataURL = useCallback((src: string) => {
if (!window.Tiff) return;
TiffXMLHttpRequestRef.current?.abort();
const xhr = new XMLHttpRequest();
TiffXMLHttpRequestRef.current = xhr;
xhr.responseType = "arraybuffer";
xhr.open("GET", src);
xhr.onload = function () {
try {
const tiff = new window.Tiff({ buffer: xhr.response });
const dataUrl = tiff.toDataURL();
setFileUrl(dataUrl);
} catch (e) {
console.log(e);
}
};
xhr.send();
}, []);
const onSetSelectionFile = useCallback(() => {
props.setBufferSelection(targetFile);
}, [targetFile]);
const ext = getFileExtension(title);
const audioIcon = useMemo(() => props.getIcon(96, ext), [ext]);
const headerIcon = useMemo(() => props.getIcon(24, ext), [ext]);
let isVideo = false;
let isAudio = false;
let canOpen = true;
let isImage = false;
let isPdf = false;
const archiveRoom =
props.archiveRoomsId === targetFile?.rootFolderId ||
(!targetFile?.security?.Rename && !targetFile?.security?.Delete);
if (canPlay(ext) && canImageView(ext)) {
canOpen = false;
props.onError?.();
}
if (canImageView(ext)) {
isImage = true;
} else {
isImage = false;
isVideo = mapSupplied[ext]
? mapSupplied[ext]?.type == mediaTypes.video
: false;
isAudio = mapSupplied[ext]
? mapSupplied[ext]?.type == mediaTypes.audio
: false;
isPdf = mapSupplied[ext] ? mapSupplied[ext]?.type == mediaTypes.pdf : false;
}
return (
<>
{canOpen && (
<ViewerWrapper
targetFile={targetFile}
userAccess={props.userAccess}
visible={props.visible}
title={title}
onClose={onClose}
fileUrl={fileUrl}
inactive={props.playlist.length <= 1}
playlist={props.playlist}
playlistPos={playlistPos}
onNextClick={nextMedia}
onSetSelectionFile={onSetSelectionFile}
contextModel={getContextModel}
onPrevClick={prevMedia}
onDeleteClick={onDelete}
isFavorite={isFavorite}
isImage={isImage}
isAudio={isAudio}
isVideo={isVideo}
isPdf={isPdf}
isPreviewFile={props.isPreviewFile}
onDownloadClick={onDownload}
archiveRoom={archiveRoom}
errorTitle={props.t("Common:MediaError")}
headerIcon={headerIcon}
audioIcon={audioIcon}
/>
)}
</>
);
}
export default MediaViewer;