Merge branch 'develop' into feature/browserlist

This commit is contained in:
Ilya Oleshko 2023-02-20 17:41:23 +03:00
commit c7465e420a
20 changed files with 584 additions and 1161 deletions

View File

@ -181,6 +181,8 @@ class FilesStore {
const newFiles = [fileInfo, ...this.files];
if (this.files.findIndex((x) => x.id === opt?.id) > -1) return;
if (newFiles.length > this.filter.pageCount && withPaging) {
newFiles.pop(); // Remove last
}

View File

@ -0,0 +1,22 @@
import styled from "styled-components";
import { Base } from "@docspace/components/themes";
const ControlBtn = styled.div`
display: inline-block;
height: 30px;
line-height: 25px;
margin: 5px;
width: 40px;
border-radius: 2px;
cursor: pointer;
text-align: center;
&:hover {
background-color: ${(props) =>
props.theme.mediaViewer.controlBtn.backgroundColor};
}
`;
ControlBtn.defaultProps = { theme: Base };
export default ControlBtn;

View File

@ -0,0 +1,15 @@
import styled from "styled-components";
type StyledButtonScrollProps = {
orientation: "left" | "right";
};
const StyledButtonScroll = styled.div<StyledButtonScrollProps>`
z-index: 307;
position: fixed;
top: calc(50% - 20px);
${(props) => (props.orientation === "left" ? "left: 20px;" : "right: 20px;")}
`;
export default StyledButtonScroll;

View File

@ -0,0 +1,47 @@
import styled from "styled-components";
const StyledMobileDetails = styled.div`
z-index: 307;
position: fixed;
top: 0;
left: 0;
right: 0;
height: 53px;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.8) 100%
);
svg {
path {
fill: #fff;
}
}
.mobile-close {
position: fixed;
left: 21px;
top: 22px;
}
.mobile-context {
position: fixed;
right: 22px;
top: 22px;
}
.title {
font-weight: 600;
margin-top: 6px;
width: calc(100% - 100px);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`;
export default StyledMobileDetails;

View File

@ -0,0 +1,23 @@
import styled from "styled-components";
type StyledSwitchToolbarProps = {
left?: boolean;
};
const StyledSwitchToolbar = styled.div<StyledSwitchToolbarProps>`
height: 100%;
z-index: 306;
position: fixed;
width: 73px;
background: inherit;
display: block;
opacity: 0;
transition: all 0.3s;
${(props) => (props.left ? "left: 0" : "right: 0")};
&:hover {
cursor: pointer;
opacity: 1;
}
`;
export default StyledSwitchToolbar;

View File

@ -0,0 +1,84 @@
import styled from "styled-components";
import { Base } from "@docspace/components/themes";
type StyledViewerContainerProps = {
visible: boolean;
};
const StyledViewerContainer = styled.div<StyledViewerContainerProps>`
color: ${(props) => props.theme.mediaViewer.color};
display: ${(props) => (props.visible ? "block" : "none")};
overflow: hidden;
span {
position: fixed;
right: 0;
bottom: 5px;
margin-right: 10px;
z-index: 305;
}
.deleteBtnContainer,
.downloadBtnContainer {
display: block;
width: 16px;
height: 16px;
margin: 4px 12px;
line-height: 19px;
svg {
path {
fill: ${(props) => props.theme.mediaViewer.fill};
}
}
}
.details {
z-index: 307;
padding-top: 21px;
height: 64px;
width: 100%;
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.8) 100%
);
position: fixed;
top: 0;
left: 0;
.title {
text-align: center;
white-space: nowrap;
overflow: hidden;
font-size: 20px;
font-weight: 600;
text-overflow: ellipsis;
width: calc(100% - 50px);
padding-left: 16px;
box-sizing: border-box;
color: ${(props) => props.theme.mediaViewer.titleColor};
}
}
.mediaPlayerClose {
position: fixed;
top: 13px;
right: 12px;
height: 17px;
&:hover {
background-color: transparent;
}
svg {
path {
fill: ${(props) => props.theme.mediaViewer.iconColor};
}
}
}
.containerVideo {
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
}
`;
StyledViewerContainer.defaultProps = { theme: Base };
export default StyledViewerContainer;

View File

@ -0,0 +1,5 @@
export { default as ControlBtn } from "./ControlBtn";
export { default as StyledButtonScroll } from "./StyledButtonScroll";
export { default as StyledSwitchToolbar } from "./StyledSwitchToolbar";
export { default as StyledViewerContainer } from "./StyledViewerContainer";
export { default as StyledMobileDetails } from "./StyledMobileDetails";

View File

@ -64,5 +64,5 @@ export const findNearestIndex = (
};
export const isSeparator = (arg: ContextMenuModel): arg is SeparatorType => {
return arg?.isSeparator;
return arg?.isSeparator !== undefined && arg.isSeparator;
};

View File

@ -0,0 +1,15 @@
import { ContextMenuModel } from "../../types";
interface MobileDetailsProps {
icon: string;
title: string;
isError: boolean;
isPreviewFile: boolean;
contextModel: () => ContextMenuModel[];
onHide: VoidFunction;
onMaskClick: VoidFunction;
onContextMenu: (e: TouchEvent) => void;
}
export default MobileDetailsProps;

View File

@ -0,0 +1,70 @@
import React, { ForwardedRef, useMemo } from "react";
import Text from "@docspace/components/text";
import ContextMenu from "@docspace/components/context-menu";
import { StyledMobileDetails } from "../../StyledComponents";
import type MobileDetailsProps from "./MobileDetails.props";
import BackArrow from "PUBLIC_DIR/images/viewer.media.back.react.svg";
import MediaContextMenu from "PUBLIC_DIR/images/vertical-dots.react.svg";
function MobileDetails(
{
icon,
title,
isError,
isPreviewFile,
onHide,
onMaskClick,
onContextMenu,
contextModel,
}: MobileDetailsProps,
ref: ForwardedRef<ContextMenu>
) {
const contextMenuHeader = useMemo(
() => ({
icon: icon,
title: title,
}),
[icon, title]
);
return (
<StyledMobileDetails>
<BackArrow className="mobile-close" onClick={onMaskClick} />
<Text
fontSize="14px"
color="#fff"
className="title"
as={undefined}
tag={undefined}
title={undefined}
textAlign={undefined}
fontWeight={undefined}
>
{title}
</Text>
{!isPreviewFile && !isError && (
<div className="details-context">
<MediaContextMenu
className="mobile-context"
onClick={onContextMenu}
/>
<ContextMenu
ref={ref}
withBackdrop={true}
onHide={onHide}
header={contextMenuHeader}
getContextModel={contextModel}
/>
</div>
)}
</StyledMobileDetails>
);
}
export default React.memo(
React.forwardRef<ContextMenu, MobileDetailsProps>(MobileDetails)
);

View File

@ -0,0 +1,23 @@
import React from "react";
import {
StyledButtonScroll,
StyledSwitchToolbar,
} from "../../StyledComponents";
import MediaNextIcon from "PUBLIC_DIR/images/viewer.next.react.svg";
type NextButtonProps = {
nextClick: VoidFunction;
};
function NextButton({ nextClick }: NextButtonProps) {
return (
<StyledSwitchToolbar onClick={nextClick}>
<StyledButtonScroll orientation="right">
<MediaNextIcon />
</StyledButtonScroll>
</StyledSwitchToolbar>
);
}
export default NextButton;

View File

@ -0,0 +1,23 @@
import React from "react";
import {
StyledButtonScroll,
StyledSwitchToolbar,
} from "../../StyledComponents";
import MediaPrevIcon from "PUBLIC_DIR/images/viewer.prew.react.svg";
type PrevButtonProps = {
prevClick: VoidFunction;
};
function PrevButton({ prevClick }: PrevButtonProps) {
return (
<StyledSwitchToolbar left onClick={prevClick}>
<StyledButtonScroll orientation="left">
<MediaPrevIcon />
</StyledButtonScroll>
</StyledSwitchToolbar>
);
}
export default PrevButton;

View File

@ -0,0 +1,36 @@
import { getCustomToolbar } from "../../helpers/getCustomToolbar";
import { ContextMenuModel, PlaylistType } from "../../types";
interface ViewerProps {
title: string;
images: { src: string; alt: string }[];
isAudio: boolean;
isVideo: boolean;
visible: boolean;
isImage: boolean;
playlist: PlaylistType[];
inactive: boolean;
audioIcon: string;
zoomSpeed: number;
errorTitle: string;
headerIcon: string;
customToolbar: () => ReturnType<typeof getCustomToolbar>;
playlistPos: number;
archiveRoom: boolean;
isPreviewFile: boolean;
onMaskClick: VoidFunction;
onNextClick: VoidFunction;
onPrevClick: VoidFunction;
contextModel: () => ContextMenuModel[];
onDownloadClick: VoidFunction;
generateContextMenu: (
isOpen: boolean,
right: string,
bottom: string
) => JSX.Element;
onSetSelectionFile: VoidFunction;
}
export default ViewerProps;

View File

@ -0,0 +1,215 @@
import ReactDOM from "react-dom";
import React, { useRef, useState, useEffect, useCallback } from "react";
import { isMobileOnly } from "react-device-detect";
import Text from "@docspace/components/text";
import IconButton from "@docspace/components/icon-button";
import ContextMenu from "@docspace/components/context-menu";
import { StyledViewer } from "@docspace/components/viewer/styled-viewer";
import ViewerPlayer from "@docspace/components/viewer/sub-components/viewer-player";
import { ControlBtn, StyledViewerContainer } from "../../StyledComponents";
import MobileDetails from "../MobileDetails";
import PrevButton from "../PrevButton";
import NextButton from "../NextButton";
import type ViewerProps from "./Viewer.props";
import ViewerMediaCloseSvgUrl from "PUBLIC_DIR/images/viewer.media.close.svg?url";
function Viewer(props: ViewerProps) {
const timerIDRef = useRef<NodeJS.Timeout>();
const containerRef = React.useRef(document.createElement("div"));
const [panelVisible, setPanelVisible] = useState<boolean>(true);
const [isOpenContextMenu, setIsOpenContextMenu] = useState<boolean>(false);
const [isError, setIsError] = useState<boolean>(false);
const [isPlay, setIsPlay] = useState<boolean | null>(null);
const [imageTimer, setImageTimer] = useState<NodeJS.Timeout>();
const contextMenuRef = useRef<ContextMenu>(null);
const videoElementRef = useRef<HTMLVideoElement>(null);
const [isFullscreen, setIsFullScreen] = useState<boolean>(false);
useEffect(() => {
document.body.appendChild(containerRef.current);
return () => {
document.body.removeChild(containerRef.current);
timerIDRef.current && clearTimeout(timerIDRef.current);
};
}, []);
useEffect(() => {
if ((!isPlay || isOpenContextMenu) && (!props.isImage || isOpenContextMenu))
return clearTimeout(timerIDRef.current);
}, [isPlay, isOpenContextMenu, props.isImage]);
useEffect(() => {
if (isMobileOnly) return;
const resetTimer = () => {
setPanelVisible(true);
clearTimeout(timerIDRef.current);
timerIDRef.current = setTimeout(() => setPanelVisible(false), 2500);
setImageTimer(timerIDRef.current);
};
document.addEventListener("mousemove", resetTimer, { passive: true });
return () => {
document.removeEventListener("mousemove", resetTimer);
clearTimeout(timerIDRef.current);
setPanelVisible(true);
};
}, [setImageTimer, setPanelVisible]);
useEffect(() => {
document.addEventListener("touchstart", onTouch);
return () => document.removeEventListener("touchstart", onTouch);
}, [setPanelVisible]);
const onTouch = useCallback(
(e: TouchEvent, canTouch?: boolean) => {
if (e.target === videoElementRef.current || canTouch) {
setPanelVisible((visible) => !visible);
}
},
[setPanelVisible]
);
const nextClick = () => {
clearTimeout(imageTimer);
props.onNextClick();
};
const prevClick = () => {
clearTimeout(imageTimer);
props.onPrevClick();
};
const onMobileContextMenu = useCallback(
(e: TouchEvent) => {
setIsOpenContextMenu((open) => !open);
props.onSetSelectionFile();
contextMenuRef.current?.show(e);
},
[props.onSetSelectionFile, setIsOpenContextMenu]
);
const onHide = useCallback(() => {
setIsOpenContextMenu(false);
}, [setIsOpenContextMenu]);
const mobileDetails = (
<MobileDetails
isError={isError}
title={props.title}
icon={props.headerIcon}
contextModel={props.contextModel}
isPreviewFile={props.isPreviewFile}
onHide={onHide}
onContextMenu={onMobileContextMenu}
onMaskClick={props.onMaskClick}
ref={contextMenuRef}
/>
);
const displayUI = (isMobileOnly && props.isAudio) || panelVisible;
const isNotFirstElement = props.playlistPos !== 0;
const isNotLastElement = props.playlistPos < props.playlist.length - 1;
return (
<StyledViewerContainer visible={props.visible}>
{!isFullscreen && !isMobileOnly && displayUI && (
<div>
<div className="details">
<Text
isBold
fontSize="14px"
className="title"
title={undefined}
tag={undefined}
as={undefined}
fontWeight={undefined}
color={undefined}
textAlign={undefined}
>
{props.title}
</Text>
<ControlBtn
onClick={props.onMaskClick}
className="mediaPlayerClose"
>
<IconButton
color={"#fff"}
iconName={ViewerMediaCloseSvgUrl}
size={28}
isClickable
/>
</ControlBtn>
</div>
</div>
)}
{props.playlist.length > 1 && !isFullscreen && displayUI && (
<>
{isNotFirstElement && <PrevButton prevClick={prevClick} />}
{isNotLastElement && <NextButton nextClick={nextClick} />}
</>
)}
{props.isImage
? ReactDOM.createPortal(
<StyledViewer
{...props}
displayUI={displayUI}
mobileDetails={mobileDetails}
setIsOpenContextMenu={setIsOpenContextMenu}
container={containerRef.current}
imageTimer={imageTimer}
onMaskClick={props.onMaskClick}
setPanelVisible={setPanelVisible}
generateContextMenu={props.generateContextMenu}
/>,
containerRef.current
)
: (props.isVideo || props.isAudio) &&
ReactDOM.createPortal(
<ViewerPlayer
{...props}
onNextClick={nextClick}
onPrevClick={prevClick}
isAudio={props.isAudio}
audioIcon={props.audioIcon}
contextModel={props.contextModel}
mobileDetails={mobileDetails}
displayUI={displayUI}
isOpenContextMenu={isOpenContextMenu}
onTouch={onTouch}
title={props.title}
setIsPlay={setIsPlay}
setIsOpenContextMenu={setIsOpenContextMenu}
isPlay={isPlay}
onMaskClick={props.onMaskClick}
setPanelVisible={setPanelVisible}
generateContextMenu={props.generateContextMenu}
setIsFullScreen={setIsFullScreen}
setIsError={setIsError}
videoRef={videoElementRef}
video={props.playlist[props.playlistPos]}
activeIndex={props.playlistPos}
/>,
containerRef.current
)}
</StyledViewerContainer>
);
}
export default Viewer;

View File

@ -1,7 +1,7 @@
import React, { useMemo, memo, useCallback } from "react";
import equal from "fast-deep-equal/react";
import { Viewer } from "@docspace/components/viewer";
import Viewer from "../Viewer";
import { isSeparator } from "../../helpers";
import { getCustomToolbar } from "../../helpers/getCustomToolbar";
import { ContextMenuModel } from "../../types";

View File

@ -1,24 +0,0 @@
import React from "react";
export default function Duration({ className, seconds }) {
return (
<time dateTime={`P${Math.round(seconds)}S`} className={className}>
{format(seconds)}
</time>
);
}
function format(seconds) {
const date = new Date(seconds * 1000);
const hh = date.getUTCHours();
const mm = date.getUTCMinutes();
const ss = pad(date.getUTCSeconds());
if (hh) {
return `${hh}:${pad(mm)}:${ss}`;
}
return `${mm}:${ss}`;
}
function pad(string) {
return ("0" + string).slice(-2);
}

View File

@ -1,415 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import styled from "styled-components";
import MediaZoomInIcon from "PUBLIC_DIR/images/media.zoomin.react.svg";
import MediaZoomOutIcon from "PUBLIC_DIR/images/media.zoomout.react.svg";
import MediaRotateLeftIcon from "PUBLIC_DIR/images/media.rotateleft.react.svg";
import MediaRotateRightIcon from "PUBLIC_DIR/images/media.rotateright.react.svg";
import MediaDeleteIcon from "PUBLIC_DIR/images/media.delete.react.svg";
import MediaDownloadIcon from "PUBLIC_DIR/images/download.react.svg";
import commonIconsStyles from "@docspace/components/utils/common-icons-style";
import MediaFavoriteIcon from "PUBLIC_DIR/images/favorite.react.svg";
import ViewerSeparator from "PUBLIC_DIR/images/viewer.separator.react.svg";
import MediaShare from "PUBLIC_DIR/images/share.react.svg";
import DropDownItem from "@docspace/components/drop-down-item";
import DropDown from "@docspace/components/drop-down";
import equal from "fast-deep-equal/react";
import { Base } from "@docspace/components/themes";
import { Viewer } from "@docspace/components/viewer";
const StyledViewer = styled(Viewer)`
.react-viewer-footer {
bottom: 5px !important;
z-index: 301 !important;
overflow: visible;
}
.react-viewer-canvas {
z-index: 300 !important;
margin-top: 50px;
}
.react-viewer-navbar,
.react-viewer-mask,
.react-viewer-attribute,
.react-viewer-close {
display: none;
}
.react-viewer-toolbar {
position: relative;
overflow: visible;
bottom: 4px;
}
.react-viewer-toolbar li {
width: 40px;
height: 30px;
margin-top: 4px;
border-radius: 2px;
cursor: pointer;
line-height: 24px;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.react-viewer-btn {
background-color: transparent;
&:hover {
background-color: ${(props) =>
props.theme.mediaViewer.imageViewer.backgroundColor};
}
}
.react-viewer-image-transition {
transition-duration: 0s;
}
li[data-key="prev"] {
left: 20px;
}
li[data-key="next"] {
right: 20px;
}
li[data-key="prev"],
li[data-key="next"] {
position: fixed;
top: calc(50% - 20px);
height: auto;
background: none;
&:hover {
background: none;
}
}
li[data-key="delete"],
li[data-key="customDownload"] {
position: fixed;
@media (max-width: 600px) {
position: initial;
}
bottom: 9px;
.controlBtn {
margin: 0;
}
}
li[data-key="delete"] {
right: 62px;
}
li[data-key="customDownload"] {
right: 12px;
}
.iconContainer {
width: 24px;
height: 24px;
line-height: 20px;
margin: 3px auto;
&.reset {
width: 18px;
}
path,
rect {
fill: ${(props) => props.theme.mediaViewer.imageViewer.fill};
}
}
.btnContainer {
display: block;
width: 16px;
height: 16px;
margin: 4px 12px;
line-height: 19px;
path,
rect {
fill: ${(props) => props.theme.mediaViewer.imageViewer.fill};
}
}
.scrollBtn {
cursor: ${(props) => (props.inactive ? "default" : "pointer")};
opacity: ${(props) => (props.inactive ? "0.2" : "1")};
&:hover {
background-color: ${(props) =>
!props.inactive
? props.theme.mediaViewer.imageViewer.backgroundColor
: props.theme.mediaViewer.imageViewer.inactiveBackgroundColor};
}
}
`;
const StyledDropDown = styled(DropDown)`
background: #333;
`;
const StyledDropDownItem = styled(DropDownItem)`
color: #fff;
.drop-down-item_icon svg {
path {
fill: #fff !important;
}
}
/* .is-separator {
height: 1px;
background: #474747;
} */
&:hover {
background: #444;
}
`;
StyledViewer.defaultProps = { theme: Base };
class ImageViewer extends React.Component {
// componentDidUpdate() {
// document.getElementsByClassName("iconContainer reset").length > 0 &&
// document.getElementsByClassName("iconContainer reset")[0].click();
// }
shouldComponentUpdate(nextProps) {
return !equal(this.props, nextProps);
}
render() {
const {
className,
visible,
images,
inactive,
onClose,
userAccess,
title,
errorTitle,
onPrevClick,
onNextClick,
playlist,
playlistPos,
isImage,
isAudio,
isVideo,
isPreviewFile,
archiveRoom,
contextModel,
audioIcon,
headerIcon,
onSetSelectionFile,
onDownloadClick,
} = this.props;
const generateContextMenu = (isOpen, right, bottom) => {
const model = contextModel();
const onItemClick = (e, item) => {
const { action, onClick } = item;
return onClick({ originalEvent: e, action: action, item });
};
return (
<StyledDropDown
open={isOpen}
isDefaultMode={false}
directionY="top"
directionX="right"
fixedDirection={true}
withBackdrop={false}
manualY={(bottom || "63") + "px"}
manualX={(right || "-31") + "px"}
>
{model.map((item) => {
if (item.disabled) return;
const onClick = (e) => {
onClose();
onItemClick(e, item);
};
return (
<StyledDropDownItem
className={`${item.isSeparator ? "is-separator" : ""}`}
key={item.key}
label={item.label}
icon={item.icon ? item.icon : ""}
action={item.action}
onClick={onClick}
/>
);
})}
</StyledDropDown>
);
};
var customToolbar = [
{
key: "zoomOut",
percent: true,
actionType: 2,
render: (
<div className="iconContainer zoomOut">
<MediaZoomOutIcon size="scale" />
</div>
),
},
{
key: "percent",
actionType: 999,
},
{
key: "zoomIn",
actionType: 1,
render: (
<div className="iconContainer zoomIn">
<MediaZoomInIcon size="scale" />
</div>
),
},
{
key: "rotateLeft",
actionType: 5,
render: (
<div className="iconContainer rotateLeft">
<MediaRotateLeftIcon size="scale" />
</div>
),
},
{
key: "rotateRight",
actionType: 6,
render: (
<div className="iconContainer rotateRight">
<MediaRotateRightIcon size="scale" />
</div>
),
},
{
key: "separator download-separator",
actionType: -1,
noHover: true,
render: (
<div className="separator" style={{ height: "16px" }}>
<ViewerSeparator size="scale" />
</div>
),
},
// {
// key: "share",
// actionType: 101,
// render: (
// <div className="iconContainer share" style={{ height: "16px" }}>
// <MediaShare size="scale" />
// </div>
// ),
// },
{
key: "download",
actionType: 102,
render: (
<div className="iconContainer download" style={{ height: "16px" }}>
<MediaDownloadIcon size="scale" />
</div>
),
},
{
key: "context-separator",
actionType: -1,
noHover: true,
render: (
<div className="separator" style={{ height: "16px" }}>
<ViewerSeparator size="scale" />
</div>
),
},
{
key: "context-menu",
actionType: -1,
},
{
key: "delete",
actionType: 103,
render: (
<div className="iconContainer viewer-delete">
<MediaDeleteIcon size="scale" />
</div>
),
},
{
key: "favorite",
actionType: 104,
render: (
<div className="iconContainer viewer-favorite">
<MediaFavoriteIcon size="scale" />
</div>
),
},
];
customToolbar.forEach((button) => {
switch (button.key) {
case "prev":
button.onClick = this.props.onPrevClick;
break;
case "next":
button.onClick = this.props.onNextClick;
break;
case "delete":
button.onClick = this.props.onDeleteClick;
break;
case "download":
button.onClick = onDownloadClick;
break;
default:
break;
}
});
const canShare = playlist[playlistPos].canShare;
const toolbars =
!canShare && userAccess
? customToolbar.filter(
(x) => x.key !== "share" && x.key !== "share-separator"
)
: customToolbar.filter((x) => x.key !== "delete");
return (
<div className={className}>
<Viewer
inactive={inactive}
visible={visible}
zoomSpeed={0.25}
title={title}
errorTitle={errorTitle}
contextModel={contextModel}
generateContextMenu={generateContextMenu}
isImage={isImage}
headerIcon={headerIcon}
isAudio={isAudio}
isVideo={isVideo}
isPreviewFile={isPreviewFile}
archiveRoom={archiveRoom}
audioIcon={audioIcon}
onSetSelectionFile={onSetSelectionFile}
onMaskClick={onClose}
onNextClick={onNextClick}
onPrevClick={onPrevClick}
onDownloadClick={onDownloadClick}
playlist={playlist}
playlistPos={playlistPos}
customToolbar={() => toolbars}
images={images}
/>
</div>
);
}
}
ImageViewer.propTypes = {
className: PropTypes.string,
visible: PropTypes.bool,
inactive: PropTypes.bool,
images: PropTypes.arrayOf(PropTypes.object),
onNextClick: PropTypes.func,
onPrevClick: PropTypes.func,
onDeleteClick: PropTypes.func,
onDownloadClick: PropTypes.func,
onClose: PropTypes.func,
};
export default ImageViewer;

View File

@ -1,138 +0,0 @@
import React from "react";
import styled from "styled-components";
import PropTypes from "prop-types";
import { Base } from "@docspace/components/themes";
const StyledProgress = styled.div`
display: inline-block;
.slider-container {
display: inline-block;
border-radius: 2px;
position: relative;
width: ${(props) => props.width}px;
height: 6px;
background: ${(props) =>
props.theme.mediaViewer.progressBar.backgroundColor};
margin: 15px 0;
vertical-align: middle;
}
.fill {
cursor: pointer;
width: ${(props) => 100 * props.value}%;
position: absolute;
top: calc(50% - 3px);
height: 6px;
background: ${(props) => props.theme.mediaViewer.progressBar.background};
border-radius: 2px;
}
input[type="range"] {
display: block;
overflow: visible;
background: transparent;
width: ${(props) => props.width}px;
height: 6px;
outline: none;
margin: 0;
-webkit-appearance: none;
position: relative;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
position: relative;
appearance: none;
box-sizing: content-box;
width: 12px;
height: 12px;
margin-top: -3px;
background: white;
border-radius: 50%;
cursor: pointer;
}
input[type="range"]::-moz-range-thumb {
position: relative;
appearance: none;
box-sizing: content-box;
width: 12px;
height: 12px;
background: white;
border-radius: 50%;
margin-top: -3px;
cursor: pointer;
}
input[type="range"]::-ms-thumb {
position: relative;
appearance: none;
box-sizing: content-box;
width: 12px;
height: 12px;
background: white;
border-radius: 50%;
margin-top: -3px;
cursor: pointer;
}
input[type="range"]::-webkit-slider-runnable-track {
margin: 12px 0;
height: 6px;
border-radius: 2px;
cursor: pointer;
-webkit-appearance: none;
text-align: right;
pointer-events: none;
}
input[type="range"]::-moz-range-track {
margin: 12px 0;
height: 6px;
border-radius: 2px;
cursor: pointer;
-webkit-appearance: none;
text-align: right;
pointer-events: none;
}
input[type="range"]::-ms-track {
border-color: transparent;
color: transparent;
margin: 12px 0;
height: 6px;
border-radius: 2px;
cursor: pointer;
-webkit-appearance: none;
text-align: right;
pointer-events: none;
}
`;
StyledProgress.defaultProps = { theme: Base };
const Progress = (props) => {
return (
<StyledProgress {...props}>
<div className="slider-container">
<div className="fill"></div>
<input
type="range"
min={0}
max={0.999999}
step="any"
value={props.value}
onMouseDown={props.handleSeekMouseDown}
onChange={(event) => props.handleSeekChange(event)}
onMouseUp={props.handleSeekMouseUp}
/>
</div>
</StyledProgress>
);
};
Progress.propTypes = {
value: PropTypes.number,
handleSeekMouseDown: PropTypes.func,
handleSeekChange: PropTypes.func,
handleSeekMouseUp: PropTypes.func,
};
export default Progress;

View File

@ -1,579 +0,0 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import styled, { css } from "styled-components";
import { findDOMNode } from "react-dom";
import screenfull from "screenfull";
import ReactPlayer from "react-player";
import Duration from "./duration";
import Progress from "./progress";
import MediaPauseIcon from "PUBLIC_DIR/images/media.pause.react.svg";
import MediaPlayIcon from "PUBLIC_DIR/images/media.play.react.svg";
import MediaFullScreenIcon from "PUBLIC_DIR/images/media.fullscreen.video.react.svg";
import MediaMuteIcon from "PUBLIC_DIR/images/media.mute.react.svg";
import MediaMuteOffIcon from "PUBLIC_DIR/images/media.muteoff.react.svg";
import commonIconsStyles from "@docspace/components/utils/common-icons-style";
import { Base } from "@docspace/components/themes";
const iconsStyles = css`
path,
stroke,
rect {
fill: ${(props) => props.theme.mediaViewer.videoViewer.fill};
}
`;
const controlsHeight = 40;
const StyledControls = styled.div`
height: ${(props) => props.height}px;
display: block;
position: fixed;
z-index: 301;
${(props) =>
!props.isVideo &&
`background-color: ${props.theme.mediaViewer.videoViewer.backgroundColor};`}
top: calc(50% + ${(props) => props.top}px);
left: ${(props) => props.left}px;
`;
StyledControls.defaultProps = { theme: Base };
const StyledVideoControlBtn = styled.div`
display: inline-block;
height: 26px;
line-height: 30px;
margin: 5px 2px;
width: 38px;
border-radius: 2px;
cursor: pointer;
text-align: center;
vertical-align: top;
&:hover {
background-color: ${(props) =>
props.theme.mediaViewer.videoViewer.background};
}
.playBtnContainer {
width: 16px;
height: 16px;
line-height: 0;
margin: 5px auto;
}
.pauseBtnContainer {
display: block;
width: 16px;
height: 16px;
margin: 3px 10px;
line-height: 19px;
}
.muteBtnContainer {
display: block;
width: 16px;
height: 16px;
margin: 3px 11px;
line-height: 19px;
}
.fullscreenBtnContainer {
display: block;
width: 16px;
height: 16px;
margin: 3px 11px;
line-height: 19px;
}
`;
StyledVideoControlBtn.defaultProps = { theme: Base };
const StyledMediaPauseIcon = styled(MediaPauseIcon)`
${commonIconsStyles}
${iconsStyles}
`;
StyledMediaPauseIcon.defaultProps = { theme: Base };
const StyledMediaPlayIcon = styled(MediaPlayIcon)`
${commonIconsStyles}
${iconsStyles}
`;
StyledMediaPlayIcon.defaultProps = { theme: Base };
const StyledMediaFullScreenIcon = styled(MediaFullScreenIcon)`
${commonIconsStyles}
${iconsStyles}
`;
StyledMediaFullScreenIcon.defaultProps = { theme: Base };
const StyledMediaMuteIcon = styled(MediaMuteIcon)`
${commonIconsStyles}
path:first-child {
stroke: ${(props) => props.theme.mediaViewer.videoViewer.stroke};
}
path:last-child {
fill: ${(props) => props.theme.mediaViewer.videoViewer.fill};
}
`;
const StyledMediaMuteOffIcon = styled(MediaMuteOffIcon)`
${commonIconsStyles}
path, rect {
fill: ${(props) => props.theme.mediaViewer.videoViewer.fill};
}
`;
StyledMediaMuteIcon.defaultProps = { theme: Base };
const VideoControlBtn = (props) => {
return (
<StyledVideoControlBtn {...props}>{props.children}</StyledVideoControlBtn>
);
};
VideoControlBtn.propTypes = {
children: PropTypes.any,
};
const Controls = (props) => {
return <StyledControls {...props}>{props.children}</StyledControls>;
};
Controls.propTypes = {
children: PropTypes.any,
};
const PlayBtn = (props) => {
return (
<VideoControlBtn onClick={props.onClick}>
{props.playing ? (
<div className="pauseBtnContainer">
<StyledMediaPauseIcon size="scale" />
</div>
) : (
<div className="playBtnContainer">
<StyledMediaPlayIcon size="scale" />
</div>
)}
</VideoControlBtn>
);
};
PlayBtn.propTypes = {
playing: PropTypes.bool,
onClick: PropTypes.func,
};
const FullScreenBtn = (props) => {
return (
<VideoControlBtn onClick={props.onClick}>
<div className="fullscreenBtnContainer">
<StyledMediaFullScreenIcon size="scale" />
</div>
</VideoControlBtn>
);
};
FullScreenBtn.propTypes = {
onClick: PropTypes.func,
};
const StyledValumeContainer = styled.div`
display: inline-block;
vertical-align: top;
line-height: 39px;
position: relative;
.muteConteiner {
display: none;
position: absolute;
width: 40px;
height: 80px;
border-radius: 5px;
top: -76px;
left: 5px;
background: black;
}
&:hover {
.muteConteiner {
display: inline-block;
}
}
.mute {
display: inline-block;
transform: rotate(-90deg);
margin: 22px -14px;
}
`;
const StyledDuration = styled.div`
display: inline-block;
height: 26px;
line-height: 26px;
margin: 5px;
width: 60px;
text-align: center;
vertical-align: top;
border-radius: 2px;
cursor: pointer;
&:hover {
background-color: ${(props) =>
props.theme.mediaViewer.videoViewer.background};
}
`;
StyledValumeContainer.defaultProps = { theme: Base };
const StyledVideoViewer = styled.div`
color: ${(props) => props.theme.mediaViewer.videoViewer.color};
.playerWrapper {
display: ${(props) => (props.isVideo ? "block" : "none")};
width: ${(props) => props.width}px;
height: ${(props) => props.height}px;
left: ${(props) => props.left}px;
top: calc(50% - ${(props) => props.top / 2}px);
z-index: 301;
position: fixed;
padding-bottom: 40px;
background-color: ${(props) =>
props.theme.mediaViewer.videoViewer.backgroundColor};
video {
z-index: 300;
}
}
`;
StyledVideoViewer.defaultProps = { theme: Base };
const ErrorContainer = styled.div`
z-index: 301;
display: block;
position: fixed;
left: calc(50% - 110px);
top: calc(50% - 40px);
background-color: ${(props) =>
props.theme.mediaViewer.videoViewer.backgroundColorError};
color: ${(props) => props.theme.mediaViewer.videoViewer.colorError};
border-radius: 10px;
padding: 20px;
text-align: center;
`;
ErrorContainer.defaultProps = { theme: Base };
class ValumeBtn extends Component {
constructor(props) {
super(props);
}
render() {
return (
<StyledValumeContainer>
<div className="muteConteiner">
<Progress
className="mute"
width={this.props.width}
value={this.props.volume}
onMouseDown={this.props.onMouseDown}
handleSeekChange={this.props.onChange}
onMouseUp={this.props.handleSeekMouseUp}
/>
</div>
<VideoControlBtn onClick={this.props.onChangeMute}>
<div className="muteBtnContainer">
{this.props.muted ? (
<StyledMediaMuteOffIcon size="scale" />
) : (
<StyledMediaMuteIcon size="scale" />
)}
</div>
</VideoControlBtn>
</StyledValumeContainer>
);
}
}
ValumeBtn.propTypes = {
width: PropTypes.number,
volume: PropTypes.number,
muted: PropTypes.bool,
onMouseDown: PropTypes.func,
onChange: PropTypes.func,
handleSeekMouseUp: PropTypes.func,
onChangeMute: PropTypes.func,
};
class VideoViewer extends Component {
state = {
url: this.props.url,
pip: false,
playing: false,
controls: false,
light: false,
volume: 0.3,
muted: false,
played: 0,
loaded: 0,
duration: 0,
playbackRate: 1.0,
loop: false,
isNew: false,
error: false,
isLoaded: false,
};
componentDidMount() {
document.addEventListener("keydown", this.onKeydown, false);
}
componentWillUnmount() {
document.removeEventListener("keydown", this.onKeydown, false);
}
componentDidUpdate(prevProps) {
if (this.props.url !== prevProps.url) {
this.setState({
url: this.props.url,
isNew: true,
error: false,
});
}
}
onKeydown = (e) => {
if (e.keyCode === 32) this.handlePlayPause();
};
handlePlayPause = () => {
this.setState({ playing: !this.state.playing, isNew: false });
};
handleStop = () => {
this.setState({ url: null, playing: false });
};
handleVolumeChange = (e) => {
this.setState({
volume: parseFloat(e.target.value),
muted: false,
});
};
handleToggleMuted = () => {
this.setState({ muted: !this.state.muted });
};
handlePlay = () => {
this.setState({ playing: true });
};
handleEnablePIP = () => {
this.setState({ pip: true });
};
handleDisablePIP = () => {
this.setState({ pip: false });
};
handlePause = () => {
this.setState({ playing: false });
};
handleSeekMouseDown = (e) => {
this.setState({ seeking: true });
};
handleSeekChange = (e) => {
this.setState({ played: parseFloat(e.target.value) });
};
handleSeekMouseUp = (e) => {
console.log(!isNaN(parseFloat(e.target.value)), parseFloat(e.target.value));
if (!isNaN(parseFloat(e.target.value))) {
this.setState({ seeking: false });
this.player.seekTo(parseFloat(e.target.value));
}
};
handleProgress = (state) => {
if (!this.state.seeking) {
this.setState(state);
}
};
handleEnded = () => {
this.setState({ playing: this.state.loop });
};
handleDuration = (duration) => {
this.setState({ duration });
};
handleClickFullscreen = () => {
screenfull.request(findDOMNode(this.player));
};
ref = (player) => {
this.player = player;
};
resizePlayer = (videoSize, screenSize) => {
var ratio = videoSize.h / videoSize.w;
if (videoSize.h > screenSize.h) {
videoSize.h = screenSize.h;
videoSize.w = videoSize.h / ratio;
}
if (videoSize.w > screenSize.w) {
videoSize.w = screenSize.w;
videoSize.h = videoSize.w * ratio;
}
return {
width: videoSize.w,
height: videoSize.h,
};
};
onError = (e) => {
console.log("onError", e);
this.setState({ error: true });
};
onPlay = () => {
this.setState({ playing: !this.state.isNew, isNew: false, isLoaded: true });
};
render() {
const {
url,
playing,
controls,
light,
volume,
muted,
loop,
played,
loaded,
duration,
playbackRate,
pip,
error,
isLoaded,
} = this.state;
const { errorLabel } = this.props;
const parentOffset = this.props.getOffset() || 0;
var screenSize = {
w: window.innerWidth,
h: window.innerHeight,
};
screenSize.h -= parentOffset + controlsHeight;
let width = screenSize.w;
let height = screenSize.h;
let centerAreaOx = screenSize.w / 2 + document.documentElement.scrollLeft;
let centerAreaOy = screenSize.h / 2 + document.documentElement.scrollTop;
let videoElement = document.getElementsByTagName("video")[0];
if (videoElement) {
width = this.props.isVideo
? videoElement.videoWidth || 480
: screenSize.w - 150;
height = this.props.isVideo ? videoElement.videoHeight || 270 : 0;
let resize = this.resizePlayer(
{
w: width,
h: height,
},
screenSize
);
width = resize.width;
height = resize.height;
}
let left = this.props.isVideo
? centerAreaOx - width / 2
: centerAreaOx - width / 2;
const videoControlBtnWidth = 220;
const audioControlBtnWidth = 170;
let progressWidth = this.props.isVideo
? width - videoControlBtnWidth
: width - audioControlBtnWidth;
if (error) {
return (
<ErrorContainer>
<p>{errorLabel}</p>
</ErrorContainer>
);
}
return (
<StyledVideoViewer
isVideo={this.props.isVideo}
width={width}
height={height}
left={left}
top={height + controlsHeight}
>
<div>
<div className="playerWrapper" onClick={this.handlePlayPause}>
<ReactPlayer
ref={this.ref}
className="react-player"
style={{ opacity: isLoaded ? 1 : 0 }}
width="100%"
height="100%"
url={url}
pip={pip}
playing={playing}
playsinline={true}
controls={controls}
light={light}
loop={loop}
playbackRate={playbackRate}
volume={volume}
muted={muted}
onPlay={this.onPlay}
onEnablePIP={this.handleEnablePIP}
onDisablePIP={this.handleDisablePIP}
onPause={this.handlePause}
onEnded={this.handleEnded}
onError={this.onError}
onProgress={this.handleProgress}
onDuration={this.handleDuration}
/>
</div>
<Controls
height={controlsHeight}
left={left}
top={height / 2 - controlsHeight / 2}
isVideo={this.props.isVideo}
>
<PlayBtn onClick={this.handlePlayPause} playing={playing} />
<Progress
value={played}
width={progressWidth}
onMouseDown={this.handleSeekMouseDown}
handleSeekChange={this.handleSeekChange}
onMouseUp={this.handleSeekMouseUp}
onTouchEnd={this.handleSeekMouseUp}
/>
<StyledDuration>
-<Duration seconds={duration * (1 - played)} />
</StyledDuration>
<ValumeBtn
width={64}
muted={muted}
volume={muted ? 0 : volume}
onChangeMute={this.handleToggleMuted}
onChange={this.handleVolumeChange}
/>
{this.props.isVideo && (
<FullScreenBtn onClick={this.handleClickFullscreen} />
)}
</Controls>
</div>
</StyledVideoViewer>
);
}
}
VideoViewer.propTypes = {
isVideo: PropTypes.bool,
url: PropTypes.string,
getOffset: PropTypes.func,
errorLabel: PropTypes.string,
};
export default VideoViewer;

View File

@ -356,9 +356,6 @@ export default function ViewerPlayer(props) {
setPanelVisible,
onTouch,
isOpenContextMenu,
setGlobalTimer,
globalTimer,
videoControls,
setIsOpenContextMenu,
contextModel,
onDownloadClick,
@ -418,10 +415,12 @@ export default function ViewerPlayer(props) {
const inputRef = React.useRef(null);
const volumeRef = React.useRef(null);
const actionRef = React.useRef(null);
const videoControls = React.useRef(null);
const mobileProgressRef = React.useRef(null);
const [state, dispatch] = React.useReducer(reducer, initialState);
const [currentVolume, setCurrentVolume] = React.useState(stateVolume);
const [globalTimer, setGlobalTimer] = React.useState(null);
const speedIcons = [<Icon05x />, <Icon1x />, <Icon15x />, <Icon2x />];
const handlers = useSwipeable({
onSwiping: (e) => {