/** * Chromecast Connection Menu * Shows device info, volume control, and disconnect option * Simple menu for when connected but not actively controlling playback */ import { Ionicons } from "@expo/vector-icons"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Modal, Pressable, View } from "react-native"; import { Slider } from "react-native-awesome-slider"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { useCastDevice, useCastSession } from "react-native-google-cast"; import { useSharedValue } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Text } from "@/components/common/Text"; interface ChromecastConnectionMenuProps { visible: boolean; onClose: () => void; onDisconnect?: () => Promise; } export const ChromecastConnectionMenu: React.FC< ChromecastConnectionMenuProps > = ({ visible, onClose, onDisconnect }) => { const insets = useSafeAreaInsets(); const { t } = useTranslation(); const castDevice = useCastDevice(); const castSession = useCastSession(); // Volume state - use refs to avoid triggering re-renders during sliding const [displayVolume, setDisplayVolume] = useState(50); const [isMuted, setIsMuted] = useState(false); const isMutedRef = useRef(false); const volumeValue = useSharedValue(50); const minimumValue = useSharedValue(0); const maximumValue = useSharedValue(100); const isSliding = useRef(false); const lastSetVolume = useRef(50); const protocolColor = "#a855f7"; // Get initial volume and mute state when menu opens useEffect(() => { if (!visible || !castSession) return; // Get initial states const fetchInitialState = async () => { try { const vol = await castSession.getVolume(); if (vol !== undefined) { const percent = Math.round(vol * 100); setDisplayVolume(percent); volumeValue.value = percent; lastSetVolume.current = percent; } const muted = await castSession.isMute(); isMutedRef.current = muted; setIsMuted(muted); } catch { // Ignore errors } }; fetchInitialState(); // Poll for external volume changes (physical buttons) - only when not sliding const interval = setInterval(async () => { if (isSliding.current) return; try { const vol = await castSession.getVolume(); if (vol !== undefined) { const percent = Math.round(vol * 100); // Only update if external change detected (not our own change) if (Math.abs(percent - lastSetVolume.current) > 2) { setDisplayVolume(percent); volumeValue.value = percent; lastSetVolume.current = percent; } } const muted = await castSession.isMute(); if (muted !== isMutedRef.current) { isMutedRef.current = muted; setIsMuted(muted); } } catch { // Ignore errors } }, 1000); // Poll less frequently return () => clearInterval(interval); }, [visible, castSession, volumeValue]); // Volume change during sliding - update display only, don't call API const handleVolumeChange = useCallback((value: number) => { const rounded = Math.round(value); setDisplayVolume(rounded); }, []); // Volume change complete - call API const handleVolumeComplete = useCallback( async (value: number) => { isSliding.current = false; const rounded = Math.round(value); setDisplayVolume(rounded); lastSetVolume.current = rounded; try { if (castSession) { await castSession.setVolume(value / 100); } } catch (error) { console.error("[Connection Menu] Volume error:", error); } }, [castSession], ); // Toggle mute const handleToggleMute = useCallback(async () => { if (!castSession) return; try { const newMute = !isMuted; await castSession.setMute(newMute); isMutedRef.current = newMute; setIsMuted(newMute); } catch (error) { console.error("[Connection Menu] Mute error:", error); } }, [castSession, isMuted]); // Disconnect const handleDisconnect = useCallback(async () => { try { if (onDisconnect) { await onDisconnect(); } } catch (error) { console.error("[Connection Menu] Disconnect error:", error); } finally { onClose(); } }, [onDisconnect, onClose]); return ( e.stopPropagation()} > {/* Header with device name */} {castDevice?.friendlyName || t("casting_player.chromecast")} {t("casting_player.connected")} {/* Volume Control */} {t("casting_player.volume")} {isMuted ? t("casting_player.muted") : `${displayVolume}%`} { isSliding.current = true; }} onValueChange={async (value) => { volumeValue.value = value; handleVolumeChange(value); if (isMuted) { isMutedRef.current = false; setIsMuted(false); try { await castSession?.setMute(false); } catch (error: unknown) { console.error( "[ChromecastConnectionMenu] Failed to unmute:", error, ); isMutedRef.current = true; setIsMuted(true); // Rollback on failure } } }} onSlidingComplete={handleVolumeComplete} panHitSlop={{ top: 20, bottom: 20, left: 0, right: 0 }} /> {/* Disconnect button */} {t("casting_player.disconnect")} ); };