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: "", +});