3
0
mirror of https://github.com/Qortal/q-tube.git synced 2025-02-14 11:15: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 { setFilteredSubscriptions } from "./state/features/videoSlice.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 () => {
const account = await qortalRequest({
@ -28,6 +13,7 @@ export const getUserName = async () => {
if (nameData?.length > 0) return nameData[0].name;
else return "";
};
export const filterVideosByName = (
subscriptionList: SubscriptionData[],
userName: string

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { Button, ButtonProps, Tooltip } from "@mui/material";
import { MouseEvent, useEffect, useState } from "react";
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 {
subscribe,

View File

@ -1,77 +1,92 @@
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 }) => {
const videoRef = useRef(null);
const [durations, setDurations] = useState([]);
const canvasRef = useRef(null);
export interface FrameExtractorProps {
videoFile: File;
onFramesExtracted: (imgs, index?: number) => Promise<void>;
videoDurations?: Signal<number[]>;
index?: number;
}
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 = [];
export const FrameExtractor = ({
videoFile,
onFramesExtracted,
videoDurations,
index,
}: FrameExtractorProps) => {
useSignals();
const videoRef = useRef(null);
const [durations, setDurations] = useState([]);
const canvasRef = useRef(null);
for (let i = 0; i < 4; i++) {
const randomTime = Math.random() * section + i * section;
timestamps.push(randomTime);
}
useEffect(() => {
const video = videoRef.current;
video.addEventListener("loadedmetadata", () => {
const duration = video.duration;
if (isFinite(duration)) {
// Proceed with your logic
setDurations(timestamps);
} else {
onFramesExtracted([])
const newVideoDurations = [...videoDurations.value];
newVideoDurations[index] = duration;
videoDurations.value = [...newVideoDurations];
const section = duration / 4;
const timestamps = [];
for (let i = 0; i < 4; i++) {
const randomTime = Math.random() * section + i * section;
timestamps.push(randomTime);
}
});
}, [videoFile]);
console.log({durations})
setDurations(timestamps);
} else {
onFramesExtracted([]);
}
});
}, [videoFile]);
useEffect(() => {
if (durations.length === 4) {
extractFrames();
}
}, [durations]);
useEffect(() => {
if (durations.length === 4) {
extractFrames();
}
}, [durations]);
const fileUrl = useMemo(() => {
return URL.createObjectURL(videoFile);
}, [videoFile]);
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<void>((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);
};
const extractFrames = async () => {
const video = videoRef.current;
const canvas = canvasRef.current;
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const context = canvas.getContext("2d");
return (
<div>
<video ref={videoRef} style={{ display: 'none' }} src={fileUrl}></video>
<canvas ref={canvasRef} style={{ display: 'none' }}></canvas>
</div>
);
const frameData = [];
for (const time of durations) {
await new Promise<void>(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>
);
};

View File

@ -2,6 +2,7 @@ import { Box, CircularProgress, Typography } from "@mui/material";
import { setVideoPlaying } from "../../../../state/features/globalSlice.ts";
import { useDispatch } from "react-redux";
import { PlayArrow } from "@mui/icons-material";
import { formatTime } from "../../../../utils/numberFunctions.ts";
import { useVideoContext } from "./VideoContext.ts";
export const LoadingVideo = () => {
@ -13,6 +14,7 @@ export const LoadingVideo = () => {
canPlay,
from,
togglePlay,
duration,
} = useVideoContext();
const getDownloadProgress = (current: number, total: number) => {
@ -83,36 +85,50 @@ export const LoadingVideo = () => {
)}
</Box>
)}
{((!src && !isLoading.value) || (!startPlay.value && !canPlay.value)) && (
<Box
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
display="flex"
justifyContent="center"
alignItems="center"
zIndex={500}
bgcolor="rgba(0, 0, 0, 0.6)"
onClick={() => {
if (from === "create") return;
dispatch(setVideoPlaying(null));
togglePlay();
}}
sx={{
cursor: "pointer",
}}
>
<PlayArrow
sx={{
width: "50px",
height: "50px",
color: "white",
{((!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
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
display="flex"
justifyContent="center"
alignItems="center"
zIndex={500}
bgcolor="rgba(0, 0, 0, 0.6)"
onClick={() => {
if (from === "create") return;
dispatch(setVideoPlaying(null));
togglePlay();
}}
/>
</Box>
sx={{
cursor: "pointer",
}}
>
<PlayArrow
sx={{
width: "50px",
height: "50px",
color: "white",
}}
/>
</Box>
</>
)}
</>
);

View File

@ -1,5 +1,5 @@
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 ReactDOM from "react-dom";
@ -35,6 +35,7 @@ export const useVideoControlsState = (
progress,
videoObjectFit,
canPlay,
isMobileView,
} = videoPlayerState;
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 () => {
if (!videoRef.current || !src) return;
const currentTime = videoRef.current.currentTime;
@ -154,6 +130,7 @@ export const useVideoControlsState = (
if (firstPlay.value) setPlaying(true); // makes the video play when fully loaded
firstPlay.value = false;
isLoading.value = false;
updatePlaybackRate(persistSelector.playbackRate);
};
const setPlaying = async (setPlay: boolean) => {
@ -380,6 +357,14 @@ export const useVideoControlsState = (
}
}, [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 {
reloadVideo,
togglePlay,
@ -387,7 +372,6 @@ export const useVideoControlsState = (
increaseSpeed,
togglePictureInPicture,
toggleFullscreen,
formatTime,
keyboardShortcutsUp,
keyboardShortcutsDown,
handleCanPlay,

View File

@ -8,15 +8,13 @@ import {
VolumeUp,
} from "@mui/icons-material";
import { IconButton, Slider, Typography } from "@mui/material";
import { useSignalEffect, useSignals } from "@preact/signals-react/runtime";
import { useEffect } from "react";
import { formatTime } from "../../../../utils/numberFunctions.ts";
import { ControlsContainer } from "../VideoPlayer-styles.ts";
import { MobileControls } from "./MobileControls.tsx";
import { useVideoContext } from "./VideoContext.ts";
export const VideoControls = () => {
useSignals();
const {
reloadVideo,
togglePlay,
@ -24,7 +22,6 @@ export const VideoControls = () => {
increaseSpeed,
togglePictureInPicture,
toggleFullscreen,
formatTime,
toggleMute,
onProgressChange,
toggleRef,
@ -40,14 +37,6 @@ export const VideoControls = () => {
showControlsFullScreen,
} = 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 =
isMobileView.value && canPlay.value && showControlsFullScreen.value;

View File

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

View File

@ -12,6 +12,7 @@ import {
VolumeOff,
} from "@mui/icons-material";
import { styled } from "@mui/system";
import { formatTime } from "../../../utils/numberFunctions.ts";
import { MyContext } from "../../../wrappers/DownloadWrapper.tsx";
import { useDispatch, useSelector } from "react-redux";
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 () => {
if (!videoRef.current) return;
const src = videoRef.current.src;

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import BlockIcon from "@mui/icons-material/Block";
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 { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
@ -14,6 +14,7 @@ import {
Video,
} from "../../../state/features/videoSlice.ts";
import { RootState } from "../../../state/store.ts";
import { formatTime } from "../../../utils/numberFunctions.ts";
import { formatDate } from "../../../utils/time.ts";
import { VideoCardImageContainer } from "./VideoCardImageContainer.tsx";
import {
@ -74,7 +75,6 @@ export const VideoList = ({ videos }: VideoListProps) => {
videoObj = existingVideo;
hasHash = true;
}
// nb. this prevents showing metadata for a video which
// belongs to a different user
if (
@ -147,7 +147,6 @@ export const VideoList = ({ videos }: VideoListProps) => {
/>
<VideoCardTitle>{videoObj?.title}</VideoCardTitle>
<BottomParent>
<NameContainer
onClick={e => {
@ -237,6 +236,19 @@ export const VideoList = ({ videos }: VideoListProps) => {
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
width={266}
height={150}

View File

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

View File

@ -1,5 +1,3 @@
export const truncateNumber = (value: string | number, sigDigits: number) => {
return Number(value).toFixed(sigDigits);
};
@ -21,3 +19,45 @@ export const setNumberWithinBounds = (
export const numberToInt = (num: number) => {
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;
}