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",
"private": true,
"version": "2.0.0",
"version": "2.1.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@ -1,10 +1,11 @@
import React from "react";
import { smallScreenSizeString } from "../../constants/Misc.ts";
import { CardContentContainerComment } from "../common/Comments/Comments-styles";
import {
CrowdfundSubTitle,
CrowdfundSubTitleRow,
} 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";
export const Playlists = ({
@ -13,7 +14,8 @@ export const Playlists = ({
onClick,
}) => {
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 (
<Box
@ -21,7 +23,7 @@ export const Playlists = ({
display: "flex",
flexDirection: "column",
width: "100%",
maxHeight: "30.94vw",
height: isScreenSmall ? "200px" : videoPlayerHeight,
}}
>
<CardContentContainerComment
@ -32,7 +34,7 @@ export const Playlists = ({
}}
>
{playlistData?.videos?.map((vid, index) => {
const isCurrentVidPlayling =
const isCurrentVidPlaying =
vid?.identifier === currentVideoIdentifier;
return (
@ -42,15 +44,15 @@ export const Playlists = ({
display: "flex",
gap: "10px",
width: "100%",
background: isCurrentVidPlayling && theme.palette.primary.main,
background: isCurrentVidPlaying && theme.palette.primary.main,
alignItems: "center",
padding: "10px",
borderRadius: "5px",
cursor: isCurrentVidPlayling ? "default" : "pointer",
cursor: isCurrentVidPlaying ? "default" : "pointer",
userSelect: "none",
}}
onClick={() => {
if (isCurrentVidPlayling) return;
if (isCurrentVidPlaying) return;
onClick(vid.name, vid.identifier);
// navigate(`/video/${vid.name}/${vid.identifier}`)
}}

View File

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

View File

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

View File

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

View File

@ -1,32 +1,29 @@
import React, { useEffect, useState } from "react";
import ThumbUpIcon from "@mui/icons-material/ThumbUp";
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControl,
Input,
InputAdornment,
InputLabel,
MenuItem,
Modal,
Select,
Tooltip,
} from "@mui/material";
import qortImg from "../../../assets/img/qort.png";
import { MultiplePublish } from "../../Publish/MultiplePublish/MultiplePublishAll.tsx";
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { setNotification } from "../../../state/features/notificationsSlice.ts";
import ShortUniqueId from "short-unique-id";
import qortImg from "../../../assets/img/qort.png";
import {
objectToBase64,
objectToFile,
} from "../../../utils/PublishFormatter.ts";
FOR,
FOR_SUPER_LIKE,
SUPER_LIKE_BASE,
} from "../../../constants/Identifiers.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 {
CrowdfundActionButton,
CrowdfundActionButtonRow,
@ -34,16 +31,7 @@ import {
NewCrowdfundTitle,
Spacer,
} from "../../Publish/PublishVideo/PublishVideo-styles.tsx";
import { utf8ToBase64 } from "../SuperLikesList/CommentEditor.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";
import { CommentInput } from "../Comments/Comments-styles.tsx";
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 {
Accordion,
AccordionDetails,
@ -11,29 +11,28 @@ import {
ListItemIcon,
Popover,
Typography,
useTheme
} from '@mui/material'
import { Movie } from '@mui/icons-material'
import { useSelector } from 'react-redux'
import { RootState } from '../../state/store'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import { useLocation, useNavigate } from 'react-router-dom'
import { DownloadingLight } from '../../assets/svgs/DownloadingLight'
import { DownloadedLight } from '../../assets/svgs/DownloadedLight'
useTheme,
} from "@mui/material";
import { Movie } from "@mui/icons-material";
import { useSelector } from "react-redux";
import { RootState } from "../../state/store";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { useLocation, useNavigate } from "react-router-dom";
import { DownloadingLight } from "../../assets/svgs/DownloadingLight";
import { DownloadedLight } from "../../assets/svgs/DownloadedLight";
export const DownloadTaskManager: React.FC = () => {
const { downloads } = useSelector((state: RootState) => state.global)
const location = useLocation()
const theme = useTheme()
const [visible, setVisible] = useState(false)
const [hidden, setHidden] = useState(true)
const navigate = useNavigate()
const { downloads } = useSelector((state: RootState) => state.global);
const theme = useTheme();
const [visible, setVisible] = useState(false);
const [hidden, setHidden] = useState(true);
const navigate = useNavigate();
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const [openDownload, setOpenDownload] = useState<boolean>(false);
const hashMapVideos = useSelector(
(state: RootState) => state.video.hashMapVideos
);
const handleClick = (event?: React.MouseEvent<HTMLDivElement>) => {
const target = event?.currentTarget as unknown as HTMLButtonElement | null;
setAnchorEl(target);
@ -49,155 +48,159 @@ export const DownloadTaskManager: React.FC = () => {
if (visible) {
setTimeout(() => {
setHidden(true)
setVisible(false)
}, 3000)
setHidden(true);
setVisible(false);
}, 3000);
}
}, [visible])
}, [visible]);
useEffect(() => {
if (Object.keys(downloads).length === 0) return
setVisible(true)
setHidden(false)
}, [downloads])
if (Object.keys(downloads).length === 0) return;
setVisible(true);
setHidden(false);
}, [downloads]);
if (!downloads || Object.keys(downloads).length === 0) return null;
let downloadInProgress = false;
if (
!downloads ||
Object.keys(downloads).length === 0
)
return null
let downloadInProgress = false
if(Object.keys(downloads).find((key)=> (downloads[key]?.status?.status !== 'READY' && downloads[key]?.status?.status !== 'DOWNLOADED'))){
downloadInProgress = true
Object.keys(downloads).find(
key =>
downloads[key]?.status?.status !== "READY" &&
downloads[key]?.status?.status !== "DOWNLOADED"
)
) {
downloadInProgress = true;
}
return (
<Box>
<Button onClick={(e: any) => {
handleClick(e);
setOpenDownload(true);
}}>
{downloadInProgress ? (
<DownloadingLight height='24px' width='24px' className='download-icon' />
) : (
<DownloadedLight height='24px' width='24px' />
)}
<Button
sx={{ padding: "0px 0px", minWidth: "0px" }}
onClick={(e: any) => {
handleClick(e);
setOpenDownload(true);
}}
>
{downloadInProgress ? (
<DownloadingLight
height="24px"
width="24px"
className="download-icon"
/>
) : (
<DownloadedLight height="24px" width="24px" />
)}
</Button>
</Button>
<Popover
id={"download-popover"}
open={openDownload}
anchorEl={anchorEl}
onClose={handleCloseDownload}
anchorOrigin={{
vertical: "bottom",
horizontal: "left"
<Popover
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
sx={{
maxHeight: '50vh',
overflow: 'auto',
width: '250px',
gap: '5px',
display: 'flex',
flexDirection: 'column',
{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;
const id =
downloadObj?.identifier + "_metadata-" + downloadObj?.name;
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}
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 userName = downloadObj?.name;
const identifier = downloadObj?.identifier;
if (identifier && userName)
navigate(`/video/${userName}/${identifier}_metadata`);
}}
>
<Box
sx={{
width: "100%",
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={{
borderRadius: "5px",
color: theme.palette.secondary.main,
}}
/>
</Box>
<Typography
sx={{
display: 'flex',
flexDirection: 'column',
width: '100%',
justifyContent: 'center',
background: theme.palette.primary.main,
fontFamily: "Arial",
color: theme.palette.text.primary,
cursor: 'pointer',
padding: '2px',
}}
onClick={() => {
const id = downloadObj?.properties?.jsonId
if (!id) return
navigate(
`/video/${downloadObj?.properties?.user}/${id}`
)
}}
variant="caption"
>
<Box
sx={{
width: '100%',
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={{
borderRadius: '5px',
color: theme.palette.secondary.main
}}
/>
</Box>
<Typography
sx={{
fontFamily: 'Arial',
color: theme.palette.text.primary
}}
variant="caption"
>
{`${progress?.toFixed(0)}%`}{' '}
{status && status === 'REFETCHING' && '- refetching'}
{status && status === 'DOWNLOADED' && '- building'}
</Typography>
</Box>
<Typography
sx={{
fontSize: '10px',
width: '100%',
textAlign: 'end',
fontFamily: 'Arial',
color: theme.palette.text.primary,
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
}}
>
{downloadObj?.identifier}
</Typography>
</ListItem>
)
})}
</List>
</Popover>
{`${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>
)
}
);
};

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 { PopMenu } from "../PopMenu.tsx";
import ListSuperLikes from "./ListSuperLikes";
import { useSelector } from "react-redux";
import { RootState } from "../../../state/store";
import ThumbUpIcon from "@mui/icons-material/ThumbUp";
export const ListSuperLikeContainer = () => {
const superlikelist = useSelector(
(state: RootState) => state.global.superlikelistAll
);
return (
<Box>
const isScreenLarge = useMediaQuery("(min-width:1200px)");
const superlikeListComponent = (
<>
<Typography
sx={{
fontSize: "18px",
@ -20,6 +22,53 @@ export const ListSuperLikeContainer = () => {
Recent Super likes
</Typography>
<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>
);
};

View File

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

View File

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

View File

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

View File

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

View File

@ -7,12 +7,14 @@ import {
VolumeOff,
VolumeUp,
} 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 { ControlsContainer } from "../VideoPlayer-styles.ts";
import { MobileControls } from "./MobileControls.tsx";
import { useVideoContext } from "./VideoContext.ts";
import { useSignalEffect } from "@preact/signals-react";
export const VideoControls = () => {
const {
@ -28,7 +30,6 @@ export const VideoControls = () => {
from,
videoRef,
canPlay,
isMobileView,
isMuted,
playbackRate,
playing,
@ -37,8 +38,8 @@ export const VideoControls = () => {
showControlsFullScreen,
} = useVideoContext();
const showMobileControls =
isMobileView.value && canPlay.value && showControlsFullScreen.value;
const isScreenSmall = !useMediaQuery(`(min-width:580px)`);
const showMobileControls = isScreenSmall && canPlay.value;
return (
<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, {
useContext,
useEffect,
@ -9,7 +13,14 @@ import React, {
import { useDispatch, useSelector } from "react-redux";
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 { MyContext } from "../../../wrappers/DownloadWrapper.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 playing = useSignal(false);
const isMuted = useSignal(false);
const progress = useSignal(0);
const isLoading = useSignal(false);
const canPlay = useSignal(false);
const startPlay = useSignal(false);
const isMobileView = useSignal(false);
const isMuted = useSignal(persistSelector.isMuted);
const volume = useSignal(persistSelector.volume);
const mutedVolume = useSignal(persistSelector.volume);
const mutedVolume = useSignal(persistSelector.mutedVolume);
const playbackRate = useSignal(persistSelector.playbackRate);
const anchorEl = useSignal(null);
const videoObjectFit = useSignal<StretchVideoType>(
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 {
name,
identifier,
@ -279,7 +310,6 @@ export const useVideoPlayerState = (props: VideoPlayerProps, ref: any) => {
isLoading,
canPlay,
startPlay,
isMobileView,
volume,
mutedVolume,
playbackRate,

View File

@ -55,7 +55,6 @@ export const VideoPlayer = forwardRef<videoRefType, VideoPlayerProps>(
startPlay,
videoObjectFit,
showControlsFullScreen,
duration,
} = contextData;
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 { LightModeSVG } from "../../../assets/svgs/LightModeSVG";
import { DarkModeSVG } from "../../../assets/svgs/DarkModeSVG";
import { fontSizeSmall } from "../../../constants/Misc.ts";
export const CustomAppBar = styled(AppBar)(({ theme }) => ({
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
justifyContent: "start",
alignItems: "center",
width: "100%",
padding: "5px 16px",
padding: "5px 16px 5px 5px",
backgroundImage: "none",
borderBottom: `1px solid ${theme.palette.primary.light}`,
backgroundColor: theme.palette.background.default,
[theme.breakpoints.only("xs")]: {
gap: "15px",
},
height: "50px",
}));
export const LogoContainer = styled("div")({
@ -85,16 +83,15 @@ export const DropdownText = styled(Typography)(({ theme }) => ({
export const NavbarName = styled(Typography)(({ theme }) => ({
fontFamily: "Raleway",
fontSize: "18px",
fontSize: fontSizeSmall,
color: theme.palette.text.primary,
margin: "0 10px",
marginRight: "10px",
}));
export const ThemeSelectRow = styled(Box)({
display: "flex",
alignItems: "center",
gap: "5px",
flexBasis: 0,
gap: "10px",
height: "100%",
});

View File

@ -1,43 +1,12 @@
import React, { useState, useRef } from "react";
import { Box, Button, Input, Popover, useTheme } from "@mui/material";
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 { Box, useMediaQuery } from "@mui/material";
import React from "react";
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 { PublishMenu } from "./Components/PublishMenu.tsx";
import { QtubeLogo } from "./Components/QtubeLogo.tsx";
import { UserMenu } from "./Components/UserMenu.tsx";
import { CustomAppBar } from "./Navbar-styles";
interface Props {
isAuthenticated: boolean;
userName: string | null;
@ -46,296 +15,39 @@ interface Props {
setTheme: (val: string) => void;
}
const NavBar: React.FC<Props> = ({
isAuthenticated,
userName,
userAvatar,
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);
};
const NavBar: React.FC<Props> = ({ isAuthenticated, userName, userAvatar }) => {
const isScreenSmall = !useMediaQuery(`(min-width:600px)`);
const isSecure = isAuthenticated && !!userName;
const gapSize = 10;
return (
<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
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
width: "100%",
justifyContent: "space-between",
gap: `${gapSize}px`,
}}
>
<Popover
id={idNotification}
open={openPopover}
anchorEl={anchorElNotification}
onClose={closeNotificationPopover}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
<QtubeLogo />
<Box
sx={{
display: "flex",
gap: `${isScreenSmall ? gapSize : gapSize * 2}px`,
alignItems: "center",
}}
>
<Box
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",
}}
/>
{isSecure && <Notifications />}
<SearchIcon
sx={{
cursor: "pointer",
}}
onClick={() => {
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}
<DownloadTaskManager />
<UserMenu
isShowMenu={isSecure}
userAvatar={userAvatar}
userName={userName}
/>
)}
<PublishMenu isDisplayed={isSecure} />
</Box>
</Box>
</CustomAppBar>
);

View File

@ -14,3 +14,12 @@ export const fontSizeExLarge = "150%";
export const maxCommentLength = 10_000;
export const minFileSize = 1_000;
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 { CommentSection } from "../../../components/common/Comments/CommentSection.tsx";
import { SuperLikesSection } from "../../../components/common/SuperLikesList/SuperLikesSection.tsx";
import { DisplayHtml } from "../../../components/common/TextEditor/DisplayHtml.tsx";
import { VideoPlayer } from "../../../components/common/VideoPlayer/VideoPlayer.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 { VideoActionsBar } from "../VideoContent/VideoActionsBar.tsx";
import { usePlaylistContentState } from "./PlaylistContent-State.ts";
@ -40,6 +46,8 @@ export const PlaylistContent = () => {
loadingSuperLikes,
} = usePlaylistContentState();
const isScreenSmall = !useMediaQuery(`(min-width:700px)`);
return videoData && videoData?.videos?.length === 0 ? (
<Box
sx={{
@ -55,197 +63,202 @@ export const PlaylistContent = () => {
display: "flex",
alignItems: "center",
flexDirection: "column",
padding: "0px 10px",
marginLeft: "5%",
padding: "0px",
marginLeft: "2%",
}}
onClick={focusVideo}
>
<VideoPlayerContainer
sx={{
marginBottom: "30px",
width: "100%",
display: "grid",
gridTemplateColumns: isScreenSmall ? "1fr" : "60vw auto",
gap: "20px",
}}
>
<>
{videoReference && (
<Box
sx={{
display: "grid",
gridTemplateColumns: "55vw 35vw",
width: "100vw",
gap: "3vw",
aspectRatio: "16/9",
}}
>
{videoReference && (
<Box
sx={{
aspectRatio: "16/9",
}}
>
<VideoPlayer
name={videoReference?.name}
service={videoReference?.service}
identifier={videoReference?.identifier}
user={channelName}
jsonId={id}
poster={videoCover || ""}
nextVideo={nextVideo}
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}
/>
)}
<VideoPlayer
name={videoReference?.name}
service={videoReference?.service}
identifier={videoReference?.identifier}
user={channelName}
jsonId={id}
poster={videoCover || ""}
nextVideo={nextVideo}
onEnd={onEndVideo}
autoPlay={doAutoPlay}
ref={containerRef}
videoStyles={{
videoContainer: { aspectRatio: "16 / 9" },
video: { aspectRatio: "16 / 9" },
}}
/>
</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 && (
<CommentSection
postId={videoData?.id || ""}
postName={channelName || ""}
{playlistData && (
<Playlists
playlistData={playlistData}
currentVideoIdentifier={videoData?.id}
onClick={getVideoData}
/>
)}
</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>
);
};

View File

@ -1,75 +1,22 @@
import { Avatar, Box, useTheme } from "@mui/material";
import { FollowButton } from "../../../components/common/ContentButtons/FollowButton.tsx";
import { SubscribeButton } from "../../../components/common/ContentButtons/SubscribeButton.tsx";
import { RootState } from "../../../state/store.ts";
import {
AuthorTextComment,
StyledCardColComment,
StyledCardHeaderComment,
} from "./VideoContent-styles.tsx";
import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { Box, SxProps, Theme, useMediaQuery } from "@mui/material";
import { smallScreenSizeString } from "../../../constants/Misc.ts";
import { ChannelButtons } from "./ChannelButtons.tsx";
import { ChannelName, ChannelParams } from "./ChannelName.tsx";
import { Spacer } from "./VideoContent-styles.tsx";
export interface ChannelActionsParams {
channelName: string;
}
export const ChannelActions = ({ channelName }: ChannelActionsParams) => {
const navigate = useNavigate();
const theme = useTheme();
const userName = useSelector((state: RootState) => state.auth.user?.name);
export const ChannelActions = ({ channelName, sx }: ChannelParams) => {
const boxSX: SxProps<Theme> = {
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
rowGap: "10px",
columnGap: "20px",
};
return (
<Box>
<StyledCardHeaderComment
sx={{
"& .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 sx={{ ...boxSX, ...sx }}>
<ChannelName channelName={channelName} />
<ChannelButtons channelName={channelName} />
</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 { FollowButton } from "../../../components/common/ContentButtons/FollowButton.tsx";
import DownloadIcon from "@mui/icons-material/Download";
import { Box, SxProps, Theme, useMediaQuery } from "@mui/material";
import { useMemo } from "react";
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 FileElement from "../../../components/common/FileElement.tsx";
import { videoRefType } from "../../../components/common/VideoPlayer/VideoPlayer.tsx";
import { titleFormatterOnSave } from "../../../constants/Misc.ts";
import { useFetchSuperLikes } from "../../../hooks/useFetchSuperLikes.tsx";
import DownloadIcon from "@mui/icons-material/Download";
import { RootState } from "../../../state/store.ts";
import {
smallScreenSizeString,
titleFormatterOnSave,
} from "../../../constants/Misc.ts";
import { ChannelActions } from "./ChannelActions.tsx";
import {
AuthorTextComment,
FileAttachmentContainer,
FileAttachmentFont,
StyledCardColComment,
StyledCardHeaderComment,
} from "./VideoContent-styles.tsx";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { useMemo, useState } from "react";
export interface VideoActionsBarProps {
channelName: string;
@ -82,11 +75,12 @@ export const VideoActionsBar = ({
return (
<Box
sx={{
width: "80%",
display: "grid",
gridTemplateColumns: "1fr 1fr",
marginTop: "15px",
display: "flex",
flexDirection: "row",
alignItems: "center",
flexWrap: "wrap",
gap: "20px",
...sx,
}}
>
@ -95,6 +89,8 @@ export const VideoActionsBar = ({
sx={{
display: "flex",
flexDirection: "row",
height: "100%",
alignItems: "center",
}}
>
{videoData && (
@ -110,28 +106,30 @@ export const VideoActionsBar = ({
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>
{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>
);
};

View File

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

View File

@ -1,12 +1,18 @@
import { Box, Typography } from "@mui/material";
import React from "react";
import { Box, Typography, useMediaQuery } from "@mui/material";
import React, { useEffect, useState } from "react";
import DeletedVideo from "../../../assets/img/DeletedVideo.jpg";
import { CommentSection } from "../../../components/common/Comments/CommentSection.tsx";
import { SuperLikesSection } from "../../../components/common/SuperLikesList/SuperLikesSection.tsx";
import { DisplayHtml } from "../../../components/common/TextEditor/DisplayHtml.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 { formatDate } from "../../../utils/time.ts";
import { VideoActionsBar } from "./VideoActionsBar.tsx";
@ -18,6 +24,7 @@ import {
VideoPlayerContainer,
VideoTitle,
} from "./VideoContent-styles.tsx";
import { useSignal } from "@preact/signals-react";
export const VideoContent = () => {
const {
@ -39,18 +46,47 @@ export const VideoContent = () => {
superLikeList,
setSuperLikeList,
} = 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 (
<>
<Box
sx={{
display: "flex",
flexDirection: "column",
padding: "0px 10px 0px 5%",
padding: `0px 0px 0px ${isScreenSmall ? "5px" : "2%"}`,
width: "100%",
}}
onClick={focusVideo}
>
{videoReference ? (
<VideoPlayerContainer>
<VideoPlayerContainer
sx={{
width: `${videoWidth}%`,
marginLeft: "0%",
}}
>
<VideoPlayer
name={videoReference?.name}
service={videoReference?.service}
@ -77,64 +113,61 @@ export const VideoContent = () => {
<Box sx={{ width: "55vw", aspectRatio: "16/9" }}></Box>
)}
<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
channelName={channelName}
videoData={videoData}
setSuperLikeList={setSuperLikeList}
superLikeList={superLikeList}
videoReference={videoReference}
sx={{ width: "calc(100% - 5px)" }}
/>
<Box
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" />
<Spacer height="15px" />
{videoData?.fullDescription && (
<Box
sx={{
background: "#333333",
borderRadius: "5px",
padding: "5px",
width: "70%",
width: "95%",
cursor: !descriptionHeight
? "default"
: isExpandedDescription

View File

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

View File

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

View File

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

View File

@ -1,10 +1,15 @@
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 LazyLoad from "../../components/common/LazyLoad";
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 { FiltersCol, VideoManagerRow } from "./Components/VideoList-styles.tsx";
import VideoList from "./Components/VideoList.tsx";
@ -29,79 +34,77 @@ export const Home = ({ mode }: HomeProps) => {
paddingLeft: "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 (
<>
<Grid container sx={{ width: "100%" }}>
<Box sx={{ ...homeBaseSX, ...homeColumns }}>
<SearchSidebar onSearch={getVideosHandler} />
<Grid item xs={12} md={10} lg={7} xl={8} sm={9}>
<VideoManagerRow>
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
marginTop: "20px",
}}
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<TabContext value={tabValue}>
<TabList
onChange={changeTab}
textColor={"secondary"}
indicatorColor={"secondary"}
centered={false}
>
<SubtitleContainer
sx={{
justifyContent: "flex-start",
paddingLeft: "15px",
width: "100%",
maxWidth: "1400px",
}}
></SubtitleContainer>
<TabContext value={tabValue}>
<TabList
onChange={changeTab}
textColor={"secondary"}
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}>
<Tab label="All" value={"all"} sx={tabSX} />
<Tab label="Subscriptions" value={"subscriptions"} sx={tabSX} />
</TabList>
<TabPanel value={"all"} sx={tabPaneSX}>
<VideoList videos={videos} />
<LazyLoad
onLoadMore={getVideosHandler}
isLoading={isLoading}
></LazyLoad>
</TabPanel>
<TabPanel value={"subscriptions"} sx={tabPaneSX}>
{filteredSubscriptionList.length > 0 ? (
<>
<VideoList videos={videos} />
<LazyLoad
onLoadMore={getVideosHandler}
isLoading={isLoading}
></LazyLoad>
</TabPanel>
<TabPanel value={"subscriptions"} sx={tabPaneSX}>
{filteredSubscriptionList.length > 0 ? (
<>
<VideoList videos={videos} />
<LazyLoad
onLoadMore={getVideosHandler}
isLoading={isLoading}
></LazyLoad>
</>
) : !isLoading ? (
<div style={{ textAlign: "center" }}>
You have no subscriptions
</div>
) : (
<></>
)}
</TabPanel>
</TabContext>
</Box>
</VideoManagerRow>
</Grid>
<FiltersCol item xs={0} lg={3} xl={2}>
<ListSuperLikeContainer />
</FiltersCol>
</Grid>
</>
) : (
!isLoading && (
<div style={{ textAlign: "center" }}>
You have no subscriptions
</div>
)
)}
</TabPanel>
</TabContext>
</Box>
<ListSuperLikeContainer />
</Box>
</>
);
};

View File

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