mirror of
https://github.com/Qortal/q-tube.git
synced 2025-02-11 17:55:51 +00:00
added unlimited vids in playlist and made uploading easier
This commit is contained in:
parent
d3d4e002fe
commit
3ab514e1cf
@ -11,6 +11,7 @@ import { Home } from "./pages/Home/Home";
|
||||
import { VideoContent } from "./pages/VideoContent/VideoContent";
|
||||
import DownloadWrapper from "./wrappers/DownloadWrapper";
|
||||
import { IndividualProfile } from "./pages/IndividualProfile/IndividualProfile";
|
||||
import { PlaylistContent } from "./pages/PlaylistContent/PlaylistContent";
|
||||
|
||||
function App() {
|
||||
// const themeColor = window._qdnTheme
|
||||
@ -27,6 +28,7 @@ function App() {
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/video/:name/:id" element={<VideoContent />} />
|
||||
<Route path="/playlist/:name/:id" element={<PlaylistContent />} />
|
||||
<Route path="/channel/:name" element={<IndividualProfile />} />
|
||||
</Routes>
|
||||
</GlobalWrapper>
|
||||
|
@ -306,7 +306,7 @@ export const EditPlaylist = () => {
|
||||
subcategory
|
||||
};
|
||||
|
||||
const codes = videoStructured.map((item) => `c:${item.code};`).join("");
|
||||
const codes = videoStructured.map((item) => `c:${item.code};`).slice(0,10).join("");
|
||||
let metadescription =
|
||||
`**category:${category};subcategory:${subcategory};${codes}**` +
|
||||
stringDescription.slice(0, 120);
|
||||
@ -436,13 +436,6 @@ export const EditPlaylist = () => {
|
||||
};
|
||||
|
||||
const addVideo = (data) => {
|
||||
if(playlistData?.videos?.length > 9){
|
||||
dispatch(setNotification({
|
||||
msg: "Max 10 videos per playlist",
|
||||
alertType: "error",
|
||||
}));
|
||||
return
|
||||
}
|
||||
const copyData = structuredClone(playlistData);
|
||||
copyData.videos = [...copyData.videos, { ...data }];
|
||||
setPlaylistData(copyData);
|
||||
|
@ -4,7 +4,7 @@ import { CrowdfundSubTitle, CrowdfundSubTitleRow } from '../UploadVideo/Upload-s
|
||||
import { Box, Typography, useTheme } from '@mui/material'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
export const Playlists = ({playlistData, currentVideoIdentifier}) => {
|
||||
export const Playlists = ({playlistData, currentVideoIdentifier, onClick}) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate()
|
||||
|
||||
@ -44,8 +44,8 @@ export const Playlists = ({playlistData, currentVideoIdentifier}) => {
|
||||
}}
|
||||
onClick={()=> {
|
||||
if(isCurrentVidPlayling) return
|
||||
|
||||
navigate(`/video/${vid.name}/${vid.identifier}`)
|
||||
onClick(vid.name, vid.identifier)
|
||||
// navigate(`/video/${vid.name}/${vid.identifier}`)
|
||||
}}
|
||||
>
|
||||
<Typography sx={{
|
||||
|
@ -56,6 +56,7 @@ import {
|
||||
import { CardContentContainerComment } from "../common/Comments/Comments-styles";
|
||||
import { TextEditor } from "../common/TextEditor/TextEditor";
|
||||
import { extractTextFromHTML } from "../common/TextEditor/utils";
|
||||
import { FiltersCheckbox, FiltersRow, FiltersSubContainer } from "../../pages/Home/VideoList-styles";
|
||||
|
||||
const uid = new ShortUniqueId();
|
||||
const shortuid = new ShortUniqueId({ length: 5 });
|
||||
@ -88,6 +89,8 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [title, setTitle] = useState<string>("");
|
||||
const [description, setDescription] = useState<string>("");
|
||||
const [coverImageForAll, setCoverImageForAll] = useState<null | string>("");
|
||||
|
||||
const [step, setStep] = useState<string>("videos");
|
||||
const [playlistCoverImage, setPlaylistCoverImage] = useState<null | string>(
|
||||
null
|
||||
@ -108,17 +111,29 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
|
||||
const [playlistSetting, setPlaylistSetting] = useState<null | string>(null);
|
||||
const [publishes, setPublishes] = useState<any[]>([]);
|
||||
const [isCheckTitleByFile, setIsCheckTitleByFile] = useState(false)
|
||||
const [isCheckSameCoverImage, setIsCheckSameCoverImage] = useState(false)
|
||||
const [isCheckDescriptionIsTitle, setIsCheckDescriptionIsTitle] = useState(false)
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
accept: {
|
||||
"video/*": [],
|
||||
},
|
||||
maxFiles: 10,
|
||||
maxSize: 419430400, // 400 MB in bytes
|
||||
onDrop: (acceptedFiles, rejectedFiles) => {
|
||||
const formatArray = acceptedFiles.map((item) => {
|
||||
|
||||
let formatTitle = ''
|
||||
if(isCheckTitleByFile && item?.name){
|
||||
const fileExtensionSplit = item?.name?.split(".");
|
||||
if (fileExtensionSplit?.length > 1) {
|
||||
formatTitle = fileExtensionSplit[0]
|
||||
}
|
||||
formatTitle = (formatTitle || "").replace(/[^a-zA-Z0-9\s-_!?]/g, "");
|
||||
}
|
||||
return {
|
||||
file: item,
|
||||
title: "",
|
||||
title: formatTitle,
|
||||
description: "",
|
||||
coverImage: "",
|
||||
};
|
||||
@ -175,6 +190,8 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
if (!playlistCoverImage) throw new Error("Please select cover image");
|
||||
if (!selectedCategory) throw new Error("Please select a category");
|
||||
}
|
||||
if(files?.length === 0) throw new Error("Please select at least one file");
|
||||
if(isCheckSameCoverImage && !coverImageForAll) throw new Error("Please select cover image");
|
||||
if (!userAddress) throw new Error("Unable to locate user address");
|
||||
let errorMsg = "";
|
||||
let name = "";
|
||||
@ -204,10 +221,10 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
|
||||
for (const publish of files) {
|
||||
const title = publish.title;
|
||||
const description = publish.description;
|
||||
const description = isCheckDescriptionIsTitle ? publish.title : publish.description;
|
||||
const category = selectedCategoryVideos.id;
|
||||
const subcategory = selectedSubCategoryVideos?.id || "";
|
||||
const coverImage = publish.coverImage;
|
||||
const coverImage = isCheckSameCoverImage ? coverImageForAll : publish.coverImage;
|
||||
const file = publish.file;
|
||||
const sanitizeTitle = title
|
||||
.replace(/[^a-zA-Z0-9\s-]/g, "")
|
||||
@ -345,7 +362,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
subcategory,
|
||||
};
|
||||
|
||||
const codes = videos.map((item) => `c:${item.code};`).join("");
|
||||
const codes = videos.map((item) => `c:${item.code};`).slice(0,10).join("");
|
||||
|
||||
let metadescription =
|
||||
`**category:${category};subcategory:${subcategory};${codes}**` +
|
||||
@ -398,7 +415,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
videos: videosInPlaylist,
|
||||
};
|
||||
const codes = videosInPlaylist
|
||||
.map((item) => `c:${item.code};`)
|
||||
.map((item) => `c:${item.code};`).slice(0,10)
|
||||
.join("");
|
||||
|
||||
let metadescription =
|
||||
@ -502,11 +519,13 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
|
||||
const next = () => {
|
||||
try {
|
||||
if(isCheckSameCoverImage && !coverImageForAll) throw new Error("Please select cover image");
|
||||
if(files?.length === 0) throw new Error("Please select at least one file");
|
||||
if (!selectedCategoryVideos) throw new Error("Please select a category");
|
||||
files.forEach((file) => {
|
||||
if (!file.title) throw new Error("Please enter a title");
|
||||
if (!file.description) throw new Error("Please enter a description");
|
||||
if (!file.coverImage) throw new Error("Please select cover image");
|
||||
if (!isCheckTitleByFile && !file.description) throw new Error("Please enter a description");
|
||||
if (!isCheckSameCoverImage && !file.coverImage) throw new Error("Please select cover image");
|
||||
});
|
||||
|
||||
setStep("playlist");
|
||||
@ -560,6 +579,38 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
|
||||
{step === "videos" && (
|
||||
<>
|
||||
<FiltersSubContainer>
|
||||
<FiltersRow>
|
||||
Populate Titles by filename (when the files are picked)
|
||||
<FiltersCheckbox
|
||||
checked={isCheckTitleByFile}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsCheckTitleByFile(e.target.checked);
|
||||
}}
|
||||
inputProps={{ "aria-label": "controlled" }}
|
||||
/>
|
||||
</FiltersRow>
|
||||
<FiltersRow>
|
||||
All videos use the same Cover Image
|
||||
<FiltersCheckbox
|
||||
checked={isCheckSameCoverImage}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsCheckSameCoverImage(e.target.checked);
|
||||
}}
|
||||
inputProps={{ "aria-label": "controlled" }}
|
||||
/>
|
||||
</FiltersRow>
|
||||
<FiltersRow>
|
||||
Populate all descriptions by Title
|
||||
<FiltersCheckbox
|
||||
checked={isCheckDescriptionIsTitle}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsCheckDescriptionIsTitle(e.target.checked);
|
||||
}}
|
||||
inputProps={{ "aria-label": "controlled" }}
|
||||
/>
|
||||
</FiltersRow>
|
||||
</FiltersSubContainer>
|
||||
<Box
|
||||
{...getRootProps()}
|
||||
sx={{
|
||||
@ -575,6 +626,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
Drag and drop a video files here or click to select files
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
@ -631,11 +683,46 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
{files?.length > 0 && isCheckSameCoverImage && (
|
||||
<>
|
||||
{!coverImageForAll ? (
|
||||
<ImageUploader
|
||||
onPick={(img: string) =>
|
||||
setCoverImageForAll(img)
|
||||
}
|
||||
>
|
||||
<AddCoverImageButton variant="contained">
|
||||
Add Cover Image
|
||||
<AddLogoIcon
|
||||
sx={{
|
||||
height: "25px",
|
||||
width: "auto",
|
||||
}}
|
||||
></AddLogoIcon>
|
||||
</AddCoverImageButton>
|
||||
</ImageUploader>
|
||||
) : (
|
||||
<LogoPreviewRow>
|
||||
<CoverImagePreview src={coverImageForAll} alt="logo" />
|
||||
<TimesIcon
|
||||
color={theme.palette.text.primary}
|
||||
onClickFunc={() =>
|
||||
setCoverImageForAll(null)
|
||||
}
|
||||
height={"32"}
|
||||
width={"32"}
|
||||
></TimesIcon>
|
||||
</LogoPreviewRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{files.map((file, index) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<Typography>{file?.file?.name}</Typography>
|
||||
{!file?.coverImage ? (
|
||||
{!isCheckSameCoverImage && (
|
||||
<>
|
||||
{!file?.coverImage ? (
|
||||
<ImageUploader
|
||||
onPick={(img: string) =>
|
||||
handleOnchange(index, "coverImage", img)
|
||||
@ -664,6 +751,9 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
></TimesIcon>
|
||||
</LogoPreviewRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<CustomInputField
|
||||
name="title"
|
||||
label="Title of video"
|
||||
@ -675,12 +765,17 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
inputProps={{ maxLength: 180 }}
|
||||
required
|
||||
/>
|
||||
<Typography sx={{
|
||||
{!isCheckDescriptionIsTitle && (
|
||||
<>
|
||||
<Typography sx={{
|
||||
fontSize: '18px'
|
||||
}}>Description of video</Typography>
|
||||
<TextEditor inlineContent={file?.description} setInlineContent={(value)=> {
|
||||
handleOnchange(index, "description", value)
|
||||
}} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* <CustomInputField
|
||||
name="description"
|
||||
label="Describe your video in a few words"
|
||||
@ -1055,6 +1150,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
setPlaylistTitle("");
|
||||
setPlaylistDescription("");
|
||||
setSelectedCategory(null);
|
||||
setCoverImageForAll(null)
|
||||
setSelectedSubCategory(null);
|
||||
setSelectedCategoryVideos(null);
|
||||
setSelectedSubCategoryVideos(null);
|
||||
|
@ -61,6 +61,9 @@ interface VideoPlayerProps {
|
||||
customStyle?: any
|
||||
user?: string
|
||||
jsonId?: string
|
||||
nextVideo?: any
|
||||
onEnd?: ()=> void
|
||||
autoPlay?: boolean
|
||||
}
|
||||
|
||||
export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
@ -72,7 +75,10 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
from = null,
|
||||
customStyle = {},
|
||||
user = '',
|
||||
jsonId = ''
|
||||
jsonId = '',
|
||||
nextVideo,
|
||||
onEnd,
|
||||
autoPlay
|
||||
}) => {
|
||||
const dispatch = useDispatch()
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null)
|
||||
@ -89,6 +95,8 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const videoPlaying = useSelector((state: RootState) => state.global.videoPlaying);
|
||||
const reDownload = useRef<boolean>(false)
|
||||
const reDownloadNextVid = useRef<boolean>(false)
|
||||
|
||||
const isFetchingProperties = useRef<boolean>(false)
|
||||
|
||||
const status = useRef<null | string>(null)
|
||||
@ -131,6 +139,8 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const decreaseSpeed = () => {
|
||||
if (videoRef.current) {
|
||||
updatePlaybackRate(playbackRate - speedChange);
|
||||
@ -139,6 +149,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
|
||||
useEffect(()=> {
|
||||
reDownload.current = false
|
||||
reDownloadNextVid.current = false
|
||||
setIsLoading(false)
|
||||
setCanPlay(false)
|
||||
setProgress(0)
|
||||
@ -148,6 +159,15 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
status.current = null
|
||||
}, [identifier])
|
||||
|
||||
useEffect(()=> {
|
||||
if(autoPlay && identifier){
|
||||
setStartPlay(true)
|
||||
setPlaying(true)
|
||||
togglePlay(undefined, true)
|
||||
|
||||
}
|
||||
}, [autoPlay, startPlay, identifier])
|
||||
|
||||
|
||||
|
||||
const refetch = React.useCallback(async () => {
|
||||
@ -172,7 +192,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
|
||||
const toggleRef = useRef<any>(null)
|
||||
const { downloadVideo } = useContext(MyContext)
|
||||
const togglePlay = async () => {
|
||||
const togglePlay = async (event?: any, isPlay?: boolean) => {
|
||||
if (!videoRef.current) return
|
||||
setStartPlay(true)
|
||||
if (!src || resourceStatus?.status !== 'READY') {
|
||||
@ -185,12 +205,17 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
})
|
||||
getSrc()
|
||||
}
|
||||
if (playing) {
|
||||
if (playing && !isPlay) {
|
||||
videoRef.current.pause()
|
||||
} else {
|
||||
videoRef.current.play()
|
||||
}
|
||||
setPlaying(!playing)
|
||||
if(isPlay){
|
||||
setPlaying(true)
|
||||
} else {
|
||||
setPlaying(!playing)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const onVolumeChange = (_: any, value: number | number[]) => {
|
||||
@ -212,6 +237,9 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
|
||||
const handleEnded = () => {
|
||||
setPlaying(false)
|
||||
if(onEnd){
|
||||
onEnd()
|
||||
}
|
||||
}
|
||||
|
||||
const updateProgress = () => {
|
||||
@ -445,9 +473,37 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
) {
|
||||
refetchInInterval()
|
||||
reDownload.current = true
|
||||
|
||||
}
|
||||
}, [getSrc, resourceStatus])
|
||||
|
||||
useEffect(() => {
|
||||
if(resourceStatus?.status){
|
||||
status.current = resourceStatus?.status
|
||||
}
|
||||
if (
|
||||
resourceStatus?.status === 'READY' &&
|
||||
reDownloadNextVid?.current === false
|
||||
) {
|
||||
if(nextVideo){
|
||||
downloadVideo({
|
||||
name: nextVideo?.name,
|
||||
service: nextVideo?.service,
|
||||
identifier: nextVideo?.identifier,
|
||||
properties: {
|
||||
jsonId: nextVideo?.jsonId,
|
||||
user
|
||||
}
|
||||
})
|
||||
}
|
||||
reDownloadNextVid.current = true
|
||||
|
||||
}
|
||||
}, [getSrc, resourceStatus])
|
||||
|
||||
|
||||
|
||||
|
||||
const handleMenuOpen = (event: any) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
@ -580,6 +636,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<VideoContainer
|
||||
tabIndex={0}
|
||||
|
@ -632,7 +632,7 @@ export const VideoList = ({ mode }: VideoListProps) => {
|
||||
onClick={() => {
|
||||
if(!hasHash) return
|
||||
navigate(
|
||||
`/video/${videoObj?.videos?.[0]?.name}/${videoObj?.videos?.[0]?.identifier}`
|
||||
`/playlist/${videoObj?.user}/${videoObj?.id}`
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
81
src/pages/PlaylistContent/PlaylistContent-styles.tsx
Normal file
81
src/pages/PlaylistContent/PlaylistContent-styles.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { styled } from "@mui/system";
|
||||
import { Box, Grid, Typography, Checkbox } from "@mui/material";
|
||||
|
||||
export const VideoPlayerContainer = styled(Box)(({ theme }) => ({
|
||||
maxWidth: '95%',
|
||||
width: '1000px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
}));
|
||||
|
||||
export const VideoTitle = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Raleway",
|
||||
fontSize: "20px",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
wordBreak: "break-word"
|
||||
}));
|
||||
|
||||
export const VideoDescription = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Raleway",
|
||||
fontSize: "16px",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
wordBreak: "break-word"
|
||||
}));
|
||||
|
||||
export const Spacer = ({height}: any)=> {
|
||||
return <Box sx={{
|
||||
height: height
|
||||
}} />
|
||||
}
|
||||
|
||||
export const StyledCardHeaderComment = styled(Box)({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
gap: '5px',
|
||||
padding: '7px 0px'
|
||||
})
|
||||
export const StyledCardCol = styled(Box)({
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
flexDirection: 'column',
|
||||
gap: '2px',
|
||||
alignItems: 'flex-start',
|
||||
width: '100%'
|
||||
})
|
||||
|
||||
export const StyledCardColComment = styled(Box)({
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
flexDirection: 'column',
|
||||
gap: '2px',
|
||||
alignItems: 'flex-start',
|
||||
width: '100%'
|
||||
})
|
||||
|
||||
export const AuthorTextComment = styled(Typography)({
|
||||
fontFamily: 'Raleway, sans-serif',
|
||||
fontSize: '16px',
|
||||
lineHeight: '1.2'
|
||||
})
|
||||
|
||||
export const FileAttachmentContainer = styled(Box)(({ theme }) =>({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "20px",
|
||||
padding: "5px 10px",
|
||||
border: `1px solid ${theme.palette.text.primary}`,
|
||||
}));
|
||||
|
||||
export const FileAttachmentFont = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Mulish",
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: "16px",
|
||||
letterSpacing: 0,
|
||||
fontWeight: 400,
|
||||
userSelect: "none",
|
||||
whiteSpace: 'nowrap'
|
||||
}));
|
531
src/pages/PlaylistContent/PlaylistContent.tsx
Normal file
531
src/pages/PlaylistContent/PlaylistContent.tsx
Normal file
@ -0,0 +1,531 @@
|
||||
import React, { useState, useMemo, useRef, useEffect, useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { setIsLoadingGlobal } from "../../state/features/globalSlice";
|
||||
import { Avatar, Box, Typography, useTheme } from "@mui/material";
|
||||
import { VideoPlayer } from "../../components/common/VideoPlayer";
|
||||
import { RootState } from "../../state/store";
|
||||
import { addToHashMap } from "../../state/features/videoSlice";
|
||||
import AttachFileIcon from "@mui/icons-material/AttachFile";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
|
||||
import mockImg from "../../test/mockimg.jpg";
|
||||
import {
|
||||
AuthorTextComment,
|
||||
FileAttachmentContainer,
|
||||
FileAttachmentFont,
|
||||
Spacer,
|
||||
StyledCardColComment,
|
||||
StyledCardHeaderComment,
|
||||
VideoDescription,
|
||||
VideoPlayerContainer,
|
||||
VideoTitle,
|
||||
} from "./PlaylistContent-styles";
|
||||
import { setUserAvatarHash } from "../../state/features/globalSlice";
|
||||
import {
|
||||
formatDate,
|
||||
formatDateSeconds,
|
||||
formatTimestampSeconds,
|
||||
} from "../../utils/time";
|
||||
import { NavbarName } from "../../components/layout/Navbar/Navbar-styles";
|
||||
import { CommentSection } from "../../components/common/Comments/CommentSection";
|
||||
import {
|
||||
CrowdfundSubTitle,
|
||||
CrowdfundSubTitleRow,
|
||||
} from "../../components/UploadVideo/Upload-styles";
|
||||
import { QTUBE_VIDEO_BASE } from "../../constants";
|
||||
import { Playlists } from "../../components/Playlists/Playlists";
|
||||
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml";
|
||||
import FileElement from "../../components/common/FileElement";
|
||||
|
||||
export const PlaylistContent = () => {
|
||||
const { name, id } = useParams();
|
||||
const [doAutoPlay, setDoAutoPlay] = useState(false)
|
||||
const [isExpandedDescription, setIsExpandedDescription] =
|
||||
useState<boolean>(false);
|
||||
const [descriptionHeight, setDescriptionHeight] =
|
||||
useState<null | number>(null);
|
||||
|
||||
const userAvatarHash = useSelector(
|
||||
(state: RootState) => state.global.userAvatarHash
|
||||
);
|
||||
const contentRef = useRef(null);
|
||||
|
||||
|
||||
|
||||
const avatarUrl = useMemo(() => {
|
||||
let url = "";
|
||||
if (name && userAvatarHash[name]) {
|
||||
url = userAvatarHash[name];
|
||||
}
|
||||
|
||||
return url;
|
||||
}, [userAvatarHash, name]);
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
const [videoData, setVideoData] = useState<any>(null);
|
||||
const [playlistData, setPlaylistData] = useState<any>(null);
|
||||
|
||||
const hashMapVideos = useSelector(
|
||||
(state: RootState) => state.video.hashMapVideos
|
||||
);
|
||||
const videoReference = useMemo(() => {
|
||||
if (!videoData) return null;
|
||||
const { videoReference } = videoData;
|
||||
if (
|
||||
videoReference?.identifier &&
|
||||
videoReference?.name &&
|
||||
videoReference?.service
|
||||
) {
|
||||
return videoReference;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [videoData]);
|
||||
|
||||
const videoCover = useMemo(() => {
|
||||
if (!videoData) return null;
|
||||
const { videoImage } = videoData;
|
||||
return videoImage || null;
|
||||
}, [videoData]);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const getVideoData = React.useCallback(async (name: string, id: string) => {
|
||||
try {
|
||||
if (!name || !id) return;
|
||||
dispatch(setIsLoadingGlobal(true));
|
||||
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}&limit=1&includemetadata=true&reverse=true&excludeblocked=true&name=${name}&exactmatchnames=true&offset=0&identifier=${id}`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseDataSearch = await response.json();
|
||||
|
||||
if (responseDataSearch?.length > 0) {
|
||||
let resourceData = responseDataSearch[0];
|
||||
resourceData = {
|
||||
title: resourceData?.metadata?.title,
|
||||
category: resourceData?.metadata?.category,
|
||||
categoryName: resourceData?.metadata?.categoryName,
|
||||
tags: resourceData?.metadata?.tags || [],
|
||||
description: resourceData?.metadata?.description,
|
||||
created: resourceData?.created,
|
||||
updated: resourceData?.updated,
|
||||
user: resourceData.name,
|
||||
videoImage: "",
|
||||
id: resourceData.identifier,
|
||||
};
|
||||
|
||||
const responseData = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: name,
|
||||
service: "DOCUMENT",
|
||||
identifier: id,
|
||||
});
|
||||
|
||||
if (responseData && !responseData.error) {
|
||||
const combinedData = {
|
||||
...resourceData,
|
||||
...responseData,
|
||||
};
|
||||
|
||||
setVideoData(combinedData);
|
||||
dispatch(addToHashMap(combinedData));
|
||||
checkforPlaylist(name, id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} finally {
|
||||
dispatch(setIsLoadingGlobal(false));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const checkforPlaylist = React.useCallback(async (name, id) => {
|
||||
try {
|
||||
dispatch(setIsLoadingGlobal(true));
|
||||
|
||||
if (!name || !id) return;
|
||||
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=PLAYLIST&identifier=${id}&limit=1&includemetadata=true&reverse=true&excludeblocked=true&name=${name}&exactmatchnames=true&offset=0`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseDataSearch = await response.json();
|
||||
|
||||
if (responseDataSearch?.length > 0) {
|
||||
let resourceData = responseDataSearch[0];
|
||||
resourceData = {
|
||||
title: resourceData?.metadata?.title,
|
||||
category: resourceData?.metadata?.category,
|
||||
categoryName: resourceData?.metadata?.categoryName,
|
||||
tags: resourceData?.metadata?.tags || [],
|
||||
description: resourceData?.metadata?.description,
|
||||
created: resourceData?.created,
|
||||
updated: resourceData?.updated,
|
||||
name: resourceData.name,
|
||||
videoImage: "",
|
||||
identifier: resourceData.identifier,
|
||||
service: resourceData.service,
|
||||
};
|
||||
|
||||
const responseData = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resourceData.name,
|
||||
service: resourceData.service,
|
||||
identifier: resourceData.identifier,
|
||||
});
|
||||
|
||||
if (responseData && !responseData.error) {
|
||||
const combinedData = {
|
||||
...resourceData,
|
||||
...responseData,
|
||||
};
|
||||
const videos = [];
|
||||
if (combinedData?.videos) {
|
||||
for (const vid of combinedData.videos) {
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${vid.identifier}&limit=1&includemetadata=true&reverse=true&name=${vid.name}&exactmatchnames=true&offset=0`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseDataSearchVid = await response.json();
|
||||
|
||||
if (responseDataSearchVid?.length > 0) {
|
||||
let resourceData2 = responseDataSearchVid[0];
|
||||
videos.push(resourceData2);
|
||||
}
|
||||
}
|
||||
}
|
||||
combinedData.videos = videos;
|
||||
setPlaylistData(combinedData);
|
||||
if(combinedData?.videos?.length > 0){
|
||||
const vid = combinedData?.videos[0]
|
||||
const existingVideo = hashMapVideos[vid?.identifier];
|
||||
|
||||
if (existingVideo) {
|
||||
setVideoData(existingVideo);
|
||||
} else {
|
||||
getVideoData(vid?.name, vid?.identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
} finally {
|
||||
dispatch(setIsLoadingGlobal(false));
|
||||
|
||||
}
|
||||
}, [hashMapVideos]);
|
||||
|
||||
// React.useEffect(() => {
|
||||
// if (name && id) {
|
||||
// const existingVideo = hashMapVideos[id];
|
||||
|
||||
// if (existingVideo) {
|
||||
// setVideoData(existingVideo);
|
||||
// checkforPlaylist(name, id, existingVideo?.code);
|
||||
// } else {
|
||||
// getVideoData(name, id);
|
||||
// }
|
||||
// }
|
||||
// }, [id, name]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (name && id) {
|
||||
checkforPlaylist(name, id);
|
||||
|
||||
}
|
||||
}, [id, name]);
|
||||
|
||||
// const getAvatar = React.useCallback(async (author: string) => {
|
||||
// try {
|
||||
// let url = await qortalRequest({
|
||||
// action: 'GET_QDN_RESOURCE_URL',
|
||||
// name: author,
|
||||
// service: 'THUMBNAIL',
|
||||
// identifier: 'qortal_avatar'
|
||||
// })
|
||||
|
||||
// setAvatarUrl(url)
|
||||
// dispatch(setUserAvatarHash({
|
||||
// name: author,
|
||||
// url
|
||||
// }))
|
||||
// } catch (error) { }
|
||||
// }, [])
|
||||
|
||||
// React.useEffect(() => {
|
||||
// if (name && !avatarUrl) {
|
||||
// const existingAvatar = userAvatarHash[name]
|
||||
|
||||
// if (existingAvatar) {
|
||||
// setAvatarUrl(existingAvatar)
|
||||
// } else {
|
||||
// getAvatar(name)
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// }, [name, userAvatarHash])
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
const height = contentRef.current.offsetHeight;
|
||||
if (height > 100) { // Assuming 100px is your threshold
|
||||
setDescriptionHeight(100)
|
||||
}
|
||||
}
|
||||
}, [videoData]);
|
||||
|
||||
|
||||
const nextVideo = useMemo(()=> {
|
||||
|
||||
const currentVideoIndex = playlistData?.videos?.findIndex((item)=> item?.identifier === videoData?.id)
|
||||
if(currentVideoIndex !== -1){
|
||||
const nextVideoIndex = currentVideoIndex + 1
|
||||
const findVideo = playlistData?.videos[nextVideoIndex] || null
|
||||
if(findVideo){
|
||||
const id = findVideo?.identifier?.replace("_metadata", "");
|
||||
return {
|
||||
...findVideo,
|
||||
service: 'VIDEO',
|
||||
identifier: id,
|
||||
jsonId: findVideo?.identifier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}, [playlistData, videoData])
|
||||
|
||||
const onEndVideo = useCallback(()=> {
|
||||
const currentVideoIndex = playlistData?.videos?.findIndex((item)=> item?.identifier === videoData?.id)
|
||||
if(currentVideoIndex !== -1){
|
||||
const nextVideoIndex = currentVideoIndex + 1
|
||||
const findVideo = playlistData?.videos[nextVideoIndex] || null
|
||||
if(findVideo){
|
||||
getVideoData(findVideo?.name, findVideo?.identifier)
|
||||
setDoAutoPlay(true)
|
||||
}
|
||||
}
|
||||
|
||||
}, [videoData, playlistData])
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexDirection: "column",
|
||||
padding: "20px 10px",
|
||||
}}
|
||||
>
|
||||
<VideoPlayerContainer
|
||||
sx={{
|
||||
marginBottom: "30px",
|
||||
}}
|
||||
>
|
||||
{videoData && videoData?.videos?.length === 0 ? (
|
||||
<>
|
||||
<Box sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<Typography>This playlist is empty</Typography>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{videoReference && (
|
||||
<VideoPlayer
|
||||
name={videoReference?.name}
|
||||
service={videoReference?.service}
|
||||
identifier={videoReference?.identifier}
|
||||
user={name}
|
||||
jsonId={id}
|
||||
poster={videoCover || ""}
|
||||
nextVideo={nextVideo}
|
||||
onEnd={onEndVideo}
|
||||
autoPlay={doAutoPlay}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Spacer height="15px" />
|
||||
<Box sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end'
|
||||
}}>
|
||||
<FileAttachmentContainer>
|
||||
|
||||
<FileAttachmentFont>
|
||||
save to disk
|
||||
</FileAttachmentFont>
|
||||
<FileElement
|
||||
fileInfo={{...videoReference,
|
||||
filename: videoData?.filename || videoData?.title?.slice(0,20) + '.mp4',
|
||||
mimeType: videoData?.videoType || '"video/mp4',
|
||||
|
||||
}}
|
||||
title={videoData?.filename || videoData?.title?.slice(0,20)}
|
||||
customStyles={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<DownloadIcon />
|
||||
</FileElement>
|
||||
</FileAttachmentContainer>
|
||||
</Box>
|
||||
<VideoTitle
|
||||
variant="h1"
|
||||
color="textPrimary"
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{videoData?.title}
|
||||
</VideoTitle>
|
||||
{videoData?.created && (
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
}}
|
||||
color={theme.palette.text.primary}
|
||||
>
|
||||
{formatDate(videoData.created)}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Spacer height="15px" />
|
||||
<Box
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate(`/channel/${name}`);
|
||||
}}
|
||||
>
|
||||
<StyledCardHeaderComment
|
||||
sx={{
|
||||
"& .MuiCardHeader-content": {
|
||||
overflow: "hidden",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Avatar
|
||||
src={`/arbitrary/THUMBNAIL/${name}/qortal_avatar`}
|
||||
alt={`${name}'s avatar`}
|
||||
/>
|
||||
</Box>
|
||||
<StyledCardColComment>
|
||||
<AuthorTextComment
|
||||
color={
|
||||
theme.palette.mode === "light"
|
||||
? theme.palette.text.secondary
|
||||
: "#d6e8ff"
|
||||
}
|
||||
>
|
||||
{name}
|
||||
</AuthorTextComment>
|
||||
</StyledCardColComment>
|
||||
</StyledCardHeaderComment>
|
||||
</Box>
|
||||
<Spacer height="15px" />
|
||||
<Box
|
||||
sx={{
|
||||
background: "#333333",
|
||||
borderRadius: "5px",
|
||||
padding: "5px",
|
||||
width: "100%",
|
||||
cursor: !descriptionHeight ? "default" : isExpandedDescription ? "default" : "pointer",
|
||||
position: "relative",
|
||||
}}
|
||||
className={!descriptionHeight ? "": isExpandedDescription ? "" : "hover-click"}
|
||||
>
|
||||
{descriptionHeight && !isExpandedDescription && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "0px",
|
||||
right: "0px",
|
||||
left: "0px",
|
||||
bottom: "0px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isExpandedDescription) return;
|
||||
setIsExpandedDescription(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Box
|
||||
ref={contentRef}
|
||||
sx={{
|
||||
height: !descriptionHeight ? 'auto' : isExpandedDescription ? "auto" : "100px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{videoData?.htmlDescription ? (
|
||||
<DisplayHtml html={videoData?.htmlDescription} />
|
||||
) : (
|
||||
<VideoDescription variant="body1" color="textPrimary" sx={{
|
||||
cursor: 'default'
|
||||
}}>
|
||||
{videoData?.fullDescription}
|
||||
</VideoDescription>
|
||||
)}
|
||||
</Box>
|
||||
{descriptionHeight && (
|
||||
<Typography
|
||||
onClick={() => {
|
||||
setIsExpandedDescription((prev) => !prev);
|
||||
}}
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
fontSize: "16px",
|
||||
cursor: "pointer",
|
||||
paddingLeft: "15px",
|
||||
paddingTop: "15px",
|
||||
}}
|
||||
>
|
||||
{isExpandedDescription ? "Show less" : "...more"}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
</VideoPlayerContainer>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
width: "100%",
|
||||
maxWidth: "1200px",
|
||||
}}
|
||||
>
|
||||
<CommentSection postId={id || ""} postName={name || ""} />
|
||||
{playlistData && (
|
||||
<Playlists playlistData={playlistData} currentVideoIdentifier={videoData?.id} onClick={getVideoData} />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -64,7 +64,6 @@ export const VideoContent = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [videoData, setVideoData] = useState<any>(null);
|
||||
const [playlistData, setPlaylistData] = useState<any>(null);
|
||||
|
||||
const hashMapVideos = useSelector(
|
||||
(state: RootState) => state.video.hashMapVideos
|
||||
@ -134,7 +133,6 @@ export const VideoContent = () => {
|
||||
|
||||
setVideoData(combinedData);
|
||||
dispatch(addToHashMap(combinedData));
|
||||
checkforPlaylist(name, id, combinedData?.code);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@ -143,79 +141,13 @@ export const VideoContent = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const checkforPlaylist = React.useCallback(async (name, id, code) => {
|
||||
try {
|
||||
if (!name || !id || !code) return;
|
||||
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=PLAYLIST&description=c:${code}&limit=1&includemetadata=true&reverse=true&excludeblocked=true&name=${name}&exactmatchnames=true&offset=0`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseDataSearch = await response.json();
|
||||
|
||||
if (responseDataSearch?.length > 0) {
|
||||
let resourceData = responseDataSearch[0];
|
||||
resourceData = {
|
||||
title: resourceData?.metadata?.title,
|
||||
category: resourceData?.metadata?.category,
|
||||
categoryName: resourceData?.metadata?.categoryName,
|
||||
tags: resourceData?.metadata?.tags || [],
|
||||
description: resourceData?.metadata?.description,
|
||||
created: resourceData?.created,
|
||||
updated: resourceData?.updated,
|
||||
name: resourceData.name,
|
||||
videoImage: "",
|
||||
identifier: resourceData.identifier,
|
||||
service: resourceData.service,
|
||||
};
|
||||
|
||||
const responseData = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resourceData.name,
|
||||
service: resourceData.service,
|
||||
identifier: resourceData.identifier,
|
||||
});
|
||||
|
||||
if (responseData && !responseData.error) {
|
||||
const combinedData = {
|
||||
...resourceData,
|
||||
...responseData,
|
||||
};
|
||||
const videos = [];
|
||||
if (combinedData?.videos) {
|
||||
for (const vid of combinedData.videos) {
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${vid.identifier}&limit=1&includemetadata=true&reverse=true&name=${vid.name}&exactmatchnames=true&offset=0`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseDataSearchVid = await response.json();
|
||||
|
||||
if (responseDataSearchVid?.length > 0) {
|
||||
let resourceData2 = responseDataSearchVid[0];
|
||||
videos.push(resourceData2);
|
||||
}
|
||||
}
|
||||
}
|
||||
combinedData.videos = videos;
|
||||
setPlaylistData(combinedData);
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
}, []);
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
if (name && id) {
|
||||
const existingVideo = hashMapVideos[id];
|
||||
|
||||
if (existingVideo) {
|
||||
setVideoData(existingVideo);
|
||||
checkforPlaylist(name, id, existingVideo?.code);
|
||||
} else {
|
||||
getVideoData(name, id);
|
||||
}
|
||||
@ -256,14 +188,12 @@ export const VideoContent = () => {
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
const height = contentRef.current.offsetHeight;
|
||||
console.log({height})
|
||||
if (height > 100) { // Assuming 100px is your threshold
|
||||
setDescriptionHeight(100)
|
||||
}
|
||||
}
|
||||
}, [videoData]);
|
||||
|
||||
console.log({ videoData });
|
||||
|
||||
return (
|
||||
<Box
|
||||
@ -438,7 +368,6 @@ export const VideoContent = () => {
|
||||
|
||||
</Box>
|
||||
</VideoPlayerContainer>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
@ -448,9 +377,6 @@ export const VideoContent = () => {
|
||||
}}
|
||||
>
|
||||
<CommentSection postId={id || ""} postName={name || ""} />
|
||||
{playlistData && (
|
||||
<Playlists playlistData={playlistData} currentVideoIdentifier={id} />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user