refactor: move audio tracks to their own menu

This commit is contained in:
Tim 2024-12-10 20:40:17 +01:00
parent 03ee936e86
commit c811c28be6
8 changed files with 186 additions and 45 deletions

View 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);
}
}
}
}
}

View 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;

View file

@ -0,0 +1,2 @@
import AudioMenu from './AudioMenu';
export default AudioMenu;

View file

@ -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,

View file

@ -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

View file

@ -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,

View file

@ -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>

View file

@ -61,3 +61,10 @@ type Catalog<T, D = any> = {
installed?: boolean,
deepLinks?: D,
};
type AudioTrack = {
id: string,
label: string,
lang: string,
origin: string,
};