feat: init side drawer component

This commit is contained in:
Timothy Z. 2024-12-06 14:39:16 +02:00
parent 2f8a5df83d
commit a726ffb60d
7 changed files with 268 additions and 67 deletions

View file

@ -35,9 +35,9 @@ const ControlBar = ({
onVolumeChangeRequested,
onSeekRequested,
onToggleSubtitlesMenu,
onToggleInfoMenu,
// onToggleInfoMenu,
onToggleSpeedMenu,
onToggleVideosMenu,
onToggleSideDrawer,
onToggleOptionsMenu,
onToggleStatisticsMenu,
...props
@ -48,9 +48,9 @@ const ControlBar = ({
const onSubtitlesButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.subtitlesMenuClosePrevented = true;
}, []);
const onInfoButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.infoMenuClosePrevented = true;
}, []);
// const onInfoButtonMouseDown = React.useCallback((event) => {
// event.nativeEvent.infoMenuClosePrevented = true;
// }, []);
const onSpeedButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.speedMenuClosePrevented = true;
}, []);
@ -151,9 +151,9 @@ const ControlBar = ({
<Button className={classnames(styles['control-bar-button'], { 'disabled': playbackSpeed === null })} tabIndex={-1} onMouseDown={onSpeedButtonMouseDown} onClick={onToggleSpeedMenu}>
<Icon className={styles['icon']} name={'speed'} />
</Button>
<Button className={classnames(styles['control-bar-button'], { 'disabled': metaItem === null || metaItem.type !== 'Ready' })} tabIndex={-1} onMouseDown={onInfoButtonMouseDown} onClick={onToggleInfoMenu}>
{/* <Button className={classnames(styles['control-bar-button'], { 'disabled': metaItem === null || metaItem.type !== 'Ready' })} tabIndex={-1} onMouseDown={onInfoButtonMouseDown} onClick={onToggleInfoMenu}>
<Icon className={styles['icon']} name={'about'} />
</Button>
</Button> */}
<Button className={classnames(styles['control-bar-button'], { 'disabled': !chromecastServiceActive })} tabIndex={-1} onClick={onChromecastButtonClick}>
<Icon className={styles['icon']} name={'cast'} />
</Button>
@ -162,7 +162,7 @@ const ControlBar = ({
</Button>
{
metaItem?.content?.videos?.length > 0 ?
<Button className={styles['control-bar-button']} tabIndex={-1} onMouseDown={onVideosButtonMouseDown} onClick={onToggleVideosMenu}>
<Button className={styles['control-bar-button']} tabIndex={-1} onMouseDown={onVideosButtonMouseDown} onClick={onToggleSideDrawer}>
<Icon className={styles['icon']} name={'episodes'} />
</Button>
:
@ -202,7 +202,7 @@ ControlBar.propTypes = {
onToggleSubtitlesMenu: PropTypes.func,
onToggleInfoMenu: PropTypes.func,
onToggleSpeedMenu: PropTypes.func,
onToggleVideosMenu: PropTypes.func,
onToggleSideDrawer: PropTypes.func,
onToggleOptionsMenu: PropTypes.func,
onToggleStatisticsMenu: PropTypes.func,
};

View file

@ -15,9 +15,7 @@ const Error = require('./Error');
const ControlBar = require('./ControlBar');
const NextVideoPopup = require('./NextVideoPopup');
const StatisticsMenu = require('./StatisticsMenu');
const InfoMenu = require('./InfoMenu');
const OptionsMenu = require('./OptionsMenu');
const VideosMenu = require('./VideosMenu');
const SubtitlesMenu = require('./SubtitlesMenu');
const SpeedMenu = require('./SpeedMenu');
const usePlayer = require('./usePlayer');
@ -26,6 +24,8 @@ const useStatistics = require('./useStatistics');
const useVideo = require('./useVideo');
const styles = require('./styles');
const Video = require('./Video');
const { default: SideDrawer } = require('./SideDrawer/SideDrawer');
const { default: Icon } = require('@stremio/stremio-icons/react');
const Player = ({ urlParams, queryParams }) => {
const { t } = useTranslation();
@ -54,27 +54,24 @@ const Player = ({ urlParams, queryParams }) => {
const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] = useBinaryState(false);
const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false);
const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] = useBinaryState(false);
const [statisticsMenuOpen, , closeStatisticsMenu, toggleStatisticsMenu] = useBinaryState(false);
const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false);
const [sideDrawerOpen, openSideDrawer, closeSideDrawer, toggleSideDrawer] = useBinaryState(false);
const menusOpen = React.useMemo(() => {
return optionsMenuOpen || subtitlesMenuOpen || infoMenuOpen || speedMenuOpen || videosMenuOpen || statisticsMenuOpen;
}, [optionsMenuOpen, subtitlesMenuOpen, infoMenuOpen, speedMenuOpen, videosMenuOpen, statisticsMenuOpen]);
return optionsMenuOpen || subtitlesMenuOpen || speedMenuOpen || statisticsMenuOpen;
}, [optionsMenuOpen, subtitlesMenuOpen, speedMenuOpen, statisticsMenuOpen]);
const closeMenus = React.useCallback(() => {
closeOptionsMenu();
closeSubtitlesMenu();
closeInfoMenu();
closeSpeedMenu();
closeVideosMenu();
closeStatisticsMenu();
}, []);
const overlayHidden = React.useMemo(() => {
return immersed && !casting && video.state.paused !== null && !video.state.paused && !menusOpen && !nextVideoPopupOpen;
return immersed && !casting && video.state.paused !== null && !video.state.paused && !menusOpen && !nextVideoPopupOpen && !sideDrawerOpen;
}, [immersed, casting, video.state.paused, menusOpen, nextVideoPopupOpen]);
const nextVideoPopupDismissed = React.useRef(false);
@ -237,15 +234,9 @@ const Player = ({ urlParams, queryParams }) => {
if (!event.nativeEvent.subtitlesMenuClosePrevented) {
closeSubtitlesMenu();
}
if (!event.nativeEvent.infoMenuClosePrevented) {
closeInfoMenu();
}
if (!event.nativeEvent.speedMenuClosePrevented) {
closeSpeedMenu();
}
if (!event.nativeEvent.videosMenuClosePrevented) {
closeVideosMenu();
}
if (!event.nativeEvent.statisticsMenuClosePrevented) {
closeStatisticsMenu();
}
@ -412,19 +403,19 @@ const Player = ({ urlParams, queryParams }) => {
}
}, [video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
React.useEffect(() => {
if (player.metaItem === null || player.metaItem.type !== 'Ready') {
closeInfoMenu();
closeVideosMenu();
}
}, [player.metaItem]);
React.useEffect(() => {
if (video.state.playbackSpeed === null) {
closeSpeedMenu();
}
}, [video.state.playbackSpeed]);
React.useEffect(() => {
if (sideDrawerOpen) {
closeMenus();
setImmersed(true);
}
}, [sideDrawerOpen]);
React.useEffect(() => {
const toastFilter = (item) => item?.dataset?.type === 'CoreEvent';
toast.addFilter(toastFilter);
@ -521,7 +512,7 @@ const Player = ({ urlParams, queryParams }) => {
case 'KeyI': {
closeMenus();
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
toggleInfoMenu();
toggleSideDrawer();
}
break;
@ -534,14 +525,6 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
case 'KeyV': {
closeMenus();
if (player.metaItem !== null && player.metaItem.type === 'Ready' && player.metaItem?.content?.videos?.length > 0) {
toggleVideosMenu();
}
break;
}
case 'KeyD': {
closeMenus();
if (streamingServer.statistics !== null && streamingServer.statistics.type !== 'Err' && player.selected && typeof player.selected.stream.infoHash === 'string' && typeof player.selected.stream.fileIdx === 'number') {
@ -563,11 +546,11 @@ const Player = ({ urlParams, queryParams }) => {
};
const onWheel = ({ deltaY }) => {
if (deltaY > 0) {
if (!menusOpen && video.state.volume !== null) {
if (!menusOpen && !sideDrawerOpen && video.state.volume !== null) {
onVolumeChangeRequested(video.state.volume - 5);
}
} else {
if (!menusOpen && video.state.volume !== null) {
if (!menusOpen && !sideDrawerOpen && video.state.volume !== null) {
onVolumeChangeRequested(video.state.volume + 5);
}
}
@ -582,7 +565,7 @@ const Player = ({ urlParams, queryParams }) => {
window.removeEventListener('keyup', onKeyUp);
window.removeEventListener('wheel', onWheel);
};
}, [player.metaItem, player.selected, streamingServer.statistics, settings.seekTimeDuration, settings.seekShortTimeDuration, routeFocused, menusOpen, nextVideoPopupOpen, video.state.paused, video.state.time, video.state.volume, video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, video.state.playbackSpeed, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu, toggleStatisticsMenu]);
}, [player.metaItem, player.selected, streamingServer.statistics, settings.seekTimeDuration, settings.seekShortTimeDuration, routeFocused, menusOpen, nextVideoPopupOpen, video.state.paused, video.state.time, video.state.volume, video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, video.state.playbackSpeed, toggleSubtitlesMenu, toggleStatisticsMenu, toggleSideDrawer]);
React.useEffect(() => {
video.events.on('error', onError);
@ -691,10 +674,9 @@ const Player = ({ urlParams, queryParams }) => {
onSeekRequested={onSeekRequested}
onToggleOptionsMenu={toggleOptionsMenu}
onToggleSubtitlesMenu={toggleSubtitlesMenu}
onToggleInfoMenu={toggleInfoMenu}
onToggleSpeedMenu={toggleSpeedMenu}
onToggleVideosMenu={toggleVideosMenu}
onToggleStatisticsMenu={toggleStatisticsMenu}
onToggleSideDrawer={toggleSideDrawer}
onMouseMove={onBarMouseMove}
onMouseOver={onBarMouseMove}
/>
@ -719,6 +701,22 @@ const Player = ({ urlParams, queryParams }) => {
:
null
}
{
player.metaItem !== null && player.metaItem.type === 'Ready' ?
<>
<div className={classnames(styles['layer'], styles['side-drawer-button'], { [styles['open']]: sideDrawerOpen })} onClick={openSideDrawer}>
<Icon name={'chevron-back'} className={styles['icon']} />
</div>
<SideDrawer
metaItem={player.metaItem.content}
seriesInfo={player.seriesInfo}
className={classnames(styles['layer'], styles['side-drawer-layer'], { [styles['open']]: sideDrawerOpen })}
closeSideBar={closeSideDrawer}
sideDrawerOpen={sideDrawerOpen}
/>
</>
: null
}
{
subtitlesMenuOpen ?
<SubtitlesMenu
@ -746,17 +744,6 @@ const Player = ({ urlParams, queryParams }) => {
:
null
}
{
infoMenuOpen ?
<InfoMenu
className={classnames(styles['layer'], styles['menu-layer'])}
stream={player.selected !== null ? player.selected.stream : null}
addon={player.addon}
metaItem={player.metaItem !== null && player.metaItem.type === 'Ready' ? player.metaItem.content : null}
/>
:
null
}
{
speedMenuOpen ?
<SpeedMenu
@ -767,16 +754,6 @@ const Player = ({ urlParams, queryParams }) => {
:
null
}
{
videosMenuOpen ?
<VideosMenu
className={classnames(styles['layer'], styles['menu-layer'])}
metaItem={player.metaItem !== null && player.metaItem.type === 'Ready' ? player.metaItem.content : null}
seriesInfo={player.seriesInfo}
/>
:
null
}
{
optionsMenuOpen ?
<OptionsMenu

View file

@ -0,0 +1,32 @@
// Copyright (C) 2017-2024 Smart code 203358507
.overlay {
display: none;
&.open {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.2);
z-index: 1;
}
}
.side-drawer {
position: relative;
height: 100%;
z-index: 2;
.info {
overflow: hidden;
padding: 1rem;
}
.content {
overflow: auto;
max-height: 58%;
}
}

View file

@ -0,0 +1,106 @@
import React from 'react';
import MetaPreview from 'stremio/common/MetaPreview/MetaPreview';
import Video from '../../MetaDetails/VideosList/Video/Video';
import styles from './SideDrawer.less';
import classNames from 'classnames';
import { CONSTANTS } from 'stremio/common';
import SeasonsBar from 'stremio/routes/MetaDetails/VideosList/SeasonsBar';
type Props = {
seriesInfo: any;
metaItem: MetaItem;
className?: string;
closeSideBar: () => void;
sideDrawerOpen: boolean;
};
const SideDrawer = ({ seriesInfo, className, closeSideBar, sideDrawerOpen, ...props }: Props) => {
const [season, setSeason] = React.useState<number>(seriesInfo?.season);
const metaItem = React.useMemo(() => {
return props.metaItem !== null && Array.isArray(props.metaItem.videos) && seriesInfo ?
{
...props.metaItem,
links: props.metaItem.links.filter(({ category }) => category === CONSTANTS.SHARE_LINK_CATEGORY)
}
:
props.metaItem;
}, [props.metaItem]);
const videos = React.useMemo(() => {
return props.metaItem && Array.isArray(props.metaItem.videos) ?
props.metaItem.videos.filter((video) => video.season === season)
:
props.metaItem.videos;
}, [props.metaItem, season]);
const seasons = React.useMemo(() => {
return props.metaItem && props.metaItem.videos
.map(({ season }) => season)
.filter((season, index, seasons) => {
return season !== null && season !== undefined &&
!isNaN(season) &&
typeof season === 'number' &&
seasons.indexOf(season) === index;
})
.sort((a, b) => (a || Number.MAX_SAFE_INTEGER) - (b || Number.MAX_SAFE_INTEGER));
}, [props.metaItem.videos]);
const seasonOnSelect = React.useCallback((event: { value: string }) => {
setSeason(parseInt(event.value));
}, []);
return (
<>
<div className={classNames(styles['overlay'], { [styles['open']]: sideDrawerOpen })} onClick={closeSideBar} />
<div className={classNames(styles['side-drawer'], className)}>
<div className={styles['info']}>
{
metaItem !== null ?
<MetaPreview
compact={true}
name={metaItem.name}
logo={metaItem.logo}
runtime={metaItem.runtime}
releaseInfo={metaItem.releaseInfo}
released={metaItem.released}
description={metaItem.description}
links={metaItem.links}
/>
:
null
}
</div>
{
videos !== null && seriesInfo ?
<>
<SeasonsBar
season={season}
seasons={seasons}
onSelect={seasonOnSelect}
/>
<div className={styles['content']}>
{videos.map((video, index) => (
<Video
key={index}
className={styles['video']}
id={video.id}
title={video.title}
thumbnail={video.thumbnail}
episode={video.episode}
released={video.released}
upcoming={video.upcoming}
watched={video.watched}
progress={video.progress}
deepLinks={video.deepLinks}
scheduled={video.scheduled}
/>
))}
</div>
</>
: null
}
</div>
</>
);
};
export default SideDrawer;

View file

@ -1,6 +1,7 @@
// Copyright (C) 2017-2023 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
@import (reference) '~stremio/common/screen-sizes.less';
:import('~stremio/common/Slider/styles.less') {
active-slider-within: active-slider-within;
@ -112,5 +113,85 @@ html:not(.active-slider-within) {
backdrop-filter: blur(15px);
overflow: auto;
}
&.side-drawer-button {
right: -4rem;
top: 50%;
left: initial;
bottom: initial;
height: 12.5rem;
width: 7.5rem;
transform: translateY(-50%);
display: flex;
justify-content: center;
align-items: center;
background-color: var(--modal-background-color);
cursor: pointer;
border-top-left-radius: 50%;
border-bottom-left-radius: 50%;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
.icon {
width: 2.5rem;
height: 2.5rem;
color: var(--primary-foreground-color);
opacity: 0.6;
margin-right: 4rem;
transition: all 0.3s ease-in-out;
}
&.open {
display: none;
}
&:hover {
.icon {
opacity: 1;
}
}
}
&.side-drawer-layer {
top: 0;
right: 0;
left: initial;
bottom: initial;
max-width: 30%;
height: 100vh;
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
background-color: var(--modal-background-color);
box-shadow: 0 1.35rem 2.7rem var(--color-background-dark5-40),
0 1.1rem 0.85rem var(--color-background-dark5-20);
backdrop-filter: blur(15px);
overflow: visible;
padding: 1rem;
z-index: 1;
transition: transform 0.3s ease-in-out;
transform: translateX(100%);
&.open {
transform: translateX(0);
}
@media screen and (max-width: @small) {
max-width: 40%;
}
@media screen and (max-width: @xsmall) {
max-width: 50%;
}
@media screen and (max-width: @xxsmall) {
max-width: 60%;
}
@media screen and (max-width: @minimum) {
max-width: 70%;
}
}
}
}

View file

@ -15,7 +15,7 @@ type MetaItemPreview = {
posterShape: PosterShape,
releaseInfo: string | null,
runtime: string | null,
released: string | null,
released: Date | null | undefined,
trailerStreams: TrailerStream[],
links: Link[],
behaviorHints: BehaviorHints,

View file

@ -14,4 +14,9 @@ type Video = {
episode?: number,
streams: Stream[],
trailerStreams: TrailerStream[],
watched: boolean,
progress: number,
upcoming: boolean,
deepLinks: VideoDeepLinks,
scheduled: boolean,
};