3
0
mirror of https://github.com/Qortal/q-tube.git synced 2025-02-11 17:55:51 +00:00

Mobile support for Home, Video, and Channel pages.

DownloadTaskManager.tsx:
 1. Fixed bug that made clicking on videos do nothing instead of going to that video's page.

 2. It also displays the video title instead of identifier.

 3. The menu is a lot wider, so it is far easier to see the full titles of videos

VideoPlayer.tsx now persists across sessions whether it was muted and its volume when unmuting.

When screen is small, VideoPlayer automatically switches to Mobile Controls. Mobile Controls now show Playback Rate and Fullscreen buttons.

 DownloadTaskManager.tsx now shows the video title instead of identifier.
This commit is contained in:
Qortal Dev 2024-12-13 15:34:12 -07:00
parent da0747463b
commit 49a232136d
35 changed files with 1265 additions and 1036 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "qtube", "name": "qtube",
"private": true, "private": true,
"version": "2.0.0", "version": "2.1.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@ -1,10 +1,11 @@
import React from "react"; import React from "react";
import { smallScreenSizeString } from "../../constants/Misc.ts";
import { CardContentContainerComment } from "../common/Comments/Comments-styles"; import { CardContentContainerComment } from "../common/Comments/Comments-styles";
import { import {
CrowdfundSubTitle, CrowdfundSubTitle,
CrowdfundSubTitleRow, CrowdfundSubTitleRow,
} from "../Publish/PublishVideo/PublishVideo-styles.tsx"; } from "../Publish/PublishVideo/PublishVideo-styles.tsx";
import { Box, Typography, useTheme } from "@mui/material"; import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
export const Playlists = ({ export const Playlists = ({
@ -13,7 +14,8 @@ export const Playlists = ({
onClick, onClick,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const navigate = useNavigate(); const isScreenSmall = !useMediaQuery(`(min-width:700px)`);
const videoPlayerHeight = "33.75vw"; // This is videoplayer width * 9/16 (inverse of aspect ratio)
return ( return (
<Box <Box
@ -21,7 +23,7 @@ export const Playlists = ({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
width: "100%", width: "100%",
maxHeight: "30.94vw", height: isScreenSmall ? "200px" : videoPlayerHeight,
}} }}
> >
<CardContentContainerComment <CardContentContainerComment
@ -32,7 +34,7 @@ export const Playlists = ({
}} }}
> >
{playlistData?.videos?.map((vid, index) => { {playlistData?.videos?.map((vid, index) => {
const isCurrentVidPlayling = const isCurrentVidPlaying =
vid?.identifier === currentVideoIdentifier; vid?.identifier === currentVideoIdentifier;
return ( return (
@ -42,15 +44,15 @@ export const Playlists = ({
display: "flex", display: "flex",
gap: "10px", gap: "10px",
width: "100%", width: "100%",
background: isCurrentVidPlayling && theme.palette.primary.main, background: isCurrentVidPlaying && theme.palette.primary.main,
alignItems: "center", alignItems: "center",
padding: "10px", padding: "10px",
borderRadius: "5px", borderRadius: "5px",
cursor: isCurrentVidPlayling ? "default" : "pointer", cursor: isCurrentVidPlaying ? "default" : "pointer",
userSelect: "none", userSelect: "none",
}} }}
onClick={() => { onClick={() => {
if (isCurrentVidPlayling) return; if (isCurrentVidPlaying) return;
onClick(vid.name, vid.identifier); onClick(vid.name, vid.identifier);
// navigate(`/video/${vid.name}/${vid.identifier}`) // navigate(`/video/${vid.name}/${vid.identifier}`)
}} }}

View File

@ -15,8 +15,10 @@ import {
Typography, Typography,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { useSignal } from "@preact/signals-react";
import { useSignals } from "@preact/signals-react/runtime";
import Compressor from "compressorjs"; import Compressor from "compressorjs";
import React, { useEffect, useState } from "react"; import React, { useState } from "react";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
@ -27,6 +29,7 @@ import {
} from "../../../constants/Identifiers.ts"; } from "../../../constants/Identifiers.ts";
import { import {
maxSize, maxSize,
menuIconSize,
titleFormatter, titleFormatter,
videoMaxSize, videoMaxSize,
} from "../../../constants/Misc.ts"; } from "../../../constants/Misc.ts";
@ -38,7 +41,6 @@ import {
import { setNotification } from "../../../state/features/notificationsSlice.ts"; import { setNotification } from "../../../state/features/notificationsSlice.ts";
import { RootState } from "../../../state/store.ts"; import { RootState } from "../../../state/store.ts";
import { formatBytes } from "../../../utils/numberFunctions.ts";
import { objectToBase64 } from "../../../utils/PublishFormatter.ts"; import { objectToBase64 } from "../../../utils/PublishFormatter.ts";
import { getFileName } from "../../../utils/stringFunctions.ts"; import { getFileName } from "../../../utils/stringFunctions.ts";
import { CardContentContainerComment } from "../../common/Comments/Comments-styles.tsx"; import { CardContentContainerComment } from "../../common/Comments/Comments-styles.tsx";
@ -66,8 +68,7 @@ import {
StyledButton, StyledButton,
TimesIcon, TimesIcon,
} from "./PublishVideo-styles.tsx"; } from "./PublishVideo-styles.tsx";
import { signal, Signal, useSignal } from "@preact/signals-react"; import VideoLibraryIcon from "@mui/icons-material/VideoLibrary";
import { useSignals } from "@preact/signals-react/runtime";
export const toBase64 = (file: File): Promise<string | ArrayBuffer | null> => export const toBase64 = (file: File): Promise<string | ArrayBuffer | null> =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
@ -82,13 +83,14 @@ export const toBase64 = (file: File): Promise<string | ArrayBuffer | null> =>
const uid = new ShortUniqueId(); const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 }); const shortuid = new ShortUniqueId({ length: 5 });
interface NewCrowdfundProps { interface PublishVideoProps {
editId?: string; editId?: string;
editContent?: null | { editContent?: null | {
title: string; title: string;
user: string; user: string;
coverImage: string | null; coverImage: string | null;
}; };
afterClose?: () => void;
} }
interface VideoFile { interface VideoFile {
@ -97,7 +99,11 @@ interface VideoFile {
description: string; description: string;
coverImage?: string; coverImage?: string;
} }
export const PublishVideo = ({ editId, editContent }: NewCrowdfundProps) => { export const PublishVideo = ({
editId,
editContent,
afterClose,
}: PublishVideoProps) => {
const theme = useTheme(); const theme = useTheme();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false); const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
@ -188,13 +194,9 @@ export const PublishVideo = ({ editId, editContent }: NewCrowdfundProps) => {
}, },
}); });
// useEffect(() => {
// if (editContent) {
// }
// }, [editContent]);
const onClose = () => { const onClose = () => {
setIsOpen(false); setIsOpen(false);
if (afterClose) afterClose();
}; };
const search = async () => { const search = async () => {
@ -633,12 +635,20 @@ export const PublishVideo = ({ editId, editContent }: NewCrowdfundProps) => {
{editId ? null : ( {editId ? null : (
<StyledButton <StyledButton
color="primary" color="primary"
startIcon={<AddBoxIcon />} startIcon={
<VideoLibraryIcon
sx={{
color: "#FF0033",
width: menuIconSize,
height: menuIconSize,
}}
/>
}
onClick={() => { onClick={() => {
setIsOpen(true); setIsOpen(true);
}} }}
> >
add video Video
</StyledButton> </StyledButton>
)} )}
</> </>

View File

@ -159,7 +159,7 @@ export const BlockIconContainer = styled(Box)({
}); });
export const CommentsContainer = styled(Box)({ export const CommentsContainer = styled(Box)({
width: "70%", width: "90%",
maxWidth: "1000px", maxWidth: "1000px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",

View File

@ -160,10 +160,10 @@ export const LikeAndDislike = ({ name, identifier }: LikeAndDislikeProps) => {
sx={{ sx={{
padding: "5px", padding: "5px",
borderRadius: "7px", borderRadius: "7px",
gap: "5px", gap: "10px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
marginRight: "10px", marginRight: "20px",
height: "53px", height: "53px",
}} }}
> >

View File

@ -1,32 +1,29 @@
import React, { useEffect, useState } from "react";
import ThumbUpIcon from "@mui/icons-material/ThumbUp"; import ThumbUpIcon from "@mui/icons-material/ThumbUp";
import { import {
Box, Box,
Button,
Dialog,
DialogActions,
DialogContent, DialogContent,
DialogTitle,
FormControl,
Input,
InputAdornment, InputAdornment,
InputLabel, InputLabel,
MenuItem,
Modal, Modal,
Select,
Tooltip, Tooltip,
} from "@mui/material"; } from "@mui/material";
import qortImg from "../../../assets/img/qort.png"; import React, { useEffect, useState } from "react";
import { MultiplePublish } from "../../Publish/MultiplePublish/MultiplePublishAll.tsx";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { setNotification } from "../../../state/features/notificationsSlice.ts";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import qortImg from "../../../assets/img/qort.png";
import { import {
objectToBase64, FOR,
objectToFile, FOR_SUPER_LIKE,
} from "../../../utils/PublishFormatter.ts"; SUPER_LIKE_BASE,
} from "../../../constants/Identifiers.ts";
import { minPriceSuperlike } from "../../../constants/Misc.ts"; import { minPriceSuperlike } from "../../../constants/Misc.ts";
import { CommentInput } from "../Comments/Comments-styles.tsx"; import { setNotification } from "../../../state/features/notificationsSlice.ts";
import { RootState } from "../../../state/store.ts";
import BoundedNumericTextField from "../../../utils/BoundedNumericTextField.tsx";
import { numberToInt, truncateNumber } from "../../../utils/numberFunctions.ts";
import { objectToBase64 } from "../../../utils/PublishFormatter.ts";
import { getUserBalance } from "../../../utils/qortalRequestFunctions.ts";
import { MultiplePublish } from "../../Publish/MultiplePublish/MultiplePublishAll.tsx";
import { import {
CrowdfundActionButton, CrowdfundActionButton,
CrowdfundActionButtonRow, CrowdfundActionButtonRow,
@ -34,16 +31,7 @@ import {
NewCrowdfundTitle, NewCrowdfundTitle,
Spacer, Spacer,
} from "../../Publish/PublishVideo/PublishVideo-styles.tsx"; } from "../../Publish/PublishVideo/PublishVideo-styles.tsx";
import { utf8ToBase64 } from "../SuperLikesList/CommentEditor.tsx"; import { CommentInput } from "../Comments/Comments-styles.tsx";
import { RootState } from "../../../state/store.ts";
import {
FOR,
FOR_SUPER_LIKE,
SUPER_LIKE_BASE,
} from "../../../constants/Identifiers.ts";
import BoundedNumericTextField from "../../../utils/BoundedNumericTextField.tsx";
import { numberToInt, truncateNumber } from "../../../utils/numberFunctions.ts";
import { getUserBalance } from "../../../utils/qortalRequestFunctions.ts";
const uid = new ShortUniqueId({ length: 4 }); const uid = new ShortUniqueId({ length: 4 });

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from "react";
import { import {
Accordion, Accordion,
AccordionDetails, AccordionDetails,
@ -11,29 +11,28 @@ import {
ListItemIcon, ListItemIcon,
Popover, Popover,
Typography, Typography,
useTheme useTheme,
} from '@mui/material' } from "@mui/material";
import { Movie } from '@mui/icons-material' import { Movie } from "@mui/icons-material";
import { useSelector } from 'react-redux' import { useSelector } from "react-redux";
import { RootState } from '../../state/store' import { RootState } from "../../state/store";
import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from "react-router-dom";
import { DownloadingLight } from '../../assets/svgs/DownloadingLight' import { DownloadingLight } from "../../assets/svgs/DownloadingLight";
import { DownloadedLight } from '../../assets/svgs/DownloadedLight' import { DownloadedLight } from "../../assets/svgs/DownloadedLight";
export const DownloadTaskManager: React.FC = () => { export const DownloadTaskManager: React.FC = () => {
const { downloads } = useSelector((state: RootState) => state.global) const { downloads } = useSelector((state: RootState) => state.global);
const location = useLocation() const theme = useTheme();
const theme = useTheme() const [visible, setVisible] = useState(false);
const [visible, setVisible] = useState(false) const [hidden, setHidden] = useState(true);
const [hidden, setHidden] = useState(true) const navigate = useNavigate();
const navigate = useNavigate()
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null); const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const [openDownload, setOpenDownload] = useState<boolean>(false); const [openDownload, setOpenDownload] = useState<boolean>(false);
const hashMapVideos = useSelector(
(state: RootState) => state.video.hashMapVideos
);
const handleClick = (event?: React.MouseEvent<HTMLDivElement>) => { const handleClick = (event?: React.MouseEvent<HTMLDivElement>) => {
const target = event?.currentTarget as unknown as HTMLButtonElement | null; const target = event?.currentTarget as unknown as HTMLButtonElement | null;
setAnchorEl(target); setAnchorEl(target);
@ -49,155 +48,159 @@ export const DownloadTaskManager: React.FC = () => {
if (visible) { if (visible) {
setTimeout(() => { setTimeout(() => {
setHidden(true) setHidden(true);
setVisible(false) setVisible(false);
}, 3000) }, 3000);
} }
}, [visible]) }, [visible]);
useEffect(() => { useEffect(() => {
if (Object.keys(downloads).length === 0) return if (Object.keys(downloads).length === 0) return;
setVisible(true) setVisible(true);
setHidden(false) setHidden(false);
}, [downloads]) }, [downloads]);
if (!downloads || Object.keys(downloads).length === 0) return null;
let downloadInProgress = false;
if ( if (
!downloads || Object.keys(downloads).find(
Object.keys(downloads).length === 0 key =>
) downloads[key]?.status?.status !== "READY" &&
return null downloads[key]?.status?.status !== "DOWNLOADED"
)
) {
let downloadInProgress = false downloadInProgress = true;
if(Object.keys(downloads).find((key)=> (downloads[key]?.status?.status !== 'READY' && downloads[key]?.status?.status !== 'DOWNLOADED'))){
downloadInProgress = true
} }
return ( return (
<Box> <Box>
<Button onClick={(e: any) => { <Button
handleClick(e); sx={{ padding: "0px 0px", minWidth: "0px" }}
setOpenDownload(true); onClick={(e: any) => {
}}> handleClick(e);
{downloadInProgress ? ( setOpenDownload(true);
<DownloadingLight height='24px' width='24px' className='download-icon' /> }}
) : ( >
<DownloadedLight height='24px' width='24px' /> {downloadInProgress ? (
)} <DownloadingLight
height="24px"
</Button> width="24px"
className="download-icon"
<Popover />
id={"download-popover"} ) : (
open={openDownload} <DownloadedLight height="24px" width="24px" />
anchorEl={anchorEl} )}
onClose={handleCloseDownload} </Button>
anchorOrigin={{
vertical: "bottom", <Popover
horizontal: "left" id={"download-popover"}
open={openDownload}
anchorEl={anchorEl}
onClose={handleCloseDownload}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
sx={{ marginTop: "12px" }}
>
<List
sx={{
maxHeight: "50vh",
overflow: "auto",
width: "100%",
maxWidth: "400px",
gap: "5px",
display: "flex",
flexDirection: "column",
backgroundColor: "#555555",
}} }}
> >
<List {Object.keys(downloads).map((download: any) => {
sx={{ const downloadObj = downloads[download];
maxHeight: '50vh', const progress = downloads[download]?.status?.percentLoaded || 0;
overflow: 'auto', const status = downloads[download]?.status?.status;
width: '250px', const service = downloads[download]?.service;
gap: '5px', const id =
display: 'flex', downloadObj?.identifier + "_metadata-" + downloadObj?.name;
flexDirection: 'column', const videoTitle = hashMapVideos[id]?.title;
}}
>
{Object.keys(downloads)
.map((download: any) => {
const downloadObj = downloads[download]
const progress = downloads[download]?.status?.percentLoaded || 0
const status = downloads[download]?.status?.status
const service = downloads[download]?.service
return (
<ListItem
key={downloadObj?.identifier}
sx={{
display: 'flex',
flexDirection: 'column',
width: '100%',
justifyContent: 'center',
background: theme.palette.primary.main,
color: theme.palette.text.primary,
cursor: 'pointer',
padding: '2px',
}}
onClick={() => {
const id = downloadObj?.properties?.jsonId
if (!id) return
navigate(
`/video/${downloadObj?.properties?.user}/${id}`
)
}}
>
<Box
sx={{
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}
>
<ListItemIcon>
{service === 'VIDEO' && (
<Movie sx={{ color: theme.palette.text.primary }} />
)}
</ListItemIcon>
<Box return (
sx={{ width: '100px', marginLeft: 1, marginRight: 1 }} <ListItem
> key={downloadObj?.identifier}
<LinearProgress sx={{
variant="determinate" display: "flex",
value={progress} flexDirection: "column",
sx={{ width: "100%",
borderRadius: '5px', justifyContent: "center",
color: theme.palette.secondary.main background: theme.palette.primary.main,
}} color: theme.palette.text.primary,
/> cursor: "pointer",
</Box> padding: "2px",
<Typography }}
sx={{ onClick={() => {
fontFamily: 'Arial', const userName = downloadObj?.name;
color: theme.palette.text.primary const identifier = downloadObj?.identifier;
}}
variant="caption" if (identifier && userName)
> navigate(`/video/${userName}/${identifier}_metadata`);
{`${progress?.toFixed(0)}%`}{' '} }}
{status && status === 'REFETCHING' && '- refetching'} >
{status && status === 'DOWNLOADED' && '- building'} <Box
</Typography> sx={{
</Box> width: "100%",
<Typography display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<ListItemIcon>
{service === "VIDEO" && (
<Movie sx={{ color: theme.palette.text.primary }} />
)}
</ListItemIcon>
<Box sx={{ width: "100px", marginLeft: 1, marginRight: 1 }}>
<LinearProgress
variant="determinate"
value={progress}
sx={{ sx={{
fontSize: '10px', borderRadius: "5px",
width: '100%', color: theme.palette.secondary.main,
textAlign: 'end',
fontFamily: 'Arial',
color: theme.palette.text.primary,
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
}} }}
> />
{downloadObj?.identifier} </Box>
</Typography> <Typography
</ListItem> sx={{
) fontFamily: "Arial",
})} color: theme.palette.text.primary,
</List> }}
</Popover> variant="caption"
>
{`${progress?.toFixed(0)}%`}{" "}
{status && status === "REFETCHING" && "- refetching"}
{status && status === "DOWNLOADED" && "- building"}
</Typography>
</Box>
<Typography
sx={{
fontSize: "10px",
width: "100%",
textAlign: "start",
fontFamily: "Arial",
color: theme.palette.text.primary,
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
}}
>
{videoTitle || downloadObj?.identifier}
</Typography>
</ListItem>
);
})}
</List>
</Popover>
</Box> </Box>
);
) };
}

View File

@ -1,16 +1,18 @@
import { Box, Typography } from "@mui/material"; import { Box, Tooltip, Typography, useMediaQuery } from "@mui/material";
import React from "react"; import React from "react";
import { PopMenu } from "../PopMenu.tsx";
import ListSuperLikes from "./ListSuperLikes"; import ListSuperLikes from "./ListSuperLikes";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "../../../state/store"; import { RootState } from "../../../state/store";
import ThumbUpIcon from "@mui/icons-material/ThumbUp";
export const ListSuperLikeContainer = () => { export const ListSuperLikeContainer = () => {
const superlikelist = useSelector( const superlikelist = useSelector(
(state: RootState) => state.global.superlikelistAll (state: RootState) => state.global.superlikelistAll
); );
return ( const isScreenLarge = useMediaQuery("(min-width:1200px)");
<Box> const superlikeListComponent = (
<>
<Typography <Typography
sx={{ sx={{
fontSize: "18px", fontSize: "18px",
@ -20,6 +22,53 @@ export const ListSuperLikeContainer = () => {
Recent Super likes Recent Super likes
</Typography> </Typography>
<ListSuperLikes superlikes={superlikelist} /> <ListSuperLikes superlikes={superlikelist} />
</>
);
// @ts-ignore
return (
<Box sx={{ paddingLeft: "5px" }}>
{isScreenLarge ? (
<>{superlikeListComponent}</>
) : (
<PopMenu
showExpandIcon={false}
popoverProps={{
open: undefined,
sx: {
display: "flex",
justifyContent: "center",
alignItems: "center",
},
anchorReference: "none",
}}
MenuHeader={
<Tooltip title={"Show recent Superlikes"} placement={"left"} arrow>
<Box
sx={{
padding: "5px",
borderRadius: "7px",
outline: "1px gold solid",
height: "53px",
position: "absolute",
top: "60px",
right: "2%",
display: "flex",
alignItems: "center",
}}
>
<ThumbUpIcon
style={{
color: "gold",
}}
/>
</Box>
</Tooltip>
}
>
{superlikeListComponent}
</PopMenu>
)}
</Box> </Box>
); );
}; };

View File

@ -64,7 +64,6 @@ export function extractIdValue(metadescription) {
} }
export const Notifications = () => { export const Notifications = () => {
const dispatch = useDispatch();
const [anchorElNotification, setAnchorElNotification] = const [anchorElNotification, setAnchorElNotification] =
useState<HTMLButtonElement | null>(null); useState<HTMLButtonElement | null>(null);
const [notifications, setNotifications] = useState<any[]>([]); const [notifications, setNotifications] = useState<any[]>([]);
@ -238,7 +237,7 @@ export const Notifications = () => {
badgeContent={notificationBadgeLength} badgeContent={notificationBadgeLength}
color="primary" color="primary"
sx={{ sx={{
margin: "0px 12px", margin: "0px",
}} }}
> >
<Button <Button
@ -267,6 +266,7 @@ export const Notifications = () => {
vertical: "bottom", vertical: "bottom",
horizontal: "left", horizontal: "left",
}} }}
sx={{ marginTop: "12px" }}
> >
<Box> <Box>
<List <List

View File

@ -0,0 +1,77 @@
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { Box, Popover, PopoverProps, SxProps, Theme } from "@mui/material";
import {
forwardRef,
PropsWithChildren,
useImperativeHandle,
useState,
} from "react";
import { AvatarContainer } from "../layout/Navbar/Navbar-styles.tsx";
export interface PopMenuProps {
MenuHeader: React.ReactNode;
containerSX?: SxProps<Theme>;
showExpandIcon?: boolean;
popoverProps?: PopoverProps;
}
export type PopMenuRefType = {
closePopover: () => void;
};
export const PopMenu = forwardRef<
PopMenuRefType,
PropsWithChildren<PopMenuProps>
>(
(
{
containerSX,
popoverProps,
MenuHeader,
showExpandIcon = true,
children,
}: PropsWithChildren<PopMenuProps>,
ref
) => {
const [isOpenUserDropdown, setIsOpenUserDropdown] =
useState<boolean>(false);
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const openPopover = (event: React.MouseEvent<HTMLDivElement>) => {
const target = event.currentTarget as unknown as HTMLButtonElement | null;
setAnchorEl(target);
setIsOpenUserDropdown(true);
};
const closePopover = () => {
setAnchorEl(null);
setIsOpenUserDropdown(false);
};
useImperativeHandle(ref, () => ({
closePopover: () => closePopover(),
}));
return (
<>
<AvatarContainer sx={containerSX} onClick={openPopover}>
{MenuHeader}
{showExpandIcon && <ExpandMoreIcon sx={{ color: "#ACB6BF" }} />}
</AvatarContainer>
<Popover
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
{...popoverProps}
open={isOpenUserDropdown}
anchorEl={anchorEl}
onClose={closePopover}
>
<Box sx={{ display: "contents" }}>{children}</Box>
</Popover>
</>
);
}
);

View File

@ -7,10 +7,15 @@ import {
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Typography, Typography,
useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import React, { useCallback, useState, useEffect } from "react"; import React, { useCallback, useState, useEffect } from "react";
import ThumbUpIcon from "@mui/icons-material/ThumbUp"; import ThumbUpIcon from "@mui/icons-material/ThumbUp";
import {
fontSizeSmall,
smallScreenSizeString,
} from "../../../constants/Misc.ts";
import { CommentEditor } from "./CommentEditor"; import { CommentEditor } from "./CommentEditor";
import { import {
@ -36,9 +41,9 @@ interface CommentProps {
postId: string; postId: string;
postName: string; postName: string;
onSubmit: (obj?: any, isEdit?: boolean) => void; onSubmit: (obj?: any, isEdit?: boolean) => void;
amount?: null | number amount?: null | number;
isSuperLike?: boolean isSuperLike?: boolean;
hasHash?: boolean hasHash?: boolean;
} }
export const Comment = ({ export const Comment = ({
comment, comment,
@ -47,7 +52,7 @@ export const Comment = ({
onSubmit, onSubmit,
amount, amount,
isSuperLike, isSuperLike,
hasHash hasHash,
}: CommentProps) => { }: CommentProps) => {
const [isReplying, setIsReplying] = useState<boolean>(false); const [isReplying, setIsReplying] = useState<boolean>(false);
const [isEditing, setIsEditing] = useState<boolean>(false); const [isEditing, setIsEditing] = useState<boolean>(false);
@ -199,7 +204,7 @@ export const CommentCard = ({
children, children,
setCurrentEdit, setCurrentEdit,
isReply, isReply,
amount amount,
}: any) => { }: any) => {
const [avatarUrl, setAvatarUrl] = React.useState<string>(""); const [avatarUrl, setAvatarUrl] = React.useState<string>("");
const { user } = useSelector((state: RootState) => state.auth); const { user } = useSelector((state: RootState) => state.auth);
@ -225,6 +230,13 @@ export const CommentCard = ({
getAvatar(name); getAvatar(name);
}, [name]); }, [name]);
const isScreenSmall = !useMediaQuery(`(min-width:${smallScreenSizeString})`);
const superLikeHeaderSX = {
display: "flex",
flexDirection: isScreenSmall ? "column" : "row",
alignItems: "center",
};
return ( return (
<CardContentContainerComment> <CardContentContainerComment>
<StyledCardHeaderComment <StyledCardHeaderComment
@ -234,40 +246,48 @@ export const CommentCard = ({
}, },
}} }}
> >
<Box> <Box sx={superLikeHeaderSX}>
<Avatar <Box
src={avatarUrl} sx={{
alt={`${name}'s avatar`} display: "flex",
sx={{ width: "35px", height: "35px" }} gap: "10px",
/> alignItems: "center",
</Box> }}
<StyledCardColComment> >
<Box sx={{ <Avatar
display: 'flex', src={avatarUrl}
gap: '10px', alt={`${name}'s avatar`}
alignItems: 'center' sx={{ width: "35px", height: "35px" }}
}}> />
<AuthorTextComment>{name}</AuthorTextComment> <AuthorTextComment>{name}</AuthorTextComment>
{!isReply && (
<ThumbUpIcon
style={{
color: "gold",
cursor: "pointer",
}}
/>
)}
{amount && (
<Typography sx={{
fontSize: '20px',
color: 'gold'
}}>
{parseFloat(amount)?.toFixed(2)} QORT
</Typography>
)}
</Box> </Box>
<StyledCardColComment
</StyledCardColComment> sx={{
marginTop: isScreenSmall ? "10px" : "0px",
marginLeft: isScreenSmall ? "0px" : "10px",
}}
>
{!isReply && (
<ThumbUpIcon
style={{
color: "gold",
cursor: "pointer",
marginRight: "10px",
}}
/>
)}
{amount && (
<Typography
sx={{
fontSize: fontSizeSmall,
color: "gold",
}}
>
{parseFloat(amount)?.toFixed(2)} QORT
</Typography>
)}
</StyledCardColComment>
</Box>
</StyledCardHeaderComment> </StyledCardHeaderComment>
<StyledCardContentComment> <StyledCardContentComment>
<StyledCardComment>{message}</StyledCardComment> <StyledCardComment>{message}</StyledCardComment>

View File

@ -63,9 +63,9 @@ export const StyledCardCol = styled(Box)({
export const StyledCardColComment = styled(Box)({ export const StyledCardColComment = styled(Box)({
display: "flex", display: "flex",
overflow: "hidden", overflow: "hidden",
flexDirection: "column", flexDirection: "row",
gap: "2px", gap: "2px",
alignItems: "flex-start", alignItems: "center",
width: "100%", width: "100%",
}); });
@ -159,7 +159,7 @@ export const BlockIconContainer = styled(Box)({
}); });
export const CommentsContainer = styled(Box)({ export const CommentsContainer = styled(Box)({
width: "70%", width: "98%",
maxWidth: "1000px", maxWidth: "1000px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",

View File

@ -42,7 +42,6 @@ export const MobileControls = () => {
<IconButton <IconButton
sx={{ sx={{
color: "rgba(255, 255, 255, 0.7)", color: "rgba(255, 255, 255, 0.7)",
marginLeft: "15px",
}} }}
onClick={reloadVideo} onClick={reloadVideo}
> >
@ -55,11 +54,26 @@ export const MobileControls = () => {
max={videoRef.current?.duration || 100} max={videoRef.current?.duration || 100}
sx={{ flexGrow: 1, mx: 2 }} sx={{ flexGrow: 1, mx: 2 }}
/> />
<Typography
sx={{
color: "rgba(255, 255, 255, 0.7)",
fontSize: "14px",
userSelect: "none",
minWidth: "30px",
}}
onClick={() => increaseSpeed()}
>
{playbackRate}x
</Typography>
<Fullscreen onClick={toggleFullscreen} />
<IconButton <IconButton
edge="end" edge="end"
color="inherit" color="inherit"
aria-label="menu" aria-label="menu"
onClick={handleMenuOpen} onClick={handleMenuOpen}
sx={{ minWidth: "30px" }}
> >
<MoreIcon /> <MoreIcon />
</IconButton> </IconButton>
@ -85,22 +99,10 @@ export const MobileControls = () => {
step={0.01} step={0.01}
/> />
</MenuItem> </MenuItem>
<MenuItem onClick={() => increaseSpeed()}>
<Typography
sx={{
color: "rgba(255, 255, 255, 0.7)",
fontSize: "14px",
}}
>
Speed: {playbackRate}x
</Typography>
</MenuItem>
<MenuItem onClick={togglePictureInPicture}> <MenuItem onClick={togglePictureInPicture}>
<PictureInPicture /> <PictureInPicture />
</MenuItem> </MenuItem>
<MenuItem onClick={toggleFullscreen}>
<Fullscreen />
</MenuItem>
</Menu> </Menu>
</> </>
); );

View File

@ -3,11 +3,13 @@ import { useSignalEffect, useSignals } from "@preact/signals-react/runtime";
import { useEffect } from "react"; import { useEffect } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { Key } from "ts-key-enum"; import { Key } from "ts-key-enum";
import { useIsMobile } from "../../../../hooks/useIsMobile.ts"; import { useIsMobile } from "../../../../hooks/useIsMobile.ts";
import { setVideoPlaying } from "../../../../state/features/globalSlice.ts"; import { setVideoPlaying } from "../../../../state/features/globalSlice.ts";
import { import {
setIsMuted,
setMutedVolumeSetting,
setReduxPlaybackRate, setReduxPlaybackRate,
setStretchVideoSetting, setStretchVideoSetting,
setVolumeSetting, setVolumeSetting,
@ -36,13 +38,12 @@ export const useVideoControlsState = (
progress, progress,
videoObjectFit, videoObjectFit,
canPlay, canPlay,
isMobileView,
} = videoPlayerState; } = videoPlayerState;
const { identifier, autoPlay } = props; const { identifier, autoPlay } = props;
const isMobile = useIsMobile();
const showControlsFullScreen = useSignal(true); const showControlsFullScreen = useSignal(true);
const persistSelector = store.getState().persist; const dispatch = useDispatch();
const persistSelector = useSelector((root: RootState) => root.persist);
const videoPlaying = useSelector( const videoPlaying = useSelector(
(state: RootState) => state.global.videoPlaying (state: RootState) => state.global.videoPlaying
@ -58,7 +59,6 @@ export const useVideoControlsState = (
videoRef.current.playbackRate = newSpeed; videoRef.current.playbackRate = newSpeed;
playbackRate.value = newSpeed; playbackRate.value = newSpeed;
store.dispatch(setReduxPlaybackRate(newSpeed));
} }
}; };
@ -156,10 +156,9 @@ export const useVideoControlsState = (
const onVolumeChange = (_: any, value: number | number[]) => { const onVolumeChange = (_: any, value: number | number[]) => {
if (!videoRef.current) return; if (!videoRef.current) return;
const newVolume = value as number; const newVolume = value as number;
videoRef.current.volume = newVolume;
volume.value = newVolume;
isMuted.value = false; isMuted.value = false;
store.dispatch(setVolumeSetting(newVolume)); mutedVolume.value = newVolume;
volume.value = newVolume;
}; };
useEffect(() => { useEffect(() => {
@ -170,12 +169,10 @@ export const useVideoControlsState = (
isMuted.value = true; isMuted.value = true;
mutedVolume.value = volume.value; mutedVolume.value = volume.value;
volume.value = 0; volume.value = 0;
if (videoRef.current) videoRef.current.volume = 0;
}; };
const unMute = () => { const unMute = () => {
isMuted.value = false; isMuted.value = false;
volume.value = mutedVolume.value; volume.value = mutedVolume.value;
if (videoRef.current) videoRef.current.volume = mutedVolume.value;
}; };
const toggleMute = () => { const toggleMute = () => {
@ -191,12 +188,10 @@ export const useVideoControlsState = (
newVolume = Math.max(newVolume, minVolume); newVolume = Math.max(newVolume, minVolume);
newVolume = Math.min(newVolume, maxVolume); newVolume = Math.min(newVolume, maxVolume);
newVolume = +newVolume.toFixed(2);
isMuted.value = false; isMuted.value = false;
mutedVolume.value = newVolume; mutedVolume.value = newVolume;
videoRef.current.volume = newVolume;
volume.value = newVolume; volume.value = newVolume;
store.dispatch(setVolumeSetting(newVolume));
} }
}; };
const setProgressRelative = (secondsChange: number) => { const setProgressRelative = (secondsChange: number) => {
@ -228,7 +223,6 @@ export const useVideoControlsState = (
persistSelector.stretchVideoSetting === "contain" ? "fill" : "contain"; persistSelector.stretchVideoSetting === "contain" ? "fill" : "contain";
videoObjectFit.value = newStretchVideoSetting; videoObjectFit.value = newStretchVideoSetting;
store.dispatch(setStretchVideoSetting(newStretchVideoSetting));
}; };
const keyboardShortcutsDown = (e: React.KeyboardEvent<HTMLDivElement>) => { const keyboardShortcutsDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
e.preventDefault(); e.preventDefault();
@ -350,14 +344,13 @@ export const useVideoControlsState = (
videoRef.current.play().then(() => { videoRef.current.play().then(() => {
playing.value = true; playing.value = true;
startPlay.value = true; startPlay.value = true;
store.dispatch(setVideoPlaying(null)); dispatch(setVideoPlaying(null));
}); });
} }
}, [videoPlaying, identifier, src]); }, [videoPlaying, identifier, src]);
useSignalEffect(() => { useSignalEffect(() => {
console.log("canPlay is: ", canPlay.value); // makes the function execute when canPlay changes console.log("canPlay is: ", canPlay.value); // makes the function execute when canPlay changes
if (isMobile) isMobileView.value = true;
}); });
return { return {

View File

@ -7,12 +7,14 @@ import {
VolumeOff, VolumeOff,
VolumeUp, VolumeUp,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { IconButton, Slider, Typography } from "@mui/material"; import { IconButton, Slider, Typography, useMediaQuery } from "@mui/material";
import { smallScreenSizeString } from "../../../../constants/Misc.ts";
import { formatTime } from "../../../../utils/numberFunctions.ts"; import { formatTime } from "../../../../utils/numberFunctions.ts";
import { ControlsContainer } from "../VideoPlayer-styles.ts"; import { ControlsContainer } from "../VideoPlayer-styles.ts";
import { MobileControls } from "./MobileControls.tsx"; import { MobileControls } from "./MobileControls.tsx";
import { useVideoContext } from "./VideoContext.ts"; import { useVideoContext } from "./VideoContext.ts";
import { useSignalEffect } from "@preact/signals-react";
export const VideoControls = () => { export const VideoControls = () => {
const { const {
@ -28,7 +30,6 @@ export const VideoControls = () => {
from, from,
videoRef, videoRef,
canPlay, canPlay,
isMobileView,
isMuted, isMuted,
playbackRate, playbackRate,
playing, playing,
@ -37,8 +38,8 @@ export const VideoControls = () => {
showControlsFullScreen, showControlsFullScreen,
} = useVideoContext(); } = useVideoContext();
const showMobileControls = const isScreenSmall = !useMediaQuery(`(min-width:580px)`);
isMobileView.value && canPlay.value && showControlsFullScreen.value; const showMobileControls = isScreenSmall && canPlay.value;
return ( return (
<ControlsContainer <ControlsContainer

View File

@ -1,4 +1,8 @@
import { useSignal, useSignals } from "@preact/signals-react/runtime"; import {
useSignal,
useSignalEffect,
useSignals,
} from "@preact/signals-react/runtime";
import React, { import React, {
useContext, useContext,
useEffect, useEffect,
@ -9,7 +13,14 @@ import React, {
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { setVideoPlaying } from "../../../state/features/globalSlice.ts"; import { setVideoPlaying } from "../../../state/features/globalSlice.ts";
import { StretchVideoType } from "../../../state/features/persistSlice.ts"; import {
setIsMuted,
setMutedVolumeSetting,
setReduxPlaybackRate,
setStretchVideoSetting,
setVolumeSetting,
StretchVideoType,
} from "../../../state/features/persistSlice.ts";
import { RootState } from "../../../state/store.ts"; import { RootState } from "../../../state/store.ts";
import { MyContext } from "../../../wrappers/DownloadWrapper.tsx"; import { MyContext } from "../../../wrappers/DownloadWrapper.tsx";
import { VideoPlayerProps } from "./VideoPlayer.tsx"; import { VideoPlayerProps } from "./VideoPlayer.tsx";
@ -19,20 +30,40 @@ export const useVideoPlayerState = (props: VideoPlayerProps, ref: any) => {
const persistSelector = useSelector((state: RootState) => state.persist); const persistSelector = useSelector((state: RootState) => state.persist);
const playing = useSignal(false); const playing = useSignal(false);
const isMuted = useSignal(false);
const progress = useSignal(0); const progress = useSignal(0);
const isLoading = useSignal(false); const isLoading = useSignal(false);
const canPlay = useSignal(false); const canPlay = useSignal(false);
const startPlay = useSignal(false); const startPlay = useSignal(false);
const isMobileView = useSignal(false);
const isMuted = useSignal(persistSelector.isMuted);
const volume = useSignal(persistSelector.volume); const volume = useSignal(persistSelector.volume);
const mutedVolume = useSignal(persistSelector.volume); const mutedVolume = useSignal(persistSelector.mutedVolume);
const playbackRate = useSignal(persistSelector.playbackRate); const playbackRate = useSignal(persistSelector.playbackRate);
const anchorEl = useSignal(null);
const videoObjectFit = useSignal<StretchVideoType>( const videoObjectFit = useSignal<StretchVideoType>(
persistSelector.stretchVideoSetting persistSelector.stretchVideoSetting
); );
useSignalEffect(() => {
dispatch(setIsMuted(isMuted.value));
});
useSignalEffect(() => {
dispatch(setVolumeSetting(volume.value));
if (videoRef.current) videoRef.current.volume = volume.value;
});
useSignalEffect(() => {
dispatch(setMutedVolumeSetting(mutedVolume.value));
});
useSignalEffect(() => {
if (videoRef.current) videoRef.current.playbackRate = playbackRate.value;
dispatch(setReduxPlaybackRate(playbackRate.value));
});
useSignalEffect(() => {
dispatch(setStretchVideoSetting(videoObjectFit.value));
});
const anchorEl = useSignal(null);
const { const {
name, name,
identifier, identifier,
@ -279,7 +310,6 @@ export const useVideoPlayerState = (props: VideoPlayerProps, ref: any) => {
isLoading, isLoading,
canPlay, canPlay,
startPlay, startPlay,
isMobileView,
volume, volume,
mutedVolume, mutedVolume,
playbackRate, playbackRate,

View File

@ -55,7 +55,6 @@ export const VideoPlayer = forwardRef<videoRefType, VideoPlayerProps>(
startPlay, startPlay,
videoObjectFit, videoObjectFit,
showControlsFullScreen, showControlsFullScreen,
duration,
} = contextData; } = contextData;
return ( return (

View File

@ -0,0 +1,73 @@
import { useDispatch } from "react-redux";
import { headerIconSize, menuIconSize } from "../../../../constants/Misc.ts";
import { setEditPlaylist } from "../../../../state/features/videoSlice.ts";
import { StyledButton } from "../../../Publish/PublishVideo/PublishVideo-styles.tsx";
import { PublishVideo } from "../../../Publish/PublishVideo/PublishVideo.tsx";
import {
AvatarContainer,
DropdownContainer,
DropdownText,
NavbarName,
} from "../Navbar-styles.tsx";
import AddBoxIcon from "@mui/icons-material/AddBox";
import { PopMenu, PopMenuRefType } from "../../../common/PopMenu.tsx";
import { useRef } from "react";
import { useMediaQuery } from "@mui/material";
export interface PublishButtonsProps {
isDisplayed: boolean;
}
import PlaylistAddIcon from "@mui/icons-material/PlaylistAdd";
export const PublishMenu = ({ isDisplayed }: PublishButtonsProps) => {
const dispatch = useDispatch();
const popMenuRef = useRef<PopMenuRefType>(null);
const isScreenSmall = !useMediaQuery(`(min-width:600px)`);
return (
<>
{isDisplayed && (
<PopMenu
MenuHeader={
<>
{!isScreenSmall && (
<NavbarName sx={{ marginRight: "5px" }}>Publish</NavbarName>
)}
<AddBoxIcon
sx={{
color: "DarkGreen",
width: headerIconSize,
height: headerIconSize,
}}
/>
</>
}
ref={popMenuRef}
>
<DropdownContainer>
<PublishVideo afterClose={popMenuRef?.current?.closePopover} />
</DropdownContainer>
<DropdownContainer onClick={popMenuRef?.current?.closePopover}>
<StyledButton
color="primary"
startIcon={
<PlaylistAddIcon
sx={{
color: "#00BFFF",
width: menuIconSize,
height: menuIconSize,
}}
/>
}
onClick={() => {
dispatch(setEditPlaylist({ mode: "new" }));
}}
>
Playlist
</StyledButton>
</DropdownContainer>
</PopMenu>
)}
</>
);
};

View File

@ -0,0 +1,40 @@
import { useNavigate } from "react-router-dom";
import Logo from "../../../../assets/img/logo.webp";
import {
addFilteredVideos,
setFilterValue,
setIsFiltering,
} from "../../../../state/features/videoSlice.ts";
import { LogoContainer, ThemeSelectRow } from "../Navbar-styles.tsx";
import { useDispatch } from "react-redux";
import { useMediaQuery } from "@mui/material";
export const QtubeLogo = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const isScreenSmall = !useMediaQuery(`(min-width:600px)`);
return (
<ThemeSelectRow>
<LogoContainer
onClick={() => {
navigate("/");
dispatch(setIsFiltering(false));
dispatch(setFilterValue(""));
dispatch(addFilteredVideos([]));
}}
>
<img
src={Logo}
style={{
width: isScreenSmall ? "50px" : "auto",
height: "45px",
padding: "2px",
marginTop: "5px",
}}
/>
</LogoContainer>
</ThemeSelectRow>
);
};

View File

@ -0,0 +1,124 @@
import { Popover, useMediaQuery, useTheme } from "@mui/material";
import { AccountCircleSVG } from "../../../../assets/svgs/AccountCircleSVG.tsx";
import { headerIconSize, menuIconSize } from "../../../../constants/Misc.ts";
import { BlockedNamesModal } from "../../../common/BlockedNamesModal/BlockedNamesModal.tsx";
import {
AvatarContainer,
DropdownContainer,
DropdownText,
NavbarName,
} from "../Navbar-styles.tsx";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { useRef, useState } from "react";
import PersonOffIcon from "@mui/icons-material/PersonOff";
import { useNavigate } from "react-router-dom";
import { PopMenu, PopMenuRefType } from "../../../common/PopMenu.tsx";
export interface NavBarMenuProps {
isShowMenu: boolean;
userAvatar: string;
userName: string | null;
}
export const UserMenu = ({
isShowMenu,
userAvatar,
userName,
}: NavBarMenuProps) => {
const isScreenSmall = !useMediaQuery(`(min-width:600px)`);
const theme = useTheme();
const [isOpenBlockedNamesModal, setIsOpenBlockedNamesModal] =
useState<boolean>(false);
const popMenuRef = useRef<PopMenuRefType>(null);
const navigate = useNavigate();
const handleMyChannelLink = () => {
navigate(`/channel/${userName}`);
};
const onCloseBlockedNames = () => {
setIsOpenBlockedNamesModal(false);
};
return (
<>
{isShowMenu && (
<>
<PopMenu
ref={popMenuRef}
MenuHeader={
<AvatarContainer>
{!isScreenSmall && <NavbarName>{userName}</NavbarName>}
{!userAvatar ? (
<AccountCircleSVG
color={theme.palette.text.primary}
width={headerIconSize}
height={headerIconSize}
/>
) : (
<img
src={userAvatar}
alt="User Avatar"
width={headerIconSize}
height={headerIconSize}
style={{
borderRadius: "50%",
}}
/>
)}
</AvatarContainer>
}
>
<DropdownContainer
onClick={() => {
handleMyChannelLink();
popMenuRef.current.closePopover();
}}
>
{!userAvatar ? (
<AccountCircleSVG
color={theme.palette.text.primary}
width={menuIconSize}
height={menuIconSize}
/>
) : (
<img
src={userAvatar}
alt="User Avatar"
width={menuIconSize}
height={menuIconSize}
style={{
borderRadius: "50%",
}}
/>
)}
<DropdownText>{userName}</DropdownText>
</DropdownContainer>
<DropdownContainer
onClick={() => {
setIsOpenBlockedNamesModal(true);
popMenuRef.current.closePopover();
}}
>
<PersonOffIcon
sx={{
color: "#e35050",
width: menuIconSize,
height: menuIconSize,
}}
/>
<DropdownText>Blocked Names</DropdownText>
</DropdownContainer>
</PopMenu>
{isOpenBlockedNamesModal && (
<BlockedNamesModal
open={isOpenBlockedNamesModal}
onClose={onCloseBlockedNames}
/>
)}
</>
)}
</>
);
};

View File

@ -2,20 +2,18 @@ import { AppBar, Button, Typography, Box } from "@mui/material";
import { styled } from "@mui/system"; import { styled } from "@mui/system";
import { LightModeSVG } from "../../../assets/svgs/LightModeSVG"; import { LightModeSVG } from "../../../assets/svgs/LightModeSVG";
import { DarkModeSVG } from "../../../assets/svgs/DarkModeSVG"; import { DarkModeSVG } from "../../../assets/svgs/DarkModeSVG";
import { fontSizeSmall } from "../../../constants/Misc.ts";
export const CustomAppBar = styled(AppBar)(({ theme }) => ({ export const CustomAppBar = styled(AppBar)(({ theme }) => ({
display: "flex", display: "flex",
flexDirection: "row", flexDirection: "row",
justifyContent: "space-between", justifyContent: "start",
alignItems: "center", alignItems: "center",
width: "100%", width: "100%",
padding: "5px 16px", padding: "5px 16px 5px 5px",
backgroundImage: "none", backgroundImage: "none",
borderBottom: `1px solid ${theme.palette.primary.light}`, borderBottom: `1px solid ${theme.palette.primary.light}`,
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
[theme.breakpoints.only("xs")]: {
gap: "15px",
},
height: "50px", height: "50px",
})); }));
export const LogoContainer = styled("div")({ export const LogoContainer = styled("div")({
@ -85,16 +83,15 @@ export const DropdownText = styled(Typography)(({ theme }) => ({
export const NavbarName = styled(Typography)(({ theme }) => ({ export const NavbarName = styled(Typography)(({ theme }) => ({
fontFamily: "Raleway", fontFamily: "Raleway",
fontSize: "18px", fontSize: fontSizeSmall,
color: theme.palette.text.primary, color: theme.palette.text.primary,
margin: "0 10px", marginRight: "10px",
})); }));
export const ThemeSelectRow = styled(Box)({ export const ThemeSelectRow = styled(Box)({
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: "5px", gap: "10px",
flexBasis: 0,
height: "100%", height: "100%",
}); });

View File

@ -1,43 +1,12 @@
import React, { useState, useRef } from "react"; import { Box, useMediaQuery } from "@mui/material";
import { Box, Button, Input, Popover, useTheme } from "@mui/material"; import React from "react";
import ExitToAppIcon from "@mui/icons-material/ExitToApp";
import { BlockedNamesModal } from "../../common/BlockedNamesModal/BlockedNamesModal";
import AddBoxIcon from "@mui/icons-material/AddBox";
import {
AvatarContainer,
CustomAppBar,
DropdownContainer,
DropdownText,
AuthenticateButton,
NavbarName,
LightModeIcon,
DarkModeIcon,
ThemeSelectRow,
LogoContainer,
} from "./Navbar-styles";
import { AccountCircleSVG } from "../../../assets/svgs/AccountCircleSVG";
import BackspaceIcon from "@mui/icons-material/Backspace";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import PersonOffIcon from "@mui/icons-material/PersonOff";
import { useNavigate } from "react-router-dom";
import SearchIcon from "@mui/icons-material/Search";
import { DownloadTaskManager } from "../../common/DownloadTaskManager"; import { DownloadTaskManager } from "../../common/DownloadTaskManager";
import Logo from "../../../assets/img/logo.webp";
import { useDispatch, useSelector } from "react-redux";
import {
addFilteredVideos,
setEditPlaylist,
setFilterValue,
setIsFiltering,
} from "../../../state/features/videoSlice";
import { RootState } from "../../../state/store";
import { useWindowSize } from "../../../hooks/useWindowSize";
import { PublishVideo } from "../../Publish/PublishVideo/PublishVideo.tsx";
import { StyledButton } from "../../Publish/PublishVideo/PublishVideo-styles.tsx";
import { Notifications } from "../../common/Notifications/Notifications"; import { Notifications } from "../../common/Notifications/Notifications";
import { PublishMenu } from "./Components/PublishMenu.tsx";
import { QtubeLogo } from "./Components/QtubeLogo.tsx";
import { UserMenu } from "./Components/UserMenu.tsx";
import { CustomAppBar } from "./Navbar-styles";
interface Props { interface Props {
isAuthenticated: boolean; isAuthenticated: boolean;
userName: string | null; userName: string | null;
@ -46,296 +15,39 @@ interface Props {
setTheme: (val: string) => void; setTheme: (val: string) => void;
} }
const NavBar: React.FC<Props> = ({ const NavBar: React.FC<Props> = ({ isAuthenticated, userName, userAvatar }) => {
isAuthenticated, const isScreenSmall = !useMediaQuery(`(min-width:600px)`);
userName, const isSecure = isAuthenticated && !!userName;
userAvatar, const gapSize = 10;
authenticate,
setTheme,
}) => {
const windowSize = useWindowSize();
const searchValRef = useRef("");
const inputRef = useRef<HTMLInputElement>(null);
const theme = useTheme();
const dispatch = useDispatch();
const navigate = useNavigate();
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(
null
);
const [openUserDropdown, setOpenUserDropdown] = useState<boolean>(false);
const [isOpenBlockedNamesModal, setIsOpenBlockedNamesModal] =
useState<boolean>(false);
const [anchorElNotification, setAnchorElNotification] =
React.useState<HTMLButtonElement | null>(null);
const filterValue = useSelector(
(state: RootState) => state.video.filterValue
);
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
const target = event.currentTarget as unknown as HTMLButtonElement | null;
setAnchorEl(target);
};
const openNotificationPopover = (event: any) => {
const target = event.currentTarget as unknown as HTMLButtonElement | null;
setAnchorElNotification(target);
};
const closeNotificationPopover = () => {
setAnchorElNotification(null);
};
const openPopover = Boolean(anchorElNotification);
const idNotification = openPopover
? "simple-popover-notification"
: undefined;
const handleCloseUserDropdown = () => {
setAnchorEl(null);
setOpenUserDropdown(false);
};
const handleMyChannelLink = () => {
navigate(`/channel/${userName}`);
};
const onCloseBlockedNames = () => {
setIsOpenBlockedNamesModal(false);
};
return ( return (
<CustomAppBar position="sticky" elevation={2}> <CustomAppBar position="sticky" elevation={2}>
<ThemeSelectRow>
<LogoContainer
onClick={() => {
navigate("/");
dispatch(setIsFiltering(false));
dispatch(setFilterValue(""));
dispatch(addFilteredVideos([]));
searchValRef.current = "";
if (!inputRef.current) return;
inputRef.current.value = "";
}}
>
<img
src={Logo}
style={{
width: "auto",
height: "45px",
padding: "2px",
marginTop: "5px",
}}
/>
</LogoContainer>
</ThemeSelectRow>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
alignItems: "center", width: "100%",
gap: "10px", justifyContent: "space-between",
gap: `${gapSize}px`,
}} }}
> >
<Popover <QtubeLogo />
id={idNotification} <Box
open={openPopover} sx={{
anchorEl={anchorElNotification} display: "flex",
onClose={closeNotificationPopover} gap: `${isScreenSmall ? gapSize : gapSize * 2}px`,
anchorOrigin={{ alignItems: "center",
vertical: "bottom",
horizontal: "left",
}} }}
> >
<Box {isSecure && <Notifications />}
sx={{
display: "flex",
alignItems: "center",
gap: 1,
padding: "5px",
}}
>
<Input
id="standard-adornment-name"
inputRef={inputRef}
onChange={e => {
searchValRef.current = e.target.value;
}}
onKeyDown={event => {
if (event.key === "Enter" || event.keyCode === 13) {
if (!searchValRef.current) {
dispatch(setIsFiltering(false));
dispatch(setFilterValue(""));
dispatch(addFilteredVideos([]));
searchValRef.current = "";
if (!inputRef.current) return;
inputRef.current.value = "";
return;
}
navigate("/");
dispatch(setIsFiltering(true));
dispatch(addFilteredVideos([]));
dispatch(setFilterValue(searchValRef.current));
}
}}
placeholder="Search"
sx={{
"&&:before": {
borderBottom: "none",
},
"&&:after": {
borderBottom: "none",
},
"&&:hover:before": {
borderBottom: "none",
},
"&&.Mui-focused:before": {
borderBottom: "none",
},
"&&.Mui-focused": {
outline: "none",
},
fontSize: "18px",
}}
/>
<SearchIcon <DownloadTaskManager />
sx={{ <UserMenu
cursor: "pointer", isShowMenu={isSecure}
}} userAvatar={userAvatar}
onClick={() => { userName={userName}
if (!searchValRef.current) {
dispatch(setIsFiltering(false));
dispatch(setFilterValue(""));
dispatch(addFilteredVideos([]));
searchValRef.current = "";
if (!inputRef.current) return;
inputRef.current.value = "";
return;
}
navigate("/");
dispatch(setIsFiltering(true));
dispatch(addFilteredVideos([]));
dispatch(setFilterValue(searchValRef.current));
}}
/>
<BackspaceIcon
sx={{
cursor: "pointer",
}}
onClick={() => {
dispatch(setIsFiltering(false));
dispatch(setFilterValue(""));
dispatch(addFilteredVideos([]));
searchValRef.current = "";
if (!inputRef.current) return;
inputRef.current.value = "";
}}
/>
</Box>
</Popover>
{isAuthenticated && userName && <Notifications />}
<DownloadTaskManager />
{isAuthenticated && userName && (
<>
<AvatarContainer
onClick={(e: any) => {
handleClick(e);
setOpenUserDropdown(true);
}}
>
<NavbarName>{userName}</NavbarName>
{!userAvatar ? (
<AccountCircleSVG
color={theme.palette.text.primary}
width="40"
height="40"
/>
) : (
<img
src={userAvatar}
alt="User Avatar"
width="40"
height="40"
style={{
borderRadius: "50%",
}}
/>
)}
<ExpandMoreIcon id="expand-icon" sx={{ color: "#ACB6BF" }} />
</AvatarContainer>
</>
)}
<AvatarContainer>
{isAuthenticated && userName && (
<>
<PublishVideo />
<StyledButton
color="primary"
startIcon={<AddBoxIcon />}
onClick={() => {
dispatch(setEditPlaylist({ mode: "new" }));
}}
>
create playlist
</StyledButton>
</>
)}
</AvatarContainer>
<Popover
id={"user-popover"}
open={openUserDropdown}
anchorEl={anchorEl}
onClose={handleCloseUserDropdown}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
>
<DropdownContainer
onClick={() => {
handleMyChannelLink();
handleCloseUserDropdown();
}}
>
{!userAvatar ? (
<AccountCircleSVG
color={theme.palette.text.primary}
width="28"
height="28"
/>
) : (
<img
src={userAvatar}
alt="User Avatar"
width="28"
height="28"
style={{
borderRadius: "50%",
}}
/>
)}
<DropdownText>My Channel</DropdownText>
</DropdownContainer>
<DropdownContainer
onClick={() => {
setIsOpenBlockedNamesModal(true);
handleCloseUserDropdown();
}}
>
<PersonOffIcon
sx={{
color: "#e35050",
}}
/>
<DropdownText>Blocked Names</DropdownText>
</DropdownContainer>
</Popover>
{isOpenBlockedNamesModal && (
<BlockedNamesModal
open={isOpenBlockedNamesModal}
onClose={onCloseBlockedNames}
/> />
)} <PublishMenu isDisplayed={isSecure} />
</Box>
</Box> </Box>
</CustomAppBar> </CustomAppBar>
); );

View File

@ -14,3 +14,12 @@ export const fontSizeExLarge = "150%";
export const maxCommentLength = 10_000; export const maxCommentLength = 10_000;
export const minFileSize = 1_000; export const minFileSize = 1_000;
export const minDuration = 5; export const minDuration = 5;
const newUIWidthDiff = 120;
const smallScreenSize = 700 - newUIWidthDiff;
const largeScreenSize = 1400 - newUIWidthDiff;
export const smallScreenSizeString = `${smallScreenSize}px`;
export const largeScreenSizeString = `${largeScreenSize}px`;
export const headerIconSize = "40px";
export const menuIconSize = "28px";

View File

@ -1,10 +1,16 @@
import { Box, Typography } from "@mui/material"; import { Box, Grid, Typography, useMediaQuery } from "@mui/material";
import React from "react"; import React from "react";
import { CommentSection } from "../../../components/common/Comments/CommentSection.tsx"; import { CommentSection } from "../../../components/common/Comments/CommentSection.tsx";
import { SuperLikesSection } from "../../../components/common/SuperLikesList/SuperLikesSection.tsx"; import { SuperLikesSection } from "../../../components/common/SuperLikesList/SuperLikesSection.tsx";
import { DisplayHtml } from "../../../components/common/TextEditor/DisplayHtml.tsx"; import { DisplayHtml } from "../../../components/common/TextEditor/DisplayHtml.tsx";
import { VideoPlayer } from "../../../components/common/VideoPlayer/VideoPlayer.tsx"; import { VideoPlayer } from "../../../components/common/VideoPlayer/VideoPlayer.tsx";
import { Playlists } from "../../../components/Playlists/Playlists.tsx"; import { Playlists } from "../../../components/Playlists/Playlists.tsx";
import {
fontSizeSmall,
minFileSize,
smallScreenSizeString,
} from "../../../constants/Misc.ts";
import { formatBytes } from "../../../utils/numberFunctions.ts";
import { formatDate } from "../../../utils/time.ts"; import { formatDate } from "../../../utils/time.ts";
import { VideoActionsBar } from "../VideoContent/VideoActionsBar.tsx"; import { VideoActionsBar } from "../VideoContent/VideoActionsBar.tsx";
import { usePlaylistContentState } from "./PlaylistContent-State.ts"; import { usePlaylistContentState } from "./PlaylistContent-State.ts";
@ -40,6 +46,8 @@ export const PlaylistContent = () => {
loadingSuperLikes, loadingSuperLikes,
} = usePlaylistContentState(); } = usePlaylistContentState();
const isScreenSmall = !useMediaQuery(`(min-width:700px)`);
return videoData && videoData?.videos?.length === 0 ? ( return videoData && videoData?.videos?.length === 0 ? (
<Box <Box
sx={{ sx={{
@ -55,197 +63,202 @@ export const PlaylistContent = () => {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
flexDirection: "column", flexDirection: "column",
padding: "0px 10px", padding: "0px",
marginLeft: "5%", marginLeft: "2%",
}} }}
onClick={focusVideo} onClick={focusVideo}
> >
<VideoPlayerContainer <VideoPlayerContainer
sx={{ sx={{
marginBottom: "30px", width: "100%",
display: "grid",
gridTemplateColumns: isScreenSmall ? "1fr" : "60vw auto",
gap: "20px",
}} }}
> >
<> {videoReference && (
<Box <Box
sx={{ sx={{
display: "grid", aspectRatio: "16/9",
gridTemplateColumns: "55vw 35vw",
width: "100vw",
gap: "3vw",
}} }}
> >
{videoReference && ( <VideoPlayer
<Box name={videoReference?.name}
sx={{ service={videoReference?.service}
aspectRatio: "16/9", identifier={videoReference?.identifier}
}} user={channelName}
> jsonId={id}
<VideoPlayer poster={videoCover || ""}
name={videoReference?.name} nextVideo={nextVideo}
service={videoReference?.service} onEnd={onEndVideo}
identifier={videoReference?.identifier} autoPlay={doAutoPlay}
user={channelName} ref={containerRef}
jsonId={id} videoStyles={{
poster={videoCover || ""} videoContainer: { aspectRatio: "16 / 9" },
nextVideo={nextVideo} video: { aspectRatio: "16 / 9" },
onEnd={onEndVideo} }}
autoPlay={doAutoPlay} />
ref={containerRef}
videoStyles={{
videoContainer: { aspectRatio: "16 / 9" },
video: { aspectRatio: "16 / 9" },
}}
/>
</Box>
)}
{playlistData && (
<Playlists
playlistData={playlistData}
currentVideoIdentifier={videoData?.id}
onClick={getVideoData}
/>
)}
</Box> </Box>
<VideoActionsBar
channelName={channelName}
videoData={videoData}
videoReference={videoReference}
superLikeList={superLikeList}
setSuperLikeList={setSuperLikeList}
sx={{ width: "100%" }}
/>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
width: "100%",
marginTop: "10px",
gap: "10px",
}}
>
<VideoTitle
variant="h1"
color="textPrimary"
sx={{
textAlign: "center",
}}
>
{videoData?.title}
</VideoTitle>
</Box>
{videoData?.created && (
<Typography
variant="h6"
sx={{
fontSize: "16px",
}}
color={theme.palette.text.primary}
>
{formatDate(videoData.created)}
</Typography>
)}
<Spacer height="30px" />
{videoData?.fullDescription && (
<Box
sx={{
background: "#333333",
borderRadius: "5px",
padding: "5px",
width: "100%",
cursor: !descriptionHeight
? "default"
: isExpandedDescription
? "default"
: "pointer",
position: "relative",
}}
className={
!descriptionHeight
? ""
: isExpandedDescription
? ""
: "hover-click"
}
>
{descriptionHeight && !isExpandedDescription && (
<Box
sx={{
position: "absolute",
top: "0px",
right: "0px",
left: "0px",
bottom: "0px",
cursor: "pointer",
}}
onClick={() => {
if (isExpandedDescription) return;
setIsExpandedDescription(true);
}}
/>
)}
<Box
ref={contentRef}
sx={{
height: !descriptionHeight
? "auto"
: isExpandedDescription
? "auto"
: `${descriptionHeight}px`,
overflow: "hidden",
}}
>
{videoData?.htmlDescription ? (
<DisplayHtml html={videoData?.htmlDescription} />
) : (
<VideoDescription
variant="body1"
color="textPrimary"
sx={{
cursor: "default",
}}
>
{videoData?.fullDescription}
</VideoDescription>
)}
</Box>
{descriptionHeight >= descriptionThreshold && (
<Typography
onClick={() => {
setIsExpandedDescription(prev => !prev);
}}
sx={{
fontWeight: "bold",
fontSize: "16px",
cursor: "pointer",
paddingLeft: "15px",
paddingTop: "15px",
}}
>
{isExpandedDescription ? "Show less" : "...more"}
</Typography>
)}
</Box>
)}
</>
{videoData?.id && videoData?.user && (
<SuperLikesSection
loadingSuperLikes={loadingSuperLikes}
superlikes={superLikeList}
postId={videoData?.id || ""}
postName={videoData?.user || ""}
/>
)} )}
{videoData?.id && channelName && ( {playlistData && (
<CommentSection <Playlists
postId={videoData?.id || ""} playlistData={playlistData}
postName={channelName || ""} currentVideoIdentifier={videoData?.id}
onClick={getVideoData}
/> />
)} )}
</VideoPlayerContainer> </VideoPlayerContainer>
<VideoActionsBar
channelName={channelName}
videoData={videoData}
videoReference={videoReference}
superLikeList={superLikeList}
setSuperLikeList={setSuperLikeList}
sx={{ width: "100%" }}
/>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
width: "100%",
marginTop: "10px",
gap: "10px",
}}
>
<VideoTitle
variant="h1"
color="textPrimary"
sx={{
textAlign: "center",
}}
>
{videoData?.title}
</VideoTitle>
</Box>
<Box
sx={{
display: "flex",
width: "100%",
gap: "20px",
}}
>
{videoData?.created && (
<Typography
variant="h2"
sx={{
fontSize: fontSizeSmall,
}}
color={theme.palette.text.primary}
>
{formatDate(videoData.created)}
</Typography>
)}
{videoData?.fileSize > minFileSize && (
<Typography
variant="h1"
sx={{
fontSize: "90%",
}}
color={"green"}
>
{formatBytes(videoData.fileSize, 2, "Decimal")}
</Typography>
)}
</Box>
<Spacer height="30px" />
{videoData?.fullDescription && (
<Box
sx={{
background: "#333333",
borderRadius: "5px",
padding: "5px",
width: "95%",
alignSelf: "flex-start",
cursor: !descriptionHeight
? "default"
: isExpandedDescription
? "default"
: "pointer",
}}
className={
!descriptionHeight ? "" : isExpandedDescription ? "" : "hover-click"
}
>
{descriptionHeight && !isExpandedDescription && (
<Box
sx={{
position: "absolute",
top: "0px",
right: "0px",
left: "0px",
bottom: "0px",
cursor: "pointer",
}}
onClick={() => {
if (isExpandedDescription) return;
setIsExpandedDescription(true);
}}
/>
)}
<Box
ref={contentRef}
sx={{
height: !descriptionHeight
? "auto"
: isExpandedDescription
? "auto"
: `${descriptionHeight}px`,
overflow: "hidden",
}}
>
{videoData?.htmlDescription ? (
<DisplayHtml html={videoData?.htmlDescription} />
) : (
<VideoDescription
variant="body1"
color="textPrimary"
sx={{
cursor: "default",
}}
>
{videoData?.fullDescription}
</VideoDescription>
)}
</Box>
{descriptionHeight >= descriptionThreshold && (
<Typography
onClick={() => {
setIsExpandedDescription(prev => !prev);
}}
sx={{
fontWeight: "bold",
fontSize: "16px",
cursor: "pointer",
paddingLeft: "15px",
paddingTop: "15px",
}}
>
{isExpandedDescription ? "Show less" : "...more"}
</Typography>
)}
</Box>
)}
{videoData?.id && videoData?.user && (
<SuperLikesSection
loadingSuperLikes={loadingSuperLikes}
superlikes={superLikeList}
postId={videoData?.id || ""}
postName={videoData?.user || ""}
/>
)}
{videoData?.id && channelName && (
<CommentSection
postId={videoData?.id || ""}
postName={channelName || ""}
/>
)}
</Box> </Box>
); );
}; };

View File

@ -1,75 +1,22 @@
import { Avatar, Box, useTheme } from "@mui/material"; import { Box, SxProps, Theme, useMediaQuery } from "@mui/material";
import { FollowButton } from "../../../components/common/ContentButtons/FollowButton.tsx"; import { smallScreenSizeString } from "../../../constants/Misc.ts";
import { SubscribeButton } from "../../../components/common/ContentButtons/SubscribeButton.tsx"; import { ChannelButtons } from "./ChannelButtons.tsx";
import { RootState } from "../../../state/store.ts"; import { ChannelName, ChannelParams } from "./ChannelName.tsx";
import { import { Spacer } from "./VideoContent-styles.tsx";
AuthorTextComment,
StyledCardColComment,
StyledCardHeaderComment,
} from "./VideoContent-styles.tsx";
import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
export interface ChannelActionsParams { export const ChannelActions = ({ channelName, sx }: ChannelParams) => {
channelName: string; const boxSX: SxProps<Theme> = {
} display: "flex",
export const ChannelActions = ({ channelName }: ChannelActionsParams) => { flexDirection: "row",
const navigate = useNavigate(); flexWrap: "wrap",
const theme = useTheme(); rowGap: "10px",
const userName = useSelector((state: RootState) => state.auth.user?.name); columnGap: "20px",
};
return ( return (
<Box> <Box sx={{ ...boxSX, ...sx }}>
<StyledCardHeaderComment <ChannelName channelName={channelName} />
sx={{ <ChannelButtons channelName={channelName} />
"& .MuiCardHeader-content": {
overflow: "hidden",
},
}}
>
<Box
sx={{
cursor: "pointer",
}}
onClick={() => {
navigate(`/channel/${channelName}`);
}}
>
<Avatar
src={`/arbitrary/THUMBNAIL/${channelName}/qortal_avatar`}
alt={`${channelName}'s avatar`}
/>
</Box>
<StyledCardColComment>
<AuthorTextComment
color={
theme.palette.mode === "light"
? theme.palette.text.secondary
: "#d6e8ff"
}
sx={{
cursor: "pointer",
}}
onClick={() => {
navigate(`/channel/${channelName}`);
}}
>
{channelName}
{channelName !== userName && (
<>
<SubscribeButton
subscriberName={channelName}
sx={{ marginLeft: "20px" }}
/>
<FollowButton
followerName={channelName}
sx={{ marginLeft: "20px" }}
/>
</>
)}
</AuthorTextComment>
</StyledCardColComment>
</StyledCardHeaderComment>
</Box> </Box>
); );
}; };

View File

@ -0,0 +1,24 @@
import { useSelector } from "react-redux";
import { FollowButton } from "../../../components/common/ContentButtons/FollowButton.tsx";
import { SubscribeButton } from "../../../components/common/ContentButtons/SubscribeButton.tsx";
import { RootState } from "../../../state/store.ts";
import { ChannelParams } from "./ChannelName.tsx";
import { StyledCardColComment } from "./VideoContent-styles.tsx";
export const ChannelButtons = ({ channelName, sx }: ChannelParams) => {
const userName = useSelector((state: RootState) => state.auth.user?.name);
return (
<StyledCardColComment sx={{ alignItems: "center", ...sx }}>
{channelName !== userName && (
<>
<SubscribeButton subscriberName={channelName} />
<FollowButton
followerName={channelName}
sx={{ marginLeft: "20px" }}
/>
</>
)}
</StyledCardColComment>
);
};

View File

@ -0,0 +1,58 @@
import { Avatar, Box, SxProps, Theme, useTheme } from "@mui/material";
import {
AuthorTextComment,
StyledCardHeaderComment,
} from "./VideoContent-styles.tsx";
import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
export interface ChannelParams {
channelName: string;
sx?: SxProps<Theme>;
}
export const ChannelName = ({ channelName }: ChannelParams) => {
const navigate = useNavigate();
const theme = useTheme();
return (
<StyledCardHeaderComment
sx={{
"& .MuiCardHeader-content": {
overflow: "hidden",
},
}}
>
<Box
sx={{
display: "flex",
flexDirection: "row",
alignItems: "center",
cursor: "pointer",
}}
onClick={() => {
navigate(`/channel/${channelName}`);
}}
>
<Avatar
src={`/arbitrary/THUMBNAIL/${channelName}/qortal_avatar`}
alt={`${channelName}'s avatar`}
/>
<AuthorTextComment
color={
theme.palette.mode === "light"
? theme.palette.text.secondary
: "#d6e8ff"
}
sx={{
cursor: "pointer",
display: "inline",
marginLeft: "10px",
}}
>
{channelName}
</AuthorTextComment>
</Box>
</StyledCardHeaderComment>
);
};

View File

@ -1,25 +1,18 @@
import { Avatar, Box, SxProps, Theme, useTheme } from "@mui/material"; import DownloadIcon from "@mui/icons-material/Download";
import { FollowButton } from "../../../components/common/ContentButtons/FollowButton.tsx"; import { Box, SxProps, Theme, useMediaQuery } from "@mui/material";
import { useMemo } from "react";
import { LikeAndDislike } from "../../../components/common/ContentButtons/LikeAndDislike.tsx"; import { LikeAndDislike } from "../../../components/common/ContentButtons/LikeAndDislike.tsx";
import { SubscribeButton } from "../../../components/common/ContentButtons/SubscribeButton.tsx";
import { SuperLike } from "../../../components/common/ContentButtons/SuperLike.tsx"; import { SuperLike } from "../../../components/common/ContentButtons/SuperLike.tsx";
import FileElement from "../../../components/common/FileElement.tsx"; import FileElement from "../../../components/common/FileElement.tsx";
import { videoRefType } from "../../../components/common/VideoPlayer/VideoPlayer.tsx"; import {
import { titleFormatterOnSave } from "../../../constants/Misc.ts"; smallScreenSizeString,
import { useFetchSuperLikes } from "../../../hooks/useFetchSuperLikes.tsx"; titleFormatterOnSave,
import DownloadIcon from "@mui/icons-material/Download"; } from "../../../constants/Misc.ts";
import { RootState } from "../../../state/store.ts";
import { ChannelActions } from "./ChannelActions.tsx"; import { ChannelActions } from "./ChannelActions.tsx";
import { import {
AuthorTextComment,
FileAttachmentContainer, FileAttachmentContainer,
FileAttachmentFont, FileAttachmentFont,
StyledCardColComment,
StyledCardHeaderComment,
} from "./VideoContent-styles.tsx"; } from "./VideoContent-styles.tsx";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { useMemo, useState } from "react";
export interface VideoActionsBarProps { export interface VideoActionsBarProps {
channelName: string; channelName: string;
@ -82,11 +75,12 @@ export const VideoActionsBar = ({
return ( return (
<Box <Box
sx={{ sx={{
width: "80%",
display: "grid",
gridTemplateColumns: "1fr 1fr",
marginTop: "15px", marginTop: "15px",
display: "flex",
flexDirection: "row",
alignItems: "center", alignItems: "center",
flexWrap: "wrap",
gap: "20px",
...sx, ...sx,
}} }}
> >
@ -95,6 +89,8 @@ export const VideoActionsBar = ({
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "row", flexDirection: "row",
height: "100%",
alignItems: "center",
}} }}
> >
{videoData && ( {videoData && (
@ -110,28 +106,30 @@ export const VideoActionsBar = ({
setSuperLikeList(prev => [val, ...prev]); setSuperLikeList(prev => [val, ...prev]);
}} }}
/> />
<FileAttachmentContainer>
<FileAttachmentFont>Save to Disk</FileAttachmentFont>
<FileElement
fileInfo={{
...videoReference,
filename: saveAsFilename,
mimeType: videoData?.videoType || '"video/mp4',
}}
title={videoData?.filename || videoData?.title?.slice(0, 20)}
customStyles={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<DownloadIcon />
</FileElement>
</FileAttachmentContainer>
</> </>
)} )}
</Box> </Box>
{videoData && (
<FileAttachmentContainer sx={{ width: "100%", maxWidth: "340px" }}>
<FileAttachmentFont>Save Video</FileAttachmentFont>
<FileElement
fileInfo={{
...videoReference,
filename: saveAsFilename,
mimeType: videoData?.videoType || '"video/mp4',
}}
title={videoData?.filename || videoData?.title?.slice(0, 20)}
customStyles={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<DownloadIcon />
</FileElement>
</FileAttachmentContainer>
)}
</Box> </Box>
); );
}; };

View File

@ -1,6 +1,7 @@
import { styled } from "@mui/system"; import { styled } from "@mui/system";
import { Box, Grid, Typography, Checkbox } from "@mui/material"; import { Box, Grid, Typography, Checkbox } from "@mui/material";
import { fontSizeMedium } from "../../../constants/Misc.ts"; import { fontSizeMedium, fontSizeSmall } from "../../../constants/Misc.ts";
import { useIsMobile } from "../../../hooks/useIsMobile.ts";
export const VideoContentContainer = styled(Box)(({ theme }) => ({ export const VideoContentContainer = styled(Box)(({ theme }) => ({
display: "flex", display: "flex",
@ -8,10 +9,7 @@ export const VideoContentContainer = styled(Box)(({ theme }) => ({
alignItems: "start", alignItems: "start",
})); }));
export const VideoPlayerContainer = styled(Box)(({ theme }) => ({ export const VideoPlayerContainer = styled(Box)(({ theme }) => ({}));
width: "55vw",
marginLeft: "5%",
}));
export const VideoTitle = styled(Typography)(({ theme }) => ({ export const VideoTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Raleway", fontFamily: "Raleway",
@ -57,14 +55,14 @@ export const StyledCardCol = styled(Box)({
export const StyledCardColComment = styled(Box)({ export const StyledCardColComment = styled(Box)({
display: "flex", display: "flex",
overflow: "hidden", overflow: "hidden",
flexDirection: "column", flexDirection: "row",
gap: "2px", gap: "2px",
alignItems: "flex-start", alignItems: "flex-start",
}); });
export const AuthorTextComment = styled(Typography)({ export const AuthorTextComment = styled(Typography)({
fontFamily: "Raleway, sans-serif", fontFamily: "Raleway, sans-serif",
fontSize: "20px", fontSize: fontSizeMedium,
lineHeight: "1.2", lineHeight: "1.2",
}); });
@ -73,7 +71,6 @@ export const FileAttachmentContainer = styled(Box)(({ theme }) => ({
alignItems: "center", alignItems: "center",
padding: "5px 10px", padding: "5px 10px",
border: `1px solid ${theme.palette.text.primary}`, border: `1px solid ${theme.palette.text.primary}`,
width: "350px",
height: "50px", height: "50px",
})); }));

View File

@ -1,12 +1,18 @@
import { Box, Typography } from "@mui/material"; import { Box, Typography, useMediaQuery } from "@mui/material";
import React from "react"; import React, { useEffect, useState } from "react";
import DeletedVideo from "../../../assets/img/DeletedVideo.jpg"; import DeletedVideo from "../../../assets/img/DeletedVideo.jpg";
import { CommentSection } from "../../../components/common/Comments/CommentSection.tsx"; import { CommentSection } from "../../../components/common/Comments/CommentSection.tsx";
import { SuperLikesSection } from "../../../components/common/SuperLikesList/SuperLikesSection.tsx"; import { SuperLikesSection } from "../../../components/common/SuperLikesList/SuperLikesSection.tsx";
import { DisplayHtml } from "../../../components/common/TextEditor/DisplayHtml.tsx"; import { DisplayHtml } from "../../../components/common/TextEditor/DisplayHtml.tsx";
import { VideoPlayer } from "../../../components/common/VideoPlayer/VideoPlayer.tsx"; import { VideoPlayer } from "../../../components/common/VideoPlayer/VideoPlayer.tsx";
import { minFileSize } from "../../../constants/Misc.ts"; import {
fontSizeSmall,
largeScreenSizeString,
minFileSize,
smallScreenSizeString,
} from "../../../constants/Misc.ts";
import { useIsMobile } from "../../../hooks/useIsMobile.ts";
import { formatBytes } from "../../../utils/numberFunctions.ts"; import { formatBytes } from "../../../utils/numberFunctions.ts";
import { formatDate } from "../../../utils/time.ts"; import { formatDate } from "../../../utils/time.ts";
import { VideoActionsBar } from "./VideoActionsBar.tsx"; import { VideoActionsBar } from "./VideoActionsBar.tsx";
@ -18,6 +24,7 @@ import {
VideoPlayerContainer, VideoPlayerContainer,
VideoTitle, VideoTitle,
} from "./VideoContent-styles.tsx"; } from "./VideoContent-styles.tsx";
import { useSignal } from "@preact/signals-react";
export const VideoContent = () => { export const VideoContent = () => {
const { const {
@ -39,18 +46,47 @@ export const VideoContent = () => {
superLikeList, superLikeList,
setSuperLikeList, setSuperLikeList,
} = useVideoContentState(); } = useVideoContentState();
const isScreenSmall = !useMediaQuery(`(min-width:${smallScreenSizeString})`);
const [screenWidth, setScreenWidth] = useState<number>(
window.innerWidth + 120
);
let videoWidth = 100;
const maxWidth = 95;
const pixelsPerPercent = 17.5;
const smallScreenPixels = 700;
if (!isScreenSmall)
videoWidth =
maxWidth - (screenWidth - smallScreenPixels) / pixelsPerPercent;
const minWidthPercent = 70;
if (videoWidth < minWidthPercent) videoWidth = minWidthPercent;
useEffect(() => {
window.addEventListener("resize", e => {
setScreenWidth(window.innerWidth + 120);
});
}, []);
return ( return (
<> <>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
padding: "0px 10px 0px 5%", padding: `0px 0px 0px ${isScreenSmall ? "5px" : "2%"}`,
width: "100%",
}} }}
onClick={focusVideo} onClick={focusVideo}
> >
{videoReference ? ( {videoReference ? (
<VideoPlayerContainer> <VideoPlayerContainer
sx={{
width: `${videoWidth}%`,
marginLeft: "0%",
}}
>
<VideoPlayer <VideoPlayer
name={videoReference?.name} name={videoReference?.name}
service={videoReference?.service} service={videoReference?.service}
@ -77,64 +113,61 @@ export const VideoContent = () => {
<Box sx={{ width: "55vw", aspectRatio: "16/9" }}></Box> <Box sx={{ width: "55vw", aspectRatio: "16/9" }}></Box>
)} )}
<VideoContentContainer> <VideoContentContainer>
<VideoTitle
variant={isScreenSmall ? "h2" : "h1"}
color="textPrimary"
sx={{
textAlign: "start",
marginTop: "10px",
}}
>
{videoData?.title}
</VideoTitle>
<Box>
{videoData?.created && (
<Typography
variant="h2"
sx={{
fontSize: fontSizeSmall,
display: "inline",
}}
color={theme.palette.text.primary}
>
{formatDate(videoData.created)}
</Typography>
)}
{videoData?.fileSize > minFileSize && (
<Typography
variant="h1"
sx={{
fontSize: "90%",
display: "inline",
marginLeft: "20px",
}}
color={"green"}
>
{formatBytes(videoData.fileSize, 2, "Decimal")}
</Typography>
)}
</Box>
<VideoActionsBar <VideoActionsBar
channelName={channelName} channelName={channelName}
videoData={videoData} videoData={videoData}
setSuperLikeList={setSuperLikeList} setSuperLikeList={setSuperLikeList}
superLikeList={superLikeList} superLikeList={superLikeList}
videoReference={videoReference} videoReference={videoReference}
sx={{ width: "calc(100% - 5px)" }}
/> />
<Box <Spacer height="15px" />
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
width: "100%",
marginTop: "20px",
gap: "10px",
}}
>
<VideoTitle
variant="h1"
color="textPrimary"
sx={{
textAlign: "start",
}}
>
{videoData?.title}
</VideoTitle>
</Box>
{videoData?.fileSize > minFileSize && (
<Typography
variant="h1"
sx={{
fontSize: "90%",
}}
color={"green"}
>
{formatBytes(videoData.fileSize, 2, "Decimal")}
</Typography>
)}
{videoData?.created && (
<Typography
variant="h6"
sx={{
fontSize: "16px",
}}
color={theme.palette.text.primary}
>
{formatDate(videoData.created)}
</Typography>
)}
<Spacer height="30px" />
{videoData?.fullDescription && ( {videoData?.fullDescription && (
<Box <Box
sx={{ sx={{
background: "#333333", background: "#333333",
borderRadius: "5px", borderRadius: "5px",
padding: "5px", padding: "5px",
width: "70%", width: "95%",
cursor: !descriptionHeight cursor: !descriptionHeight
? "default" ? "default"
: isExpandedDescription : isExpandedDescription

View File

@ -7,9 +7,11 @@ import {
MenuItem, MenuItem,
OutlinedInput, OutlinedInput,
Select, Select,
useMediaQuery,
} from "@mui/material"; } from "@mui/material";
import { StatsData } from "../../../components/StatsData.tsx"; import { StatsData } from "../../../components/StatsData.tsx";
import { categories, subCategories } from "../../../constants/Categories.ts"; import { categories, subCategories } from "../../../constants/Categories.ts";
import { smallScreenSizeString } from "../../../constants/Misc.ts";
import { useSidebarState } from "./SearchSidebar-State.ts"; import { useSidebarState } from "./SearchSidebar-State.ts";
import { import {
FiltersCol, FiltersCol,
@ -38,8 +40,19 @@ export const SearchSidebar = ({ onSearch }: SearchSidebarProps) => {
filtersToDefault, filtersToDefault,
} = useSidebarState(onSearch); } = useSidebarState(onSearch);
const filtersStyle = { width: "75px", marginRight: "10px" };
const isScreenSmall = !useMediaQuery(`(min-width:600px)`);
return ( return (
<FiltersCol item xs={12} md={2} lg={2} xl={2} sm={3}> <Box
sx={{
marginLeft: "5px",
marginRight: isScreenSmall ? "5px" : "0px",
alignItems: "center",
display: "flex",
flexDirection: "column",
}}
>
<FiltersContainer> <FiltersContainer>
<StatsData /> <StatsData />
<Input <Input
@ -101,12 +114,11 @@ export const SearchSidebar = ({ onSearch }: SearchSidebarProps) => {
/> />
<FiltersSubContainer> <FiltersSubContainer>
<FormControl sx={{ width: "100%", marginTop: "30px" }}> <FormControl sx={{ width: "98%", marginTop: "30px" }}>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
gap: "20px", gap: "20px",
alignItems: "center",
flexDirection: "column", flexDirection: "column",
}} }}
> >
@ -201,7 +213,7 @@ export const SearchSidebar = ({ onSearch }: SearchSidebarProps) => {
</FiltersSubContainer> </FiltersSubContainer>
<FiltersSubContainer> <FiltersSubContainer>
<FiltersRow> <FiltersRow>
Videos <span style={filtersStyle}>Videos</span>
<FiltersRadioButton <FiltersRadioButton
checked={filterType === "videos"} checked={filterType === "videos"}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
@ -211,7 +223,7 @@ export const SearchSidebar = ({ onSearch }: SearchSidebarProps) => {
/> />
</FiltersRow> </FiltersRow>
<FiltersRow> <FiltersRow>
Playlists <span style={filtersStyle}> Playlists</span>
<FiltersRadioButton <FiltersRadioButton
checked={filterType === "playlists"} checked={filterType === "playlists"}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
@ -221,12 +233,15 @@ export const SearchSidebar = ({ onSearch }: SearchSidebarProps) => {
/> />
</FiltersRow> </FiltersRow>
</FiltersSubContainer> </FiltersSubContainer>
<Button <Button
onClick={() => { onClick={() => {
filtersToDefault(); filtersToDefault();
}} }}
sx={{ sx={{
marginTop: "20px", marginTop: "20px",
width: "80%",
alignSelf: "center",
}} }}
variant="contained" variant="contained"
> >
@ -238,12 +253,14 @@ export const SearchSidebar = ({ onSearch }: SearchSidebarProps) => {
}} }}
sx={{ sx={{
marginTop: "20px", marginTop: "20px",
width: "80%",
alignSelf: "center",
}} }}
variant="contained" variant="contained"
> >
Search Search
</Button> </Button>
</FiltersContainer> </FiltersContainer>
</FiltersCol> </Box>
); );
}; };

View File

@ -9,6 +9,7 @@ import {
Autocomplete, Autocomplete,
Radio, Radio,
} from "@mui/material"; } from "@mui/material";
import { fontSizeMedium, fontSizeSmall } from "../../../constants/Misc.ts";
export const VideoContainer = styled(Grid)(({ theme }) => ({ export const VideoContainer = styled(Grid)(({ theme }) => ({
position: "relative", position: "relative",
@ -23,7 +24,7 @@ export const VideoContainer = styled(Grid)(({ theme }) => ({
export const VideoCardContainer = styled("div")(({ theme }) => ({ export const VideoCardContainer = styled("div")(({ theme }) => ({
display: "grid", display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
gap: theme.spacing(2), gap: theme.spacing(2),
padding: "10px", padding: "10px",
width: "100%", width: "100%",
@ -31,7 +32,7 @@ export const VideoCardContainer = styled("div")(({ theme }) => ({
export const VideoCardCol = styled("div")({ export const VideoCardCol = styled("div")({
position: "relative", position: "relative",
minWidth: "250px", // Minimum width of each item minWidth: "200px", // Minimum width of each item
maxWidth: "1fr", // Maximum width, allowing the item to fill the column maxWidth: "1fr", // Maximum width, allowing the item to fill the column
// ... other styles // ... other styles
}); });
@ -132,16 +133,14 @@ export const FiltersCol = styled(Grid)(({ theme }) => ({
export const FiltersContainer = styled(Box)(({ theme }) => ({ export const FiltersContainer = styled(Box)(({ theme }) => ({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "space-between",
})); }));
export const FiltersRow = styled(Box)(({ theme }) => ({ export const FiltersRow = styled(Box)(({ theme }) => ({
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "center",
width: "100%", width: "100%",
padding: "0 15px", fontSize: fontSizeSmall,
fontSize: "16px",
userSelect: "none", userSelect: "none",
})); }));
@ -161,7 +160,7 @@ export const FiltersRadioButton = styled(Radio)(({ theme }) => ({
export const FiltersSubContainer = styled(Box)(({ theme }) => ({ export const FiltersSubContainer = styled(Box)(({ theme }) => ({
display: "flex", display: "flex",
alignItems: "center", alignItems: "start",
flexDirection: "column", flexDirection: "column",
gap: "5px", gap: "5px",
})); }));

View File

@ -145,7 +145,6 @@ export const VideoList = ({ videos }: VideoListProps) => {
maxHeight: "50%", maxHeight: "50%",
}} }}
/> />
<VideoCardTitle>{videoObj?.title}</VideoCardTitle> <VideoCardTitle>{videoObj?.title}</VideoCardTitle>
<BottomParent> <BottomParent>
<NameContainer <NameContainer

View File

@ -1,10 +1,15 @@
import { TabContext, TabList, TabPanel } from "@mui/lab"; import { TabContext, TabList, TabPanel } from "@mui/lab";
import { Box, Grid, Tab } from "@mui/material"; import { Box, Grid, Tab, useMediaQuery } from "@mui/material";
import React from "react"; import React from "react";
import LazyLoad from "../../components/common/LazyLoad"; import LazyLoad from "../../components/common/LazyLoad";
import { ListSuperLikeContainer } from "../../components/common/ListSuperLikes/ListSuperLikeContainer.tsx"; import { ListSuperLikeContainer } from "../../components/common/ListSuperLikes/ListSuperLikeContainer.tsx";
import { fontSizeMedium } from "../../constants/Misc.ts"; import {
fontSizeLarge,
fontSizeMedium,
fontSizeSmall,
} from "../../constants/Misc.ts";
import { useIsMobile } from "../../hooks/useIsMobile.ts";
import { SearchSidebar } from "./Components/SearchSidebar.tsx"; import { SearchSidebar } from "./Components/SearchSidebar.tsx";
import { FiltersCol, VideoManagerRow } from "./Components/VideoList-styles.tsx"; import { FiltersCol, VideoManagerRow } from "./Components/VideoList-styles.tsx";
import VideoList from "./Components/VideoList.tsx"; import VideoList from "./Components/VideoList.tsx";
@ -29,79 +34,77 @@ export const Home = ({ mode }: HomeProps) => {
paddingLeft: "0px", paddingLeft: "0px",
paddingRight: "0px", paddingRight: "0px",
}; };
const isScreenSmall = !useMediaQuery("(min-width:600px)");
const isScreenLarge = useMediaQuery("(min-width:1200px)");
const tabSX = {
fontSize: isScreenSmall ? fontSizeSmall : fontSizeLarge,
paddingLeft: "0px",
paddingRight: "0px",
};
const homeBaseSX = { display: "grid", width: "100%" };
const bigGridSX = { gridTemplateColumns: "200px auto 250px" };
const mediumGridSX = { gridTemplateColumns: "200px auto" };
const smallGridSX = { gridTemplateColumns: "100%", gap: "20px" };
let homeColumns: object;
if (isScreenLarge) homeColumns = bigGridSX;
else if (!isScreenSmall) homeColumns = mediumGridSX;
else homeColumns = smallGridSX;
return ( return (
<> <>
<Grid container sx={{ width: "100%" }}> <Box sx={{ ...homeBaseSX, ...homeColumns }}>
<SearchSidebar onSearch={getVideosHandler} /> <SearchSidebar onSearch={getVideosHandler} />
<Grid item xs={12} md={10} lg={7} xl={8} sm={9}>
<VideoManagerRow> <Box
<Box sx={{
sx={{ width: "100%",
width: "100%", display: "flex",
display: "flex", flexDirection: "column",
flexDirection: "column", alignItems: "center",
alignItems: "center", }}
marginTop: "20px", >
}} <TabContext value={tabValue}>
<TabList
onChange={changeTab}
textColor={"secondary"}
indicatorColor={"secondary"}
centered={false}
> >
<SubtitleContainer <Tab label="All" value={"all"} sx={tabSX} />
sx={{ <Tab label="Subscriptions" value={"subscriptions"} sx={tabSX} />
justifyContent: "flex-start", </TabList>
paddingLeft: "15px", <TabPanel value={"all"} sx={tabPaneSX}>
width: "100%", <VideoList videos={videos} />
maxWidth: "1400px", <LazyLoad
}} onLoadMore={getVideosHandler}
></SubtitleContainer> isLoading={isLoading}
<TabContext value={tabValue}> ></LazyLoad>
<TabList </TabPanel>
onChange={changeTab} <TabPanel value={"subscriptions"} sx={tabPaneSX}>
textColor={"secondary"} {filteredSubscriptionList.length > 0 ? (
indicatorColor={"secondary"} <>
>
<Tab
label="All Videos"
value={"all"}
sx={{ fontSize: fontSizeMedium }}
/>
<Tab
label="Subscriptions"
value={"subscriptions"}
sx={{ fontSize: fontSizeMedium }}
/>
</TabList>
<TabPanel value={"all"} sx={tabPaneSX}>
<VideoList videos={videos} /> <VideoList videos={videos} />
<LazyLoad <LazyLoad
onLoadMore={getVideosHandler} onLoadMore={getVideosHandler}
isLoading={isLoading} isLoading={isLoading}
></LazyLoad> ></LazyLoad>
</TabPanel> </>
<TabPanel value={"subscriptions"} sx={tabPaneSX}> ) : (
{filteredSubscriptionList.length > 0 ? ( !isLoading && (
<> <div style={{ textAlign: "center" }}>
<VideoList videos={videos} /> You have no subscriptions
<LazyLoad </div>
onLoadMore={getVideosHandler} )
isLoading={isLoading} )}
></LazyLoad> </TabPanel>
</> </TabContext>
) : !isLoading ? ( </Box>
<div style={{ textAlign: "center" }}>
You have no subscriptions <ListSuperLikeContainer />
</div> </Box>
) : (
<></>
)}
</TabPanel>
</TabContext>
</Box>
</VideoManagerRow>
</Grid>
<FiltersCol item xs={0} lg={3} xl={2}>
<ListSuperLikeContainer />
</FiltersCol>
</Grid>
</> </>
); );
}; };

View File

@ -19,6 +19,8 @@ interface settingsState {
subscriptionListFilter: SubscriptionListFilterType; subscriptionListFilter: SubscriptionListFilterType;
showStats: boolean; showStats: boolean;
volume: number; volume: number;
mutedVolume: number;
isMuted: boolean;
} }
const initialState: settingsState = { const initialState: settingsState = {
@ -30,6 +32,8 @@ const initialState: settingsState = {
subscriptionListFilter: "currentNameOnly", subscriptionListFilter: "currentNameOnly",
showStats: true, showStats: true,
volume: 0.5, volume: 0.5,
mutedVolume: 0,
isMuted: false,
}; };
export const persistSlice = createSlice({ export const persistSlice = createSlice({
@ -77,6 +81,12 @@ export const persistSlice = createSlice({
setVolumeSetting: (state, action: PayloadAction<number>) => { setVolumeSetting: (state, action: PayloadAction<number>) => {
state.volume = action.payload; state.volume = action.payload;
}, },
setMutedVolumeSetting: (state, action: PayloadAction<number>) => {
state.mutedVolume = action.payload;
},
setIsMuted: (state, action: PayloadAction<boolean>) => {
state.isMuted = action.payload;
},
}, },
}); });
@ -89,6 +99,8 @@ export const {
changeFilterType, changeFilterType,
resetSubscriptions, resetSubscriptions,
setVolumeSetting, setVolumeSetting,
setMutedVolumeSetting,
setIsMuted,
} = persistSlice.actions; } = persistSlice.actions;
export default persistSlice.reducer; export default persistSlice.reducer;