feat(player): add transitions to menus

This commit is contained in:
Tim 2026-04-02 09:17:00 +02:00
parent eb23d3e4db
commit 3f5dedd072
9 changed files with 78 additions and 87 deletions

View file

@ -88,7 +88,7 @@
.fade-active {
opacity: 1;
transition: opacity 0.3s cubic-bezier(0.32, 0, 0.67, 0);
transition: opacity 0.1s cubic-bezier(0.32, 0, 0.67, 0);
}
.fade-exit {

View file

@ -5,9 +5,10 @@ type Props = {
children: JSX.Element,
when: boolean,
name: string,
duration?: number,
};
const Transition = ({ children, when, name }: Props) => {
const Transition = ({ children, when, name, duration }: Props) => {
const [element, setElement] = useState<HTMLElement | null>(null);
const [mounted, setMounted] = useState(false);
@ -29,6 +30,10 @@ const Transition = ({ children, when, name }: Props) => {
);
}, [name, state, active, children]);
const style = useMemo(() => {
if (duration) return { transitionDuration: `${duration}ms` };
}, [duration]);
const onTransitionEnd = useCallback(() => {
state === 'exit' && setMounted(false);
}, [state]);
@ -53,6 +58,7 @@ const Transition = ({ children, when, name }: Props) => {
mounted && cloneElement(children, {
ref: callbackRef,
className,
style,
})
);
};

View file

