3
0
mirror of https://github.com/Qortal/q-tube.git synced 2025-02-16 04:05:52 +00:00

Fixed bug that starts videos at normal speed instead of saved playbackRate.

Removed unnecessary useSignals() hook from components.

StatsData.tsx uses Signals instead of Redux.

New videos or edited videos that change the source file now display how long the video is and its file size.
This commit is contained in:
Qortal Dev 2024-11-16 07:57:30 -07:00
parent 9614b1c132
commit ee96d52f0e
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;
}