This commit is contained in:
Timothy Z. 2026-05-20 13:03:17 +00:00 committed by GitHub
commit 14c20c4ea0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 150 additions and 4 deletions

View file

@ -0,0 +1,56 @@
// Copyright (C) 2017-2026 Smart code 203358507
import React, { forwardRef, memo, MouseEvent } from 'react';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import Option from '../OptionsMenu/Option';
import styles from './styles.less';
type CastDevice = {
id: string,
name: string,
};
type Props = {
className: string,
devices: CastDevice[],
loading: boolean,
onDeviceSelected: (deviceId: string) => void,
};
const CastDevicesMenu = memo(forwardRef<HTMLDivElement, Props>(({ className, devices, loading, onDeviceSelected }, ref) => {
const { t } = useTranslation();
const onMouseDown = (event: MouseEvent) => {
// @ts-expect-error: Property 'castDevicesMenuClosePrevented' does not exist on type 'MouseEvent'.
event.nativeEvent.castDevicesMenuClosePrevented = true;
};
return (
<div ref={ref} className={classNames(className, styles['cast-devices-menu-container'])} onMouseDown={onMouseDown}>
{
devices.length > 0 ?
devices.map(({ id, name }) => (
<Option
key={id}
icon={'cast'}
label={t('PLAYER_PLAY_IN', { device: name })}
deviceId={id}
onClick={onDeviceSelected}
/>
))
:
<div className={styles['message']}>
{
loading ?
t('STREAM_LOADING')
:
t('PLAYER_NO_CAST_DEVICES_FOUND', { defaultValue: 'No cast devices found' })
}
</div>
}
</div>
);
}));
export default CastDevicesMenu;

View file

@ -0,0 +1,5 @@
// Copyright (C) 2017-2026 Smart code 203358507
import CastDevicesMenu from './CastDevicesMenu';
export default CastDevicesMenu;

View file

@ -0,0 +1,13 @@
// Copyright (C) 2017-2026 Smart code 203358507
.cast-devices-menu-container {
width: 16rem;
padding: 1rem;
.message {
padding: 1rem;
color: var(--primary-foreground-color);
opacity: 0.7;
text-align: center;
}
}

View file

