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:
parent
9614b1c132
commit
ee96d52f0e
@ -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,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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -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