diff --git a/packages/client/src/pages/Home/MediaViewer/index.js b/packages/client/src/pages/Home/MediaViewer/index.js
index 000e281c1a..3a0da85f61 100644
--- a/packages/client/src/pages/Home/MediaViewer/index.js
+++ b/packages/client/src/pages/Home/MediaViewer/index.js
@@ -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,
};
}
)(
diff --git a/packages/client/src/store/MediaViewerDataStore.js b/packages/client/src/store/MediaViewerDataStore.js
index 0a50b03be3..7ad3732a80 100644
--- a/packages/client/src/store/MediaViewerDataStore.js
+++ b/packages/client/src/store/MediaViewerDataStore.js
@@ -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,
diff --git a/packages/common/components/MediaViewer/MediaViewer.js b/packages/common/components/MediaViewer/MediaViewer.js
deleted file mode 100644
index aa62afac04..0000000000
--- a/packages/common/components/MediaViewer/MediaViewer.js
+++ /dev/null
@@ -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 && (
-
- )}
- >
- );
- }
-}
-
-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;
diff --git a/packages/common/components/MediaViewer/MediaViewer.props.ts b/packages/common/components/MediaViewer/MediaViewer.props.ts
new file mode 100644
index 0000000000..f1a83794f8
--- /dev/null
+++ b/packages/common/components/MediaViewer/MediaViewer.props.ts
@@ -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;
+}
diff --git a/packages/common/components/MediaViewer/helpers/index.ts b/packages/common/components/MediaViewer/helpers/index.ts
new file mode 100644
index 0000000000..79daa46e26
--- /dev/null
+++ b/packages/common/components/MediaViewer/helpers/index.ts
@@ -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;
+
+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;
+};
diff --git a/packages/common/components/MediaViewer/index.js b/packages/common/components/MediaViewer/index.js
deleted file mode 100644
index 76bd8cb268..0000000000
--- a/packages/common/components/MediaViewer/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export default from "./MediaViewer";
diff --git a/packages/common/components/MediaViewer/index.tsx b/packages/common/components/MediaViewer/index.tsx
new file mode 100644
index 0000000000..782e3e4f20
--- /dev/null
+++ b/packages/common/components/MediaViewer/index.tsx
@@ -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("");
+ const [canSwipeImage, setCanSwipeImage] = useState(true);
+ const [fileUrl, setFileUrl] = useState(() => {
+ 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(() => {
+ 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 && (
+
+ )}
+ >
+ );
+}
+
+export default MediaViewer;
diff --git a/packages/common/components/MediaViewer/types/index.ts b/packages/common/components/MediaViewer/types/index.ts
new file mode 100644
index 0000000000..7b152d2249
--- /dev/null
+++ b/packages/common/components/MediaViewer/types/index.ts
@@ -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;
+}
diff --git a/packages/components/selection-area/SelectionArea.js b/packages/components/selection-area/SelectionArea.js
index 3191c31b43..f896bda910 100644
--- a/packages/components/selection-area/SelectionArea.js
+++ b/packages/components/selection-area/SelectionArea.js
@@ -234,6 +234,9 @@ class SelectionArea extends React.Component {
passive: false,
});
document.addEventListener("mouseup", this.onTapStop);
+
+ window.addEventListener("blur", this.onTapStop);
+
this.scrollElement.addEventListener("scroll", this.onScroll);
};
@@ -242,6 +245,7 @@ class SelectionArea extends React.Component {
document.removeEventListener("mousemove", this.onTapMove);
document.removeEventListener("mouseup", this.onTapStop);
+ window.removeEventListener("blur", this.onTapStop);
this.scrollElement.removeEventListener("scroll", this.onScroll);
};
@@ -256,7 +260,11 @@ class SelectionArea extends React.Component {
folderHeaderHeight,
} = this.props;
- if (e.target.closest(".not-selectable")) return;
+ if (
+ e.target.closest(".not-selectable") ||
+ e.target.closest(".row-selected")
+ )
+ return;
const selectables = document.getElementsByClassName(selectableClass);
if (!selectables.length) return;
diff --git a/packages/components/viewer/sub-components/viewer-base.js b/packages/components/viewer/sub-components/viewer-base.js
index 5b55e9f92f..2e16c09dac 100644
--- a/packages/components/viewer/sub-components/viewer-base.js
+++ b/packages/components/viewer/sub-components/viewer-base.js
@@ -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});
}
}