From 9cf2932d8101885352c38a7e336cf28d8aa68e86 Mon Sep 17 00:00:00 2001 From: Qortal Seth Date: Tue, 26 Mar 2024 17:11:15 -0600 Subject: [PATCH] Follow, Like, and Dislike buttons Added to Video, Playlist, and Channel pages The Subscribe and Follow buttons no longer appear when the user views their own videos Minimum Superlike amount lowered from 10 to 1 QORT Fixed bug that made searches in the Home page Subscriptions Tab return results from all videos. Fixed bug that prevented filtering by name on Subscriptions Tab Clicking in area around video gives it focus, allowing hotkeys to work, orange border around video when focused is removed Subscription tab doesn't say "You have no subscriptions" while loading videos --- src/App.tsx | 8 +- src/components/Playlists/Playlists.tsx | 2 +- .../EditPlaylist/EditPlaylist.tsx | 24 +- .../EditPlaylist/Upload-styles.tsx | 28 +- .../EditVideo/EditVideo-styles.tsx | 28 +- .../{ => Publish}/EditVideo/EditVideo.tsx | 24 +- .../MultiplePublish/MultiplePublishAll.tsx | 4 +- .../PlaylistListEdit/PlaylistListEdit.tsx | 8 +- .../PublishVideo/PublishVideo-styles.tsx | 2 +- .../PublishVideo/PublishVideo.tsx | 32 +- src/components/StatsData.tsx | 7 +- .../common/Comments/CommentSection.tsx | 2 +- .../common/ContentButtons/FollowButton.tsx | 159 ++ .../LikeAndDislike-functions.ts | 66 + .../common/ContentButtons/LikeAndDislike.tsx | 230 +++ .../{ => ContentButtons}/SubscribeButton.tsx | 48 +- .../SuperLike.tsx | 97 +- .../common/Notifications/Notifications.tsx | 2 +- .../SuperLikesList/SuperLikesSection.tsx | 2 +- .../common/VideoPlayer/VideoPlayer-styles.ts | 25 +- .../common/VideoPlayer/VideoPlayer.tsx | 1759 +++++++++-------- src/components/layout/Navbar/Navbar.tsx | 4 +- src/constants/Categories.ts | 11 +- src/constants/Identifiers.ts | 4 + src/constants/Misc.ts | 5 +- src/hooks/useFetchVideos.tsx | 24 +- .../IndividualProfile/IndividualProfile.tsx | 45 +- .../IndividualProfile/Profile-styles.tsx | 0 .../PlaylistContent-styles.tsx | 0 .../PlaylistContent/PlaylistContent.tsx | 139 +- .../VideoContent/VideoContent-styles.tsx | 0 .../VideoContent/VideoContent.tsx | 143 +- src/pages/Home/Home.tsx | 27 +- src/state/features/persistSlice.ts | 21 +- src/state/features/videoSlice.ts | 2 +- src/utils/qortalRequestFunctions.ts | 28 + src/utils/qortalRequestTypes.ts | 17 + src/wrappers/GlobalWrapper.tsx | 22 +- 38 files changed, 1839 insertions(+), 1210 deletions(-) rename src/components/{ => Publish}/EditPlaylist/EditPlaylist.tsx (96%) rename src/components/{ => Publish}/EditPlaylist/Upload-styles.tsx (97%) rename src/components/{ => Publish}/EditVideo/EditVideo-styles.tsx (97%) rename src/components/{ => Publish}/EditVideo/EditVideo.tsx (95%) rename src/components/{common => Publish}/MultiplePublish/MultiplePublishAll.tsx (98%) rename src/components/{ => Publish}/PlaylistListEdit/PlaylistListEdit.tsx (95%) rename src/components/{ => Publish}/PublishVideo/PublishVideo-styles.tsx (99%) rename src/components/{ => Publish}/PublishVideo/PublishVideo.tsx (97%) create mode 100644 src/components/common/ContentButtons/FollowButton.tsx create mode 100644 src/components/common/ContentButtons/LikeAndDislike-functions.ts create mode 100644 src/components/common/ContentButtons/LikeAndDislike.tsx rename src/components/common/{ => ContentButtons}/SubscribeButton.tsx (70%) rename src/components/common/{SuperLike => ContentButtons}/SuperLike.tsx (86%) rename src/pages/{ => ContentPages}/IndividualProfile/IndividualProfile.tsx (53%) rename src/pages/{ => ContentPages}/IndividualProfile/Profile-styles.tsx (100%) rename src/pages/{ => ContentPages}/PlaylistContent/PlaylistContent-styles.tsx (100%) rename src/pages/{ => ContentPages}/PlaylistContent/PlaylistContent.tsx (82%) rename src/pages/{ => ContentPages}/VideoContent/VideoContent-styles.tsx (100%) rename src/pages/{ => ContentPages}/VideoContent/VideoContent.tsx (79%) diff --git a/src/App.tsx b/src/App.tsx index 455ad31..84231f3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,14 +8,14 @@ import { Provider } from "react-redux"; import GlobalWrapper from "./wrappers/GlobalWrapper"; import Notification from "./components/common/Notification/Notification"; import { Home } from "./pages/Home/Home"; -import { VideoContent } from "./pages/VideoContent/VideoContent"; +import { VideoContent } from "./pages/ContentPages/VideoContent/VideoContent"; import DownloadWrapper from "./wrappers/DownloadWrapper"; -import { IndividualProfile } from "./pages/IndividualProfile/IndividualProfile"; -import { PlaylistContent } from "./pages/PlaylistContent/PlaylistContent"; +import { IndividualProfile } from "./pages/ContentPages/IndividualProfile/IndividualProfile"; +import { PlaylistContent } from "./pages/ContentPages/PlaylistContent/PlaylistContent"; import { PersistGate } from "redux-persist/integration/react"; import { persistStore } from "redux-persist"; import { setFilteredSubscriptions } from "./state/features/videoSlice.ts"; -import { SubscriptionData } from "./components/common/SubscribeButton.tsx"; +import { SubscriptionData } from "./components/common/ContentButtons/SubscribeButton.tsx"; export const getUserName = async () => { const account = await qortalRequest({ diff --git a/src/components/Playlists/Playlists.tsx b/src/components/Playlists/Playlists.tsx index 046863b..7834c55 100644 --- a/src/components/Playlists/Playlists.tsx +++ b/src/components/Playlists/Playlists.tsx @@ -3,7 +3,7 @@ import { CardContentContainerComment } from "../common/Comments/Comments-styles" import { CrowdfundSubTitle, CrowdfundSubTitleRow, -} from "../PublishVideo/PublishVideo-styles.tsx"; +} from "../Publish/PublishVideo/PublishVideo-styles.tsx"; import { Box, Typography, useTheme } from "@mui/material"; import { useNavigate } from "react-router-dom"; diff --git a/src/components/EditPlaylist/EditPlaylist.tsx b/src/components/Publish/EditPlaylist/EditPlaylist.tsx similarity index 96% rename from src/components/EditPlaylist/EditPlaylist.tsx rename to src/components/Publish/EditPlaylist/EditPlaylist.tsx index d224dc2..52a4c5e 100644 --- a/src/components/EditPlaylist/EditPlaylist.tsx +++ b/src/components/Publish/EditPlaylist/EditPlaylist.tsx @@ -12,7 +12,7 @@ import { NewCrowdfundTitle, StyledButton, TimesIcon, -} from "./Upload-styles"; +} from "./Upload-styles.tsx"; import { Box, FormControl, @@ -30,9 +30,9 @@ import { useDispatch, useSelector } from "react-redux"; import AddBoxIcon from "@mui/icons-material/AddBox"; import { useDropzone } from "react-dropzone"; -import { setNotification } from "../../state/features/notificationsSlice"; -import { objectToBase64, uint8ArrayToBase64 } from "../../utils/toBase64"; -import { RootState } from "../../state/store"; +import { setNotification } from "../../../state/features/notificationsSlice.ts"; +import { objectToBase64, uint8ArrayToBase64 } from "../../../utils/toBase64.ts"; +import { RootState } from "../../../state/store.ts"; import { upsertVideosBeginning, addToHashMap, @@ -41,17 +41,17 @@ import { updateVideo, updateInHashMap, setEditPlaylist, -} from "../../state/features/videoSlice"; -import ImageUploader from "../common/ImageUploader"; -import { categories, subCategories } from "../../constants/Categories.ts"; -import { Playlists } from "../Playlists/Playlists"; -import { PlaylistListEdit } from "../PlaylistListEdit/PlaylistListEdit"; -import { TextEditor } from "../common/TextEditor/TextEditor"; -import { extractTextFromHTML } from "../common/TextEditor/utils"; +} from "../../../state/features/videoSlice.ts"; +import ImageUploader from "../../common/ImageUploader.tsx"; +import { categories, subCategories } from "../../../constants/Categories.ts"; +import { Playlists } from "../../Playlists/Playlists.tsx"; +import { PlaylistListEdit } from "../PlaylistListEdit/PlaylistListEdit.tsx"; +import { TextEditor } from "../../common/TextEditor/TextEditor.tsx"; +import { extractTextFromHTML } from "../../common/TextEditor/utils.ts"; import { QTUBE_PLAYLIST_BASE, QTUBE_VIDEO_BASE, -} from "../../constants/Identifiers.ts"; +} from "../../../constants/Identifiers.ts"; const uid = new ShortUniqueId(); const shortuid = new ShortUniqueId({ length: 5 }); diff --git a/src/components/EditPlaylist/Upload-styles.tsx b/src/components/Publish/EditPlaylist/Upload-styles.tsx similarity index 97% rename from src/components/EditPlaylist/Upload-styles.tsx rename to src/components/Publish/EditPlaylist/Upload-styles.tsx index b1b8813..53cb8e0 100644 --- a/src/components/EditPlaylist/Upload-styles.tsx +++ b/src/components/Publish/EditPlaylist/Upload-styles.tsx @@ -9,10 +9,10 @@ import { Rating, TextField, Typography, - Select + Select, } from "@mui/material"; import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate"; -import { TimesSVG } from "../../assets/svgs/TimesSVG"; +import { TimesSVG } from "../../../assets/svgs/TimesSVG.tsx"; export const DoubleLine = styled(Typography)` display: -webkit-box; @@ -159,8 +159,6 @@ export const CustomInputField = styled(TextField)(({ theme }) => ({ }, })); - - export const CrowdfundTitle = styled(Typography)(({ theme }) => ({ fontFamily: "Copse", letterSpacing: "1px", @@ -539,8 +537,8 @@ export const NoReviewsFont = styled(Typography)(({ theme }) => ({ export const StyledButton = styled(Button)(({ theme }) => ({ fontWeight: 600, - color: theme.palette.text.primary -})) + color: theme.palette.text.primary, +})); export const CustomSelect = styled(Select)(({ theme }) => ({ fontFamily: "Mulish", @@ -549,34 +547,34 @@ export const CustomSelect = styled(Select)(({ theme }) => ({ fontWeight: 400, color: theme.palette.text.primary, backgroundColor: theme.palette.background.default, - '& .MuiSelect-select': { - padding: '12px', + "& .MuiSelect-select": { + padding: "12px", fontFamily: "Mulish", fontSize: "19px", letterSpacing: "0px", fontWeight: 400, borderRadius: theme.shape.borderRadius, // Match border radius }, - '&:before': { + "&:before": { // Underline style borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf", }, - '&:after': { + "&:after": { // Underline style when focused borderBottomColor: theme.palette.secondary.main, }, - '& .MuiOutlinedInput-root': { - '& fieldset': { + "& .MuiOutlinedInput-root": { + "& fieldset": { borderColor: "#E0E3E7", }, - '&:hover fieldset': { + "&:hover fieldset": { borderColor: "#B2BAC2", }, - '&.Mui-focused fieldset': { + "&.Mui-focused fieldset": { borderColor: "#6F7E8C", }, }, - '& .MuiInputBase-root': { + "& .MuiInputBase-root": { fontFamily: "Mulish", fontSize: "19px", letterSpacing: "0px", diff --git a/src/components/EditVideo/EditVideo-styles.tsx b/src/components/Publish/EditVideo/EditVideo-styles.tsx similarity index 97% rename from src/components/EditVideo/EditVideo-styles.tsx rename to src/components/Publish/EditVideo/EditVideo-styles.tsx index b1b8813..53cb8e0 100644 --- a/src/components/EditVideo/EditVideo-styles.tsx +++ b/src/components/Publish/EditVideo/EditVideo-styles.tsx @@ -9,10 +9,10 @@ import { Rating, TextField, Typography, - Select + Select, } from "@mui/material"; import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate"; -import { TimesSVG } from "../../assets/svgs/TimesSVG"; +import { TimesSVG } from "../../../assets/svgs/TimesSVG.tsx"; export const DoubleLine = styled(Typography)` display: -webkit-box; @@ -159,8 +159,6 @@ export const CustomInputField = styled(TextField)(({ theme }) => ({ }, })); - - export const CrowdfundTitle = styled(Typography)(({ theme }) => ({ fontFamily: "Copse", letterSpacing: "1px", @@ -539,8 +537,8 @@ export const NoReviewsFont = styled(Typography)(({ theme }) => ({ export const StyledButton = styled(Button)(({ theme }) => ({ fontWeight: 600, - color: theme.palette.text.primary -})) + color: theme.palette.text.primary, +})); export const CustomSelect = styled(Select)(({ theme }) => ({ fontFamily: "Mulish", @@ -549,34 +547,34 @@ export const CustomSelect = styled(Select)(({ theme }) => ({ fontWeight: 400, color: theme.palette.text.primary, backgroundColor: theme.palette.background.default, - '& .MuiSelect-select': { - padding: '12px', + "& .MuiSelect-select": { + padding: "12px", fontFamily: "Mulish", fontSize: "19px", letterSpacing: "0px", fontWeight: 400, borderRadius: theme.shape.borderRadius, // Match border radius }, - '&:before': { + "&:before": { // Underline style borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf", }, - '&:after': { + "&:after": { // Underline style when focused borderBottomColor: theme.palette.secondary.main, }, - '& .MuiOutlinedInput-root': { - '& fieldset': { + "& .MuiOutlinedInput-root": { + "& fieldset": { borderColor: "#E0E3E7", }, - '&:hover fieldset': { + "&:hover fieldset": { borderColor: "#B2BAC2", }, - '&.Mui-focused fieldset': { + "&.Mui-focused fieldset": { borderColor: "#6F7E8C", }, }, - '& .MuiInputBase-root': { + "& .MuiInputBase-root": { fontFamily: "Mulish", fontSize: "19px", letterSpacing: "0px", diff --git a/src/components/EditVideo/EditVideo.tsx b/src/components/Publish/EditVideo/EditVideo.tsx similarity index 95% rename from src/components/EditVideo/EditVideo.tsx rename to src/components/Publish/EditVideo/EditVideo.tsx index 56cd622..a5fa0cc 100644 --- a/src/components/EditVideo/EditVideo.tsx +++ b/src/components/Publish/EditVideo/EditVideo.tsx @@ -34,9 +34,9 @@ import { useDispatch, useSelector } from "react-redux"; import AddBoxIcon from "@mui/icons-material/AddBox"; import { useDropzone } from "react-dropzone"; -import { setNotification } from "../../state/features/notificationsSlice"; -import { objectToBase64, uint8ArrayToBase64 } from "../../utils/toBase64"; -import { RootState } from "../../state/store"; +import { setNotification } from "../../../state/features/notificationsSlice.ts"; +import { objectToBase64, uint8ArrayToBase64 } from "../../../utils/toBase64.ts"; +import { RootState } from "../../../state/store.ts"; import { upsertVideosBeginning, addToHashMap, @@ -44,16 +44,16 @@ import { setEditVideo, updateVideo, updateInHashMap, -} from "../../state/features/videoSlice"; -import ImageUploader from "../common/ImageUploader"; -import { categories, subCategories } from "../../constants/Categories.ts"; -import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll"; -import { TextEditor } from "../common/TextEditor/TextEditor"; -import { extractTextFromHTML } from "../common/TextEditor/utils"; +} from "../../../state/features/videoSlice.ts"; +import ImageUploader from "../../common/ImageUploader.tsx"; +import { categories, subCategories } from "../../../constants/Categories.ts"; +import { MultiplePublish } from "../MultiplePublish/MultiplePublishAll.tsx"; +import { TextEditor } from "../../common/TextEditor/TextEditor.tsx"; +import { extractTextFromHTML } from "../../common/TextEditor/utils.ts"; import { toBase64 } from "../PublishVideo/PublishVideo.tsx"; -import { FrameExtractor } from "../common/FrameExtractor/FrameExtractor"; -import { QTUBE_VIDEO_BASE } from "../../constants/Identifiers.ts"; -import { titleFormatter } from "../../constants/Misc.ts"; +import { FrameExtractor } from "../../common/FrameExtractor/FrameExtractor.tsx"; +import { QTUBE_VIDEO_BASE } from "../../../constants/Identifiers.ts"; +import { titleFormatter } from "../../../constants/Misc.ts"; const uid = new ShortUniqueId(); const shortuid = new ShortUniqueId({ length: 5 }); diff --git a/src/components/common/MultiplePublish/MultiplePublishAll.tsx b/src/components/Publish/MultiplePublish/MultiplePublishAll.tsx similarity index 98% rename from src/components/common/MultiplePublish/MultiplePublishAll.tsx rename to src/components/Publish/MultiplePublish/MultiplePublishAll.tsx index 6993710..ac9024a 100644 --- a/src/components/common/MultiplePublish/MultiplePublishAll.tsx +++ b/src/components/Publish/MultiplePublish/MultiplePublishAll.tsx @@ -8,8 +8,8 @@ import { useTheme, } from "@mui/material"; import React, { useCallback, useEffect, useState, useRef } from "react"; -import { CircleSVG } from "../../../assets/svgs/CircleSVG"; -import { EmptyCircleSVG } from "../../../assets/svgs/EmptyCircleSVG"; +import { CircleSVG } from "../../../assets/svgs/CircleSVG.tsx"; +import { EmptyCircleSVG } from "../../../assets/svgs/EmptyCircleSVG.tsx"; import { styled } from "@mui/system"; interface Publish { diff --git a/src/components/PlaylistListEdit/PlaylistListEdit.tsx b/src/components/Publish/PlaylistListEdit/PlaylistListEdit.tsx similarity index 95% rename from src/components/PlaylistListEdit/PlaylistListEdit.tsx rename to src/components/Publish/PlaylistListEdit/PlaylistListEdit.tsx index 90fba1c..49187f3 100644 --- a/src/components/PlaylistListEdit/PlaylistListEdit.tsx +++ b/src/components/Publish/PlaylistListEdit/PlaylistListEdit.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { CardContentContainerComment } from "../common/Comments/Comments-styles"; +import { CardContentContainerComment } from "../../common/Comments/Comments-styles.tsx"; import { CrowdfundSubTitle, CrowdfundSubTitleRow, @@ -7,11 +7,11 @@ import { import { Box, Button, Input, Typography, useTheme } from "@mui/material"; import { useNavigate } from "react-router-dom"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; -import { removeVideo } from "../../state/features/videoSlice"; +import { removeVideo } from "../../../state/features/videoSlice.ts"; import AddIcon from "@mui/icons-material/Add"; import { useSelector } from "react-redux"; -import { RootState } from "../../state/store"; -import { QTUBE_VIDEO_BASE } from "../../constants/Identifiers.ts"; +import { RootState } from "../../../state/store.ts"; +import { QTUBE_VIDEO_BASE } from "../../../constants/Identifiers.ts"; export const PlaylistListEdit = ({ playlistData, removeVideo, addVideo }) => { const theme = useTheme(); const navigate = useNavigate(); diff --git a/src/components/PublishVideo/PublishVideo-styles.tsx b/src/components/Publish/PublishVideo/PublishVideo-styles.tsx similarity index 99% rename from src/components/PublishVideo/PublishVideo-styles.tsx rename to src/components/Publish/PublishVideo/PublishVideo-styles.tsx index 6495a8a..bf3d011 100644 --- a/src/components/PublishVideo/PublishVideo-styles.tsx +++ b/src/components/Publish/PublishVideo/PublishVideo-styles.tsx @@ -12,7 +12,7 @@ import { Select, } from "@mui/material"; import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate"; -import { TimesSVG } from "../../assets/svgs/TimesSVG"; +import { TimesSVG } from "../../../assets/svgs/TimesSVG.tsx"; export const DoubleLine = styled(Typography)` display: -webkit-box; diff --git a/src/components/PublishVideo/PublishVideo.tsx b/src/components/Publish/PublishVideo/PublishVideo.tsx similarity index 97% rename from src/components/PublishVideo/PublishVideo.tsx rename to src/components/Publish/PublishVideo/PublishVideo.tsx index ab59967..a5952df 100644 --- a/src/components/PublishVideo/PublishVideo.tsx +++ b/src/components/Publish/PublishVideo/PublishVideo.tsx @@ -37,36 +37,36 @@ import AddBoxIcon from "@mui/icons-material/AddBox"; import { useDropzone } from "react-dropzone"; import AddIcon from "@mui/icons-material/Add"; -import { setNotification } from "../../state/features/notificationsSlice"; -import { objectToBase64, uint8ArrayToBase64 } from "../../utils/toBase64"; -import { RootState } from "../../state/store"; +import { setNotification } from "../../../state/features/notificationsSlice.ts"; +import { objectToBase64, uint8ArrayToBase64 } from "../../../utils/toBase64.ts"; +import { RootState } from "../../../state/store.ts"; import { upsertVideosBeginning, addToHashMap, upsertVideos, -} from "../../state/features/videoSlice"; -import ImageUploader from "../common/ImageUploader"; -import { categories, subCategories } from "../../constants/Categories.ts"; -import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll"; +} from "../../../state/features/videoSlice.ts"; +import ImageUploader from "../../common/ImageUploader.tsx"; +import { categories, subCategories } from "../../../constants/Categories.ts"; +import { MultiplePublish } from "../MultiplePublish/MultiplePublishAll.tsx"; import { CrowdfundSubTitle, CrowdfundSubTitleRow, -} from "../EditPlaylist/Upload-styles"; -import { CardContentContainerComment } from "../common/Comments/Comments-styles"; -import { TextEditor } from "../common/TextEditor/TextEditor"; -import { extractTextFromHTML } from "../common/TextEditor/utils"; +} from "../EditPlaylist/Upload-styles.tsx"; +import { CardContentContainerComment } from "../../common/Comments/Comments-styles.tsx"; +import { TextEditor } from "../../common/TextEditor/TextEditor.tsx"; +import { extractTextFromHTML } from "../../common/TextEditor/utils.ts"; import { FiltersCheckbox, FiltersRow, FiltersSubContainer, -} from "../../pages/Home/VideoList-styles"; -import { FrameExtractor } from "../common/FrameExtractor/FrameExtractor"; +} from "../../../pages/Home/VideoList-styles.tsx"; +import { FrameExtractor } from "../../common/FrameExtractor/FrameExtractor.tsx"; import { QTUBE_PLAYLIST_BASE, QTUBE_VIDEO_BASE, -} from "../../constants/Identifiers.ts"; -import { titleFormatter } from "../../constants/Misc.ts"; -import { getFileName } from "../../utils/stringFunctions.ts"; +} from "../../../constants/Identifiers.ts"; +import { titleFormatter } from "../../../constants/Misc.ts"; +import { getFileName } from "../../../utils/stringFunctions.ts"; export const toBase64 = (file: File): Promise => new Promise((resolve, reject) => { diff --git a/src/components/StatsData.tsx b/src/components/StatsData.tsx index 115dea8..a474ca3 100644 --- a/src/components/StatsData.tsx +++ b/src/components/StatsData.tsx @@ -12,8 +12,6 @@ export const StatsData = () => { width: "100%", padding: "20px 0px", backgroundColor: theme.palette.background.default, - borderTop: `1px solid ${theme.palette.background.paper}`, - borderRight: `1px solid ${theme.palette.background.paper}`, })); const { @@ -51,7 +49,10 @@ export const StatsData = () => {
Average:{" "} - {videosPerNamePublished} + + {videosPerNamePublished > 0 && + Number(videosPerNamePublished).toFixed(0)} +
); diff --git a/src/components/common/Comments/CommentSection.tsx b/src/components/common/Comments/CommentSection.tsx index df325b1..99f3fb0 100644 --- a/src/components/common/Comments/CommentSection.tsx +++ b/src/components/common/Comments/CommentSection.tsx @@ -17,7 +17,7 @@ import { import { CrowdfundSubTitle, CrowdfundSubTitleRow, -} from "../../PublishVideo/PublishVideo-styles.tsx"; +} from "../../Publish/PublishVideo/PublishVideo-styles.tsx"; import { COMMENT_BASE } from "../../../constants/Identifiers.ts"; interface CommentSectionProps { diff --git a/src/components/common/ContentButtons/FollowButton.tsx b/src/components/common/ContentButtons/FollowButton.tsx new file mode 100644 index 0000000..5739d3d --- /dev/null +++ b/src/components/common/ContentButtons/FollowButton.tsx @@ -0,0 +1,159 @@ +import { Box, Button, ButtonProps } from "@mui/material"; +import Tooltip, { TooltipProps, tooltipClasses } from "@mui/material/Tooltip"; +import { MouseEvent, useEffect, useState } from "react"; +import { styled } from "@mui/material/styles"; + +interface FollowButtonProps extends ButtonProps { + followerName: string; +} + +export type FollowData = { + userName: string; + followerName: string; +}; + +export const FollowButton = ({ followerName, ...props }: FollowButtonProps) => { + const [followingList, setFollowingList] = useState([]); + const [followingSize, setFollowingSize] = useState(""); + const [followingItemCount, setFollowingItemCount] = useState(""); + const isFollowingName = () => { + return followingList.includes(followerName); + }; + + useEffect(() => { + qortalRequest({ + action: "GET_LIST_ITEMS", + list_name: "followedNames", + }).then(followList => { + setFollowingList(followList); + }); + getFollowSize(); + }, []); + + const followName = () => { + if (followingList.includes(followerName) === false) { + qortalRequest({ + action: "ADD_LIST_ITEM", + list_name: "followedNames", + item: followerName, + }).then(response => { + if (response === false) console.log("followName failed"); + else { + setFollowingList([...followingList, followerName]); + console.log("following Name: ", followerName); + } + }); + } + }; + const unfollowName = () => { + if (followingList.includes(followerName)) { + qortalRequest({ + action: "DELETE_LIST_ITEM", + list_name: "followedNames", + item: followerName, + }).then(response => { + if (response === false) console.log("unfollowName failed"); + else { + const listWithoutName = followingList.filter( + item => followerName !== item + ); + setFollowingList(listWithoutName); + console.log("unfollowing Name: ", followerName); + } + }); + } + }; + + const manageFollow = (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + isFollowingName() ? unfollowName() : followName(); + }; + + const verticalPadding = "3px"; + const horizontalPadding = "8px"; + const buttonStyle = { + fontSize: "15px", + fontWeight: "700", + paddingTop: verticalPadding, + paddingBottom: verticalPadding, + paddingLeft: horizontalPadding, + paddingRight: horizontalPadding, + borderRadius: 28, + color: "white", + width: "96px", + height: "45px", + ...props.sx, + }; + + const getFollowSize = () => { + qortalRequest({ + action: "LIST_QDN_RESOURCES", + name: followerName, + limit: 0, + includeMetadata: false, + }).then(publishesList => { + let totalSize = 0; + let itemsCount = 0; + publishesList.map(publish => { + totalSize += +publish.size; + itemsCount++; + }); + setFollowingSize(formatBytes(totalSize)); + setFollowingItemCount(itemsCount.toString()); + }); + }; + + function formatBytes(bytes: number, decimals = 2) { + if (!+bytes) return "0 Bytes"; + + const k = 1024; + 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]}`; + } + + const TooltipLine = styled("div")(({ theme }) => ({ + fontSize: "18px", + })); + + const tooltipTitle = followingSize && ( + <> + + Following a name automatically downloads all of its content to your + node. The more followers a name has, the faster its content will + download for everyone. + +
+ {`${followerName}'s Current Download Size: ${followingSize}`} + {`Number of Files: ${followingItemCount}`} + + ); + + const CustomWidthTooltip = styled(({ className, ...props }: TooltipProps) => ( + + ))({ + [`& .${tooltipClasses.tooltip}`]: { + maxWidth: 600, + }, + }); + + return ( + <> + + + + + ); +}; diff --git a/src/components/common/ContentButtons/LikeAndDislike-functions.ts b/src/components/common/ContentButtons/LikeAndDislike-functions.ts new file mode 100644 index 0000000..bb17191 --- /dev/null +++ b/src/components/common/ContentButtons/LikeAndDislike-functions.ts @@ -0,0 +1,66 @@ +import { fetchResourcesByIdentifier } from "../../../utils/qortalRequestFunctions.ts"; +import { DISLIKE, LIKE, LikeType, NEUTRAL } from "./LikeAndDislike.tsx"; + +export const getCurrentLikeType = async ( + username: string, + likeIdentifier: string +): Promise => { + try { + const response = await qortalRequest({ + action: "FETCH_QDN_RESOURCE", + name: username, + service: "CHAIN_COMMENT", + identifier: likeIdentifier, + }); + return response?.likeType; + } catch (e) { + console.log("liketype error: ", e); + return NEUTRAL; + } +}; + +type ResourceType = { likeType: LikeType }; +export type LikesAndDislikes = { likes: number; dislikes: number }; +const countLikesAndDislikes = (likesAndDislikes: ResourceType[]) => { + let totalLikeCount = 0; + let totalDislikeCount = 0; + likesAndDislikes.map(likeOrDislike => { + const likeType = likeOrDislike.likeType; + if (likeType === LIKE) totalLikeCount += 1; + if (likeType === DISLIKE) totalDislikeCount += 1; + }); + return { + likes: totalLikeCount, + dislikes: totalDislikeCount, + } as LikesAndDislikes; +}; +export const getCurrentLikesAndDislikesCount = async ( + likeIdentifier: string +) => { + try { + const likesAndDislikes = await fetchResourcesByIdentifier( + "CHAIN_COMMENT", + likeIdentifier + ); + return countLikesAndDislikes(likesAndDislikes); + } catch (e) { + console.log(e); + return undefined; + } +}; + +export function formatLikeCount(likeCount: number, decimals = 2) { + if (!+likeCount) return ""; + + const sigDigits = Math.floor(Math.log10(likeCount) / 3); + if (sigDigits < 1) return likeCount.toString(); + + const sigDigitSize = 1000; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ["K", "M", "B"]; + + const sigDigitsToTheThousands = Math.pow(sigDigitSize, sigDigits); + const sigDigitLikeCount = (likeCount / sigDigitsToTheThousands).toFixed(dm); + + return `${sigDigitLikeCount}${sizes[sigDigits - 1] || ""}`; +} diff --git a/src/components/common/ContentButtons/LikeAndDislike.tsx b/src/components/common/ContentButtons/LikeAndDislike.tsx new file mode 100644 index 0000000..3e9c169 --- /dev/null +++ b/src/components/common/ContentButtons/LikeAndDislike.tsx @@ -0,0 +1,230 @@ +import React, { useEffect, useState } from "react"; +import ThumbUpIcon from "@mui/icons-material/ThumbUp"; +import ThumbDownIcon from "@mui/icons-material/ThumbDown"; +import ThumbUpOffAltOutlinedIcon from "@mui/icons-material/ThumbUpOffAltOutlined"; +import ThumbDownOffAltOutlinedIcon from "@mui/icons-material/ThumbDownOffAltOutlined"; +import { Box, Tooltip } from "@mui/material"; +import { useDispatch, useSelector } from "react-redux"; +import { setNotification } from "../../../state/features/notificationsSlice.ts"; +import ShortUniqueId from "short-unique-id"; +import { objectToBase64 } from "../../../utils/toBase64.ts"; +import { RootState } from "../../../state/store.ts"; +import { FOR, FOR_LIKE, LIKE_BASE } from "../../../constants/Identifiers.ts"; +import { + formatLikeCount, + getCurrentLikesAndDislikesCount, + getCurrentLikeType, + LikesAndDislikes, +} from "./LikeAndDislike-functions.ts"; + +interface LikeAndDislikeProps { + name: string; + identifier: string; +} +export enum LikeType { + Like = 1, + Neutral = 0, + Dislike = -1, +} +export const LIKE = LikeType.Like; +export const DISLIKE = LikeType.Dislike; +export const NEUTRAL = LikeType.Neutral; +export const LikeAndDislike = ({ name, identifier }: LikeAndDislikeProps) => { + const username = useSelector((state: RootState) => state.auth?.user?.name); + const dispatch = useDispatch(); + const [likeCount, setLikeCount] = useState(0); + const [dislikeCount, setDislikeCount] = useState(0); + const [currentLikeType, setCurrentLikeType] = useState(NEUTRAL); + const likeIdentifier = `${LIKE_BASE}${identifier.slice(0, 39)}`; + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + type PromiseReturn = [LikeType, LikesAndDislikes]; + + Promise.all([ + getCurrentLikeType(username, likeIdentifier), + getCurrentLikesAndDislikesCount(likeIdentifier), + ]).then(([likeType, likesAndDislikes]: PromiseReturn) => { + setCurrentLikeType(likeType); + + setLikeCount(likesAndDislikes?.likes || 0); + setDislikeCount(likesAndDislikes?.dislikes || 0); + setIsLoading(false); + }); + }, []); + + const updateLikeDataState = (newLikeType: LikeType) => { + const setSuccessNotification = (msg: string) => + dispatch( + setNotification({ + msg, + alertType: "success", + }) + ); + setCurrentLikeType(newLikeType); + switch (newLikeType) { + case NEUTRAL: + if (currentLikeType === LIKE) { + setLikeCount(count => count - 1); + setSuccessNotification("Like Removed"); + } else { + setDislikeCount(count => count - 1); + setSuccessNotification("Dislike Removed"); + } + + break; + case LIKE: + if (currentLikeType === DISLIKE) setDislikeCount(count => count - 1); + setLikeCount(count => count + 1); + setSuccessNotification("Like Successful"); + break; + case DISLIKE: + if (currentLikeType === LIKE) setLikeCount(count => count - 1); + setDislikeCount(count => count + 1); + setSuccessNotification("Dislike Successful"); + break; + } + }; + function publishLike(chosenLikeType: LikeType) { + if (isLoading) { + dispatch( + setNotification({ + msg: "Wait for Like Data to load first", + alertType: "error", + }) + ); + return; + } + try { + if (!username) throw new Error("You need a name to publish"); + if (!name) throw new Error("Could not retrieve content creator's name"); + if (!identifier) throw new Error("Could not retrieve id of video post"); + + if (username === name) { + dispatch( + setNotification({ + msg: "You cannot send yourself a like", + alertType: "error", + }) + ); + return; + } + qortalRequest({ + action: "GET_NAME_DATA", + name: name, + }).then(resName => { + const address = resName.owner; + if (!address) + throw new Error("Could not retrieve content creator's address"); + }); + + objectToBase64({ + likeType: chosenLikeType, + }).then(likeToBase64 => { + qortalRequest({ + action: "PUBLISH_QDN_RESOURCE", + name: username, + service: "CHAIN_COMMENT", + data64: likeToBase64, + title: "", + identifier: likeIdentifier, + filename: `like_metadata.json`, + }).then(() => { + updateLikeDataState(chosenLikeType); + }); + }); + } catch (error: any) { + dispatch( + setNotification({ + msg: + error || + error?.error || + error?.message || + "Failed to publish Like or Dislike", + alertType: "error", + }) + ); + throw new Error("Failed to publish Super Like"); + } + } + + return ( + <> + + + + {currentLikeType === LIKE ? ( + publishLike(NEUTRAL)} /> + ) : ( + publishLike(LIKE)} /> + )} + {likeCount > 0 && ( +
+ + {formatLikeCount(likeCount)} + +
+ )} + + {currentLikeType === DISLIKE ? ( + publishLike(NEUTRAL)} + color={"error"} + /> + ) : ( + publishLike(DISLIKE)} + color={"error"} + /> + )} + {dislikeCount > 0 && ( +
+ + {formatLikeCount(dislikeCount)} + +
+ )} +
+
+
+ + ); +}; diff --git a/src/components/common/SubscribeButton.tsx b/src/components/common/ContentButtons/SubscribeButton.tsx similarity index 70% rename from src/components/common/SubscribeButton.tsx rename to src/components/common/ContentButtons/SubscribeButton.tsx index cdb7cb9..2214289 100644 --- a/src/components/common/SubscribeButton.tsx +++ b/src/components/common/ContentButtons/SubscribeButton.tsx @@ -1,10 +1,14 @@ -import { Button, ButtonProps } from "@mui/material"; +import { Button, ButtonProps, Tooltip } from "@mui/material"; import { MouseEvent, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { RootState } from "../../state/store.ts"; -import { subscribe, unSubscribe } from "../../state/features/persistSlice.ts"; -import { setFilteredSubscriptions } from "../../state/features/videoSlice.ts"; -import { subscriptionListFilter } from "../../App.tsx"; +import { RootState } from "../../../state/store.ts"; +import { + subscribe, + unSubscribe, +} from "../../../state/features/persistSlice.ts"; +import { setFilteredSubscriptions } from "../../../state/features/videoSlice.ts"; +import { subscriptionListFilter } from "../../../App.tsx"; +import { styled } from "@mui/material/styles"; interface SubscribeButtonProps extends ButtonProps { subscriberName: string; @@ -89,15 +93,31 @@ export const SubscribeButton = ({ height: "45px", ...props.sx, }; + + const TooltipLine = styled("div")(({ theme }) => ({ + fontSize: "18px", + })); + + const tooltipTitle = ( + <> + + Subscribing to a name lets you see their content on the Subscriptions + tab of the Home Page. This does NOT download any data to your node. + + + ); + return ( - + + + ); }; diff --git a/src/components/common/SuperLike/SuperLike.tsx b/src/components/common/ContentButtons/SuperLike.tsx similarity index 86% rename from src/components/common/SuperLike/SuperLike.tsx rename to src/components/common/ContentButtons/SuperLike.tsx index 5e1be7b..ac33288 100644 --- a/src/components/common/SuperLike/SuperLike.tsx +++ b/src/components/common/ContentButtons/SuperLike.tsx @@ -17,26 +17,25 @@ import { Tooltip, } from "@mui/material"; import qortImg from "../../../assets/img/qort.png"; -import { MultiplePublish } from "../MultiplePublish/MultiplePublishAll"; +import { MultiplePublish } from "../../Publish/MultiplePublish/MultiplePublishAll.tsx"; import { useDispatch, useSelector } from "react-redux"; -import { setNotification } from "../../../state/features/notificationsSlice"; +import { setNotification } from "../../../state/features/notificationsSlice.ts"; import ShortUniqueId from "short-unique-id"; -import { objectToBase64 } from "../../../utils/toBase64"; +import { objectToBase64 } from "../../../utils/toBase64.ts"; import { minPriceSuperlike } from "../../../constants/Misc.ts"; -import { CommentInput } from "../Comments/Comments-styles"; +import { CommentInput } from "../Comments/Comments-styles.tsx"; import { CrowdfundActionButton, CrowdfundActionButtonRow, ModalBody, NewCrowdfundTitle, Spacer, -} from "../../PublishVideo/PublishVideo-styles.tsx"; -import { utf8ToBase64 } from "../SuperLikesList/CommentEditor"; -import { RootState } from "../../../state/store"; +} from "../../Publish/PublishVideo/PublishVideo-styles.tsx"; +import { utf8ToBase64 } from "../SuperLikesList/CommentEditor.tsx"; +import { RootState } from "../../../state/store.ts"; import { FOR, FOR_SUPER_LIKE, - QTUBE_VIDEO_BASE, SUPER_LIKE_BASE, } from "../../../constants/Identifiers.ts"; import BoundedNumericTextField from "../../../utils/BoundedNumericTextField.tsx"; @@ -158,7 +157,7 @@ export const SuperLike = ({ for: `${name}_${FOR_SUPER_LIKE}`, }, about: - "Super likes are a way to suppert your favorite content creators. Attach a message to the Super like and have your message seen before normal comments. There is a minimum superLikeAmount for a Super like. Each Super like is verified before displaying to make there aren't any non-paid Super likes", + "Super likes are a way to support your favorite content creators. Attach a message to the Super like and have your message seen before normal comments. There is a minimum superLikeAmount for a Super like. Each Super like is verified before displaying to make there aren't any non-paid Super likes", }); // Description is obtained from raw data // const base64 = utf8ToBase64(comment); @@ -185,26 +184,16 @@ export const SuperLike = ({ setIsOpenMultiplePublish(true); } catch (error: any) { - let notificationObj: any = null; - if (typeof error === "string") { - notificationObj = { - msg: error || "Failed to publish Super Like", + dispatch( + setNotification({ + msg: + error || + error?.error || + error?.message || + "Failed to publish Super Like", alertType: "error", - }; - } else if (typeof error?.error === "string") { - notificationObj = { - msg: error?.error || "Failed to publish Super Like", - alertType: "error", - }; - } else { - notificationObj = { - msg: error?.message || "Failed to publish Super Like", - alertType: "error", - }; - } - if (!notificationObj) return; - dispatch(setNotification(notificationObj)); - + }) + ); throw new Error("Failed to publish Super Like"); } } @@ -239,8 +228,6 @@ export const SuperLike = ({ flexShrink: 0, }} > - - - {numberOfSuperlikes === 0 ? null : ( -
- {numberOfSuperlikes} - {"Qort - {truncateNumber(totalAmount,0)} -
- )} + {numberOfSuperlikes === 0 ? null : ( +
+ + {numberOfSuperlikes} + + {"Qort + {truncateNumber(totalAmount, 0)} +
+ )}
@@ -301,10 +294,10 @@ export const SuperLike = ({ - Amount in QORT (min 10 QORT) + Amount in QORT (min 1 QORT) ({ + position: "relative", + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + width: "100%", + height: "100%", + margin: 0, + padding: 0, + maxHeight: "70vh", + "&:focus": { outline: "none" }, +})); export const VideoElement = styled("video")` width: 100%; diff --git a/src/components/common/VideoPlayer/VideoPlayer.tsx b/src/components/common/VideoPlayer/VideoPlayer.tsx index 92b4498..3cbfc5e 100644 --- a/src/components/common/VideoPlayer/VideoPlayer.tsx +++ b/src/components/common/VideoPlayer/VideoPlayer.tsx @@ -1,4 +1,12 @@ -import React, { useContext, useEffect, useMemo, useRef, useState } from "react"; +import React, { + MutableRefObject, + useContext, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, +} from "react"; import ReactDOM from "react-dom"; import { Box, IconButton, Slider } from "@mui/material"; import { CircularProgress, Typography } from "@mui/material"; @@ -45,922 +53,947 @@ interface VideoPlayerProps { style?: CSS.Properties; } -export const VideoPlayer: React.FC = ({ - poster, - name, - identifier, - service, - autoplay = true, - from = null, - customStyle = {}, - user = "", - jsonId = "", - nextVideo, - onEnd, - autoPlay, - style = {}, -}) => { - const videoSelector = useSelector((state: RootState) => state.video); - const persistSelector = useSelector((state: RootState) => state.persist); - const dispatch = useDispatch(); - const videoRef = useRef(null); - const [playing, setPlaying] = useState(false); - const [volume, setVolume] = useState(1); - const [mutedVolume, setMutedVolume] = useState(1); - const [isMuted, setIsMuted] = useState(false); - const [progress, setProgress] = useState(0); - const [isLoading, setIsLoading] = useState(false); - const [canPlay, setCanPlay] = useState(false); - const [startPlay, setStartPlay] = useState(false); - const [isMobileView, setIsMobileView] = useState(false); - const [playbackRate, setPlaybackRate] = useState( - persistSelector.playbackRate - ); - const [anchorEl, setAnchorEl] = useState(null); - const [showControlsFullScreen, setShowControlsFullScreen] = - useState(true); - const videoPlaying = useSelector( - (state: RootState) => state.global.videoPlaying - ); - const { downloads } = useSelector((state: RootState) => state.global); - - const reDownload = useRef(false); - const reDownloadNextVid = useRef(false); - - const isFetchingProperties = useRef(false); - - const status = useRef(null); - - const download = useMemo(() => { - if (!downloads || !identifier) return {}; - const findDownload = downloads[identifier]; - - if (!findDownload) return {}; - return findDownload; - }, [downloads, identifier]); - - const src = useMemo(() => { - return download?.url || ""; - }, [download?.url]); - const resourceStatus = useMemo(() => { - return download?.status || {}; - }, [download]); - - const minSpeed = 0.25; - const maxSpeed = 4.0; - const speedChange = 0.25; - - const updatePlaybackRate = (newSpeed: number) => { - if (videoRef.current) { - if (newSpeed > maxSpeed || newSpeed < minSpeed) newSpeed = minSpeed; - - videoRef.current.playbackRate = playbackRate; - setPlaybackRate(newSpeed); - dispatch(setReduxPlaybackRate(newSpeed)); - } - }; +export type refType = { + getContainerRef: () => React.MutableRefObject; + getVideoRef: () => React.MutableRefObject; +}; +export const VideoPlayer = React.forwardRef( + ( + { + poster, + name, + identifier, + service, + autoplay = true, + from = null, + customStyle = {}, + user = "", + jsonId = "", + nextVideo, + onEnd, + autoPlay, + style = {}, + }: VideoPlayerProps, + ref + ) => { + const videoSelector = useSelector((state: RootState) => state.video); + const persistSelector = useSelector((state: RootState) => state.persist); + const dispatch = useDispatch(); + const videoRef = useRef(null); + const containerRef = useRef(null); + const [playing, setPlaying] = useState(false); + const [volume, setVolume] = useState(1); + const [mutedVolume, setMutedVolume] = useState(1); + const [isMuted, setIsMuted] = useState(false); + const [progress, setProgress] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [canPlay, setCanPlay] = useState(false); + const [startPlay, setStartPlay] = useState(false); + const [isMobileView, setIsMobileView] = useState(false); + const [playbackRate, setPlaybackRate] = useState( + persistSelector.playbackRate + ); + const [anchorEl, setAnchorEl] = useState(null); + const [showControlsFullScreen, setShowControlsFullScreen] = + useState(true); + const videoPlaying = useSelector( + (state: RootState) => state.global.videoPlaying + ); + const { downloads } = useSelector((state: RootState) => state.global); + + const reDownload = useRef(false); + const reDownloadNextVid = useRef(false); + + const isFetchingProperties = useRef(false); + + const status = useRef(null); + + const download = useMemo(() => { + if (!downloads || !identifier) return {}; + const findDownload = downloads[identifier]; + + if (!findDownload) return {}; + return findDownload; + }, [downloads, identifier]); + + const src = useMemo(() => { + return download?.url || ""; + }, [download?.url]); + const resourceStatus = useMemo(() => { + return download?.status || {}; + }, [download]); + + const minSpeed = 0.25; + const maxSpeed = 4.0; + const speedChange = 0.25; + + useImperativeHandle(ref, () => ({ + getVideoRef: () => { + return videoRef; + }, + getContainerRef: () => { + return containerRef; + }, + })); + const updatePlaybackRate = (newSpeed: number) => { + if (videoRef.current) { + if (newSpeed > maxSpeed || newSpeed < minSpeed) newSpeed = minSpeed; + + videoRef.current.playbackRate = playbackRate; + setPlaybackRate(newSpeed); + dispatch(setReduxPlaybackRate(newSpeed)); + } + }; - const increaseSpeed = (wrapOverflow = true) => { - const changedSpeed = playbackRate + speedChange; - let newSpeed = wrapOverflow - ? changedSpeed - : Math.min(changedSpeed, maxSpeed); + const increaseSpeed = (wrapOverflow = true) => { + const changedSpeed = playbackRate + speedChange; + let newSpeed = wrapOverflow + ? changedSpeed + : Math.min(changedSpeed, maxSpeed); - if (videoRef.current) { - updatePlaybackRate(newSpeed); - } - }; + if (videoRef.current) { + updatePlaybackRate(newSpeed); + } + }; - const decreaseSpeed = () => { - if (videoRef.current) { - updatePlaybackRate(playbackRate - speedChange); - } - }; - - useEffect(() => { - reDownload.current = false; - reDownloadNextVid.current = false; - setIsLoading(false); - setCanPlay(false); - setProgress(0); - setPlaying(false); - setStartPlay(false); - isFetchingProperties.current = false; - status.current = null; - }, [identifier]); - - useEffect(() => { - if (autoPlay && identifier) { - setStartPlay(true); - setPlaying(true); - togglePlay(undefined, true); - } - }, [autoPlay, startPlay, identifier]); - - const refetch = React.useCallback(async () => { - if (!name || !identifier || !service || isFetchingProperties.current) - return; - try { - isFetchingProperties.current = true; - await qortalRequest({ - action: "GET_QDN_RESOURCE_PROPERTIES", - name, - service, - identifier, - }); - } catch (error) { - } finally { + const decreaseSpeed = () => { + if (videoRef.current) { + updatePlaybackRate(playbackRate - speedChange); + } + }; + + useEffect(() => { + reDownload.current = false; + reDownloadNextVid.current = false; + setIsLoading(false); + setCanPlay(false); + setProgress(0); + setPlaying(false); + setStartPlay(false); isFetchingProperties.current = false; - } - }, [identifier, name, service]); - - const toggleRef = useRef(null); - const { downloadVideo } = useContext(MyContext); - const togglePlay = async (event?: any, isPlay?: boolean) => { - if (!videoRef.current) return; - setStartPlay(true); - if (!src || resourceStatus?.status !== "READY") { - const el = document.getElementById("videoWrapper"); - if (el) { - el?.parentElement?.removeChild(el); + status.current = null; + }, [identifier]); + + useEffect(() => { + if (autoPlay && identifier) { + setStartPlay(true); + setPlaying(true); + togglePlay(undefined, true); } - ReactDOM.flushSync(() => { - setIsLoading(true); - }); - getSrc(); - } - if (playing && !isPlay) { - videoRef.current.pause(); - } else { - videoRef.current.play(); - } - if (isPlay) { - setPlaying(true); - } else { - setPlaying(!playing); - } - }; - - const onVolumeChange = (_: any, value: number | number[]) => { - if (!videoRef.current) return; - videoRef.current.volume = value as number; - setVolume(value as number); - setIsMuted(false); - }; - - const onProgressChange = (_: any, value: number | number[]) => { - if (!videoRef.current) return; - videoRef.current.currentTime = value as number; - setProgress(value as number); - if (!playing) { - videoRef.current.play(); - setPlaying(true); - } - }; + }, [autoPlay, startPlay, identifier]); + + const refetch = React.useCallback(async () => { + if (!name || !identifier || !service || isFetchingProperties.current) + return; + try { + isFetchingProperties.current = true; + await qortalRequest({ + action: "GET_QDN_RESOURCE_PROPERTIES", + name, + service, + identifier, + }); + } catch (error) { + } finally { + isFetchingProperties.current = false; + } + }, [identifier, name, service]); - const handleEnded = () => { - setPlaying(false); - if (onEnd) { - onEnd(); - } - }; + const toggleRef = useRef(null); + const { downloadVideo } = useContext(MyContext); + const togglePlay = async (event?: any, isPlay?: boolean) => { + if (!videoRef.current) return; + setStartPlay(true); + if (!src || resourceStatus?.status !== "READY") { + const el = document.getElementById("videoWrapper"); + if (el) { + el?.parentElement?.removeChild(el); + } + ReactDOM.flushSync(() => { + setIsLoading(true); + }); + getSrc(); + } + if (playing && !isPlay) { + videoRef.current.pause(); + } else { + videoRef.current.play(); + } + if (isPlay) { + setPlaying(true); + } else { + setPlaying(!playing); + } + }; - const updateProgress = () => { - if (!videoRef.current) return; - setProgress(videoRef.current.currentTime); - }; + const onVolumeChange = (_: any, value: number | number[]) => { + if (!videoRef.current) return; + videoRef.current.volume = value as number; + setVolume(value as number); + setIsMuted(false); + }; - const [isFullscreen, setIsFullscreen] = useState(false); + const onProgressChange = (_: any, value: number | number[]) => { + if (!videoRef.current) return; + videoRef.current.currentTime = value as number; + setProgress(value as number); + if (!playing) { + videoRef.current.play(); + setPlaying(true); + } + }; - const enterFullscreen = () => { - if (!videoRef.current) return; - if (videoRef.current.requestFullscreen) { - videoRef.current.requestFullscreen(); - } - }; + const handleEnded = () => { + setPlaying(false); + if (onEnd) { + onEnd(); + } + }; - const exitFullscreen = () => { - if (document.exitFullscreen) { - document.exitFullscreen(); - } - }; - - const toggleFullscreen = () => { - isFullscreen ? exitFullscreen() : enterFullscreen(); - }; - const togglePictureInPicture = async () => { - if (!videoRef.current) return; - if (document.pictureInPictureElement === videoRef.current) { - await document.exitPictureInPicture(); - } else { - await videoRef.current.requestPictureInPicture(); - } - }; + const updateProgress = () => { + if (!videoRef.current) return; + setProgress(videoRef.current.currentTime); + }; + + const [isFullscreen, setIsFullscreen] = useState(false); - useEffect(() => { - const handleFullscreenChange = () => { - setIsFullscreen(!!document.fullscreenElement); + const enterFullscreen = () => { + if (!videoRef.current) return; + if (videoRef.current.requestFullscreen) { + videoRef.current.requestFullscreen(); + } }; - document.addEventListener("fullscreenchange", handleFullscreenChange); - return () => { - document.removeEventListener("fullscreenchange", handleFullscreenChange); + const exitFullscreen = () => { + if (document.exitFullscreen) { + document.exitFullscreen(); + } }; - }, []); - - useEffect(() => { - if ( - videoPlaying && - videoPlaying.id === identifier && - src && - videoRef?.current - ) { - handleCanPlay(); - videoRef.current.volume = videoPlaying.volume; - videoRef.current.currentTime = videoPlaying.currentTime; - videoRef.current.play(); - setPlaying(true); - setStartPlay(true); - dispatch(setVideoPlaying(null)); - } - }, [videoPlaying, identifier, src]); - - const handleCanPlay = () => { - setIsLoading(false); - setCanPlay(true); - updatePlaybackRate(playbackRate); - }; - - const getSrc = React.useCallback(async () => { - if (!name || !identifier || !service || !jsonId || !user) return; - try { - downloadVideo({ - name, - service, - identifier, - properties: { - jsonId, - user, - }, - }); - } catch (error) { - console.error(error); - } - }, [identifier, name, service, jsonId, user]); - useEffect(() => { - const videoElement = videoRef.current; + const toggleFullscreen = () => { + isFullscreen ? exitFullscreen() : enterFullscreen(); + }; + const togglePictureInPicture = async () => { + if (!videoRef.current) return; + if (document.pictureInPictureElement === videoRef.current) { + await document.exitPictureInPicture(); + } else { + await videoRef.current.requestPictureInPicture(); + } + }; - const handleLeavePictureInPicture = async (event: any) => { - const target = event?.target; - if (target) { - target.pause(); - if (setPlaying) { - setPlaying(false); - } + useEffect(() => { + const handleFullscreenChange = () => { + setIsFullscreen(!!document.fullscreenElement); + }; + + document.addEventListener("fullscreenchange", handleFullscreenChange); + return () => { + document.removeEventListener( + "fullscreenchange", + handleFullscreenChange + ); + }; + }, []); + + useEffect(() => { + if ( + videoPlaying && + videoPlaying.id === identifier && + src && + videoRef?.current + ) { + handleCanPlay(); + videoRef.current.volume = videoPlaying.volume; + videoRef.current.currentTime = videoPlaying.currentTime; + videoRef.current.play(); + setPlaying(true); + setStartPlay(true); + dispatch(setVideoPlaying(null)); } + }, [videoPlaying, identifier, src]); + + const handleCanPlay = () => { + setIsLoading(false); + setCanPlay(true); + updatePlaybackRate(playbackRate); }; - if (videoElement) { - videoElement.addEventListener( - "leavepictureinpicture", - handleLeavePictureInPicture - ); - } + const getSrc = React.useCallback(async () => { + if (!name || !identifier || !service || !jsonId || !user) return; + try { + downloadVideo({ + name, + service, + identifier, + properties: { + jsonId, + user, + }, + }); + } catch (error) { + console.error(error); + } + }, [identifier, name, service, jsonId, user]); + + useEffect(() => { + const videoElement = videoRef.current; + + const handleLeavePictureInPicture = async (event: any) => { + const target = event?.target; + if (target) { + target.pause(); + if (setPlaying) { + setPlaying(false); + } + } + }; - return () => { if (videoElement) { - videoElement.removeEventListener( + videoElement.addEventListener( "leavepictureinpicture", handleLeavePictureInPicture ); } - }; - }, []); - - useEffect(() => { - const videoElement = videoRef.current; - - const minimizeVideo = async () => { - if (!videoElement) return; - - dispatch(setVideoPlaying(videoElement)); - // const handleClose = () => { - // if (videoElement && videoElement.parentElement) { - // const el = document.getElementById('videoWrapper') - // if (el) { - // el?.parentElement?.removeChild(el) - // } - // } - // } - // const createCloseButton = (): HTMLButtonElement => { - // const closeButton = document.createElement('button') - // closeButton.textContent = 'X' - // closeButton.style.position = 'absolute' - // closeButton.style.top = '0' - // closeButton.style.right = '0' - // closeButton.style.backgroundColor = 'rgba(255, 255, 255, 0.7)' - // closeButton.style.border = 'none' - // closeButton.style.fontWeight = 'bold' - // closeButton.style.fontSize = '1.2rem' - // closeButton.style.cursor = 'pointer' - // closeButton.style.padding = '2px 8px' - // closeButton.style.borderRadius = '0 0 0 4px' - - // closeButton.addEventListener('click', handleClose) - - // return closeButton - // } - // const buttonClose = createCloseButton() - // const videoWrapper = document.createElement('div') - // videoWrapper.id = 'videoWrapper' - // videoWrapper.style.position = 'fixed' - // videoWrapper.style.zIndex = '900000009' - // videoWrapper.style.bottom = '0px' - // videoWrapper.style.right = '0px' - - // videoElement.parentElement?.insertBefore(videoWrapper, videoElement) - // videoWrapper.appendChild(videoElement) - - // videoWrapper.appendChild(buttonClose) - // videoElement.controls = true - // videoElement.style.height = 'auto' - // videoElement.style.width = '300px' - - // document.body.appendChild(videoWrapper) - }; - return () => { - if (videoElement) { - if (videoElement && !videoElement.paused && !videoElement.ended) { - minimizeVideo(); + return () => { + if (videoElement) { + videoElement.removeEventListener( + "leavepictureinpicture", + handleLeavePictureInPicture + ); } - } - }; - }, []); + }; + }, []); + + useEffect(() => { + const videoElement = videoRef.current; + + const minimizeVideo = async () => { + if (!videoElement) return; + + dispatch(setVideoPlaying(videoElement)); + // const handleClose = () => { + // if (videoElement && videoElement.parentElement) { + // const el = document.getElementById('videoWrapper') + // if (el) { + // el?.parentElement?.removeChild(el) + // } + // } + // } + // const createCloseButton = (): HTMLButtonElement => { + // const closeButton = document.createElement('button') + // closeButton.textContent = 'X' + // closeButton.style.position = 'absolute' + // closeButton.style.top = '0' + // closeButton.style.right = '0' + // closeButton.style.backgroundColor = 'rgba(255, 255, 255, 0.7)' + // closeButton.style.border = 'none' + // closeButton.style.fontWeight = 'bold' + // closeButton.style.fontSize = '1.2rem' + // closeButton.style.cursor = 'pointer' + // closeButton.style.padding = '2px 8px' + // closeButton.style.borderRadius = '0 0 0 4px' + + // closeButton.addEventListener('click', handleClose) + + // return closeButton + // } + // const buttonClose = createCloseButton() + // const videoWrapper = document.createElement('div') + // videoWrapper.id = 'videoWrapper' + // videoWrapper.style.position = 'fixed' + // videoWrapper.style.zIndex = '900000009' + // videoWrapper.style.bottom = '0px' + // videoWrapper.style.right = '0px' + + // videoElement.parentElement?.insertBefore(videoWrapper, videoElement) + // videoWrapper.appendChild(videoElement) + + // videoWrapper.appendChild(buttonClose) + // videoElement.controls = true + // videoElement.style.height = 'auto' + // videoElement.style.width = '300px' + + // document.body.appendChild(videoWrapper) + }; + + return () => { + if (videoElement) { + if (videoElement && !videoElement.paused && !videoElement.ended) { + minimizeVideo(); + } + } + }; + }, []); - function formatTime(seconds: number): string { - seconds = Math.floor(seconds); - let minutes: number | string = Math.floor(seconds / 60); - let hours: number | string = Math.floor(minutes / 60); + function formatTime(seconds: number): string { + seconds = Math.floor(seconds); + let 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; + let remainingSeconds: number | string = seconds % 60; + let remainingMinutes: number | string = minutes % 60; - if (remainingSeconds < 10) { - remainingSeconds = "0" + remainingSeconds; - } + if (remainingSeconds < 10) { + remainingSeconds = "0" + remainingSeconds; + } - if (remainingMinutes < 10) { - remainingMinutes = "0" + remainingMinutes; - } + if (remainingMinutes < 10) { + remainingMinutes = "0" + remainingMinutes; + } - if (hours === 0) { - hours = ""; - } else { - hours = hours + ":"; + if (hours === 0) { + hours = ""; + } else { + hours = hours + ":"; + } + + return hours + remainingMinutes + ":" + remainingSeconds; } - return hours + remainingMinutes + ":" + remainingSeconds; - } + const reloadVideo = () => { + if (!videoRef.current) return; + const currentTime = videoRef.current.currentTime; + videoRef.current.src = src; + videoRef.current.load(); + videoRef.current.currentTime = currentTime; + if (playing) { + videoRef.current.play(); + } + }; - const reloadVideo = () => { - if (!videoRef.current) return; - const currentTime = videoRef.current.currentTime; - videoRef.current.src = src; - videoRef.current.load(); - videoRef.current.currentTime = currentTime; - if (playing) { - videoRef.current.play(); - } - }; + const refetchInInterval = () => { + try { + const interval = setInterval(() => { + if (status?.current === "DOWNLOADED") { + refetch(); + } + if (status?.current === "READY") { + clearInterval(interval); + } + }, 7500); + } catch (error) {} + }; - const refetchInInterval = () => { - try { - const interval = setInterval(() => { - if (status?.current === "DOWNLOADED") { - refetch(); - } - if (status?.current === "READY") { - clearInterval(interval); + useEffect(() => { + if (resourceStatus?.status) { + status.current = resourceStatus?.status; + } + if ( + resourceStatus?.status === "DOWNLOADED" && + reDownload?.current === false + ) { + refetchInInterval(); + reDownload.current = true; + } + }, [getSrc, resourceStatus]); + + useEffect(() => { + if (resourceStatus?.status) { + status.current = resourceStatus?.status; + } + if ( + resourceStatus?.status === "READY" && + reDownloadNextVid?.current === false + ) { + if (nextVideo) { + downloadVideo({ + name: nextVideo?.name, + service: nextVideo?.service, + identifier: nextVideo?.identifier, + properties: { + jsonId: nextVideo?.jsonId, + user, + }, + }); } - }, 7500); - } catch (error) {} - }; + reDownloadNextVid.current = true; + } + }, [getSrc, resourceStatus]); - useEffect(() => { - if (resourceStatus?.status) { - status.current = resourceStatus?.status; - } - if ( - resourceStatus?.status === "DOWNLOADED" && - reDownload?.current === false - ) { - refetchInInterval(); - reDownload.current = true; - } - }, [getSrc, resourceStatus]); + const handleMenuOpen = (event: any) => { + setAnchorEl(event.currentTarget); + }; - useEffect(() => { - if (resourceStatus?.status) { - status.current = resourceStatus?.status; - } - if ( - resourceStatus?.status === "READY" && - reDownloadNextVid?.current === false - ) { - if (nextVideo) { - downloadVideo({ - name: nextVideo?.name, - service: nextVideo?.service, - identifier: nextVideo?.identifier, - properties: { - jsonId: nextVideo?.jsonId, - user, - }, - }); + const handleMenuClose = () => { + setAnchorEl(null); + }; + + useEffect(() => { + const videoWidth = videoRef?.current?.offsetWidth; + if (videoWidth && videoWidth <= 600) { + setIsMobileView(true); } - reDownloadNextVid.current = true; - } - }, [getSrc, resourceStatus]); + }, [canPlay]); - const handleMenuOpen = (event: any) => { - setAnchorEl(event.currentTarget); - }; + const getDownloadProgress = (current: number, total: number) => { + const progress = (current / total) * 100; + return Number.isNaN(progress) ? "" : progress.toFixed(0) + "%"; + }; + const mute = () => { + setIsMuted(true); + setMutedVolume(volume); + setVolume(0); + if (videoRef.current) videoRef.current.volume = 0; + }; + const unMute = () => { + setIsMuted(false); + setVolume(mutedVolume); + if (videoRef.current) videoRef.current.volume = mutedVolume; + }; - const handleMenuClose = () => { - setAnchorEl(null); - }; + const toggleMute = () => { + isMuted ? unMute() : mute(); + }; - useEffect(() => { - const videoWidth = videoRef?.current?.offsetWidth; - if (videoWidth && videoWidth <= 600) { - setIsMobileView(true); - } - }, [canPlay]); - - const getDownloadProgress = (current: number, total: number) => { - const progress = (current / total) * 100; - return Number.isNaN(progress) ? "" : progress.toFixed(0) + "%"; - }; - const mute = () => { - setIsMuted(true); - setMutedVolume(volume); - setVolume(0); - if (videoRef.current) videoRef.current.volume = 0; - }; - const unMute = () => { - setIsMuted(false); - setVolume(mutedVolume); - if (videoRef.current) videoRef.current.volume = mutedVolume; - }; - - const toggleMute = () => { - isMuted ? unMute() : mute(); - }; - - const changeVolume = (volumeChange: number) => { - if (videoRef.current) { - const minVolume = 0; - const maxVolume = 1; - - let newVolume = volumeChange + volume; - - newVolume = Math.max(newVolume, minVolume); - newVolume = Math.min(newVolume, maxVolume); + const changeVolume = (volumeChange: number) => { + if (videoRef.current) { + const minVolume = 0; + const maxVolume = 1; - setIsMuted(false); - setMutedVolume(newVolume); - videoRef.current.volume = newVolume; - setVolume(newVolume); - } - }; - const setProgressRelative = (secondsChange: number) => { - if (videoRef.current) { - const currentTime = videoRef.current?.currentTime; - const minTime = 0; - const maxTime = videoRef.current?.duration || 100; - - let newTime = currentTime + secondsChange; - newTime = Math.max(newTime, minTime); - newTime = Math.min(newTime, maxTime); - videoRef.current.currentTime = newTime; - setProgress(newTime); - } - }; - - const setProgressAbsolute = (videoPercent: number) => { - if (videoRef.current) { - videoPercent = Math.min(videoPercent, 100); - videoPercent = Math.max(videoPercent, 0); - const finalTime = (videoRef.current?.duration * videoPercent) / 100; - videoRef.current.currentTime = finalTime; - setProgress(finalTime); - } - }; - - const keyboardShortcutsDown = (e: React.KeyboardEvent) => { - e.preventDefault(); - - switch (e.key) { - case Key.Add: - increaseSpeed(false); - break; - case "+": - increaseSpeed(false); - break; - case ">": - increaseSpeed(false); - break; - - case Key.Subtract: - decreaseSpeed(); - break; - case "-": - decreaseSpeed(); - break; - case "<": - decreaseSpeed(); - break; - - case Key.ArrowLeft: - { - if (e.shiftKey) setProgressRelative(-300); - else if (e.ctrlKey) setProgressRelative(-60); - else if (e.altKey) setProgressRelative(-10); - else setProgressRelative(-5); - } - break; - - case Key.ArrowRight: - { - if (e.shiftKey) setProgressRelative(300); - else if (e.ctrlKey) setProgressRelative(60); - else if (e.altKey) setProgressRelative(10); - else setProgressRelative(5); - } - break; - - case Key.ArrowDown: - changeVolume(-0.05); - break; - case Key.ArrowUp: - changeVolume(0.05); - break; - } - }; - - const keyboardShortcutsUp = (e: React.KeyboardEvent) => { - e.preventDefault(); - - switch (e.key) { - case " ": - togglePlay(); - break; - case "m": - toggleMute(); - break; - - case "f": - enterFullscreen(); - break; - case Key.Escape: - exitFullscreen(); - break; - - case "0": - setProgressAbsolute(0); - break; - case "1": - setProgressAbsolute(10); - break; - case "2": - setProgressAbsolute(20); - break; - case "3": - setProgressAbsolute(30); - break; - case "4": - setProgressAbsolute(40); - break; - case "5": - setProgressAbsolute(50); - break; - case "6": - setProgressAbsolute(60); - break; - case "7": - setProgressAbsolute(70); - break; - case "8": - setProgressAbsolute(80); - break; - case "9": - setProgressAbsolute(90); - break; - } - }; - - return ( - - {isLoading && ( - - - {resourceStatus && ( - - {resourceStatus?.status === "NOT_PUBLISHED" && ( - <>Video file was not published. Please inform the publisher! - )} - {resourceStatus?.status === "REFETCHING" ? ( - <> + let newVolume = volumeChange + volume; + + newVolume = Math.max(newVolume, minVolume); + newVolume = Math.min(newVolume, maxVolume); + + setIsMuted(false); + setMutedVolume(newVolume); + videoRef.current.volume = newVolume; + setVolume(newVolume); + } + }; + const setProgressRelative = (secondsChange: number) => { + if (videoRef.current) { + const currentTime = videoRef.current?.currentTime; + const minTime = 0; + const maxTime = videoRef.current?.duration || 100; + + let newTime = currentTime + secondsChange; + newTime = Math.max(newTime, minTime); + newTime = Math.min(newTime, maxTime); + videoRef.current.currentTime = newTime; + setProgress(newTime); + } + }; + + const setProgressAbsolute = (videoPercent: number) => { + if (videoRef.current) { + videoPercent = Math.min(videoPercent, 100); + videoPercent = Math.max(videoPercent, 0); + const finalTime = (videoRef.current?.duration * videoPercent) / 100; + videoRef.current.currentTime = finalTime; + setProgress(finalTime); + } + }; + + const keyboardShortcutsDown = (e: React.KeyboardEvent) => { + e.preventDefault(); + + switch (e.key) { + case Key.Add: + increaseSpeed(false); + break; + case "+": + increaseSpeed(false); + break; + case ">": + increaseSpeed(false); + break; + + case Key.Subtract: + decreaseSpeed(); + break; + case "-": + decreaseSpeed(); + break; + case "<": + decreaseSpeed(); + break; + + case Key.ArrowLeft: + { + if (e.shiftKey) setProgressRelative(-300); + else if (e.ctrlKey) setProgressRelative(-60); + else if (e.altKey) setProgressRelative(-10); + else setProgressRelative(-5); + } + break; + + case Key.ArrowRight: + { + if (e.shiftKey) setProgressRelative(300); + else if (e.ctrlKey) setProgressRelative(60); + else if (e.altKey) setProgressRelative(10); + else setProgressRelative(5); + } + break; + + case Key.ArrowDown: + changeVolume(-0.05); + break; + case Key.ArrowUp: + changeVolume(0.05); + break; + } + }; + + const keyboardShortcutsUp = (e: React.KeyboardEvent) => { + e.preventDefault(); + + switch (e.key) { + case " ": + togglePlay(); + break; + case "m": + toggleMute(); + break; + + case "f": + enterFullscreen(); + break; + case Key.Escape: + exitFullscreen(); + break; + + case "0": + setProgressAbsolute(0); + break; + case "1": + setProgressAbsolute(10); + break; + case "2": + setProgressAbsolute(20); + break; + case "3": + setProgressAbsolute(30); + break; + case "4": + setProgressAbsolute(40); + break; + case "5": + setProgressAbsolute(50); + break; + case "6": + setProgressAbsolute(60); + break; + case "7": + setProgressAbsolute(70); + break; + case "8": + setProgressAbsolute(80); + break; + case "9": + setProgressAbsolute(90); + break; + } + }; + + return ( + + {isLoading && ( + + + {resourceStatus && ( + + {resourceStatus?.status === "NOT_PUBLISHED" && ( + <> + Video file was not published. Please inform the publisher! + + )} + {resourceStatus?.status === "REFETCHING" ? ( + <> + <> + {getDownloadProgress( + resourceStatus?.localChunkCount, + resourceStatus?.totalChunkCount + )} + + + <> Refetching in 25 seconds + + ) : resourceStatus?.status === "DOWNLOADED" ? ( + <>Download Completed: building video... + ) : resourceStatus?.status !== "READY" ? ( <> {getDownloadProgress( resourceStatus?.localChunkCount, resourceStatus?.totalChunkCount )} - - <> Refetching in 25 seconds - - ) : resourceStatus?.status === "DOWNLOADED" ? ( - <>Download Completed: building video... - ) : resourceStatus?.status !== "READY" ? ( - <> - {getDownloadProgress( - resourceStatus?.localChunkCount, - resourceStatus?.totalChunkCount - )} - - ) : ( - <>Fetching video... - )} - - )} - - )} - {((!src && !isLoading) || !startPlay) && ( - { - if (from === "create") return; - dispatch(setVideoPlaying(null)); - togglePlay(); - }} - sx={{ - cursor: "pointer", - }} - > - Fetching video... + )} + + )} + + )} + {((!src && !isLoading) || !startPlay) && ( + { + if (from === "create") return; + dispatch(setVideoPlaying(null)); + togglePlay(); + }} sx={{ - width: "50px", - height: "50px", - color: "white", + cursor: "pointer", }} - /> - - )} - - { - setShowControlsFullScreen(true); - }} - onMouseLeave={e => { - setShowControlsFullScreen(false); - }} - preload="metadata" - style={ - startPlay - ? { - ...customStyle, - objectFit: persistSelector.stretchVideoSetting, - height: "calc(100% - 80px)", - } - : { ...customStyle, height: "100%" } - } - /> - - - {isMobileView && canPlay && showControlsFullScreen ? ( - <> - - {playing ? : } - - + - - - - - - - - - - - - increaseSpeed()}> - - Speed: {playbackRate}x - - - + + )} + + { + setShowControlsFullScreen(true); + }} + onMouseLeave={e => { + setShowControlsFullScreen(false); + }} + preload="metadata" + style={ + startPlay + ? { + ...customStyle, + objectFit: persistSelector.stretchVideoSetting, + height: "calc(100% - 80px)", + } + : { ...customStyle, height: "100%" } + } + /> + + + {isMobileView && canPlay && showControlsFullScreen ? ( + <> + + {playing ? : } + + + + + + + + + + + + + + increaseSpeed()}> + + Speed: {playbackRate}x + + + + + + + + + + + ) : canPlay ? ( + <> + + {playing ? : } + + + + + + + {progress && videoRef.current?.duration && formatTime(progress)} + / + {progress && + videoRef.current?.duration && + formatTime(videoRef.current?.duration)} + + + {isMuted ? : } + + + increaseSpeed()} + > + Speed: {playbackRate}x + + + - - + + - - - - ) : canPlay ? ( - <> - - {playing ? : } - - - - - - - {progress && videoRef.current?.duration && formatTime(progress)}/ - {progress && - videoRef.current?.duration && - formatTime(videoRef.current?.duration)} - - - {isMuted ? : } - - - increaseSpeed()} - > - Speed: {playbackRate}x - - - - - - - - - - ) : null} - - - ); -}; + + + ) : null} + + + ); + } +); diff --git a/src/components/layout/Navbar/Navbar.tsx b/src/components/layout/Navbar/Navbar.tsx index df28207..051a3cd 100644 --- a/src/components/layout/Navbar/Navbar.tsx +++ b/src/components/layout/Navbar/Navbar.tsx @@ -35,8 +35,8 @@ import { } from "../../../state/features/videoSlice"; import { RootState } from "../../../state/store"; import { useWindowSize } from "../../../hooks/useWindowSize"; -import { PublishVideo } from "../../PublishVideo/PublishVideo.tsx"; -import { StyledButton } from "../../PublishVideo/PublishVideo-styles.tsx"; +import { PublishVideo } from "../../Publish/PublishVideo/PublishVideo.tsx"; +import { StyledButton } from "../../Publish/PublishVideo/PublishVideo-styles.tsx"; import { Notifications } from "../../common/Notifications/Notifications"; interface Props { isAuthenticated: boolean; diff --git a/src/constants/Categories.ts b/src/constants/Categories.ts index 85b4ec1..63ac90e 100644 --- a/src/constants/Categories.ts +++ b/src/constants/Categories.ts @@ -39,6 +39,7 @@ export const categories = [ { id: 24, name: "Anime" }, { id: 25, name: "Cartoons" }, { id: 26, name: "Qortal" }, + { id: 99, name: "Other" }, ].sort(sortCategory); export const subCategories: CategoryMap = { @@ -59,7 +60,7 @@ export const subCategories: CategoryMap = { { id: 113, name: "Indie Films" }, { id: 114, name: "International Films" }, { id: 115, name: "Biographies & True Stories" }, - { id: 116, name: "Other" }, + { id: 199, name: "Other" }, ].sort(sortCategory), 2: [ // Series @@ -78,14 +79,14 @@ export const subCategories: CategoryMap = { { id: 213, name: "Anthologies" }, { id: 214, name: "International Series" }, { id: 215, name: "Miniseries" }, - { id: 216, name: "Other" }, + { id: 299, name: "Other" }, ].sort(sortCategory), 4: [ // Education { id: 400, name: "Tutorial" }, - { id: 401, name: "Documentary" }, { id: 401, name: "Qortal" }, - { id: 402, name: "Other" }, + { id: 402, name: "Documentary" }, + { id: 499, name: "Other" }, ].sort(sortCategory), 24: [ @@ -102,6 +103,6 @@ export const subCategories: CategoryMap = { { id: 2411, name: "Harem" }, { id: 2412, name: "Ecchi" }, { id: 2413, name: "Idol" }, - { id: 2414, name: "Other" }, + { id: 2499, name: "Other" }, ].sort(sortCategory), }; diff --git a/src/constants/Identifiers.ts b/src/constants/Identifiers.ts index d568662..b31b257 100644 --- a/src/constants/Identifiers.ts +++ b/src/constants/Identifiers.ts @@ -8,8 +8,12 @@ export const QTUBE_PLAYLIST_BASE = useTestIdentifiers export const SUPER_LIKE_BASE = useTestIdentifiers ? "MYTEST_superlike_" : "qtube_superlike_"; + +export const LIKE_BASE = useTestIdentifiers ? "MYTEST_like_" : "qtube_like_"; + export const COMMENT_BASE = useTestIdentifiers ? "qcomment_v1_MYTEST_" : "qcomment_v1_qtube_"; export const FOR = useTestIdentifiers ? "FORTEST5" : "FOR0962"; export const FOR_SUPER_LIKE = useTestIdentifiers ? "MYTEST_sl" : `qtube_sl`; +export const FOR_LIKE = useTestIdentifiers ? "MYTEST_like" : `qtube_like`; diff --git a/src/constants/Misc.ts b/src/constants/Misc.ts index bed6f07..2edb2a9 100644 --- a/src/constants/Misc.ts +++ b/src/constants/Misc.ts @@ -1,6 +1,3 @@ -export const minPriceSuperlike = 10; +export const minPriceSuperlike = 1; export const titleFormatter = /[^a-zA-Z0-9\s-_!?()&'",.;:|—~@#$%^*+=<>]/g; export const titleFormatterOnSave = /[^a-zA-Z0-9\s-_!()&',.;—~@#$%^+=]/g; - -export const allTabValue = "all"; -export const subscriptionTabValue = "subscriptions"; diff --git a/src/hooks/useFetchVideos.tsx b/src/hooks/useFetchVideos.tsx index f181bff..d9de1db 100644 --- a/src/hooks/useFetchVideos.tsx +++ b/src/hooks/useFetchVideos.tsx @@ -24,9 +24,9 @@ import { QTUBE_PLAYLIST_BASE, QTUBE_VIDEO_BASE, } from "../constants/Identifiers.ts"; -import { allTabValue, subscriptionTabValue } from "../constants/Misc.ts"; import { persistReducer } from "redux-persist"; import { subscriptionListFilter } from "../App.tsx"; +import { ContentType, VideoListType } from "../state/features/persistSlice.ts"; export const useFetchVideos = () => { const dispatch = useDispatch(); @@ -194,15 +194,15 @@ export const useFetchVideos = () => { category?: string; subcategory?: string; keywords?: string; - type?: string; + contentType?: ContentType; }; - const emptyFilters = { + const emptyFilters: FilterType = { name: "", category: "", subcategory: "", keywords: "", - type: "", + contentType: "videos", }; const getVideos = React.useCallback( async ( @@ -210,16 +210,16 @@ export const useFetchVideos = () => { reset?: boolean, resetFilters?: boolean, limit?: number, - listType = allTabValue + videoListType: VideoListType = "all" ) => { - emptyFilters.type = filters.type; + emptyFilters.contentType = filters.contentType; try { const { name = "", category = "", subcategory = "", keywords = "", - type = filters.type, + contentType = filters.contentType, }: FilterType = resetFilters ? emptyFilters : filters; let offset = videos.length; if (reset) { @@ -231,9 +231,7 @@ export const useFetchVideos = () => { if (name) { defaultUrl = defaultUrl + `&name=${name}`; - } - - if (listType === subscriptionTabValue) { + } else if (videoListType === "subscriptions") { const filteredSubscribeList = await subscriptionListFilter(false); filteredSubscribeList.map(sub => { defaultUrl += `&name=${sub.subscriberName}`; @@ -252,7 +250,7 @@ export const useFetchVideos = () => { if (keywords) { defaultUrl = defaultUrl + `&query=${keywords}`; } - if (type === "playlists") { + if (contentType === "playlists") { defaultUrl = defaultUrl + `&service=PLAYLIST`; defaultUrl = defaultUrl + `&identifier=${QTUBE_PLAYLIST_BASE}`; } else { @@ -431,9 +429,7 @@ export const useFetchVideos = () => { const totalVideosPublished = responseData.length; const uniqueNames = new Set(responseData.map(video => video.name)); const totalNamesPublished = uniqueNames.size; - const videosPerNamePublished = ( - totalVideosPublished / totalNamesPublished - ).toFixed(0); + const videosPerNamePublished = totalVideosPublished / totalNamesPublished; dispatch(setTotalVideosPublished(totalVideosPublished)); dispatch(setTotalNamesPublished(totalNamesPublished)); diff --git a/src/pages/IndividualProfile/IndividualProfile.tsx b/src/pages/ContentPages/IndividualProfile/IndividualProfile.tsx similarity index 53% rename from src/pages/IndividualProfile/IndividualProfile.tsx rename to src/pages/ContentPages/IndividualProfile/IndividualProfile.tsx index acf1d42..5f941d9 100644 --- a/src/pages/IndividualProfile/IndividualProfile.tsx +++ b/src/pages/ContentPages/IndividualProfile/IndividualProfile.tsx @@ -1,34 +1,37 @@ import React, { useMemo } from "react"; -import { VideoListComponentLevel } from "../Home/VideoListComponentLevel"; -import { HeaderContainer, ProfileContainer } from "./Profile-styles"; +import { VideoListComponentLevel } from "../../Home/VideoListComponentLevel.tsx"; +import { HeaderContainer, ProfileContainer } from "./Profile-styles.tsx"; import { AuthorTextComment, StyledCardColComment, StyledCardHeaderComment, -} from "../VideoContent/VideoContent-styles"; +} from "../VideoContent/VideoContent-styles.tsx"; import { Avatar, Box, useTheme } from "@mui/material"; import { useParams } from "react-router-dom"; import { useSelector } from "react-redux"; -import { setUserAvatarHash } from "../../state/features/globalSlice"; -import { RootState } from "../../state/store"; -import { SubscribeButton } from "../../components/common/SubscribeButton.tsx"; +import { setUserAvatarHash } from "../../../state/features/globalSlice.ts"; +import { RootState } from "../../../state/store.ts"; +import { SubscribeButton } from "../../../components/common/ContentButtons/SubscribeButton.tsx"; +import { FollowButton } from "../../../components/common/ContentButtons/FollowButton.tsx"; export const IndividualProfile = () => { - const { name: paramName } = useParams(); + const { name: channelName } = useParams(); + const userName = useSelector((state: RootState) => state.auth.user?.name); const userAvatarHash = useSelector( (state: RootState) => state.global.userAvatarHash ); + const theme = useTheme(); const avatarUrl = useMemo(() => { let url = ""; - if (paramName && userAvatarHash[paramName]) { - url = userAvatarHash[paramName]; + if (channelName && userAvatarHash[channelName]) { + url = userAvatarHash[channelName]; } return url; - }, [userAvatarHash, paramName]); + }, [userAvatarHash, channelName]); return ( @@ -46,8 +49,8 @@ export const IndividualProfile = () => { > @@ -58,13 +61,21 @@ export const IndividualProfile = () => { : "#d6e8ff" } > - {paramName} + {channelName} - + {channelName !== userName && ( + <> + + + + )} diff --git a/src/pages/IndividualProfile/Profile-styles.tsx b/src/pages/ContentPages/IndividualProfile/Profile-styles.tsx similarity index 100% rename from src/pages/IndividualProfile/Profile-styles.tsx rename to src/pages/ContentPages/IndividualProfile/Profile-styles.tsx diff --git a/src/pages/PlaylistContent/PlaylistContent-styles.tsx b/src/pages/ContentPages/PlaylistContent/PlaylistContent-styles.tsx similarity index 100% rename from src/pages/PlaylistContent/PlaylistContent-styles.tsx rename to src/pages/ContentPages/PlaylistContent/PlaylistContent-styles.tsx diff --git a/src/pages/PlaylistContent/PlaylistContent.tsx b/src/pages/ContentPages/PlaylistContent/PlaylistContent.tsx similarity index 82% rename from src/pages/PlaylistContent/PlaylistContent.tsx rename to src/pages/ContentPages/PlaylistContent/PlaylistContent.tsx index ced8b00..2dce472 100644 --- a/src/pages/PlaylistContent/PlaylistContent.tsx +++ b/src/pages/ContentPages/PlaylistContent/PlaylistContent.tsx @@ -7,15 +7,18 @@ import React, { } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useNavigate, useParams } from "react-router-dom"; -import { setIsLoadingGlobal } from "../../state/features/globalSlice"; +import { setIsLoadingGlobal } from "../../../state/features/globalSlice.ts"; import { Avatar, Box, Typography, useTheme } from "@mui/material"; -import { VideoPlayer } from "../../components/common/VideoPlayer/VideoPlayer.tsx"; -import { RootState } from "../../state/store"; -import { addToHashMap } from "../../state/features/videoSlice"; +import { + refType, + VideoPlayer, +} from "../../../components/common/VideoPlayer/VideoPlayer.tsx"; +import { RootState } from "../../../state/store.ts"; +import { addToHashMap } from "../../../state/features/videoSlice.ts"; import AttachFileIcon from "@mui/icons-material/AttachFile"; import DownloadIcon from "@mui/icons-material/Download"; -import mockImg from "../../test/mockimg.jpg"; +import mockImg from "../../../test/mockimg.jpg"; import { AuthorTextComment, FileAttachmentContainer, @@ -26,39 +29,43 @@ import { VideoDescription, VideoPlayerContainer, VideoTitle, -} from "./PlaylistContent-styles"; -import { setUserAvatarHash } from "../../state/features/globalSlice"; +} from "./PlaylistContent-styles.tsx"; +import { setUserAvatarHash } from "../../../state/features/globalSlice.ts"; import { formatDate, formatDateSeconds, formatTimestampSeconds, -} from "../../utils/time"; -import { NavbarName } from "../../components/layout/Navbar/Navbar-styles"; -import { CommentSection } from "../../components/common/Comments/CommentSection"; +} from "../../../utils/time.ts"; +import { NavbarName } from "../../../components/layout/Navbar/Navbar-styles.tsx"; +import { CommentSection } from "../../../components/common/Comments/CommentSection.tsx"; import { CrowdfundSubTitle, CrowdfundSubTitleRow, -} from "../../components/PublishVideo/PublishVideo-styles.tsx"; -import { Playlists } from "../../components/Playlists/Playlists"; -import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml"; -import FileElement from "../../components/common/FileElement"; -import { SuperLike } from "../../components/common/SuperLike/SuperLike"; -import { useFetchSuperLikes } from "../../hooks/useFetchSuperLikes"; +} from "../../../components/Publish/PublishVideo/PublishVideo-styles.tsx"; +import { Playlists } from "../../../components/Playlists/Playlists.tsx"; +import { DisplayHtml } from "../../../components/common/TextEditor/DisplayHtml.tsx"; +import FileElement from "../../../components/common/FileElement.tsx"; +import { SuperLike } from "../../../components/common/ContentButtons/SuperLike.tsx"; +import { useFetchSuperLikes } from "../../../hooks/useFetchSuperLikes.tsx"; import { extractSigValue, getPaymentInfo, isTimestampWithinRange, -} from "../VideoContent/VideoContent"; -import { SuperLikesSection } from "../../components/common/SuperLikesList/SuperLikesSection"; +} from "../VideoContent/VideoContent.tsx"; +import { SuperLikesSection } from "../../../components/common/SuperLikesList/SuperLikesSection.tsx"; import { QTUBE_VIDEO_BASE, SUPER_LIKE_BASE, -} from "../../constants/Identifiers.ts"; -import { minPriceSuperlike } from "../../constants/Misc.ts"; -import { SubscribeButton } from "../../components/common/SubscribeButton.tsx"; +} from "../../../constants/Identifiers.ts"; +import { minPriceSuperlike } from "../../../constants/Misc.ts"; +import { SubscribeButton } from "../../../components/common/ContentButtons/SubscribeButton.tsx"; +import { FollowButton } from "../../../components/common/ContentButtons/FollowButton.tsx"; +import { LikeAndDislike } from "../../../components/common/ContentButtons/LikeAndDislike.tsx"; export const PlaylistContent = () => { - const { name, id } = useParams(); + const { name: channelName, id } = useParams(); + const userName = useSelector((state: RootState) => state.auth.user?.name); + const [doAutoPlay, setDoAutoPlay] = useState(false); const [isExpandedDescription, setIsExpandedDescription] = useState(false); @@ -68,6 +75,7 @@ export const PlaylistContent = () => { const [superlikeList, setSuperlikelist] = useState([]); const [loadingSuperLikes, setLoadingSuperLikes] = useState(false); const { addSuperlikeRawDataGetToList } = useFetchSuperLikes(); + const containerRef = useRef(null); const calculateAmountSuperlike = useMemo(() => { const totalQort = superlikeList?.reduce((acc, curr) => { @@ -95,10 +103,10 @@ export const PlaylistContent = () => { }; useEffect(() => { - if (name) { - getAddressName(name); + if (channelName) { + getAddressName(channelName); } - }, [name]); + }, [channelName]); const userAvatarHash = useSelector( (state: RootState) => state.global.userAvatarHash @@ -107,12 +115,12 @@ export const PlaylistContent = () => { const avatarUrl = useMemo(() => { let url = ""; - if (name && userAvatarHash[name]) { - url = userAvatarHash[name]; + if (channelName && userAvatarHash[channelName]) { + url = userAvatarHash[channelName]; } return url; - }, [userAvatarHash, name]); + }, [userAvatarHash, channelName]); const navigate = useNavigate(); const theme = useTheme(); @@ -282,10 +290,10 @@ export const PlaylistContent = () => { ); React.useEffect(() => { - if (name && id) { - checkforPlaylist(name, id); + if (channelName && id) { + checkforPlaylist(channelName, id); } - }, [id, name]); + }, [id, channelName]); useEffect(() => { if (contentRef.current) { @@ -395,6 +403,14 @@ export const PlaylistContent = () => { getComments(videoData?.id, nameAddress); }, [getComments, videoData?.id, nameAddress]); + const focusVideo = (e: React.MouseEvent) => { + const focusRef = containerRef.current?.getContainerRef()?.current; + const isCorrectTarget = e.currentTarget == e.target; + if (focusRef && isCorrectTarget) { + focusRef.focus({ preventScroll: true }); + } + }; + return ( { flexDirection: "column", padding: "20px 10px", }} + onClick={focusVideo} > { name={videoReference?.name} service={videoReference?.service} identifier={videoReference?.identifier} - user={name} + user={channelName} jsonId={id} poster={videoCover || ""} nextVideo={nextVideo} onEnd={onEndVideo} autoPlay={doAutoPlay} customStyle={{ aspectRatio: "16/9" }} + ref={containerRef} /> )} {playlistData && ( @@ -473,12 +491,12 @@ export const PlaylistContent = () => { cursor: "pointer", }} onClick={() => { - navigate(`/channel/${name}`); + navigate(`/channel/${channelName}`); }} > @@ -492,14 +510,22 @@ export const PlaylistContent = () => { cursor: "pointer", }} onClick={() => { - navigate(`/channel/${name}`); + navigate(`/channel/${channelName}`); }} > - {name} - + {channelName} + {channelName !== userName && ( + <> + + + + )} @@ -511,16 +537,22 @@ export const PlaylistContent = () => { }} > {videoData && ( - { - setSuperlikelist(prev => [val, ...prev]); - }} - /> + <> + + { + setSuperlikelist(prev => [val, ...prev]); + }} + /> + )} Save to Disk @@ -677,7 +709,10 @@ export const PlaylistContent = () => { maxWidth: "1200px", }} > - + ); diff --git a/src/pages/VideoContent/VideoContent-styles.tsx b/src/pages/ContentPages/VideoContent/VideoContent-styles.tsx similarity index 100% rename from src/pages/VideoContent/VideoContent-styles.tsx rename to src/pages/ContentPages/VideoContent/VideoContent-styles.tsx diff --git a/src/pages/VideoContent/VideoContent.tsx b/src/pages/ContentPages/VideoContent/VideoContent.tsx similarity index 79% rename from src/pages/VideoContent/VideoContent.tsx rename to src/pages/ContentPages/VideoContent/VideoContent.tsx index 247c565..451fbb9 100644 --- a/src/pages/VideoContent/VideoContent.tsx +++ b/src/pages/ContentPages/VideoContent/VideoContent.tsx @@ -7,15 +7,18 @@ import React, { } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useNavigate, useParams } from "react-router-dom"; -import { setIsLoadingGlobal } from "../../state/features/globalSlice"; +import { setIsLoadingGlobal } from "../../../state/features/globalSlice.ts"; import { Avatar, Box, Typography, useTheme } from "@mui/material"; -import { VideoPlayer } from "../../components/common/VideoPlayer/VideoPlayer.tsx"; -import { RootState } from "../../state/store"; -import { addToHashMap } from "../../state/features/videoSlice"; +import { + refType, + VideoPlayer, +} from "../../../components/common/VideoPlayer/VideoPlayer.tsx"; +import { RootState } from "../../../state/store.ts"; +import { addToHashMap } from "../../../state/features/videoSlice.ts"; import AttachFileIcon from "@mui/icons-material/AttachFile"; import DownloadIcon from "@mui/icons-material/Download"; -import mockImg from "../../test/mockimg.jpg"; +import mockImg from "../../../test/mockimg.jpg"; import { AuthorTextComment, FileAttachmentContainer, @@ -26,37 +29,39 @@ import { VideoDescription, VideoPlayerContainer, VideoTitle, -} from "./VideoContent-styles"; -import { setUserAvatarHash } from "../../state/features/globalSlice"; +} from "./VideoContent-styles.tsx"; +import { setUserAvatarHash } from "../../../state/features/globalSlice.ts"; import { formatDate, formatDateSeconds, formatTimestampSeconds, -} from "../../utils/time"; -import { NavbarName } from "../../components/layout/Navbar/Navbar-styles"; -import { CommentSection } from "../../components/common/Comments/CommentSection"; +} from "../../../utils/time.ts"; +import { NavbarName } from "../../../components/layout/Navbar/Navbar-styles.tsx"; +import { CommentSection } from "../../../components/common/Comments/CommentSection.tsx"; import { CrowdfundSubTitle, CrowdfundSubTitleRow, -} from "../../components/PublishVideo/PublishVideo-styles.tsx"; -import { Playlists } from "../../components/Playlists/Playlists"; -import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml"; -import FileElement from "../../components/common/FileElement"; -import { SuperLike } from "../../components/common/SuperLike/SuperLike"; -import { CommentContainer } from "../../components/common/Comments/Comments-styles"; -import { Comment } from "../../components/common/Comments/Comment"; -import { SuperLikesSection } from "../../components/common/SuperLikesList/SuperLikesSection"; -import { useFetchSuperLikes } from "../../hooks/useFetchSuperLikes"; +} from "../../../components/Publish/PublishVideo/PublishVideo-styles.tsx"; +import { Playlists } from "../../../components/Playlists/Playlists.tsx"; +import { DisplayHtml } from "../../../components/common/TextEditor/DisplayHtml.tsx"; +import FileElement from "../../../components/common/FileElement.tsx"; +import { SuperLike } from "../../../components/common/ContentButtons/SuperLike.tsx"; +import { CommentContainer } from "../../../components/common/Comments/Comments-styles.tsx"; +import { Comment } from "../../../components/common/Comments/Comment.tsx"; +import { SuperLikesSection } from "../../../components/common/SuperLikesList/SuperLikesSection.tsx"; +import { useFetchSuperLikes } from "../../../hooks/useFetchSuperLikes.tsx"; import { FOR_SUPER_LIKE, QTUBE_VIDEO_BASE, SUPER_LIKE_BASE, -} from "../../constants/Identifiers.ts"; +} from "../../../constants/Identifiers.ts"; import { minPriceSuperlike, titleFormatterOnSave, -} from "../../constants/Misc.ts"; -import { SubscribeButton } from "../../components/common/SubscribeButton.tsx"; +} from "../../../constants/Misc.ts"; +import { SubscribeButton } from "../../../components/common/ContentButtons/SubscribeButton.tsx"; +import { FollowButton } from "../../../components/common/ContentButtons/FollowButton.tsx"; +import { LikeAndDislike } from "../../../components/common/ContentButtons/LikeAndDislike.tsx"; export function isTimestampWithinRange(resTimestamp, resCreated) { // Calculate the absolute difference in milliseconds @@ -116,12 +121,15 @@ export const getPaymentInfo = async (signature: string) => { }; export const VideoContent = () => { - const { name, id } = useParams(); + const { name: channelName, id } = useParams(); + const userName = useSelector((state: RootState) => state.auth.user?.name); + const [isExpandedDescription, setIsExpandedDescription] = useState(false); const [superlikeList, setSuperlikelist] = useState([]); const [loadingSuperLikes, setLoadingSuperLikes] = useState(false); const { addSuperlikeRawDataGetToList } = useFetchSuperLikes(); + const containerRef = useRef(null); const calculateAmountSuperlike = useMemo(() => { const totalQort = superlikeList?.reduce((acc, curr) => { @@ -143,6 +151,7 @@ export const VideoContent = () => { const userAvatarHash = useSelector( (state: RootState) => state.global.userAvatarHash ); + const contentRef = useRef(null); const getAddressName = async name => { @@ -157,18 +166,18 @@ export const VideoContent = () => { }; useEffect(() => { - if (name) { - getAddressName(name); + if (channelName) { + getAddressName(channelName); } - }, [name]); + }, [channelName]); const avatarUrl = useMemo(() => { let url = ""; - if (name && userAvatarHash[name]) { - url = userAvatarHash[name]; + if (channelName && userAvatarHash[channelName]) { + url = userAvatarHash[channelName]; } return url; - }, [userAvatarHash, name]); + }, [userAvatarHash, channelName]); const navigate = useNavigate(); const theme = useTheme(); @@ -279,16 +288,16 @@ export const VideoContent = () => { }, []); React.useEffect(() => { - if (name && id) { - const existingVideo = hashMapVideos[id + "-" + name]; + if (channelName && id) { + const existingVideo = hashMapVideos[id + "-" + channelName]; if (existingVideo) { setVideoData(existingVideo); } else { - getVideoData(name, id); + getVideoData(channelName, id); } } - }, [id, name]); + }, [id, channelName]); useEffect(() => { if (contentRef.current) { @@ -367,6 +376,14 @@ export const VideoContent = () => { (state: RootState) => state.persist.subscriptionList ); + const focusVideo = (e: React.MouseEvent) => { + const focusRef = containerRef.current?.getContainerRef()?.current; + const isCorrectTarget = e.currentTarget == e.target; + if (focusRef && isCorrectTarget) { + focusRef.focus({ preventScroll: true }); + } + }; + return ( { flexDirection: "column", padding: "20px 10px", }} + onClick={focusVideo} > { name={videoReference?.name} service={videoReference?.service} identifier={videoReference?.identifier} - user={name} + user={channelName} jsonId={id} poster={videoCover || ""} customStyle={{ aspectRatio: "16/9" }} + ref={containerRef} /> )} { cursor: "pointer", }} onClick={() => { - navigate(`/channel/${name}`); + navigate(`/channel/${channelName}`); }} > @@ -433,14 +452,22 @@ export const VideoContent = () => { cursor: "pointer", }} onClick={() => { - navigate(`/channel/${name}`); + navigate(`/channel/${channelName}`); }} > - {name} - + {channelName} + {channelName !== userName && ( + <> + + + + )} @@ -452,16 +479,22 @@ export const VideoContent = () => { }} > {videoData && ( - { - setSuperlikelist(prev => [val, ...prev]); - }} - /> + <> + + { + setSuperlikelist(prev => [val, ...prev]); + }} + /> + )} Save to Disk @@ -598,7 +631,7 @@ export const VideoContent = () => { loadingSuperLikes={loadingSuperLikes} superlikes={superlikeList} postId={id || ""} - postName={name || ""} + postName={channelName || ""} /> { maxWidth: "1200px", }} > - + ); diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index ce33036..558669b 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -37,12 +37,12 @@ import { import { changeFilterType, resetSubscriptions, + VideoListType, } from "../../state/features/persistSlice.ts"; import { categories, subCategories } from "../../constants/Categories.ts"; import { ListSuperLikeContainer } from "../../components/common/ListSuperLikes/ListSuperLikeContainer.tsx"; import { TabContext, TabList, TabPanel } from "@mui/lab"; import VideoList from "./VideoList.tsx"; -import { allTabValue, subscriptionTabValue } from "../../constants/Misc.ts"; import { setHomePageSelectedTab } from "../../state/features/persistSlice.ts"; import { StatsData } from "../../components/StatsData.tsx"; @@ -74,7 +74,9 @@ export const Home = ({ mode }: HomeProps) => { ); const [isLoading, setIsLoading] = useState(false); - const [tabValue, setTabValue] = useState(persistReducer.selectedTab); + const [tabValue, setTabValue] = useState( + persistReducer.selectedTab + ); const tabFontSize = "20px"; @@ -123,14 +125,14 @@ export const Home = ({ mode }: HomeProps) => { if (!firstFetch.current || !afterFetch.current) return; if (isFetching.current) return; isFetching.current = true; - console.log("in getvideoshandler"); + await getVideos( { name: filterName, category: selectedCategoryVideos?.id, subcategory: selectedSubCategoryVideos?.id, keywords: filterSearch, - type: filterType, + contentType: filterType, }, reset, resetFilters, @@ -171,7 +173,7 @@ export const Home = ({ mode }: HomeProps) => { category: "", subcategory: "", keywords: "", - type: filterType, + contentType: filterType, }, null, null, @@ -269,11 +271,10 @@ export const Home = ({ mode }: HomeProps) => { }; useEffect(() => { - console.log("useeffect 5"); getVideosHandler(true); }, [tabValue]); - const changeTab = (e: React.SyntheticEvent, newValue: string) => { + const changeTab = (e: React.SyntheticEvent, newValue: VideoListType) => { setTabValue(newValue); dispatch(setHomePageSelectedTab(newValue)); }; @@ -516,23 +517,23 @@ export const Home = ({ mode }: HomeProps) => { > - + - + {filteredSubscriptionList.length > 0 ? ( <> @@ -541,10 +542,12 @@ export const Home = ({ mode }: HomeProps) => { isLoading={isLoading} > - ) : ( + ) : !isLoading ? (
You have no subscriptions
+ ) : ( + <> )}
diff --git a/src/state/features/persistSlice.ts b/src/state/features/persistSlice.ts index 8a3747d..1b02954 100644 --- a/src/state/features/persistSlice.ts +++ b/src/state/features/persistSlice.ts @@ -1,14 +1,19 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { allTabValue, subscriptionTabValue } from "../../constants/Misc.ts"; -import { SubscriptionData } from "../../components/common/SubscribeButton.tsx"; - -type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down"; -type SubscriptionListFilterType = "ALL" | "currentNameOnly"; +import { SubscriptionData } from "../../components/common/ContentButtons/SubscribeButton.tsx"; +export type StretchVideoType = + | "contain" + | "fill" + | "cover" + | "none" + | "scale-down"; +export type SubscriptionListFilterType = "ALL" | "currentNameOnly"; +export type ContentType = "videos" | "playlists"; +export type VideoListType = "all" | "subscriptions"; interface settingsState { - selectedTab: string; + selectedTab: VideoListType; stretchVideoSetting: StretchVideoType; - filterType: string; + filterType: ContentType; subscriptionList: SubscriptionData[]; playbackRate: number; subscriptionListFilter: SubscriptionListFilterType; @@ -16,7 +21,7 @@ interface settingsState { } const initialState: settingsState = { - selectedTab: allTabValue, + selectedTab: "all", stretchVideoSetting: "contain", filterType: "videos", subscriptionList: [], diff --git a/src/state/features/videoSlice.ts b/src/state/features/videoSlice.ts index 15c38a1..19e685c 100644 --- a/src/state/features/videoSlice.ts +++ b/src/state/features/videoSlice.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { SubscriptionData } from "../../components/common/SubscribeButton"; +import { SubscriptionData } from "../../components/common/ContentButtons/SubscribeButton.tsx"; interface GlobalState { videos: Video[]; diff --git a/src/utils/qortalRequestFunctions.ts b/src/utils/qortalRequestFunctions.ts index bd1111d..c64c0bc 100644 --- a/src/utils/qortalRequestFunctions.ts +++ b/src/utils/qortalRequestFunctions.ts @@ -2,6 +2,7 @@ import { AccountInfo, AccountName, GetRequestData, + SearchResourcesResponse, SearchTransactionResponse, TransactionSearchParams, } from "./qortalRequestTypes.ts"; @@ -46,3 +47,30 @@ export const searchTransactions = async (params: TransactionSearchParams) => { ...params, })) as SearchTransactionResponse[]; }; + +export const fetchResourcesByIdentifier = async ( + service: string, + identifier: string +) => { + const names: SearchResourcesResponse[] = await qortalRequest({ + action: "SEARCH_QDN_RESOURCES", + service, + identifier, + includeMetadata: false, + }); + const distinctNames = names.filter( + (searchResponse, index) => names.indexOf(searchResponse) === index + ); + + const promises: Promise[] = []; + distinctNames.map(response => { + const resource: Promise = qortalRequest({ + action: "FETCH_QDN_RESOURCE", + name: response.name, + service, + identifier, + }); + promises.push(resource); + }); + return (await Promise.all(promises)) as T[]; +}; diff --git a/src/utils/qortalRequestTypes.ts b/src/utils/qortalRequestTypes.ts index 5a6cf54..085a28e 100644 --- a/src/utils/qortalRequestTypes.ts +++ b/src/utils/qortalRequestTypes.ts @@ -23,6 +23,23 @@ export interface SearchTransactionResponse { amount: string; } +export interface MetaData { + title: string; + description: string; + tags: string[]; + mimeType: string; +} + +export interface SearchResourcesResponse { + name: string; + service: string; + identifier: string; + metadata?: MetaData; + size: number; + created: number; + updated: number; +} + export type TransactionType = | "GENESIS" | "PAYMENT" diff --git a/src/wrappers/GlobalWrapper.tsx b/src/wrappers/GlobalWrapper.tsx index fa7a136..c80f223 100644 --- a/src/wrappers/GlobalWrapper.tsx +++ b/src/wrappers/GlobalWrapper.tsx @@ -18,14 +18,14 @@ import { import { VideoPlayerGlobal } from "../components/common/VideoPlayer/VideoPlayerGlobal.tsx"; import { Rnd } from "react-rnd"; import { RequestQueue } from "../utils/queue"; -import { EditVideo } from "../components/EditVideo/EditVideo"; -import { EditPlaylist } from "../components/EditPlaylist/EditPlaylist"; +import { EditVideo } from "../components/Publish/EditVideo/EditVideo"; +import { EditPlaylist } from "../components/Publish/EditPlaylist/EditPlaylist"; import ConsentModal from "../components/common/ConsentModal"; import { extractSigValue, getPaymentInfo, isTimestampWithinRange, -} from "../pages/VideoContent/VideoContent"; +} from "../pages/ContentPages/VideoContent/VideoContent"; import { useFetchSuperLikes } from "../hooks/useFetchSuperLikes"; import { SUPER_LIKE_BASE } from "../constants/Identifiers.ts"; import { minPriceSuperlike } from "../constants/Misc.ts"; @@ -143,8 +143,8 @@ const GlobalWrapper: React.FC = ({ children, setTheme }) => { const getSuperlikes = useCallback(async () => { try { - let totalCount = 0 - let validCount = 0 + let totalCount = 0; + let validCount = 0; let comments: any[] = []; while (validCount < 20 && totalCount < 100) { const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${SUPER_LIKE_BASE}&limit=1&offset=${totalCount}&includemetadata=true&reverse=true&excludeblocked=true`; @@ -177,12 +177,12 @@ const GlobalWrapper: React.FC = ({ children, setTheme }) => { }); comments = [ ...comments, - { - ...comment, - message: "", - amount: res.amount, - }, - ]; + { + ...comment, + message: "", + amount: res.amount, + }, + ]; validCount++; } } catch (error) {}