mirror of
https://github.com/Qortal/q-tube.git
synced 2025-02-11 17:55:51 +00:00
added preview images on hover
This commit is contained in:
parent
5e211737b0
commit
469688b4e0
@ -1,4 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import Compressor from 'compressorjs'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AddCoverImageButton,
|
AddCoverImageButton,
|
||||||
AddLogoIcon,
|
AddLogoIcon,
|
||||||
@ -13,6 +15,8 @@ import {
|
|||||||
StyledButton,
|
StyledButton,
|
||||||
TimesIcon,
|
TimesIcon,
|
||||||
} from "./Upload-styles";
|
} from "./Upload-styles";
|
||||||
|
import { CircularProgress } from "@mui/material";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -46,6 +50,8 @@ import { QTUBE_VIDEO_BASE, categories, subCategories } from "../../constants";
|
|||||||
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublish";
|
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublish";
|
||||||
import { TextEditor } from "../common/TextEditor/TextEditor";
|
import { TextEditor } from "../common/TextEditor/TextEditor";
|
||||||
import { extractTextFromHTML } from "../common/TextEditor/utils";
|
import { extractTextFromHTML } from "../common/TextEditor/utils";
|
||||||
|
import { toBase64 } from "../UploadVideo/UploadVideo";
|
||||||
|
import { FrameExtractor } from "../common/FrameExtractor/FrameExtractor";
|
||||||
|
|
||||||
const uid = new ShortUniqueId();
|
const uid = new ShortUniqueId();
|
||||||
const shortuid = new ShortUniqueId({ length: 5 });
|
const shortuid = new ShortUniqueId({ length: 5 });
|
||||||
@ -88,6 +94,8 @@ export const EditVideo = () => {
|
|||||||
useState<any>(null);
|
useState<any>(null);
|
||||||
const [selectedSubCategoryVideos, setSelectedSubCategoryVideos] =
|
const [selectedSubCategoryVideos, setSelectedSubCategoryVideos] =
|
||||||
useState<any>(null);
|
useState<any>(null);
|
||||||
|
const [imageExtracts, setImageExtracts] = useState<any>([])
|
||||||
|
|
||||||
|
|
||||||
const { getRootProps, getInputProps } = useDropzone({
|
const { getRootProps, getInputProps } = useDropzone({
|
||||||
accept: {
|
accept: {
|
||||||
@ -205,6 +213,7 @@ export const EditVideo = () => {
|
|||||||
setVideoPropertiesToSetToRedux(null);
|
setVideoPropertiesToSetToRedux(null);
|
||||||
setFile(null);
|
setFile(null);
|
||||||
setTitle("");
|
setTitle("");
|
||||||
|
setImageExtracts([])
|
||||||
setDescription("");
|
setDescription("");
|
||||||
setCoverImage("");
|
setCoverImage("");
|
||||||
};
|
};
|
||||||
@ -270,6 +279,7 @@ export const EditVideo = () => {
|
|||||||
fullDescription,
|
fullDescription,
|
||||||
videoImage: coverImage,
|
videoImage: coverImage,
|
||||||
videoReference: editVideoProperties.videoReference,
|
videoReference: editVideoProperties.videoReference,
|
||||||
|
extracts: file ? imageExtracts : editVideoProperties?.extracts,
|
||||||
commentsId: editVideoProperties.commentsId,
|
commentsId: editVideoProperties.commentsId,
|
||||||
category,
|
category,
|
||||||
subcategory,
|
subcategory,
|
||||||
@ -346,21 +356,7 @@ export const EditVideo = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOnchange = (index: number, type: string, value: string) => {
|
|
||||||
// setFiles((prev) => {
|
|
||||||
// let formattedValue = value
|
|
||||||
// console.log({type})
|
|
||||||
// if(type === 'title'){
|
|
||||||
// formattedValue = value.replace(/[^a-zA-Z0-9\s]/g, "")
|
|
||||||
// }
|
|
||||||
// const copyFiles = [...prev];
|
|
||||||
// copyFiles[index] = {
|
|
||||||
// ...copyFiles[index],
|
|
||||||
// [type]: formattedValue,
|
|
||||||
// };
|
|
||||||
// return copyFiles;
|
|
||||||
// });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOptionCategoryChangeVideos = (
|
const handleOptionCategoryChangeVideos = (
|
||||||
event: SelectChangeEvent<string>
|
event: SelectChangeEvent<string>
|
||||||
@ -380,6 +376,44 @@ export const EditVideo = () => {
|
|||||||
setSelectedSubCategoryVideos(selectedOption || null);
|
setSelectedSubCategoryVideos(selectedOption || null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onFramesExtracted = async (imgs)=> {
|
||||||
|
try {
|
||||||
|
let imagesExtracts = []
|
||||||
|
|
||||||
|
for (const img of imgs){
|
||||||
|
try {
|
||||||
|
let compressedFile
|
||||||
|
const image = img
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
new Compressor(image, {
|
||||||
|
quality: .8,
|
||||||
|
maxWidth: 750,
|
||||||
|
mimeType: 'image/webp',
|
||||||
|
success(result) {
|
||||||
|
const file = new File([result], 'name', {
|
||||||
|
type: 'image/webp'
|
||||||
|
})
|
||||||
|
compressedFile = file
|
||||||
|
resolve()
|
||||||
|
},
|
||||||
|
error(err) {}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!compressedFile) continue
|
||||||
|
const base64Img = await toBase64(compressedFile)
|
||||||
|
imagesExtracts.push(base64Img)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setImageExtracts(imagesExtracts)
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
@ -466,6 +500,9 @@ export const EditVideo = () => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
{file && (
|
||||||
|
<FrameExtractor videoFile={file} onFramesExtracted={(imgs)=> onFramesExtracted(imgs)}/>
|
||||||
|
)}
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{!coverImage ? (
|
{!coverImage ? (
|
||||||
<ImageUploader onPick={(img: string) => setCoverImage(img)}>
|
<ImageUploader onPick={(img: string) => setCoverImage(img)}>
|
||||||
@ -548,7 +585,11 @@ export const EditVideo = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
publishQDNResource();
|
publishQDNResource();
|
||||||
}}
|
}}
|
||||||
|
disabled={file && imageExtracts.length === 0}
|
||||||
>
|
>
|
||||||
|
{file && imageExtracts.length === 0 && (
|
||||||
|
<CircularProgress color="secondary" size={14} />
|
||||||
|
)}
|
||||||
Publish
|
Publish
|
||||||
</CrowdfundActionButton>
|
</CrowdfundActionButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -44,7 +44,7 @@ const ResponsiveImage: React.FC<ResponsiveImageProps> = ({
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
padding: '2px',
|
padding: '2px',
|
||||||
maxHeight: '50%'
|
height: '100%'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import Compressor from 'compressorjs'
|
||||||
import {
|
import {
|
||||||
AddCoverImageButton,
|
AddCoverImageButton,
|
||||||
AddLogoIcon,
|
AddLogoIcon,
|
||||||
@ -13,6 +14,8 @@ import {
|
|||||||
StyledButton,
|
StyledButton,
|
||||||
TimesIcon,
|
TimesIcon,
|
||||||
} from "./Upload-styles";
|
} from "./Upload-styles";
|
||||||
|
import { CircularProgress } from "@mui/material";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@ -57,6 +60,17 @@ import { CardContentContainerComment } from "../common/Comments/Comments-styles"
|
|||||||
import { TextEditor } from "../common/TextEditor/TextEditor";
|
import { TextEditor } from "../common/TextEditor/TextEditor";
|
||||||
import { extractTextFromHTML } from "../common/TextEditor/utils";
|
import { extractTextFromHTML } from "../common/TextEditor/utils";
|
||||||
import { FiltersCheckbox, FiltersRow, FiltersSubContainer } from "../../pages/Home/VideoList-styles";
|
import { FiltersCheckbox, FiltersRow, FiltersSubContainer } from "../../pages/Home/VideoList-styles";
|
||||||
|
import { FrameExtractor } from "../common/FrameExtractor/FrameExtractor";
|
||||||
|
|
||||||
|
export const toBase64 = (file: File): Promise<string | ArrayBuffer | null> =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
reader.onload = () => resolve(reader.result)
|
||||||
|
reader.onerror = (error) => {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const uid = new ShortUniqueId();
|
const uid = new ShortUniqueId();
|
||||||
const shortuid = new ShortUniqueId({ length: 5 });
|
const shortuid = new ShortUniqueId({ length: 5 });
|
||||||
@ -114,7 +128,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
|||||||
const [isCheckTitleByFile, setIsCheckTitleByFile] = useState(false)
|
const [isCheckTitleByFile, setIsCheckTitleByFile] = useState(false)
|
||||||
const [isCheckSameCoverImage, setIsCheckSameCoverImage] = useState(false)
|
const [isCheckSameCoverImage, setIsCheckSameCoverImage] = useState(false)
|
||||||
const [isCheckDescriptionIsTitle, setIsCheckDescriptionIsTitle] = useState(false)
|
const [isCheckDescriptionIsTitle, setIsCheckDescriptionIsTitle] = useState(false)
|
||||||
|
const [imageExtracts, setImageExtracts] = useState<any>({})
|
||||||
const { getRootProps, getInputProps } = useDropzone({
|
const { getRootProps, getInputProps } = useDropzone({
|
||||||
accept: {
|
accept: {
|
||||||
"video/*": [],
|
"video/*": [],
|
||||||
@ -219,7 +233,8 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
|||||||
|
|
||||||
let listOfPublishes = [];
|
let listOfPublishes = [];
|
||||||
|
|
||||||
for (const publish of files) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const publish = files[i]
|
||||||
const title = publish.title;
|
const title = publish.title;
|
||||||
const description = isCheckDescriptionIsTitle ? publish.title : publish.description;
|
const description = isCheckDescriptionIsTitle ? publish.title : publish.description;
|
||||||
const category = selectedCategoryVideos.id;
|
const category = selectedCategoryVideos.id;
|
||||||
@ -271,6 +286,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
|||||||
identifier: identifier,
|
identifier: identifier,
|
||||||
service: "VIDEO",
|
service: "VIDEO",
|
||||||
},
|
},
|
||||||
|
extracts: imageExtracts[i],
|
||||||
commentsId: `${QTUBE_VIDEO_BASE}_cm_${id}`,
|
commentsId: `${QTUBE_VIDEO_BASE}_cm_${id}`,
|
||||||
category,
|
category,
|
||||||
subcategory,
|
subcategory,
|
||||||
@ -539,6 +555,49 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onFramesExtracted = async (imgs, index)=> {
|
||||||
|
try {
|
||||||
|
let imagesExtracts = []
|
||||||
|
|
||||||
|
for (const img of imgs){
|
||||||
|
try {
|
||||||
|
let compressedFile
|
||||||
|
const image = img
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
new Compressor(image, {
|
||||||
|
quality: .8,
|
||||||
|
maxWidth: 750,
|
||||||
|
mimeType: 'image/webp',
|
||||||
|
success(result) {
|
||||||
|
const file = new File([result], 'name', {
|
||||||
|
type: 'image/webp'
|
||||||
|
})
|
||||||
|
compressedFile = file
|
||||||
|
resolve()
|
||||||
|
},
|
||||||
|
error(err) {}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!compressedFile) continue
|
||||||
|
const base64Img = await toBase64(compressedFile)
|
||||||
|
imagesExtracts.push(base64Img)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setImageExtracts((prev)=> {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[index]: imagesExtracts
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{username && (
|
{username && (
|
||||||
@ -719,6 +778,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
|||||||
{files.map((file, index) => {
|
{files.map((file, index) => {
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
|
<FrameExtractor videoFile={file.file} onFramesExtracted={(imgs)=> onFramesExtracted(imgs, index)}/>
|
||||||
<Typography>{file?.file?.name}</Typography>
|
<Typography>{file?.file?.name}</Typography>
|
||||||
{!isCheckSameCoverImage && (
|
{!isCheckSameCoverImage && (
|
||||||
<>
|
<>
|
||||||
@ -1126,24 +1186,30 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
|||||||
) : (
|
) : (
|
||||||
<CrowdfundActionButton
|
<CrowdfundActionButton
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
disabled={files?.length !== Object.keys(imageExtracts)?.length}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
next();
|
next();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Next
|
{files?.length !== Object.keys(imageExtracts)?.length ? 'Generating image extracts' : ''}
|
||||||
|
{files?.length !== Object.keys(imageExtracts)?.length && (
|
||||||
|
<CircularProgress color="secondary" size={14} />
|
||||||
|
)}
|
||||||
|
Next
|
||||||
</CrowdfundActionButton>
|
</CrowdfundActionButton>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</CrowdfundActionButtonRow>
|
</CrowdfundActionButtonRow>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{isOpenMultiplePublish && (
|
{isOpenMultiplePublish && (
|
||||||
<MultiplePublish
|
<MultiplePublish
|
||||||
isOpen={isOpenMultiplePublish}
|
isOpen={isOpenMultiplePublish}
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
setIsOpenMultiplePublish(false);
|
setIsOpenMultiplePublish(false);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
setImageExtracts({})
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
setStep("videos");
|
setStep("videos");
|
||||||
setPlaylistCoverImage(null);
|
setPlaylistCoverImage(null);
|
||||||
|
77
src/components/common/FrameExtractor/FrameExtractor.tsx
Normal file
77
src/components/common/FrameExtractor/FrameExtractor.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import React, { useEffect, useRef, useState, useMemo } from 'react';
|
||||||
|
|
||||||
|
export const FrameExtractor = ({ videoFile, onFramesExtracted }) => {
|
||||||
|
const videoRef = useRef(null);
|
||||||
|
const [durations, setDurations] = useState([]);
|
||||||
|
const canvasRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
video.addEventListener('loadedmetadata', () => {
|
||||||
|
const duration = video.duration;
|
||||||
|
if (isFinite(duration)) {
|
||||||
|
// Proceed with your logic
|
||||||
|
|
||||||
|
console.log('duration', duration)
|
||||||
|
const section = duration / 4;
|
||||||
|
let timestamps = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const randomTime = Math.random() * section + i * section;
|
||||||
|
timestamps.push(randomTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDurations(timestamps);
|
||||||
|
} else {
|
||||||
|
onFramesExtracted([])
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [videoFile]);
|
||||||
|
|
||||||
|
console.log({durations})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (durations.length === 4) {
|
||||||
|
extractFrames();
|
||||||
|
}
|
||||||
|
}, [durations]);
|
||||||
|
|
||||||
|
const fileUrl = useMemo(() => {
|
||||||
|
return URL.createObjectURL(videoFile);
|
||||||
|
}, [videoFile]);
|
||||||
|
|
||||||
|
const extractFrames = async () => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
canvas.width = video.videoWidth;
|
||||||
|
canvas.height = video.videoHeight;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
|
||||||
|
let frameData = [];
|
||||||
|
|
||||||
|
for (const time of durations) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
video.currentTime = time;
|
||||||
|
const onSeeked = () => {
|
||||||
|
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||||
|
canvas.toBlob(blob => {
|
||||||
|
frameData.push(blob);
|
||||||
|
resolve();
|
||||||
|
}, 'image/png');
|
||||||
|
video.removeEventListener('seeked', onSeeked);
|
||||||
|
};
|
||||||
|
video.addEventListener('seeked', onSeeked, { once: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onFramesExtracted(frameData);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<video ref={videoRef} style={{ display: 'none' }} src={fileUrl}></video>
|
||||||
|
<canvas ref={canvasRef} style={{ display: 'none' }}></canvas>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
const useTestIdentifiers = false;
|
const useTestIdentifiers = true;
|
||||||
|
|
||||||
export const QTUBE_VIDEO_BASE = useTestIdentifiers
|
export const QTUBE_VIDEO_BASE = useTestIdentifiers
|
||||||
? "MYTEST_vid_"
|
? "MYTEST_vid_"
|
||||||
|
42
src/pages/Home/VideoCardImageContainer.tsx
Normal file
42
src/pages/Home/VideoCardImageContainer.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import ResponsiveImage from "../../components/ResponsiveImage";
|
||||||
|
|
||||||
|
export const VideoCardImageContainer = ({
|
||||||
|
videoImage,
|
||||||
|
frameImages,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
}) => {
|
||||||
|
const [previewImage, setPreviewImage] = useState(null);
|
||||||
|
const intervalRef = React.useRef(null);
|
||||||
|
|
||||||
|
const startPreview = () => {
|
||||||
|
let frameIndex = 0;
|
||||||
|
intervalRef.current = setInterval(() => {
|
||||||
|
setPreviewImage(frameImages[frameIndex]);
|
||||||
|
frameIndex = (frameIndex + 1) % frameImages.length;
|
||||||
|
}, 500); // Change frame every 500 ms
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopPreview = () => {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
setPreviewImage(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height,
|
||||||
|
maxWidth: "100%",
|
||||||
|
}}
|
||||||
|
onMouseEnter={startPreview}
|
||||||
|
onMouseLeave={stopPreview}
|
||||||
|
>
|
||||||
|
<ResponsiveImage
|
||||||
|
src={previewImage || videoImage}
|
||||||
|
width={266}
|
||||||
|
height={150}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -64,6 +64,7 @@ import { PlaylistSVG } from "../../assets/svgs/PlaylistSVG";
|
|||||||
import BlockIcon from "@mui/icons-material/Block";
|
import BlockIcon from "@mui/icons-material/Block";
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
import { LiskSuperLikeContainer } from "../../components/common/ListSuperLikes/LiskSuperLikeContainer";
|
import { LiskSuperLikeContainer } from "../../components/common/ListSuperLikes/LiskSuperLikeContainer";
|
||||||
|
import { VideoCardImageContainer } from "./VideoCardImageContainer";
|
||||||
|
|
||||||
interface VideoListProps {
|
interface VideoListProps {
|
||||||
mode?: string;
|
mode?: string;
|
||||||
@ -696,11 +697,13 @@ export const VideoList = ({ mode }: VideoListProps) => {
|
|||||||
navigate(`/video/${videoObj?.user}/${videoObj?.id}`);
|
navigate(`/video/${videoObj?.user}/${videoObj?.id}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ResponsiveImage
|
<VideoCardImageContainer width={266}
|
||||||
|
height={150} videoImage={videoObj.videoImage} frameImages={videoObj?.extracts || []} />
|
||||||
|
{/* <ResponsiveImage
|
||||||
src={videoObj.videoImage}
|
src={videoObj.videoImage}
|
||||||
width={266}
|
width={266}
|
||||||
height={150}
|
height={150}
|
||||||
/>
|
/> */}
|
||||||
<VideoCardTitle>{videoObj.title}</VideoCardTitle>
|
<VideoCardTitle>{videoObj.title}</VideoCardTitle>
|
||||||
<BottomParent>
|
<BottomParent>
|
||||||
<NameContainer
|
<NameContainer
|
||||||
|
@ -1,66 +1,68 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from "react-redux";
|
||||||
import { RootState } from '../../state/store'
|
import { RootState } from "../../state/store";
|
||||||
|
import { Avatar, Box, Button, Typography, useTheme } from "@mui/material";
|
||||||
|
import { useFetchVideos } from "../../hooks/useFetchVideos";
|
||||||
|
import LazyLoad from "../../components/common/LazyLoad";
|
||||||
import {
|
import {
|
||||||
Avatar,
|
BottomParent,
|
||||||
Box,
|
NameContainer,
|
||||||
Button,
|
ProductManagerRow,
|
||||||
Typography,
|
VideoCard,
|
||||||
useTheme
|
VideoCardCol,
|
||||||
} from '@mui/material'
|
VideoCardContainer,
|
||||||
import { useFetchVideos } from '../../hooks/useFetchVideos'
|
VideoCardName,
|
||||||
import LazyLoad from '../../components/common/LazyLoad'
|
VideoCardTitle,
|
||||||
import { BottomParent, NameContainer, ProductManagerRow, VideoCard, VideoCardCol, VideoCardContainer, VideoCardName, VideoCardTitle, VideoContainer, VideoUploadDate } from './VideoList-styles'
|
VideoContainer,
|
||||||
import ResponsiveImage from '../../components/ResponsiveImage'
|
VideoUploadDate,
|
||||||
import { formatDate, formatTimestampSeconds } from '../../utils/time'
|
} from "./VideoList-styles";
|
||||||
import { Video } from '../../state/features/videoSlice'
|
import ResponsiveImage from "../../components/ResponsiveImage";
|
||||||
import { queue } from '../../wrappers/GlobalWrapper'
|
import { formatDate, formatTimestampSeconds } from "../../utils/time";
|
||||||
import { QTUBE_VIDEO_BASE } from '../../constants'
|
import { Video } from "../../state/features/videoSlice";
|
||||||
|
import { queue } from "../../wrappers/GlobalWrapper";
|
||||||
|
import { QTUBE_VIDEO_BASE } from "../../constants";
|
||||||
|
import { VideoCardImageContainer } from "./VideoCardImageContainer";
|
||||||
|
|
||||||
interface VideoListProps {
|
interface VideoListProps {
|
||||||
mode?: string
|
mode?: string;
|
||||||
}
|
}
|
||||||
export const VideoListComponentLevel = ({ mode }: VideoListProps) => {
|
export const VideoListComponentLevel = ({ mode }: VideoListProps) => {
|
||||||
const { name: paramName } = useParams()
|
const { name: paramName } = useParams();
|
||||||
const theme = useTheme()
|
const theme = useTheme();
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
const firstFetch = useRef(false)
|
const firstFetch = useRef(false);
|
||||||
const afterFetch = useRef(false)
|
const afterFetch = useRef(false);
|
||||||
const hashMapVideos = useSelector(
|
const hashMapVideos = useSelector(
|
||||||
(state: RootState) => state.video.hashMapVideos
|
(state: RootState) => state.video.hashMapVideos
|
||||||
)
|
);
|
||||||
|
|
||||||
const countNewVideos = useSelector(
|
const countNewVideos = useSelector(
|
||||||
(state: RootState) => state.video.countNewVideos
|
(state: RootState) => state.video.countNewVideos
|
||||||
)
|
);
|
||||||
const userAvatarHash = useSelector(
|
const userAvatarHash = useSelector(
|
||||||
(state: RootState) => state.global.userAvatarHash
|
(state: RootState) => state.global.userAvatarHash
|
||||||
)
|
);
|
||||||
|
|
||||||
const [videos, setVideos] = React.useState<Video[]>([])
|
const [videos, setVideos] = React.useState<Video[]>([]);
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate();
|
||||||
const {
|
const { getVideo, getNewVideos, checkNewVideos, checkAndUpdateVideo } =
|
||||||
getVideo,
|
useFetchVideos();
|
||||||
getNewVideos,
|
|
||||||
checkNewVideos,
|
|
||||||
checkAndUpdateVideo
|
|
||||||
} = useFetchVideos()
|
|
||||||
|
|
||||||
const getVideos = React.useCallback(async () => {
|
const getVideos = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const offset = videos.length
|
const offset = videos.length;
|
||||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}_&limit=20&includemetadata=false&reverse=true&excludeblocked=true&name=${paramName}&exactmatchnames=true&offset=${offset}`
|
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}_&limit=20&includemetadata=false&reverse=true&excludeblocked=true&name=${paramName}&exactmatchnames=true&offset=${offset}`;
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json",
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
const responseData = await response.json()
|
const responseData = await response.json();
|
||||||
|
|
||||||
const structureData = responseData.map((video: any): Video => {
|
const structureData = responseData.map((video: any): Video => {
|
||||||
return {
|
return {
|
||||||
title: video?.metadata?.title,
|
title: video?.metadata?.title,
|
||||||
@ -71,128 +73,126 @@ export const VideoListComponentLevel = ({ mode }: VideoListProps) => {
|
|||||||
created: video?.created,
|
created: video?.created,
|
||||||
updated: video?.updated,
|
updated: video?.updated,
|
||||||
user: video.name,
|
user: video.name,
|
||||||
videoImage: '',
|
videoImage: "",
|
||||||
id: video.identifier
|
id: video.identifier,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
|
|
||||||
const copiedVideos: Video[] = [...videos]
|
const copiedVideos: Video[] = [...videos];
|
||||||
structureData.forEach((video: Video) => {
|
structureData.forEach((video: Video) => {
|
||||||
const index = videos.findIndex((p) => p.id === video.id)
|
const index = videos.findIndex((p) => p.id === video.id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
copiedVideos[index] = video
|
copiedVideos[index] = video;
|
||||||
} else {
|
} else {
|
||||||
copiedVideos.push(video)
|
copiedVideos.push(video);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
setVideos(copiedVideos)
|
setVideos(copiedVideos);
|
||||||
|
|
||||||
for (const content of structureData) {
|
for (const content of structureData) {
|
||||||
if (content.user && content.id) {
|
if (content.user && content.id) {
|
||||||
const res = checkAndUpdateVideo(content)
|
const res = checkAndUpdateVideo(content);
|
||||||
if (res) {
|
if (res) {
|
||||||
queue.push(() => getVideo(content.user, content.id, content));
|
queue.push(() => getVideo(content.user, content.id, content));
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
} finally {
|
} finally {
|
||||||
|
|
||||||
}
|
}
|
||||||
}, [videos, hashMapVideos])
|
}, [videos, hashMapVideos]);
|
||||||
|
|
||||||
|
|
||||||
const getVideosHandler = React.useCallback(async () => {
|
const getVideosHandler = React.useCallback(async () => {
|
||||||
if(!firstFetch.current || !afterFetch.current) return
|
if (!firstFetch.current || !afterFetch.current) return;
|
||||||
await getVideos()
|
await getVideos();
|
||||||
}, [getVideos])
|
}, [getVideos]);
|
||||||
|
|
||||||
|
|
||||||
const getVideosHandlerMount = React.useCallback(async () => {
|
const getVideosHandlerMount = React.useCallback(async () => {
|
||||||
if(firstFetch.current) return
|
if (firstFetch.current) return;
|
||||||
firstFetch.current = true
|
firstFetch.current = true;
|
||||||
await getVideos()
|
await getVideos();
|
||||||
afterFetch.current = true
|
afterFetch.current = true;
|
||||||
setIsLoading(false)
|
setIsLoading(false);
|
||||||
}, [getVideos])
|
}, [getVideos]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!firstFetch.current) {
|
||||||
|
getVideosHandlerMount();
|
||||||
|
|
||||||
useEffect(()=> {
|
|
||||||
if(!firstFetch.current){
|
|
||||||
getVideosHandlerMount()
|
|
||||||
}
|
}
|
||||||
|
}, [getVideosHandlerMount]);
|
||||||
|
|
||||||
}, [getVideosHandlerMount ])
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProductManagerRow>
|
<ProductManagerRow>
|
||||||
<Box sx={{
|
<Box
|
||||||
width: '100%',
|
sx={{
|
||||||
display: 'flex',
|
width: "100%",
|
||||||
flexDirection: 'column',
|
display: "flex",
|
||||||
alignItems: 'center'
|
flexDirection: "column",
|
||||||
}}>
|
alignItems: "center",
|
||||||
|
}}
|
||||||
<VideoCardContainer>
|
>
|
||||||
{videos.map((video: any, index: number) => {
|
<VideoCardContainer>
|
||||||
const existingVideo = hashMapVideos[video.id]
|
{videos.map((video: any, index: number) => {
|
||||||
let hasHash = false
|
const existingVideo = hashMapVideos[video.id];
|
||||||
let videoObj = video
|
let hasHash = false;
|
||||||
if (existingVideo) {
|
let videoObj = video;
|
||||||
videoObj = existingVideo
|
if (existingVideo) {
|
||||||
hasHash = true
|
videoObj = existingVideo;
|
||||||
}
|
hasHash = true;
|
||||||
|
}
|
||||||
|
|
||||||
let avatarUrl = ''
|
let avatarUrl = "";
|
||||||
if(userAvatarHash[videoObj?.user]){
|
if (userAvatarHash[videoObj?.user]) {
|
||||||
avatarUrl = userAvatarHash[videoObj?.user]
|
avatarUrl = userAvatarHash[videoObj?.user];
|
||||||
}
|
}
|
||||||
|
|
||||||
if(hasHash && (!videoObj?.videoImage || videoObj?.videoImage?.length < 50)){
|
if (
|
||||||
return null
|
hasHash &&
|
||||||
}
|
(!videoObj?.videoImage || videoObj?.videoImage?.length < 50)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
return (
|
<VideoCardCol key={videoObj.id}>
|
||||||
<VideoCardCol
|
|
||||||
|
|
||||||
key={videoObj.id}
|
|
||||||
>
|
|
||||||
|
|
||||||
<VideoCard
|
<VideoCard
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate(`/video/${videoObj.user}/${videoObj.id}`)
|
navigate(`/video/${videoObj.user}/${videoObj.id}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ResponsiveImage src={videoObj.videoImage} width={266} height={150}/>
|
<VideoCardImageContainer
|
||||||
|
width={266}
|
||||||
|
height={150}
|
||||||
|
videoImage={videoObj.videoImage}
|
||||||
|
frameImages={videoObj?.extracts || []}
|
||||||
|
/>
|
||||||
<VideoCardTitle>{videoObj.title}</VideoCardTitle>
|
<VideoCardTitle>{videoObj.title}</VideoCardTitle>
|
||||||
<BottomParent>
|
<BottomParent>
|
||||||
<NameContainer>
|
<NameContainer>
|
||||||
<Avatar sx={{height: 24, width: 24}} src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`} alt={`${videoObj.user}'s avatar`} />
|
<Avatar
|
||||||
<VideoCardName>{videoObj.user}</VideoCardName>
|
sx={{ height: 24, width: 24 }}
|
||||||
</NameContainer>
|
src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`}
|
||||||
|
alt={`${videoObj.user}'s avatar`}
|
||||||
{videoObj?.created && (
|
/>
|
||||||
<VideoUploadDate>{formatDate(videoObj.created)}</VideoUploadDate>
|
<VideoCardName>{videoObj.user}</VideoCardName>
|
||||||
)}
|
</NameContainer>
|
||||||
|
|
||||||
|
{videoObj?.created && (
|
||||||
|
<VideoUploadDate>
|
||||||
|
{formatDate(videoObj.created)}
|
||||||
|
</VideoUploadDate>
|
||||||
|
)}
|
||||||
</BottomParent>
|
</BottomParent>
|
||||||
</VideoCard>
|
</VideoCard>
|
||||||
|
</VideoCardCol>
|
||||||
|
);
|
||||||
</VideoCardCol>
|
})}
|
||||||
)
|
|
||||||
})}
|
|
||||||
</VideoCardContainer>
|
</VideoCardContainer>
|
||||||
<LazyLoad onLoadMore={getVideosHandler} isLoading={isLoading}></LazyLoad>
|
<LazyLoad
|
||||||
</Box>
|
onLoadMore={getVideosHandler}
|
||||||
|
isLoading={isLoading}
|
||||||
|
></LazyLoad>
|
||||||
|
</Box>
|
||||||
</ProductManagerRow>
|
</ProductManagerRow>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user