mirror of https://github.com/Qortal/q-tube
Browse Source
Filter added that removes characters that Operating Systems don't allow in filenames when saving file VideoList-styles.tsx uses Radio button instead of Checkbox for main page video/playlist filter Video player has aspect ratio of 16 / 9, doesn't put controls over video, and removes controls if mouse exits video when in fullscreen (but only when playing for some reason) Created new redux slice called settingsSlice.ts. It is used to store settings that are saved to disk automaticallypull/9/head
Qortal Dev
7 months ago
21 changed files with 2283 additions and 1839 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,582 @@
|
||||
import React from 'react' |
||||
import { VideoList } from './VideoList' |
||||
import React, { useCallback, useEffect, useRef, useState } from "react"; |
||||
import { useNavigate } from "react-router-dom"; |
||||
import ReactDOM from "react-dom"; |
||||
import { useSelector, useDispatch } from "react-redux"; |
||||
import { RootState } from "../../state/store"; |
||||
import { |
||||
Avatar, |
||||
Box, |
||||
Button, |
||||
FormControl, |
||||
Grid, |
||||
Input, |
||||
InputLabel, |
||||
MenuItem, |
||||
OutlinedInput, |
||||
Select, |
||||
SelectChangeEvent, |
||||
Tab, |
||||
Tabs, |
||||
Tooltip, |
||||
Typography, |
||||
useTheme, |
||||
} from "@mui/material"; |
||||
import { useFetchVideos } from "../../hooks/useFetchVideos"; |
||||
import LazyLoad from "../../components/common/LazyLoad"; |
||||
import { |
||||
BlockIconContainer, |
||||
BottomParent, |
||||
FilterSelect, |
||||
FiltersCheckbox, |
||||
FiltersCol, |
||||
FiltersContainer, |
||||
FiltersRow, |
||||
FiltersSubContainer, |
||||
FiltersTitle, |
||||
IconsBox, |
||||
NameContainer, |
||||
VideoCardCol, |
||||
ProductManagerRow, |
||||
VideoCardContainer, |
||||
VideoCard, |
||||
VideoCardName, |
||||
VideoCardTitle, |
||||
VideoContainer, |
||||
VideoUploadDate, |
||||
FiltersRadioButton, |
||||
} from "./VideoList-styles"; |
||||
import ResponsiveImage from "../../components/ResponsiveImage"; |
||||
import { formatDate, formatTimestampSeconds } from "../../utils/time"; |
||||
import { Subtitle, SubtitleContainer } from "./Home-styles"; |
||||
import { ExpandMoreSVG } from "../../assets/svgs/ExpandMoreSVG"; |
||||
import { |
||||
addVideos, |
||||
blockUser, |
||||
changeFilterType, |
||||
changeSelectedCategoryVideos, |
||||
changeSelectedSubCategoryVideos, |
||||
changefilterName, |
||||
changefilterSearch, |
||||
clearVideoList, |
||||
setEditPlaylist, |
||||
setEditVideo, |
||||
} from "../../state/features/videoSlice"; |
||||
import { categories, subCategories } from "../../constants/Categories.ts"; |
||||
import { Playlists } from "../../components/Playlists/Playlists"; |
||||
import { PlaylistSVG } from "../../assets/svgs/PlaylistSVG"; |
||||
import BlockIcon from "@mui/icons-material/Block"; |
||||
import EditIcon from "@mui/icons-material/Edit"; |
||||
import { ListSuperLikeContainer } from "../../components/common/ListSuperLikes/ListSuperLikeContainer.tsx"; |
||||
import { VideoCardImageContainer } from "./VideoCardImageContainer"; |
||||
import { SubscribeButton } from "../../components/common/SubscribeButton.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/settingsSlice.ts"; |
||||
|
||||
import { useSelector } from 'react-redux' |
||||
import { RootState } from '../../state/store' |
||||
interface HomeProps { |
||||
mode?: string; |
||||
} |
||||
export const Home = ({ mode }: HomeProps) => { |
||||
const theme = useTheme(); |
||||
const prevVal = useRef(""); |
||||
const isFiltering = useSelector( |
||||
(state: RootState) => state.video.isFiltering |
||||
); |
||||
const filterValue = useSelector( |
||||
(state: RootState) => state.video.filterValue |
||||
); |
||||
|
||||
const settingsState = useSelector((state: RootState) => state.settings); |
||||
const [isLoading, setIsLoading] = useState<boolean>(false); |
||||
const [tabValue, setTabValue] = useState<string>(settingsState.selectedTab); |
||||
|
||||
const tabFontSize = "20px"; |
||||
const filterType = useSelector((state: RootState) => state.video.filterType); |
||||
|
||||
const setFilterType = payload => { |
||||
dispatch(changeFilterType(payload)); |
||||
}; |
||||
const filterSearch = useSelector( |
||||
(state: RootState) => state.video.filterSearch |
||||
); |
||||
|
||||
const setFilterSearch = payload => { |
||||
dispatch(changefilterSearch(payload)); |
||||
}; |
||||
const filterName = useSelector((state: RootState) => state.video.filterName); |
||||
|
||||
const setFilterName = payload => { |
||||
dispatch(changefilterName(payload)); |
||||
}; |
||||
|
||||
const selectedCategoryVideos = useSelector( |
||||
(state: RootState) => state.video.selectedCategoryVideos |
||||
); |
||||
|
||||
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 isFetchingFiltered = useRef(false); |
||||
const isFetching = useRef(false); |
||||
|
||||
const countNewVideos = useSelector( |
||||
(state: RootState) => state.video.countNewVideos |
||||
); |
||||
|
||||
const videoSelector = useSelector((state: RootState) => state.video); |
||||
|
||||
const { videos: globalVideos } = useSelector( |
||||
(state: RootState) => state.video |
||||
); |
||||
|
||||
const { getVideos, getNewVideos, checkNewVideos, getVideosFiltered } = |
||||
useFetchVideos(); |
||||
|
||||
const getVideosHandler = React.useCallback( |
||||
async (reset?: boolean, resetFilers?: 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, |
||||
resetFilers, |
||||
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]); |
||||
|
||||
export const Home = () => { |
||||
const getVideosHandlerMount = React.useCallback(async () => { |
||||
if (firstFetch.current) return; |
||||
firstFetch.current = true; |
||||
setIsLoading(true); |
||||
|
||||
await getVideos({}, 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", |
||||
}} |
||||
/> |
||||
<FiltersTitle> |
||||
Categories |
||||
<ExpandMoreSVG |
||||
color={theme.palette.text.primary} |
||||
height={"22"} |
||||
width={"22"} |
||||
/> |
||||
</FiltersTitle> |
||||
<FiltersSubContainer> |
||||
<FormControl sx={{ width: "100%" }}> |
||||
<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> |
||||
<FiltersTitle> |
||||
Type |
||||
<ExpandMoreSVG |
||||
color={theme.palette.text.primary} |
||||
height={"22"} |
||||
width={"22"} |
||||
/> |
||||
</FiltersTitle> |
||||
<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="Subsciptions" |
||||
value={subscriptionTabValue} |
||||
sx={{ fontSize: tabFontSize }} |
||||
/> |
||||
</TabList> |
||||
<TabPanel value="all" sx={{ width: "100%" }}> |
||||
<VideoList videos={videos} /> |
||||
<LazyLoad |
||||
onLoadMore={getVideosHandler} |
||||
isLoading={isLoading} |
||||
></LazyLoad> |
||||
</TabPanel> |
||||
<TabPanel value="subscriptions" sx={{ width: "100%" }}> |
||||
<VideoList videos={videos} /> |
||||
<LazyLoad |
||||
onLoadMore={getVideosHandler} |
||||
isLoading={isLoading} |
||||
></LazyLoad> |
||||
</TabPanel> |
||||
</TabContext> |
||||
</Box> |
||||
</ProductManagerRow> |
||||
</Grid> |
||||
<FiltersCol item xs={0} lg={3} xl={2}> |
||||
<ListSuperLikeContainer /> |
||||
</FiltersCol> |
||||
</Grid> |
||||
); |
||||
}; |
||||
|
@ -0,0 +1,30 @@
|
||||
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; |
||||
} |
||||
|
||||
const initialState: settingsState = { |
||||
selectedTab: subscriptionTabValue, |
||||
stretchVideoSetting: "contain", |
||||
}; |
||||
|
||||
export const settingsSlice = createSlice({ |
||||
name: "settings", |
||||
initialState, |
||||
reducers: { |
||||
setHomePageSelectedTab: (state, action) => { |
||||
state.selectedTab = action.payload; |
||||
}, |
||||
setStretchVideoSetting: (state, action) => { |
||||
state.stretchVideoSetting = action.payload; |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
export const { setHomePageSelectedTab } = settingsSlice.actions; |
||||
|
||||
export default settingsSlice.reducer; |
@ -1,27 +1,54 @@
|
||||
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/settingsSlice"; |
||||
import { |
||||
persistReducer, |
||||
FLUSH, |
||||
REHYDRATE, |
||||
PAUSE, |
||||
PERSIST, |
||||
PURGE, |
||||
REGISTER, |
||||
} from "redux-persist"; |
||||
import storage from "redux-persist/lib/storage"; |
||||
|
||||
const persistVideoConfig = { |
||||
key: "video", |
||||
version: 1, |
||||
storage, |
||||
whitelist: ["subscriptionList"], |
||||
}; |
||||
|
||||
const persistSettingsConfig = { |
||||
key: "settings", |
||||
version: 1, |
||||
storage, |
||||
}; |
||||
|
||||
const reducer = combineReducers({ |
||||
notifications: notificationsReducer, |
||||
auth: authReducer, |
||||
global: globalReducer, |
||||
video: persistReducer(persistVideoConfig, videoReducer), |
||||
settings: 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