refactor: align player menus with buttons

This commit is contained in:
Tim 2024-02-14 04:03:06 +01:00
parent dc180589fc
commit 0ca7e5a53e
47 changed files with 572 additions and 505 deletions

140
package-lock.json generated
View file

@ -46,6 +46,7 @@
"@babel/plugin-proposal-object-rest-spread": "7.16.0",
"@babel/preset-env": "7.16.0",
"@babel/preset-react": "7.16.0",
"@types/classnames": "^2.3.1",
"@types/react": "^18.2.9",
"babel-loader": "8.2.3",
"clean-webpack-plugin": "4.0.0",
@ -63,6 +64,8 @@
"postcss-loader": "6.2.0",
"readdirp": "3.6.0",
"terser-webpack-plugin": "5.2.4",
"ts-loader": "^9.5.1",
"typescript": "^5.3.3",
"webpack": "5.61.0",
"webpack-cli": "4.9.1",
"webpack-dev-server": "^4.7.4",
@ -3085,6 +3088,16 @@
"@types/node": "*"
}
},
"node_modules/@types/classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-zeOWb0JGBoVmlQoznvqXbE0tEC/HONsnoUNH19Hc96NFsTAwTXbTqb8FMYkru1F/iqp7a18Ws3nWJvtA1sHD1A==",
"deprecated": "This is a stub types definition. classnames provides its own type definitions, so you do not need this installed.",
"dev": true,
"dependencies": {
"classnames": "*"
}
},
"node_modules/@types/connect": {
"version": "3.4.35",
"dev": true,
@ -13023,6 +13036,120 @@
"node": ">=6"
}
},
"node_modules/ts-loader": {
"version": "9.5.1",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz",
"integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==",
"dev": true,
"dependencies": {
"chalk": "^4.1.0",
"enhanced-resolve": "^5.0.0",
"micromatch": "^4.0.0",
"semver": "^7.3.4",
"source-map": "^0.7.4"
},
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"typescript": "*",
"webpack": "^5.0.0"
}
},
"node_modules/ts-loader/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/ts-loader/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/ts-loader/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/ts-loader/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/ts-loader/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/ts-loader/node_modules/semver": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ts-loader/node_modules/source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
"dev": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/ts-loader/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/tslib": {
"version": "1.14.1",
"license": "0BSD"
@ -13077,6 +13204,19 @@
"is-typedarray": "^1.0.0"
}
},
"node_modules/typescript": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/unbox-primitive": {
"version": "1.0.1",
"dev": true,

View file

@ -49,6 +49,7 @@
"@babel/plugin-proposal-object-rest-spread": "7.16.0",
"@babel/preset-env": "7.16.0",
"@babel/preset-react": "7.16.0",
"@types/classnames": "^2.3.1",
"@types/react": "^18.2.9",
"babel-loader": "8.2.3",
"clean-webpack-plugin": "4.0.0",
@ -66,6 +67,8 @@
"postcss-loader": "6.2.0",
"readdirp": "3.6.0",
"terser-webpack-plugin": "5.2.4",
"ts-loader": "^9.5.1",
"typescript": "^5.3.3",
"webpack": "5.61.0",
"webpack-cli": "4.9.1",
"webpack-dev-server": "^4.7.4",

View file

