mirror of https://github.com/Qortal/q-tube
21 changed files with 2257 additions and 1846 deletions
@ -0,0 +1,32 @@
|
||||
import { styled } from "@mui/system"; |
||||
import { Box } from "@mui/material"; |
||||
|
||||
export const VideoContainer = styled(Box)` |
||||
position: relative; |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
justify-content: center; |
||||
width: 100%; |
||||
height: 100%; |
||||
margin: 0; |
||||
padding: 0; |
||||
max-height: 70vh; |
||||
`;
|
||||
|
||||
export const VideoElement = styled("video")` |
||||
width: 100%; |
||||
background: rgb(33, 33, 33); |
||||
max-height: 70vh; |
||||
`;
|
||||
|
||||
export const ControlsContainer = styled(Box)` |
||||
position: absolute; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: space-between; |
||||
bottom: 0; |
||||
left: 0; |
||||
right: 0; |
||||
background-color: rgba(0, 0, 0, 0.6); |
||||
`;
|
@ -0,0 +1,682 @@
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from "react"; |
||||
import ReactDOM from "react-dom"; |
||||
import { Box, IconButton, Slider, useTheme } from "@mui/material"; |
||||
import { CircularProgress, Typography } from "@mui/material"; |
||||
import { Key } from "ts-key-enum"; |
||||
import { |
||||
PlayArrow, |
||||
Pause, |
||||
VolumeUp, |
||||
Fullscreen, |
||||
PictureInPicture, |
||||
VolumeOff, |
||||
} from "@mui/icons-material"; |
||||
import { styled } from "@mui/system"; |
||||
import { MyContext } from "../../../wrappers/DownloadWrapper.tsx"; |
||||
import { useDispatch, useSelector } from "react-redux"; |
||||
import { RootState } from "../../../state/store.ts"; |
||||
import { Refresh } from "@mui/icons-material"; |
||||
import CloseIcon from "@mui/icons-material/Close"; |
||||
|
||||
import { Menu, MenuItem } from "@mui/material"; |
||||
import { MoreVert as MoreIcon } from "@mui/icons-material"; |
||||
import { setVideoPlaying } from "../../../state/features/globalSlice.ts"; |
||||
const VideoContainer = styled(Box)` |
||||
position: relative; |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
justify-content: center; |
||||
width: 100%; |
||||
height: 100%; |
||||
margin: 0px; |
||||
padding: 0px; |
||||
`;
|
||||
|
||||
const VideoElement = styled("video")` |
||||
width: 100%; |
||||
height: auto; |
||||
max-height: calc(100vh - 150px); |
||||
background: rgb(33, 33, 33); |
||||
`;
|
||||
|
||||
const ControlsContainer = styled(Box)` |
||||
position: absolute; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: space-between; |
||||
bottom: 0; |
||||
left: 0; |
||||
right: 0; |
||||
padding: 8px; |
||||
background-color: rgba(0, 0, 0, 0.6); |
||||
`;
|
||||
|
||||
interface VideoPlayerProps { |
||||
src?: string; |
||||
poster?: string; |
||||
name?: string; |
||||
identifier?: string; |
||||
service?: string; |
||||
autoplay?: boolean; |
||||
from?: string | null; |
||||
customStyle?: any; |
||||
user?: string; |
||||
jsonId?: string; |
||||
element?: null | any; |
||||
checkIfDrag?: () => boolean; |
||||
} |
||||
|
||||
export const VideoPlayerGlobal: React.FC<VideoPlayerProps> = ({ |
||||
poster, |
||||
name, |
||||
identifier, |
||||
service, |
||||
autoplay = true, |
||||
from = null, |
||||
customStyle = {}, |
||||
user = "", |
||||
jsonId = "", |
||||
element, |
||||
checkIfDrag, |
||||
}) => { |
||||
const theme = useTheme(); |
||||
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null); |
||||
const [playing, setPlaying] = useState(false); |
||||
const [volume, setVolume] = useState(1); |
||||
const [mutedVolume, setMutedVolume] = useState(1); |
||||
const [isMuted, setIsMuted] = useState(false); |
||||
const [progress, setProgress] = useState(0); |
||||
const [isLoading, setIsLoading] = useState(false); |
||||
const [canPlay, setCanPlay] = useState(false); |
||||
const [startPlay, setStartPlay] = useState(false); |
||||
const [isMobileView, setIsMobileView] = useState(false); |
||||
const [playbackRate, setPlaybackRate] = useState(1); |
||||
const [anchorEl, setAnchorEl] = useState(null); |
||||
const dispatch = useDispatch(); |
||||
const reDownload = useRef<boolean>(false); |
||||
const { downloads } = useSelector((state: RootState) => state.global); |
||||
const download = useMemo(() => { |
||||
if (!downloads || !identifier) return {}; |
||||
const findDownload = downloads[identifier]; |
||||
|
||||
if (!findDownload) return {}; |
||||
return findDownload; |
||||
}, [downloads, identifier]); |
||||
|
||||
const resourceStatus = useMemo(() => { |
||||
return download?.status || {}; |
||||
}, [download]); |
||||
|
||||
const minSpeed = 0.25; |
||||
const maxSpeed = 4.0; |
||||
const speedChange = 0.25; |
||||
|
||||
const updatePlaybackRate = (newSpeed: number) => { |
||||
if (videoRef.current) { |
||||
if (newSpeed > maxSpeed || newSpeed < minSpeed) newSpeed = minSpeed; |
||||
videoRef.current.playbackRate = newSpeed; |
||||
setPlaybackRate(newSpeed); |
||||
} |
||||
}; |
||||
|
||||
const increaseSpeed = (wrapOverflow = true) => { |
||||
const changedSpeed = playbackRate + speedChange; |
||||
let newSpeed = wrapOverflow |
||||
? changedSpeed |
||||
: Math.min(changedSpeed, maxSpeed); |
||||
|
||||
if (videoRef.current) { |
||||
updatePlaybackRate(newSpeed); |
||||
} |
||||
}; |
||||
|
||||
const decreaseSpeed = () => { |
||||
if (videoRef.current) { |
||||
updatePlaybackRate(playbackRate - speedChange); |
||||
} |
||||
}; |
||||
|
||||
const toggleRef = useRef<any>(null); |
||||
const { downloadVideo } = useContext(MyContext); |
||||
const togglePlay = async () => { |
||||
if (checkIfDrag && checkIfDrag()) return; |
||||
if (!videoRef.current) return; |
||||
if (playing) { |
||||
videoRef.current.pause(); |
||||
} else { |
||||
videoRef.current.play(); |
||||
} |
||||
setPlaying(prev => !prev); |
||||
}; |
||||
|
||||
const onVolumeChange = (_: any, value: number | number[]) => { |
||||
if (!videoRef.current) return; |
||||
videoRef.current.volume = value as number; |
||||
setVolume(value as number); |
||||
setIsMuted(false); |
||||
}; |
||||
|
||||
const onProgressChange = (_: any, value: number | number[]) => { |
||||
if (!videoRef.current) return; |
||||
videoRef.current.currentTime = value as number; |
||||
setProgress(value as number); |
||||
if (!playing) { |
||||
videoRef.current.play(); |
||||
setPlaying(true); |
||||
} |
||||
}; |
||||
|
||||
const handleEnded = () => { |
||||
setPlaying(false); |
||||
}; |
||||
|
||||
const updateProgress = () => { |
||||
if (!videoRef.current) return; |
||||
setProgress(videoRef.current.currentTime); |
||||
}; |
||||
|
||||
const [isFullscreen, setIsFullscreen] = useState(false); |
||||
|
||||
const enterFullscreen = () => { |
||||
if (!videoRef.current) return; |
||||
if (videoRef.current.requestFullscreen) { |
||||
videoRef.current.requestFullscreen(); |
||||
} |
||||
}; |
||||
|
||||
const exitFullscreen = () => { |
||||
if (document.exitFullscreen) { |
||||
document.exitFullscreen(); |
||||
} |
||||
}; |
||||
|
||||
const toggleFullscreen = () => { |
||||
isFullscreen ? exitFullscreen() : enterFullscreen(); |
||||
}; |
||||
const togglePictureInPicture = async () => { |
||||
if (!videoRef.current) return; |
||||
if (document.pictureInPictureElement === videoRef.current) { |
||||
await document.exitPictureInPicture(); |
||||
} else { |
||||
await videoRef.current.requestPictureInPicture(); |
||||
} |
||||
}; |
||||
|
||||
useEffect(() => { |
||||
const handleFullscreenChange = () => { |
||||
setIsFullscreen(!!document.fullscreenElement); |
||||
}; |
||||
|
||||
document.addEventListener("fullscreenchange", handleFullscreenChange); |
||||
return () => { |
||||
document.removeEventListener("fullscreenchange", handleFullscreenChange); |
||||
}; |
||||
}, []); |
||||
|
||||
const handleCanPlay = () => { |
||||
setIsLoading(false); |
||||
setCanPlay(true); |
||||
}; |
||||
|
||||
useEffect(() => { |
||||
const videoElement = videoRef.current; |
||||
|
||||
const handleLeavePictureInPicture = async (event: any) => { |
||||
const target = event?.target; |
||||
if (target) { |
||||
target.pause(); |
||||
if (setPlaying) { |
||||
setPlaying(false); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
if (videoElement) { |
||||
videoElement.addEventListener( |
||||
"leavepictureinpicture", |
||||
handleLeavePictureInPicture |
||||
); |
||||
} |
||||
|
||||
return () => { |
||||
if (videoElement) { |
||||
videoElement.removeEventListener( |
||||
"leavepictureinpicture", |
||||
handleLeavePictureInPicture |
||||
); |
||||
} |
||||
}; |
||||
}, []); |
||||
|
||||
function formatTime(seconds: number): string { |
||||
seconds = Math.floor(seconds); |
||||
let minutes: number | string = Math.floor(seconds / 60); |
||||
let hours: number | string = Math.floor(minutes / 60); |
||||
|
||||
let remainingSeconds: number | string = seconds % 60; |
||||
let remainingMinutes: number | string = minutes % 60; |
||||
|
||||
if (remainingSeconds < 10) { |
||||
remainingSeconds = "0" + remainingSeconds; |
||||
} |
||||
|
||||
if (remainingMinutes < 10) { |
||||
remainingMinutes = "0" + remainingMinutes; |
||||
} |
||||
|
||||
if (hours === 0) { |
||||
hours = ""; |
||||
} else { |
||||
hours = hours + ":"; |
||||
} |
||||
|
||||
return hours + remainingMinutes + ":" + remainingSeconds; |
||||
} |
||||
|
||||
const reloadVideo = () => { |
||||
if (!videoRef.current) return; |
||||
const src = videoRef.current.src; |
||||
const currentTime = videoRef.current.currentTime; |
||||
videoRef.current.src = src; |
||||
videoRef.current.load(); |
||||
videoRef.current.currentTime = currentTime; |
||||
if (playing) { |
||||
videoRef.current.play(); |
||||
} |
||||
}; |
||||
|
||||
const handleMenuOpen = (event: any) => { |
||||
setAnchorEl(event.currentTarget); |
||||
}; |
||||
|
||||
const handleMenuClose = () => { |
||||
setAnchorEl(null); |
||||
}; |
||||
|
||||
useEffect(() => { |
||||
const videoWidth = videoRef?.current?.offsetWidth; |
||||
if (videoWidth && videoWidth <= 600) { |
||||
setIsMobileView(true); |
||||
} |
||||
}, [canPlay]); |
||||
|
||||
const getDownloadProgress = (current: number, total: number) => { |
||||
const progress = (current / total) * 100; |
||||
return Number.isNaN(progress) ? "" : progress.toFixed(0) + "%"; |
||||
}; |
||||
const mute = () => { |
||||
setIsMuted(true); |
||||
setMutedVolume(volume); |
||||
setVolume(0); |
||||
if (videoRef.current) videoRef.current.volume = 0; |
||||
}; |
||||
const unMute = () => { |
||||
setIsMuted(false); |
||||
setVolume(mutedVolume); |
||||
if (videoRef.current) videoRef.current.volume = mutedVolume; |
||||
}; |
||||
|
||||
const toggleMute = () => { |
||||
isMuted ? unMute() : mute(); |
||||
}; |
||||
|
||||
const changeVolume = (volumeChange: number) => { |
||||
if (videoRef.current) { |
||||
const minVolume = 0; |
||||
const maxVolume = 1; |
||||
|
||||
let newVolume = volumeChange + volume; |
||||
|
||||
newVolume = Math.max(newVolume, minVolume); |
||||
newVolume = Math.min(newVolume, maxVolume); |
||||
|
||||
setIsMuted(false); |
||||
setMutedVolume(newVolume); |
||||
videoRef.current.volume = newVolume; |
||||
setVolume(newVolume); |
||||
} |
||||
}; |
||||
const setProgressRelative = (secondsChange: number) => { |
||||
if (videoRef.current) { |
||||
const currentTime = videoRef.current?.currentTime; |
||||
const minTime = 0; |
||||
const maxTime = videoRef.current?.duration || 100; |
||||
|
||||
let newTime = currentTime + secondsChange; |
||||
newTime = Math.max(newTime, minTime); |
||||
newTime = Math.min(newTime, maxTime); |
||||
videoRef.current.currentTime = newTime; |
||||
setProgress(newTime); |
||||
} |
||||
}; |
||||
|
||||
const setProgressAbsolute = (videoPercent: number) => { |
||||
if (videoRef.current) { |
||||
videoPercent = Math.min(videoPercent, 100); |
||||
videoPercent = Math.max(videoPercent, 0); |
||||
const finalTime = (videoRef.current?.duration * videoPercent) / 100; |
||||
videoRef.current.currentTime = finalTime; |
||||
setProgress(finalTime); |
||||
} |
||||
}; |
||||
|
||||
const keyboardShortcutsDown = (e: React.KeyboardEvent<HTMLDivElement>) => { |
||||
e.preventDefault(); |
||||
|
||||
switch (e.key) { |
||||
case Key.Add: |
||||
increaseSpeed(false); |
||||
break; |
||||
case "+": |
||||
increaseSpeed(false); |
||||
break; |
||||
case ">": |
||||
increaseSpeed(false); |
||||
break; |
||||
|
||||
case Key.Subtract: |
||||
decreaseSpeed(); |
||||
break; |
||||
case "-": |
||||
decreaseSpeed(); |
||||
break; |
||||
case "<": |
||||
decreaseSpeed(); |
||||
break; |
||||
|
||||
case Key.ArrowLeft: |
||||
{ |
||||
if (e.shiftKey) setProgressRelative(-300); |
||||
else if (e.ctrlKey) setProgressRelative(-60); |
||||
else if (e.altKey) setProgressRelative(-10); |
||||
else setProgressRelative(-5); |
||||
} |
||||
break; |
||||
|
||||
case Key.ArrowRight: |
||||
{ |
||||
if (e.shiftKey) setProgressRelative(300); |
||||
else if (e.ctrlKey) setProgressRelative(60); |
||||
else if (e.altKey) setProgressRelative(10); |
||||
else setProgressRelative(5); |
||||
} |
||||
break; |
||||
|
||||
case Key.ArrowDown: |
||||
changeVolume(-0.05); |
||||
break; |
||||
case Key.ArrowUp: |
||||
changeVolume(0.05); |
||||
break; |
||||
} |
||||
}; |
||||
|
||||
const keyboardShortcutsUp = (e: React.KeyboardEvent<HTMLDivElement>) => { |
||||
e.preventDefault(); |
||||
|
||||
switch (e.key) { |
||||
case " ": |
||||
togglePlay(); |
||||
break; |
||||
case "m": |
||||
toggleMute(); |
||||
break; |
||||
|
||||
case "f": |
||||
enterFullscreen(); |
||||
break; |
||||
case Key.Escape: |
||||
exitFullscreen(); |
||||
break; |
||||
|
||||
case "0": |
||||
setProgressAbsolute(0); |
||||
break; |
||||
case "1": |
||||
setProgressAbsolute(10); |
||||
break; |
||||
case "2": |
||||
setProgressAbsolute(20); |
||||
break; |
||||
case "3": |
||||
setProgressAbsolute(30); |
||||
break; |
||||
case "4": |
||||
setProgressAbsolute(40); |
||||
break; |
||||
case "5": |
||||
setProgressAbsolute(50); |
||||
break; |
||||
case "6": |
||||
setProgressAbsolute(60); |
||||
break; |
||||
case "7": |
||||
setProgressAbsolute(70); |
||||
break; |
||||
case "8": |
||||
setProgressAbsolute(80); |
||||
break; |
||||
case "9": |
||||
setProgressAbsolute(90); |
||||
break; |
||||
} |
||||
}; |
||||
|
||||
useEffect(() => { |
||||
if (element) { |
||||
let oldElement = document.getElementById("videoPlayer"); |
||||
if (oldElement && oldElement?.parentNode) { |
||||
oldElement?.parentNode.replaceChild(element, oldElement); |
||||
videoRef.current = element; |
||||
setPlaying(true); |
||||
setCanPlay(true); |
||||
setStartPlay(true); |
||||
videoRef?.current?.addEventListener("click", () => {}); |
||||
videoRef?.current?.addEventListener("timeupdate", updateProgress); |
||||
videoRef?.current?.addEventListener("ended", handleEnded); |
||||
} |
||||
} |
||||
}, [element]); |
||||
|
||||
return ( |
||||
<VideoContainer |
||||
tabIndex={0} |
||||
onKeyUp={keyboardShortcutsUp} |
||||
onKeyDown={keyboardShortcutsDown} |
||||
style={{ |
||||
padding: from === "create" ? "8px" : 0, |
||||
zIndex: 1000, |
||||
backgroundColor: theme.palette.background.default, |
||||
}} |
||||
> |
||||
<div className="closePlayer"> |
||||
<CloseIcon |
||||
onClick={() => { |
||||
dispatch(setVideoPlaying(null)); |
||||
}} |
||||
sx={{ |
||||
cursor: "pointer", |
||||
backgroundColor: "rgba(0,0,0,.5)", |
||||
}} |
||||
></CloseIcon> |
||||
</div> |
||||
<div onClick={togglePlay}> |
||||
<VideoElement id="videoPlayer" /> |
||||
</div> |
||||
<ControlsContainer |
||||
style={{ |
||||
bottom: from === "create" ? "15px" : 0, |
||||
}} |
||||
> |
||||
{isMobileView && canPlay ? ( |
||||
<> |
||||
<IconButton |
||||
sx={{ |
||||
color: "rgba(255, 255, 255, 0.7)", |
||||
}} |
||||
onClick={togglePlay} |
||||
> |
||||
{playing ? <Pause /> : <PlayArrow />} |
||||
</IconButton> |
||||
<IconButton |
||||
sx={{ |
||||
color: "rgba(255, 255, 255, 0.7)", |
||||
marginLeft: "15px", |
||||
}} |
||||
onClick={reloadVideo} |
||||
> |
||||
<Refresh /> |
||||
</IconButton> |
||||
<Slider |
||||
value={progress} |
||||
onChange={onProgressChange} |
||||
min={0} |
||||
max={videoRef.current?.duration || 100} |
||||
sx={{ flexGrow: 1, mx: 2 }} |
||||
/> |
||||
<IconButton |
||||
edge="end" |
||||
color="inherit" |
||||
aria-label="menu" |
||||
onClick={handleMenuOpen} |
||||
> |
||||
<MoreIcon /> |
||||
</IconButton> |
||||
<Menu |
||||
id="simple-menu" |
||||
anchorEl={anchorEl} |
||||
keepMounted |
||||
open={Boolean(anchorEl)} |
||||
onClose={handleMenuClose} |
||||
PaperProps={{ |
||||
style: { |
||||
width: "250px", |
||||
}, |
||||
}} |
||||
> |
||||
<MenuItem> |
||||
<VolumeUp /> |
||||
<Slider |
||||
value={volume} |
||||
onChange={onVolumeChange} |
||||
min={0} |
||||
max={1} |
||||
step={0.01} |
||||
/> |
||||
</MenuItem> |
||||
<MenuItem onClick={() => increaseSpeed()}> |
||||
<Typography |
||||
sx={{ |
||||
color: "rgba(255, 255, 255, 0.7)", |
||||
fontSize: "14px", |
||||
}} |
||||
> |
||||
Speed: {playbackRate}x |
||||
</Typography> |
||||
</MenuItem> |
||||
<MenuItem onClick={togglePictureInPicture}> |
||||
<PictureInPicture /> |
||||
</MenuItem> |
||||
<MenuItem onClick={toggleFullscreen}> |
||||
<Fullscreen /> |
||||
</MenuItem> |
||||
</Menu> |
||||
</> |
||||
) : canPlay ? ( |
||||
<> |
||||
<IconButton |
||||
sx={{ |
||||
color: "rgba(255, 255, 255, 0.7)", |
||||
}} |
||||
onClick={togglePlay} |
||||
> |
||||
{playing ? <Pause /> : <PlayArrow />} |
||||
</IconButton> |
||||
<IconButton |
||||
sx={{ |
||||
color: "rgba(255, 255, 255, 0.7)", |
||||
marginLeft: "15px", |
||||
}} |
||||
onClick={reloadVideo} |
||||
> |
||||
<Refresh /> |
||||
</IconButton> |
||||
<Slider |
||||
value={progress} |
||||
onChange={onProgressChange} |
||||
min={0} |
||||
max={videoRef.current?.duration || 100} |
||||
sx={{ flexGrow: 1, mx: 2 }} |
||||
/> |
||||
<Typography |
||||
sx={{ |
||||
fontSize: "14px", |
||||
marginRight: "5px", |
||||
color: "rgba(255, 255, 255, 0.7)", |
||||
visibility: |
||||
!videoRef.current?.duration || !progress |
||||
? "hidden" |
||||
: "visible", |
||||
}} |
||||
> |
||||
{progress && videoRef.current?.duration && formatTime(progress)}/ |
||||
{progress && |
||||
videoRef.current?.duration && |
||||
formatTime(videoRef.current?.duration)} |
||||
</Typography> |
||||
<IconButton |
||||
sx={{ |
||||
color: "rgba(255, 255, 255, 0.7)", |
||||
marginRight: "10px", |
||||
}} |
||||
onClick={toggleMute} |
||||
> |
||||
{isMuted ? <VolumeOff /> : <VolumeUp />} |
||||
</IconButton> |
||||
<Slider |
||||
value={volume} |
||||
onChange={onVolumeChange} |
||||
min={0} |
||||
max={1} |
||||
step={0.01} |
||||
sx={{ |
||||
maxWidth: "100px", |
||||
}} |
||||
/> |
||||
<IconButton |
||||
sx={{ |
||||
color: "rgba(255, 255, 255, 0.7)", |
||||
fontSize: "14px", |
||||
marginLeft: "5px", |
||||
}} |
||||
onClick={e => increaseSpeed()} |
||||
> |
||||
Speed: {playbackRate}x |
||||
</IconButton> |
||||
|
||||
<IconButton |
||||
sx={{ |
||||
color: "rgba(255, 255, 255, 0.7)", |
||||
marginLeft: "15px", |
||||
}} |
||||
ref={toggleRef} |
||||
onClick={togglePictureInPicture} |
||||
> |
||||
<PictureInPicture /> |
||||
</IconButton> |
||||
<IconButton |
||||
sx={{ |
||||
color: "rgba(255, 255, 255, 0.7)", |
||||
}} |
||||
onClick={toggleFullscreen} |
||||
> |
||||
<Fullscreen /> |
||||
</IconButton> |
||||
</> |
||||
) : null} |
||||
</ControlsContainer> |
||||
</VideoContainer> |
||||
); |
||||
}; |
@ -1,648 +0,0 @@
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react' |
||||
import ReactDOM from 'react-dom' |
||||
import { Box, IconButton, Slider, useTheme } from '@mui/material' |
||||
import { CircularProgress, Typography } from '@mui/material' |
||||
import { Key } from 'ts-key-enum' |
||||
import { |
||||
PlayArrow, |
||||
Pause, |
||||
VolumeUp, |
||||
Fullscreen, |
||||
PictureInPicture, VolumeOff |
||||
} from '@mui/icons-material' |
||||
import { styled } from '@mui/system' |
||||
import { MyContext } from '../../wrappers/DownloadWrapper' |
||||
import { useDispatch, useSelector } from 'react-redux' |
||||
import { RootState } from '../../state/store' |
||||
import { Refresh } from '@mui/icons-material' |
||||
import CloseIcon from '@mui/icons-material/Close'; |
||||
|
||||
import { Menu, MenuItem } from '@mui/material' |
||||
import { MoreVert as MoreIcon } from '@mui/icons-material' |
||||
import { setVideoPlaying } from '../../state/features/globalSlice' |
||||
const VideoContainer = styled(Box)` |
||||
position: relative; |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
justify-content: center; |
||||
width: 100%; |
||||
height: 100%; |
||||
margin: 0px; |
||||
padding: 0px; |
||||
` |
||||
|
||||
const VideoElement = styled('video')` |
||||
width: 100%; |
||||
height: auto; |
||||
max-height: calc(100vh - 150px); |
||||
background: rgb(33, 33, 33); |
||||
` |
||||
|
||||
const ControlsContainer = styled(Box)` |
||||
position: absolute; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: space-between; |
||||
bottom: 0; |
||||
left: 0; |
||||
right: 0; |
||||
padding: 8px; |
||||
background-color: rgba(0, 0, 0, 0.6); |
||||
` |
||||
|
||||
interface VideoPlayerProps { |
||||
src?: string |
||||
poster?: string |
||||
name?: string |
||||
identifier?: string |
||||
service?: string |
||||
autoplay?: boolean |
||||
from?: string | null |
||||
customStyle?: any |
||||
user?: string |
||||
jsonId?: string |
||||
element?: null | any |
||||
checkIfDrag?: ()=> boolean; |
||||
} |
||||
|
||||
export const VideoPlayerGlobal: React.FC<VideoPlayerProps> = ({ |
||||
poster, |
||||
name, |
||||
identifier, |
||||
service, |
||||
autoplay = true, |
||||
from = null, |
||||
customStyle = {}, |
||||
user = '', |
||||
jsonId = '', |
||||
element, |
||||
checkIfDrag |
||||
}) => { |
||||
const theme = useTheme() |
||||
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null) |
||||
const [playing, setPlaying] = useState(false) |
||||
const [volume, setVolume] = useState(1) |
||||
const [mutedVolume, setMutedVolume] = useState(1) |
||||
const [isMuted, setIsMuted] = useState(false) |
||||
const [progress, setProgress] = useState(0) |
||||
const [isLoading, setIsLoading] = useState(false) |
||||
const [canPlay, setCanPlay] = useState(false) |
||||
const [startPlay, setStartPlay] = useState(false) |
||||
const [isMobileView, setIsMobileView] = useState(false) |
||||
const [playbackRate, setPlaybackRate] = useState(1) |
||||
const [anchorEl, setAnchorEl] = useState(null) |
||||
const dispatch = useDispatch() |
||||
const reDownload = useRef<boolean>(false) |
||||
const { downloads } = useSelector((state: RootState) => state.global) |
||||
const download = useMemo(() => { |
||||
if (!downloads || !identifier) return {} |
||||
const findDownload = downloads[identifier] |
||||
|
||||
if (!findDownload) return {} |
||||
return findDownload |
||||
}, [downloads, identifier]) |
||||
|
||||
|
||||
const resourceStatus = useMemo(() => { |
||||
return download?.status || {} |
||||
}, [download]) |
||||
|
||||
const minSpeed = 0.25; |
||||
const maxSpeed = 4.0; |
||||
const speedChange = 0.25; |
||||
|
||||
const updatePlaybackRate = (newSpeed: number) => { |
||||
if (videoRef.current) { |
||||
if (newSpeed > maxSpeed || newSpeed < minSpeed) |
||||
newSpeed = minSpeed |
||||
videoRef.current.playbackRate = newSpeed |
||||
setPlaybackRate(newSpeed) |
||||
} |
||||
} |
||||
|
||||
const increaseSpeed = (wrapOverflow = true) => { |
||||
const changedSpeed = playbackRate + speedChange |
||||
let newSpeed = wrapOverflow ? changedSpeed : Math.min(changedSpeed, maxSpeed) |
||||
|
||||
|
||||
if (videoRef.current) { |
||||
updatePlaybackRate(newSpeed); |
||||
} |
||||
} |
||||
|
||||
const decreaseSpeed = () => { |
||||
if (videoRef.current) { |
||||
updatePlaybackRate(playbackRate - speedChange); |
||||
} |
||||
} |
||||
|
||||
|
||||
const toggleRef = useRef<any>(null) |
||||
const { downloadVideo } = useContext(MyContext) |
||||
const togglePlay = async () => { |
||||
|
||||
if(checkIfDrag && checkIfDrag()) return |
||||
if (!videoRef.current) return |
||||
if (playing) { |
||||
videoRef.current.pause() |
||||
} else { |
||||
videoRef.current.play() |
||||
} |
||||
setPlaying((prev)=> !prev) |
||||
} |
||||
|
||||
const onVolumeChange = (_: any, value: number | number[]) => { |
||||
if (!videoRef.current) return |
||||
videoRef.current.volume = value as number |
||||
setVolume(value as number) |
||||
setIsMuted(false) |
||||
} |
||||
|
||||
const onProgressChange = (_: any, value: number | number[]) => { |
||||
if (!videoRef.current) return |
||||
videoRef.current.currentTime = value as number |
||||
setProgress(value as number) |
||||
if (!playing) { |
||||
videoRef.current.play() |
||||
setPlaying(true) |
||||
} |
||||
} |
||||
|
||||
const handleEnded = () => { |
||||
setPlaying(false) |
||||
} |
||||
|
||||
const updateProgress = () => { |
||||
if (!videoRef.current) return |
||||
setProgress(videoRef.current.currentTime) |
||||
} |
||||
|
||||
const [isFullscreen, setIsFullscreen] = useState(false) |
||||
|
||||
const enterFullscreen = () => { |
||||
if (!videoRef.current) return |
||||
if (videoRef.current.requestFullscreen) { |
||||
videoRef.current.requestFullscreen() |
||||
} |
||||
} |
||||
|
||||
const exitFullscreen = () => { |
||||
if (document.exitFullscreen) { |
||||
document.exitFullscreen() |
||||
} |
||||
} |
||||
|
||||
const toggleFullscreen = () => { |
||||
isFullscreen ? exitFullscreen() : enterFullscreen() |
||||
} |
||||
const togglePictureInPicture = async () => { |
||||
if (!videoRef.current) return |
||||
if (document.pictureInPictureElement === videoRef.current) { |
||||
await document.exitPictureInPicture() |
||||
} else { |
||||
await videoRef.current.requestPictureInPicture() |
||||
} |
||||
} |
||||
|
||||
useEffect(() => { |
||||
const handleFullscreenChange = () => { |
||||
setIsFullscreen(!!document.fullscreenElement) |
||||
} |
||||
|
||||
document.addEventListener('fullscreenchange', handleFullscreenChange) |
||||
return () => { |
||||
document.removeEventListener('fullscreenchange', handleFullscreenChange) |
||||
} |
||||
}, []) |
||||
|
||||
|
||||
const handleCanPlay = () => { |
||||
setIsLoading(false) |
||||
setCanPlay(true) |
||||
} |
||||
|
||||
|
||||
|
||||
useEffect(() => { |
||||
const videoElement = videoRef.current |
||||
|
||||
const handleLeavePictureInPicture = async (event: any) => { |
||||
const target = event?.target |
||||
if (target) { |
||||
target.pause() |
||||
if (setPlaying) { |
||||
setPlaying(false) |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (videoElement) { |
||||
videoElement.addEventListener( |
||||
'leavepictureinpicture', |
||||
handleLeavePictureInPicture |
||||
) |
||||
} |
||||
|
||||
return () => { |
||||
if (videoElement) { |
||||
videoElement.removeEventListener( |
||||
'leavepictureinpicture', |
||||
handleLeavePictureInPicture |
||||
) |
||||
} |
||||
} |
||||
}, []) |
||||
|
||||
|
||||
|
||||
function formatTime(seconds: number): string { |
||||
seconds = Math.floor(seconds) |
||||
let minutes: number | string = Math.floor(seconds / 60) |
||||
let hours: number | string = Math.floor(minutes / 60) |
||||
|
||||
let remainingSeconds: number | string = seconds % 60 |
||||
let remainingMinutes: number | string = minutes % 60 |
||||
|
||||
if (remainingSeconds < 10) { |
||||
remainingSeconds = '0' + remainingSeconds |
||||
} |
||||
|
||||
if (remainingMinutes < 10) { |
||||
remainingMinutes = '0' + remainingMinutes |
||||
} |
||||
|
||||
if (hours === 0) { |
||||
hours = '' |
||||
} |
||||
else { |
||||
hours = hours + ':' |
||||
} |
||||
|
||||
return hours + remainingMinutes + ':' + remainingSeconds |
||||
} |
||||
|
||||
const reloadVideo = () => { |
||||
if (!videoRef.current) return |
||||
const src = videoRef.current.src |
||||
const currentTime = videoRef.current.currentTime |
||||
videoRef.current.src = src |
||||
videoRef.current.load() |
||||
videoRef.current.currentTime = currentTime |
||||
if (playing) { |
||||
videoRef.current.play() |
||||
} |
||||
} |
||||
|
||||
|
||||
const handleMenuOpen = (event: any) => { |
||||
setAnchorEl(event.currentTarget) |
||||
} |
||||
|
||||
|
||||
const handleMenuClose = () => { |
||||
setAnchorEl(null) |
||||
} |
||||
|
||||
useEffect(() => { |
||||
const videoWidth = videoRef?.current?.offsetWidth |
||||
if (videoWidth && videoWidth <= 600) { |
||||
setIsMobileView(true) |
||||
} |
||||
}, [canPlay]) |
||||
|
||||
const getDownloadProgress = (current: number, total: number) => { |
||||
const progress = current / total * 100; |
||||
return Number.isNaN(progress) ? '' : progress.toFixed(0) + '%' |
||||
} |
||||
const mute = () => { |
||||
setIsMuted(true) |
||||
setMutedVolume(volume) |
||||
setVolume(0) |
||||
if (videoRef.current) videoRef.current.volume = 0 |
||||
} |
||||
const unMute = () => { |
||||
setIsMuted(false) |
||||
setVolume(mutedVolume) |
||||
if (videoRef.current) videoRef.current.volume = mutedVolume |
||||
} |
||||
|
||||
const toggleMute = () => { |
||||
isMuted ? unMute() : mute(); |
||||
} |
||||
|
||||
const changeVolume = (volumeChange: number) => { |
||||
if (videoRef.current) { |
||||
const minVolume = 0; |
||||
const maxVolume = 1; |
||||
|
||||
|
||||
let newVolume = volumeChange + volume |
||||
|
||||
newVolume = Math.max(newVolume, minVolume) |
||||
newVolume = Math.min(newVolume, maxVolume) |
||||
|
||||
setIsMuted(false) |
||||
setMutedVolume(newVolume) |
||||
videoRef.current.volume = newVolume |
||||
setVolume(newVolume); |
||||
} |
||||
|
||||
} |
||||
const setProgressRelative = (secondsChange: number) => { |
||||
if (videoRef.current) { |
||||
const currentTime = videoRef.current?.currentTime |
||||
const minTime = 0 |
||||
const maxTime = videoRef.current?.duration || 100 |
||||
|
||||
let newTime = currentTime + secondsChange; |
||||
newTime = Math.max(newTime, minTime) |
||||
newTime = Math.min(newTime, maxTime) |
||||
videoRef.current.currentTime = newTime; |
||||
setProgress(newTime); |
||||
} |
||||
} |
||||
|
||||
const setProgressAbsolute = (videoPercent: number) => { |
||||
if (videoRef.current) { |
||||
videoPercent = Math.min(videoPercent, 100) |
||||
videoPercent = Math.max(videoPercent, 0) |
||||
const finalTime = videoRef.current?.duration * videoPercent / 100 |
||||
videoRef.current.currentTime = finalTime |
||||
setProgress(finalTime); |
||||
} |
||||
} |
||||
|
||||
|
||||
const keyboardShortcutsDown = (e: React.KeyboardEvent<HTMLDivElement>) => { |
||||
e.preventDefault() |
||||
|
||||
switch (e.key) { |
||||
case Key.Add: increaseSpeed(false); break; |
||||
case '+': increaseSpeed(false); break; |
||||
case '>': increaseSpeed(false); break; |
||||
|
||||
case Key.Subtract: decreaseSpeed(); break; |
||||
case '-': decreaseSpeed(); break; |
||||
case '<': decreaseSpeed(); break; |
||||
|
||||
case Key.ArrowLeft: { |
||||
if (e.shiftKey) setProgressRelative(-300); |
||||
else if (e.ctrlKey) setProgressRelative(-60); |
||||
else if (e.altKey) setProgressRelative(-10); |
||||
else setProgressRelative(-5); |
||||
} break; |
||||
|
||||
case Key.ArrowRight: { |
||||
if (e.shiftKey) setProgressRelative(300); |
||||
else if (e.ctrlKey) setProgressRelative(60); |
||||
else if (e.altKey) setProgressRelative(10); |
||||
else setProgressRelative(5); |
||||
} break; |
||||
|
||||
case Key.ArrowDown: changeVolume(-0.05); break; |
||||
case Key.ArrowUp: changeVolume(0.05); break; |
||||
} |
||||
} |
||||
|
||||
const keyboardShortcutsUp = (e: React.KeyboardEvent<HTMLDivElement>) => { |
||||
e.preventDefault() |
||||
|
||||
switch (e.key) { |
||||
case ' ': togglePlay(); break; |
||||
case 'm': toggleMute(); break; |
||||
|
||||
case 'f': enterFullscreen(); break; |
||||
case Key.Escape: exitFullscreen(); break; |
||||
|
||||
case '0': setProgressAbsolute(0); break; |
||||
case '1': setProgressAbsolute(10); break; |
||||
case '2': setProgressAbsolute(20); break; |
||||
case '3': setProgressAbsolute(30); break; |
||||
case '4': setProgressAbsolute(40); break; |
||||
case '5': setProgressAbsolute(50); break; |
||||
case '6': setProgressAbsolute(60); break; |
||||
case '7': setProgressAbsolute(70); break; |
||||
case '8': setProgressAbsolute(80); break; |
||||
case '9': setProgressAbsolute(90); break; |
||||
} |
||||
} |
||||
|
||||
useEffect(()=> { |
||||
if(element){ |
||||
let oldElement = document.getElementById('videoPlayer'); |
||||
if(oldElement && oldElement?.parentNode){ |
||||
oldElement?.parentNode.replaceChild(element, oldElement); |
||||
videoRef.current = element |
||||
setPlaying(true) |
||||
setCanPlay(true) |
||||
setStartPlay(true) |
||||
videoRef?.current?.addEventListener('click', ()=> {}) |
||||
videoRef?.current?.addEventListener('timeupdate', updateProgress) |
||||
videoRef?.current?.addEventListener('ended', handleEnded) |
||||
|
||||
} |
||||
|
||||
} |
||||
}, [element]) |
||||
|
||||
return ( |
||||
<VideoContainer |
||||
tabIndex={0} |
||||
onKeyUp={keyboardShortcutsUp} |
||||
onKeyDown={keyboardShortcutsDown} |
||||
style={{ |
||||
padding: from === 'create' ? '8px' : 0, |
||||
zIndex: 1000, |
||||
backgroundColor: theme.palette.background.default, |
||||
}} |
||||
> |
||||
<div className="closePlayer"> |
||||
|
||||
<CloseIcon onClick={()=> { |
||||
dispatch(setVideoPlaying(null)) |
||||
}} sx={{ |
||||
cursor: 'pointer', |
||||
backgroundColor: 'rgba(0,0,0,.5)' |
||||
}}></CloseIcon> |
||||
</div> |
||||
<div onClick={togglePlay}> |
||||
<VideoElement |
||||
id="videoPlayer" |
||||
/> |
||||
</div> |
||||
<ControlsContainer |
||||
style={{ |
||||
bottom: from === 'create' ? '15px' : 0 |
||||
}} |
||||
> |
||||
{isMobileView && canPlay ? ( |
||||
<> |
||||
<IconButton |
||||
sx={{ |
||||
color: 'rgba(255, 255, 255, 0.7)' |
||||
}} |
||||
onClick={togglePlay} |
||||
> |
||||
{playing ? <Pause /> : <PlayArrow />} |
||||
</IconButton> |
||||
<IconButton |
||||
sx={{ |
||||
color: 'rgba(255, 255, 255, 0.7)', |
||||
marginLeft: '15px' |
||||
}} |
||||
onClick={reloadVideo} |
||||
> |
||||
<Refresh /> |
||||
</IconButton> |
||||
<Slider |
||||
value={progress} |
||||
onChange={onProgressChange} |
||||
min={0} |
||||
max={videoRef.current?.duration || 100} |
||||
sx={{ flexGrow: 1, mx: 2 }} |
||||
/> |
||||
<IconButton |
||||
edge="end" |
||||
color="inherit" |
||||
aria-label="menu" |
||||
onClick={handleMenuOpen} |
||||
> |
||||
<MoreIcon /> |
||||
</IconButton> |
||||
<Menu |
||||
id="simple-menu" |
||||
anchorEl={anchorEl} |
||||
keepMounted |
||||
open={Boolean(anchorEl)} |
||||
onClose={handleMenuClose} |
||||
PaperProps={{ |
||||
style: { |
||||
width: '250px' |
||||
} |
||||
}} |
||||
> |
||||
<MenuItem> |
||||
<VolumeUp /> |
||||
<Slider |
||||
value={volume} |
||||
onChange={onVolumeChange} |
||||
min={0} |
||||
max={1} |
||||
step={0.01} /> |
||||
</MenuItem> |
||||
<MenuItem onClick={() => increaseSpeed()}> |
||||
<Typography |
||||
sx={{ |
||||
color: 'rgba(255, 255, 255, 0.7)', |
||||
fontSize: '14px' |
||||
}} |
||||
> |
||||
Speed: {playbackRate}x |
||||
</Typography> |
||||
</MenuItem> |
||||
<MenuItem onClick={togglePictureInPicture}> |
||||
<PictureInPicture /> |
||||
</MenuItem> |
||||
<MenuItem onClick={toggleFullscreen}> |
||||
<Fullscreen /> |
||||
</MenuItem> |
||||
</Menu> |
||||
</> |
||||
) : canPlay ? ( |
||||
<> |
||||
<IconButton |
||||
sx={{ |
||||
color: 'rgba(255, 255, 255, 0.7)' |
||||
}} |
||||
onClick={togglePlay} |
||||
> |
||||
{playing ? <Pause /> : <PlayArrow />} |
||||
</IconButton> |
||||
<IconButton |
||||
sx={{ |
||||
color: 'rgba(255, 255, 255, 0.7)', |
||||
marginLeft: '15px' |
||||
}} |
||||
onClick={reloadVideo} |
||||
> |
||||
<Refresh /> |
||||
</IconButton> |
||||
<Slider |
||||
value={progress} |
||||
onChange={onProgressChange} |
||||
min={0} |
||||
max={videoRef.current?.duration || 100} |
||||
sx={{ flexGrow: 1, mx: 2 }} |
||||
/> |
||||
<Typography |
||||
sx={{ |
||||
fontSize: '14px', |
||||
marginRight: '5px', |
||||
color: 'rgba(255, 255, 255, 0.7)', |
||||
visibility: |
||||
!videoRef.current?.duration || !progress |
||||
? 'hidden' |
||||
: 'visible' |
||||
}} |
||||
> |
||||
{progress && videoRef.current?.duration && formatTime(progress)}/ |
||||
{progress && |
||||
videoRef.current?.duration && |
||||
formatTime(videoRef.current?.duration)} |
||||
</Typography> |
||||
<IconButton |
||||
sx={{ |
||||
color: 'rgba(255, 255, 255, 0.7)', |
||||
marginRight: '10px' |
||||
}} |
||||
onClick={toggleMute} |
||||
> |
||||
{isMuted ? <VolumeOff /> : <VolumeUp />} |
||||
</IconButton> |
||||
<Slider |
||||
value={volume} |
||||
onChange={onVolumeChange} |
||||
min={0} |
||||
max={1} |
||||
step={0.01} |
||||
sx={{ |
||||
maxWidth: '100px' |
||||
}} |
||||
/> |
||||
<IconButton |
||||
sx={{ |
||||
color: 'rgba(255, 255, 255, 0.7)', |
||||
fontSize: '14px', |
||||
marginLeft: '5px' |
||||
}} |
||||
onClick={(e) => increaseSpeed()} |
||||
> |
||||
Speed: {playbackRate}x |
||||
</IconButton> |
||||
|
||||
<IconButton |
||||
sx={{ |
||||
color: 'rgba(255, 255, 255, 0.7)', |
||||
marginLeft: '15px' |
||||
}} |
||||
ref={toggleRef} |
||||
onClick={togglePictureInPicture} |
||||
> |
||||
<PictureInPicture /> |
||||
</IconButton> |
||||
<IconButton |
||||
sx={{ |
||||
color: 'rgba(255, 255, 255, 0.7)' |
||||
}} |
||||
onClick={toggleFullscreen} |
||||
> |
||||
<Fullscreen /> |
||||
</IconButton> |
||||
</> |
||||
) : null} |
||||
</ControlsContainer> |
||||
</VideoContainer> |
||||
) |
||||
} |
@ -1,4 +1,6 @@
|
||||
export const minPriceSuperlike = 10; |
||||
export const titleFormatter = /[^a-zA-Z0-9\s-_!?()&'",.;:|—~@#$%^*+=<>]/g; |
||||
export const titleFormatterOnSave = /[^a-zA-Z0-9\s-_!()&',.;—~@#$%^+=]/g; |
||||
|
||||
export const titleSaveFormatter = /[^a-zA-Z0-9\s-_!()&',.;—~@#$%^+=]/g; |
||||
export const allTabValue = "all"; |
||||
export const subscriptionTabValue = "subscriptions"; |
||||
|
@ -1,77 +1,77 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react' |
||||
import { useNavigate } from 'react-router-dom' |
||||
import { useSelector } from 'react-redux' |
||||
import { RootState } from '../../state/store' |
||||
import React, { useCallback, useEffect, useRef, useState } from "react"; |
||||
import { useNavigate } from "react-router-dom"; |
||||
import { useSelector } from "react-redux"; |
||||
import { RootState } from "../../state/store"; |
||||
import { Box, useTheme } from "@mui/material"; |
||||
import { |
||||
Avatar, |
||||
Box, |
||||
Button, |
||||
Typography, |
||||
useTheme |
||||
} from '@mui/material' |
||||
import { useFetchVideos } from '../../hooks/useFetchVideos' |
||||
import LazyLoad from '../../components/common/LazyLoad' |
||||
import { BottomParent, NameContainer, VideoCard, VideoCardName, VideoCardTitle, VideoContainer, VideoUploadDate } from './VideoList-styles' |
||||
import ResponsiveImage from '../../components/ResponsiveImage' |
||||
import { formatDate, formatTimestampSeconds } from '../../utils/time' |
||||
import { ChannelCard, ChannelTitle } from './Home-styles' |
||||
BottomParent, |
||||
NameContainer, |
||||
VideoCard, |
||||
VideoCardName, |
||||
VideoCardTitle, |
||||
VideoContainer, |
||||
VideoUploadDate, |
||||
} from "./VideoList-styles"; |
||||
import ResponsiveImage from "../../components/ResponsiveImage"; |
||||
import { formatDate, formatTimestampSeconds } from "../../utils/time"; |
||||
import { ChannelCard, ChannelTitle } from "./Home-styles"; |
||||
|
||||
interface VideoListProps { |
||||
mode?: string |
||||
mode?: string; |
||||
} |
||||
export const Channels = ({ mode }: VideoListProps) => { |
||||
const theme = useTheme() |
||||
const navigate = useNavigate() |
||||
const publishNames = useSelector((state: RootState)=> state.global.publishNames) |
||||
const theme = useTheme(); |
||||
const navigate = useNavigate(); |
||||
const publishNames = useSelector( |
||||
(state: RootState) => state.global.publishNames |
||||
); |
||||
const userAvatarHash = useSelector( |
||||
(state: RootState) => state.global.userAvatarHash |
||||
) |
||||
|
||||
|
||||
|
||||
); |
||||
|
||||
return ( |
||||
<Box sx={{ |
||||
width: '100%', |
||||
display: 'flex', |
||||
flexDirection: 'column', |
||||
alignItems: 'center', |
||||
minHeight: '50vh' |
||||
}}> |
||||
<VideoContainer> |
||||
{publishNames && publishNames?.slice(0, 10).map((name)=> { |
||||
let avatarUrl = '' |
||||
if(userAvatarHash[name]){ |
||||
avatarUrl = userAvatarHash[name] |
||||
} |
||||
return ( |
||||
<Box |
||||
<Box |
||||
sx={{ |
||||
display: 'flex', |
||||
flex: 0, |
||||
alignItems: 'center', |
||||
width: 'auto', |
||||
position: 'relative', |
||||
' @media (max-width: 450px)': { |
||||
width: '100%' |
||||
} |
||||
width: "100%", |
||||
display: "flex", |
||||
flexDirection: "column", |
||||
alignItems: "center", |
||||
minHeight: "50vh", |
||||
}} |
||||
key={name} |
||||
> |
||||
<ChannelCard |
||||
<VideoContainer> |
||||
{publishNames && |
||||
publishNames?.slice(0, 10).map(name => { |
||||
let avatarUrl = ""; |
||||
if (userAvatarHash[name]) { |
||||
avatarUrl = userAvatarHash[name]; |
||||
} |
||||
return ( |
||||
<Box |
||||
sx={{ |
||||
display: "flex", |
||||
flex: 0, |
||||
alignItems: "center", |
||||
width: "auto", |
||||
position: "relative", |
||||
" @media (max-width: 450px)": { |
||||
width: "100%", |
||||
}, |
||||
}} |
||||
key={name} |
||||
> |
||||
<ChannelCard |
||||
onClick={() => { |
||||
navigate(`/channel/${name}`) |
||||
navigate(`/channel/${name}`); |
||||
}} |
||||
> |
||||
<ChannelTitle>{name}</ChannelTitle> |
||||
<ResponsiveImage src={avatarUrl} width={50} height={50}/> |
||||
</ChannelCard> |
||||
</Box> |
||||
) |
||||
})} |
||||
</VideoContainer> |
||||
> |
||||
<ChannelTitle>{name}</ChannelTitle> |
||||
<ResponsiveImage src={avatarUrl} width={50} height={50} /> |
||||
</ChannelCard> |
||||
</Box> |
||||
); |
||||
})} |
||||
</VideoContainer> |
||||
</Box> |
||||
) |
||||
} |
||||
|
||||
|
||||
); |
||||
}; |
||||
|
@ -1,15 +1,540 @@
|
||||
import React from 'react' |
||||
import { VideoList } from './VideoList' |
||||
import React, { useEffect, useRef, useState } from "react"; |
||||
import ReactDOM from "react-dom"; |
||||
import { useSelector, useDispatch } from "react-redux"; |
||||
import { RootState } from "../../state/store"; |
||||
import { |
||||
Box, |
||||
Button, |
||||
FormControl, |
||||
Grid, |
||||
Input, |
||||
InputLabel, |
||||
MenuItem, |
||||
OutlinedInput, |
||||
Select, |
||||
SelectChangeEvent, |
||||
Tab, |
||||
useTheme, |
||||
} from "@mui/material"; |
||||
import { useFetchVideos } from "../../hooks/useFetchVideos"; |
||||
import LazyLoad from "../../components/common/LazyLoad"; |
||||
import { |
||||
FiltersCol, |
||||
FiltersContainer, |
||||
FiltersRow, |
||||
FiltersSubContainer, |
||||
ProductManagerRow, |
||||
FiltersRadioButton, |
||||
} from "./VideoList-styles"; |
||||
import { SubtitleContainer } from "./Home-styles"; |
||||
|
||||
import { useSelector } from 'react-redux' |
||||
import { RootState } from '../../state/store' |
||||
import { |
||||
changeSelectedCategoryVideos, |
||||
changeSelectedSubCategoryVideos, |
||||
changefilterName, |
||||
changefilterSearch, |
||||
} from "../../state/features/videoSlice"; |
||||
import { changeFilterType } from "../../state/features/persistSlice.ts"; |
||||
import { categories, subCategories } from "../../constants/Categories.ts"; |
||||
import { ListSuperLikeContainer } from "../../components/common/ListSuperLikes/ListSuperLikeContainer.tsx"; |
||||
import { TabContext, TabList, TabPanel } from "@mui/lab"; |
||||
import VideoList from "./VideoList.tsx"; |
||||
import { allTabValue, subscriptionTabValue } from "../../constants/Misc.ts"; |
||||
import { setHomePageSelectedTab } from "../../state/features/persistSlice.ts"; |
||||
|
||||
export const Home = () => { |
||||
interface HomeProps { |
||||
mode?: string; |
||||
} |
||||
export const Home = ({ mode }: HomeProps) => { |
||||
const prevVal = useRef(""); |
||||
const isFiltering = useSelector( |
||||
(state: RootState) => state.video.isFiltering |
||||
); |
||||
const filterValue = useSelector( |
||||
(state: RootState) => state.video.filterValue |
||||
); |
||||
const persistSelector = useSelector((state: RootState) => state.persist); |
||||
const filterType = useSelector( |
||||
(state: RootState) => state.persist.filterType |
||||
); |
||||
const filterSearch = useSelector( |
||||
(state: RootState) => state.video.filterSearch |
||||
); |
||||
const filterName = useSelector((state: RootState) => state.video.filterName); |
||||
const selectedCategoryVideos = useSelector( |
||||
(state: RootState) => state.video.selectedCategoryVideos |
||||
); |
||||
|
||||
const { videos: globalVideos } = useSelector( |
||||
(state: RootState) => state.video |
||||
); |
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false); |
||||
const [tabValue, setTabValue] = useState<string>(persistSelector.selectedTab); |
||||
|
||||
const tabFontSize = "20px"; |
||||
|
||||
const setFilterType = payload => { |
||||
dispatch(changeFilterType(payload)); |
||||
}; |
||||
|
||||
useEffect(() => { |
||||
// Makes displayed videos reload when switching filter type. Removes need to click Search button after changing type
|
||||
getVideosHandler(true); |
||||
}, [filterType]); |
||||
const setFilterSearch = payload => { |
||||
dispatch(changefilterSearch(payload)); |
||||
}; |
||||
|
||||
const setFilterName = payload => { |
||||
dispatch(changefilterName(payload)); |
||||
}; |
||||
|
||||
const setSelectedCategoryVideos = payload => { |
||||
dispatch(changeSelectedCategoryVideos(payload)); |
||||
}; |
||||
const selectedSubCategoryVideos = useSelector( |
||||
(state: RootState) => state.video.selectedSubCategoryVideos |
||||
); |
||||
|
||||
const setSelectedSubCategoryVideos = payload => { |
||||
dispatch(changeSelectedSubCategoryVideos(payload)); |
||||
}; |
||||
|
||||
const dispatch = useDispatch(); |
||||
const filteredVideos = useSelector( |
||||
(state: RootState) => state.video.filteredVideos |
||||
); |
||||
|
||||
const isFilterMode = useRef(false); |
||||
const firstFetch = useRef(false); |
||||
const afterFetch = useRef(false); |
||||
const isFetching = useRef(false); |
||||
|
||||
const { getVideos, getNewVideos, checkNewVideos, getVideosFiltered } = |
||||
useFetchVideos(); |
||||
|
||||
const getVideosHandler = React.useCallback( |
||||
async (reset?: boolean, resetFilters?: boolean) => { |
||||
if (!firstFetch.current || !afterFetch.current) return; |
||||
if (isFetching.current) return; |
||||
isFetching.current = true; |
||||
await getVideos( |
||||
{ |
||||
name: filterName, |
||||
category: selectedCategoryVideos?.id, |
||||
subcategory: selectedSubCategoryVideos?.id, |
||||
keywords: filterSearch, |
||||
type: filterType, |
||||
}, |
||||
reset, |
||||
resetFilters, |
||||
20, |
||||
tabValue |
||||
); |
||||
isFetching.current = false; |
||||
}, |
||||
[ |
||||
getVideos, |
||||
filterValue, |
||||
getVideosFiltered, |
||||
isFiltering, |
||||
filterName, |
||||
selectedCategoryVideos, |
||||
selectedSubCategoryVideos, |
||||
filterSearch, |
||||
filterType, |
||||
tabValue, |
||||
] |
||||
); |
||||
|
||||
useEffect(() => { |
||||
if (isFiltering && filterValue !== prevVal?.current) { |
||||
prevVal.current = filterValue; |
||||
getVideosHandler(); |
||||
} |
||||
}, [filterValue, isFiltering, filteredVideos]); |
||||
|
||||
const getVideosHandlerMount = React.useCallback(async () => { |
||||
if (firstFetch.current) return; |
||||
firstFetch.current = true; |
||||
setIsLoading(true); |
||||
|
||||
await getVideos({ type: filterType }, null, null, 20, tabValue); |
||||
afterFetch.current = true; |
||||
isFetching.current = false; |
||||
|
||||
setIsLoading(false); |
||||
}, [getVideos]); |
||||
|
||||
let videos = globalVideos; |
||||
|
||||
if (isFiltering) { |
||||
videos = filteredVideos; |
||||
isFilterMode.current = true; |
||||
} else { |
||||
isFilterMode.current = false; |
||||
} |
||||
|
||||
// const interval = useRef<any>(null);
|
||||
|
||||
// const checkNewVideosFunc = useCallback(() => {
|
||||
// let isCalling = false;
|
||||
// interval.current = setInterval(async () => {
|
||||
// if (isCalling || !firstFetch.current) return;
|
||||
// isCalling = true;
|
||||
// await checkNewVideos();
|
||||
// isCalling = false;
|
||||
// }, 30000); // 1 second interval
|
||||
// }, [checkNewVideos]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (isFiltering && interval.current) {
|
||||
// clearInterval(interval.current);
|
||||
// return;
|
||||
// }
|
||||
// checkNewVideosFunc();
|
||||
|
||||
// return () => {
|
||||
// if (interval?.current) {
|
||||
// clearInterval(interval.current);
|
||||
// }
|
||||
// };
|
||||
// }, [mode, checkNewVideosFunc, isFiltering]);
|
||||
|
||||
useEffect(() => { |
||||
if ( |
||||
!firstFetch.current && |
||||
!isFilterMode.current && |
||||
globalVideos.length === 0 |
||||
) { |
||||
isFetching.current = true; |
||||
getVideosHandlerMount(); |
||||
} else { |
||||
firstFetch.current = true; |
||||
afterFetch.current = true; |
||||
} |
||||
}, [getVideosHandlerMount, globalVideos]); |
||||
|
||||
const filtersToDefault = async () => { |
||||
setFilterType("videos"); |
||||
setFilterSearch(""); |
||||
setFilterName(""); |
||||
setSelectedCategoryVideos(null); |
||||
setSelectedSubCategoryVideos(null); |
||||
|
||||
ReactDOM.flushSync(() => { |
||||
getVideosHandler(true, true); |
||||
}); |
||||
}; |
||||
|
||||
const handleOptionCategoryChangeVideos = ( |
||||
event: SelectChangeEvent<string> |
||||
) => { |
||||
const optionId = event.target.value; |
||||
const selectedOption = categories.find(option => option.id === +optionId); |
||||
setSelectedCategoryVideos(selectedOption || null); |
||||
}; |
||||
const handleOptionSubCategoryChangeVideos = ( |
||||
event: SelectChangeEvent<string>, |
||||
subcategories: any[] |
||||
) => { |
||||
const optionId = event.target.value; |
||||
const selectedOption = subcategories.find( |
||||
option => option.id === +optionId |
||||
); |
||||
setSelectedSubCategoryVideos(selectedOption || null); |
||||
}; |
||||
|
||||
const handleInputKeyDown = (event: any) => { |
||||
if (event.key === "Enter") { |
||||
getVideosHandler(true); |
||||
} |
||||
}; |
||||
|
||||
useEffect(() => { |
||||
getVideosHandler(true); |
||||
}, [tabValue]); |
||||
|
||||
const changeTab = (e: React.SyntheticEvent, newValue: string) => { |
||||
setTabValue(newValue); |
||||
dispatch(setHomePageSelectedTab(newValue)); |
||||
}; |
||||
|
||||
return ( |
||||
<> |
||||
<VideoList /> |
||||
</> |
||||
|
||||
) |
||||
} |
||||
<Grid container sx={{ width: "100%" }}> |
||||
<FiltersCol item xs={12} md={2} lg={2} xl={2} sm={3}> |
||||
<FiltersContainer> |
||||
<Input |
||||
id="standard-adornment-name" |
||||
onChange={e => { |
||||
setFilterSearch(e.target.value); |
||||
}} |
||||
value={filterSearch} |
||||
placeholder="Search" |
||||
onKeyDown={handleInputKeyDown} |
||||
sx={{ |
||||
borderBottom: "1px solid white", |
||||
"&&:before": { |
||||
borderBottom: "none", |
||||
}, |
||||
"&&:after": { |
||||
borderBottom: "none", |
||||
}, |
||||
"&&:hover:before": { |
||||
borderBottom: "none", |
||||
}, |
||||
"&&.Mui-focused:before": { |
||||
borderBottom: "none", |
||||
}, |
||||
"&&.Mui-focused": { |
||||
outline: "none", |
||||
}, |
||||
fontSize: "18px", |
||||
}} |
||||
/> |
||||
<Input |
||||
id="standard-adornment-name" |
||||
onChange={e => { |
||||
setFilterName(e.target.value); |
||||
}} |
||||
value={filterName} |
||||
placeholder="User's Name (Exact)" |
||||
onKeyDown={handleInputKeyDown} |
||||
sx={{ |
||||
marginTop: "20px", |
||||
borderBottom: "1px solid white", |
||||
"&&:before": { |
||||
borderBottom: "none", |
||||
}, |
||||
"&&:after": { |
||||
borderBottom: "none", |
||||
}, |
||||
"&&:hover:before": { |
||||
borderBottom: "none", |
||||
}, |
||||
"&&.Mui-focused:before": { |
||||
borderBottom: "none", |
||||
}, |
||||
"&&.Mui-focused": { |
||||
outline: "none", |
||||
}, |
||||
fontSize: "18px", |
||||
}} |
||||
/> |
||||
|
||||
<FiltersSubContainer> |
||||
<FormControl sx={{ width: "100%", marginTop: "30px" }}> |
||||
<Box |
||||
sx={{ |
||||
display: "flex", |
||||
gap: "20px", |
||||
alignItems: "center", |
||||
flexDirection: "column", |
||||
}} |
||||
> |
||||
<FormControl fullWidth sx={{ marginBottom: 1 }}> |
||||
<InputLabel |
||||
sx={{ |
||||
fontSize: "16px", |
||||
}} |
||||
id="Category" |
||||
> |
||||
Category |
||||
</InputLabel> |
||||
<Select |
||||
labelId="Category" |
||||
input={<OutlinedInput label="Category" />} |
||||
value={selectedCategoryVideos?.id || ""} |
||||
onChange={handleOptionCategoryChangeVideos} |
||||
sx={{ |
||||
// Target the input field
|
||||
".MuiSelect-select": { |
||||
fontSize: "16px", // Change font size for the selected value
|
||||
padding: "10px 5px 15px 15px;", |
||||
}, |
||||
// Target the dropdown icon
|
||||
".MuiSelect-icon": { |
||||
fontSize: "20px", // Adjust if needed
|
||||
}, |
||||
// Target the dropdown menu
|
||||
"& .MuiMenu-paper": { |
||||
".MuiMenuItem-root": { |
||||
fontSize: "14px", // Change font size for the menu items
|
||||
}, |
||||
}, |
||||
}} |
||||
> |
||||
{categories.map(option => ( |
||||
<MenuItem key={option.id} value={option.id}> |
||||
{option.name} |
||||
</MenuItem> |
||||
))} |
||||
</Select> |
||||
</FormControl> |
||||
{selectedCategoryVideos && |
||||
subCategories[selectedCategoryVideos?.id] && ( |
||||
<FormControl fullWidth sx={{ marginBottom: 2 }}> |
||||
<InputLabel |
||||
sx={{ |
||||
fontSize: "16px", |
||||
}} |
||||
id="Sub-Category" |
||||
> |
||||
Sub-Category |
||||
</InputLabel> |
||||
<Select |
||||
labelId="Sub-Category" |
||||
input={<OutlinedInput label="Sub-Category" />} |
||||
value={selectedSubCategoryVideos?.id || ""} |
||||
onChange={e => |
||||
handleOptionSubCategoryChangeVideos( |
||||
e, |
||||
subCategories[selectedCategoryVideos?.id] |
||||
) |
||||
} |
||||
sx={{ |
||||
// Target the input field
|
||||
".MuiSelect-select": { |
||||
fontSize: "16px", // Change font size for the selected value
|
||||
padding: "10px 5px 15px 15px;", |
||||
}, |
||||
// Target the dropdown icon
|
||||
".MuiSelect-icon": { |
||||
fontSize: "20px", // Adjust if needed
|
||||
}, |
||||
// Target the dropdown menu
|
||||
"& .MuiMenu-paper": { |
||||
".MuiMenuItem-root": { |
||||
fontSize: "14px", // Change font size for the menu items
|
||||
}, |
||||
}, |
||||
}} |
||||
> |
||||
{subCategories[selectedCategoryVideos.id].map( |
||||
option => ( |
||||
<MenuItem key={option.id} value={option.id}> |
||||
{option.name} |
||||
</MenuItem> |
||||
) |
||||
)} |
||||
</Select> |
||||
</FormControl> |
||||
)} |
||||
</Box> |
||||
</FormControl> |
||||
</FiltersSubContainer> |
||||
<FiltersSubContainer> |
||||
<FiltersRow> |
||||
Videos |
||||
<FiltersRadioButton |
||||
checked={filterType === "videos"} |
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { |
||||
setFilterType("videos"); |
||||
}} |
||||
inputProps={{ "aria-label": "controlled" }} |
||||
/> |
||||
</FiltersRow> |
||||
<FiltersRow> |
||||
Playlists |
||||
<FiltersRadioButton |
||||
checked={filterType === "playlists"} |
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { |
||||
setFilterType("playlists"); |
||||
}} |
||||
inputProps={{ "aria-label": "controlled" }} |
||||
/> |
||||
</FiltersRow> |
||||
</FiltersSubContainer> |
||||
<Button |
||||
onClick={() => { |
||||
filtersToDefault(); |
||||
}} |
||||
sx={{ |
||||
marginTop: "20px", |
||||
}} |
||||
variant="contained" |
||||
> |
||||
reset |
||||
</Button> |
||||
<Button |
||||
onClick={() => { |
||||
getVideosHandler(true); |
||||
}} |
||||
sx={{ |
||||
marginTop: "20px", |
||||
}} |
||||
variant="contained" |
||||
> |
||||
Search |
||||
</Button> |
||||
</FiltersContainer> |
||||
</FiltersCol> |
||||
<Grid item xs={12} md={10} lg={7} xl={8} sm={9}> |
||||
<ProductManagerRow> |
||||
<Box |
||||
sx={{ |
||||
width: "100%", |
||||
display: "flex", |
||||
flexDirection: "column", |
||||
alignItems: "center", |
||||
marginTop: "20px", |
||||
}} |
||||
> |
||||
<SubtitleContainer |
||||
sx={{ |
||||
justifyContent: "flex-start", |
||||
paddingLeft: "15px", |
||||
width: "100%", |
||||
maxWidth: "1400px", |
||||
}} |
||||
></SubtitleContainer> |
||||
<TabContext value={tabValue}> |
||||
<TabList |
||||
onChange={changeTab} |
||||
textColor={"secondary"} |
||||
indicatorColor={"secondary"} |
||||
> |
||||
<Tab |
||||
label="All Videos" |
||||
value={allTabValue} |
||||
sx={{ fontSize: tabFontSize }} |
||||
/> |
||||
<Tab |
||||
label="Subscriptions" |
||||
value={subscriptionTabValue} |
||||
sx={{ fontSize: tabFontSize }} |
||||
/> |
||||
</TabList> |
||||
<TabPanel value={allTabValue} sx={{ width: "100%" }}> |
||||
<VideoList videos={videos} /> |
||||
<LazyLoad |
||||
onLoadMore={getVideosHandler} |
||||
isLoading={isLoading} |
||||
></LazyLoad> |
||||
</TabPanel> |
||||
<TabPanel value={subscriptionTabValue} sx={{ width: "100%" }}> |
||||
{persistSelector.subscriptionList.length > 0 ? ( |
||||
<> |
||||
<VideoList videos={videos} /> |
||||
<LazyLoad |
||||
onLoadMore={getVideosHandler} |
||||
isLoading={isLoading} |
||||
></LazyLoad> |
||||
</> |
||||
) : ( |
||||
<div style={{ textAlign: "center" }}> |
||||
You have no subscriptions |
||||
</div> |
||||
)} |
||||
</TabPanel> |
||||
</TabContext> |
||||
</Box> |
||||
</ProductManagerRow> |
||||
</Grid> |
||||
<FiltersCol item xs={0} lg={3} xl={2}> |
||||
<ListSuperLikeContainer /> |
||||
</FiltersCol> |
||||
</Grid> |
||||
); |
||||
}; |
||||
|
@ -0,0 +1,59 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; |
||||
import { subscriptionTabValue } from "../../constants/Misc.ts"; |
||||
|
||||
type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down"; |
||||
interface settingsState { |
||||
selectedTab: string; |
||||
stretchVideoSetting: StretchVideoType; |
||||
filterType: string; |
||||
subscriptionList: string[]; |
||||
playbackRate: number; |
||||
} |
||||
|
||||
const initialState: settingsState = { |
||||
selectedTab: subscriptionTabValue, |
||||
stretchVideoSetting: "contain", |
||||
filterType: "videos", |
||||
subscriptionList: [], |
||||
playbackRate: 1, |
||||
}; |
||||
|
||||
export const persistSlice = createSlice({ |
||||
name: "persist", |
||||
initialState, |
||||
reducers: { |
||||
setHomePageSelectedTab: (state, action) => { |
||||
state.selectedTab = action.payload; |
||||
}, |
||||
setStretchVideoSetting: (state, action) => { |
||||
state.stretchVideoSetting = action.payload; |
||||
}, |
||||
subscribe: (state, action: PayloadAction<string>) => { |
||||
const currentSubscriptions = state.subscriptionList; |
||||
if (!currentSubscriptions.includes(action.payload)) { |
||||
state.subscriptionList = [...currentSubscriptions, action.payload]; |
||||
} |
||||
}, |
||||
unSubscribe: (state, action) => { |
||||
state.subscriptionList = state.subscriptionList.filter( |
||||
item => item !== action.payload |
||||
); |
||||
}, |
||||
setReduxPlaybackRate: (state, action) => { |
||||
state.playbackRate = action.payload; |
||||
}, |
||||
changeFilterType: (state, action) => { |
||||
state.filterType = action.payload; |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
export const { |
||||
setHomePageSelectedTab, |
||||
subscribe, |
||||
unSubscribe, |
||||
setReduxPlaybackRate, |
||||
changeFilterType, |
||||
} = persistSlice.actions; |
||||
|
||||
export default persistSlice.reducer; |
@ -1,27 +1,47 @@
|
||||
import { configureStore } from '@reduxjs/toolkit' |
||||
import notificationsReducer from './features/notificationsSlice' |
||||
import authReducer from './features/authSlice' |
||||
import globalReducer from './features/globalSlice' |
||||
import videoReducer from './features/videoSlice' |
||||
import { combineReducers, configureStore } from "@reduxjs/toolkit"; |
||||
import notificationsReducer from "./features/notificationsSlice"; |
||||
import authReducer from "./features/authSlice"; |
||||
import globalReducer from "./features/globalSlice"; |
||||
import videoReducer from "./features/videoSlice"; |
||||
import settingsReducer from "./features/persistSlice.ts"; |
||||
import { |
||||
persistReducer, |
||||
FLUSH, |
||||
REHYDRATE, |
||||
PAUSE, |
||||
PERSIST, |
||||
PURGE, |
||||
REGISTER, |
||||
} from "redux-persist"; |
||||
import storage from "redux-persist/lib/storage"; |
||||
|
||||
const persistSettingsConfig = { |
||||
key: "persist", |
||||
version: 1, |
||||
storage, |
||||
}; |
||||
|
||||
const reducer = combineReducers({ |
||||
notifications: notificationsReducer, |
||||
auth: authReducer, |
||||
global: globalReducer, |
||||
video: videoReducer, |
||||
persist: persistReducer(persistSettingsConfig, settingsReducer), |
||||
}); |
||||
|
||||
export const store = configureStore({ |
||||
reducer: { |
||||
notifications: notificationsReducer, |
||||
auth: authReducer, |
||||
global: globalReducer, |
||||
video: videoReducer, |
||||
}, |
||||
middleware: (getDefaultMiddleware) => |
||||
reducer, |
||||
middleware: getDefaultMiddleware => |
||||
getDefaultMiddleware({ |
||||
serializableCheck: false |
||||
serializableCheck: false, |
||||
}), |
||||
preloadedState: undefined // optional, can be any valid state object
|
||||
}) |
||||
preloadedState: undefined, // optional, can be any valid state object
|
||||
}); |
||||
|
||||
// Define the RootState type, which is the type of the entire Redux state tree.
|
||||
// This is useful when you need to access the state in a component or elsewhere.
|
||||
export type RootState = ReturnType<typeof store.getState> |
||||
export type RootState = ReturnType<typeof store.getState>; |
||||
|
||||
// Define the AppDispatch type, which is the type of the Redux store's dispatch function.
|
||||
// This is useful when you need to dispatch an action in a component or elsewhere.
|
||||
export type AppDispatch = typeof store.dispatch |
||||
export type AppDispatch = typeof store.dispatch; |
||||
|
Loading…
Reference in new issue