From 53eaad448b1999ee6e8c12b59a0529be7b353ef2 Mon Sep 17 00:00:00 2001 From: IrohDW Date: Fri, 20 Dec 2024 10:21:58 -0700 Subject: [PATCH] Updates to VideoPlayer controls All controls and hotkeys work when the VideoPlayer is fullscreen Controls are below video instead of inside of it Controls have tooltips showing what they do and their hotkeys Each control is a separate component that is used in both mobile and normal controls Video Progress slider is above controls to save horizontal space Controls will disappear when fullscreen if mouse leaves video player, or after 5 seconds of inactivity Default port in vite.config.ts set to 3000 for simplicity. --- package-lock.json | 20 +- package.json | 1 + .../VideoPlayer/Components/MobileControls.tsx | 109 ------- .../Components/MobileControlsBar.tsx | 66 ++++ .../Components/VideoControls-State.ts | 62 ++-- .../VideoPlayer/Components/VideoControls.tsx | 297 +++++++++++------- .../Components/VideoControlsBar.tsx | 62 ++++ .../common/VideoPlayer/VideoPlayer-State.ts | 4 + .../common/VideoPlayer/VideoPlayer-styles.ts | 5 +- .../common/VideoPlayer/VideoPlayer.tsx | 51 +-- src/constants/Misc.ts | 1 + src/hooks/useIdleTimeout.ts | 14 + .../VideoContent/VideoContent.tsx | 9 +- src/pages/Home/Home.tsx | 11 +- src/utils/CustomFontTooltip.tsx | 23 ++ vite.config.ts | 9 +- 16 files changed, 440 insertions(+), 304 deletions(-) delete mode 100644 src/components/common/VideoPlayer/Components/MobileControls.tsx create mode 100644 src/components/common/VideoPlayer/Components/MobileControlsBar.tsx create mode 100644 src/components/common/VideoPlayer/Components/VideoControlsBar.tsx create mode 100644 src/hooks/useIdleTimeout.ts create mode 100644 src/utils/CustomFontTooltip.tsx diff --git a/package-lock.json b/package-lock.json index fc3ff92..5c70df0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "qtube", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "qtube", - "version": "2.0.0", + "version": "2.1.0", "dependencies": { "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", @@ -23,6 +23,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", + "react-idle-timer": "^5.7.2", "react-intersection-observer": "^9.4.3", "react-quill": "^2.0.0", "react-redux": "^8.0.5", @@ -3958,6 +3959,15 @@ "react": ">= 16.8 || 18.0.0" } }, + "node_modules/react-idle-timer": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/react-idle-timer/-/react-idle-timer-5.7.2.tgz", + "integrity": "sha512-+BaPfc7XEUU5JFkwZCx6fO1bLVK+RBlFH+iY4X34urvIzZiZINP6v2orePx3E6pAztJGE7t4DzvL7if2SL/0GQ==", + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-intersection-observer": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.5.0.tgz", @@ -7321,6 +7331,12 @@ "prop-types": "^15.8.1" } }, + "react-idle-timer": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/react-idle-timer/-/react-idle-timer-5.7.2.tgz", + "integrity": "sha512-+BaPfc7XEUU5JFkwZCx6fO1bLVK+RBlFH+iY4X34urvIzZiZINP6v2orePx3E6pAztJGE7t4DzvL7if2SL/0GQ==", + "requires": {} + }, "react-intersection-observer": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.5.0.tgz", diff --git a/package.json b/package.json index 0e9e6af..7bda89b 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", + "react-idle-timer": "^5.7.2", "react-intersection-observer": "^9.4.3", "react-quill": "^2.0.0", "react-redux": "^8.0.5", diff --git a/src/components/common/VideoPlayer/Components/MobileControls.tsx b/src/components/common/VideoPlayer/Components/MobileControls.tsx deleted file mode 100644 index d5c8a39..0000000 --- a/src/components/common/VideoPlayer/Components/MobileControls.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { - Fullscreen, - MoreVert as MoreIcon, - Pause, - PictureInPicture, - PlayArrow, - Refresh, - VolumeUp, -} from "@mui/icons-material"; -import { IconButton, Menu, MenuItem, Slider, Typography } from "@mui/material"; -import { useVideoContext } from "./VideoContext.ts"; - -export const MobileControls = () => { - const { - togglePlay, - reloadVideo, - onProgressChange, - videoRef, - handleMenuOpen, - handleMenuClose, - onVolumeChange, - increaseSpeed, - togglePictureInPicture, - toggleFullscreen, - playing, - progress, - anchorEl, - volume, - playbackRate, - } = useVideoContext(); - - return ( - <> - togglePlay()} - > - {playing.value ? : } - - - - - - increaseSpeed()} - > - {playbackRate}x - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/src/components/common/VideoPlayer/Components/MobileControlsBar.tsx b/src/components/common/VideoPlayer/Components/MobileControlsBar.tsx new file mode 100644 index 0000000..6376afc --- /dev/null +++ b/src/components/common/VideoPlayer/Components/MobileControlsBar.tsx @@ -0,0 +1,66 @@ +import { MoreVert as MoreIcon } from "@mui/icons-material"; +import { Box, IconButton, Menu, MenuItem } from "@mui/material"; +import { useVideoContext } from "./VideoContext.ts"; +import { + FullscreenButton, + PictureInPictureButton, + PlaybackRate, + PlayButton, + ProgressSlider, + ReloadButton, + VideoTime, + VolumeButton, + VolumeSlider, +} from "./VideoControls.tsx"; + +export const MobileControlsBar = () => { + const { handleMenuOpen, handleMenuClose, anchorEl } = useVideoContext(); + + const controlGroupSX = { display: "flex", gap: "5px", alignItems: "center" }; + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/components/common/VideoPlayer/Components/VideoControls-State.ts b/src/components/common/VideoPlayer/Components/VideoControls-State.ts index 3ba70d1..ea0f92f 100644 --- a/src/components/common/VideoPlayer/Components/VideoControls-State.ts +++ b/src/components/common/VideoPlayer/Components/VideoControls-State.ts @@ -5,16 +5,8 @@ import { useEffect } from "react"; import ReactDOM from "react-dom"; import { useDispatch, useSelector } from "react-redux"; import { Key } from "ts-key-enum"; -import { useIsMobile } from "../../../../hooks/useIsMobile.ts"; import { setVideoPlaying } from "../../../../state/features/globalSlice.ts"; -import { - setIsMuted, - setMutedVolumeSetting, - setReduxPlaybackRate, - setStretchVideoSetting, - setVolumeSetting, -} from "../../../../state/features/persistSlice.ts"; -import { RootState, store } from "../../../../state/store.ts"; +import { RootState } from "../../../../state/store.ts"; import { useVideoPlayerState } from "../VideoPlayer-State.ts"; import { VideoPlayerProps } from "../VideoPlayer.tsx"; @@ -38,6 +30,7 @@ export const useVideoControlsState = ( progress, videoObjectFit, canPlay, + containerRef, } = videoPlayerState; const { identifier, autoPlay } = props; @@ -78,16 +71,15 @@ export const useVideoControlsState = ( const isFullscreen = useSignal(false); const enterFullscreen = () => { - if (!videoRef.current) return; - if (videoRef.current.requestFullscreen) { - videoRef.current.requestFullscreen(); + if (!containerRef.current) return; + + if (containerRef.current.requestFullscreen && !isFullscreen.value) { + containerRef.current.requestFullscreen(); } }; const exitFullscreen = () => { - if (document.exitFullscreen) { - document.exitFullscreen(); - } + if (isFullscreen.value) document.exitFullscreen(); }; const toggleFullscreen = () => { @@ -218,14 +210,20 @@ export const useVideoControlsState = ( } }; - const toggleStretchVideoSetting = () => { - const newStretchVideoSetting = - persistSelector.stretchVideoSetting === "contain" ? "fill" : "contain"; - - videoObjectFit.value = newStretchVideoSetting; + const setStretchVideoSetting = (value: "contain" | "fill") => { + videoObjectFit.value = value; }; - const keyboardShortcutsDown = (e: React.KeyboardEvent) => { + + const toggleStretchVideoSetting = () => { + videoObjectFit.value = + videoObjectFit.value === "contain" ? "fill" : "contain"; + }; + + const keyboardShortcuts = ( + e: KeyboardEvent | React.KeyboardEvent + ) => { e.preventDefault(); + // console.log("hotkey is: ", '"' + e.key + '"'); switch (e.key) { case "o": @@ -276,13 +274,6 @@ export const useVideoControlsState = ( case Key.ArrowUp: changeVolume(0.05); break; - } - }; - - const keyboardShortcutsUp = (e: React.KeyboardEvent) => { - e.preventDefault(); - - switch (e.key) { case " ": togglePlay(); break; @@ -291,12 +282,20 @@ export const useVideoControlsState = ( break; case "f": - enterFullscreen(); + toggleFullscreen(); break; case Key.Escape: exitFullscreen(); break; + case "r": + reloadVideo(); + break; + + case "p": + togglePictureInPicture(); + break; + case "0": setProgressAbsolute(0); break; @@ -360,11 +359,12 @@ export const useVideoControlsState = ( increaseSpeed, togglePictureInPicture, toggleFullscreen, - keyboardShortcutsUp, - keyboardShortcutsDown, + keyboardShortcuts, handleCanPlay, toggleMute, showControlsFullScreen, setPlaying, + isFullscreen, + setStretchVideoSetting, }; }; diff --git a/src/components/common/VideoPlayer/Components/VideoControls.tsx b/src/components/common/VideoPlayer/Components/VideoControls.tsx index 93e620b..9702a1f 100644 --- a/src/components/common/VideoPlayer/Components/VideoControls.tsx +++ b/src/components/common/VideoPlayer/Components/VideoControls.tsx @@ -1,3 +1,8 @@ +import { IconButton, Slider, Typography } from "@mui/material"; +import { fontSizeExSmall, fontSizeSmall } from "../../../../constants/Misc.ts"; +import { CustomFontTooltip } from "../../../../utils/CustomFontTooltip.tsx"; +import { formatTime } from "../../../../utils/numberFunctions.ts"; +import { useVideoContext } from "./VideoContext.ts"; import { Fullscreen, Pause, @@ -7,142 +12,190 @@ import { VolumeOff, VolumeUp, } from "@mui/icons-material"; -import { IconButton, Slider, Typography, useMediaQuery } from "@mui/material"; -import { smallScreenSizeString } from "../../../../constants/Misc.ts"; -import { formatTime } from "../../../../utils/numberFunctions.ts"; - -import { ControlsContainer } from "../VideoPlayer-styles.ts"; -import { MobileControls } from "./MobileControls.tsx"; -import { useVideoContext } from "./VideoContext.ts"; import { useSignalEffect } from "@preact/signals-react"; -export const VideoControls = () => { - const { - reloadVideo, - togglePlay, - onVolumeChange, - increaseSpeed, - togglePictureInPicture, - toggleFullscreen, - toggleMute, - onProgressChange, - toggleRef, - from, - videoRef, - canPlay, - isMuted, - playbackRate, - playing, - progress, - volume, - showControlsFullScreen, - } = useVideoContext(); +export const PlayButton = () => { + const { togglePlay, playing } = useVideoContext(); + return ( + + togglePlay()} + > + {playing.value ? : } + + + ); +}; - const isScreenSmall = !useMediaQuery(`(min-width:580px)`); - const showMobileControls = isScreenSmall && canPlay.value; +export const ReloadButton = () => { + const { reloadVideo } = useVideoContext(); + return ( + + + + + + ); +}; + +export const ProgressSlider = () => { + const { progress, onProgressChange, videoRef } = useVideoContext(); + const sliderThumbSize = "16px"; + return ( + + ); +}; + +export const VideoTime = () => { + const { videoRef, progress, isScreenSmall } = useVideoContext(); return ( - - {showMobileControls ? ( - - ) : canPlay.value ? ( - <> - togglePlay()} - > - {playing.value ? : } - - - - - - - {progress.value && - videoRef.current?.duration && - formatTime(progress.value)} - / - {progress.value && - videoRef.current?.duration && - formatTime(videoRef.current?.duration)} - - - {isMuted.value ? : } - - - increaseSpeed()} - > - Speed: {playbackRate}x - + + {videoRef.current?.duration ? formatTime(progress.value) : ""} + {" / "} + {videoRef.current?.duration + ? formatTime(videoRef.current?.duration) + : ""} + + + ); +}; +export const VolumeButton = () => { + const { isMuted, toggleMute } = useVideoContext(); + return ( + + + {isMuted.value ? : } + + + ); +}; + +export const VolumeSlider = () => { + const { volume, onVolumeChange } = useVideoContext(); + return ( + + ); +}; + +export const PlaybackRate = () => { + const { playbackRate, increaseSpeed, isScreenSmall } = useVideoContext(); + return ( + + increaseSpeed()} + > + + {playbackRate}x + + + + ); +}; + +export const PictureInPictureButton = () => { + const { isFullscreen, toggleRef, togglePictureInPicture } = useVideoContext(); + return ( + <> + {!isFullscreen.value && ( + - - - - - ) : null} - + + )} + + ); +}; + +export const FullscreenButton = () => { + const { toggleFullscreen } = useVideoContext(); + return ( + + toggleFullscreen()} + > + + + ); }; diff --git a/src/components/common/VideoPlayer/Components/VideoControlsBar.tsx b/src/components/common/VideoPlayer/Components/VideoControlsBar.tsx new file mode 100644 index 0000000..c0d7f2d --- /dev/null +++ b/src/components/common/VideoPlayer/Components/VideoControlsBar.tsx @@ -0,0 +1,62 @@ +import { Box } from "@mui/material"; +import { ControlsContainer } from "../VideoPlayer-styles.ts"; +import { MobileControlsBar } from "./MobileControlsBar.tsx"; +import { useVideoContext } from "./VideoContext.ts"; +import { + FullscreenButton, + PictureInPictureButton, + PlaybackRate, + PlayButton, + ProgressSlider, + ReloadButton, + VideoTime, + VolumeButton, + VolumeSlider, +} from "./VideoControls.tsx"; +import { useSignalEffect } from "@preact/signals-react"; + +export const VideoControlsBar = () => { + const { from, canPlay, showControlsFullScreen, isScreenSmall, progress } = + useVideoContext(); + + const showMobileControls = isScreenSmall && canPlay.value; + const controlsHeight = "40px"; + const controlGroupSX = { + display: "flex", + gap: "5px", + alignItems: "center", + height: controlsHeight, + }; + + return ( + + {showMobileControls ? ( + + ) : canPlay.value ? ( + <> + + + + + + + + + + + + + + + + + + ) : null} + + ); +}; diff --git a/src/components/common/VideoPlayer/VideoPlayer-State.ts b/src/components/common/VideoPlayer/VideoPlayer-State.ts index 8ffcfaa..f688d04 100644 --- a/src/components/common/VideoPlayer/VideoPlayer-State.ts +++ b/src/components/common/VideoPlayer/VideoPlayer-State.ts @@ -1,3 +1,4 @@ +import { useMediaQuery } from "@mui/material"; import { useSignal, useSignalEffect, @@ -12,6 +13,7 @@ import React, { } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { smallVideoSize } from "../../../constants/Misc.ts"; import { setVideoPlaying } from "../../../state/features/globalSlice.ts"; import { setIsMuted, @@ -292,6 +294,7 @@ export const useVideoPlayerState = (props: VideoPlayerProps, ref: any) => { anchorEl.value = null; }; + const isScreenSmall = !useMediaQuery(smallVideoSize); return { containerRef, resourceStatus, @@ -315,5 +318,6 @@ export const useVideoPlayerState = (props: VideoPlayerProps, ref: any) => { playbackRate, anchorEl, videoObjectFit, + isScreenSmall, }; }; diff --git a/src/components/common/VideoPlayer/VideoPlayer-styles.ts b/src/components/common/VideoPlayer/VideoPlayer-styles.ts index 87c56d8..84bab4b 100644 --- a/src/components/common/VideoPlayer/VideoPlayer-styles.ts +++ b/src/components/common/VideoPlayer/VideoPlayer-styles.ts @@ -20,12 +20,9 @@ export const VideoElement = styled("video")(({ theme }) => ({ })); //1075 x 604 export const ControlsContainer = styled(Box)` - position: absolute; + width: 100%; display: flex; align-items: center; justify-content: space-between; - bottom: 0; - left: 0; - right: 0; background-color: rgba(0, 0, 0, 0.6); `; diff --git a/src/components/common/VideoPlayer/VideoPlayer.tsx b/src/components/common/VideoPlayer/VideoPlayer.tsx index 11d42cc..aa133d5 100644 --- a/src/components/common/VideoPlayer/VideoPlayer.tsx +++ b/src/components/common/VideoPlayer/VideoPlayer.tsx @@ -1,8 +1,9 @@ import CSS from "csstype"; import { forwardRef } from "react"; +import useIdleTimeout from "../../../hooks/useIdleTimeout.ts"; import { LoadingVideo } from "./Components/LoadingVideo.tsx"; import { useContextData, VideoContext } from "./Components/VideoContext.ts"; -import { VideoControls } from "./Components/VideoControls.tsx"; +import { VideoControlsBar } from "./Components/VideoControlsBar.tsx"; import { VideoContainer, VideoElement } from "./VideoPlayer-styles.ts"; export interface VideoStyles { @@ -37,8 +38,7 @@ export const VideoPlayer = forwardRef( const contextData = useContextData(props, ref); const { - keyboardShortcutsUp, - keyboardShortcutsDown, + keyboardShortcuts, from, videoStyles, containerRef, @@ -55,18 +55,35 @@ export const VideoPlayer = forwardRef( startPlay, videoObjectFit, showControlsFullScreen, + isFullscreen, } = contextData; + const showControls = + !isFullscreen.value || + (isFullscreen.value && showControlsFullScreen.value); + + const idleTime = 5000; // Time in milliseconds + useIdleTimeout({ + onIdle: () => (showControlsFullScreen.value = false), + onActive: () => (showControlsFullScreen.value = true), + idleTime, + }); + return ( { + showControlsFullScreen.value = true; + }} + onMouseLeave={e => { + showControlsFullScreen.value = false; + }} ref={containerRef} > @@ -83,23 +100,17 @@ export const VideoPlayer = forwardRef( onEnded={handleEnded} // onLoadedMetadata={handleLoadedMetadata} onCanPlay={handleCanPlay} - onMouseEnter={e => { - showControlsFullScreen.value = true; - }} - onMouseLeave={e => { - showControlsFullScreen.value = false; - }} preload="metadata" - style={ - startPlay.value - ? { - ...videoStyles?.video, - objectFit: videoObjectFit.value, - } - : { height: "100%", ...videoStyles } - } + style={{ + ...videoStyles?.video, + objectFit: isFullscreen ? "fill" : videoObjectFit.value, + height: + isFullscreen.value && showControlsFullScreen.value + ? "calc(100vh - 40px)" + : "100%", + }} /> - + {showControls && } ); diff --git a/src/constants/Misc.ts b/src/constants/Misc.ts index d581dd9..108d72e 100644 --- a/src/constants/Misc.ts +++ b/src/constants/Misc.ts @@ -21,5 +21,6 @@ const largeScreenSize = 1400 - newUIWidthDiff; export const smallScreenSizeString = `${smallScreenSize}px`; export const largeScreenSizeString = `${largeScreenSize}px`; +export const smallVideoSize = `(min-width:720px)`; export const headerIconSize = "40px"; export const menuIconSize = "28px"; diff --git a/src/hooks/useIdleTimeout.ts b/src/hooks/useIdleTimeout.ts new file mode 100644 index 0000000..b5b893e --- /dev/null +++ b/src/hooks/useIdleTimeout.ts @@ -0,0 +1,14 @@ +import { useContext, useState } from "react"; +import { useIdleTimer } from "react-idle-timer"; + +const useIdleTimeout = ({ onIdle, onActive, idleTime = 10_000 }) => { + const idleTimer = useIdleTimer({ + timeout: idleTime, + onIdle: onIdle, + onActive: onActive, + }); + return { + idleTimer, + }; +}; +export default useIdleTimeout; diff --git a/src/pages/ContentPages/VideoContent/VideoContent.tsx b/src/pages/ContentPages/VideoContent/VideoContent.tsx index e2ac061..9cd4cf2 100644 --- a/src/pages/ContentPages/VideoContent/VideoContent.tsx +++ b/src/pages/ContentPages/VideoContent/VideoContent.tsx @@ -11,6 +11,7 @@ import { largeScreenSizeString, minFileSize, smallScreenSizeString, + smallVideoSize, } from "../../../constants/Misc.ts"; import { useIsMobile } from "../../../hooks/useIsMobile.ts"; import { formatBytes } from "../../../utils/numberFunctions.ts"; @@ -47,7 +48,7 @@ export const VideoContent = () => { setSuperLikeList, } = useVideoContentState(); - const isScreenSmall = !useMediaQuery(`(min-width:${smallScreenSizeString})`); + const isScreenSmall = !useMediaQuery(smallVideoSize); const [screenWidth, setScreenWidth] = useState( window.innerWidth + 120 ); @@ -75,7 +76,7 @@ export const VideoContent = () => { sx={{ display: "flex", flexDirection: "column", - padding: `0px 0px 0px ${isScreenSmall ? "5px" : "2%"}`, + padding: `0px 0px 0px ${isScreenSmall ? "0px" : "2%"}`, width: "100%", }} onClick={focusVideo} @@ -112,7 +113,9 @@ export const VideoContent = () => { ) : ( )} - + ) => { + if (!fontSize) fontSize = "160%"; + const text = {title}; + + // put controls into individual components + return ( + +
{children}
+
+ ); +}; diff --git a/vite.config.ts b/vite.config.ts index 5c33a21..9d7d20b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,9 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], - base: "" -}) + server: { port: 3000 }, + base: "", +});