3
0
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:
PhilReact 2023-12-13 12:59:32 +02:00
parent d3d4e002fe
commit 3ab514e1cf
9 changed files with 787 additions and 101 deletions

View File

@ -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>

View File

@ -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);

View File

@ -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={{

View File

@ -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);

View File

@ -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}

View File

@ -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}`
);
}}
>

View 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'
}));

View 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>
);
};

View File

@ -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>
);