3
0
mirror of https://github.com/Qortal/q-tube.git synced 2025-02-11 17:55:51 +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 { 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,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 [durations, setDurations] = useState([]);
const canvasRef = useRef(null);
useEffect(() => {
const video = videoRef.current;
video.addEventListener('loadedmetadata', () => {
video.addEventListener("loadedmetadata", () => {
const duration = video.duration;
if (isFinite(duration)) {
// Proceed with your logic
console.log('duration', duration)
const newVideoDurations = [...videoDurations.value];
newVideoDurations[index] = duration;
videoDurations.value = [...newVideoDurations];
const section = duration / 4;
let timestamps = [];
const timestamps = [];
for (let i = 0; i < 4; i++) {
const randomTime = Math.random() * section + i * section;
@ -23,13 +41,11 @@ export const FrameExtractor = ({ videoFile, onFramesExtracted }) => {
setDurations(timestamps);
} else {
onFramesExtracted([])
onFramesExtracted([]);
}
});
}, [videoFile]);
console.log({durations})
useEffect(() => {
if (durations.length === 4) {
extractFrames();
@ -45,33 +61,32 @@ export const FrameExtractor = ({ videoFile, onFramesExtracted }) => {
const canvas = canvasRef.current;
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const context = canvas.getContext('2d');
const context = canvas.getContext("2d");
let frameData = [];
const frameData = [];
for (const time of durations) {
await new Promise<void>((resolve) => {
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);
}, "image/png");
video.removeEventListener("seeked", onSeeked);
};
video.addEventListener('seeked', onSeeked, { once: true });
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>
<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,7 +85,20 @@ export const LoadingVideo = () => {
)}
</Box>
)}
{((!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}
@ -113,6 +128,7 @@ export const LoadingVideo = () => {
}}
/>
</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;
}