@ -45,6 +45,9 @@ const useTorrent = require('./useTorrent');
const useTranslate = require('./useTranslate');
const platform = require('./platform');
const EventModal = require('./EventModal');
const { default: useOnClickOutside } = require('./useOnClickOutside');
const { default: useKeyboardEvent } = require('./useKeyboardEvent');
const { default: useMouseEvent } = require('./useMouseEvent');
module.exports = {
AddonDetailsModal,
@ -96,4 +99,7 @@ module.exports = {
useTranslate,
platform,
EventModal,
useOnClickOutside,
useKeyboardEvent,
useMouseEvent,
};

View file

@ -0,0 +1,16 @@
// Copyright (C) 2017-2024 Smart code 203358507
import { useEffect } from 'react';
const useKeyboardEvent = (name: string, handler: (shift: boolean) => void, ignore?: boolean) => {
useEffect(() => {
const onKeyDown = ({ code, shiftKey }: KeyboardEvent) => {
!ignore && code === name && handler(shiftKey);
};
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, [handler, ignore]);
};
export default useKeyboardEvent;

View file

@ -0,0 +1,17 @@
// Copyright (C) 2017-2024 Smart code 203358507
import { useEffect } from 'react';
const useMouseEvent = (name: string, handler: () => void, ignore?: boolean) => {
useEffect(() => {
const onWheel = ({ deltaY }: WheelEvent) => {
!ignore && name === 'ScrollDown' && deltaY > 0 && handler();
!ignore && name === 'ScrollUp' && deltaY < 0 && handler();
};
document.addEventListener('wheel', onWheel);
return () => document.removeEventListener('wheel', onWheel);
}, [handler, ignore]);
};
export default useMouseEvent;

View file

@ -0,0 +1,19 @@
// Copyright (C) 2017-2024 Smart code 203358507
import { useEffect } from 'react';
const useOnClickOutside = (ref: React.MutableRefObject<HTMLElement>, handler: () => void) => {
useEffect(() => {
const onClickOutside = (event: MouseEvent) => {
const element = event.target as Node;
if (ref.current && !ref.current.contains(element)) {
handler();
}
};
document.addEventListener('click', onClickOutside);
return () => document.removeEventListener('click', onClickOutside);
}, [handler]);
};
export default useOnClickOutside;

2
src/modules.d.ts vendored
View file

@ -1,2 +1,2 @@
declare module '*';
declare module 'classnames';
declare module 'stremio/common';

View file

@ -0,0 +1,66 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { useCallback, useEffect, useRef } from 'react';
import classNames from 'classnames';
import Icon from '@stremio/stremio-icons/react';
import { Button, useBinaryState, useOnClickOutside, useKeyboardEvent } from 'stremio/common';
import styles from './styles.less';
type Props = {
children?: JSX.Element,
className?: string,
disabled?: boolean,
icon: string,
title?: string,
shortcut?: string,
onMenuChange?: (state: boolean) => void,
onClick?: () => void,
};
const Control = ({ children, className, disabled, icon, title, shortcut, onMenuChange, onClick }: Props) => {
const ref = useRef(null);
const [menuOpen,, closeMenu, toggleMenu] = useBinaryState();
const onButtonClick = () => {
toggleMenu();
onClick && onClick();
};
const onMenuClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
};
const onClickOutside = useCallback(() => {
menuOpen && closeMenu();
}, [menuOpen]);
useEffect(() => {
onMenuChange && onMenuChange(menuOpen);
}, [menuOpen, onMenuChange]);
useOnClickOutside(ref, onClickOutside);
useKeyboardEvent('Escape', closeMenu);
shortcut && useKeyboardEvent(shortcut, toggleMenu);
return (
<Button
ref={ref}
className={classNames(className, styles['control-button'], { 'disabled': disabled })}
tabIndex={-1}
title={title}
onClick={onButtonClick}
>
<Icon className={styles['icon']} name={icon} />
{
children && menuOpen ?
<div className={styles['menu-container']} onClick={onMenuClick}>
{children}
</div>
:
null
}
</Button>
);
};
export default Control;

View file

@ -0,0 +1,4 @@
// Copyright (C) 2017-2024 Smart code 203358507
import Control from './Control';
export default Control;

View file

@ -0,0 +1,36 @@
// Copyright (C) 2017-2024 Smart code 203358507
.control-button {
flex: none;
position: relative;
width: 4rem;
height: 5rem;
display: flex;
justify-content: center;
align-items: center;
overflow: visible;
&:global(.disabled) {
.icon {
opacity: 0.5;
}
}
.icon {
flex: none;
width: 2.5rem;
height: 2.5rem;
color: var(--primary-foreground-color);
}
.menu-container {
position: absolute;
right: 0;
bottom: 8rem;
border-radius: var(--border-radius);
background-color: var(--modal-background-color);
box-shadow: 0 1.35rem 2.7rem rgba(0, 0, 0, 0.4), 0 1.1rem 0.85rem rgba(0, 0, 0, 0.2);
backdrop-filter: blur(15px);
overflow: auto;
}
}

View file

@ -1,107 +1,100 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2024 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { Button } = require('stremio/common');
const { t } = require('i18next');
const { useBinaryState } = require('stremio/common');
const { useServices } = require('stremio/services');
const useStatistics = require('./useStatistics');
const SeekBar = require('./SeekBar');
const VolumeSlider = require('./VolumeSlider');
const { StatisticsMenu, SpeedMenu, InfoMenu, VideosMenu, SubtitlesMenu, OptionsMenu } = require('./Menus');
const { default: Control } = require('./Control');
const styles = require('./styles');
const { useBinaryState } = require('stremio/common');
const { t } = require('i18next');
const ControlBar = ({
className,
paused,
time,
duration,
buffered,
volume,
muted,
playbackSpeed,
subtitlesTracks,
audioTracks,
metaItem,
nextVideo,
stream,
statistics,
onPlayRequested,
onPauseRequested,
video,
player,
streamingServer,
onPlayPauseRequested,
onNextVideoRequested,
onMuteRequested,
onUnmuteRequested,
onVolumeChangeRequested,
onSeekRequested,
onToggleSubtitlesMenu,
onToggleInfoMenu,
onToggleSpeedMenu,
onToggleVideosMenu,
onToggleOptionsMenu,
onToggleStatisticsMenu,
onPlaybackSpeedChangeRequested,
onSubtitlesTrackSelected,
onExtraSubtitlesTrackSelected,
onAudioTrackSelected,
onSubtitlesOffsetChanged,
onSubtitlesSizeChanged,
onExtraSubtitlesDelayChanged,
onMenuChange,
...props
}) => {
const { chromecast } = useServices();
const statistics = useStatistics(player, streamingServer);
const [chromecastServiceActive, setChromecastServiceActive] = React.useState(() => chromecast.active);
const [buttonsMenuOpen, , , toogleButtonsMenu] = useBinaryState(false);
const onSubtitlesButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.subtitlesMenuClosePrevented = true;
}, []);
const onInfoButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.infoMenuClosePrevented = true;
}, []);
const onSpeedButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.speedMenuClosePrevented = true;
}, []);
const onVideosButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.videosMenuClosePrevented = true;
}, []);
const onOptionsButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.optionsMenuClosePrevented = true;
}, []);
const onStatisticsButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.statisticsMenuClosePrevented = true;
}, []);
const onPlayPauseButtonClick = React.useCallback(() => {
if (paused) {
if (typeof onPlayRequested === 'function') {
onPlayRequested();
}
} else {
if (typeof onPauseRequested === 'function') {
onPauseRequested();
}
}
}, [paused, onPlayRequested, onPauseRequested]);
const onNextVideoButtonClick = React.useCallback(() => {
if (nextVideo !== null && typeof onNextVideoRequested === 'function') {
onNextVideoRequested();
}
}, [nextVideo, onNextVideoRequested]);
const [mobileMenuOpen, , , toogleMobileMenu] = useBinaryState(false);
const {
subtitlesTracks,
extraSubtitlesTracks,
audioTracks,
selectedAudioTrackId,
selectedSubtitlesTrackId,
selectedExtraSubtitlesTrackId,
subtitlesOffset,
subtitlesSize,
extraSubtitlesOffset,
extraSubtitlesDelay,
extraSubtitlesSize,
} = video.state;
const { paused, buffered, muted, volume, time, duration, playbackSpeed } = video.state;
const { seriesInfo, nextVideo, addon } = player;
const tracks = React.useMemo(() => {
return subtitlesTracks && extraSubtitlesTracks && audioTracks ? subtitlesTracks.concat(extraSubtitlesTracks).concat(audioTracks): [];
}, [subtitlesTracks, extraSubtitlesTracks, audioTracks]);
const metaItem = React.useMemo(() => {
return player.metaItem !== null && player.metaItem.type === 'Ready' ? player.metaItem.content : null;
}, [player]);
const stream = React.useMemo(() => {
return player.selected !== null ? player.selected.stream : null;
}, [player]);
const playbackDevices = React.useMemo(() => {
return streamingServer.playbackDevices !== null && streamingServer.playbackDevices.type === 'Ready' ? streamingServer.playbackDevices.content : [];
}, [streamingServer]);
const onMuteButtonClick = React.useCallback(() => {
if (muted) {
if (typeof onUnmuteRequested === 'function') {
onUnmuteRequested();
}
} else {
if (typeof onMuteRequested === 'function') {
onMuteRequested();
}
}
muted ? onUnmuteRequested() : onMuteRequested();
}, [muted, onMuteRequested, onUnmuteRequested]);
const onChromecastButtonClick = React.useCallback(() => {
chromecast.transport.requestSession();
}, []);
const volumeIcon = React.useMemo(() => {
return (typeof muted === 'boolean' && muted) ? 'volume-mute' :
(volume === null || isNaN(volume)) ? 'volume-off' :
volume < 30 ? 'volume-low' :
volume < 70 ? 'volume-medium' :
'volume-high';
}, [muted, volume]);
React.useEffect(() => {
const onStateChanged = () => {
setChromecastServiceActive(chromecast.active);
};
const onStateChanged = () => setChromecastServiceActive(chromecast.active);
chromecast.on('stateChanged', onStateChanged);
return () => {
chromecast.off('stateChanged', onStateChanged);
};
return () => chromecast.off('stateChanged', onStateChanged);
}, []);
return (
<div {...props} className={classnames(className, styles['control-bar-container'])}>
<SeekBar
@ -112,65 +105,98 @@ const ControlBar = ({
onSeekRequested={onSeekRequested}
/>
<div className={styles['control-bar-buttons-container']}>
<Button className={classnames(styles['control-bar-button'], { 'disabled': typeof paused !== 'boolean' })} title={paused ? t('PLAYER_PLAY') : t('PLAYER_PAUSE')} tabIndex={-1} onClick={onPlayPauseButtonClick}>
<Icon className={styles['icon']} name={typeof paused !== 'boolean' || paused ? 'play' : 'pause'} />
</Button>
<Control
icon={typeof paused !== 'boolean' || paused ? 'play' : 'pause'}
title={paused ? t('PLAYER_PLAY') : t('PLAYER_PAUSE')}
disabled={typeof paused !== 'boolean'}
onClick={onPlayPauseRequested}
/>
{
nextVideo !== null ?
<Button className={classnames(styles['control-bar-button'])} title={t('PLAYER_NEXT_VIDEO')} tabIndex={-1} onClick={onNextVideoButtonClick}>
<Icon className={styles['icon']} name={'next'} />
</Button>
nextVideo ?
<Control
disabled={typeof paused !== 'boolean'}
icon={'next'}
title={t('PLAYER_NEXT_VIDEO')}
onClick={onNextVideoRequested}
/>
:
null
}
<Button className={classnames(styles['control-bar-button'], { 'disabled': typeof muted !== 'boolean' })} title={muted ? t('PLAYER_UNMUTE') : t('PLAYER_MUTE')} tabIndex={-1} onClick={onMuteButtonClick}>
<Icon
className={styles['icon']}
name={
(typeof muted === 'boolean' && muted) ? 'volume-mute' :
(volume === null || isNaN(volume)) ? 'volume-off' :
volume < 30 ? 'volume-low' :
volume < 70 ? 'volume-medium' :
'volume-high'
}
/>
</Button>
<Control
disabled={typeof muted !== 'boolean'}
icon={volumeIcon}
title={muted ? t('PLAYER_UNMUTE') : t('PLAYER_MUTE')}
onClick={onMuteButtonClick}
/>
<VolumeSlider
className={styles['volume-slider']}
volume={volume}
onVolumeChangeRequested={onVolumeChangeRequested}
/>
<div className={styles['spacing']} />
<Button className={styles['control-bar-buttons-menu-button']} onClick={toogleButtonsMenu}>
<Icon className={styles['icon']} name={'more-vertical'} />
</Button>
<div className={classnames(styles['control-bar-buttons-menu-container'], { 'open': buttonsMenuOpen })}>
<Button className={classnames(styles['control-bar-button'], { 'disabled': statistics === null || statistics.type === 'Err' || stream === null || typeof stream.infoHash !== 'string' || typeof stream.fileIdx !== 'number' })} tabIndex={-1} onMouseDown={onStatisticsButtonMouseDown} onClick={onToggleStatisticsMenu}>
<Icon className={styles['icon']} name={'network'} />
</Button>
<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}>
<Icon className={styles['icon']} name={'about'} />
</Button>
<Button className={classnames(styles['control-bar-button'], { 'disabled': !chromecastServiceActive })} tabIndex={-1} onClick={onChromecastButtonClick}>
<Icon className={styles['icon']} name={'cast'} />
</Button>
<Button className={classnames(styles['control-bar-button'], { 'disabled': (!Array.isArray(subtitlesTracks) || subtitlesTracks.length === 0) && (!Array.isArray(audioTracks) || audioTracks.length === 0) })} tabIndex={-1} onMouseDown={onSubtitlesButtonMouseDown} onClick={onToggleSubtitlesMenu}>
<Icon className={styles['icon']} name={'subtitles'} />
</Button>
<Control
className={styles['menu-button']}
icon={'more-vertical'}
onClick={toogleMobileMenu}
/>
<div className={classnames(styles['controls-menu-container'], { 'open': mobileMenuOpen })}>
<Control icon={'network'} disabled={!statistics || statistics.infoHash === null || !stream} shortcut={'KeyD'} onMenuChange={onMenuChange}>
<StatisticsMenu {...statistics} />
</Control>
<Control icon={'speed'} disabled={!playbackSpeed} shortcut={'KeyR'} onMenuChange={onMenuChange}>
<SpeedMenu
playbackSpeed={playbackSpeed}
onChange={onPlaybackSpeedChangeRequested}
/>
</Control>
<Control icon={'about'} disabled={!metaItem || !stream} shortcut={'KeyI'} onMenuChange={onMenuChange}>
<InfoMenu
stream={stream}
addon={addon}
metaItem={metaItem}
/>
</Control>
<Control icon={'cast'} disabled={!chromecastServiceActive} onClick={onChromecastButtonClick} />
<Control icon={'subtitles'} disabled={tracks.length === 0} shortcut={'KeyS'} onMenuChange={onMenuChange}>
<SubtitlesMenu
audioTracks={audioTracks}
selectedAudioTrackId={selectedAudioTrackId}
subtitlesTracks={subtitlesTracks}
selectedSubtitlesTrackId={selectedSubtitlesTrackId}
subtitlesOffset={subtitlesOffset}
subtitlesSize={subtitlesSize}
extraSubtitlesTracks={extraSubtitlesTracks}
selectedExtraSubtitlesTrackId={selectedExtraSubtitlesTrackId}
extraSubtitlesOffset={extraSubtitlesOffset}
extraSubtitlesDelay={extraSubtitlesDelay}
extraSubtitlesSize={extraSubtitlesSize}
onSubtitlesTrackSelected={onSubtitlesTrackSelected}
onExtraSubtitlesTrackSelected={onExtraSubtitlesTrackSelected}
onAudioTrackSelected={onAudioTrackSelected}
onSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
onSubtitlesSizeChanged={onSubtitlesSizeChanged}
onExtraSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
onExtraSubtitlesDelayChanged={onExtraSubtitlesDelayChanged}
onExtraSubtitlesSizeChanged={onSubtitlesSizeChanged}
/>
</Control>
{
metaItem?.content?.videos?.length > 0 ?
<Button className={styles['control-bar-button']} tabIndex={-1} onMouseDown={onVideosButtonMouseDown} onClick={onToggleVideosMenu}>
<Icon className={styles['icon']} name={'episodes'} />
</Button>
metaItem?.videos?.length > 0 ?
<Control icon={'episodes'} shortcut={'KeyV'} onMenuChange={onMenuChange}>
<VideosMenu
metaItem={metaItem}
seriesInfo={seriesInfo}
/>
</Control>
:
null
}
<Button className={styles['control-bar-button']} tabIndex={-1} onMouseDown={onOptionsButtonMouseDown} onClick={onToggleOptionsMenu}>
<Icon className={styles['icon']} name={'more-horizontal'} />
</Button>
<Control icon={'more-horizontal'} onMenuChange={onMenuChange}>
<OptionsMenu
stream={stream}
playbackDevices={playbackDevices}
/>
</Control>
</div>
</div>
</div>
@ -179,32 +205,23 @@ const ControlBar = ({
ControlBar.propTypes = {
className: PropTypes.string,
paused: PropTypes.bool,
time: PropTypes.number,
duration: PropTypes.number,
buffered: PropTypes.number,
volume: PropTypes.number,
muted: PropTypes.bool,
playbackSpeed: PropTypes.number,
subtitlesTracks: PropTypes.array,
audioTracks: PropTypes.array,
metaItem: PropTypes.object,
nextVideo: PropTypes.object,
stream: PropTypes.object,
statistics: PropTypes.object,
onPlayRequested: PropTypes.func,
onPauseRequested: PropTypes.func,
video: PropTypes.object,
player: PropTypes.object,
streamingServer: PropTypes.object,
onPlayPauseRequested: PropTypes.func,
onNextVideoRequested: PropTypes.func,
onMuteRequested: PropTypes.func,
onUnmuteRequested: PropTypes.func,
onVolumeChangeRequested: PropTypes.func,
onSeekRequested: PropTypes.func,
onToggleSubtitlesMenu: PropTypes.func,
onToggleInfoMenu: PropTypes.func,
onToggleSpeedMenu: PropTypes.func,
onToggleVideosMenu: PropTypes.func,
onToggleOptionsMenu: PropTypes.func,
onToggleStatisticsMenu: PropTypes.func,
onPlaybackSpeedChangeRequested: PropTypes.func,
onSubtitlesTrackSelected: PropTypes.func,
onExtraSubtitlesTrackSelected: PropTypes.func,
onAudioTrackSelected: PropTypes.func,
onSubtitlesOffsetChanged: PropTypes.func,
onSubtitlesSizeChanged: PropTypes.func,
onExtraSubtitlesDelayChanged: PropTypes.func,
onMenuChange: PropTypes.func,
};
module.exports = ControlBar;

View file

@ -8,7 +8,7 @@ const classnames = require('classnames');
const { MetaPreview, CONSTANTS } = require('stremio/common');
const styles = require('./styles');
const InfoMenu = ({ className, ...props }) => {
const InfoMenu = ({ ...props }) => {
const metaItem = React.useMemo(() => {
return props.metaItem !== null ?
{
@ -22,7 +22,7 @@ const InfoMenu = ({ className, ...props }) => {
event.nativeEvent.infoMenuClosePrevented = true;
}, []);
return (
<div className={classnames(className, styles['info-menu-container'])} onMouseDown={onMouseDown}>
<div className={classnames(styles['info-menu-container'])} onMouseDown={onMouseDown}>
{
metaItem !== null ?
<MetaPreview
@ -68,7 +68,6 @@ const InfoMenu = ({ className, ...props }) => {
};
InfoMenu.propTypes = {
className: PropTypes.string,
metaItem: PropTypes.object,
addon: PropTypes.object,
stream: PropTypes.object

View file

@ -9,16 +9,19 @@ const styles = require('./styles');
const RATES = Array.from(Array(8).keys(), (n) => n * 0.25 + 0.25).reverse();
const SpeedMenu = ({ className, playbackSpeed, onPlaybackSpeedChanged }) => {
const SpeedMenu = ({ className, playbackSpeed, onChange }) => {
const { t } = useTranslation();
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.speedMenuClosePrevented = true;
}, []);
const onOptionSelect = React.useCallback((value) => {
if (typeof onPlaybackSpeedChanged === 'function') {
onPlaybackSpeedChanged(value);
if (typeof onChange === 'function') {
onChange(value);
}
}, [onPlaybackSpeedChanged]);
}, [onChange]);
return (
<div className={classnames(className, styles['speed-menu-container'])} onMouseDown={onMouseDown}>
<div className={styles['title']}>
@ -44,7 +47,7 @@ const SpeedMenu = ({ className, playbackSpeed, onPlaybackSpeedChanged }) => {
SpeedMenu.propTypes = {
className: PropTypes.string,
playbackSpeed: PropTypes.number,
onPlaybackSpeedChanged: PropTypes.func,
onChange: PropTypes.func,
};
module.exports = SpeedMenu;

View file

@ -3,7 +3,7 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const Video = require('../../MetaDetails/VideosList/Video');
const Video = require('../../../../MetaDetails/VideosList/Video');
const styles = require('./styles');
const VideosMenu = ({ className, metaItem, seriesInfo }) => {

View file

@ -0,0 +1,17 @@
// Copyright (C) 2017-2024 Smart code 203358507
const InfoMenu = require('./InfoMenu');
const OptionsMenu = require('./OptionsMenu');
const SpeedMenu = require('./SpeedMenu');
const StatisticsMenu = require('./StatisticsMenu');
const SubtitlesMenu = require('./SubtitlesMenu');
const VideosMenu = require('./VideosMenu');
module.exports = {
InfoMenu,
OptionsMenu,
SpeedMenu,
StatisticsMenu,
SubtitlesMenu,
VideosMenu,
};

View file

@ -1,6 +1,5 @@
// Copyright (C) 2017-2023 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
@import (reference) '~stremio/common/screen-sizes.less';
.control-bar-container {
@ -17,28 +16,7 @@
display: flex;
flex-direction: row;
align-items: center;
.control-bar-button {
flex: none;
width: 4rem;
height: 5rem;
display: flex;
justify-content: center;
align-items: center;
&:global(.disabled) {
.icon {
opacity: 0.5;
}
}
.icon {
flex: none;
width: 2.5rem;
height: 2.5rem;
color: var(--primary-foreground-color);
}
}
overflow: visible;
.volume-slider {
--track-size: 0.35rem;
@ -53,26 +31,15 @@
flex: 1;
}
.control-bar-buttons-menu-button {
flex: none;
width: 4rem;
height: 4rem;
.menu-button {
display: none;
justify-content: center;
align-items: center;
.icon {
flex: none;
width: 2.5rem;
height: 2.5rem;
color: var(--primary-foreground-color);
}
}
.control-bar-buttons-menu-container {
.controls-menu-container {
flex: none;
display: flex;
flex-direction: row;
overflow: visible;
}
}
}
@ -94,11 +61,11 @@
display: none;
}
.control-bar-buttons-menu-button {
.menu-button {
display: flex;
}
.control-bar-buttons-menu-container {
.controls-menu-container {
position: absolute;
right: 0.15rem;
bottom: 4.5rem;
@ -106,8 +73,7 @@
padding: 0.5rem;
border-radius: var(--border-radius);
background-color: var(--modal-background-color);
box-shadow: 0 1.35rem 2.7rem @color-background-dark5-40,
0 1.1rem 0.85rem @color-background-dark5-20;
box-shadow: 0 1.35rem 2.7rem rgba(0, 0, 0, 0.4), 0 1.1rem 0.85rem rgba(0, 0, 0, 0.2);
&:not(:global(.open)) {
display: none;

View file

@ -6,23 +6,15 @@ const classnames = require('classnames');
const debounce = require('lodash.debounce');
const langs = require('langs');
const { useTranslation } = require('react-i18next');
const { useRouteFocused } = require('stremio-router');
const { useServices } = require('stremio/services');
const { HorizontalNavBar, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender } = require('stremio/common');
const { HorizontalNavBar, useFullscreen, useBinaryState, useToast, useStreamingServer, useKeyboardEvent, useMouseEvent, withCoreSuspender } = require('stremio/common');
const BufferingLoader = require('./BufferingLoader');
const VolumeChangeIndicator = require('./VolumeChangeIndicator');
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');
const useSettings = require('./useSettings');
const useStatistics = require('./useStatistics');
const useVideo = require('./useVideo');
const styles = require('./styles');
const Video = require('./Video');
@ -37,9 +29,7 @@ const Player = ({ urlParams, queryParams }) => {
const [player, videoParamsChanged, timeChanged, pausedChanged, ended, nextVideo] = usePlayer(urlParams);
const [settings, updateSettings] = useSettings();
const streamingServer = useStreamingServer();
const statistics = useStatistics(player, streamingServer);
const video = useVideo();
const routeFocused = useRouteFocused();
const toast = useToast();
const [casting, setCasting] = React.useState(() => {
@ -48,32 +38,18 @@ const Player = ({ urlParams, queryParams }) => {
const [immersed, setImmersed] = React.useState(true);
const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []);
const [, , , toggleFullscreen] = useFullscreen();
const [,,, toggleFullscreen] = useFullscreen();
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 [areMenusOpen, setMenusState] = React.useState(false);
const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false);
const menusOpen = React.useMemo(() => {
return optionsMenuOpen || subtitlesMenuOpen || infoMenuOpen || speedMenuOpen || videosMenuOpen || statisticsMenuOpen;
}, [optionsMenuOpen, subtitlesMenuOpen, infoMenuOpen, speedMenuOpen, videosMenuOpen, statisticsMenuOpen]);
const closeMenus = React.useCallback(() => {
closeOptionsMenu();
closeSubtitlesMenu();
closeInfoMenu();
closeSpeedMenu();
closeVideosMenu();
closeStatisticsMenu();
}, []);
const ignoreShortcuts = React.useMemo(() => {
return areMenusOpen || nextVideoPopupOpen;
}, [areMenusOpen, nextVideoPopupOpen]);
const overlayHidden = React.useMemo(() => {
return immersed && !casting && video.state.paused !== null && !video.state.paused && !menusOpen && !nextVideoPopupOpen;
}, [immersed, casting, video.state.paused, menusOpen, nextVideoPopupOpen]);
return immersed && !casting && video.state.paused !== null && !video.state.paused && !areMenusOpen && !nextVideoPopupOpen;
}, [immersed, casting, video.state.paused, areMenusOpen, nextVideoPopupOpen]);
const nextVideoPopupDismissed = React.useRef(false);
const defaultSubtitlesSelected = React.useRef(false);
@ -145,6 +121,11 @@ const Player = ({ urlParams, queryParams }) => {
}, []);
const onPauseRequestedDebounced = React.useCallback(debounce(onPauseRequested, 200), []);
const onPlayPauseRequested = React.useCallback(() => {
video.state.paused ? onPlayRequested() : onPauseRequested();
}, [video.state.paused]);
const onMuteRequested = React.useCallback(() => {
video.setProp('muted', true);
}, []);
@ -157,11 +138,29 @@ const Player = ({ urlParams, queryParams }) => {
video.setProp('volume', volume);
}, []);
const onVolumeIncreaseRequested = React.useCallback(() => {
video.state.volume !== null && onVolumeChangeRequested(video.state.volume + 5);
}, [video.state.volume]);
const onVolumeDereaseRequested = React.useCallback(() => {
video.state.volume !== null && onVolumeChangeRequested(video.state.volume - 5);
}, [video.state.volume]);
const onSeekRequested = React.useCallback((time) => {
video.setProp('time', time);
}, []);
const onPlaybackSpeedChanged = React.useCallback((rate) => {
const onSeekForwardRequested = React.useCallback((short) => {
const duration = short ? settings.seekShortTimeDuration : settings.seekTimeDuration;
video.state.time !== null && onSeekRequested(video.state.time + duration);
}, [video.state.time]);
const onSeekBackwardRequested = React.useCallback((short) => {
const duration = short ? settings.seekShortTimeDuration : settings.seekTimeDuration;
video.state.time !== null && onSeekRequested(video.state.time - duration);
}, [video.state.time]);
const onPlaybackSpeedChangeRequested = React.useCallback((rate) => {
video.setProp('playbackSpeed', rate);
}, []);
@ -226,27 +225,6 @@ const Player = ({ urlParams, queryParams }) => {
toggleFullscreen();
}, [toggleFullscreen]);
const onContainerMouseDown = React.useCallback((event) => {
if (!event.nativeEvent.optionsMenuClosePrevented) {
closeOptionsMenu();
}
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();
}
}, []);
const onContainerMouseMove = React.useCallback((event) => {
setImmersed(false);
if (!event.nativeEvent.immersePrevented) {
@ -403,27 +381,6 @@ const Player = ({ urlParams, queryParams }) => {
nextVideoPopupDismissed.current = false;
}, [video.state.stream]);
React.useEffect(() => {
if ((!Array.isArray(video.state.subtitlesTracks) || video.state.subtitlesTracks.length === 0) &&
(!Array.isArray(video.state.extraSubtitlesTracks) || video.state.extraSubtitlesTracks.length === 0) &&
(!Array.isArray(video.state.audioTracks) || video.state.audioTracks.length === 0)) {
closeSubtitlesMenu();
}
}, [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(() => {
const toastFilter = (item) => item?.dataset?.type === 'CoreEvent';
toast.addFilter(toastFilter);
@ -460,118 +417,13 @@ const Player = ({ urlParams, queryParams }) => {
};
}, []);
React.useLayoutEffect(() => {
const onKeyDown = (event) => {
switch (event.code) {
case 'Space': {
if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) {
if (video.state.paused) {
onPlayRequested();
} else {
onPauseRequested();
}
}
break;
}
case 'ArrowRight': {
if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration;
onSeekRequested(video.state.time + seekDuration);
}
break;
}
case 'ArrowLeft': {
if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration;
onSeekRequested(video.state.time - seekDuration);
}
break;
}
case 'ArrowUp': {
if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
onVolumeChangeRequested(video.state.volume + 5);
}
break;
}
case 'ArrowDown': {
if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
onVolumeChangeRequested(video.state.volume - 5);
}
break;
}
case 'KeyS': {
closeMenus();
if ((Array.isArray(video.state.subtitlesTracks) && video.state.subtitlesTracks.length > 0) ||
(Array.isArray(video.state.extraSubtitlesTracks) && video.state.extraSubtitlesTracks.length > 0) ||
(Array.isArray(video.state.audioTracks) && video.state.audioTracks.length > 0)) {
toggleSubtitlesMenu();
}
break;
}
case 'KeyI': {
closeMenus();
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
toggleInfoMenu();
}
break;
}
case 'KeyR': {
closeMenus();
if (video.state.playbackSpeed !== null) {
toggleSpeedMenu();
}
break;
}
case 'KeyV': {
closeMenus();
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
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') {
toggleStatisticsMenu();
}
break;
}
case 'Escape': {
closeMenus();
break;
}
}
};
const onWheel = ({ deltaY }) => {
if (deltaY > 0) {
if (!menusOpen && video.state.volume !== null) {
onVolumeChangeRequested(video.state.volume - 5);
}
} else {
if (!menusOpen && video.state.volume !== null) {
onVolumeChangeRequested(video.state.volume + 5);
}
}
};
if (routeFocused) {
window.addEventListener('keydown', onKeyDown);
window.addEventListener('wheel', onWheel);
}
return () => {
window.removeEventListener('keydown', onKeyDown);
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]);
useKeyboardEvent('Space', onPlayPauseRequested, ignoreShortcuts);
useKeyboardEvent('ArrowRight', onSeekForwardRequested, ignoreShortcuts);
useKeyboardEvent('ArrowLeft', onSeekBackwardRequested, ignoreShortcuts);
useKeyboardEvent('ArrowUp', onVolumeIncreaseRequested, ignoreShortcuts);
useKeyboardEvent('ArrowDown', onVolumeDereaseRequested, ignoreShortcuts);
useMouseEvent('ScrollUp', onVolumeIncreaseRequested, ignoreShortcuts);
useMouseEvent('ScrollDown', onVolumeDereaseRequested, ignoreShortcuts);
React.useEffect(() => {
video.events.on('error', onError);
@ -599,10 +451,10 @@ const Player = ({ urlParams, queryParams }) => {
return (
<div className={classnames(styles['player-container'], { [styles['overlayHidden']]: overlayHidden })}
onMouseDown={onContainerMouseDown}
onMouseMove={onContainerMouseMove}
onMouseOver={onContainerMouseMove}
onMouseLeave={onContainerMouseLeave}>
onMouseLeave={onContainerMouseLeave}
>
<Video
ref={video.containerElement}
className={styles['layer']}
@ -626,7 +478,7 @@ const Player = ({ urlParams, queryParams }) => {
null
}
{
menusOpen ?
areMenusOpen ?
<div className={styles['layer']} />
:
null
@ -636,6 +488,7 @@ const Player = ({ urlParams, queryParams }) => {
<VolumeChangeIndicator
muted={video.state.muted}
volume={video.state.volume}
onVolumeChangeRequested={onVolumeChangeRequested}
/>
:
null
@ -650,32 +503,23 @@ const Player = ({ urlParams, queryParams }) => {
/>
<ControlBar
className={classnames(styles['layer'], styles['control-bar-layer'])}
paused={video.state.paused}
time={video.state.time}
duration={video.state.duration}
buffered={video.state.buffered}
volume={video.state.volume}
muted={video.state.muted}
playbackSpeed={video.state.playbackSpeed}
subtitlesTracks={video.state.subtitlesTracks.concat(video.state.extraSubtitlesTracks)}
audioTracks={video.state.audioTracks}
metaItem={player.metaItem}
nextVideo={player.nextVideo}
stream={player.selected !== null ? player.selected.stream : null}
statistics={statistics}
onPlayRequested={onPlayRequested}
onPauseRequested={onPauseRequested}
video={video}
player={player}
streamingServer={streamingServer}
onPlayPauseRequested={onPlayPauseRequested}
onNextVideoRequested={onNextVideoRequested}
onMuteRequested={onMuteRequested}
onUnmuteRequested={onUnmuteRequested}
onVolumeChangeRequested={onVolumeChangeRequested}
onSeekRequested={onSeekRequested}
onToggleOptionsMenu={toggleOptionsMenu}
onToggleSubtitlesMenu={toggleSubtitlesMenu}
onToggleInfoMenu={toggleInfoMenu}
onToggleSpeedMenu={toggleSpeedMenu}
onToggleVideosMenu={toggleVideosMenu}
onToggleStatisticsMenu={toggleStatisticsMenu}
onPlaybackSpeedChangeRequested={onPlaybackSpeedChangeRequested}
onSubtitlesTrackSelected={onSubtitlesTrackSelected}
onExtraSubtitlesTrackSelected={onExtraSubtitlesTrackSelected}
onAudioTrackSelected={onAudioTrackSelected}
onSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
onSubtitlesSizeChanged={onSubtitlesSizeChanged}
onExtraSubtitlesDelayChanged={onExtraSubtitlesDelayChanged}
onMenuChange={setMenusState}
onMouseMove={onBarMouseMove}
onMouseOver={onBarMouseMove}
/>
@ -691,83 +535,6 @@ const Player = ({ urlParams, queryParams }) => {
:
null
}
{
statisticsMenuOpen ?
<StatisticsMenu
className={classnames(styles['layer'], styles['menu-layer'])}
{...statistics}
/>
:
null
}
{
subtitlesMenuOpen ?
<SubtitlesMenu
className={classnames(styles['layer'], styles['menu-layer'])}
audioTracks={video.state.audioTracks}
selectedAudioTrackId={video.state.selectedAudioTrackId}
subtitlesTracks={video.state.subtitlesTracks}
selectedSubtitlesTrackId={video.state.selectedSubtitlesTrackId}
subtitlesOffset={video.state.subtitlesOffset}
subtitlesSize={video.state.subtitlesSize}
extraSubtitlesTracks={video.state.extraSubtitlesTracks}
selectedExtraSubtitlesTrackId={video.state.selectedExtraSubtitlesTrackId}
extraSubtitlesOffset={video.state.extraSubtitlesOffset}
extraSubtitlesDelay={video.state.extraSubtitlesDelay}
extraSubtitlesSize={video.state.extraSubtitlesSize}
onSubtitlesTrackSelected={onSubtitlesTrackSelected}
onExtraSubtitlesTrackSelected={onExtraSubtitlesTrackSelected}
onAudioTrackSelected={onAudioTrackSelected}
onSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
onSubtitlesSizeChanged={onSubtitlesSizeChanged}
onExtraSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
onExtraSubtitlesDelayChanged={onExtraSubtitlesDelayChanged}
onExtraSubtitlesSizeChanged={onSubtitlesSizeChanged}
/>
:
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
className={classnames(styles['layer'], styles['menu-layer'])}
playbackSpeed={video.state.playbackSpeed}
onPlaybackSpeedChanged={onPlaybackSpeedChanged}
/>
:
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
className={classnames(styles['layer'], styles['menu-layer'])}
stream={player.selected.stream}
playbackDevices={streamingServer.playbackDevices !== null && streamingServer.playbackDevices.type === 'Ready' ? streamingServer.playbackDevices.content : []}
/>
:
null
}
</div>
);
};

View file

@ -84,20 +84,5 @@ html:not(.active-slider-within) {
content: "";
}
}
&.menu-layer {
top: initial;
left: initial;
right: 2rem;
bottom: 8rem;
max-height: calc(100% - 13.5rem);
max-width: calc(100% - 4rem);
border-radius: var(--border-radius);
background-color: var(--modal-background-color);
box-shadow: 0 1.35rem 2.7rem @color-background-dark5-40,
0 1.1rem 0.85rem @color-background-dark5-20;
backdrop-filter: blur(15px);
overflow: auto;
}
}
}

View file

@ -2,7 +2,7 @@ type LibraryItemPlayer = Pick<LibraryItem, '_id'> & {
state: Pick<LibraryItemState, 'timeOffset' | 'video_id'>,
};
type VideoPlayer = Video & {
type PlayerVideo = Video & {
upcomming: boolean,
watched: boolean,
progress: boolean | null,
@ -10,8 +10,8 @@ type VideoPlayer = Video & {
deepLinks: VideoDeepLinks,
};
type MetaItemPlayer = MetaItemPreview & {
videos: VideoPlayer[],
type PlayerMetaItem = MetaItemPreview & {
videos: PlayerVideo[],
};
type SelectedStream = Stream & {
@ -28,8 +28,8 @@ type Subtitle = {
type Player = {
addon: Addon | null,
libraryItem: LibraryItemPlayer | null,
metaItem: Loadable<MetaItemPlayer> | null,
nextVideo: VideoPlayer | null,
metaItem: Loadable<PlayerMetaItem> | null,
nextVideo: PlayerVideo | null,
selected: {
stream: SelectedStream,
metaRequest: ResourceRequest,

View file

@ -3,7 +3,7 @@
const fs = require('fs');
const readdirp = require('readdirp');
const COPYRIGHT_HEADER = /^\/\/ Copyright \(C\) 2017-2023 Smart code 203358507.*/;
const COPYRIGHT_HEADER = /^\/\/ Copyright \(C\) 2017-\d{4} Smart code 203358507.*/;
describe('copyright', () => {
test('js', async () => {

View file

@ -1,7 +1,7 @@
{
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable"],
"jsx": "preserve",
"jsx": "react",
"rootDir": "./src",
"moduleResolution": "node",
"baseUrl": ".",
@ -9,9 +9,10 @@
"stremio/*": ["src/*"],
},
"resolveJsonModule": true,
"esModuleInterop": true,
"allowJs": true,
"checkJs": false,
"noEmit": true,
"noEmit": false,
"strict": false
},
"include": [

View file

@ -27,6 +27,11 @@ module.exports = (env, argv) => ({
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: 'ts-loader',
},
{
test: /\.js$/,
exclude: /node_modules/,
@ -142,7 +147,7 @@ module.exports = (env, argv) => ({
]
},
resolve: {
extensions: ['.js', '.json', '.less', '.wasm'],
extensions: ['.tsx', '.ts', '.js', '.json', '.less', '.wasm'],
alias: {
'stremio': path.join(__dirname, 'src'),
'stremio-router': path.join(__dirname, 'src', 'router')