@ -39,6 +39,8 @@ const ControlBar = React.forwardRef(({
onToggleSpeedMenu,
onToggleSideDrawer,
onToggleOptionsMenu,
shellCastSupported,
onToggleCastDevicesMenu,
videoScale,
videoScaleLabel,
onVideoScaleChanged,
@ -68,6 +70,9 @@ const ControlBar = React.forwardRef(({
const onStatisticsButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.statisticsMenuClosePrevented = true;
}, []);
const onCastDevicesButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.castDevicesMenuClosePrevented = true;
}, []);
const onPlayPauseButtonClick = React.useCallback(() => {
if (paused) {
if (typeof onPlayRequested === 'function') {
@ -95,9 +100,19 @@ const ControlBar = React.forwardRef(({
}
}
}, [muted, onMuteRequested, onUnmuteRequested]);
const castButtonDisabled = platform.shell.active ? !shellCastSupported : !chromecastServiceActive;
const onChromecastButtonClick = React.useCallback(() => {
if (platform.shell.active) {
if (shellCastSupported && typeof onToggleCastDevicesMenu === 'function') {
onToggleCastDevicesMenu();
}
return;
}
if (castButtonDisabled) {
return;
}
chromecast.transport.requestSession();
}, []);
}, [castButtonDisabled, platform.shell.active, shellCastSupported, onToggleCastDevicesMenu]);
React.useEffect(() => {
const onStateChanged = () => {
setChromecastServiceActive(chromecast.active);
@ -162,7 +177,7 @@ const ControlBar = React.forwardRef(({
<Button className={classnames(styles['control-bar-button'], { 'disabled': playbackSpeed === null })} tabIndex={-1} onMouseDown={onSpeedButtonMouseDown} onClick={onToggleSpeedMenu}>
<Icon className={styles['icon']} name={'speed'} />
</Button>
<Button className={classnames(styles['control-bar-button'], { 'disabled': !chromecastServiceActive })} tabIndex={-1} onClick={onChromecastButtonClick}>
<Button className={classnames(styles['control-bar-button'], { 'disabled': castButtonDisabled })} tabIndex={-1} onMouseDown={onCastDevicesButtonMouseDown} onClick={onChromecastButtonClick}>
<Icon className={styles['icon']} name={'cast'} />
</Button>
<Button className={classnames(styles['control-bar-button'], { 'disabled': !Array.isArray(subtitlesTracks) || subtitlesTracks.length === 0 })} tabIndex={-1} onMouseDown={onSubtitlesButtonMouseDown} onClick={onToggleSubtitlesMenu}>
@ -221,6 +236,8 @@ ControlBar.propTypes = {
onToggleSpeedMenu: PropTypes.func,
onToggleSideDrawer: PropTypes.func,
onToggleOptionsMenu: PropTypes.func,
shellCastSupported: PropTypes.bool,
onToggleCastDevicesMenu: PropTypes.func,
onToggleStatisticsMenu: PropTypes.func,
onMouseOver: PropTypes.func,
onMouseMove: PropTypes.func,

View file

@ -19,6 +19,7 @@ const ControlBar = require('./ControlBar');
const NextVideoPopup = require('./NextVideoPopup');
const StatisticsMenu = require('./StatisticsMenu');
const OptionsMenu = require('./OptionsMenu');
const { default: CastDevicesMenu } = require('./CastDevicesMenu');
const SubtitlesMenu = require('./SubtitlesMenu');
const { default: AudioMenu } = require('./AudioMenu');
const SpeedMenu = require('./SpeedMenu');
@ -82,12 +83,13 @@ const Player = ({ urlParams, queryParams }) => {
const [audioMenuOpen, , closeAudioMenu, toggleAudioMenu] = useBinaryState(false);
const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false);
const [statisticsMenuOpen, , closeStatisticsMenu, toggleStatisticsMenu] = useBinaryState(false);
const [castDevicesMenuOpen, , closeCastDevicesMenu, toggleCastDevicesMenu] = useBinaryState(false);
const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false);
const [sideDrawerOpen, , closeSideDrawer, toggleSideDrawer] = useBinaryState(false);
const menusOpen = React.useMemo(() => {
return optionsMenuOpen || subtitlesMenuOpen || audioMenuOpen || speedMenuOpen || statisticsMenuOpen || sideDrawerOpen || nextVideoPopupOpen;
}, [optionsMenuOpen, subtitlesMenuOpen, audioMenuOpen, speedMenuOpen, statisticsMenuOpen, sideDrawerOpen, nextVideoPopupOpen]);
return optionsMenuOpen || subtitlesMenuOpen || audioMenuOpen || speedMenuOpen || statisticsMenuOpen || castDevicesMenuOpen || sideDrawerOpen || nextVideoPopupOpen;
}, [optionsMenuOpen, subtitlesMenuOpen, audioMenuOpen, speedMenuOpen, statisticsMenuOpen, castDevicesMenuOpen, sideDrawerOpen, nextVideoPopupOpen]);
const closeMenus = React.useCallback(() => {
closeOptionsMenu();
@ -95,9 +97,49 @@ const Player = ({ urlParams, queryParams }) => {
closeAudioMenu();
closeSpeedMenu();
closeStatisticsMenu();
closeCastDevicesMenu();
closeSideDrawer();
}, []);
const castDevices = React.useMemo(() => {
return playbackDevices.filter(({ type }) => type === 'chromecast' || type === 'tv');
}, [playbackDevices]);
const castDevicesLoading = platform.shell.active && streamingServer.playbackDevices !== null && streamingServer.playbackDevices.type === 'Loading';
const castStreamingUrl = React.useMemo(() => {
return player.selected?.stream?.deepLinks?.externalPlayer?.streaming || null;
}, [player.selected]);
const shellCastSupported = platform.shell.active && castStreamingUrl !== null;
const refreshCastDevices = React.useCallback(() => {
if (platform.shell.active) {
core.transport.dispatch({
action: 'StreamingServer',
args: {
action: 'RefreshPlaybackDevices',
}
});
}
}, [platform.shell.active]);
const onCastDeviceSelected = React.useCallback((deviceId) => {
if (castStreamingUrl) {
core.transport.dispatch({
action: 'StreamingServer',
args: {
action: 'PlayOnDevice',
args: {
device: deviceId,
source: castStreamingUrl,
}
}
});
closeCastDevicesMenu();
}
}, [castStreamingUrl]);
React.useEffect(() => {
if (castDevicesMenuOpen) {
refreshCastDevices();
}
}, [castDevicesMenuOpen, refreshCastDevices]);
const {
streamSubtitles,
allSubtitleTracks,
@ -294,6 +336,9 @@ const Player = ({ urlParams, queryParams }) => {
if (!event.nativeEvent.statisticsMenuClosePrevented) {
closeStatisticsMenu();
}
if (!event.nativeEvent.castDevicesMenuClosePrevented) {
closeCastDevicesMenu();
}
closeSideDrawer();
}, []);
@ -878,6 +923,8 @@ const Player = ({ urlParams, queryParams }) => {
onVolumeChangeRequested={onVolumeChangeRequested}
onSeekRequested={onSeekRequested}
onToggleOptionsMenu={toggleOptionsMenu}
shellCastSupported={shellCastSupported}
onToggleCastDevicesMenu={toggleCastDevicesMenu}
onToggleSubtitlesMenu={toggleSubtitlesMenu}
onToggleAudioMenu={toggleAudioMenu}
onToggleSpeedMenu={toggleSpeedMenu}
@ -913,6 +960,14 @@ const Player = ({ urlParams, queryParams }) => {
{...statistics}
/>
</Transition>
<Transition when={castDevicesMenuOpen} name={'fade'}>
<CastDevicesMenu
className={classnames(styles['layer'], styles['menu-layer'])}
devices={castDevices}
loading={castDevicesLoading}
onDeviceSelected={onCastDeviceSelected}
/>
</Transition>
<Transition when={sideDrawerOpen} name={'slide-left'}>
<SideDrawer
className={classnames(styles['layer'], styles['side-drawer-layer'])}