From 7d0910d3823b87a9c79528d102ce5226e75ac877 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Fri, 19 Jun 2026 15:39:04 +0300 Subject: [PATCH] feat: add full casting support --- .../CastDevicesMenu/CastDevicesMenu.tsx | 56 +++++++++++++++++ src/routes/Player/CastDevicesMenu/index.ts | 5 ++ src/routes/Player/CastDevicesMenu/styles.less | 13 ++++ src/routes/Player/ControlBar/ControlBar.js | 21 ++++++- src/routes/Player/OptionsMenu/OptionsMenu.js | 25 ++------ src/routes/Player/Player.js | 62 ++++++++++++++++++- src/routes/Player/usePlayOnDevice.ts | 36 +++++++++++ 7 files changed, 194 insertions(+), 24 deletions(-) create mode 100644 src/routes/Player/CastDevicesMenu/CastDevicesMenu.tsx create mode 100644 src/routes/Player/CastDevicesMenu/index.ts create mode 100644 src/routes/Player/CastDevicesMenu/styles.less create mode 100644 src/routes/Player/usePlayOnDevice.ts diff --git a/src/routes/Player/CastDevicesMenu/CastDevicesMenu.tsx b/src/routes/Player/CastDevicesMenu/CastDevicesMenu.tsx new file mode 100644 index 000000000..1dcafdb8d --- /dev/null +++ b/src/routes/Player/CastDevicesMenu/CastDevicesMenu.tsx @@ -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(({ 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 ( +
+ { + devices.length > 0 ? + devices.map(({ id, name }) => ( +
+ ); +})); + +export default CastDevicesMenu; diff --git a/src/routes/Player/CastDevicesMenu/index.ts b/src/routes/Player/CastDevicesMenu/index.ts new file mode 100644 index 000000000..76ea8ae60 --- /dev/null +++ b/src/routes/Player/CastDevicesMenu/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2026 Smart code 203358507 + +import CastDevicesMenu from './CastDevicesMenu'; + +export default CastDevicesMenu; diff --git a/src/routes/Player/CastDevicesMenu/styles.less b/src/routes/Player/CastDevicesMenu/styles.less new file mode 100644 index 000000000..4b4d861c6 --- /dev/null +++ b/src/routes/Player/CastDevicesMenu/styles.less @@ -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; + } +} diff --git a/src/routes/Player/ControlBar/ControlBar.js b/src/routes/Player/ControlBar/ControlBar.js index 647fb2775..bffbf8ecd 100644 --- a/src/routes/Player/ControlBar/ControlBar.js +++ b/src/routes/Player/ControlBar/ControlBar.js @@ -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); @@ -163,7 +178,7 @@ const ControlBar = React.forwardRef(({ -