@ -1,4 +1,4 @@
import React, { MouseEvent, useCallback } from 'react';
import React, { forwardRef, MouseEvent, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import { languages } from 'stremio/common';
@ -12,7 +12,7 @@ type Props = {
onAudioTrackSelected: (id: string) => void,
};
const AudioMenu = ({ className, selectedAudioTrackId, audioTracks, onAudioTrackSelected }: Props) => {
const AudioMenu = forwardRef<HTMLDivElement, Props>(({ className, selectedAudioTrackId, audioTracks, onAudioTrackSelected }: Props, ref) => {
const { t } = useTranslation();
const onAudioTrackClick = useCallback(({ currentTarget }: MouseEvent) => {
@ -26,7 +26,7 @@ const AudioMenu = ({ className, selectedAudioTrackId, audioTracks, onAudioTrackS
};
return (
<div className={classNames(className, styles['audio-menu'])} onMouseDown={onMouseDown}>
<div ref={ref} className={classNames(className, styles['audio-menu'])} onMouseDown={onMouseDown}>
<div className={styles['container']}>
<div className={styles['header']}>
{ t('AUDIO_TRACKS') }
@ -62,6 +62,6 @@ const AudioMenu = ({ className, selectedAudioTrackId, audioTracks, onAudioTrackS
</div>
</div>
);
};
});
export default AudioMenu;

View file

@ -61,7 +61,7 @@ const Indicator = ({ className, videoState, disabled }: Props) => {
}, [videoState]);
return (
<Transition when={shown && !disabled} name={'fade'}>
<Transition when={shown && !disabled} name={'fade'} duration={300}>
<div className={classNames(className, styles['indicator-container'])}>
<div className={styles['indicator']}>
<div>{label} {value}</div>

View file

@ -9,7 +9,7 @@ const { useServices } = require('stremio/services');
const Option = require('./Option');
const styles = require('./styles');
const OptionsMenu = ({ className, stream, playbackDevices, extraSubtitlesTracks, selectedExtraSubtitlesTrackId }) => {
const OptionsMenu = React.forwardRef(({ className, stream, playbackDevices, extraSubtitlesTracks, selectedExtraSubtitlesTrackId }, ref) => {
const { t } = useTranslation();
const { core } = useServices();
const platform = usePlatform();
@ -108,7 +108,7 @@ const OptionsMenu = ({ className, stream, playbackDevices, extraSubtitlesTracks,
}, []);
return (
<div className={classnames(className, styles['options-menu-container'])} onMouseDown={onMouseDown}>
<div ref={ref} className={classnames(className, styles['options-menu-container'])} onMouseDown={onMouseDown}>
{
streamingUrl || downloadUrl ?
<Option
@ -167,7 +167,7 @@ const OptionsMenu = ({ className, stream, playbackDevices, extraSubtitlesTracks,
}
</div>
);
};
});
OptionsMenu.propTypes = {
className: PropTypes.string,

View file

@ -1000,15 +1000,12 @@ const Player = ({ urlParams, queryParams }) => {
:
null
}
{
statisticsMenuOpen ?
<StatisticsMenu
className={classnames(styles['layer'], styles['menu-layer'])}
{...statistics}
/>
:
null
}
<Transition when={statisticsMenuOpen} name={'fade'}>
<StatisticsMenu
className={classnames(styles['layer'], styles['menu-layer'])}
{...statistics}
/>
</Transition>
<Transition when={sideDrawerOpen} name={'slide-left'}>
<SideDrawer
className={classnames(styles['layer'], styles['side-drawer-layer'])}
@ -1018,65 +1015,53 @@ const Player = ({ urlParams, queryParams }) => {
selected={player.selected?.streamRequest?.path.id}
/>
</Transition>
{
subtitlesMenuOpen ?
<SubtitlesMenu
className={classnames(styles['layer'], styles['menu-layer'])}
subtitlesLanguage={settings.subtitlesLanguage}
interfaceLanguage={settings.interfaceLanguage}
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}
onSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
onSubtitlesSizeChanged={onSubtitlesSizeChanged}
onExtraSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
onExtraSubtitlesDelayChanged={onExtraSubtitlesDelayChanged}
onExtraSubtitlesSizeChanged={onSubtitlesSizeChanged}
/>
:
null
}
{
audioMenuOpen ?
<AudioMenu
className={classnames(styles['layer'], styles['menu-layer'])}
audioTracks={video.state.audioTracks}
selectedAudioTrackId={video.state.selectedAudioTrackId}
onAudioTrackSelected={onAudioTrackSelected}
/>
:
null
}
{
speedMenuOpen ?
<SpeedMenu
className={classnames(styles['layer'], styles['menu-layer'])}
playbackSpeed={video.state.playbackSpeed}
onPlaybackSpeedChanged={onPlaybackSpeedChanged}
/>
:
null
}
{
optionsMenuOpen ?
<OptionsMenu
className={classnames(styles['layer'], styles['menu-layer'])}
stream={player.selected.stream}
playbackDevices={playbackDevices}
extraSubtitlesTracks={video.state.extraSubtitlesTracks}
selectedExtraSubtitlesTrackId={video.state.selectedExtraSubtitlesTrackId}
/>
:
null
}
<Transition when={subtitlesMenuOpen} name={'fade'}>
<SubtitlesMenu
className={classnames(styles['layer'], styles['menu-layer'])}
subtitlesLanguage={settings.subtitlesLanguage}
interfaceLanguage={settings.interfaceLanguage}
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}
onSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
onSubtitlesSizeChanged={onSubtitlesSizeChanged}
onExtraSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
onExtraSubtitlesDelayChanged={onExtraSubtitlesDelayChanged}
onExtraSubtitlesSizeChanged={onSubtitlesSizeChanged}
/>
</Transition>
<Transition when={audioMenuOpen} name={'fade'}>
<AudioMenu
className={classnames(styles['layer'], styles['menu-layer'])}
audioTracks={video.state.audioTracks}
selectedAudioTrackId={video.state.selectedAudioTrackId}
onAudioTrackSelected={onAudioTrackSelected}
/>
</Transition>
<Transition when={speedMenuOpen} name={'fade'}>
<SpeedMenu
className={classnames(styles['layer'], styles['menu-layer'])}
playbackSpeed={video.state.playbackSpeed}
onPlaybackSpeedChanged={onPlaybackSpeedChanged}
/>
</Transition>
<Transition when={optionsMenuOpen} name={'fade'}>
<OptionsMenu
className={classnames(styles['layer'], styles['menu-layer'])}
stream={player.selected?.stream}
playbackDevices={playbackDevices}
extraSubtitlesTracks={video.state.extraSubtitlesTracks}
selectedExtraSubtitlesTrackId={video.state.selectedExtraSubtitlesTrackId}
/>
</Transition>
</div>
);
};

View file

@ -9,7 +9,7 @@ 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 = React.forwardRef(({ className, playbackSpeed, onPlaybackSpeedChanged }, ref) => {
const { t } = useTranslation();
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.speedMenuClosePrevented = true;
@ -20,7 +20,7 @@ const SpeedMenu = ({ className, playbackSpeed, onPlaybackSpeedChanged }) => {
}
}, [onPlaybackSpeedChanged]);
return (
<div className={classnames(className, styles['speed-menu-container'])} onMouseDown={onMouseDown}>
<div ref={ref} className={classnames(className, styles['speed-menu-container'])} onMouseDown={onMouseDown}>
<div className={styles['title']}>
{ t('PLAYBACK_SPEED') }
</div>
@ -39,7 +39,7 @@ const SpeedMenu = ({ className, playbackSpeed, onPlaybackSpeedChanged }) => {
</div>
</div>
);
};
});
SpeedMenu.propTypes = {
className: PropTypes.string,

View file

@ -6,10 +6,10 @@ const classNames = require('classnames');
const PropTypes = require('prop-types');
const styles = require('./styles.less');
const StatisticsMenu = ({ className, peers, speed, completed, infoHash }) => {
const StatisticsMenu = React.forwardRef(({ className, peers, speed, completed, infoHash }, ref) => {
const { t } = useTranslation();
return (
<div className={classNames(className, styles['statistics-menu-container'])}>
<div ref={ref} className={classNames(className, styles['statistics-menu-container'])}>
<div className={styles['title']}>
{t('PLAYER_STATISTICS')}
</div>
@ -49,7 +49,7 @@ const StatisticsMenu = ({ className, peers, speed, completed, infoHash }) => {
</div>
</div>
);
};
});
StatisticsMenu.propTypes = {
className: PropTypes.string,

View file

@ -30,7 +30,7 @@ const sortByValues = (items, values) => items.sort((a, b) => {
return left - right;
});
const SubtitlesMenu = React.memo((props) => {
const SubtitlesMenu = React.memo(React.forwardRef((props, ref) => {
const subtitlesTracks = React.useMemo(() => {
return normalizeTracksLang(Array.isArray(props.subtitlesTracks) ? props.subtitlesTracks : []);
}, [props.subtitlesTracks]);
@ -153,7 +153,7 @@ const SubtitlesMenu = React.memo((props) => {
}
}, [props.selectedSubtitlesTrackId, props.selectedExtraSubtitlesTrackId, props.subtitlesOffset, props.extraSubtitlesOffset, props.onSubtitlesOffsetChanged, props.onExtraSubtitlesOffsetChanged]);
return (
<div className={classnames(props.className, styles['subtitles-menu-container'])} onMouseDown={onMouseDown}>
<div ref={ref} className={classnames(props.className, styles['subtitles-menu-container'])} onMouseDown={onMouseDown}>
<div className={styles['languages-container']}>
<div className={styles['languages-header']}>{ t('PLAYER_SUBTITLES_LANGUAGES') }</div>
<div className={styles['languages-list']}>
@ -255,7 +255,7 @@ const SubtitlesMenu = React.memo((props) => {
</div>
</div>
);
});
}));
SubtitlesMenu.displayName = 'MainNavBars';