mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-21 14:26:35 +01:00
fix: Refactors Chromecast casting player
Refactors the Chromecast casting player for better compatibility, UI improvements, and stability. - Adds auto-selection of stereo audio tracks for improved Chromecast compatibility - Refactors episode list to filter out virtual episodes and allow season selection - Improves UI layout and styling - Removes connection quality indicator - Fixes progress reporting to Jellyfin - Updates volume control to use CastSession for device volume
This commit is contained in:
@@ -4,58 +4,111 @@
|
||||
*/
|
||||
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { Image } from "expo-image";
|
||||
import { router } from "expo-router";
|
||||
import { useAtomValue } from "jotai";
|
||||
import React from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Pressable, View } from "react-native";
|
||||
import {
|
||||
MediaPlayerState,
|
||||
useCastDevice,
|
||||
useMediaStatus,
|
||||
useRemoteMediaClient,
|
||||
} from "react-native-google-cast";
|
||||
import Animated, { SlideInDown, SlideOutDown } from "react-native-reanimated";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { useCasting } from "@/hooks/useCasting";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import {
|
||||
formatTime,
|
||||
getPosterUrl,
|
||||
getProtocolIcon,
|
||||
getProtocolName,
|
||||
} from "@/utils/casting/helpers";
|
||||
import { formatTime, getPosterUrl } from "@/utils/casting/helpers";
|
||||
import { CASTING_CONSTANTS } from "@/utils/casting/types";
|
||||
|
||||
export const CastingMiniPlayer: React.FC = () => {
|
||||
const api = useAtomValue(apiAtom);
|
||||
const insets = useSafeAreaInsets();
|
||||
const {
|
||||
isConnected,
|
||||
protocol,
|
||||
currentItem,
|
||||
currentDevice,
|
||||
progress,
|
||||
duration,
|
||||
isPlaying,
|
||||
togglePlayPause,
|
||||
} = useCasting(null);
|
||||
const castDevice = useCastDevice();
|
||||
const mediaStatus = useMediaStatus();
|
||||
const remoteMediaClient = useRemoteMediaClient();
|
||||
|
||||
if (!isConnected || !currentItem || !protocol) {
|
||||
const currentItem = useMemo(() => {
|
||||
return mediaStatus?.mediaInfo?.customData as BaseItemDto | undefined;
|
||||
}, [mediaStatus?.mediaInfo?.customData]);
|
||||
|
||||
// Live progress state that updates every second when playing
|
||||
const [liveProgress, setLiveProgress] = useState(
|
||||
mediaStatus?.streamPosition || 0,
|
||||
);
|
||||
|
||||
// Sync live progress with mediaStatus and poll every second when playing
|
||||
useEffect(() => {
|
||||
if (mediaStatus?.streamPosition) {
|
||||
setLiveProgress(mediaStatus.streamPosition);
|
||||
}
|
||||
|
||||
// Update every second when playing
|
||||
const interval = setInterval(() => {
|
||||
if (
|
||||
mediaStatus?.playerState === MediaPlayerState.PLAYING &&
|
||||
mediaStatus?.streamPosition !== undefined
|
||||
) {
|
||||
setLiveProgress((prev) => prev + 1);
|
||||
} else if (mediaStatus?.streamPosition !== undefined) {
|
||||
// Sync with actual position when paused/buffering
|
||||
setLiveProgress(mediaStatus.streamPosition);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [mediaStatus?.playerState, mediaStatus?.streamPosition]);
|
||||
|
||||
const progress = liveProgress * 1000; // Convert to ms
|
||||
const duration = (mediaStatus?.mediaInfo?.streamDuration || 0) * 1000;
|
||||
const isPlaying = mediaStatus?.playerState === MediaPlayerState.PLAYING;
|
||||
|
||||
// For episodes, use season poster; for other content, use item poster
|
||||
const posterUrl = useMemo(() => {
|
||||
if (!api?.basePath || !currentItem) return null;
|
||||
|
||||
if (
|
||||
currentItem.Type === "Episode" &&
|
||||
currentItem.SeriesId &&
|
||||
currentItem.ParentIndexNumber
|
||||
) {
|
||||
// Build season poster URL using SeriesId and season number
|
||||
return `${api.basePath}/Items/${currentItem.SeriesId}/Images/Primary?fillHeight=120&fillWidth=80&quality=96&tag=${currentItem.SeasonId}`;
|
||||
}
|
||||
|
||||
// For non-episodes, use item's own poster
|
||||
return getPosterUrl(
|
||||
api.basePath,
|
||||
currentItem.Id,
|
||||
currentItem.ImageTags?.Primary,
|
||||
80,
|
||||
120,
|
||||
);
|
||||
}, [api?.basePath, currentItem]);
|
||||
|
||||
if (!castDevice || !currentItem || !mediaStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const posterUrl = getPosterUrl(
|
||||
api?.basePath,
|
||||
currentItem.Id,
|
||||
currentItem.ImageTags?.Primary,
|
||||
80,
|
||||
120,
|
||||
);
|
||||
|
||||
const progressPercent = duration > 0 ? (progress / duration) * 100 : 0;
|
||||
const protocolColor = "#a855f7"; // Streamyfin purple
|
||||
const TAB_BAR_HEIGHT = 49; // Standard tab bar height
|
||||
const TAB_BAR_HEIGHT = 80; // Standard tab bar height
|
||||
|
||||
const handlePress = () => {
|
||||
router.push("/casting-player");
|
||||
};
|
||||
|
||||
const handleTogglePlayPause = (e: any) => {
|
||||
e.stopPropagation();
|
||||
if (isPlaying) {
|
||||
remoteMediaClient?.pause();
|
||||
} else {
|
||||
remoteMediaClient?.play();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
entering={SlideInDown.duration(CASTING_CONSTANTS.ANIMATION_DURATION)}
|
||||
@@ -141,11 +194,7 @@ export const CastingMiniPlayer: React.FC = () => {
|
||||
marginTop: 2,
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name={getProtocolIcon(protocol)}
|
||||
size={12}
|
||||
color={protocolColor}
|
||||
/>
|
||||
<Ionicons name='tv' size={12} color={protocolColor} />
|
||||
<Text
|
||||
style={{
|
||||
color: protocolColor,
|
||||
@@ -153,7 +202,7 @@ export const CastingMiniPlayer: React.FC = () => {
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{currentDevice?.name || getProtocolName(protocol)}
|
||||
{castDevice.friendlyName || "Chromecast"}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
@@ -167,17 +216,7 @@ export const CastingMiniPlayer: React.FC = () => {
|
||||
</View>
|
||||
|
||||
{/* Play/Pause button */}
|
||||
<Pressable
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
if (isConnected && protocol) {
|
||||
togglePlayPause();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
padding: 8,
|
||||
}}
|
||||
>
|
||||
<Pressable onPress={handleTogglePlayPause} style={{ padding: 8 }}>
|
||||
<Ionicons
|
||||
name={isPlaying ? "pause" : "play"}
|
||||
size={28}
|
||||
|
||||
@@ -8,6 +8,7 @@ import React, { useEffect, useState } from "react";
|
||||
import { Modal, Pressable, View } from "react-native";
|
||||
import { Slider } from "react-native-awesome-slider";
|
||||
import type { Device } from "react-native-google-cast";
|
||||
import { useCastSession, useRemoteMediaClient } 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";
|
||||
@@ -19,9 +20,6 @@ interface ChromecastDeviceSheetProps {
|
||||
onDisconnect: () => Promise<void>;
|
||||
volume?: number;
|
||||
onVolumeChange?: (volume: number) => Promise<void>;
|
||||
showTechnicalInfo?: boolean;
|
||||
connectionQuality?: "excellent" | "good" | "fair" | "poor";
|
||||
bitrate?: number;
|
||||
}
|
||||
|
||||
export const ChromecastDeviceSheet: React.FC<ChromecastDeviceSheetProps> = ({
|
||||
@@ -31,19 +29,34 @@ export const ChromecastDeviceSheet: React.FC<ChromecastDeviceSheetProps> = ({
|
||||
onDisconnect,
|
||||
volume = 0.5,
|
||||
onVolumeChange,
|
||||
showTechnicalInfo = false,
|
||||
connectionQuality = "good",
|
||||
bitrate,
|
||||
}) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const [isDisconnecting, setIsDisconnecting] = useState(false);
|
||||
const volumeValue = useSharedValue(volume * 100);
|
||||
const minimumValue = useSharedValue(0);
|
||||
const maximumValue = useSharedValue(100);
|
||||
const castSession = useCastSession();
|
||||
const remoteMediaClient = useRemoteMediaClient();
|
||||
|
||||
// Sync volume slider with prop changes
|
||||
// Sync volume slider with prop changes (updates from physical buttons)
|
||||
useEffect(() => {
|
||||
volumeValue.value = volume * 100;
|
||||
}, [volume, volumeValue]);
|
||||
|
||||
// Poll for volume updates when sheet is visible to catch physical button changes
|
||||
useEffect(() => {
|
||||
if (!visible || !remoteMediaClient) return;
|
||||
|
||||
// Request status update to get latest volume from device
|
||||
const interval = setInterval(() => {
|
||||
remoteMediaClient.requestStatus().catch(() => {
|
||||
// Ignore errors - device might be disconnected
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [visible, remoteMediaClient]);
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
setIsDisconnecting(true);
|
||||
try {
|
||||
@@ -57,8 +70,19 @@ export const ChromecastDeviceSheet: React.FC<ChromecastDeviceSheetProps> = ({
|
||||
};
|
||||
|
||||
const handleVolumeComplete = async (value: number) => {
|
||||
if (onVolumeChange) {
|
||||
await onVolumeChange(value / 100);
|
||||
const newVolume = value / 100;
|
||||
try {
|
||||
// Use CastSession.setVolume for DEVICE volume control
|
||||
// This works even when no media is playing, unlike setStreamVolume
|
||||
if (castSession) {
|
||||
castSession.setVolume(newVolume);
|
||||
console.log("[Volume] Set device volume via CastSession:", newVolume);
|
||||
} else if (onVolumeChange) {
|
||||
// Fallback to prop method if session not available
|
||||
await onVolumeChange(newVolume);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Volume] Error setting volume:", error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -120,61 +144,7 @@ export const ChromecastDeviceSheet: React.FC<ChromecastDeviceSheetProps> = ({
|
||||
{device?.friendlyName || device?.deviceId || "Unknown Device"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Connection Quality */}
|
||||
<View style={{ marginBottom: 20 }}>
|
||||
<Text style={{ color: "#999", fontSize: 12, marginBottom: 8 }}>
|
||||
Connection Quality
|
||||
</Text>
|
||||
<View
|
||||
style={{ flexDirection: "row", alignItems: "center", gap: 8 }}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: 6,
|
||||
backgroundColor:
|
||||
connectionQuality === "excellent"
|
||||
? "#10b981"
|
||||
: connectionQuality === "good"
|
||||
? "#fbbf24"
|
||||
: connectionQuality === "fair"
|
||||
? "#f97316"
|
||||
: "#ef4444",
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color:
|
||||
connectionQuality === "excellent"
|
||||
? "#10b981"
|
||||
: connectionQuality === "good"
|
||||
? "#fbbf24"
|
||||
: connectionQuality === "fair"
|
||||
? "#f97316"
|
||||
: "#ef4444",
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
textTransform: "capitalize",
|
||||
}}
|
||||
>
|
||||
{connectionQuality}
|
||||
</Text>
|
||||
</View>
|
||||
{bitrate && (
|
||||
<Text style={{ color: "#999", fontSize: 12, marginTop: 4 }}>
|
||||
Bitrate: {(bitrate / 1000000).toFixed(1)} Mbps
|
||||
{connectionQuality === "poor" &&
|
||||
" (Low bitrate may cause buffering)"}
|
||||
{connectionQuality === "fair" && " (Moderate quality)"}
|
||||
{connectionQuality === "good" && " (Good quality)"}
|
||||
{connectionQuality === "excellent" && " (Maximum quality)"}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{device?.deviceId && showTechnicalInfo && (
|
||||
{device?.deviceId && (
|
||||
<View style={{ marginBottom: 20 }}>
|
||||
<Text style={{ color: "#999", fontSize: 12, marginBottom: 4 }}>
|
||||
Device ID
|
||||
@@ -183,11 +153,10 @@ export const ChromecastDeviceSheet: React.FC<ChromecastDeviceSheetProps> = ({
|
||||
style={{ color: "white", fontSize: 14 }}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{device.deviceId}
|
||||
{device?.deviceId}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Volume control */}
|
||||
<View style={{ marginBottom: 24 }}>
|
||||
<View
|
||||
@@ -211,8 +180,8 @@ export const ChromecastDeviceSheet: React.FC<ChromecastDeviceSheetProps> = ({
|
||||
<Slider
|
||||
style={{ width: "100%", height: 40 }}
|
||||
progress={volumeValue}
|
||||
minimumValue={useSharedValue(0)}
|
||||
maximumValue={useSharedValue(100)}
|
||||
minimumValue={minimumValue}
|
||||
maximumValue={maximumValue}
|
||||
theme={{
|
||||
disableMinTrackTintColor: "#333",
|
||||
maximumTrackTintColor: "#333",
|
||||
@@ -231,13 +200,11 @@ export const ChromecastDeviceSheet: React.FC<ChromecastDeviceSheetProps> = ({
|
||||
}}
|
||||
onSlidingComplete={handleVolumeComplete}
|
||||
panHitSlop={{ top: 20, bottom: 20, left: 0, right: 0 }}
|
||||
disable={false}
|
||||
/>
|
||||
</View>
|
||||
<Ionicons name='volume-high' size={20} color='#999' />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Disconnect button */}
|
||||
<Pressable
|
||||
onPress={handleDisconnect}
|
||||
@@ -253,7 +220,12 @@ export const ChromecastDeviceSheet: React.FC<ChromecastDeviceSheetProps> = ({
|
||||
opacity: isDisconnecting ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
<Ionicons name='power' size={20} color='white' />
|
||||
<Ionicons
|
||||
name='power'
|
||||
size={20}
|
||||
color='white'
|
||||
style={{ marginTop: 2 }}
|
||||
/>
|
||||
<Text style={{ color: "white", fontSize: 16, fontWeight: "600" }}>
|
||||
{isDisconnecting ? "Disconnecting..." : "Stop Casting"}
|
||||
</Text>
|
||||
|
||||
@@ -7,8 +7,8 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import type { Api } from "@jellyfin/sdk";
|
||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { Image } from "expo-image";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { FlatList, Modal, Pressable, View } from "react-native";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { FlatList, Modal, Pressable, ScrollView, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { truncateTitle } from "@/utils/casting/helpers";
|
||||
@@ -33,10 +33,47 @@ export const ChromecastEpisodeList: React.FC<ChromecastEpisodeListProps> = ({
|
||||
}) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const flatListRef = useRef<FlatList>(null);
|
||||
const [selectedSeason, setSelectedSeason] = useState<number | null>(null);
|
||||
|
||||
// Get unique seasons from episodes
|
||||
const seasons = useMemo(() => {
|
||||
const seasonSet = new Set<number>();
|
||||
for (const ep of episodes) {
|
||||
if (ep.ParentIndexNumber !== undefined && ep.ParentIndexNumber !== null) {
|
||||
seasonSet.add(ep.ParentIndexNumber);
|
||||
}
|
||||
}
|
||||
return Array.from(seasonSet).sort((a, b) => a - b);
|
||||
}, [episodes]);
|
||||
|
||||
// Filter episodes by selected season and exclude virtual episodes
|
||||
const filteredEpisodes = useMemo(() => {
|
||||
let eps = episodes;
|
||||
|
||||
// Filter by season if selected
|
||||
if (selectedSeason !== null) {
|
||||
eps = eps.filter((ep) => ep.ParentIndexNumber === selectedSeason);
|
||||
}
|
||||
|
||||
// Filter out virtual episodes (episodes without actual video files)
|
||||
// LocationType === "Virtual" means the episode doesn't have a media file
|
||||
eps = eps.filter((ep) => ep.LocationType !== "Virtual");
|
||||
|
||||
return eps;
|
||||
}, [episodes, selectedSeason]);
|
||||
|
||||
// Set initial season to current episode's season
|
||||
useEffect(() => {
|
||||
if (currentItem?.ParentIndexNumber !== undefined) {
|
||||
setSelectedSeason(currentItem.ParentIndexNumber);
|
||||
}
|
||||
}, [currentItem]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && currentItem && episodes.length > 0) {
|
||||
const currentIndex = episodes.findIndex((ep) => ep.Id === currentItem.Id);
|
||||
if (visible && currentItem && filteredEpisodes.length > 0) {
|
||||
const currentIndex = filteredEpisodes.findIndex(
|
||||
(ep) => ep.Id === currentItem.Id,
|
||||
);
|
||||
if (currentIndex !== -1 && flatListRef.current) {
|
||||
// Delay to ensure FlatList is rendered
|
||||
setTimeout(() => {
|
||||
@@ -48,7 +85,7 @@ export const ChromecastEpisodeList: React.FC<ChromecastEpisodeListProps> = ({
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
}, [visible, currentItem, episodes]);
|
||||
}, [visible, currentItem, filteredEpisodes]);
|
||||
|
||||
const renderEpisode = ({ item }: { item: BaseItemDto }) => {
|
||||
const isCurrentEpisode = item.Id === currentItem?.Id;
|
||||
@@ -125,6 +162,15 @@ export const ChromecastEpisodeList: React.FC<ChromecastEpisodeListProps> = ({
|
||||
</Text>
|
||||
)}
|
||||
<View style={{ flexDirection: "row", gap: 8, alignItems: "center" }}>
|
||||
{item.ParentIndexNumber !== undefined &&
|
||||
item.IndexNumber !== undefined && (
|
||||
<Text
|
||||
style={{ color: "#a855f7", fontSize: 11, fontWeight: "600" }}
|
||||
>
|
||||
S{String(item.ParentIndexNumber).padStart(2, "0")}:E
|
||||
{String(item.IndexNumber).padStart(2, "0")}
|
||||
</Text>
|
||||
)}
|
||||
{item.ProductionYear && (
|
||||
<Text style={{ color: "#666", fontSize: 11 }}>
|
||||
{item.ProductionYear}
|
||||
@@ -176,27 +222,66 @@ export const ChromecastEpisodeList: React.FC<ChromecastEpisodeListProps> = ({
|
||||
{/* Header */}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#333",
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "white", fontSize: 18, fontWeight: "600" }}>
|
||||
Episodes
|
||||
</Text>
|
||||
<Pressable onPress={onClose} style={{ padding: 8 }}>
|
||||
<Ionicons name='close' size={24} color='white' />
|
||||
</Pressable>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: seasons.length > 1 ? 12 : 0,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "white", fontSize: 18, fontWeight: "600" }}>
|
||||
Episodes
|
||||
</Text>
|
||||
<Pressable onPress={onClose} style={{ padding: 8 }}>
|
||||
<Ionicons name='close' size={24} color='white' />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* Season selector */}
|
||||
{seasons.length > 1 && (
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={{ gap: 8 }}
|
||||
>
|
||||
{seasons.map((season) => (
|
||||
<Pressable
|
||||
key={season}
|
||||
onPress={() => setSelectedSeason(season)}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
backgroundColor:
|
||||
selectedSeason === season ? "#a855f7" : "#1a1a1a",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: "white",
|
||||
fontSize: 14,
|
||||
fontWeight: selectedSeason === season ? "600" : "400",
|
||||
}}
|
||||
>
|
||||
Season {season}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Episode list */}
|
||||
<FlatList
|
||||
ref={flatListRef}
|
||||
data={episodes}
|
||||
data={filteredEpisodes}
|
||||
renderItem={renderEpisode}
|
||||
keyExtractor={(item) => item.Id || ""}
|
||||
contentContainerStyle={{
|
||||
|
||||
@@ -30,8 +30,6 @@ interface ChromecastSettingsMenuProps {
|
||||
onSubtitleTrackChange: (track: SubtitleTrack | null) => void;
|
||||
playbackSpeed: number;
|
||||
onPlaybackSpeedChange: (speed: number) => void;
|
||||
showTechnicalInfo: boolean;
|
||||
onToggleTechnicalInfo: () => void;
|
||||
}
|
||||
|
||||
const PLAYBACK_SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
|
||||
@@ -51,8 +49,6 @@ export const ChromecastSettingsMenu: React.FC<ChromecastSettingsMenuProps> = ({
|
||||
onSubtitleTrackChange,
|
||||
playbackSpeed,
|
||||
onPlaybackSpeedChange,
|
||||
showTechnicalInfo,
|
||||
onToggleTechnicalInfo,
|
||||
}) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const [expandedSection, setExpandedSection] = useState<string | null>(null);
|
||||
@@ -316,50 +312,6 @@ export const ChromecastSettingsMenu: React.FC<ChromecastSettingsMenuProps> = ({
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Technical Info Toggle */}
|
||||
<Pressable
|
||||
onPress={onToggleTechnicalInfo}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#333",
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
|
||||
>
|
||||
<Ionicons name='information-circle' size={20} color='white' />
|
||||
<Text
|
||||
style={{ color: "white", fontSize: 16, fontWeight: "500" }}
|
||||
>
|
||||
Show Technical Info
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
width: 50,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
backgroundColor: showTechnicalInfo ? "#a855f7" : "#333",
|
||||
justifyContent: "center",
|
||||
alignItems: showTechnicalInfo ? "flex-end" : "flex-start",
|
||||
padding: 2,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 13,
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
</ScrollView>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
|
||||
Reference in New Issue
Block a user