change to popover

This commit is contained in:
PhilReact 2025-06-16 02:13:25 +03:00
parent 793449f486
commit ad2b56d95a
5 changed files with 308 additions and 106 deletions

View File

@ -1,4 +1,16 @@
import { Box, IconButton, Popper, Slider, Typography } from "@mui/material"; import {
alpha,
Box,
ButtonBase,
Divider,
Fade,
IconButton,
Popover,
Popper,
Slider,
Typography,
useTheme,
} from "@mui/material";
export const fontSizeExSmall = "60%"; export const fontSizeExSmall = "60%";
export const fontSizeSmall = "80%"; export const fontSizeSmall = "80%";
import AspectRatioIcon from "@mui/icons-material/AspectRatio"; import AspectRatioIcon from "@mui/icons-material/AspectRatio";
@ -14,11 +26,14 @@ import {
import { formatTime } from "../../utils/time.js"; import { formatTime } from "../../utils/time.js";
import { CustomFontTooltip } from "./CustomFontTooltip.js"; import { CustomFontTooltip } from "./CustomFontTooltip.js";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import SlowMotionVideoIcon from "@mui/icons-material/SlowMotionVideo";
const buttonPaddingBig = "6px"; const buttonPaddingBig = "6px";
const buttonPaddingSmall = "4px"; const buttonPaddingSmall = "4px";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos";
import CheckIcon from "@mui/icons-material/Check";
export const PlayButton = ({togglePlay, isPlaying , isScreenSmall}: any) => { export const PlayButton = ({ togglePlay, isPlaying, isScreenSmall }: any) => {
return ( return (
<CustomFontTooltip title="Pause/Play (Spacebar)" placement="bottom" arrow> <CustomFontTooltip title="Pause/Play (Spacebar)" placement="bottom" arrow>
<IconButton <IconButton
@ -34,7 +49,7 @@ export const PlayButton = ({togglePlay, isPlaying , isScreenSmall}: any) => {
); );
}; };
export const ReloadButton = ({reloadVideo, isScreenSmall}: any) => { export const ReloadButton = ({ reloadVideo, isScreenSmall }: any) => {
return ( return (
<CustomFontTooltip title="Reload Video (R)" placement="bottom" arrow> <CustomFontTooltip title="Reload Video (R)" placement="bottom" arrow>
<IconButton <IconButton
@ -50,12 +65,12 @@ export const ReloadButton = ({reloadVideo, isScreenSmall}: any) => {
); );
}; };
export const ProgressSlider = ({progress, duration, playerRef}: any) => { export const ProgressSlider = ({ progress, duration, playerRef }: any) => {
const sliderRef = useRef(null); const sliderRef = useRef(null);
const [hoverX, setHoverX] = useState<number | null>(null); const [hoverX, setHoverX] = useState<number | null>(null);
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null); const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
const [showDuration, setShowDuration] = useState(0) const [showDuration, setShowDuration] = useState(0);
const onProgressChange = (_: any, value: number | number[]) => { const onProgressChange = (_: any, value: number | number[]) => {
if (!playerRef.current) return; if (!playerRef.current) return;
@ -69,8 +84,6 @@ export const ProgressSlider = ({progress, duration, playerRef}: any) => {
const debounceTimeoutRef = useRef<any>(null); const debounceTimeoutRef = useRef<any>(null);
const previousBlobUrlRef = useRef<string | null>(null); const previousBlobUrlRef = useRef<string | null>(null);
const handleMouseMove = (e: React.MouseEvent) => { const handleMouseMove = (e: React.MouseEvent) => {
const slider = sliderRef.current; const slider = sliderRef.current;
if (!slider) return; if (!slider) return;
@ -79,10 +92,10 @@ export const ProgressSlider = ({progress, duration, playerRef}: any) => {
const x = e.clientX - rect.left; const x = e.clientX - rect.left;
const percent = x / rect.width; const percent = x / rect.width;
const time = Math.min(Math.max(0, percent * duration), duration); const time = Math.min(Math.max(0, percent * duration), duration);
console.log('hello100') console.log("hello100");
setHoverX(e.clientX); setHoverX(e.clientX);
setShowDuration(time) setShowDuration(time);
if (debounceTimeoutRef.current) clearTimeout(debounceTimeoutRef.current); if (debounceTimeoutRef.current) clearTimeout(debounceTimeoutRef.current);
// debounceTimeoutRef.current = setTimeout(() => { // debounceTimeoutRef.current = setTimeout(() => {
@ -112,29 +125,31 @@ export const ProgressSlider = ({progress, duration, playerRef}: any) => {
}, []); }, []);
const hoverAnchorRef = useRef<HTMLDivElement | null>(null); const hoverAnchorRef = useRef<HTMLDivElement | null>(null);
if(hoverX){ if (hoverX) {
console.log('thumbnailUrl', thumbnailUrl, hoverX) console.log("thumbnailUrl", thumbnailUrl, hoverX);
} }
console.log('duration', duration) console.log("duration", duration);
return ( return (
<Box position="relative" sx={{ <Box
width: '100%', position="relative"
padding: '0px 10px' sx={{
}}> width: "100%",
padding: "0px 10px",
}}
>
<Box <Box
ref={hoverAnchorRef} ref={hoverAnchorRef}
sx={{ sx={{
position: 'absolute', position: "absolute",
left: hoverX ?? -9999, left: hoverX ?? -9999,
top: 0, top: 0,
width: '1px', width: "1px",
height: '1px', height: "1px",
pointerEvents: 'none', pointerEvents: "none",
}} }}
/> />
<Slider <Slider
ref={sliderRef} ref={sliderRef}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
@ -145,36 +160,53 @@ console.log('thumbnailUrl', thumbnailUrl, hoverX)
max={duration || 100} max={duration || 100}
step={0.1} step={0.1}
sx={{ sx={{
color: "#00abff", color: "#00abff",
padding: "0px", padding: "0px",
borderRadius: '0px', borderRadius: "0px",
height: '0px', height: "0px",
"@media (pointer: coarse)": { padding: "0px" }, "@media (pointer: coarse)": { padding: "0px" },
"& .MuiSlider-thumb": { "& .MuiSlider-thumb": {
backgroundColor: "red", backgroundColor: "red",
width: "14px", width: "14px",
height: "14px", height: "14px",
}, },
"& .MuiSlider-thumb::after": { width: "14px", height: "14px", backgroundColor: 'red' }, "& .MuiSlider-thumb::after": {
"& .MuiSlider-rail": { opacity: 0.5, height: "6px", backgroundColor: '#73859f80' }, width: "14px",
"& .MuiSlider-track": { height: "6px", border: "0px" , backgroundColor: 'red'}, height: "14px",
backgroundColor: "red",
},
"& .MuiSlider-rail": {
opacity: 0.5,
height: "6px",
backgroundColor: "#73859f80",
},
"& .MuiSlider-track": {
height: "6px",
border: "0px",
backgroundColor: "red",
},
}} }}
/> />
{hoverX !== null && ( {hoverX !== null && (
<Popper <Popper
open open
anchorEl={hoverAnchorRef.current} placement="top" anchorEl={hoverAnchorRef.current}
placement="top"
disablePortal disablePortal
modifiers={[{ name: "offset", options: { offset: [-20, 0] } }]} modifiers={[{ name: "offset", options: { offset: [-10, 0] } }]}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
bgcolor: alpha("#181818", 0.75),
padding: '5px',
borderRadius: '5px'
}}
> >
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}>
{/* <Box {/* <Box
sx={{ sx={{
width: 250, width: 250,
@ -197,11 +229,15 @@ console.log('thumbnailUrl', thumbnailUrl, hoverX)
style={{ width: "100%", height: "100%", objectFit: "cover" }} style={{ width: "100%", height: "100%", objectFit: "cover" }}
/> />
</Box> */} </Box> */}
<Typography sx={{ <Typography
fontSize: '0.8rom', sx={{
textShadow: '0 0 5px rgba(0, 0, 0, 0.7)' fontSize: "0.8rom",
textShadow: "0 0 5px rgba(0, 0, 0, 0.7)",
}}>{formatTime(showDuration)}</Typography> fontFamily: "sans-serif"
}}
>
{formatTime(showDuration)}
</Typography>
</Box> </Box>
</Popper> </Popper>
)} )}
@ -209,9 +245,7 @@ console.log('thumbnailUrl', thumbnailUrl, hoverX)
); );
}; };
export const VideoTime = ({progress, isScreenSmall, duration}: any) => { export const VideoTime = ({ progress, isScreenSmall, duration }: any) => {
return ( return (
<CustomFontTooltip <CustomFontTooltip
title="Seek video in 10% increments (0-9)" title="Seek video in 10% increments (0-9)"
@ -221,20 +255,21 @@ export const VideoTime = ({progress, isScreenSmall, duration}: any) => {
<Typography <Typography
sx={{ sx={{
fontSize: isScreenSmall ? fontSizeExSmall : fontSizeSmall, fontSize: isScreenSmall ? fontSizeExSmall : fontSizeSmall,
color: 'white', color: "white",
visibility: typeof duration !== 'number' ? 'hidden' : 'visible', visibility: typeof duration !== "number" ? "hidden" : "visible",
whiteSpace: 'nowrap', whiteSpace: "nowrap",
fontFamily: "sans-serif"
}} }}
> >
{typeof duration === 'number' ? formatTime(progress) : ''} {typeof duration === "number" ? formatTime(progress) : ""}
{' / '} {" / "}
{typeof duration === 'number' ? formatTime(duration) : ''} {typeof duration === "number" ? formatTime(duration) : ""}
</Typography> </Typography>
</CustomFontTooltip> </CustomFontTooltip>
); );
}; };
const VolumeButton = ({isMuted, toggleMute}: any) => { const VolumeButton = ({ isMuted, toggleMute }: any) => {
return ( return (
<CustomFontTooltip <CustomFontTooltip
title="Toggle Mute (M), Raise (UP), Lower (DOWN)" title="Toggle Mute (M), Raise (UP), Lower (DOWN)"
@ -289,33 +324,173 @@ export const VolumeControl = ({ sliderWidth, onVolumeChange, volume }: any) => {
sx={{ display: "flex", gap: "5px", alignItems: "center", width: "100%" }} sx={{ display: "flex", gap: "5px", alignItems: "center", width: "100%" }}
> >
<VolumeButton /> <VolumeButton />
<VolumeSlider width={sliderWidth} onVolumeChange={onVolumeChange} volume={volume} /> <VolumeSlider
width={sliderWidth}
onVolumeChange={onVolumeChange}
volume={volume}
/>
</Box> </Box>
); );
}; };
export const PlaybackRate = ({playbackRate, increaseSpeed, isScreenSmall}: any) => { const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 2.5, 3];
export const PlaybackRate = ({
playbackRate,
increaseSpeed,
isScreenSmall,
onSelect,
}: any) => {
const [isOpen, setIsOpen] = useState(false);
const btnRef = useRef(null);
const theme = useTheme();
const onBack = () => {
setIsOpen(false);
};
return ( return (
<>
<CustomFontTooltip <CustomFontTooltip
title="Video Speed. Increase (+ or >), Decrease (- or <)" title="Video Speed. Increase (+ or >), Decrease (- or <)"
placement="bottom" placement="bottom"
arrow arrow
> >
<IconButton <IconButton
ref={btnRef}
sx={{ sx={{
color: "white", color: "white",
fontSize: fontSizeSmall, fontSize: fontSizeSmall,
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig, padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
}} }}
onClick={() => increaseSpeed()} onClick={() => setIsOpen(true)}
> >
{playbackRate}x <SlowMotionVideoIcon />
</IconButton> </IconButton>
</CustomFontTooltip> </CustomFontTooltip>
<Popover
open={isOpen}
anchorEl={btnRef.current}
onClose={() => setIsOpen(false)}
slots={{
transition: Fade,
}}
slotProps={{
transition: {
timeout: 200,
},
paper: {
sx: {
bgcolor: alpha("#181818", 0.98),
color: "white",
opacity: 0.9,
borderRadius: 2,
boxShadow: 5,
p: 1,
minWidth: 225,
height: 300,
overflow: "hidden",
display: "flex",
flexDirection: "column",
},
},
}}
anchorOrigin={{
vertical: "top",
horizontal: "center",
}}
transformOrigin={{
vertical: "bottom",
horizontal: "center",
}}
>
<Box
sx={{
padding: "5px 0px 10px 0px",
display: "flex",
gap: "10px",
width: "100%",
}}
>
<ButtonBase onClick={onBack}>
<ArrowBackIosIcon
sx={{
fontSize: "1.15em",
}}
/>
</ButtonBase>
<ButtonBase>
<Typography
onClick={onBack}
sx={{
fontSize: "0.85rem",
}}
>
Playback speed
</Typography>
</ButtonBase>
</Box>
<Divider />
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
overflow: "auto",
"::-webkit-scrollbar-track": {
backgroundColor: "transparent",
},
"::-webkit-scrollbar": {
width: "16px",
height: "10px",
},
"::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.primary.main,
borderRadius: "8px",
backgroundClip: "content-box",
border: "4px solid transparent",
transition: "0.3s background-color",
},
"::-webkit-scrollbar-thumb:hover": {
backgroundColor: theme.palette.primary.dark,
},
}}
>
{speeds?.map((speed) => {
const isSelected = speed === playbackRate;
return (
<ButtonBase
disabled={isSelected}
key={speed}
onClick={() => {
onSelect(speed)
setIsOpen(false)
}}
sx={{
px: 2,
py: 1,
"&:hover": {
backgroundColor: "rgba(255, 255, 255, 0.1)",
},
width: "100%",
justifyContent: "space-between",
}}
>
<Typography>{speed}</Typography>
{isSelected ? <CheckIcon /> : <ArrowForwardIosIcon />}
</ButtonBase>
);
})}
</Box>
</Popover>
</>
); );
}; };
export const ObjectFitButton = ({toggleObjectFit, isScreenSmall}: any) => { export const ObjectFitButton = ({ toggleObjectFit, isScreenSmall }: any) => {
return ( return (
<CustomFontTooltip title="Toggle Aspect Ratio (O)" placement="bottom" arrow> <CustomFontTooltip title="Toggle Aspect Ratio (O)" placement="bottom" arrow>
<IconButton <IconButton
@ -331,8 +506,12 @@ export const ObjectFitButton = ({toggleObjectFit, isScreenSmall}: any) => {
); );
}; };
export const PictureInPictureButton = ({isFullscreen, toggleRef, togglePictureInPicture, isScreenSmall}: any) => { export const PictureInPictureButton = ({
isFullscreen,
toggleRef,
togglePictureInPicture,
isScreenSmall,
}: any) => {
return ( return (
<> <>
{!isFullscreen && ( {!isFullscreen && (
@ -357,8 +536,7 @@ export const PictureInPictureButton = ({isFullscreen, toggleRef, togglePictureIn
); );
}; };
export const FullscreenButton = ({toggleFullscreen, isScreenSmall}: any) => { export const FullscreenButton = ({ toggleFullscreen, isScreenSmall }: any) => {
return ( return (
<CustomFontTooltip title="Toggle Fullscreen (F)" placement="bottom" arrow> <CustomFontTooltip title="Toggle Fullscreen (F)" placement="bottom" arrow>
<IconButton <IconButton

View File

@ -14,6 +14,7 @@ import {
} from "./VideoControls"; } from "./VideoControls";
import { Ref } from "react"; import { Ref } from "react";
import SubtitlesIcon from '@mui/icons-material/Subtitles'; import SubtitlesIcon from '@mui/icons-material/Subtitles';
import { CustomFontTooltip } from "./CustomFontTooltip";
interface VideoControlsBarProps { interface VideoControlsBarProps {
canPlay: boolean canPlay: boolean
isScreenSmall: boolean isScreenSmall: boolean
@ -36,9 +37,10 @@ interface VideoControlsBarProps {
playbackRate: number playbackRate: number
openSubtitleManager: ()=> void openSubtitleManager: ()=> void
subtitleBtnRef: any subtitleBtnRef: any
onSelectPlaybackRate: (rate: number)=> void;
} }
export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, increaseSpeed,decreaseSpeed, isFullScreen, showControlsFullScreen, reloadVideo, onVolumeChange, volume, isPlaying, canPlay, isScreenSmall, controlsHeight, playerRef, duration, progress, togglePlay, toggleFullscreen, extractFrames, openSubtitleManager}: VideoControlsBarProps) => { export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, increaseSpeed,decreaseSpeed, isFullScreen, showControlsFullScreen, reloadVideo, onVolumeChange, volume, isPlaying, canPlay, isScreenSmall, controlsHeight, playerRef, duration, progress, togglePlay, toggleFullscreen, extractFrames, openSubtitleManager, onSelectPlaybackRate}: VideoControlsBarProps) => {
const showMobileControls = isScreenSmall && canPlay; const showMobileControls = isScreenSmall && canPlay;
@ -96,12 +98,18 @@ export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, in
</Box> </Box>
<Box sx={{...controlGroupSX, marginLeft: 'auto'}}> <Box sx={{...controlGroupSX, marginLeft: 'auto'}}>
<PlaybackRate playbackRate={playbackRate} increaseSpeed={increaseSpeed} decreaseSpeed={decreaseSpeed} /> <PlaybackRate onSelect={onSelectPlaybackRate} playbackRate={playbackRate} increaseSpeed={increaseSpeed} decreaseSpeed={decreaseSpeed} />
<ObjectFitButton /> {/* <ObjectFitButton /> */}
<CustomFontTooltip
title="Subtitles"
placement="bottom"
arrow
>
<IconButton ref={subtitleBtnRef} onClick={openSubtitleManager}> <IconButton ref={subtitleBtnRef} onClick={openSubtitleManager}>
<SubtitlesIcon /> <SubtitlesIcon />
</IconButton> </IconButton>
<PictureInPictureButton /> </CustomFontTooltip>
{/* <PictureInPictureButton /> */}
<FullscreenButton toggleFullscreen={toggleFullscreen} /> <FullscreenButton toggleFullscreen={toggleFullscreen} />
</Box> </Box>
</Box> </Box>

View File

@ -206,6 +206,7 @@ export const VideoPlayer = ({
status, status,
percentLoaded, percentLoaded,
showControlsFullScreen, showControlsFullScreen,
onSelectPlaybackRate
} = useVideoPlayerController({ } = useVideoPlayerController({
autoPlay, autoPlay,
playerRef, playerRef,
@ -214,6 +215,27 @@ export const VideoPlayer = ({
isPlayerInitialized, isPlayerInitialized,
}); });
console.log('isFullscreen', isFullscreen)
const enterFullscreen = useCallback(() => {
const ref = containerRef?.current as any;
if (!ref) return;
if (ref.requestFullscreen && !isFullscreen) {
ref.requestFullscreen();
}
}, []);
const exitFullscreen = useCallback(() => {
document?.exitFullscreen();
}, [isFullscreen]);
const toggleFullscreen = useCallback(() => {
console.log('togglefull', isFullscreen)
isFullscreen ? exitFullscreen() : enterFullscreen();
}, [isFullscreen]);
const hotkeyHandlers = useMemo( const hotkeyHandlers = useMemo(
() => ({ () => ({
reloadVideo, reloadVideo,
@ -227,6 +249,7 @@ export const VideoPlayer = ({
toggleMute, toggleMute,
setProgressAbsolute, setProgressAbsolute,
setAlwaysShowControls, setAlwaysShowControls,
toggleFullscreen
}), }),
[ [
reloadVideo, reloadVideo,
@ -240,6 +263,7 @@ export const VideoPlayer = ({
toggleMute, toggleMute,
setProgressAbsolute, setProgressAbsolute,
setAlwaysShowControls, setAlwaysShowControls,
toggleFullscreen
] ]
); );
@ -346,22 +370,6 @@ export const VideoPlayer = ({
}; };
}, [isPlayerInitialized]); }, [isPlayerInitialized]);
const enterFullscreen = () => {
const ref = containerRef?.current as any;
if (!ref) return;
if (ref.requestFullscreen && !isFullscreen) {
ref.requestFullscreen();
}
};
const exitFullscreen = () => {
if (isFullscreen) document.exitFullscreen();
};
const toggleFullscreen = () => {
isFullscreen ? exitFullscreen() : enterFullscreen();
};
const canvasRef = useRef(null); const canvasRef = useRef(null);
const videoRefForCanvas = useRef<any>(null); const videoRefForCanvas = useRef<any>(null);
@ -744,6 +752,7 @@ export const VideoPlayer = ({
duration={duration} duration={duration}
progress={localProgress} progress={localProgress}
openSubtitleManager={openSubtitleManager} openSubtitleManager={openSubtitleManager}
onSelectPlaybackRate={onSelectPlaybackRate}
/> />
)} )}

View File

@ -280,6 +280,7 @@ const togglePlay = useCallback(async () => {
} }
}, [togglePlay, isReady]); }, [togglePlay, isReady]);
return { return {
reloadVideo, reloadVideo,
togglePlay, togglePlay,
@ -299,6 +300,6 @@ const togglePlay = useCallback(async () => {
isReady, isReady,
resourceUrl, resourceUrl,
startPlay, startPlay,
status, percentLoaded, showControlsFullScreen status, percentLoaded, showControlsFullScreen, onSelectPlaybackRate: updatePlaybackRate
}; };
}; };

View File

@ -12,6 +12,7 @@ interface UseVideoControls {
changeVolume: (delta: number) => void; changeVolume: (delta: number) => void;
toggleMute: () => void; toggleMute: () => void;
setProgressAbsolute: (percent: number) => void; setProgressAbsolute: (percent: number) => void;
toggleFullscreen: ()=> void;
} }
export const useVideoPlayerHotKeys = (props: UseVideoControls) => { export const useVideoPlayerHotKeys = (props: UseVideoControls) => {
@ -26,6 +27,7 @@ export const useVideoPlayerHotKeys = (props: UseVideoControls) => {
changeVolume, changeVolume,
toggleMute, toggleMute,
setProgressAbsolute, setProgressAbsolute,
toggleFullscreen
} = props; } = props;
const handleKeyDown = useCallback((e: KeyboardEvent) => { const handleKeyDown = useCallback((e: KeyboardEvent) => {
@ -46,6 +48,9 @@ export const useVideoPlayerHotKeys = (props: UseVideoControls) => {
case "o": case "o":
toggleObjectFit(); toggleObjectFit();
break; break;
case "f":
toggleFullscreen();
break;
case "c": case "c":
toggleAlwaysShowControls(); toggleAlwaysShowControls();
break; break;
@ -127,6 +132,7 @@ export const useVideoPlayerHotKeys = (props: UseVideoControls) => {
changeVolume, changeVolume,
toggleMute, toggleMute,
setProgressAbsolute, setProgressAbsolute,
toggleFullscreen
]); ]);
useEffect(() => { useEffect(() => {