Merge branch 'release/v1.0.0' of github.com:ONLYOFFICE/DocSpace into release/v1.0.0
This commit is contained in:
commit
38861445e2
@ -33,13 +33,16 @@ x-service: &x-service-base
|
||||
|
||||
services:
|
||||
onlyoffice-elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:${ELK_VERSION}
|
||||
image: onlyoffice/elasticsearch:${ELK_VERSION}
|
||||
container_name: ${ELK_HOST}
|
||||
restart: always
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- bootstrap.memory_lock=true
|
||||
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
|
||||
- "ES_JAVA_OPTS=-Xms4g -Xmx4g -Dlog4j2.formatMsgNoLookups=true"
|
||||
- "indices.fielddata.cache.size=30%"
|
||||
- "indices.memory.index_buffer_size=30%"
|
||||
- "ingest.geoip.downloader.enabled=false"
|
||||
ulimits:
|
||||
memlock:
|
||||
soft: -1
|
||||
|
@ -38,7 +38,6 @@ services:
|
||||
- "ES_JAVA_OPTS=-Xms4g -Xmx4g -Dlog4j2.formatMsgNoLookups=true"
|
||||
- "indices.fielddata.cache.size=30%"
|
||||
- "indices.memory.index_buffer_size=30%"
|
||||
- "ingest.geoip.downloader.enabled=false"
|
||||
ulimits:
|
||||
memlock:
|
||||
soft: -1
|
||||
|
@ -90,7 +90,7 @@ class Program
|
||||
//culture = "ru";
|
||||
//exportPath = @"C:\Git\portals\";
|
||||
//key = "*,HtmlMaster*";
|
||||
key = "*";
|
||||
//key = "*";
|
||||
|
||||
if (format == "json")
|
||||
{
|
||||
|
@ -31,6 +31,7 @@ const InfoPanelHeaderContent = (props) => {
|
||||
getIsRooms,
|
||||
getIsGallery,
|
||||
getIsAccounts,
|
||||
getIsTrash,
|
||||
isRootFolder,
|
||||
// rootFolderType,
|
||||
// selectionParentRoom,
|
||||
@ -39,11 +40,13 @@ const InfoPanelHeaderContent = (props) => {
|
||||
const isRooms = getIsRooms();
|
||||
const isGallery = getIsGallery();
|
||||
const isAccounts = getIsAccounts();
|
||||
const isTrash = getIsTrash();
|
||||
|
||||
const isNoItem = isRootFolder && selection?.isSelectedFolder;
|
||||
const isSeveralItems = selection && Array.isArray(selection);
|
||||
|
||||
const withSubmenu = !isNoItem && !isSeveralItems && !isGallery && !isAccounts;
|
||||
const withSubmenu =
|
||||
!isNoItem && !isSeveralItems && !isGallery && !isAccounts && !isTrash;
|
||||
|
||||
const closeInfoPanel = () => setIsVisible(false);
|
||||
|
||||
@ -144,6 +147,7 @@ export default inject(({ auth, selectedFolderStore }) => {
|
||||
getIsRooms,
|
||||
getIsGallery,
|
||||
getIsAccounts,
|
||||
getIsTrash,
|
||||
//selectionParentRoom,
|
||||
} = auth.infoPanelStore;
|
||||
const {
|
||||
@ -161,6 +165,7 @@ export default inject(({ auth, selectedFolderStore }) => {
|
||||
getIsRooms,
|
||||
getIsGallery,
|
||||
getIsAccounts,
|
||||
getIsTrash,
|
||||
|
||||
isRootFolder,
|
||||
// rootFolderType,
|
||||
|
@ -8,7 +8,7 @@ const getDefaultStyles = ({ $currentColorScheme, hasError, theme }) =>
|
||||
:focus-within {
|
||||
border-color: ${hasError
|
||||
? theme?.textArea.focusErrorBorderColor
|
||||
: $currentColorScheme.main?.accent};
|
||||
: theme.textArea.focusBorderColor};
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -82,3 +82,28 @@ export const findNearestIndex = (
|
||||
export const isSeparator = (arg: ContextMenuModel): arg is SeparatorType => {
|
||||
return arg?.isSeparator !== undefined && arg.isSeparator;
|
||||
};
|
||||
|
||||
export const convertToTwoDigitString = (time: number): string => {
|
||||
return time < 10 ? `0${time}` : time.toString();
|
||||
};
|
||||
|
||||
export const formatTime = (time: number): string => {
|
||||
if (isNullOrUndefined(time) || isNaN(time) || time <= 0) return "00:00";
|
||||
|
||||
let seconds: number = Math.floor(time % 60);
|
||||
let minutes: number = Math.floor(time / 60) % 60;
|
||||
let hours: number = Math.floor(time / 3600);
|
||||
|
||||
const convertedHours = convertToTwoDigitString(hours);
|
||||
const convertedMinutes = convertToTwoDigitString(minutes);
|
||||
const convertedSeconds = convertToTwoDigitString(seconds);
|
||||
|
||||
if (hours === 0) {
|
||||
return `${convertedMinutes}:${convertedSeconds}`;
|
||||
}
|
||||
return `${convertedHours}:${convertedMinutes}:${convertedSeconds}`;
|
||||
};
|
||||
|
||||
export const compareTo = (a: number, b: number) => {
|
||||
return Math.trunc(a) > Math.trunc(b);
|
||||
};
|
||||
|
@ -65,6 +65,12 @@ function MediaViewer({
|
||||
}
|
||||
}, [props.playlist.length]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
props.onClose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const { playlist, files, setBufferSelection } = props;
|
||||
|
||||
@ -331,15 +337,6 @@ function MediaViewer({
|
||||
|
||||
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;
|
||||
|
@ -23,7 +23,7 @@ import {
|
||||
ImperativeHandle,
|
||||
ToolbarItemType,
|
||||
} from "../ImageViewerToolbar/ImageViewerToolbar.props";
|
||||
import { ToolbarActionType, KeyboardEventKeys } from "../../helpers";
|
||||
import { ToolbarActionType, KeyboardEventKeys, compareTo } from "../../helpers";
|
||||
|
||||
const MaxScale = 5;
|
||||
const MinScale = 0.5;
|
||||
@ -190,10 +190,6 @@ function ImageViewer({
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
const compareTo = (a: number, b: number) => {
|
||||
return Math.trunc(a) > Math.trunc(b);
|
||||
};
|
||||
|
||||
const getSizeByAngle = (
|
||||
width: number,
|
||||
height: number,
|
||||
|
@ -0,0 +1,6 @@
|
||||
interface PlayerBigPlayButtonProps {
|
||||
onClick: VoidFunction;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export default PlayerBigPlayButtonProps;
|
@ -0,0 +1,11 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const WrapperPlayerBigPlayButton = styled.div`
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export default WrapperPlayerBigPlayButton;
|
@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import BigIconPlay from "PUBLIC_DIR/images/media.bgplay.react.svg";
|
||||
import WrapperPlayerBigPlayButton from "./PlayerBigPlayButton.styled";
|
||||
import PlayerBigPlayButtonProps from "./PlayerBigPlayButton.props";
|
||||
|
||||
function PlayerBigPlayButton({ visible, onClick }: PlayerBigPlayButtonProps) {
|
||||
if (!visible) return <></>;
|
||||
|
||||
return (
|
||||
<WrapperPlayerBigPlayButton>
|
||||
<BigIconPlay onClick={onClick} />
|
||||
</WrapperPlayerBigPlayButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlayerBigPlayButton;
|
@ -0,0 +1,12 @@
|
||||
interface PlayerDesktopContextMenuProps {
|
||||
generateContextMenu: (
|
||||
isOpen: boolean,
|
||||
right?: string,
|
||||
bottom?: string
|
||||
) => JSX.Element;
|
||||
isPreviewFile: boolean;
|
||||
hideContextMenu: boolean;
|
||||
onDownloadClick: VoidFunction;
|
||||
}
|
||||
|
||||
export default PlayerDesktopContextMenuProps;
|
@ -0,0 +1,40 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
export const PlayerDesktopContextMenuWrapper = styled.div`
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 48px;
|
||||
height: 48px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& > svg {
|
||||
padding-left: 19px;
|
||||
padding-bottom: 3px;
|
||||
width: 18px;
|
||||
height: 20px;
|
||||
|
||||
path {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
rect {
|
||||
stroke: #fff;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DownloadIconWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 48px;
|
||||
height: 48px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
@ -0,0 +1,61 @@
|
||||
import React, { memo, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
DownloadIconWrapper,
|
||||
PlayerDesktopContextMenuWrapper,
|
||||
} from "./PlayerDesktopContextMenu.styled";
|
||||
import PlayerDesktopContextMenuProps from "./PlayerDesktopContextMenu.props";
|
||||
|
||||
import MediaContextMenu from "PUBLIC_DIR/images/vertical-dots.react.svg";
|
||||
import DownloadReactSvgUrl from "PUBLIC_DIR/images/download.react.svg";
|
||||
|
||||
const ContextRight = "9";
|
||||
const ContextBottom = "48";
|
||||
|
||||
function PlayerDesktopContextMenu({
|
||||
isPreviewFile,
|
||||
hideContextMenu,
|
||||
onDownloadClick,
|
||||
generateContextMenu,
|
||||
}: PlayerDesktopContextMenuProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isOpenContext, setIsOpenContext] = useState<boolean>(false);
|
||||
|
||||
const context = useMemo(
|
||||
() => generateContextMenu(isOpenContext, ContextRight, ContextBottom),
|
||||
[generateContextMenu, isOpenContext]
|
||||
);
|
||||
|
||||
const toggleContext = () => setIsOpenContext((pre) => !pre);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (event: MouseEvent | TouchEvent) => {
|
||||
if (!ref.current || ref.current.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
setIsOpenContext(false);
|
||||
};
|
||||
document.addEventListener("mousedown", listener);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", listener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (hideContextMenu) {
|
||||
return (
|
||||
<DownloadIconWrapper onClick={onDownloadClick}>
|
||||
<DownloadReactSvgUrl />
|
||||
</DownloadIconWrapper>
|
||||
);
|
||||
}
|
||||
if (isPreviewFile) return <></>;
|
||||
|
||||
return (
|
||||
<PlayerDesktopContextMenuWrapper ref={ref} onClick={toggleContext}>
|
||||
<MediaContextMenu />
|
||||
{context}
|
||||
</PlayerDesktopContextMenuWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(PlayerDesktopContextMenu);
|
@ -0,0 +1,34 @@
|
||||
import { mobile } from "@docspace/components/utils/device";
|
||||
import React, { forwardRef, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import { formatTime } from "../../helpers";
|
||||
|
||||
const PlayerDurationWrapper = styled.div`
|
||||
width: 102px;
|
||||
color: #fff;
|
||||
user-select: none;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
|
||||
margin-left: 10px;
|
||||
|
||||
@media ${mobile} {
|
||||
margin-left: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
type PlayerDurationProps = {
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
};
|
||||
|
||||
function PlayerDuration({ currentTime, duration }: PlayerDurationProps) {
|
||||
return (
|
||||
<PlayerDurationWrapper>
|
||||
<time>{formatTime(currentTime)}</time> /{" "}
|
||||
<time>{formatTime(duration)}</time>
|
||||
</PlayerDurationWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlayerDuration;
|
@ -0,0 +1,7 @@
|
||||
interface PlayerFullSceenProps {
|
||||
isAudio: boolean;
|
||||
isFullScreen: boolean;
|
||||
onClick: VoidFunction;
|
||||
}
|
||||
|
||||
export default PlayerFullSceenProps;
|
@ -0,0 +1,15 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
export const PlayerFullSceenWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 48px;
|
||||
height: 48px;
|
||||
|
||||
padding-left: 10px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
@ -0,0 +1,23 @@
|
||||
import React, { memo } from "react";
|
||||
import PlayerFullSceenProps from "./PlayerFullScreen.props";
|
||||
|
||||
import { PlayerFullSceenWrapper } from "./PlayerFullScreen.styled";
|
||||
|
||||
import IconFullScreen from "PUBLIC_DIR/images/videoplayer.full.react.svg";
|
||||
import IconExitFullScreen from "PUBLIC_DIR/images/videoplayer.exit.react.svg";
|
||||
|
||||
function PlayerFullScreen({
|
||||
isAudio,
|
||||
onClick,
|
||||
isFullScreen,
|
||||
}: PlayerFullSceenProps) {
|
||||
if (isAudio) return <></>;
|
||||
|
||||
return (
|
||||
<PlayerFullSceenWrapper onClick={onClick}>
|
||||
{isFullScreen ? <IconExitFullScreen /> : <IconFullScreen />}
|
||||
</PlayerFullSceenWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(PlayerFullScreen);
|
@ -0,0 +1,8 @@
|
||||
import { ContextMenuModel } from "./../../types/index";
|
||||
interface PlayerMessageErrorProps {
|
||||
errorTitle: string;
|
||||
model: ContextMenuModel[];
|
||||
onMaskClick: VoidFunction;
|
||||
}
|
||||
|
||||
export default PlayerMessageErrorProps;
|
@ -0,0 +1,65 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
export const StyledMediaError = styled.div`
|
||||
position: fixed;
|
||||
z-index: 1006;
|
||||
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
width: 267px;
|
||||
height: 56px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
|
||||
opacity: 1;
|
||||
border-radius: 20px;
|
||||
`;
|
||||
|
||||
export const StyledErrorToolbar = styled.div`
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
z-index: 1006;
|
||||
|
||||
transform: translateX(-50%);
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
padding: 10px 24px;
|
||||
border-radius: 18px;
|
||||
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
svg {
|
||||
path {
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
|
||||
rect: {
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
.toolbar-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
`;
|
@ -0,0 +1,54 @@
|
||||
import React from "react";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { ReactSVG } from "react-svg";
|
||||
|
||||
import Text from "@docspace/components/text";
|
||||
import PlayerMessageErrorProps from "./PlayerMessageError.props";
|
||||
import {
|
||||
StyledErrorToolbar,
|
||||
StyledMediaError,
|
||||
} from "./PlayerMessageError.styled";
|
||||
import { isSeparator } from "../../helpers";
|
||||
|
||||
function PlayerMessageError({
|
||||
errorTitle,
|
||||
model,
|
||||
onMaskClick,
|
||||
}: PlayerMessageErrorProps) {
|
||||
const items = !isMobile
|
||||
? model.filter((el) => el.key !== "rename")
|
||||
: model.filter((el) => el.key === "delete" || el.key === "download");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StyledMediaError>
|
||||
{/*@ts-ignore*/}
|
||||
<Text
|
||||
fontSize="15px"
|
||||
color={"#fff"}
|
||||
textAlign="center"
|
||||
className="title"
|
||||
>
|
||||
{errorTitle}
|
||||
</Text>
|
||||
</StyledMediaError>
|
||||
<StyledErrorToolbar>
|
||||
{items.map((item) => {
|
||||
if (item.disabled || isSeparator(item)) return;
|
||||
|
||||
const onClick = () => {
|
||||
onMaskClick();
|
||||
item.onClick();
|
||||
};
|
||||
return (
|
||||
<div className="toolbar-item" key={item.key} onClick={onClick}>
|
||||
<ReactSVG src={item.icon} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</StyledErrorToolbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlayerMessageError;
|
@ -0,0 +1,35 @@
|
||||
import React, { memo } from "react";
|
||||
|
||||
import IconPlay from "PUBLIC_DIR/images/videoplayer.play.react.svg";
|
||||
import IconStop from "PUBLIC_DIR/images/videoplayer.stop.react.svg";
|
||||
import styled from "styled-components";
|
||||
|
||||
type PlayerPlayButtonProps = {
|
||||
isPlaying: boolean;
|
||||
onClick: VoidFunction;
|
||||
};
|
||||
|
||||
const WrapperPlayerPlayButton = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
margin-left: -10px;
|
||||
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
function PlayerPlayButton({ isPlaying, onClick }: PlayerPlayButtonProps) {
|
||||
const onTouchStart = (event: React.TouchEvent<HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
return (
|
||||
<WrapperPlayerPlayButton onClick={onClick} onTouchStart={onTouchStart}>
|
||||
{isPlaying ? <IconStop /> : <IconPlay />}
|
||||
</WrapperPlayerPlayButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(PlayerPlayButton);
|
@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
|
||||
import Icon05x from "PUBLIC_DIR/images/media.viewer05x.react.svg";
|
||||
import Icon1x from "PUBLIC_DIR/images/media.viewer1x.react.svg";
|
||||
import Icon15x from "PUBLIC_DIR/images/media.viewer15x.react.svg";
|
||||
import Icon2x from "PUBLIC_DIR/images/media.viewer2x.react.svg";
|
||||
|
||||
import { SpeedRecord, SpeedType } from "./PlayerSpeedControl.props";
|
||||
|
||||
export enum SpeedIndex {
|
||||
Speed_X05 = 0,
|
||||
Speed_X10 = 1,
|
||||
Speed_X15 = 2,
|
||||
Speed_X20 = 3,
|
||||
}
|
||||
|
||||
export const speedIcons = [<Icon05x />, <Icon1x />, <Icon15x />, <Icon2x />];
|
||||
|
||||
export const speeds: SpeedType = ["X0.5", "X1", "X1.5", "X2"];
|
||||
|
||||
export const speedRecord: SpeedRecord<SpeedType> = {
|
||||
"X0.5": 0.5,
|
||||
X1: 1,
|
||||
"X1.5": 1.5,
|
||||
X2: 2,
|
||||
};
|
||||
|
||||
export const DefaultIndexSpeed = SpeedIndex.Speed_X10;
|
||||
export const MillisecondShowSpeedToast = 2000;
|
||||
|
||||
/**
|
||||
*The function returns the following index based on the logic from the layout
|
||||
*https://www.figma.com/file/T49yt13Eiu7nzvj4ymfssV/DocSpace-1.0.0?node-id=34536-418523&t=Yv2Rp3stGISIQNcm-0
|
||||
*/
|
||||
export const getNextIndexSpeed = (currentSpeedIndex: number) => {
|
||||
switch (currentSpeedIndex) {
|
||||
case SpeedIndex.Speed_X10:
|
||||
return SpeedIndex.Speed_X05;
|
||||
case SpeedIndex.Speed_X05:
|
||||
return SpeedIndex.Speed_X15;
|
||||
case SpeedIndex.Speed_X15:
|
||||
return SpeedIndex.Speed_X20;
|
||||
case SpeedIndex.Speed_X20:
|
||||
return SpeedIndex.Speed_X10;
|
||||
default:
|
||||
return DefaultIndexSpeed;
|
||||
}
|
||||
};
|
@ -0,0 +1,11 @@
|
||||
export interface PlayerSpeedControlProps {
|
||||
handleSpeedChange: (speed: number) => void;
|
||||
onMouseLeave: VoidFunction;
|
||||
src?: string;
|
||||
}
|
||||
|
||||
export type SpeedType = ["X0.5", "X1", "X1.5", "X2"];
|
||||
|
||||
export type SpeedRecord<T extends SpeedType> = {
|
||||
[Key in T[number]]: number;
|
||||
};
|
@ -0,0 +1,89 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
export const SpeedControlWrapper = styled.div`
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
svg {
|
||||
path {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
rect {
|
||||
stroke: #fff;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DropDown = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
height: 120px;
|
||||
width: 48px;
|
||||
|
||||
padding: 4px 0px;
|
||||
|
||||
position: absolute;
|
||||
bottom: 48px;
|
||||
z-index: 50;
|
||||
|
||||
color: #fff;
|
||||
background: #333;
|
||||
text-align: center;
|
||||
border-radius: 7px 7px 0px 0px;
|
||||
`;
|
||||
|
||||
export const DropDownItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 30px;
|
||||
width: 48px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background: #222;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ToastSpeed = styled.div`
|
||||
position: fixed;
|
||||
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
width: 72px;
|
||||
height: 56px;
|
||||
|
||||
border-radius: 9px;
|
||||
visibility: visible;
|
||||
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: rgba(51, 51, 51, 0.65);
|
||||
|
||||
svg {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
path {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
rect {
|
||||
stroke: #fff;
|
||||
}
|
||||
`;
|
@ -0,0 +1,108 @@
|
||||
import React, { memo, useEffect, useRef, useState } from "react";
|
||||
import { isMobileOnly } from "react-device-detect";
|
||||
|
||||
import {
|
||||
DropDown,
|
||||
DropDownItem,
|
||||
SpeedControlWrapper,
|
||||
ToastSpeed,
|
||||
} from "./PlayerSpeedControl.styled";
|
||||
|
||||
import { PlayerSpeedControlProps } from "./PlayerSpeedControl.props";
|
||||
|
||||
import {
|
||||
DefaultIndexSpeed,
|
||||
getNextIndexSpeed,
|
||||
MillisecondShowSpeedToast,
|
||||
speedIcons,
|
||||
speedRecord,
|
||||
speeds,
|
||||
} from "./PlayerSpeedControl.helper";
|
||||
|
||||
function PlayerSpeedControl({
|
||||
handleSpeedChange,
|
||||
onMouseLeave,
|
||||
src,
|
||||
}: PlayerSpeedControlProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const timerRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
const [currentIndexSpeed, setCurrentIndexSpeed] = useState<number>(
|
||||
DefaultIndexSpeed
|
||||
);
|
||||
const [isOpenSpeedContextMenu, setIsOpenSpeedContextMenu] = useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [speedToastVisible, setSpeedToastVisible] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentIndexSpeed(DefaultIndexSpeed);
|
||||
}, [src]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (event: MouseEvent | TouchEvent) => {
|
||||
if (!ref.current || ref.current.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsOpenSpeedContextMenu(false);
|
||||
};
|
||||
document.addEventListener("mousedown", listener);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", listener);
|
||||
clearTimeout(timerRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const toggle = () => {
|
||||
if (isMobileOnly) {
|
||||
const nextIndexSpeed = getNextIndexSpeed(currentIndexSpeed);
|
||||
|
||||
setCurrentIndexSpeed(nextIndexSpeed);
|
||||
|
||||
const newSpeed = speedRecord[speeds[nextIndexSpeed]];
|
||||
|
||||
handleSpeedChange(newSpeed);
|
||||
|
||||
setSpeedToastVisible(true);
|
||||
clearTimeout(timerRef.current);
|
||||
|
||||
timerRef.current = setTimeout(() => {
|
||||
setSpeedToastVisible(false);
|
||||
}, MillisecondShowSpeedToast);
|
||||
} else {
|
||||
setIsOpenSpeedContextMenu((prev) => !prev);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{speedToastVisible && (
|
||||
<ToastSpeed>{speedIcons[currentIndexSpeed]}</ToastSpeed>
|
||||
)}
|
||||
<SpeedControlWrapper ref={ref} onClick={toggle}>
|
||||
{speedIcons[currentIndexSpeed]}
|
||||
|
||||
{isOpenSpeedContextMenu && (
|
||||
<DropDown onMouseLeave={onMouseLeave}>
|
||||
{speeds.map((speed, index) => (
|
||||
<DropDownItem
|
||||
key={speed}
|
||||
onClick={() => {
|
||||
setCurrentIndexSpeed(index);
|
||||
handleSpeedChange(speedRecord[speed]);
|
||||
onMouseLeave();
|
||||
}}
|
||||
>
|
||||
{speed}
|
||||
</DropDownItem>
|
||||
))}
|
||||
</DropDown>
|
||||
)}
|
||||
</SpeedControlWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(PlayerSpeedControl);
|
@ -0,0 +1,9 @@
|
||||
interface PlayerTimelineProps {
|
||||
value: number;
|
||||
duration: number;
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onMouseEnter: VoidFunction;
|
||||
onMouseLeave: VoidFunction;
|
||||
}
|
||||
|
||||
export default PlayerTimelineProps;
|
@ -0,0 +1,181 @@
|
||||
import { isMobile } from "react-device-detect";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
export const HoverProgress = styled.div`
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
|
||||
height: 6px;
|
||||
|
||||
border-radius: 5px;
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
`;
|
||||
|
||||
const mobileCss = css`
|
||||
margin-top: 16px;
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
background: #fff;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
background: #fff;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
input[type="range"]::-ms-fill-upper {
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
background: #fff;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
export const PlayerTimelineWrapper = styled.div`
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
margin-top: 92px;
|
||||
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
time {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: -25px;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
/* height: 6px; */
|
||||
input {
|
||||
height: 6px;
|
||||
}
|
||||
${HoverProgress} {
|
||||
display: block;
|
||||
}
|
||||
transition: 0.1s height ease-in;
|
||||
}
|
||||
|
||||
&:hover time {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
|
||||
outline: none;
|
||||
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
|
||||
border-radius: 5px;
|
||||
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
background-image: linear-gradient(#fff, #fff);
|
||||
background-repeat: no-repeat;
|
||||
|
||||
z-index: 1;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
input[type="range"]::-ms-fill-upper {
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
visibility: visible;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
opacity: 1 !important;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
visibility: visible;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
opacity: 1 !important;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
input[type="range"]::-ms-fill-upper {
|
||||
visibility: visible;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
opacity: 1 !important;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
${isMobile && mobileCss}
|
||||
`;
|
@ -0,0 +1,117 @@
|
||||
import React, { useRef } from "react";
|
||||
import { formatTime } from "../../helpers";
|
||||
import PlayerTimelineProps from "./PlayerTimeline.props";
|
||||
import { HoverProgress, PlayerTimelineWrapper } from "./PlayerTimeline.styled";
|
||||
|
||||
function PlayerTimeline({
|
||||
value,
|
||||
duration,
|
||||
onChange,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
}: PlayerTimelineProps) {
|
||||
const timelineTooltipRef = useRef<HTMLTimeElement>(null);
|
||||
const timelineRef = useRef<HTMLDivElement>(null);
|
||||
const hoverProgressRef = useRef<HTMLDivElement>(null);
|
||||
const setTimeoutTimelineTooltipRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
const showTimelineTooltip = () => {
|
||||
if (!timelineTooltipRef.current) return;
|
||||
|
||||
const callback = () => {
|
||||
if (timelineTooltipRef.current) {
|
||||
timelineTooltipRef.current.style.removeProperty("display");
|
||||
setTimeoutTimelineTooltipRef.current = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
if (setTimeoutTimelineTooltipRef.current) {
|
||||
clearTimeout(setTimeoutTimelineTooltipRef.current);
|
||||
setTimeoutTimelineTooltipRef.current = setTimeout(callback, 500);
|
||||
} else {
|
||||
timelineTooltipRef.current.style.display = "block";
|
||||
setTimeoutTimelineTooltipRef.current = setTimeout(callback, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!timelineTooltipRef.current || !timelineRef.current) return;
|
||||
|
||||
const { clientWidth } = timelineRef.current;
|
||||
|
||||
const percent = Number(event.target.value) / 100;
|
||||
|
||||
const offsetX = clientWidth * percent;
|
||||
|
||||
const time = Math.floor(percent * duration);
|
||||
|
||||
const left =
|
||||
offsetX < 20
|
||||
? 20
|
||||
: offsetX > clientWidth - 20
|
||||
? clientWidth - 20
|
||||
: offsetX;
|
||||
|
||||
timelineTooltipRef.current.style.left = `${left}px`;
|
||||
timelineTooltipRef.current.innerText = formatTime(time);
|
||||
|
||||
showTimelineTooltip();
|
||||
|
||||
onChange(event);
|
||||
};
|
||||
|
||||
const handleMouseMove = (
|
||||
event: React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
) => {
|
||||
if (
|
||||
!timelineTooltipRef.current ||
|
||||
!timelineRef.current ||
|
||||
!hoverProgressRef.current
|
||||
)
|
||||
return;
|
||||
|
||||
const { clientWidth } = timelineRef.current;
|
||||
const { max, min } = Math;
|
||||
|
||||
const offsetX = min(max(event.nativeEvent.offsetX, 0), clientWidth);
|
||||
|
||||
const percent = Math.floor((offsetX / clientWidth) * duration);
|
||||
|
||||
hoverProgressRef.current.style.width = `${offsetX}px`;
|
||||
|
||||
const left =
|
||||
offsetX < 20
|
||||
? 20
|
||||
: offsetX > clientWidth - 20
|
||||
? clientWidth - 20
|
||||
: offsetX;
|
||||
|
||||
timelineTooltipRef.current.style.left = `${left}px`;
|
||||
timelineTooltipRef.current.innerText = formatTime(percent);
|
||||
};
|
||||
|
||||
return (
|
||||
<PlayerTimelineWrapper
|
||||
ref={timelineRef}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<time ref={timelineTooltipRef}>00:00</time>
|
||||
<HoverProgress ref={hoverProgressRef} />
|
||||
<input
|
||||
min="0"
|
||||
max="100"
|
||||
step="any"
|
||||
type="range"
|
||||
value={value}
|
||||
onChange={handleOnChange}
|
||||
style={{
|
||||
backgroundSize: `${value}% 100%`,
|
||||
}}
|
||||
/>
|
||||
</PlayerTimelineWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlayerTimeline;
|
@ -0,0 +1,147 @@
|
||||
import { tablet } from "@docspace/components/utils/device";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
export const PlayerVolumeControlWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
`;
|
||||
|
||||
export const IconWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const mobilecss = css`
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
background: #fff;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
background: #fff;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="range"]::-ms-fill-upper {
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
background: #fff;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
export const VolumeWrapper = styled.div`
|
||||
width: 123px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 9px;
|
||||
|
||||
input {
|
||||
margin-right: 15px;
|
||||
width: 80%;
|
||||
height: 4px;
|
||||
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
|
||||
border-radius: 5px;
|
||||
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
background-image: linear-gradient(#fff, #fff);
|
||||
background-repeat: no-repeat;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media ${tablet} {
|
||||
width: 63%;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
input[type="range"]::-ms-fill-upper {
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
visibility: visible;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
opacity: 1 !important;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
visibility: visible;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
opacity: 1 !important;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
input[type="range"]::-ms-fill-upper {
|
||||
visibility: visible;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
opacity: 1 !important;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
${isMobile && mobilecss}
|
||||
`;
|
@ -0,0 +1,53 @@
|
||||
import React, { memo } from "react";
|
||||
|
||||
import {
|
||||
IconWrapper,
|
||||
PlayerVolumeControlWrapper,
|
||||
VolumeWrapper,
|
||||
} from "./PlayerVolumeControl.styled";
|
||||
|
||||
import IconVolumeMax from "PUBLIC_DIR/images/media.volumemax.react.svg";
|
||||
import IconVolumeMuted from "PUBLIC_DIR/images/media.volumeoff.react.svg";
|
||||
import IconVolumeMin from "PUBLIC_DIR/images/media.volumemin.react.svg";
|
||||
|
||||
type PlayerVolumeControlProps = {
|
||||
volume: number;
|
||||
isMuted: boolean;
|
||||
toggleVolumeMute: VoidFunction;
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
};
|
||||
|
||||
function PlayerVolumeControl({
|
||||
volume,
|
||||
isMuted,
|
||||
onChange,
|
||||
toggleVolumeMute,
|
||||
}: PlayerVolumeControlProps) {
|
||||
return (
|
||||
<PlayerVolumeControlWrapper>
|
||||
<IconWrapper onClick={toggleVolumeMute}>
|
||||
{isMuted ? (
|
||||
<IconVolumeMuted />
|
||||
) : volume >= 50 ? (
|
||||
<IconVolumeMax />
|
||||
) : (
|
||||
<IconVolumeMin />
|
||||
)}
|
||||
</IconWrapper>
|
||||
<VolumeWrapper>
|
||||
<input
|
||||
style={{
|
||||
backgroundSize: `${volume}% 100%`,
|
||||
}}
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={volume}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</VolumeWrapper>
|
||||
</PlayerVolumeControlWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(PlayerVolumeControl);
|
@ -1,5 +1,5 @@
|
||||
import ReactDOM from "react-dom";
|
||||
import { isMobileOnly, isMobile } from "react-device-detect";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import React, { useRef, useState, useEffect, useCallback } from "react";
|
||||
|
||||
import ContextMenu from "@docspace/components/context-menu";
|
||||
@ -11,7 +11,7 @@ import PrevButton from "../PrevButton";
|
||||
import ImageViewer from "../ImageViewer";
|
||||
import MobileDetails from "../MobileDetails";
|
||||
import DesktopDetails from "../DesktopDetails";
|
||||
import ViewerPlayer from "../ViewerPlayer/viewer-player";
|
||||
import ViewerPlayer from "../ViewerPlayer";
|
||||
|
||||
import type ViewerProps from "./Viewer.props";
|
||||
|
||||
@ -22,15 +22,15 @@ function Viewer(props: ViewerProps) {
|
||||
|
||||
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 panelVisibleRef = useRef<boolean>(false);
|
||||
const panelToolbarRef = useRef<boolean>(false);
|
||||
|
||||
const contextMenuRef = useRef<ContextMenu>(null);
|
||||
const videoElementRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
const [isFullscreen, setIsFullScreen] = useState<boolean>(false);
|
||||
useEffect(() => {
|
||||
@ -43,27 +43,10 @@ function Viewer(props: ViewerProps) {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if ((!isPlay || isOpenContextMenu) && (!props.isImage || isOpenContextMenu))
|
||||
return clearTimeout(timerIDRef.current);
|
||||
}, [isPlay, isOpenContextMenu, props.isImage]);
|
||||
|
||||
const resetToolbarVisibleTimer = () => {
|
||||
if (panelVisibleRef.current) {
|
||||
if (isOpenContextMenu) {
|
||||
clearTimeout(timerIDRef.current);
|
||||
timerIDRef.current = setTimeout(() => {
|
||||
panelVisibleRef.current = false;
|
||||
setPanelVisible(false);
|
||||
}, 2500);
|
||||
} else {
|
||||
setPanelVisible(true);
|
||||
panelVisibleRef.current = true;
|
||||
|
||||
timerIDRef.current = setTimeout(() => {
|
||||
panelVisibleRef.current = false;
|
||||
setPanelVisible(false);
|
||||
}, 2500);
|
||||
}
|
||||
};
|
||||
}, [isOpenContextMenu]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobile) return;
|
||||
@ -79,20 +62,44 @@ function Viewer(props: ViewerProps) {
|
||||
};
|
||||
}, [setImageTimer, setPanelVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("touchstart", onTouch);
|
||||
const resetToolbarVisibleTimer = () => {
|
||||
if (panelToolbarRef.current) return;
|
||||
|
||||
return () => document.removeEventListener("touchstart", onTouch);
|
||||
}, [setPanelVisible]);
|
||||
if (panelVisibleRef.current && panelVisible) {
|
||||
clearTimeout(timerIDRef.current);
|
||||
timerIDRef.current = setTimeout(() => {
|
||||
panelVisibleRef.current = false;
|
||||
setPanelVisible(false);
|
||||
}, 2500);
|
||||
} else {
|
||||
setPanelVisible(true);
|
||||
clearTimeout(timerIDRef.current);
|
||||
panelVisibleRef.current = true;
|
||||
|
||||
const onTouch = useCallback(
|
||||
(e: TouchEvent, canTouch?: boolean) => {
|
||||
if (e.target === videoElementRef.current || canTouch) {
|
||||
setPanelVisible((visible) => !visible);
|
||||
}
|
||||
},
|
||||
[setPanelVisible]
|
||||
);
|
||||
timerIDRef.current = setTimeout(() => {
|
||||
panelVisibleRef.current = false;
|
||||
setPanelVisible(false);
|
||||
}, 2500);
|
||||
}
|
||||
};
|
||||
|
||||
const removeToolbarVisibleTimer = () => {
|
||||
clearTimeout(timerIDRef.current);
|
||||
panelVisibleRef.current = false;
|
||||
panelToolbarRef.current = true;
|
||||
};
|
||||
|
||||
const removePanelVisibleTimeout = () => {
|
||||
clearTimeout(timerIDRef.current);
|
||||
panelVisibleRef.current = true;
|
||||
panelToolbarRef.current = false;
|
||||
setPanelVisible(true);
|
||||
};
|
||||
|
||||
const restartToolbarVisibleTimer = () => {
|
||||
panelToolbarRef.current = false;
|
||||
resetToolbarVisibleTimer();
|
||||
};
|
||||
|
||||
const nextClick = () => {
|
||||
clearTimeout(imageTimer);
|
||||
@ -117,6 +124,22 @@ function Viewer(props: ViewerProps) {
|
||||
setIsOpenContextMenu(false);
|
||||
}, [setIsOpenContextMenu]);
|
||||
|
||||
const handleMaskClick = () => {
|
||||
if (isFullscreen) {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document["webkitExitFullscreen"]) {
|
||||
document["webkitExitFullscreen"]();
|
||||
} else if (document["mozCancelFullScreen"]) {
|
||||
document["mozCancelFullScreen"]();
|
||||
} else if (document["msExitFullscreen"]) {
|
||||
document["msExitFullscreen"]();
|
||||
}
|
||||
}
|
||||
|
||||
props.onMaskClick();
|
||||
};
|
||||
|
||||
const mobileDetails = (
|
||||
<MobileDetails
|
||||
onHide={onHide}
|
||||
@ -124,22 +147,20 @@ function Viewer(props: ViewerProps) {
|
||||
title={props.title}
|
||||
ref={contextMenuRef}
|
||||
icon={props.headerIcon}
|
||||
onMaskClick={props.onMaskClick}
|
||||
onMaskClick={handleMaskClick}
|
||||
contextModel={props.contextModel}
|
||||
onContextMenu={onMobileContextMenu}
|
||||
isPreviewFile={props.isPreviewFile}
|
||||
/>
|
||||
);
|
||||
|
||||
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 && !isMobile && panelVisible && (
|
||||
<DesktopDetails title={props.title} onMaskClick={props.onMaskClick} />
|
||||
<DesktopDetails title={props.title} onMaskClick={handleMaskClick} />
|
||||
)}
|
||||
|
||||
{props.playlist.length > 1 && !isFullscreen && !isMobile && (
|
||||
@ -170,28 +191,31 @@ function Viewer(props: ViewerProps) {
|
||||
: (props.isVideo || props.isAudio) &&
|
||||
ReactDOM.createPortal(
|
||||
<ViewerPlayer
|
||||
{...props}
|
||||
onNextClick={nextClick}
|
||||
onPrevClick={prevClick}
|
||||
isError={isError}
|
||||
src={props.fileUrl}
|
||||
isAudio={props.isAudio}
|
||||
isVideo={props.isVideo}
|
||||
panelVisible={panelVisible}
|
||||
audioIcon={props.audioIcon}
|
||||
contextModel={props.contextModel}
|
||||
isFullScreen={isFullscreen}
|
||||
errorTitle={props.errorTitle}
|
||||
mobileDetails={mobileDetails}
|
||||
displayUI={displayUI}
|
||||
isLastImage={!isNotLastElement}
|
||||
isFistImage={!isNotFirstElement}
|
||||
isPreviewFile={props.isPreviewFile}
|
||||
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}
|
||||
onMask={handleMaskClick}
|
||||
onPrev={props.onPrevClick}
|
||||
onNext={props.onNextClick}
|
||||
setPanelVisible={setPanelVisible}
|
||||
setIsFullScreen={setIsFullScreen}
|
||||
contextModel={props.contextModel}
|
||||
onDownloadClick={props.onDownloadClick}
|
||||
generateContextMenu={props.generateContextMenu}
|
||||
removeToolbarVisibleTimer={removeToolbarVisibleTimer}
|
||||
removePanelVisibleTimeout={removePanelVisibleTimeout}
|
||||
restartToolbarVisibleTimer={restartToolbarVisibleTimer}
|
||||
/>,
|
||||
containerRef.current
|
||||
)}
|
||||
|
@ -11,6 +11,8 @@ const StyledLoaderWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
`;
|
||||
|
||||
const StyledLoader = styled.div`
|
||||
|
@ -0,0 +1,37 @@
|
||||
import { ContextMenuModel } from "../../types";
|
||||
|
||||
interface ViewerPlayerProps {
|
||||
src?: string;
|
||||
isAudio: boolean;
|
||||
isVideo: boolean;
|
||||
isError: boolean;
|
||||
audioIcon: string;
|
||||
errorTitle: string;
|
||||
isLastImage: boolean;
|
||||
isFistImage: boolean;
|
||||
isFullScreen: boolean;
|
||||
panelVisible: boolean;
|
||||
isPreviewFile: boolean;
|
||||
isOpenContextMenu: boolean;
|
||||
mobileDetails: JSX.Element;
|
||||
|
||||
onMask: VoidFunction;
|
||||
onPrev: VoidFunction;
|
||||
onNext: VoidFunction;
|
||||
onDownloadClick: VoidFunction;
|
||||
contextModel: () => ContextMenuModel[];
|
||||
removeToolbarVisibleTimer: VoidFunction;
|
||||
removePanelVisibleTimeout: VoidFunction;
|
||||
restartToolbarVisibleTimer: VoidFunction;
|
||||
setIsError: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setPanelVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setIsFullScreen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
|
||||
generateContextMenu: (
|
||||
isOpen: boolean,
|
||||
right?: string,
|
||||
bottom?: string
|
||||
) => JSX.Element;
|
||||
}
|
||||
|
||||
export default ViewerPlayerProps;
|
@ -0,0 +1,91 @@
|
||||
import { isMobile, isMobileOnly } from "react-device-detect";
|
||||
import styled, { css } from "styled-components";
|
||||
import { animated } from "@react-spring/web";
|
||||
|
||||
export const ContainerPlayer = styled.div<{ $isFullScreen: boolean }>`
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 305;
|
||||
background-color: ${(props) =>
|
||||
props.$isFullScreen ? "#000" : "rgba(55, 55, 55, 0.6)"};
|
||||
touch-action: none;
|
||||
`;
|
||||
|
||||
export const VideoWrapper = styled(animated.div)<{ $visible: boolean }>`
|
||||
inset: 0;
|
||||
visibility: ${(props) => (props.$visible ? "visible" : "hidden")};
|
||||
opacity: ${(props) => (props.$visible ? 1 : 0)};
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
touch-action: none;
|
||||
|
||||
.audio-container {
|
||||
width: 190px;
|
||||
height: 190px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledMobilePlayerControls = css`
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
height: 80px;
|
||||
`;
|
||||
|
||||
export const StyledPlayerControls = styled.div<{ $isShow: boolean }>`
|
||||
position: fixed;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
z-index: 307;
|
||||
display: flex;
|
||||
|
||||
width: 100%;
|
||||
height: 188px;
|
||||
|
||||
visibility: ${(props) => (props.$isShow ? "visible" : "hidden")};
|
||||
opacity: ${(props) => (props.$isShow ? "1" : "0")};
|
||||
|
||||
background: linear-gradient(
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0.64) 48.44%,
|
||||
rgba(0, 0, 0, 0.89) 100%
|
||||
);
|
||||
|
||||
${isMobile && StyledMobilePlayerControls}
|
||||
`;
|
||||
|
||||
export const ControlContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
margin-top: 30px;
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
${isMobile &&
|
||||
css`
|
||||
margin-top: 8px;
|
||||
.player_right-control {
|
||||
margin-right: -8px;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export const PlayerControlsWrapper = styled.div`
|
||||
padding: 0 30px;
|
||||
width: 100%;
|
||||
|
||||
${isMobileOnly &&
|
||||
css`
|
||||
padding: 0 15px;
|
||||
`}
|
||||
`;
|
@ -0,0 +1,624 @@
|
||||
import lodash from "lodash";
|
||||
import { useGesture } from "@use-gesture/react";
|
||||
import { useSpring, animated } from "@react-spring/web";
|
||||
import { isMobile, isDesktop } from "react-device-detect";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import ViewerPlayerProps from "./ViewerPlayer.props";
|
||||
import {
|
||||
ContainerPlayer,
|
||||
ControlContainer,
|
||||
PlayerControlsWrapper,
|
||||
StyledPlayerControls,
|
||||
VideoWrapper,
|
||||
} from "./ViewerPlayer.styled";
|
||||
|
||||
import PlayerBigPlayButton from "../PlayerBigPlayButton";
|
||||
import ViewerLoader from "../ViewerLoader";
|
||||
import PlayerPlayButton from "../PlayerPlayButton";
|
||||
import PlayerDuration from "../PlayerDuration/inxed";
|
||||
import PlayerVolumeControl from "../PlayerVolumeControl";
|
||||
import PlayerTimeline from "../PlayerTimeline";
|
||||
import PlayerSpeedControl from "../PlayerSpeedControl";
|
||||
import PlayerFullScreen from "../PlayerFullScreen";
|
||||
import PlayerDesktopContextMenu from "../PlayerDesktopContextMenu";
|
||||
import { compareTo, KeyboardEventKeys } from "../../helpers";
|
||||
import PlayerMessageError from "../PlayerMessageError";
|
||||
|
||||
const VolumeLocalStorageKey = "player-volume";
|
||||
const defaultVolume = 100;
|
||||
const audioWidth = 190;
|
||||
const audioHeight = 190;
|
||||
|
||||
function ViewerPlayer({
|
||||
src,
|
||||
isAudio,
|
||||
isVideo,
|
||||
isError,
|
||||
audioIcon,
|
||||
errorTitle,
|
||||
isLastImage,
|
||||
isFistImage,
|
||||
isFullScreen,
|
||||
panelVisible,
|
||||
mobileDetails,
|
||||
isPreviewFile,
|
||||
isOpenContextMenu,
|
||||
onMask,
|
||||
onNext,
|
||||
onPrev,
|
||||
setIsError,
|
||||
contextModel,
|
||||
setPanelVisible,
|
||||
setIsFullScreen,
|
||||
onDownloadClick,
|
||||
generateContextMenu,
|
||||
removeToolbarVisibleTimer,
|
||||
removePanelVisibleTimeout,
|
||||
restartToolbarVisibleTimer,
|
||||
}: ViewerPlayerProps) {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const playerWrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||
const [isWaiting, setIsWaiting] = useState<boolean>(false);
|
||||
|
||||
const [isMuted, setIsMuted] = useState<boolean>(() => {
|
||||
const valueStorage = localStorage.getItem(VolumeLocalStorageKey);
|
||||
|
||||
if (!valueStorage) return false;
|
||||
|
||||
return valueStorage === "0";
|
||||
});
|
||||
|
||||
const [timeline, setTimeline] = useState<number>(0);
|
||||
const [duration, setDuration] = useState<number>(0);
|
||||
const [currentTime, setCurrentTime] = useState<number>(0);
|
||||
const [volume, setVolume] = useState<number>(() => {
|
||||
const valueStorage = localStorage.getItem(VolumeLocalStorageKey);
|
||||
|
||||
if (!valueStorage) return defaultVolume;
|
||||
|
||||
return JSON.parse(valueStorage);
|
||||
});
|
||||
|
||||
const [style, api] = useSpring(() => ({
|
||||
width: 0,
|
||||
height: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, [isFullScreen, isLoading]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setIsLoading(true);
|
||||
resetState();
|
||||
}, [src]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpenContextMenu && isPlaying) {
|
||||
restartToolbarVisibleTimer();
|
||||
}
|
||||
}, [isOpenContextMenu]);
|
||||
useEffect(() => {
|
||||
window.addEventListener("fullscreenchange", onExitFullScreen, {
|
||||
capture: true,
|
||||
});
|
||||
return () =>
|
||||
window.removeEventListener("fullscreenchange", onExitFullScreen, {
|
||||
capture: true,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [isPlaying]);
|
||||
|
||||
const calculateAdjustImage = (point: { x: number; y: number }) => {
|
||||
if (!playerWrapperRef.current || !containerRef.current) return point;
|
||||
|
||||
let playerBounds = playerWrapperRef.current.getBoundingClientRect();
|
||||
const containerBounds = containerRef.current.getBoundingClientRect();
|
||||
|
||||
const originalWidth = playerWrapperRef.current.clientWidth;
|
||||
const widthOverhang = (playerBounds.width - originalWidth) / 2;
|
||||
|
||||
const originalHeight = playerWrapperRef.current.clientHeight;
|
||||
const heightOverhang = (playerBounds.height - originalHeight) / 2;
|
||||
|
||||
const isWidthOutContainer = playerBounds.width >= containerBounds.width;
|
||||
|
||||
const isHeightOutContainer = playerBounds.height >= containerBounds.height;
|
||||
|
||||
if (
|
||||
compareTo(playerBounds.left, containerBounds.left) &&
|
||||
isWidthOutContainer
|
||||
) {
|
||||
point.x = widthOverhang;
|
||||
} else if (
|
||||
compareTo(containerBounds.right, playerBounds.right) &&
|
||||
isWidthOutContainer
|
||||
) {
|
||||
point.x = containerBounds.width - playerBounds.width + widthOverhang;
|
||||
} else if (!isWidthOutContainer) {
|
||||
point.x =
|
||||
(containerBounds.width - playerBounds.width) / 2 + widthOverhang;
|
||||
}
|
||||
|
||||
if (
|
||||
compareTo(playerBounds.top, containerBounds.top) &&
|
||||
isHeightOutContainer
|
||||
) {
|
||||
point.y = heightOverhang;
|
||||
} else if (
|
||||
compareTo(containerBounds.bottom, playerBounds.bottom) &&
|
||||
isHeightOutContainer
|
||||
) {
|
||||
point.y = containerBounds.height - playerBounds.height + heightOverhang;
|
||||
} else if (!isHeightOutContainer) {
|
||||
point.y =
|
||||
(containerBounds.height - playerBounds.height) / 2 + heightOverhang;
|
||||
}
|
||||
|
||||
return point;
|
||||
};
|
||||
|
||||
useGesture(
|
||||
{
|
||||
onDrag: ({ offset: [dx, dy], movement: [mdx, mdy], memo, first }) => {
|
||||
if (isDesktop) return;
|
||||
|
||||
if (first) {
|
||||
memo = style.y.get();
|
||||
}
|
||||
|
||||
api.start({
|
||||
x:
|
||||
(isFistImage && mdx > 0) || (isLastImage && mdx < 0) || isFullScreen
|
||||
? style.x.get()
|
||||
: dx,
|
||||
y: dy >= memo ? dy : style.y.get(),
|
||||
opacity: mdy > 0 ? Math.max(1 - mdy / 120, 0) : style.opacity.get(),
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
return memo;
|
||||
},
|
||||
onDragEnd: ({ movement: [mdx, mdy] }) => {
|
||||
if (isDesktop) return;
|
||||
|
||||
if (!isFullScreen) {
|
||||
if (mdx < -style.width.get() / 4) {
|
||||
return onNext();
|
||||
} else if (mdx > style.width.get() / 4) {
|
||||
return onPrev();
|
||||
}
|
||||
}
|
||||
if (mdy > 120) {
|
||||
return onMask();
|
||||
}
|
||||
|
||||
const newPoint = calculateAdjustImage({
|
||||
x: style.x.get(),
|
||||
y: style.y.get(),
|
||||
});
|
||||
|
||||
api.start({
|
||||
...newPoint,
|
||||
opacity: 1,
|
||||
});
|
||||
},
|
||||
onClick: ({ dragging, event }) => {
|
||||
if (isDesktop && event.target === containerRef.current) return onMask();
|
||||
|
||||
if (
|
||||
dragging ||
|
||||
!isMobile ||
|
||||
isAudio ||
|
||||
event.target !== containerRef.current
|
||||
)
|
||||
return;
|
||||
|
||||
if (panelVisible) {
|
||||
removeToolbarVisibleTimer();
|
||||
setPanelVisible(false);
|
||||
} else {
|
||||
isPlaying && restartToolbarVisibleTimer();
|
||||
setPanelVisible(true);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
drag: {
|
||||
from: () => [style.x.get(), style.y.get()],
|
||||
axis: "lock",
|
||||
},
|
||||
target: containerRef,
|
||||
}
|
||||
);
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.code === KeyboardEventKeys.Space) {
|
||||
togglePlay();
|
||||
}
|
||||
};
|
||||
const onExitFullScreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
setIsFullScreen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
setTimeline(0);
|
||||
setDuration(0);
|
||||
setCurrentTime(0);
|
||||
setIsPlaying(false);
|
||||
removePanelVisibleTimeout();
|
||||
};
|
||||
|
||||
const getVideoWidthHeight = (video: HTMLVideoElement): [number, number] => {
|
||||
const maxWidth = window.innerWidth;
|
||||
const maxHeight = window.innerHeight;
|
||||
|
||||
const elementWidth = isAudio ? audioWidth : video.videoWidth;
|
||||
const elementHeight = isAudio ? audioHeight : video.videoHeight;
|
||||
|
||||
let width =
|
||||
elementWidth > maxWidth
|
||||
? maxWidth
|
||||
: isFullScreen
|
||||
? Math.max(maxWidth, elementWidth)
|
||||
: Math.min(maxWidth, elementWidth);
|
||||
|
||||
let height = (width / elementWidth) * elementHeight;
|
||||
|
||||
if (height > maxHeight) {
|
||||
height = maxHeight;
|
||||
width = (height / elementHeight) * elementWidth;
|
||||
}
|
||||
|
||||
return [width, height];
|
||||
};
|
||||
|
||||
const getVideoPosition = (
|
||||
width: number,
|
||||
height: number
|
||||
): [number, number] => {
|
||||
let left = (window.innerWidth - width) / 2;
|
||||
let top = (window.innerHeight - height) / 2;
|
||||
|
||||
return [left, top];
|
||||
};
|
||||
|
||||
const setSizeAndPosition = (target: HTMLVideoElement) => {
|
||||
const [width, height] = getVideoWidthHeight(target);
|
||||
const [x, y] = getVideoPosition(width, height);
|
||||
|
||||
api.start({
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
immediate: true,
|
||||
});
|
||||
};
|
||||
|
||||
const handleResize = (event: any) => {
|
||||
const target = videoRef.current;
|
||||
|
||||
if (!target || isLoading) return;
|
||||
|
||||
setSizeAndPosition(target);
|
||||
};
|
||||
|
||||
const handleLoadedMetaDataVideo = (
|
||||
event: React.SyntheticEvent<HTMLVideoElement, Event>
|
||||
) => {
|
||||
const target = event.target as HTMLVideoElement;
|
||||
|
||||
setSizeAndPosition(target);
|
||||
|
||||
target.volume = volume / 100;
|
||||
target.muted = isMuted;
|
||||
target.playbackRate = 1;
|
||||
|
||||
setDuration(target.duration);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const togglePlay = useCallback(() => {
|
||||
if (!videoRef.current) return;
|
||||
|
||||
if (isMobile && !isPlaying && isVideo) {
|
||||
restartToolbarVisibleTimer();
|
||||
}
|
||||
|
||||
if (isPlaying) {
|
||||
videoRef.current.pause();
|
||||
setIsPlaying(false);
|
||||
setPanelVisible(true);
|
||||
isMobile && removeToolbarVisibleTimer();
|
||||
} else {
|
||||
videoRef.current.play();
|
||||
setIsPlaying(true);
|
||||
}
|
||||
}, [isPlaying, isVideo]);
|
||||
|
||||
const handleBigPlayButtonClick = () => {
|
||||
togglePlay();
|
||||
};
|
||||
|
||||
const handleTimeUpdate = () => {
|
||||
if (!videoRef.current || isLoading) return;
|
||||
|
||||
const { currentTime, duration } = videoRef.current;
|
||||
const percent = (currentTime / duration) * 100;
|
||||
|
||||
setTimeline(percent);
|
||||
|
||||
setCurrentTime(currentTime);
|
||||
};
|
||||
|
||||
const handleChangeTimeLine = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!videoRef.current) return;
|
||||
|
||||
const percent = Number(event.target.value);
|
||||
const newCurrentTime = (percent / 100) * videoRef.current.duration;
|
||||
|
||||
setTimeline(percent);
|
||||
setCurrentTime(newCurrentTime);
|
||||
videoRef.current.currentTime = newCurrentTime;
|
||||
};
|
||||
|
||||
const handleClickVideo = () => {
|
||||
if (isMobile) {
|
||||
if (!isPlaying) {
|
||||
return setPanelVisible((prev) => !prev);
|
||||
}
|
||||
|
||||
if (panelVisible) {
|
||||
videoRef.current?.pause();
|
||||
setIsPlaying(false);
|
||||
return removeToolbarVisibleTimer();
|
||||
}
|
||||
|
||||
return isPlaying && restartToolbarVisibleTimer();
|
||||
}
|
||||
togglePlay();
|
||||
};
|
||||
|
||||
const handleVideoEnded = () => {
|
||||
setIsPlaying(false);
|
||||
if (isMobile) removePanelVisibleTimeout();
|
||||
};
|
||||
|
||||
const handleVolumeChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!videoRef.current) return;
|
||||
|
||||
const newVolume = Number(event.target.value);
|
||||
localStorage.setItem(VolumeLocalStorageKey, event.target.value);
|
||||
|
||||
if (newVolume === 0) {
|
||||
setIsMuted(true);
|
||||
videoRef.current.muted = true;
|
||||
}
|
||||
|
||||
if (isMuted && newVolume > 0) {
|
||||
setIsMuted(false);
|
||||
videoRef.current.muted = false;
|
||||
}
|
||||
|
||||
videoRef.current.volume = newVolume / 100;
|
||||
setVolume(newVolume);
|
||||
},
|
||||
[isMuted]
|
||||
);
|
||||
|
||||
const handleSpeedChange = useCallback((speed: number) => {
|
||||
if (!videoRef.current) return;
|
||||
|
||||
videoRef.current.playbackRate = speed;
|
||||
}, []);
|
||||
|
||||
const toggleVolumeMute = useCallback(() => {
|
||||
if (!videoRef.current) return;
|
||||
|
||||
const volume = videoRef.current.volume * 100 || defaultVolume;
|
||||
|
||||
if (isMuted) {
|
||||
setIsMuted(false);
|
||||
setVolume(volume);
|
||||
|
||||
videoRef.current.volume = volume / 100;
|
||||
videoRef.current.muted = false;
|
||||
|
||||
localStorage.setItem(VolumeLocalStorageKey, volume.toString());
|
||||
} else {
|
||||
setIsMuted(true);
|
||||
setVolume(0);
|
||||
videoRef.current.muted = true;
|
||||
localStorage.setItem(VolumeLocalStorageKey, "0");
|
||||
}
|
||||
}, [isMuted]);
|
||||
|
||||
const toggleVideoFullscreen = useCallback(() => {
|
||||
if (!videoRef.current) return;
|
||||
|
||||
if (isFullScreen) {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document["webkitExitFullscreen"]) {
|
||||
document["webkitExitFullscreen"]();
|
||||
} else if (document["mozCancelFullScreen"]) {
|
||||
document["mozCancelFullScreen"]();
|
||||
} else if (document["msExitFullscreen"]) {
|
||||
document["msExitFullscreen"]();
|
||||
}
|
||||
} else {
|
||||
if (document.documentElement.requestFullscreen) {
|
||||
document.documentElement.requestFullscreen();
|
||||
} else if (document.documentElement["mozRequestFullScreen"]) {
|
||||
document.documentElement["mozRequestFullScreen"]();
|
||||
} else if (document.documentElement["webkitRequestFullScreen"]) {
|
||||
document.documentElement["webkitRequestFullScreen"]();
|
||||
} else if (document.documentElement["webkitEnterFullScreen"]) {
|
||||
document.documentElement["webkitEnterFullScreen"]();
|
||||
}
|
||||
}
|
||||
|
||||
setIsFullScreen((pre) => !pre);
|
||||
}, [isFullScreen]);
|
||||
|
||||
const onMouseEnter = () => {
|
||||
if (isMobile) return;
|
||||
|
||||
removeToolbarVisibleTimer();
|
||||
};
|
||||
const onMouseLeave = () => {
|
||||
if (isMobile) return;
|
||||
|
||||
restartToolbarVisibleTimer();
|
||||
};
|
||||
|
||||
const onTouchStart = () => {
|
||||
if (isPlaying && isVideo) restartToolbarVisibleTimer();
|
||||
};
|
||||
const onTouchMove = () => {
|
||||
if (isPlaying && isVideo) restartToolbarVisibleTimer();
|
||||
};
|
||||
|
||||
const model = useMemo(contextModel, [contextModel]);
|
||||
const hideContextMenu = useMemo(
|
||||
() => model.filter((item) => !item.disabled).length <= 1,
|
||||
[model]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{isMobile && panelVisible && mobileDetails}
|
||||
<ContainerPlayer ref={containerRef} $isFullScreen={isFullScreen}>
|
||||
<VideoWrapper
|
||||
$visible={!isLoading}
|
||||
style={style}
|
||||
ref={playerWrapperRef}
|
||||
>
|
||||
<animated.video
|
||||
style={lodash.omit(style, ["x", "y"])}
|
||||
src={`${src}#t=0.001`}
|
||||
playsInline
|
||||
ref={videoRef}
|
||||
hidden={isAudio}
|
||||
preload="metadata"
|
||||
onClick={handleClickVideo}
|
||||
onEnded={handleVideoEnded}
|
||||
onTimeUpdate={handleTimeUpdate}
|
||||
onPlaying={() => setIsWaiting(false)}
|
||||
onWaiting={() => setIsWaiting(true)}
|
||||
onError={() => {
|
||||
console.error("video error");
|
||||
setIsError(true);
|
||||
}}
|
||||
onLoadedMetadata={handleLoadedMetaDataVideo}
|
||||
/>
|
||||
<PlayerBigPlayButton
|
||||
onClick={handleBigPlayButtonClick}
|
||||
visible={!isPlaying && isVideo && !isError}
|
||||
/>
|
||||
{isAudio && (
|
||||
<div className="audio-container">
|
||||
<img src={audioIcon} />
|
||||
</div>
|
||||
)}
|
||||
</VideoWrapper>
|
||||
|
||||
<ViewerLoader isLoading={isLoading || (isWaiting && isPlaying)} />
|
||||
</ContainerPlayer>
|
||||
{isError ? (
|
||||
<PlayerMessageError
|
||||
model={model}
|
||||
onMaskClick={onMask}
|
||||
errorTitle={errorTitle}
|
||||
/>
|
||||
) : (
|
||||
<StyledPlayerControls
|
||||
$isShow={panelVisible && !isLoading}
|
||||
onTouchStart={onTouchStart}
|
||||
onTouchMove={onTouchMove}
|
||||
>
|
||||
<PlayerControlsWrapper>
|
||||
<PlayerTimeline
|
||||
value={timeline}
|
||||
duration={duration}
|
||||
onChange={handleChangeTimeLine}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
/>
|
||||
<ControlContainer>
|
||||
<div
|
||||
className="player_left-control"
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<PlayerPlayButton isPlaying={isPlaying} onClick={togglePlay} />
|
||||
<PlayerDuration currentTime={currentTime} duration={duration} />
|
||||
{!isMobile && (
|
||||
<PlayerVolumeControl
|
||||
volume={volume}
|
||||
isMuted={isMuted}
|
||||
onChange={handleVolumeChange}
|
||||
toggleVolumeMute={toggleVolumeMute}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="player_right-control"
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<PlayerSpeedControl
|
||||
src={src}
|
||||
onMouseLeave={onMouseLeave}
|
||||
handleSpeedChange={handleSpeedChange}
|
||||
/>
|
||||
<PlayerFullScreen
|
||||
isAudio={isAudio}
|
||||
isFullScreen={isFullScreen}
|
||||
onClick={toggleVideoFullscreen}
|
||||
/>
|
||||
{isDesktop && (
|
||||
<PlayerDesktopContextMenu
|
||||
isPreviewFile={isPreviewFile}
|
||||
hideContextMenu={hideContextMenu}
|
||||
onDownloadClick={onDownloadClick}
|
||||
generateContextMenu={generateContextMenu}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ControlContainer>
|
||||
</PlayerControlsWrapper>
|
||||
</StyledPlayerControls>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ViewerPlayer;
|
@ -1,98 +0,0 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Text from "@docspace/components/text";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { ReactSVG } from "react-svg";
|
||||
|
||||
const StyledMediaError = styled.div`
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
width: ${(props) => props.width + "px"};
|
||||
height: 56px;
|
||||
position: absolute;
|
||||
opacity: 1;
|
||||
z-index: 1006;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 20px;
|
||||
`;
|
||||
|
||||
const StyledErrorToolbar = styled.div`
|
||||
padding: 10px 24px;
|
||||
bottom: 24px;
|
||||
z-index: 1006;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 18px;
|
||||
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.toolbar-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const MediaError = ({
|
||||
width,
|
||||
height,
|
||||
onMaskClick,
|
||||
model,
|
||||
errorTitle,
|
||||
}) => {
|
||||
let errorLeft = (window.innerWidth - width) / 2 + "px";
|
||||
let errorTop = (window.innerHeight - height) / 2 + "px";
|
||||
|
||||
const items = !isMobile
|
||||
? model.filter((el) => el.key !== "rename")
|
||||
: model.filter((el) => el.key === "delete" || el.key === "download");
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledMediaError
|
||||
width={width}
|
||||
height={height}
|
||||
style={{
|
||||
left: `${errorLeft}`,
|
||||
top: `${errorTop}`,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
fontSize="15px"
|
||||
color={"#fff"}
|
||||
textAlign="center"
|
||||
className="title"
|
||||
>
|
||||
{errorTitle}
|
||||
</Text>
|
||||
</StyledMediaError>
|
||||
|
||||
<StyledErrorToolbar>
|
||||
{items.map((item) => {
|
||||
if (item.disabled) return;
|
||||
|
||||
const onClick = () => {
|
||||
onMaskClick();
|
||||
item.onClick && item.onClick();
|
||||
};
|
||||
return (
|
||||
<div className="toolbar-item" key={item.key} onClick={onClick}>
|
||||
<ReactSVG src={item.icon} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</StyledErrorToolbar>
|
||||
</>
|
||||
);
|
||||
};
|
File diff suppressed because it is too large
Load Diff
@ -315,6 +315,11 @@ class InfoPanelStore {
|
||||
const pathname = givenPathName || window.location.pathname.toLowerCase();
|
||||
return pathname.indexOf("form-gallery") !== -1;
|
||||
};
|
||||
|
||||
getIsTrash = (givenPathName) => {
|
||||
const pathname = givenPathName || window.location.pathname.toLowerCase();
|
||||
return pathname.indexOf("files/trash") !== -1;
|
||||
};
|
||||
}
|
||||
|
||||
export default InfoPanelStore;
|
||||
|
@ -99,6 +99,9 @@ const LoginForm: React.FC<ILoginFormProps> = ({
|
||||
message && setErrorText(message);
|
||||
confirmedEmail && setIdentifier(confirmedEmail);
|
||||
|
||||
confirmedEmail &&
|
||||
toastr.success(`${t("MessageEmailConfirmed")} ${t("MessageAuthorize")}`);
|
||||
|
||||
focusInput();
|
||||
|
||||
window.authCallback = authCallback;
|
||||
@ -394,11 +397,6 @@ const LoginForm: React.FC<ILoginFormProps> = ({
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{confirmedEmail && (
|
||||
<Text isBold={true} fontSize="16px">
|
||||
{t("MessageEmailConfirmed")} {t("MessageAuthorize")}
|
||||
</Text>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user