3
0
mirror of https://github.com/Qortal/q-tube.git synced 2025-02-14 19:25:52 +00:00

Merge pull request #51 from QortalSeth/main

New Published Videos show Duration and Filesize
This commit is contained in:
Qortal Dev 2024-11-16 08:25:31 -07:00 committed by GitHub
commit eebf4fce78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 320 additions and 289 deletions

View File

@ -1,20 +1,5 @@
import { useEffect, useState } from "react";
import { SubscriptionData } from "./components/common/ContentButtons/SubscribeButton.tsx"; import { SubscriptionData } from "./components/common/ContentButtons/SubscribeButton.tsx";
import { setFilteredSubscriptions } from "./state/features/videoSlice.ts";
import { store } from "./state/store.ts"; import { store } from "./state/store.ts";
import { persistStore } from "redux-persist";
export const useAppState = () => {
const [theme, setTheme] = useState("dark");
const persistor = persistStore(store);
useEffect(() => {
subscriptionListFilter(false).then(filteredList => {
store.dispatch(setFilteredSubscriptions(filteredList));
});
}, []);
return { persistor, theme, setTheme };
};
export const getUserName = async () => { export const getUserName = async () => {
const account = await qortalRequest({ const account = await qortalRequest({
@ -28,6 +13,7 @@ export const getUserName = async () => {
if (nameData?.length > 0) return nameData[0].name; if (nameData?.length > 0) return nameData[0].name;
else return ""; else return "";
}; };
export const filterVideosByName = ( export const filterVideosByName = (
subscriptionList: SubscriptionData[], subscriptionList: SubscriptionData[],
userName: string userName: string

View File

@ -1,15 +1,18 @@
import { CssBaseline } from "@mui/material"; import { CssBaseline } from "@mui/material";
import { ThemeProvider } from "@mui/material/styles"; import { ThemeProvider } from "@mui/material/styles";
import { useEffect, useState } from "react";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { Route, Routes } from "react-router-dom"; import { Route, Routes } from "react-router-dom";
import { persistStore } from "redux-persist";
import { PersistGate } from "redux-persist/integration/react"; import { PersistGate } from "redux-persist/integration/react";
import { useAppState } from "./App-State.ts"; import { subscriptionListFilter } from "./App-Functions.ts";
import Notification from "./components/common/Notification/Notification"; import Notification from "./components/common/Notification/Notification";
import { useIframe } from "./hooks/useIframe.tsx"; import { useIframe } from "./hooks/useIframe.tsx";
import { IndividualProfile } from "./pages/ContentPages/IndividualProfile/IndividualProfile"; import { IndividualProfile } from "./pages/ContentPages/IndividualProfile/IndividualProfile";
import { PlaylistContent } from "./pages/ContentPages/PlaylistContent/PlaylistContent"; import { PlaylistContent } from "./pages/ContentPages/PlaylistContent/PlaylistContent";
import { VideoContent } from "./pages/ContentPages/VideoContent/VideoContent"; import { VideoContent } from "./pages/ContentPages/VideoContent/VideoContent";
import { Home } from "./pages/Home/Home"; import { Home } from "./pages/Home/Home";
import { setFilteredSubscriptions } from "./state/features/videoSlice.ts";
import { store } from "./state/store"; import { store } from "./state/store";
import { darkTheme, lightTheme } from "./styles/theme"; import { darkTheme, lightTheme } from "./styles/theme";
import DownloadWrapper from "./wrappers/DownloadWrapper"; import DownloadWrapper from "./wrappers/DownloadWrapper";
@ -18,9 +21,16 @@ import { ScrollWrapper } from "./wrappers/ScrollWrapper.tsx";
function App() { function App() {
// const themeColor = window._qdnTheme // const themeColor = window._qdnTheme
const { persistor, theme, setTheme } = useAppState(); const persistor = persistStore(store);
const [theme, setTheme] = useState("dark");
useIframe(); useIframe();
useEffect(() => {
subscriptionListFilter(false).then(filteredList => {
store.dispatch(setFilteredSubscriptions(filteredList));
});
}, []);
return ( return (
<Provider store={store}> <Provider store={store}>
<PersistGate loading={null} persistor={persistor}> <PersistGate loading={null} persistor={persistor}>

View File

@ -1,5 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Compressor from "compressorjs"; import Compressor from "compressorjs";
import { formatBytes } from "../../../utils/numberFunctions.ts";
import { import {
AddCoverImageButton, AddCoverImageButton,
@ -62,25 +63,11 @@ import {
titleFormatter, titleFormatter,
videoMaxSize, videoMaxSize,
} from "../../../constants/Misc.ts"; } from "../../../constants/Misc.ts";
import { Signal, useSignal } from "@preact/signals-react";
const uid = new ShortUniqueId(); const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 }); const shortuid = new ShortUniqueId({ length: 5 });
interface NewCrowdfundProps {
editId?: string;
editContent?: null | {
title: string;
user: string;
coverImage: string | null;
};
}
interface VideoFile {
file: File;
title: string;
description: string;
coverImage?: string;
}
export const EditVideo = () => { export const EditVideo = () => {
const theme = useTheme(); const theme = useTheme();
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -106,6 +93,10 @@ export const EditVideo = () => {
useState<any>(null); useState<any>(null);
const [imageExtracts, setImageExtracts] = useState<any>([]); const [imageExtracts, setImageExtracts] = useState<any>([]);
const videoDuration: Signal<number[]> = useSignal([
editVideoProperties?.duration || 0,
]);
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
accept: { accept: {
"video/*": [], "video/*": [],
@ -293,6 +284,8 @@ export const EditVideo = () => {
code: editVideoProperties.code, code: editVideoProperties.code,
videoType: file?.type || "video/mp4", videoType: file?.type || "video/mp4",
filename: `${alphanumericString.trim()}.${fileExtension}`, filename: `${alphanumericString.trim()}.${fileExtension}`,
fileSize: file?.size || 0,
duration: videoDuration.value[0],
}; };
const metadescription = const metadescription =
@ -508,6 +501,8 @@ export const EditVideo = () => {
<FrameExtractor <FrameExtractor
videoFile={file} videoFile={file}
onFramesExtracted={imgs => onFramesExtracted(imgs)} onFramesExtracted={imgs => onFramesExtracted(imgs)}
videoDurations={videoDuration}
index={0}
/> />
)} )}
<React.Fragment> <React.Fragment>

View File

@ -16,7 +16,7 @@ import {
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import Compressor from "compressorjs"; import Compressor from "compressorjs";
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
@ -38,6 +38,7 @@ import {
import { setNotification } from "../../../state/features/notificationsSlice.ts"; import { setNotification } from "../../../state/features/notificationsSlice.ts";
import { RootState } from "../../../state/store.ts"; import { RootState } from "../../../state/store.ts";
import { formatBytes } from "../../../utils/numberFunctions.ts";
import { objectToBase64 } from "../../../utils/PublishFormatter.ts"; import { objectToBase64 } from "../../../utils/PublishFormatter.ts";
import { getFileName } from "../../../utils/stringFunctions.ts"; import { getFileName } from "../../../utils/stringFunctions.ts";
import { CardContentContainerComment } from "../../common/Comments/Comments-styles.tsx"; import { CardContentContainerComment } from "../../common/Comments/Comments-styles.tsx";
@ -65,6 +66,8 @@ import {
StyledButton, StyledButton,
TimesIcon, TimesIcon,
} from "./PublishVideo-styles.tsx"; } from "./PublishVideo-styles.tsx";
import { signal, Signal, useSignal } from "@preact/signals-react";
import { useSignals } from "@preact/signals-react/runtime";
export const toBase64 = (file: File): Promise<string | ArrayBuffer | null> => export const toBase64 = (file: File): Promise<string | ArrayBuffer | null> =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
@ -103,10 +106,8 @@ export const PublishVideo = ({ editId, editContent }: NewCrowdfundProps) => {
(state: RootState) => state.auth?.user?.address (state: RootState) => state.auth?.user?.address
); );
const [files, setFiles] = useState<VideoFile[]>([]); const [files, setFiles] = useState<VideoFile[]>([]);
const videoDurations = useSignal<number[]>([]);
const [isOpen, setIsOpen] = useState<boolean>(false); const [isOpen, setIsOpen] = useState<boolean>(false);
const [title, setTitle] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [coverImageForAll, setCoverImageForAll] = useState<null | string>(""); const [coverImageForAll, setCoverImageForAll] = useState<null | string>("");
const [step, setStep] = useState<string>("videos"); const [step, setStep] = useState<string>("videos");
@ -136,6 +137,14 @@ export const PublishVideo = ({ editId, editContent }: NewCrowdfundProps) => {
useState(false); useState(false);
const [imageExtracts, setImageExtracts] = useState<any>({}); const [imageExtracts, setImageExtracts] = useState<any>({});
useSignals();
const assembleVideoDurations = () => {
if (files.length === videoDurations.value.length) return;
const newArray: number[] = [];
files.map(() => newArray.push(0));
videoDurations.value = [...newArray];
};
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
accept: { accept: {
"video/*": [], "video/*": [],
@ -302,6 +311,8 @@ export const PublishVideo = ({ editId, editContent }: NewCrowdfundProps) => {
code, code,
videoType: file?.type || "video/mp4", videoType: file?.type || "video/mp4",
filename: `${alphanumericString.trim()}.${fileExtension}`, filename: `${alphanumericString.trim()}.${fileExtension}`,
fileSize: file?.size || 0,
duration: videoDurations.value[i],
}; };
const metadescription = const metadescription =
@ -818,11 +829,14 @@ export const PublishVideo = ({ editId, editContent }: NewCrowdfundProps) => {
</> </>
)} )}
{files.map((file, index) => { {files.map((file, index) => {
assembleVideoDurations();
return ( return (
<React.Fragment key={index}> <React.Fragment key={index}>
<FrameExtractor <FrameExtractor
videoFile={file.file} videoFile={file.file}
onFramesExtracted={imgs => onFramesExtracted(imgs, index)} onFramesExtracted={imgs => onFramesExtracted(imgs, index)}
videoDurations={videoDurations}
index={index}
/> />
<Typography>{file?.file?.name}</Typography> <Typography>{file?.file?.name}</Typography>
{!isCheckSameCoverImage && ( {!isCheckSameCoverImage && (
@ -1159,7 +1173,7 @@ export const PublishVideo = ({ editId, editContent }: NewCrowdfundProps) => {
</Typography> </Typography>
<TextEditor <TextEditor
inlineContent={playlistDescription} inlineContent={playlistDescription}
setInlineContent={value => { setInlineContent={(value: string) => {
setPlaylistDescription(value); setPlaylistDescription(value);
}} }}
/> />

View File

@ -4,6 +4,12 @@ import { Grid } from "@mui/material";
import { useFetchVideos } from "../hooks/useFetchVideos.tsx"; import { useFetchVideos } from "../hooks/useFetchVideos.tsx";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "../state/store.ts"; import { RootState } from "../state/store.ts";
import { signal } from "@preact/signals-react";
/* eslint-disable react-refresh/only-export-components */
export const totalVideosPublished = signal(0);
export const totalNamesPublished = signal(0);
export const videosPerNamePublished = signal(0);
export const StatsData = () => { export const StatsData = () => {
const StatsCol = styled(Grid)(({ theme }) => ({ const StatsCol = styled(Grid)(({ theme }) => ({
@ -14,44 +20,43 @@ export const StatsData = () => {
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
})); }));
const { const { getVideosCount } = useFetchVideos();
getVideos,
getNewVideos,
checkNewVideos,
getVideosFiltered,
getVideosCount,
} = useFetchVideos();
const persistReducer = useSelector((state: RootState) => state.persist); const showValueIfExists = (value: number) => {
const totalVideosPublished = useSelector( return value > 0 ? "inline" : "none";
(state: RootState) => state.global.totalVideosPublished };
);
const totalNamesPublished = useSelector(
(state: RootState) => state.global.totalNamesPublished
);
const videosPerNamePublished = useSelector(
(state: RootState) => state.global.videosPerNamePublished
);
const showStats = useSelector((state: RootState) => state.persist.showStats);
const showVideoCount = showValueIfExists(totalVideosPublished.value);
const showPublisherCount = showValueIfExists(totalNamesPublished.value);
const showAverage = showValueIfExists(videosPerNamePublished.value);
useEffect(() => { useEffect(() => {
getVideosCount(); getVideosCount();
}, [getVideosCount]); }, [getVideosCount]);
return ( return (
<StatsCol sx={{ display: persistReducer.showStats ? "block" : "none" }}> <StatsCol sx={{ display: showStats ? "block" : "none" }}>
<div> <div>
Videos:{" "} Videos:{" "}
<span style={{ fontWeight: "bold" }}>{totalVideosPublished}</span> <span style={{ fontWeight: "bold", display: showVideoCount }}>
{totalVideosPublished.value}
</span>
</div> </div>
<div> <div>
Publishers:{" "} Publishers:{" "}
<span style={{ fontWeight: "bold" }}>{totalNamesPublished}</span> <span style={{ fontWeight: "bold", display: showPublisherCount }}>
{totalNamesPublished.value}
</span>
</div> </div>
<div> <div>
Average:{" "} Average:{" "}
<span style={{ fontWeight: "bold" }}> <span
{videosPerNamePublished > 0 && style={{
Number(videosPerNamePublished).toFixed(0)} fontWeight: "bold",
display: showAverage,
}}
>
{Number(videosPerNamePublished.value).toFixed(0)}
</span> </span>
</div> </div>
</StatsCol> </StatsCol>

View File

@ -1,7 +1,7 @@
import { Button, ButtonProps, Tooltip } from "@mui/material"; import { Button, ButtonProps, Tooltip } from "@mui/material";
import { MouseEvent, useEffect, useState } from "react"; import { MouseEvent, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { subscriptionListFilter } from "../../../App-State.ts"; import { subscriptionListFilter } from "../../../App-Functions.ts";
import { RootState } from "../../../state/store.ts"; import { RootState } from "../../../state/store.ts";
import { import {
subscribe, subscribe,

View File

@ -1,20 +1,38 @@
import React, { useEffect, useRef, useState, useMemo } from 'react'; import { Signal } from "@preact/signals-react";
import { useSignals } from "@preact/signals-react/runtime";
import React, { useEffect, useRef, useState, useMemo } from "react";
export const FrameExtractor = ({ videoFile, onFramesExtracted }) => { export interface FrameExtractorProps {
videoFile: File;
onFramesExtracted: (imgs, index?: number) => Promise<void>;
videoDurations?: Signal<number[]>;
index?: number;
}
export const FrameExtractor = ({
videoFile,
onFramesExtracted,
videoDurations,
index,
}: FrameExtractorProps) => {
useSignals();
const videoRef = useRef(null); const videoRef = useRef(null);
const [durations, setDurations] = useState([]); const [durations, setDurations] = useState([]);
const canvasRef = useRef(null); const canvasRef = useRef(null);
useEffect(() => { useEffect(() => {
const video = videoRef.current; const video = videoRef.current;
video.addEventListener('loadedmetadata', () => { video.addEventListener("loadedmetadata", () => {
const duration = video.duration; const duration = video.duration;
if (isFinite(duration)) { if (isFinite(duration)) {
// Proceed with your logic // Proceed with your logic
console.log('duration', duration) const newVideoDurations = [...videoDurations.value];
newVideoDurations[index] = duration;
videoDurations.value = [...newVideoDurations];
const section = duration / 4; const section = duration / 4;
let timestamps = []; const timestamps = [];
for (let i = 0; i < 4; i++) { for (let i = 0; i < 4; i++) {
const randomTime = Math.random() * section + i * section; const randomTime = Math.random() * section + i * section;
@ -23,13 +41,11 @@ export const FrameExtractor = ({ videoFile, onFramesExtracted }) => {
setDurations(timestamps); setDurations(timestamps);
} else { } else {
onFramesExtracted([]) onFramesExtracted([]);
} }
}); });
}, [videoFile]); }, [videoFile]);
console.log({durations})
useEffect(() => { useEffect(() => {
if (durations.length === 4) { if (durations.length === 4) {
extractFrames(); extractFrames();
@ -45,33 +61,32 @@ export const FrameExtractor = ({ videoFile, onFramesExtracted }) => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
canvas.width = video.videoWidth; canvas.width = video.videoWidth;
canvas.height = video.videoHeight; canvas.height = video.videoHeight;
const context = canvas.getContext('2d'); const context = canvas.getContext("2d");
let frameData = []; const frameData = [];
for (const time of durations) { for (const time of durations) {
await new Promise<void>((resolve) => { await new Promise<void>(resolve => {
video.currentTime = time; video.currentTime = time;
const onSeeked = () => { const onSeeked = () => {
context.drawImage(video, 0, 0, canvas.width, canvas.height); context.drawImage(video, 0, 0, canvas.width, canvas.height);
canvas.toBlob(blob => { canvas.toBlob(blob => {
frameData.push(blob); frameData.push(blob);
resolve(); resolve();
}, 'image/png'); }, "image/png");
video.removeEventListener('seeked', onSeeked); video.removeEventListener("seeked", onSeeked);
}; };
video.addEventListener('seeked', onSeeked, { once: true }); video.addEventListener("seeked", onSeeked, { once: true });
}); });
} }
onFramesExtracted(frameData); onFramesExtracted(frameData);
}; };
return ( return (
<div> <div>
<video ref={videoRef} style={{ display: 'none' }} src={fileUrl}></video> <video ref={videoRef} style={{ display: "none" }} src={fileUrl}></video>
<canvas ref={canvasRef} style={{ display: 'none' }}></canvas> <canvas ref={canvasRef} style={{ display: "none" }}></canvas>
</div> </div>
); );
}; };

View File

@ -2,6 +2,7 @@ import { Box, CircularProgress, Typography } from "@mui/material";
import { setVideoPlaying } from "../../../../state/features/globalSlice.ts"; import { setVideoPlaying } from "../../../../state/features/globalSlice.ts";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { PlayArrow } from "@mui/icons-material"; import { PlayArrow } from "@mui/icons-material";
import { formatTime } from "../../../../utils/numberFunctions.ts";
import { useVideoContext } from "./VideoContext.ts"; import { useVideoContext } from "./VideoContext.ts";
export const LoadingVideo = () => { export const LoadingVideo = () => {
@ -13,6 +14,7 @@ export const LoadingVideo = () => {
canPlay, canPlay,
from, from,
togglePlay, togglePlay,
duration,
} = useVideoContext(); } = useVideoContext();
const getDownloadProgress = (current: number, total: number) => { const getDownloadProgress = (current: number, total: number) => {
@ -83,7 +85,20 @@ export const LoadingVideo = () => {
)} )}
</Box> </Box>
)} )}
{((!src && !isLoading.value) || (!startPlay.value && !canPlay.value)) && ( {((!src && !isLoading.value) || (!startPlay.value && !canPlay.value)) && (
<>
{duration && (
<Box
position="absolute"
right={0}
bottom={0}
bgcolor="#202020"
zIndex={999}
>
<Typography color="white">{formatTime(duration)}</Typography>
</Box>
)}
<Box <Box
position="absolute" position="absolute"
top={0} top={0}
@ -113,6 +128,7 @@ export const LoadingVideo = () => {
}} }}
/> />
</Box> </Box>
</>
)} )}
</> </>
); );

View File

@ -1,5 +1,5 @@
import { useSignal } from "@preact/signals-react"; import { useSignal } from "@preact/signals-react";
import { useSignals } from "@preact/signals-react/runtime"; import { useSignalEffect, useSignals } from "@preact/signals-react/runtime";
import { useEffect } from "react"; import { useEffect } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
@ -35,6 +35,7 @@ export const useVideoControlsState = (
progress, progress,
videoObjectFit, videoObjectFit,
canPlay, canPlay,
isMobileView,
} = videoPlayerState; } = videoPlayerState;
const { identifier, autoPlay } = props; const { identifier, autoPlay } = props;
@ -114,31 +115,6 @@ export const useVideoControlsState = (
}; };
}, []); }, []);
function formatTime(seconds: number): string {
seconds = Math.floor(seconds);
const minutes: number | string = Math.floor(seconds / 60);
let hours: number | string = Math.floor(minutes / 60);
let remainingSeconds: number | string = seconds % 60;
let remainingMinutes: number | string = minutes % 60;
if (remainingSeconds < 10) {
remainingSeconds = "0" + remainingSeconds;
}
if (remainingMinutes < 10) {
remainingMinutes = "0" + remainingMinutes;
}
if (hours === 0) {
hours = "";
} else {
hours = hours + ":";
}
return hours + remainingMinutes + ":" + remainingSeconds;
}
const reloadVideo = async () => { const reloadVideo = async () => {
if (!videoRef.current || !src) return; if (!videoRef.current || !src) return;
const currentTime = videoRef.current.currentTime; const currentTime = videoRef.current.currentTime;
@ -154,6 +130,7 @@ export const useVideoControlsState = (
if (firstPlay.value) setPlaying(true); // makes the video play when fully loaded if (firstPlay.value) setPlaying(true); // makes the video play when fully loaded
firstPlay.value = false; firstPlay.value = false;
isLoading.value = false; isLoading.value = false;
updatePlaybackRate(persistSelector.playbackRate);
}; };
const setPlaying = async (setPlay: boolean) => { const setPlaying = async (setPlay: boolean) => {
@ -380,6 +357,14 @@ export const useVideoControlsState = (
} }
}, [videoPlaying, identifier, src]); }, [videoPlaying, identifier, src]);
useSignalEffect(() => {
console.log("canPlay is: ", canPlay.value); // makes the function execute when canPlay changes
const videoWidth = videoRef?.current?.offsetWidth;
if (videoWidth && videoWidth <= 600) {
isMobileView.value = true;
}
});
return { return {
reloadVideo, reloadVideo,
togglePlay, togglePlay,
@ -387,7 +372,6 @@ export const useVideoControlsState = (
increaseSpeed, increaseSpeed,
togglePictureInPicture, togglePictureInPicture,
toggleFullscreen, toggleFullscreen,
formatTime,
keyboardShortcutsUp, keyboardShortcutsUp,
keyboardShortcutsDown, keyboardShortcutsDown,
handleCanPlay, handleCanPlay,

View File

@ -8,15 +8,13 @@ import {
VolumeUp, VolumeUp,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { IconButton, Slider, Typography } from "@mui/material"; import { IconButton, Slider, Typography } from "@mui/material";
import { useSignalEffect, useSignals } from "@preact/signals-react/runtime"; import { formatTime } from "../../../../utils/numberFunctions.ts";
import { useEffect } from "react";
import { ControlsContainer } from "../VideoPlayer-styles.ts"; import { ControlsContainer } from "../VideoPlayer-styles.ts";
import { MobileControls } from "./MobileControls.tsx"; import { MobileControls } from "./MobileControls.tsx";
import { useVideoContext } from "./VideoContext.ts"; import { useVideoContext } from "./VideoContext.ts";
export const VideoControls = () => { export const VideoControls = () => {
useSignals();
const { const {
reloadVideo, reloadVideo,
togglePlay, togglePlay,
@ -24,7 +22,6 @@ export const VideoControls = () => {
increaseSpeed, increaseSpeed,
togglePictureInPicture, togglePictureInPicture,
toggleFullscreen, toggleFullscreen,
formatTime,
toggleMute, toggleMute,
onProgressChange, onProgressChange,
toggleRef, toggleRef,
@ -40,14 +37,6 @@ export const VideoControls = () => {
showControlsFullScreen, showControlsFullScreen,
} = useVideoContext(); } = useVideoContext();
useSignalEffect(() => {
console.log("canPlay is: ", canPlay.value); // makes the function execute when canPlay changes
const videoWidth = videoRef?.current?.offsetWidth;
if (videoWidth && videoWidth <= 600) {
isMobileView.value = true;
}
});
const showMobileControls = const showMobileControls =
isMobileView.value && canPlay.value && showControlsFullScreen.value; isMobileView.value && canPlay.value && showControlsFullScreen.value;

View File

@ -1,4 +1,3 @@
import { useSignals } from "@preact/signals-react/runtime";
import CSS from "csstype"; import CSS from "csstype";
import { forwardRef } from "react"; import { forwardRef } from "react";
import { LoadingVideo } from "./Components/LoadingVideo.tsx"; import { LoadingVideo } from "./Components/LoadingVideo.tsx";
@ -26,6 +25,7 @@ export interface VideoPlayerProps {
onEnd?: () => void; onEnd?: () => void;
autoPlay?: boolean; autoPlay?: boolean;
style?: CSS.Properties; style?: CSS.Properties;
duration?: number;
} }
export type videoRefType = { export type videoRefType = {
@ -34,7 +34,6 @@ export type videoRefType = {
}; };
export const VideoPlayer = forwardRef<videoRefType, VideoPlayerProps>( export const VideoPlayer = forwardRef<videoRefType, VideoPlayerProps>(
(props: VideoPlayerProps, ref) => { (props: VideoPlayerProps, ref) => {
useSignals();
const contextData = useContextData(props, ref); const contextData = useContextData(props, ref);
const { const {
@ -56,6 +55,7 @@ export const VideoPlayer = forwardRef<videoRefType, VideoPlayerProps>(
startPlay, startPlay,
videoObjectFit, videoObjectFit,
showControlsFullScreen, showControlsFullScreen,
duration,
} = contextData; } = contextData;
return ( return (

View File

@ -12,6 +12,7 @@ import {
VolumeOff, VolumeOff,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { styled } from "@mui/system"; import { styled } from "@mui/system";
import { formatTime } from "../../../utils/numberFunctions.ts";
import { MyContext } from "../../../wrappers/DownloadWrapper.tsx"; import { MyContext } from "../../../wrappers/DownloadWrapper.tsx";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../../state/store.ts"; import { RootState } from "../../../state/store.ts";
@ -250,31 +251,6 @@ export const VideoPlayerGlobal: React.FC<VideoPlayerProps> = ({
}; };
}, []); }, []);
function formatTime(seconds: number): string {
seconds = Math.floor(seconds);
const minutes: number | string = Math.floor(seconds / 60);
let hours: number | string = Math.floor(minutes / 60);
let remainingSeconds: number | string = seconds % 60;
let remainingMinutes: number | string = minutes % 60;
if (remainingSeconds < 10) {
remainingSeconds = "0" + remainingSeconds;
}
if (remainingMinutes < 10) {
remainingMinutes = "0" + remainingMinutes;
}
if (hours === 0) {
hours = "";
} else {
hours = hours + ":";
}
return hours + remainingMinutes + ":" + remainingSeconds;
}
const reloadVideo = async () => { const reloadVideo = async () => {
if (!videoRef.current) return; if (!videoRef.current) return;
const src = videoRef.current.src; const src = videoRef.current.src;

View File

@ -1,6 +1,11 @@
import React from "react"; import React from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { subscriptionListFilter } from "../App-State.ts"; import { subscriptionListFilter } from "../App-Functions.ts";
import {
totalNamesPublished,
totalVideosPublished,
videosPerNamePublished,
} from "../components/StatsData.tsx";
import { import {
addVideos, addVideos,
addToHashMap, addToHashMap,
@ -11,13 +16,7 @@ import {
upsertFilteredVideos, upsertFilteredVideos,
removeFromHashMap, removeFromHashMap,
} from "../state/features/videoSlice"; } from "../state/features/videoSlice";
import { import { setIsLoadingGlobal } from "../state/features/globalSlice";
setIsLoadingGlobal,
setUserAvatarHash,
setTotalVideosPublished,
setTotalNamesPublished,
setVideosPerNamePublished,
} from "../state/features/globalSlice";
import { RootState } from "../state/store"; import { RootState } from "../state/store";
import { fetchAndEvaluateVideos } from "../utils/fetchVideos"; import { fetchAndEvaluateVideos } from "../utils/fetchVideos";
import { RequestQueue } from "../utils/queue"; import { RequestQueue } from "../utils/queue";
@ -390,14 +389,11 @@ export const useFetchVideos = () => {
}); });
const responseData = await response.json(); const responseData = await response.json();
const totalVideosPublished = responseData.length; totalVideosPublished.value = responseData.length;
const uniqueNames = new Set(responseData.map(video => video.name)); const uniqueNames = new Set(responseData.map(video => video.name));
const totalNamesPublished = uniqueNames.size; totalNamesPublished.value = uniqueNames.size;
const videosPerNamePublished = totalVideosPublished / totalNamesPublished; videosPerNamePublished.value =
totalVideosPublished.value / totalNamesPublished.value;
dispatch(setTotalVideosPublished(totalVideosPublished));
dispatch(setTotalNamesPublished(totalNamesPublished));
dispatch(setVideosPerNamePublished(videosPerNamePublished));
} catch (error) { } catch (error) {
console.log({ error }); console.log({ error });
} }

View File

@ -1,5 +1,6 @@
import { styled } from "@mui/system"; import { styled } from "@mui/system";
import { Box, Grid, Typography, Checkbox } from "@mui/material"; import { Box, Grid, Typography, Checkbox } from "@mui/material";
import { fontSizeMedium } from "../../../constants/Misc.ts";
export const VideoContentContainer = styled(Box)(({ theme }) => ({ export const VideoContentContainer = styled(Box)(({ theme }) => ({
display: "flex", display: "flex",
@ -14,7 +15,7 @@ export const VideoPlayerContainer = styled(Box)(({ theme }) => ({
export const VideoTitle = styled(Typography)(({ theme }) => ({ export const VideoTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Raleway", fontFamily: "Raleway",
fontSize: "20px", fontSize: fontSizeMedium,
color: theme.palette.text.primary, color: theme.palette.text.primary,
wordBreak: "break-word", wordBreak: "break-word",
})); }));

View File

@ -14,6 +14,7 @@ import {
SUPER_LIKE_BASE, SUPER_LIKE_BASE,
} from "../../../constants/Identifiers.ts"; } from "../../../constants/Identifiers.ts";
import { import {
fontSizeMedium,
minPriceSuperlike, minPriceSuperlike,
titleFormatterOnSave, titleFormatterOnSave,
} from "../../../constants/Misc.ts"; } from "../../../constants/Misc.ts";
@ -21,6 +22,7 @@ import { useFetchSuperLikes } from "../../../hooks/useFetchSuperLikes.tsx";
import { setIsLoadingGlobal } from "../../../state/features/globalSlice.ts"; import { setIsLoadingGlobal } from "../../../state/features/globalSlice.ts";
import { addToHashMap } from "../../../state/features/videoSlice.ts"; import { addToHashMap } from "../../../state/features/videoSlice.ts";
import { RootState } from "../../../state/store.ts"; import { RootState } from "../../../state/store.ts";
import { formatBytes } from "../../../utils/numberFunctions.ts";
import { formatDate } from "../../../utils/time.ts"; import { formatDate } from "../../../utils/time.ts";
import { VideoActionsBar } from "./VideoActionsBar.tsx"; import { VideoActionsBar } from "./VideoActionsBar.tsx";
import { import {
@ -62,7 +64,6 @@ export const VideoContent = () => {
superLikeList, superLikeList,
setSuperLikeList, setSuperLikeList,
} = useVideoContentState(); } = useVideoContentState();
return ( return (
<> <>
<Box <Box
@ -87,6 +88,7 @@ export const VideoContent = () => {
videoContainer: { aspectRatio: "16 / 9" }, videoContainer: { aspectRatio: "16 / 9" },
video: { aspectRatio: "16 / 9" }, video: { aspectRatio: "16 / 9" },
}} }}
duration={videoData?.duration}
/> />
</VideoPlayerContainer> </VideoPlayerContainer>
) : isVideoLoaded ? ( ) : isVideoLoaded ? (
@ -128,7 +130,17 @@ export const VideoContent = () => {
{videoData?.title} {videoData?.title}
</VideoTitle> </VideoTitle>
</Box> </Box>
{videoData?.fileSize && (
<Typography
variant="h1"
sx={{
fontSize: "90%",
}}
color={"green"}
>
{formatBytes(videoData.fileSize, 2, "Decimal")}
</Typography>
)}
{videoData?.created && ( {videoData?.created && (
<Typography <Typography
variant="h6" variant="h6"
@ -140,7 +152,6 @@ export const VideoContent = () => {
{formatDate(videoData.created)} {formatDate(videoData.created)}
</Typography> </Typography>
)} )}
<Spacer height="30px" /> <Spacer height="30px" />
{videoData?.fullDescription && ( {videoData?.fullDescription && (
<Box <Box

View File

@ -1,6 +1,6 @@
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 { Avatar, Box, Tooltip, useTheme } from "@mui/material"; import { Avatar, Box, Tooltip, Typography, useTheme } from "@mui/material";
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@ -14,6 +14,7 @@ import {
Video, Video,
} from "../../../state/features/videoSlice.ts"; } from "../../../state/features/videoSlice.ts";
import { RootState } from "../../../state/store.ts"; import { RootState } from "../../../state/store.ts";
import { formatTime } from "../../../utils/numberFunctions.ts";
import { formatDate } from "../../../utils/time.ts"; import { formatDate } from "../../../utils/time.ts";
import { VideoCardImageContainer } from "./VideoCardImageContainer.tsx"; import { VideoCardImageContainer } from "./VideoCardImageContainer.tsx";
import { import {
@ -74,7 +75,6 @@ export const VideoList = ({ videos }: VideoListProps) => {
videoObj = existingVideo; videoObj = existingVideo;
hasHash = true; hasHash = true;
} }
// nb. this prevents showing metadata for a video which // nb. this prevents showing metadata for a video which
// belongs to a different user // belongs to a different user
if ( if (
@ -147,7 +147,6 @@ export const VideoList = ({ videos }: VideoListProps) => {
/> />
<VideoCardTitle>{videoObj?.title}</VideoCardTitle> <VideoCardTitle>{videoObj?.title}</VideoCardTitle>
<BottomParent> <BottomParent>
<NameContainer <NameContainer
onClick={e => { onClick={e => {
@ -237,6 +236,19 @@ export const VideoList = ({ videos }: VideoListProps) => {
navigate(`/video/${videoObj?.user}/${videoObj?.id}`); navigate(`/video/${videoObj?.user}/${videoObj?.id}`);
}} }}
> >
{videoObj?.duration && (
<Box
position="absolute"
right={0}
bottom={0}
bgcolor="#202020"
zIndex={999}
>
<Typography color="white">
{formatTime(videoObj.duration)}
</Typography>
</Box>
)}
<VideoCardImageContainer <VideoCardImageContainer
width={266} width={266}
height={150} height={150}

View File

@ -1,16 +1,12 @@
import { createSlice } from '@reduxjs/toolkit' import { createSlice } from "@reduxjs/toolkit";
interface GlobalState { interface GlobalState {
isLoadingGlobal: boolean isLoadingGlobal: boolean;
downloads: any downloads: any;
userAvatarHash: Record<string, string> userAvatarHash: Record<string, string>;
publishNames: string[] | null publishNames: string[] | null;
videoPlaying: any | null videoPlaying: any | null;
superlikelistAll: any[] superlikelistAll: any[];
totalVideosPublished: number
totalNamesPublished: number
videosPerNamePublished: number
} }
const initialState: GlobalState = { const initialState: GlobalState = {
isLoadingGlobal: false, isLoadingGlobal: false,
@ -19,56 +15,44 @@ const initialState: GlobalState = {
publishNames: null, publishNames: null,
videoPlaying: null, videoPlaying: null,
superlikelistAll: [], superlikelistAll: [],
totalVideosPublished: null, };
totalNamesPublished: null,
videosPerNamePublished: null
}
export const globalSlice = createSlice({ export const globalSlice = createSlice({
name: 'global', name: "global",
initialState, initialState,
reducers: { reducers: {
setIsLoadingGlobal: (state, action) => { setIsLoadingGlobal: (state, action) => {
state.isLoadingGlobal = action.payload state.isLoadingGlobal = action.payload;
}, },
setAddToDownloads: (state, action) => { setAddToDownloads: (state, action) => {
const download = action.payload const download = action.payload;
state.downloads[download.identifier] = download state.downloads[download.identifier] = download;
}, },
updateDownloads: (state, action) => { updateDownloads: (state, action) => {
const { identifier } = action.payload const { identifier } = action.payload;
const download = action.payload const download = action.payload;
state.downloads[identifier] = { state.downloads[identifier] = {
...state.downloads[identifier], ...state.downloads[identifier],
...download ...download,
} };
}, },
setUserAvatarHash: (state, action) => { setUserAvatarHash: (state, action) => {
const avatar = action.payload const avatar = action.payload;
if (avatar?.name && avatar?.url) { if (avatar?.name && avatar?.url) {
state.userAvatarHash[avatar?.name] = avatar?.url state.userAvatarHash[avatar?.name] = avatar?.url;
} }
}, },
addPublishNames: (state, action) => { addPublishNames: (state, action) => {
state.publishNames = action.payload state.publishNames = action.payload;
}, },
setVideoPlaying: (state, action) => { setVideoPlaying: (state, action) => {
state.videoPlaying = action.payload state.videoPlaying = action.payload;
}, },
setSuperlikesAll: (state, action) => { setSuperlikesAll: (state, action) => {
state.superlikelistAll = action.payload state.superlikelistAll = action.payload;
}, },
setTotalVideosPublished: (state, action) => {
state.totalVideosPublished = action.payload
}, },
setTotalNamesPublished: (state, action) => { });
state.totalNamesPublished = action.payload
},
setVideosPerNamePublished: (state, action) => {
state.videosPerNamePublished = action.payload
},
}
})
export const { export const {
setIsLoadingGlobal, setIsLoadingGlobal,
@ -78,9 +62,6 @@ export const {
addPublishNames, addPublishNames,
setVideoPlaying, setVideoPlaying,
setSuperlikesAll, setSuperlikesAll,
setTotalVideosPublished, } = globalSlice.actions;
setTotalNamesPublished,
setVideosPerNamePublished
} = globalSlice.actions
export default globalSlice.reducer export default globalSlice.reducer;

View File

@ -1,5 +1,3 @@
export const truncateNumber = (value: string | number, sigDigits: number) => { export const truncateNumber = (value: string | number, sigDigits: number) => {
return Number(value).toFixed(sigDigits); return Number(value).toFixed(sigDigits);
}; };
@ -21,3 +19,45 @@ export const setNumberWithinBounds = (
export const numberToInt = (num: number) => { export const numberToInt = (num: number) => {
return Math.floor(num); return Math.floor(num);
}; };
type ByteFormat = "Decimal" | "Binary";
export function formatBytes(
bytes: number,
decimals = 2,
format: ByteFormat = "Binary"
) {
if (bytes === 0) return "0 Bytes";
const k = format === "Binary" ? 1024 : 1000;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
}
export function formatTime(seconds: number): string {
seconds = Math.floor(seconds);
const minutes: number | string = Math.floor(seconds / 60);
let hours: number | string = Math.floor(minutes / 60);
let remainingSeconds: number | string = seconds % 60;
let remainingMinutes: number | string = minutes % 60;
if (remainingSeconds < 10) {
remainingSeconds = "0" + remainingSeconds;
}
if (remainingMinutes < 10) {
remainingMinutes = "0" + remainingMinutes;
}
if (hours === 0) {
hours = "";
} else {
hours = hours + ":";
}
return hours + remainingMinutes + ":" + remainingSeconds;
}