3
0
mirror of https://github.com/Qortal/q-tube.git synced 2025-02-14 11:15:52 +00:00

Subscriptions to channels added

Filter added that removes characters that Operating Systems don't allow in filenames when saving file

VideoList-styles.tsx uses Radio button instead of Checkbox for main page video/playlist filter

Video player has aspect ratio of 16 / 9, doesn't put controls over video, and removes controls if mouse exits video when in fullscreen (but only when playing for some reason)

Created new redux slice called settingsSlice.ts. It is used to store settings that are saved to disk automatically

Home page remembers whether you were looking for videos or playlists

VideoPlayer.tsx remembers last playbackRate when video is loaded, this is persistent.

Videos reload when clicking on the videos or playlists filter type (user doesn't have to click Search button after changing the type)

<FiltersTitle> for type and categories have been removed due to being redundant and confusing.
This commit is contained in:
Qortal Dev 2024-02-10 16:55:24 -07:00
parent bc096b5a66
commit 7e502a3a47
10 changed files with 130 additions and 163 deletions

View File

@ -2,7 +2,7 @@ import { Button, ButtonProps } from "@mui/material";
import { MouseEvent, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../state/store.ts";
import { subscribe, unSubscribe } from "../../state/features/videoSlice.ts";
import { subscribe, unSubscribe } from "../../state/features/persistSlice.ts";
interface SubscribeButtonProps extends ButtonProps {
name: string;
@ -10,13 +10,13 @@ interface SubscribeButtonProps extends ButtonProps {
export const SubscribeButton = ({ name, ...props }: SubscribeButtonProps) => {
const dispatch = useDispatch();
const select = useSelector((state: RootState) => {
return state.video;
const persistSelector = useSelector((state: RootState) => {
return state.persist;
});
const [isSubscribed, setIsSubscribed] = useState<boolean>(false);
useEffect(() => {
setIsSubscribed(select.subscriptionList.includes(name));
setIsSubscribed(persistSelector.subscriptionList.includes(name));
}, []);
const subscribeToRedux = () => {
@ -37,7 +37,6 @@ export const SubscribeButton = ({ name, ...props }: SubscribeButtonProps) => {
const verticalPadding = "3px";
const horizontalPadding = "8px";
const buttonStyle = {
...props.sx,
fontSize: "15px",
fontWeight: "700",
paddingTop: verticalPadding,
@ -45,7 +44,8 @@ export const SubscribeButton = ({ name, ...props }: SubscribeButtonProps) => {
paddingLeft: horizontalPadding,
paddingRight: horizontalPadding,
borderRadius: 28,
height: "40px",
height: "45px",
...props.sx,
};
return (
<Button

View File

@ -26,6 +26,7 @@ import {
VideoElement,
} from "./VideoPlayer-styles.ts";
import CSS from "csstype";
import { setReduxPlaybackRate } from "../../../state/features/persistSlice.ts";
interface VideoPlayerProps {
src?: string;
@ -59,6 +60,8 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
autoPlay,
style = {},
}) => {
const videoSelector = useSelector((state: RootState) => state.video);
const persistSelector = useSelector((state: RootState) => state.persist);
const dispatch = useDispatch();
const videoRef = useRef<HTMLVideoElement | null>(null);
const [playing, setPlaying] = useState(false);
@ -70,14 +73,15 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
const [canPlay, setCanPlay] = useState(false);
const [startPlay, setStartPlay] = useState(false);
const [isMobileView, setIsMobileView] = useState(false);
const [playbackRate, setPlaybackRate] = useState(1);
const [playbackRate, setPlaybackRate] = useState(
persistSelector.playbackRate
);
const [anchorEl, setAnchorEl] = useState(null);
const [showControlsFullScreen, setShowControlsFullScreen] =
useState<boolean>(true);
const videoPlaying = useSelector(
(state: RootState) => state.global.videoPlaying
);
const settingsState = useSelector((state: RootState) => state.settings);
const { downloads } = useSelector((state: RootState) => state.global);
const reDownload = useRef<boolean>(false);
@ -109,8 +113,10 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
const updatePlaybackRate = (newSpeed: number) => {
if (videoRef.current) {
if (newSpeed > maxSpeed || newSpeed < minSpeed) newSpeed = minSpeed;
videoRef.current.playbackRate = newSpeed;
videoRef.current.playbackRate = playbackRate;
setPlaybackRate(newSpeed);
dispatch(setReduxPlaybackRate(newSpeed));
}
};
@ -282,6 +288,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
const handleCanPlay = () => {
setIsLoading(false);
setCanPlay(true);
updatePlaybackRate(playbackRate);
};
const getSrc = React.useCallback(async () => {
@ -767,18 +774,16 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
onCanPlay={handleCanPlay}
onMouseEnter={e => {
setShowControlsFullScreen(true);
console.log("entering video, fullscreen is: ", isFullscreen);
}}
onMouseLeave={e => {
setShowControlsFullScreen(false);
console.log("leaving video, fullscreen is: ", isFullscreen);
}}
preload="metadata"
style={
startPlay
? {
...customStyle,
objectFit: settingsState.stretchVideoSetting,
objectFit: persistSelector.stretchVideoSetting,
height: "calc(100% - 80px)",
}
: { ...customStyle, height: "100%" }

View File

@ -37,7 +37,7 @@ export const useFetchVideos = () => {
);
const subscriptions = useSelector(
(state: RootState) => state.video.subscriptionList
(state: RootState) => state.persist.subscriptionList
);
const checkAndUpdateVideo = React.useCallback(

View File

@ -1,10 +1,8 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import React, { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "../../state/store";
import {
Avatar,
Box,
Button,
FormControl,
@ -16,69 +14,38 @@ import {
Select,
SelectChangeEvent,
Tab,
Tabs,
Tooltip,
Typography,
useTheme,
} from "@mui/material";
import { useFetchVideos } from "../../hooks/useFetchVideos";
import LazyLoad from "../../components/common/LazyLoad";
import {
BlockIconContainer,
BottomParent,
FilterSelect,
FiltersCheckbox,
FiltersCol,
FiltersContainer,
FiltersRow,
FiltersSubContainer,
FiltersTitle,
IconsBox,
NameContainer,
VideoCardCol,
ProductManagerRow,
VideoCardContainer,
VideoCard,
VideoCardName,
VideoCardTitle,
VideoContainer,
VideoUploadDate,
FiltersRadioButton,
} from "./VideoList-styles";
import ResponsiveImage from "../../components/ResponsiveImage";
import { formatDate, formatTimestampSeconds } from "../../utils/time";
import { Subtitle, SubtitleContainer } from "./Home-styles";
import { ExpandMoreSVG } from "../../assets/svgs/ExpandMoreSVG";
import { SubtitleContainer } from "./Home-styles";
import {
addVideos,
blockUser,
changeFilterType,
changeSelectedCategoryVideos,
changeSelectedSubCategoryVideos,
changefilterName,
changefilterSearch,
clearVideoList,
setEditPlaylist,
setEditVideo,
} from "../../state/features/videoSlice";
import { changeFilterType } from "../../state/features/persistSlice.ts";
import { categories, subCategories } from "../../constants/Categories.ts";
import { Playlists } from "../../components/Playlists/Playlists";
import { PlaylistSVG } from "../../assets/svgs/PlaylistSVG";
import BlockIcon from "@mui/icons-material/Block";
import EditIcon from "@mui/icons-material/Edit";
import { ListSuperLikeContainer } from "../../components/common/ListSuperLikes/ListSuperLikeContainer.tsx";
import { VideoCardImageContainer } from "./VideoCardImageContainer";
import { SubscribeButton } from "../../components/common/SubscribeButton.tsx";
import { TabContext, TabList, TabPanel } from "@mui/lab";
import VideoList from "./VideoList.tsx";
import { allTabValue, subscriptionTabValue } from "../../constants/Misc.ts";
import { setHomePageSelectedTab } from "../../state/features/settingsSlice.ts";
import { setHomePageSelectedTab } from "../../state/features/persistSlice.ts";
interface HomeProps {
mode?: string;
}
export const Home = ({ mode }: HomeProps) => {
const theme = useTheme();
const prevVal = useRef("");
const isFiltering = useSelector(
(state: RootState) => state.video.isFiltering
@ -86,34 +53,43 @@ export const Home = ({ mode }: HomeProps) => {
const filterValue = useSelector(
(state: RootState) => state.video.filterValue
);
const persistSelector = useSelector((state: RootState) => state.persist);
const filterType = useSelector(
(state: RootState) => state.persist.filterType
);
const filterSearch = useSelector(
(state: RootState) => state.video.filterSearch
);
const filterName = useSelector((state: RootState) => state.video.filterName);
const selectedCategoryVideos = useSelector(
(state: RootState) => state.video.selectedCategoryVideos
);
const { videos: globalVideos } = useSelector(
(state: RootState) => state.video
);
const settingsState = useSelector((state: RootState) => state.settings);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [tabValue, setTabValue] = useState<string>(settingsState.selectedTab);
const [tabValue, setTabValue] = useState<string>(persistSelector.selectedTab);
const tabFontSize = "20px";
const filterType = useSelector((state: RootState) => state.video.filterType);
const setFilterType = payload => {
dispatch(changeFilterType(payload));
};
const filterSearch = useSelector(
(state: RootState) => state.video.filterSearch
);
useEffect(() => {
// Makes displayed videos reload when switching filter type. Removes need to click Search button after changing type
getVideosHandler(true);
}, [filterType]);
const setFilterSearch = payload => {
dispatch(changefilterSearch(payload));
};
const filterName = useSelector((state: RootState) => state.video.filterName);
const setFilterName = payload => {
dispatch(changefilterName(payload));
};
const selectedCategoryVideos = useSelector(
(state: RootState) => state.video.selectedCategoryVideos
);
const setSelectedCategoryVideos = payload => {
dispatch(changeSelectedCategoryVideos(payload));
};
@ -133,19 +109,8 @@ export const Home = ({ mode }: HomeProps) => {
const isFilterMode = useRef(false);
const firstFetch = useRef(false);
const afterFetch = useRef(false);
const isFetchingFiltered = useRef(false);
const isFetching = useRef(false);
const countNewVideos = useSelector(
(state: RootState) => state.video.countNewVideos
);
const videoSelector = useSelector((state: RootState) => state.video);
const { videos: globalVideos } = useSelector(
(state: RootState) => state.video
);
const { getVideos, getNewVideos, checkNewVideos, getVideosFiltered } =
useFetchVideos();
@ -357,16 +322,9 @@ export const Home = ({ mode }: HomeProps) => {
fontSize: "18px",
}}
/>
<FiltersTitle>
Categories
<ExpandMoreSVG
color={theme.palette.text.primary}
height={"22"}
width={"22"}
/>
</FiltersTitle>
<FiltersSubContainer>
<FormControl sx={{ width: "100%" }}>
<FormControl sx={{ width: "100%", marginTop: "30px" }}>
<Box
sx={{
display: "flex",
@ -466,14 +424,6 @@ export const Home = ({ mode }: HomeProps) => {
</Box>
</FormControl>
</FiltersSubContainer>
<FiltersTitle>
Type
<ExpandMoreSVG
color={theme.palette.text.primary}
height={"22"}
width={"22"}
/>
</FiltersTitle>
<FiltersSubContainer>
<FiltersRow>
Videos
@ -551,7 +501,7 @@ export const Home = ({ mode }: HomeProps) => {
sx={{ fontSize: tabFontSize }}
/>
<Tab
label="Subsciptions"
label="Subscriptions"
value={subscriptionTabValue}
sx={{ fontSize: tabFontSize }}
/>
@ -564,11 +514,19 @@ export const Home = ({ mode }: HomeProps) => {
></LazyLoad>
</TabPanel>
<TabPanel value={subscriptionTabValue} sx={{ width: "100%" }}>
<VideoList videos={videos} />
<LazyLoad
onLoadMore={getVideosHandler}
isLoading={isLoading}
></LazyLoad>
{persistSelector.subscriptionList.length > 0 ? (
<>
<VideoList videos={videos} />
<LazyLoad
onLoadMore={getVideosHandler}
isLoading={isLoading}
></LazyLoad>
</>
) : (
<div style={{ textAlign: "center" }}>
You have no subscriptions
</div>
)}
</TabPanel>
</TabContext>
</Box>

View File

@ -196,7 +196,7 @@ export const NameContainer = styled(Box)(({ theme }) => ({
justifyContent: "flex-start",
alignItems: "center",
gap: "10px",
marginBottom: "10px",
marginBottom: "2px",
}));
export const MyStoresCard = styled(Box)(({ theme }) => ({

View File

@ -64,7 +64,7 @@ export const VideoList = ({ videos }: VideoListProps) => {
return (
<VideoCardContainer>
{videos.map((video: any, index: number) => {
{videos.map((video: any) => {
const fullId = video ? `${video.id}-${video.user}` : undefined;
const existingVideo = hashMapVideos[fullId];
let hasHash = false;
@ -269,9 +269,11 @@ export const VideoList = ({ videos }: VideoListProps) => {
</VideoCardName>
</NameContainer>
{videoObj?.created && (
<VideoUploadDate>
{formatDate(videoObj.created)}
</VideoUploadDate>
<Box sx={{ flexDirection: "row", width: "100%" }}>
<VideoUploadDate sx={{ display: "inline" }}>
{formatDate(videoObj.created)}
</VideoUploadDate>
</Box>
)}
</BottomParent>
</VideoCard>

View File

@ -0,0 +1,59 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { subscriptionTabValue } from "../../constants/Misc.ts";
type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down";
interface settingsState {
selectedTab: string;
stretchVideoSetting: StretchVideoType;
filterType: string;
subscriptionList: string[];
playbackRate: number;
}
const initialState: settingsState = {
selectedTab: subscriptionTabValue,
stretchVideoSetting: "contain",
filterType: "videos",
subscriptionList: [],
playbackRate: 1,
};
export const persistSlice = createSlice({
name: "persist",
initialState,
reducers: {
setHomePageSelectedTab: (state, action) => {
state.selectedTab = action.payload;
},
setStretchVideoSetting: (state, action) => {
state.stretchVideoSetting = action.payload;
},
subscribe: (state, action: PayloadAction<string>) => {
const currentSubscriptions = state.subscriptionList;
if (!currentSubscriptions.includes(action.payload)) {
state.subscriptionList = [...currentSubscriptions, action.payload];
}
},
unSubscribe: (state, action) => {
state.subscriptionList = state.subscriptionList.filter(
item => item !== action.payload
);
},
setReduxPlaybackRate: (state, action) => {
state.playbackRate = action.payload;
},
changeFilterType: (state, action) => {
state.filterType = action.payload;
},
},
});
export const {
setHomePageSelectedTab,
subscribe,
unSubscribe,
setReduxPlaybackRate,
changeFilterType,
} = persistSlice.actions;
export default persistSlice.reducer;

View File

@ -1,30 +0,0 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { subscriptionTabValue } from "../../constants/Misc.ts";
type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down";
interface settingsState {
selectedTab: string;
stretchVideoSetting: StretchVideoType;
}
const initialState: settingsState = {
selectedTab: subscriptionTabValue,
stretchVideoSetting: "contain",
};
export const settingsSlice = createSlice({
name: "settings",
initialState,
reducers: {
setHomePageSelectedTab: (state, action) => {
state.selectedTab = action.payload;
},
setStretchVideoSetting: (state, action) => {
state.stretchVideoSetting = action.payload;
},
},
});
export const { setHomePageSelectedTab } = settingsSlice.actions;
export default settingsSlice.reducer;

View File

@ -8,14 +8,12 @@ interface GlobalState {
countNewVideos: number;
isFiltering: boolean;
filterValue: string;
filterType: string;
filterSearch: string;
filterName: string;
selectedCategoryVideos: any;
selectedSubCategoryVideos: any;
editVideoProperties: any;
editPlaylistProperties: any;
subscriptionList: string[];
}
const initialState: GlobalState = {
@ -26,14 +24,12 @@ const initialState: GlobalState = {
countNewVideos: 0,
isFiltering: false,
filterValue: "",
filterType: "videos",
filterSearch: "",
filterName: "",
selectedCategoryVideos: null,
selectedSubCategoryVideos: null,
editVideoProperties: null,
editPlaylistProperties: null,
subscriptionList: [],
};
export interface Video {
@ -62,9 +58,7 @@ export const videoSlice = createSlice({
setEditPlaylist: (state, action) => {
state.editPlaylistProperties = action.payload;
},
changeFilterType: (state, action) => {
state.filterType = action.payload;
},
changefilterSearch: (state, action) => {
state.filterSearch = action.payload;
},
@ -174,17 +168,6 @@ export const videoSlice = createSlice({
state.videos = state.videos.filter(item => item.user !== username);
},
subscribe: (state, action: PayloadAction<string>) => {
const currentSubscriptions = state.subscriptionList;
if (!currentSubscriptions.includes(action.payload)) {
state.subscriptionList = [...currentSubscriptions, action.payload];
}
},
unSubscribe: (state, action) => {
state.subscriptionList = state.subscriptionList.filter(
item => item !== action.payload
);
},
},
});
@ -205,7 +188,6 @@ export const {
setIsFiltering,
setFilterValue,
clearVideoList,
changeFilterType,
changefilterSearch,
changefilterName,
changeSelectedCategoryVideos,
@ -214,8 +196,6 @@ export const {
setEditVideo,
setEditPlaylist,
addtoHashMapSuperlikes,
subscribe,
unSubscribe,
} = videoSlice.actions;
export default videoSlice.reducer;

View File

@ -3,7 +3,7 @@ import notificationsReducer from "./features/notificationsSlice";
import authReducer from "./features/authSlice";
import globalReducer from "./features/globalSlice";
import videoReducer from "./features/videoSlice";
import settingsReducer from "./features/settingsSlice";
import settingsReducer from "./features/persistSlice.ts";
import {
persistReducer,
FLUSH,
@ -15,15 +15,8 @@ import {
} from "redux-persist";
import storage from "redux-persist/lib/storage";
const persistVideoConfig = {
key: "video",
version: 1,
storage,
whitelist: ["subscriptionList", "filterType"],
};
const persistSettingsConfig = {
key: "settings",
key: "persist",
version: 1,
storage,
};
@ -32,8 +25,8 @@ const reducer = combineReducers({
notifications: notificationsReducer,
auth: authReducer,
global: globalReducer,
video: persistReducer(persistVideoConfig, videoReducer),
settings: persistReducer(persistSettingsConfig, settingsReducer),
video: videoReducer,
persist: persistReducer(persistSettingsConfig, settingsReducer),
});
export const store = configureStore({