mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-14 00:40:25 +00:00
Merge branch 'development' into refactor/overall-app-styles
This commit is contained in:
commit
b12e44c086
10 changed files with 194 additions and 49 deletions
7
package-lock.json
generated
7
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
60
src/routes/Player/AudioMenu/AudioMenu.less
Normal file
60
src/routes/Player/AudioMenu/AudioMenu.less
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/routes/Player/AudioMenu/AudioMenu.tsx
Normal file
66
src/routes/Player/AudioMenu/AudioMenu.tsx
Normal 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;
|
||||
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,
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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