mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-20 10:42:12 +00:00
refactor: move audio tracks to their own menu
This commit is contained in:
parent
03ee936e86
commit
c811c28be6
8 changed files with 186 additions and 45 deletions
60
src/routes/Player/AudioMenu/AudioMenu.less
Normal file
60
src/routes/Player/AudioMenu/AudioMenu.less
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
.audio-menu {
|
||||
height: 25rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.container {
|
||||
flex: none;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 16rem;
|
||||
|
||||
.header {
|
||||
flex: none;
|
||||
align-self: stretch;
|
||||
padding: 1.5rem 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
|
||||
.list {
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
overflow-y: auto;
|
||||
padding: 0 1rem;
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 3.5rem;
|
||||
padding: 0 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
&:global(.selected), &:hover {
|
||||
background-color: var(--overlay-color);
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
max-height: 2.4em;
|
||||
font-size: 1.1rem;
|
||||
color: var(--primary-foreground-color);
|
||||
text-wrap: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex: none;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 100%;
|
||||
margin-left: 1rem;
|
||||
background-color: var(--secondary-accent-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/routes/Player/AudioMenu/AudioMenu.tsx
Normal file
65
src/routes/Player/AudioMenu/AudioMenu.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import React, { MouseEvent, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import classNames from 'classnames';
|
||||
import { Button, languageNames } from 'stremio/common';
|
||||
import styles from './AudioMenu.less';
|
||||
|
||||
type Props = {
|
||||
className: string,
|
||||
selectedAudioTrackId: string | null,
|
||||
audioTracks: AudioTrack[],
|
||||
onAudioTrackSelected: (id: string) => void,
|
||||
};
|
||||
|
||||
const AudioMenu = ({ className, selectedAudioTrackId, audioTracks, onAudioTrackSelected }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onMouseDown = (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const onAudioTrackClick = useCallback(({ currentTarget }: MouseEvent) => {
|
||||
const id = currentTarget.getAttribute('data-id')!;
|
||||
onAudioTrackSelected && onAudioTrackSelected(id);
|
||||
}, [onAudioTrackSelected]);
|
||||
|
||||
return (
|
||||
<div className={classNames(className, styles['audio-menu'])} onMouseDown={onMouseDown}>
|
||||
<div className={styles['container']}>
|
||||
<div className={styles['header']}>
|
||||
{ t('AUDIO_TRACKS') }
|
||||
</div>
|
||||
<div className={styles['list']}>
|
||||
{
|
||||
audioTracks.map(({ id, label, lang }, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
title={label}
|
||||
className={classNames(styles['option'], { 'selected': selectedAudioTrackId === id })}
|
||||
data-id={id}
|
||||
onClick={onAudioTrackClick}
|
||||
>
|
||||
<div className={styles['label']}>
|
||||
{
|
||||
typeof languageNames[lang] === 'string' ?
|
||||
languageNames[lang]
|
||||
:
|
||||
lang
|
||||
}
|
||||
</div>
|
||||
{
|
||||
selectedAudioTrackId === id ?
|
||||
<div className={styles['icon']} />
|
||||
:
|
||||
null
|
||||
}
|
||||
</Button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AudioMenu;
|
||||
2
src/routes/Player/AudioMenu/index.ts
Normal file
2
src/routes/Player/AudioMenu/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import AudioMenu from './AudioMenu';
|
||||
export default AudioMenu;
|
||||
|
|
@ -35,6 +35,7 @@ const ControlBar = ({
|
|||
onVolumeChangeRequested,
|
||||
onSeekRequested,
|
||||
onToggleSubtitlesMenu,
|
||||
onToggleAudioMenu,
|
||||
onToggleInfoMenu,
|
||||
onToggleSpeedMenu,
|
||||
onToggleVideosMenu,
|
||||
|
|
@ -48,6 +49,9 @@ const ControlBar = ({
|
|||
const onSubtitlesButtonMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.subtitlesMenuClosePrevented = true;
|
||||
}, []);
|
||||
const onAudioButtonMouseDown = React.useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
const onInfoButtonMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.infoMenuClosePrevented = true;
|
||||
}, []);
|
||||
|
|
@ -157,9 +161,12 @@ const ControlBar = ({
|
|||
<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}>
|
||||
<Button className={classnames(styles['control-bar-button'], { 'disabled': !Array.isArray(subtitlesTracks) || subtitlesTracks.length === 0 })} tabIndex={-1} onMouseDown={onSubtitlesButtonMouseDown} onClick={onToggleSubtitlesMenu}>
|
||||
<Icon className={styles['icon']} name={'subtitles'} />
|
||||
</Button>
|
||||
<Button className={classnames(styles['control-bar-button'], { 'disabled': !Array.isArray(audioTracks) || audioTracks.length === 0 })} tabIndex={-1} onMouseDown={onAudioButtonMouseDown} onClick={onToggleAudioMenu}>
|
||||
<Icon className={styles['icon']} name={'audio-tracks'} />
|
||||
</Button>
|
||||
{
|
||||
metaItem?.content?.videos?.length > 0 ?
|
||||
<Button className={styles['control-bar-button']} tabIndex={-1} onMouseDown={onVideosButtonMouseDown} onClick={onToggleVideosMenu}>
|
||||
|
|
@ -200,6 +207,7 @@ ControlBar.propTypes = {
|
|||
onVolumeChangeRequested: PropTypes.func,
|
||||
onSeekRequested: PropTypes.func,
|
||||
onToggleSubtitlesMenu: PropTypes.func,
|
||||
onToggleAudioMenu: PropTypes.func,
|
||||
onToggleInfoMenu: PropTypes.func,
|
||||
onToggleSpeedMenu: PropTypes.func,
|
||||
onToggleVideosMenu: PropTypes.func,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ const InfoMenu = require('./InfoMenu');
|
|||
const OptionsMenu = require('./OptionsMenu');
|
||||
const VideosMenu = require('./VideosMenu');
|
||||
const SubtitlesMenu = require('./SubtitlesMenu');
|
||||
const { default: AudioMenu } = require('./AudioMenu');
|
||||
const SpeedMenu = require('./SpeedMenu');
|
||||
const usePlayer = require('./usePlayer');
|
||||
const useSettings = require('./useSettings');
|
||||
|
|
@ -54,6 +55,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
|
||||
const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] = useBinaryState(false);
|
||||
const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
|
||||
const [audioMenuOpen, , closeAudioMenu, toggleAudioMenu] = useBinaryState(false);
|
||||
const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
|
||||
const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false);
|
||||
const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] = useBinaryState(false);
|
||||
|
|
@ -61,12 +63,13 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false);
|
||||
|
||||
const menusOpen = React.useMemo(() => {
|
||||
return optionsMenuOpen || subtitlesMenuOpen || infoMenuOpen || speedMenuOpen || videosMenuOpen || statisticsMenuOpen;
|
||||
}, [optionsMenuOpen, subtitlesMenuOpen, infoMenuOpen, speedMenuOpen, videosMenuOpen, statisticsMenuOpen]);
|
||||
return optionsMenuOpen || subtitlesMenuOpen || audioMenuOpen|| infoMenuOpen || speedMenuOpen || videosMenuOpen || statisticsMenuOpen;
|
||||
}, [optionsMenuOpen, subtitlesMenuOpen, audioMenuOpen, infoMenuOpen, speedMenuOpen, videosMenuOpen, statisticsMenuOpen]);
|
||||
|
||||
const closeMenus = React.useCallback(() => {
|
||||
closeOptionsMenu();
|
||||
closeSubtitlesMenu();
|
||||
closeAudioMenu();
|
||||
closeInfoMenu();
|
||||
closeSpeedMenu();
|
||||
closeVideosMenu();
|
||||
|
|
@ -237,6 +240,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
if (!event.nativeEvent.subtitlesMenuClosePrevented) {
|
||||
closeSubtitlesMenu();
|
||||
}
|
||||
closeAudioMenu();
|
||||
if (!event.nativeEvent.infoMenuClosePrevented) {
|
||||
closeInfoMenu();
|
||||
}
|
||||
|
|
@ -406,11 +410,16 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
|
||||
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)) {
|
||||
(!Array.isArray(video.state.extraSubtitlesTracks) || video.state.extraSubtitlesTracks.length === 0)) {
|
||||
closeSubtitlesMenu();
|
||||
}
|
||||
}, [video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
|
||||
}, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!Array.isArray(video.state.audioTracks) || video.state.audioTracks.length === 0) {
|
||||
closeAudioMenu();
|
||||
}
|
||||
}, [video.state.audioTracks]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (player.metaItem === null || player.metaItem.type !== 'Ready') {
|
||||
|
|
@ -511,13 +520,20 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
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)) {
|
||||
(Array.isArray(video.state.extraSubtitlesTracks) && video.state.extraSubtitlesTracks.length > 0)) {
|
||||
toggleSubtitlesMenu();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'KeyA': {
|
||||
closeMenus();
|
||||
if (Array.isArray(video.state.audioTracks) && video.state.audioTracks.length > 0) {
|
||||
toggleAudioMenu();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'KeyI': {
|
||||
closeMenus();
|
||||
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
|
||||
|
|
@ -691,6 +707,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
onSeekRequested={onSeekRequested}
|
||||
onToggleOptionsMenu={toggleOptionsMenu}
|
||||
onToggleSubtitlesMenu={toggleSubtitlesMenu}
|
||||
onToggleAudioMenu={toggleAudioMenu}
|
||||
onToggleInfoMenu={toggleInfoMenu}
|
||||
onToggleSpeedMenu={toggleSpeedMenu}
|
||||
onToggleVideosMenu={toggleVideosMenu}
|
||||
|
|
@ -723,8 +740,6 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
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}
|
||||
|
|
@ -736,7 +751,6 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
extraSubtitlesSize={video.state.extraSubtitlesSize}
|
||||
onSubtitlesTrackSelected={onSubtitlesTrackSelected}
|
||||
onExtraSubtitlesTrackSelected={onExtraSubtitlesTrackSelected}
|
||||
onAudioTrackSelected={onAudioTrackSelected}
|
||||
onSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
|
||||
onSubtitlesSizeChanged={onSubtitlesSizeChanged}
|
||||
onExtraSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
|
||||
|
|
@ -746,6 +760,17 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
audioMenuOpen ?
|
||||
<AudioMenu
|
||||
className={classnames(styles['layer'], styles['menu-layer'])}
|
||||
audioTracks={video.state.audioTracks}
|
||||
selectedAudioTrackId={video.state.selectedAudioTrackId}
|
||||
onAudioTrackSelected={onAudioTrackSelected}
|
||||
/>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
infoMenuOpen ?
|
||||
<InfoMenu
|
||||
|
|
|
|||
|
|
@ -144,34 +144,8 @@ const SubtitlesMenu = React.memo((props) => {
|
|||
}
|
||||
}
|
||||
}, [props.selectedSubtitlesTrackId, props.selectedExtraSubtitlesTrackId, props.subtitlesOffset, props.extraSubtitlesOffset, props.onSubtitlesOffsetChanged, props.onExtraSubtitlesOffsetChanged]);
|
||||
const audioTrackOnClick = React.useCallback((event) => {
|
||||
if (typeof props.onAudioTrackSelected === 'function') {
|
||||
props.onAudioTrackSelected(event.currentTarget.dataset.id);
|
||||
}
|
||||
}, [props.onAudioTrackSelected]);
|
||||
return (
|
||||
<div className={classnames(props.className, styles['subtitles-menu-container'])} onMouseDown={onMouseDown}>
|
||||
{
|
||||
Array.isArray(props.audioTracks) && props.audioTracks.length > 1 ?
|
||||
<div className={styles['languages-container']}>
|
||||
<div className={styles['languages-header']}>Audio Languages</div>
|
||||
<div className={styles['languages-list']}>
|
||||
{props.audioTracks.map(({ id, label, lang }, index) => (
|
||||
<Button key={index} title={label} className={classnames(styles['language-option'], { 'selected': props.selectedAudioTrackId === id })} data-id={id} onClick={audioTrackOnClick}>
|
||||
<div className={styles['language-label']}>{typeof languageNames[lang] === 'string' ? languageNames[lang] : lang}</div>
|
||||
{
|
||||
props.selectedAudioTrackId === id ?
|
||||
<div className={styles['icon']} />
|
||||
:
|
||||
null
|
||||
}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles['languages-container']}>
|
||||
<div className={styles['languages-header']}>{ t('PLAYER_SUBTITLES_LANGUAGES') }</div>
|
||||
<div className={styles['languages-list']}>
|
||||
|
|
@ -312,16 +286,8 @@ SubtitlesMenu.propTypes = {
|
|||
extraSubtitlesOffset: PropTypes.number,
|
||||
extraSubtitlesDelay: PropTypes.number,
|
||||
extraSubtitlesSize: PropTypes.number,
|
||||
audioTracks: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
lang: PropTypes.string.isRequired,
|
||||
origin: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired
|
||||
})),
|
||||
selectedAudioTrackId: PropTypes.string,
|
||||
onSubtitlesTrackSelected: PropTypes.func,
|
||||
onExtraSubtitlesTrackSelected: PropTypes.func,
|
||||
onAudioTrackSelected: PropTypes.func,
|
||||
onSubtitlesOffsetChanged: PropTypes.func,
|
||||
onSubtitlesSizeChanged: PropTypes.func,
|
||||
onExtraSubtitlesOffsetChanged: PropTypes.func,
|
||||
|
|
|
|||
|
|
@ -627,6 +627,14 @@ const Settings = () => {
|
|||
<kbd>S</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_MENU_AUDIO') }</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>A</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_MENU_INFO') }</div>
|
||||
|
|
|
|||
7
src/types/types.d.ts
vendored
7
src/types/types.d.ts
vendored
|
|
@ -61,3 +61,10 @@ type Catalog<T, D = any> = {
|
|||
installed?: boolean,
|
||||
deepLinks?: D,
|
||||
};
|
||||
|
||||
type AudioTrack = {
|
||||
id: string,
|
||||
label: string,
|
||||
lang: string,
|
||||
origin: string,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue