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:
commit
eebf4fce78
@ -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
|
14
src/App.tsx
14
src/App.tsx
@ -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}>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 (
|
||||
|
@ -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;
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -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",
|
||||
}));
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user