Merge pull request #1195 from ONLYOFFICE/feature/refactoring-mediaviewer

Feature/refactoring mediaviewer
This commit is contained in:
Alexey Safronov 2023-02-10 15:42:18 +03:00 committed by GitHub
commit 621efcf870
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 744 additions and 750 deletions

View File

@ -11,6 +11,7 @@ const FilesMediaViewer = (props) => {
t,
files,
playlist,
currentPostionIndex,
visible,
currentMediaFileId,
deleteItemAction,
@ -43,10 +44,19 @@ const FilesMediaViewer = (props) => {
extsMediaPreviewed,
setIsPreview,
isPreview,
nextMedia,
prevMedia,
resetUrl,
firstLoad,
setSelection,
} = props;
useEffect(() => {
if (visible) {
resetSelection();
}
}, [visible]);
useEffect(() => {
const previewId = queryString.parse(location.search).preview;
@ -88,6 +98,10 @@ const FilesMediaViewer = (props) => {
window.history.pushState(null, null, url);
};
const resetSelection = () => {
setSelection([]);
};
const removeQuery = (queryName) => {
const queryParams = new URLSearchParams(location.search);
@ -108,9 +122,6 @@ const FilesMediaViewer = (props) => {
}
};
const canDelete = (fileId) => true; //TODO:
const canDownload = (fileId) => true; //TODO:
const onDeleteMediaFile = (id) => {
const translations = {
deleteOperation: t("Translations:DeleteOperation"),
@ -138,22 +149,12 @@ const FilesMediaViewer = (props) => {
if (isPreview) {
setIsPreview(false);
resetUrl();
setScrollToItem({ id: previewFile.id, type: "file" });
setBufferSelection(previewFile);
if (previewFile) {
setScrollToItem({ id: previewFile.id, type: "file" });
setBufferSelection(previewFile);
}
setToPreviewFile(null);
}
// if (previewFile) {
// setIsLoading(true);
// setFirstLoad(true);
// fetchFiles(previewFile.folderId).finally(() => {
// setIsLoading(false);
// setFirstLoad(false);
// setScrollToItem({ id: previewFile.id, type: "file" });
// setBufferSelection(previewFile);
// setToPreviewFile(null);
// });
// }
setMediaViewerData({ visible: false, id: null });
@ -176,20 +177,16 @@ const FilesMediaViewer = (props) => {
t={t}
userAccess={userAccess}
currentFileId={currentMediaFileId}
allowConvert={true} //TODO:
canDelete={canDelete} //TODO:
canDownload={canDownload} //TODO:
visible={visible}
playlist={playlist}
playlistPos={currentPostionIndex}
onDelete={onDeleteMediaFile}
onDownload={onDownloadMediaFile}
onClickFavorite={onClickFavorite}
setBufferSelection={setBufferSelection}
archiveRoomsId={archiveRoomsId}
files={files}
onClickDownload={onClickDownload}
onShowInfoPanel={onShowInfoPanel}
onClickDownloadAs={onClickDownloadAs}
onClickDelete={onClickDelete}
onClickRename={onClickRename}
onMoveAction={onMoveAction}
@ -201,11 +198,10 @@ const FilesMediaViewer = (props) => {
deleteDialogVisible={deleteDialogVisible}
extsMediaPreviewed={extsMediaPreviewed}
extsImagePreviewed={extsImagePreviewed}
errorLabel={t("Translations:MediaLoadError")}
isPreviewFile={firstLoad}
previewFile={previewFile}
onChangeUrl={onChangeUrl}
isFavoritesFolder={isFavoritesFolder}
nextMedia={nextMedia}
prevMedia={prevMedia}
/>
)
);
@ -233,15 +229,19 @@ export default inject(
setIsPreview,
isPreview,
resetUrl,
setSelection,
} = filesStore;
const {
visible,
id: currentMediaFileId,
currentPostionIndex,
setMediaViewerData,
playlist,
previewFile,
setToPreviewFile,
setCurrentId,
nextMedia,
prevMedia,
} = mediaViewerDataStore;
const { deleteItemAction } = filesActionsStore;
const { getIcon, extsImagePreviewed, extsMediaPreviewed } = settingsStore;
@ -262,6 +262,9 @@ export default inject(
return {
files,
playlist,
currentPostionIndex,
nextMedia,
prevMedia,
userAccess,
visible: playlist.length > 0 && visible,
currentMediaFileId,
@ -295,6 +298,7 @@ export default inject(
onCopyAction,
onDuplicate,
archiveRoomsId,
setSelection,
};
}
)(

View File

@ -1,4 +1,8 @@
import { makeAutoObservable } from "mobx";
import { makeAutoObservable, runInAction } from "mobx";
import {
isNullOrUndefined,
findNearestIndex,
} from "@docspace/common/components/MediaViewer/helpers";
class MediaViewerDataStore {
filesStore;
@ -8,6 +12,7 @@ class MediaViewerDataStore {
visible = false;
previewFile = null;
currentItem = null;
prevPostionIndex = 0;
constructor(filesStore, settingsStore) {
makeAutoObservable(this);
@ -45,6 +50,68 @@ class MediaViewerDataStore {
this.id = id;
};
changeUrl = (id) => {
const url = "/products/files/#preview/" + id;
window.history.pushState(null, null, url);
};
nextMedia = () => {
const { setBufferSelection, files } = this.filesStore;
const postionIndex = (this.currentPostionIndex + 1) % this.playlist.length;
if (postionIndex === 0) {
return;
}
const currentFileId = this.playlist[postionIndex].fileId;
const targetFile = files.find((item) => item.id === currentFileId);
if (!isNullOrUndefined(targetFile)) setBufferSelection(targetFile);
const fileId = this.playlist[postionIndex].fileId;
this.setCurrentId(fileId);
this.changeUrl(fileId);
};
prevMedia = () => {
const { setBufferSelection, files } = this.filesStore;
let currentPlaylistPos = this.currentPostionIndex - 1;
if (currentPlaylistPos === -1) {
return;
}
const currentFileId = this.playlist[currentPlaylistPos].fileId;
const targetFile = files.find((item) => item.id === currentFileId);
if (!isNullOrUndefined(targetFile)) setBufferSelection(targetFile);
const fileId = this.playlist[currentPlaylistPos].fileId;
this.setCurrentId(fileId);
this.changeUrl(fileId);
};
get currentPostionIndex() {
if (this.playlist.length === 0) {
return 0;
}
let index = this.playlist.find((file) => file.fileId === this.id)?.id;
if (isNullOrUndefined(index)) {
index = findNearestIndex(this.playlist, this.prevPostionIndex);
}
runInAction(() => {
this.prevPostionIndex = index;
});
return index;
}
get playlist() {
const { files } = this.filesStore;
@ -83,6 +150,11 @@ class MediaViewerDataStore {
id++;
}
});
if (this.previewFile) {
runInAction(() => {
this.previewFile = null;
});
}
} else if (this.previewFile) {
playlist.push({
...this.previewFile,

View File

@ -1,722 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import styled from "styled-components";
import ImageViewer from "./sub-components/image-viewer";
import equal from "fast-deep-equal/react";
import Hammer from "hammerjs";
import { isMobileOnly } from "react-device-detect";
import { FileStatus } from "@docspace/common/constants";
import InfoOutlineReactSvgUrl from "PUBLIC_DIR/images/info.outline.react.svg?url";
import CopyReactSvgUrl from "PUBLIC_DIR/images/copy.react.svg?url";
import DuplicateReactSvgUrl from "PUBLIC_DIR/images/duplicate.react.svg?url";
import DownloadReactSvgUrl from "PUBLIC_DIR/images/download.react.svg?url";
import DownloadAsReactSvgUrl from "PUBLIC_DIR/images/download-as.react.svg?url";
import RenameReactSvgUrl from "PUBLIC_DIR/images/rename.react.svg?url";
import TrashReactSvgUrl from "PUBLIC_DIR/images/trash.react.svg?url";
import MoveReactSvgUrl from "PUBLIC_DIR/images/duplicate.react.svg?url";
const mediaTypes = Object.freeze({
audio: 1,
video: 2,
});
const ButtonKeys = Object.freeze({
leftArrow: 37,
rightArrow: 39,
upArrow: 38,
downArrow: 40,
space: 32,
esc: 27,
ctr: 17,
one: 49,
del: 46,
s: 83,
});
let ctrIsPressed = false;
class MediaViewer extends React.Component {
constructor(props) {
super(props);
const { playlist, currentFileId, visible } = props;
const item = playlist.find(
(file) => String(file.fileId) === String(currentFileId)
);
if (!item) {
console.error("MediaViewer: file not found in playlist", {
playlist,
currentFileId,
});
return;
}
const playlistPos = item ? item.id : 0;
this.state = {
visible,
allowConvert: true,
playlist,
playlistPos,
fileUrl: item.src,
canSwipeImage: true,
};
this.detailsContainer = React.createRef();
this.viewerToolbox = React.createRef();
}
updateHammer() {
const { playlistPos, playlist } = this.state;
const currentFile = playlist[playlistPos];
const { title } = currentFile;
const ext = this.getFileExtension(title);
const _this = this;
if (this.hammer) {
this.hammer.off("doubletap", this.prevMedia);
}
this.hammer = null;
setTimeout(function () {
try {
if (_this.canImageView(ext)) {
const pinch = new Hammer.Pinch();
_this.hammer = Hammer(
document.getElementsByClassName("react-viewer-canvas")[0]
);
_this.hammer.add([pinch]);
_this.hammer.on("doubletap", _this.doubleTap);
}
} catch (ex) {
//console.error("MediaViewer updateHammer", ex);
this.hammer = null;
}
}, 500);
}
componentDidUpdate(prevProps, prevState) {
const {
visible,
playlist,
currentFileId,
onEmptyPlaylistError,
} = this.props;
const { playlistPos, fileUrl } = this.state;
const src = playlist[playlistPos]?.src;
const title = playlist[playlistPos]?.title;
const ext = this.getFileExtension(title);
if (visible !== prevProps.visible) {
const newPlaylistPos =
playlist.length > 0
? playlist.find((file) => file.fileId === currentFileId).id
: 0;
this.setState({
visible: visible,
playlistPos: newPlaylistPos,
});
}
if (
src &&
src !== fileUrl &&
playlistPos === prevState.playlistPos &&
ext !== ".tif" &&
ext !== ".tiff"
) {
this.setState({ fileUrl: src });
}
if (
visible &&
visible === prevProps.visible &&
playlistPos !== prevState.playlistPos
) {
this.updateHammer();
if (ext === ".tiff" || ext === ".tif") {
this.getTiffDataURL(src);
} else {
this.setState({ fileUrl: src });
}
}
if (
visible &&
visible === prevProps.visible &&
!equal(playlist, prevProps.playlist)
) {
if (playlist.length > 0) {
this.updateHammer();
//switching from index to id
const newPlaylistPos = currentFileId
? playlist.find((file) => file.fileId === currentFileId)?.id ?? 0
: 0;
this.setState({
playlist: playlist,
playlistPos: newPlaylistPos,
});
} else {
onEmptyPlaylistError();
this.setState({
visible: false,
});
}
} else if (!equal(playlist, prevProps.playlist)) {
this.setState({
playlist: playlist,
});
}
}
componentDidMount() {
const { playlist, files, setBufferSelection } = this.props;
const { playlistPos } = this.state;
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 = this.getFileExtension(title);
if (ext === ".tiff" || ext === ".tif") {
this.getTiffDataURL(src);
}
this.updateHammer();
document.addEventListener("keydown", this.onKeydown, false);
document.addEventListener("keyup", this.onKeyup, false);
}
componentWillUnmount() {
if (this.hammer) {
this.hammer.off("doubletap", this.prevMedia);
}
document.removeEventListener("keydown", this.onKeydown, false);
document.removeEventListener("keyup", this.onKeyup, false);
this.onClose();
}
mapSupplied = {
".aac": { supply: "m4a", type: mediaTypes.audio },
".flac": { supply: "mp3", type: mediaTypes.audio },
".m4a": { supply: "m4a", type: mediaTypes.audio },
".mp3": { supply: "mp3", type: mediaTypes.audio },
".oga": { supply: "oga", type: mediaTypes.audio },
".ogg": { supply: "oga", type: mediaTypes.audio },
".wav": { supply: "wav", type: mediaTypes.audio },
".f4v": { supply: "m4v", type: mediaTypes.video },
".m4v": { supply: "m4v", type: mediaTypes.video },
".mov": { supply: "m4v", type: mediaTypes.video },
".mp4": { supply: "m4v", type: mediaTypes.video },
".ogv": { supply: "ogv", type: mediaTypes.video },
".webm": { supply: "webmv", type: mediaTypes.video },
".wmv": { supply: "m4v", type: mediaTypes.video, convertable: true },
".avi": { supply: "m4v", type: mediaTypes.video, convertable: true },
".mpeg": { supply: "m4v", type: mediaTypes.video, convertable: true },
".mpg": { supply: "m4v", type: mediaTypes.video, convertable: true },
};
canImageView = function (ext) {
const { extsImagePreviewed } = this.props;
return extsImagePreviewed.indexOf(ext) != -1;
};
canPlay = (fileTitle, allowConvert) => {
const { extsMediaPreviewed } = this.props;
const ext =
fileTitle[0] === "." ? fileTitle : this.getFileExtension(fileTitle);
const supply = this.mapSupplied[ext];
const canConvert = allowConvert || this.props.allowConvert;
return (
!!supply &&
extsMediaPreviewed.indexOf(ext) != -1 &&
(!supply.convertable || canConvert)
);
};
getFileExtension = (fileTitle) => {
if (!fileTitle) {
return "";
}
fileTitle = fileTitle.trim();
const posExt = fileTitle.lastIndexOf(".");
return 0 <= posExt ? fileTitle.substring(posExt).trim().toLowerCase() : "";
};
zoom = 1;
handleZoomEnd = () => {
this.zoom = 1;
};
handleZoomIn = (e) => {
if (this.zoom - e.scale > 0.1) {
this.zoom = e.scale;
document.querySelector('li[data-key="zoomOut"]').click();
}
};
handleZoomOut = (e) => {
if (e.scale - this.zoom > 0.3) {
this.zoom = e.scale;
document.querySelector('li[data-key="zoomIn"]').click();
}
};
doubleTap = () => {
document.querySelector('li[data-key="zoomIn"]')?.click();
};
prevMedia = () => {
const { playlistPos, playlist } = this.state;
const { setBufferSelection } = this.props;
let currentPlaylistPos = playlistPos;
currentPlaylistPos--;
if (currentPlaylistPos === -1) return;
if (currentPlaylistPos < 0) currentPlaylistPos = playlist.length - 1;
const currentFileId = playlist[currentPlaylistPos].fileId;
const targetFile = this.props.files.find(
(item) => item.id === currentFileId
);
setBufferSelection(targetFile);
this.setState({
playlistPos: currentPlaylistPos,
});
const id = playlist[currentPlaylistPos].fileId;
this.props.onChangeUrl(id);
};
nextMedia = () => {
const { playlistPos, playlist } = this.state;
const { setBufferSelection } = this.props;
let currentPlaylistPos = playlistPos;
currentPlaylistPos = (currentPlaylistPos + 1) % playlist.length;
if (currentPlaylistPos === 0) return;
const currentFileId = playlist[currentPlaylistPos].fileId;
const targetFile = this.props.files.find(
(item) => item.id === currentFileId
);
setBufferSelection(targetFile);
this.setState({
playlistPos: currentPlaylistPos,
});
const id = playlist[currentPlaylistPos].fileId;
this.props.onChangeUrl(id);
};
getOffset = () => {
if (this.detailsContainer.current && this.viewerToolbox.current) {
return (
this.detailsContainer.current.offsetHeight +
this.viewerToolbox.current.offsetHeight
);
} else {
return 0;
}
};
onDelete = () => {
const { playlist, playlistPos } = this.state;
let currentFileId =
playlist.length > 0
? playlist.find((file) => file.id === playlistPos).fileId
: 0;
this.props.onDelete && this.props.onDelete(currentFileId);
this.setState({
canSwipeImage: false,
});
};
onDownload = () => {
const { playlist, playlistPos } = this.state;
let currentFileId =
playlist.length > 0
? playlist.find((file) => file.id === playlistPos).fileId
: 0;
this.props.onDownload && this.props.onDownload(currentFileId);
};
onKeyup = (e) => {
if (ButtonKeys.ctr === e.keyCode) {
ctrIsPressed = false;
}
};
onKeydown = (e) => {
let isActionKey = false;
for (let key in ButtonKeys) {
if (ButtonKeys[key] === e.keyCode) {
e.preventDefault();
isActionKey = true;
}
}
if (isActionKey) {
switch (e.keyCode) {
case ButtonKeys.leftArrow:
if (document.fullscreenElement) return;
this.state.canSwipeImage
? ctrIsPressed
? document.getElementsByClassName("iconContainer rotateLeft")
.length > 0 &&
document
.getElementsByClassName("iconContainer rotateLeft")[0]
.click()
: this.prevMedia()
: null;
break;
case ButtonKeys.rightArrow:
if (document.fullscreenElement) return;
this.state.canSwipeImage
? ctrIsPressed
? document.getElementsByClassName("iconContainer rotateRight")
.length > 0 &&
document
.getElementsByClassName("iconContainer rotateRight")[0]
.click()
: this.nextMedia()
: null;
break;
case ButtonKeys.space:
document.getElementsByClassName("video-play").length > 0 &&
document.getElementsByClassName("video-play")[0].click();
break;
case ButtonKeys.esc:
if (!this.props.deleteDialogVisible) this.props.onClose();
break;
case ButtonKeys.upArrow:
document.getElementsByClassName("iconContainer zoomIn").length > 0 &&
document.getElementsByClassName("iconContainer zoomIn")[0].click();
break;
case ButtonKeys.downArrow:
document.getElementsByClassName("iconContainer zoomOut").length > 0 &&
document.getElementsByClassName("iconContainer zoomOut")[0].click();
break;
case ButtonKeys.ctr:
ctrIsPressed = true;
break;
case ButtonKeys.s:
if (ctrIsPressed) this.onDownload();
break;
case ButtonKeys.one:
ctrIsPressed &&
document.getElementsByClassName("iconContainer reset").length > 0 &&
document.getElementsByClassName("iconContainer reset")[0].click();
break;
case ButtonKeys.del:
this.onDelete();
break;
default:
break;
}
}
};
onClose = (e) => {
//fix memory leak
this.setState({ visible: false });
this.props.onClose(e);
};
getTiffDataURL = (src) => {
if (!window.Tiff) return;
const _this = this;
const xhr = new XMLHttpRequest();
xhr.responseType = "arraybuffer";
xhr.open("GET", src);
xhr.onload = function () {
try {
const tiff = new window.Tiff({ buffer: xhr.response });
const dataUrl = tiff.toDataURL();
_this.setState({ fileUrl: dataUrl });
} catch (e) {
console.log(e);
}
};
xhr.send();
};
render() {
const { playlistPos, playlist, visible, fileUrl } = this.state;
const {
t,
onClose,
userAccess,
canDelete,
canDownload,
errorLabel,
isPreviewFile,
onClickFavorite,
onShowInfoPanel,
onClickDownload,
onMoveAction,
onCopyAction,
onDuplicate,
onClickDownloadAs,
getIcon,
onClickRename,
onClickDelete,
setBufferSelection,
files,
archiveRoomsId,
} = this.props;
const currentFileId =
playlist.length > 0
? playlist.find((file) => file.id === playlistPos).fileId
: 0;
const currentFile = playlist[playlistPos];
const targetFile =
files.find((item) => item.id === currentFileId) || playlist[0];
const archiveRoom =
archiveRoomsId === targetFile.rootFolderId ||
(!targetFile?.security?.Rename && !targetFile?.security?.Delete);
const { title } = currentFile;
let isImage = false;
let isVideo = false;
let isAudio = false;
let canOpen = true;
const isFavorite =
(playlist[playlistPos].fileStatus & FileStatus.IsFavorite) ===
FileStatus.IsFavorite;
const ext = this.getFileExtension(title);
const onSetSelectionFile = () => {
setBufferSelection(targetFile);
};
const getContextModel = () => {
const desktopModel = [
{
key: "download",
label: t("Common:Download"),
icon: DownloadReactSvgUrl,
onClick: () => onClickDownload(targetFile, t),
disabled: false,
},
{
key: "rename",
label: t("Rename"),
icon: RenameReactSvgUrl,
onClick: () => onClickRename(targetFile),
disabled: archiveRoom,
},
{
key: "delete",
label: t("Common:Delete"),
icon: TrashReactSvgUrl,
onClick: () => onClickDelete(targetFile, t),
disabled: archiveRoom,
},
];
const model = [
{
id: "option_room-info",
key: "room-info",
label: t("Common:Info"),
icon: InfoOutlineReactSvgUrl,
onClick: () => {
return onShowInfoPanel(targetFile);
},
disabled: false,
},
{
key: "download",
label: t("Common:Download"),
icon: DownloadReactSvgUrl,
onClick: () => onClickDownload(targetFile, t),
disabled: false,
},
{
key: "move-to",
label: t("MoveTo"),
icon: MoveReactSvgUrl,
onClick: onMoveAction,
disabled: !targetFile.security.Move,
},
// {
// key: "download-as",
// label: t("Translations:DownloadAs"),
// icon: DownloadAsReactSvgUrl, // TODO: uncomment when we can download media by changing the format
// onClick: onClickDownloadAs,
// disabled: false,
// },
{
id: "option_copy-to",
key: "copy-to",
label: t("Translations:Copy"),
icon: CopyReactSvgUrl,
onClick: onCopyAction,
disabled: !targetFile.security.Copy,
},
{
id: "option_create-copy",
key: "copy",
label: t("Common:Duplicate"),
icon: DuplicateReactSvgUrl,
onClick: () => onDuplicate(targetFile, t),
disabled: !targetFile.security.Duplicate,
},
{
key: "rename",
label: t("Rename"),
icon: RenameReactSvgUrl,
onClick: () => onClickRename(targetFile),
disabled: !targetFile.security.Rename,
},
{
key: "separator0",
isSeparator: true,
disabled: !targetFile.security.Delete,
},
{
key: "delete",
label: t("Common:Delete"),
icon: TrashReactSvgUrl,
onClick: () => onClickDelete(targetFile, t),
disabled: !targetFile.security.Delete,
},
];
return isMobileOnly
? model
: isImage && !isMobileOnly
? desktopModel.filter((el) => el.key !== "download")
: desktopModel;
};
if (!this.canPlay(ext) && !this.canImageView(ext)) {
canOpen = false;
this.props.onError && this.props.onError();
}
if (this.canImageView(ext)) {
isImage = true;
} else {
isImage = false;
isVideo = this.mapSupplied[ext]
? this.mapSupplied[ext].type == mediaTypes.video
: false;
isAudio = this.mapSupplied[ext]
? this.mapSupplied[ext].type == mediaTypes.audio
: false;
}
let audioIcon = getIcon(96, ext);
let headerIcon = getIcon(24, ext);
// TODO: rewrite with fileURL
/*if (this.mapSupplied[ext])
if (!isImage && this.mapSupplied[ext].convertable && !src.includes("#")) {
src += (src.includes("?") ? "&" : "?") + "convpreview=true";
}*/
return (
<>
{canOpen && (
<ImageViewer
userAccess={userAccess}
visible={visible}
title={title}
onClose={this.onClose}
images={[{ src: fileUrl, alt: "" }]}
inactive={playlist.length <= 1}
playlist={playlist}
playlistPos={playlistPos}
onNextClick={this.nextMedia}
onSetSelectionFile={onSetSelectionFile}
contextModel={getContextModel}
onPrevClick={this.prevMedia}
onDeleteClick={this.onDelete}
isFavorite={isFavorite}
headerIcon={headerIcon}
isImage={isImage}
isAudio={isAudio}
isVideo={isVideo}
isPreviewFile={isPreviewFile}
audioIcon={audioIcon}
onDownloadClick={this.onDownload}
archiveRoom={archiveRoom}
errorTitle={t("Files:MediaError")}
// isFavoritesFolder={isFavoritesFolder}
/>
)}
</>
);
}
}
MediaViewer.propTypes = {
allowConvert: PropTypes.bool,
visible: PropTypes.bool,
currentFileId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
playlist: PropTypes.arrayOf(PropTypes.object),
extsImagePreviewed: PropTypes.arrayOf(PropTypes.string),
extsMediaPreviewed: PropTypes.arrayOf(PropTypes.string),
onError: PropTypes.func,
canDelete: PropTypes.func,
canDownload: PropTypes.func,
onDelete: PropTypes.func,
onDownload: PropTypes.func,
onClose: PropTypes.func,
onEmptyPlaylistError: PropTypes.func,
deleteDialogVisible: PropTypes.bool,
errorLabel: PropTypes.string,
isPreviewFile: PropTypes.bool,
onChangeUrl: PropTypes.func,
};
MediaViewer.defaultProps = {
currentFileId: 0,
visible: false,
allowConvert: true,
canDelete: () => {
return true;
},
canDownload: () => {
return true;
},
isPreviewFile: false,
};
export default MediaViewer;

View File

@ -0,0 +1,45 @@
import { IFile, NumberOrString, PlaylistType, TranslationType } from "./types";
export interface MediaViewerProps {
t: TranslationType;
userAccess: boolean;
currentFileId: NumberOrString;
visible: boolean;
extsMediaPreviewed: string[];
extsImagePreviewed: string[];
deleteDialogVisible: boolean;
errorLabel: string;
isPreviewFile: boolean;
files: IFile[];
playlist: PlaylistType[];
setBufferSelection: Function;
archiveRoomsId: number;
playlistPos: number;
getIcon: (size: number, ext: string, ...arg: any) => any;
onClose: VoidFunction;
onError?: VoidFunction;
onEmptyPlaylistError: VoidFunction;
onDelete: (id: NumberOrString) => void;
onDownload: (id: NumberOrString) => void;
onChangeUrl: (id: NumberOrString) => void;
onMoveAction: VoidFunction;
onCopyAction: VoidFunction;
onClickRename: (file: IFile) => void;
onShowInfoPanel: (file: IFile) => void;
onDuplicate: (file: IFile, t: TranslationType) => void;
onClickDelete: (file: IFile, t: TranslationType) => void;
onClickDownload: (file: IFile, t: TranslationType) => void;
nextMedia: VoidFunction;
prevMedia: VoidFunction;
}

View File

@ -0,0 +1,59 @@
import { NullOrUndefined, PlaylistType } from "../types";
export const mediaTypes = Object.freeze({
audio: 1,
video: 2,
});
export enum KeyboardEventKeys {
ArrowRight = "ArrowRight",
ArrowLeft = "ArrowLeft",
Escape = "Escape",
Space = "Space",
Delete = "Delete",
KeyS = "KeyS",
Numpad1 = "Numpad1",
Digit1 = "Digit1",
}
export const mapSupplied = {
".aac": { supply: "m4a", type: mediaTypes.audio },
".flac": { supply: "mp3", type: mediaTypes.audio },
".m4a": { supply: "m4a", type: mediaTypes.audio },
".mp3": { supply: "mp3", type: mediaTypes.audio },
".oga": { supply: "oga", type: mediaTypes.audio },
".ogg": { supply: "oga", type: mediaTypes.audio },
".wav": { supply: "wav", type: mediaTypes.audio },
".f4v": { supply: "m4v", type: mediaTypes.video },
".m4v": { supply: "m4v", type: mediaTypes.video },
".mov": { supply: "m4v", type: mediaTypes.video },
".mp4": { supply: "m4v", type: mediaTypes.video },
".ogv": { supply: "ogv", type: mediaTypes.video },
".webm": { supply: "webmv", type: mediaTypes.video },
".wmv": { supply: "m4v", type: mediaTypes.video, convertable: true },
".avi": { supply: "m4v", type: mediaTypes.video, convertable: true },
".mpeg": { supply: "m4v", type: mediaTypes.video, convertable: true },
".mpg": { supply: "m4v", type: mediaTypes.video, convertable: true },
} as Record<string, { supply: string; type: number } | undefined>;
export const isNullOrUndefined = (arg: unknown): arg is NullOrUndefined => {
return arg === undefined || arg === null;
};
export const findNearestIndex = (
items: PlaylistType[],
index: number
): number => {
if (!Array.isArray(items) || items.length === 0 || index < 0) {
return -1;
}
let found = items[0].id;
for (const item of items) {
if (Math.abs(item.id - index) < Math.abs(found - index)) {
found = item.id;
}
}
return found;
};

View File

@ -1 +0,0 @@
export default from "./MediaViewer";

View File

@ -0,0 +1,448 @@
import { isMobileOnly } from "react-device-detect";
import React, { useState, useCallback, useMemo, useEffect } from "react";
import ImageViewer from "./sub-components/image-viewer";
import { MediaViewerProps } from "./MediaViewer.props";
import { FileStatus } from "@docspace/common/constants";
import {
isNullOrUndefined,
KeyboardEventKeys,
mapSupplied,
mediaTypes,
} from "./helpers";
import InfoOutlineReactSvgUrl from "PUBLIC_DIR/images/info.outline.react.svg?url";
import CopyReactSvgUrl from "PUBLIC_DIR/images/copy.react.svg?url";
import DuplicateReactSvgUrl from "PUBLIC_DIR/images/duplicate.react.svg?url";
import DownloadReactSvgUrl from "PUBLIC_DIR/images/download.react.svg?url";
import RenameReactSvgUrl from "PUBLIC_DIR/images/rename.react.svg?url";
import TrashReactSvgUrl from "PUBLIC_DIR/images/trash.react.svg?url";
import MoveReactSvgUrl from "PUBLIC_DIR/images/duplicate.react.svg?url";
function MediaViewer({
playlistPos,
nextMedia,
prevMedia,
...props
}: MediaViewerProps): JSX.Element {
const [title, setTitle] = useState<string>("");
const [canSwipeImage, setCanSwipeImage] = useState<boolean>(true);
const [fileUrl, setFileUrl] = useState<string>(() => {
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(() => {
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);
}
document.addEventListener("keydown", onKeydown);
return () => {
document.removeEventListener("keydown", onKeydown);
};
}, []);
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) {
setFileUrl(src);
}
if (ext === ".tiff" || ext === ".tif") {
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.length,
props.files.length,
props.currentFileId,
playlistPos,
]);
const getContextModel = () => {
const {
t,
onClickDownload,
onClickRename,
onClickDelete,
onShowInfoPanel,
onMoveAction,
onCopyAction,
onDuplicate,
} = props;
if (!targetFile) return [];
const desktopModel = [
{
key: "download",
label: t("Common:Download"),
icon: DownloadReactSvgUrl,
onClick: () => onClickDownload(targetFile, t),
disabled: false,
},
{
key: "rename",
label: t("Rename"),
icon: RenameReactSvgUrl,
onClick: () => onClickRename(targetFile),
disabled: archiveRoom,
},
{
key: "delete",
label: t("Common:Delete"),
icon: TrashReactSvgUrl,
onClick: () => onClickDelete(targetFile, t),
disabled: archiveRoom,
},
];
const model = [
{
id: "option_room-info",
key: "room-info",
label: t("Common:Info"),
icon: InfoOutlineReactSvgUrl,
onClick: () => {
return onShowInfoPanel(targetFile);
},
disabled: false,
},
{
key: "download",
label: t("Common:Download"),
icon: DownloadReactSvgUrl,
onClick: () => onClickDownload(targetFile, t),
disabled: false,
},
{
key: "move-to",
label: t("MoveTo"),
icon: MoveReactSvgUrl,
onClick: onMoveAction,
disabled: !targetFile.security.Move,
},
{
id: "option_copy-to",
key: "copy-to",
label: t("Translations:Copy"),
icon: CopyReactSvgUrl,
onClick: onCopyAction,
disabled: !targetFile.security.Copy,
},
{
id: "option_create-copy",
key: "copy",
label: t("Common:Duplicate"),
icon: DuplicateReactSvgUrl,
onClick: () => onDuplicate(targetFile, t),
disabled: !targetFile.security.Duplicate,
},
{
key: "rename",
label: t("Rename"),
icon: RenameReactSvgUrl,
onClick: () => onClickRename(targetFile),
disabled: !targetFile.security.Rename,
},
{
key: "separator0",
isSeparator: true,
disabled: !targetFile.security.Delete,
},
{
key: "delete",
label: t("Common:Delete"),
icon: TrashReactSvgUrl,
onClick: () => onClickDelete(targetFile, t),
disabled: !targetFile.security.Delete,
},
];
return isMobileOnly
? model
: isImage && !isMobileOnly
? 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]
);
const getFileExtension = useCallback((fileTitle: string) => {
if (!fileTitle) {
return "";
}
fileTitle = fileTitle.trim();
const posExt = fileTitle.lastIndexOf(".");
return 0 <= posExt ? fileTitle.substring(posExt).trim().toLowerCase() : "";
}, []);
const onDelete = () => {
const { playlist, onDelete } = props;
let currentFileId = playlist.find((file) => file.id === playlistPos)
?.fileId;
setCanSwipeImage(false);
if (!isNullOrUndefined(currentFileId)) onDelete(currentFileId);
};
const onDownload = () => {
const { playlist, onDownload } = props;
let currentFileId = playlist.find((file) => file.id === playlistPos)
?.fileId;
if (!isNullOrUndefined(currentFileId)) onDownload(currentFileId);
};
const onKeydown = (event: KeyboardEvent) => {
const { code, ctrlKey } = event;
if (code in KeyboardEventKeys) {
event.preventDefault();
}
switch (code) {
case KeyboardEventKeys.ArrowLeft:
if (document.fullscreenElement || !canSwipeImage) return;
if (ctrlKey) {
const rotateLeftElement = document.getElementsByClassName(
"iconContainer rotateLeft"
)?.[0] as HTMLElement | undefined;
rotateLeftElement?.click();
} else {
prevMedia();
}
break;
case KeyboardEventKeys.ArrowRight:
if (document.fullscreenElement || !canSwipeImage) return;
if (ctrlKey) {
const rotateRightElement = document.getElementsByClassName(
"iconContainer rotateRight"
)?.[0] as HTMLElement | undefined;
rotateRightElement?.click();
} else {
nextMedia();
}
break;
case KeyboardEventKeys.Space:
const videoPlayElement = document.getElementsByClassName(
"video-play"
)?.[0] as HTMLElement | undefined;
videoPlayElement?.click();
break;
case KeyboardEventKeys.Escape:
if (!props.deleteDialogVisible) props.onClose();
break;
case KeyboardEventKeys.KeyS:
if (ctrlKey) onDownload();
break;
case KeyboardEventKeys.Digit1:
case KeyboardEventKeys.Numpad1:
if (ctrlKey) {
const resetElement = document.getElementsByClassName(
"iconContainer reset"
)?.[0] as HTMLElement | undefined;
resetElement?.click();
}
break;
case KeyboardEventKeys.Delete:
onDelete();
break;
default:
break;
}
};
const onClose = useCallback(() => {
props.onClose();
}, [props.onClose]);
const fetchAndSetTiffDataURL = useCallback((src: string) => {
if (!window.Tiff) return;
const xhr = new XMLHttpRequest();
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 images = useMemo(() => [{ src: fileUrl, alt: "" }], [fileUrl]);
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;
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;
}
return (
<>
{canOpen && (
<ImageViewer
userAccess={props.userAccess}
visible={props.visible}
title={title}
onClose={onClose}
images={images}
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}
isPreviewFile={props.isPreviewFile}
onDownloadClick={onDownload}
archiveRoom={archiveRoom}
errorTitle={props.t("Files:MediaError")}
headerIcon={headerIcon}
audioIcon={audioIcon}
/>
)}
</>
);
}
export default MediaViewer;

View File

@ -0,0 +1,89 @@
declare global {
interface Window {
Tiff: new (arg: object) => any;
}
}
export type TranslationType = (key: string, opt?: object) => string;
export type NumberOrString = number | string;
export type NullOrUndefined = null | undefined;
export type PlaylistType = {
id: number;
canShare: boolean;
fileExst: string;
fileId: number;
fileStatus: number;
src: string;
title: string;
};
export type CreatedType = {
id: string;
avatarSmall: string;
displayName: string;
hasAvatar: boolean;
profileUrl: string;
};
export type SecurityType = {
Comment: boolean;
Copy: boolean;
CustomFilter: boolean;
Delete: boolean;
Duplicate: boolean;
Edit: boolean;
EditHistory: boolean;
FillForms: boolean;
Lock: boolean;
Move: boolean;
Read: boolean;
ReadHistory: boolean;
Rename: boolean;
Review: boolean;
};
export type ViewAccessabilityType = {
CoAuhtoring: boolean;
Convert: boolean;
ImageView: boolean;
MediaView: boolean;
WebComment: boolean;
WebCustomFilterEditing: boolean;
WebEdit: boolean;
WebRestrictedEditing: boolean;
WebReview: boolean;
WebView: boolean;
};
export interface IFile {
id: number;
access: number;
canShare: boolean;
comment: string;
contentLength: string;
created: string;
createdBy: CreatedType;
denyDownload: boolean;
denySharing: boolean;
fileExst: string;
fileStatus: number;
fileType: number;
folderId: number;
pureContentLength: number;
rootFolderId: number;
rootFolderType: number;
security: SecurityType;
shared: boolean;
thumbnailStatus: number;
title: string;
updated: string;
updatedBy: CreatedType;
version: number;
versionGroup: number;
viewAccessability: ViewAccessabilityType;
viewUrl: string;
webUrl: string;
}

View File

@ -455,7 +455,7 @@ const ViewerBase = (props) => {
document[funcName]("keydown", handleKeydown, true);
}
if (viewerCore.current) {
viewerCore.current[funcName]("wheel", handleMouseScroll, false);
viewerCore.current[funcName]("wheel", handleMouseScroll, {passive: true});
}
}