Merge branch 'development' into refactor/overall-app-styles

This commit is contained in:
Timothy Z. 2024-12-26 16:02:37 +02:00
commit b12e44c086
10 changed files with 194 additions and 49 deletions

7
package-lock.json generated
View file

@ -36,7 +36,7 @@
"react-i18next": "^15.1.3",
"react-is": "18.3.1",
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
"stremio-translations": "github:Stremio/stremio-translations#f666d9a97cafa5aa150878b5c51a2896b5f4f1b2",
"stremio-translations": "github:Stremio/stremio-translations#a0f50634202f748a57907b645d2cd92fbaa479dd",
"url": "0.11.4",
"use-long-press": "^3.2.0"
},
@ -13372,8 +13372,9 @@
},
"node_modules/stremio-translations": {
"version": "1.44.9",
"resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#f666d9a97cafa5aa150878b5c51a2896b5f4f1b2",
"integrity": "sha512-SzaIGUMqQuMAq58sI9L/RKSs5O4eF8VKPMqnWFddBSg/tZOU9xuNYqjRPKT07cp8MRfzzGQmCKMByozTYfjdIA=="
"resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#a0f50634202f748a57907b645d2cd92fbaa479dd",
"integrity": "sha512-JJpd1JJet3T6/VTNdZ2NZ7uvHJ4zkuyqo5BnTcDGqLVNO/OpicGqKhZjE4WGSgmuhsfPBU8T0ICCfzKu2xpvKg==",
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.1.1",

View file

@ -40,7 +40,7 @@
"react-i18next": "^15.1.3",
"react-is": "18.3.1",
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
"stremio-translations": "github:Stremio/stremio-translations#f666d9a97cafa5aa150878b5c51a2896b5f4f1b2",
"stremio-translations": "github:Stremio/stremio-translations#a0f50634202f748a57907b645d2cd92fbaa479dd",
"url": "0.11.4",
"use-long-press": "^3.2.0"
},

View file

@ -0,0 +1,60 @@
.audio-menu {
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;
padding-bottom: 0.5rem;
.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,66 @@
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 onAudioTrackClick = useCallback(({ currentTarget }: MouseEvent) => {
const id = currentTarget.getAttribute('data-id')!;
onAudioTrackSelected && onAudioTrackSelected(id);
}, [onAudioTrackSelected]);
const onMouseDown = (event: MouseEvent) => {
// @ts-expect-error: Property 'audioMenuClosePrevented' does not exist on type 'MouseEvent'.
event.nativeEvent.audioMenuClosePrevented = true;
};
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,
onToggleSpeedMenu,
onToggleSideDrawer,
onToggleOptionsMenu,
@ -47,6 +48,9 @@ const ControlBar = ({
const onSubtitlesButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.subtitlesMenuClosePrevented = true;
}, []);
const onAudioButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.audioMenuClosePrevented = true;
}, []);
const onSpeedButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.speedMenuClosePrevented = true;
}, []);
@ -150,9 +154,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={onToggleSideDrawer}>
@ -193,6 +200,7 @@ ControlBar.propTypes = {
onVolumeChangeRequested: PropTypes.func,
onSeekRequested: PropTypes.func,
onToggleSubtitlesMenu: PropTypes.func,
onToggleAudioMenu: PropTypes.func,
onToggleSpeedMenu: PropTypes.func,
onToggleSideDrawer: PropTypes.func,
onToggleOptionsMenu: PropTypes.func,

View file

@ -17,6 +17,7 @@ const NextVideoPopup = require('./NextVideoPopup');
const StatisticsMenu = require('./StatisticsMenu');
const OptionsMenu = require('./OptionsMenu');
const SubtitlesMenu = require('./SubtitlesMenu');
const { default: AudioMenu } = require('./AudioMenu');
const SpeedMenu = require('./SpeedMenu');
const { default: SideDrawerButton } = require('./SideDrawerButton');
const { default: SideDrawer } = require('./SideDrawer');
@ -54,18 +55,20 @@ const Player = ({ urlParams, queryParams }) => {
const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] = useBinaryState(false);
const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
const [audioMenuOpen, , closeAudioMenu, toggleAudioMenu] = useBinaryState(false);
const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false);
const [statisticsMenuOpen, , closeStatisticsMenu, toggleStatisticsMenu] = useBinaryState(false);
const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false);
const [sideDrawerOpen, , closeSideDrawer, toggleSideDrawer] = useBinaryState(false);
const menusOpen = React.useMemo(() => {
return optionsMenuOpen || subtitlesMenuOpen || speedMenuOpen || statisticsMenuOpen || sideDrawerOpen;
}, [optionsMenuOpen, subtitlesMenuOpen, speedMenuOpen, statisticsMenuOpen, sideDrawerOpen]);
return optionsMenuOpen || subtitlesMenuOpen || audioMenuOpen || speedMenuOpen || statisticsMenuOpen || sideDrawerOpen;
}, [optionsMenuOpen, subtitlesMenuOpen, audioMenuOpen, speedMenuOpen, statisticsMenuOpen, sideDrawerOpen]);
const closeMenus = React.useCallback(() => {
closeOptionsMenu();
closeSubtitlesMenu();
closeAudioMenu();
closeSpeedMenu();
closeStatisticsMenu();
closeSideDrawer();
@ -235,6 +238,9 @@ const Player = ({ urlParams, queryParams }) => {
if (!event.nativeEvent.subtitlesMenuClosePrevented) {
closeSubtitlesMenu();
}
if (!event.nativeEvent.audioMenuClosePrevented) {
closeAudioMenu();
}
if (!event.nativeEvent.speedMenuClosePrevented) {
closeSpeedMenu();
}
@ -399,11 +405,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 (video.state.playbackSpeed === null) {
@ -497,13 +508,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') {
@ -678,6 +696,7 @@ const Player = ({ urlParams, queryParams }) => {
onSeekRequested={onSeekRequested}
onToggleOptionsMenu={toggleOptionsMenu}
onToggleSubtitlesMenu={toggleSubtitlesMenu}
onToggleAudioMenu={toggleAudioMenu}
onToggleSpeedMenu={toggleSpeedMenu}
onToggleStatisticsMenu={toggleStatisticsMenu}
onToggleSideDrawer={toggleSideDrawer}
@ -717,8 +736,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}
@ -730,7 +747,6 @@ const Player = ({ urlParams, queryParams }) => {
extraSubtitlesSize={video.state.extraSubtitlesSize}
onSubtitlesTrackSelected={onSubtitlesTrackSelected}
onExtraSubtitlesTrackSelected={onExtraSubtitlesTrackSelected}
onAudioTrackSelected={onAudioTrackSelected}
onSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
onSubtitlesSizeChanged={onSubtitlesSizeChanged}
onExtraSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
@ -740,6 +756,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
}
{
speedMenuOpen ?
<SpeedMenu

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,
};