diff --git a/src/components/player/modals/AudioTrackModal.tsx b/src/components/player/modals/AudioTrackModal.tsx index 97ec9852..cd37b69e 100644 --- a/src/components/player/modals/AudioTrackModal.tsx +++ b/src/components/player/modals/AudioTrackModal.tsx @@ -1,19 +1,18 @@ import React from 'react'; -import { View, Text, TouchableOpacity, ScrollView, useWindowDimensions, StyleSheet, Platform } from 'react-native'; +import { View, Text, TouchableOpacity, ScrollView, StyleSheet, Platform, useWindowDimensions } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import Animated, { FadeIn, FadeOut, - SlideInDown, - SlideOutDown, + SlideInRight, + SlideOutRight, } from 'react-native-reanimated'; -import { getTrackDisplayName, DEBUG_MODE } from '../utils/playerUtils'; -import { logger } from '../../../utils/logger'; +import { getTrackDisplayName } from '../utils/playerUtils'; interface AudioTrackModalProps { showAudioModal: boolean; setShowAudioModal: (show: boolean) => void; - ksAudioTracks: Array<{id: number, name: string, language?: string}>; + ksAudioTracks: Array<{ id: number, name: string, language?: string }>; selectedAudioTrack: number | null; selectAudioTrack: (trackId: number) => void; } @@ -25,98 +24,89 @@ export const AudioTrackModal: React.FC = ({ selectedAudioTrack, selectAudioTrack, }) => { - const { width, height } = useWindowDimensions(); - - const menuWidth = Math.min(width * 0.9, 420); - const menuMaxHeight = height * 0.9; + const { width } = useWindowDimensions(); + const MENU_WIDTH = Math.min(width * 0.85, 400); const handleClose = () => setShowAudioModal(false); if (!showAudioModal) return null; return ( - - {/* Backdrop matching SubtitleModal */} - - + + + - {/* Center Alignment Container */} - - - {/* Header with shared aesthetics */} - - Audio Tracks + + + + Audio Tracks + - - - {ksAudioTracks.map((track) => { - const isSelected = selectedAudioTrack === track.id; + + + {ksAudioTracks.map((track) => { + const isSelected = selectedAudioTrack === track.id; - return ( - { - selectAudioTrack(track.id); - setTimeout(handleClose, 200); - }} - style={{ - padding: 10, - borderRadius: 12, - backgroundColor: isSelected ? 'white' : 'rgba(255,255,255,0.05)', // Matches SubtitleModal item colors - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center' - }} - > - - - {getTrackDisplayName(track)} - - - {isSelected && } - - ); - })} + return ( + { + selectAudioTrack(track.id); + setTimeout(handleClose, 200); + }} + style={{ + paddingHorizontal: 16, + paddingVertical: 12, + borderRadius: 12, + backgroundColor: isSelected ? 'white' : 'rgba(255,255,255,0.06)', + borderWidth: 1, + borderColor: isSelected ? 'white' : 'rgba(255,255,255,0.1)', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center' + }} + > + + + {getTrackDisplayName(track)} + + + {isSelected && } + + ); + })} - {ksAudioTracks.length === 0 && ( - - - No audio tracks available - - )} - - - - + {ksAudioTracks.length === 0 && ( + + + No audio tracks available + + )} + + + ); }; diff --git a/src/components/player/modals/EpisodeStreamsModal.tsx b/src/components/player/modals/EpisodeStreamsModal.tsx index 969687a5..e41722b1 100644 --- a/src/components/player/modals/EpisodeStreamsModal.tsx +++ b/src/components/player/modals/EpisodeStreamsModal.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect } from 'react'; -import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, Dimensions } from 'react-native'; +import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, StyleSheet, Platform, useWindowDimensions } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; -import Animated, { - FadeIn, +import Animated, { + FadeIn, FadeOut, SlideInRight, SlideOutRight, @@ -20,16 +20,13 @@ interface EpisodeStreamsModalProps { metadata?: { id?: string; name?: string }; } -const { width } = Dimensions.get('window'); -const MENU_WIDTH = Math.min(width * 0.85, 400); - const QualityBadge = ({ quality }: { quality: string | null }) => { if (!quality) return null; - + const qualityNum = parseInt(quality); let color = '#8B5CF6'; let label = `${quality}p`; - + if (qualityNum >= 2160) { color = '#F59E0B'; label = '4K'; @@ -40,9 +37,9 @@ const QualityBadge = ({ quality }: { quality: string | null }) => { color = '#10B981'; label = 'HD'; } - + return ( - = ({ onSelectStream, metadata, }) => { + const { width } = useWindowDimensions(); + const MENU_WIDTH = Math.min(width * 0.85, 400); + const [availableStreams, setAvailableStreams] = useState<{ [providerId: string]: { streams: Stream[]; addonName: string } }>({}); const [isLoading, setIsLoading] = useState(false); const [hasErrors, setHasErrors] = useState([]); @@ -89,35 +89,34 @@ export const EpisodeStreamsModal: React.FC = ({ const fetchStreams = async () => { if (!episode || !metadata?.id) return; - + setIsLoading(true); setHasErrors([]); setAvailableStreams({}); - + try { const episodeId = episode.stremioId || `${metadata.id}:${episode.season_number}:${episode.episode_number}`; let completedProviders = 0; const expectedProviders = new Set(); const respondedProviders = new Set(); - + const installedAddons = stremioService.getInstalledAddons(); - const streamAddons = installedAddons.filter((addon: any) => + const streamAddons = installedAddons.filter((addon: any) => addon.resources && addon.resources.includes('stream') ); - + streamAddons.forEach((addon: any) => expectedProviders.add(addon.id)); - + logger.log(`[EpisodeStreamsModal] Fetching streams for ${episodeId}, expecting ${expectedProviders.size} providers`); - + await stremioService.getStreams('series', episodeId, (streams: any, addonId: any, addonName: any, error: any) => { completedProviders++; respondedProviders.add(addonId); - + if (error) { logger.warn(`[EpisodeStreamsModal] Error from ${addonName || addonId}:`, error); setHasErrors(prev => [...prev, `${addonName || addonId}: ${error.message || 'Unknown error'}`]); } else if (streams && streams.length > 0) { - // Update state incrementally for each provider setAvailableStreams(prev => ({ ...prev, [addonId]: { @@ -129,13 +128,13 @@ export const EpisodeStreamsModal: React.FC = ({ } else { logger.log(`[EpisodeStreamsModal] No streams from ${addonName || addonId}`); } - + if (completedProviders >= expectedProviders.size) { logger.log(`[EpisodeStreamsModal] All providers completed. Total providers responded: ${respondedProviders.size}`); setIsLoading(false); } }); - + // Fallback timeout setTimeout(() => { if (respondedProviders.size === 0) { @@ -144,7 +143,7 @@ export const EpisodeStreamsModal: React.FC = ({ setIsLoading(false); } }, 8000); - + } catch (error) { logger.error('[EpisodeStreamsModal] Error fetching streams:', error); setHasErrors(prev => [...prev, `Failed to fetch streams: ${error}`]); @@ -158,38 +157,16 @@ export const EpisodeStreamsModal: React.FC = ({ return match ? match[1] : null; }; - const handleClose = () => { - onClose(); - }; - if (!visible) return null; const sortedProviders = Object.entries(availableStreams); return ( - <> - {/* Backdrop */} - - - + + + + - {/* Side Menu */} = ({ right: 0, bottom: 0, width: MENU_WIDTH, - backgroundColor: '#1A1A1A', - zIndex: 9999, - elevation: 20, - shadowColor: '#000', - shadowOffset: { width: -5, height: 0 }, - shadowOpacity: 0.3, - shadowRadius: 10, - borderTopLeftRadius: 20, - borderBottomLeftRadius: 20, + backgroundColor: '#0f0f0f', + borderLeftWidth: 1, + borderColor: 'rgba(255,255,255,0.1)', }} > - {/* Header */} - - - - {episode?.name || 'Select Stream'} - - {episode && ( - - S{episode.season_number}E{episode.episode_number} + + + + + {episode?.name || 'Select Stream'} - )} + {episode && ( + + S{episode.season_number}E{episode.episode_number} + + )} + - - - - {isLoading && ( = ({ }}> {providerData.addonName} ({providerData.streams.length}) - + {providerData.streams.map((stream, index) => { const quality = getQualityFromTitle(stream.title) || stream.quality; - + return ( onSelectStream(stream)} activeOpacity={0.7} @@ -319,7 +259,7 @@ export const EpisodeStreamsModal: React.FC = ({ gap: 8, }}> = ({ {quality && } - + {(stream.size || stream.lang) && ( {stream.size && ( - + = ({ )} {stream.lang && ( - + = ({ )} - - + + @@ -434,7 +371,6 @@ export const EpisodeStreamsModal: React.FC = ({ )} - + ); }; - diff --git a/src/components/player/modals/EpisodesModal.tsx b/src/components/player/modals/EpisodesModal.tsx index 5d1dfcf9..1f6a5f3b 100644 --- a/src/components/player/modals/EpisodesModal.tsx +++ b/src/components/player/modals/EpisodesModal.tsx @@ -55,12 +55,22 @@ export const EpisodesModal: React.FC = ({ if (showEpisodesModal && metadata?.id) { setIsLoadingProgress(true); try { - const progress = await storageService.getShowProgress(metadata.id); - setEpisodeProgress(progress || {}); + const allProgress = await storageService.getAllWatchProgress(); + const progress: { [key: string]: any } = {}; + + // Filter progress for current show's episodes + Object.entries(allProgress).forEach(([key, value]) => { + if (key.includes(metadata.id!)) { + progress[key] = value; + } + }); + + setEpisodeProgress(progress); // Trakt sync logic preserved - if (await TraktService.isAuthenticated()) { - // Optional: background sync logic + const traktService = TraktService.getInstance(); + if (await traktService.isAuthenticated()) { + // Optional: background sync logic } } catch (err) { logger.error('Failed to fetch episode progress', err); @@ -84,7 +94,7 @@ export const EpisodesModal: React.FC = ({ const currentSeasonEpisodes = groupedEpisodes[selectedSeason] || []; return ( - + setShowEpisodesModal(false)}> diff --git a/src/components/player/modals/SourcesModal.tsx b/src/components/player/modals/SourcesModal.tsx index 95f35445..6bfac5d3 100644 --- a/src/components/player/modals/SourcesModal.tsx +++ b/src/components/player/modals/SourcesModal.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, Dimensions } from 'react-native'; +import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, StyleSheet, Platform, useWindowDimensions } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; -import Animated, { - FadeIn, +import Animated, { + FadeIn, FadeOut, SlideInRight, SlideOutRight, @@ -18,16 +18,13 @@ interface SourcesModalProps { isChangingSource?: boolean; } -const { width } = Dimensions.get('window'); -const MENU_WIDTH = Math.min(width * 0.85, 400); - const QualityBadge = ({ quality }: { quality: string | null }) => { if (!quality) return null; - + const qualityNum = parseInt(quality); let color = '#8B5CF6'; // Default purple let label = `${quality}p`; - + if (qualityNum >= 2160) { color = '#F59E0B'; // Gold for 4K label = '4K'; @@ -38,9 +35,9 @@ const QualityBadge = ({ quality }: { quality: string | null }) => { color = '#10B981'; // Green for 720p label = 'HD'; } - + return ( - = ({ onSelectStream, isChangingSource = false, }) => { + const { width } = useWindowDimensions(); + const MENU_WIDTH = Math.min(width * 0.85, 400); + const handleClose = () => { setShowSourcesModal(false); }; @@ -97,29 +97,11 @@ export const SourcesModal: React.FC = ({ }; return ( - <> - {/* Backdrop */} - - - + + + + - {/* Side Menu */} = ({ right: 0, bottom: 0, width: MENU_WIDTH, - backgroundColor: '#1A1A1A', - zIndex: 9999, - elevation: 20, - shadowColor: '#000', - shadowOffset: { width: -5, height: 0 }, - shadowOpacity: 0.3, - shadowRadius: 10, - borderTopLeftRadius: 20, - borderBottomLeftRadius: 20, + backgroundColor: '#0f0f0f', + borderLeftWidth: 1, + borderColor: 'rgba(255,255,255,0.1)', }} > - {/* Header */} - - - Change Source - - - - + + + Change Source + - {isChangingSource && ( = ({ }}> {providerData.addonName} ({providerData.streams.length}) - + {providerData.streams.map((stream, index) => { const isSelected = isStreamSelected(stream); const quality = getQualityFromTitle(stream.title) || stream.quality; - + return ( handleStreamSelect(stream)} @@ -243,23 +190,23 @@ export const SourcesModal: React.FC = ({ gap: 8, }}> {stream.title || stream.name || `Stream ${index + 1}`} {quality && } - + {(stream.size || stream.lang) && ( {stream.size && ( - + = ({ )} {stream.lang && ( - + = ({ )} - - + + {isSelected ? ( - + ) : ( )} @@ -330,6 +274,6 @@ export const SourcesModal: React.FC = ({ )} - + ); }; \ No newline at end of file diff --git a/src/components/player/modals/SpeedModal.tsx b/src/components/player/modals/SpeedModal.tsx index b2a4a33d..2bb7bcc6 100644 --- a/src/components/player/modals/SpeedModal.tsx +++ b/src/components/player/modals/SpeedModal.tsx @@ -1,13 +1,12 @@ import React from 'react'; -import { View, Text, TouchableOpacity, Platform, useWindowDimensions, ScrollView } from 'react-native'; +import { View, Text, TouchableOpacity, Platform, useWindowDimensions, ScrollView, StyleSheet } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; -import Animated, { - FadeIn, +import Animated, { + FadeIn, FadeOut, SlideInRight, SlideOutRight, } from 'react-native-reanimated'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; interface SpeedModalProps { showSpeedModal: boolean; @@ -30,20 +29,8 @@ export const SpeedModal: React.FC = ({ holdToSpeedValue, setHoldToSpeedValue, }) => { - const insets = useSafeAreaInsets(); - const { width, height } = useWindowDimensions(); - const isIos = Platform.OS === 'ios'; - const isLandscape = width > height; - - // Responsive tuning - more aggressive compacting - const isCompact = width < 360 || height < 640; - const sectionPad = isCompact ? 6 : 8; - const chipPadH = isCompact ? 4 : 6; - const chipPadV = isCompact ? 3 : 4; - const menuWidth = Math.min( - width * (isIos ? (isLandscape ? 0.55 : 0.8) : 0.85), - isIos ? 380 : 360 - ); + const { width } = useWindowDimensions(); + const MENU_WIDTH = Math.min(width * 0.85, 400); const speedPresets = [0.5, 1.0, 1.5, 2.0, 2.5]; const holdSpeedOptions = [1.5, 2.0]; @@ -60,268 +47,203 @@ export const SpeedModal: React.FC = ({ setHoldToSpeedValue(speed); }; - const renderSpeedModal = () => { - if (!showSpeedModal) return null; - - return ( - <> - {/* Backdrop */} - - - - - {/* Side Menu */} - - {/* Header */} - - - - Playback Speed - - - - - - - - - {/* Current Speed Display */} - - - - - Current: {currentSpeed}x - - - - - {/* Speed Presets */} - - - Speed Presets - - - - {speedPresets.map((speed) => { - const isSelected = currentSpeed === speed; - return ( - handleSpeedSelect(speed)} - style={{ - paddingHorizontal: chipPadH, - paddingVertical: chipPadV, - borderRadius: 12, - backgroundColor: isSelected ? 'rgba(59, 130, 246, 0.15)' : 'rgba(255, 255, 255, 0.05)', - borderWidth: 1, - borderColor: isSelected ? 'rgba(59, 130, 246, 0.3)' : 'rgba(255, 255, 255, 0.1)', - minWidth: 50, - alignItems: 'center', - }} - activeOpacity={0.7} - > - - {speed}x - - - ); - })} - - - - {/* Hold-to-Speed Settings */} - - - - - - Hold-to-Speed - - - - {/* Enable Toggle */} - - Enable Hold Speed - setHoldToSpeedEnabled(!holdToSpeedEnabled)} - > - - - - - {/* Hold Speed Selector */} - {holdToSpeedEnabled && ( - - Hold Speed - - {holdSpeedOptions.map((speed) => { - const isSelected = holdToSpeedValue === speed; - return ( - handleHoldSpeedSelect(speed)} - style={{ - paddingHorizontal: chipPadH, - paddingVertical: chipPadV, - borderRadius: 10, - backgroundColor: isSelected ? 'rgba(34, 197, 94, 0.15)' : 'rgba(255, 255, 255, 0.05)', - borderWidth: 1, - borderColor: isSelected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(255, 255, 255, 0.1)', - minWidth: 45, - alignItems: 'center', - }} - activeOpacity={0.7} - > - - {speed}x - - - ); - })} - - - )} - - {/* Info Text */} - - - - - - Hold left/right sides - - - Hold and press the left or right side of the video player to temporarily boost playback speed. - - - - - - - - - - ); - }; + if (!showSpeedModal) return null; return ( - <> - {renderSpeedModal()} - + + + + + + + + + Playback Speed + + + + + {/* Current Speed Display */} + + + + + Current: {currentSpeed}x + + + + + {/* Speed Presets */} + + + Speed Presets + + + + {speedPresets.map((speed) => { + const isSelected = currentSpeed === speed; + return ( + handleSpeedSelect(speed)} + style={{ + paddingHorizontal: 16, + paddingVertical: 12, + borderRadius: 12, + backgroundColor: isSelected ? 'white' : 'rgba(255,255,255,0.06)', + borderWidth: 1, + borderColor: isSelected ? 'white' : 'rgba(255,255,255,0.1)', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }} + activeOpacity={0.7} + > + + {speed}x + + {isSelected && } + + ); + })} + + + + {/* Hold-to-Speed Settings */} + + + + + Hold-to-Speed + + + + {/* Enable Toggle */} + + Enable Hold Speed + setHoldToSpeedEnabled(!holdToSpeedEnabled)} + > + + + + + {/* Hold Speed Selector */} + {holdToSpeedEnabled && ( + + Hold Speed + + {holdSpeedOptions.map((speed) => { + const isSelected = holdToSpeedValue === speed; + return ( + handleHoldSpeedSelect(speed)} + style={{ + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 20, + backgroundColor: isSelected ? 'white' : 'rgba(255,255,255,0.06)', + borderWidth: 1, + borderColor: isSelected ? 'white' : 'rgba(255,255,255,0.1)', + }} + activeOpacity={0.7} + > + + {speed}x + + + ); + })} + + + )} + + {/* Info Text */} + + + + + + Hold left/right sides + + + Hold and press the left or right side of the video player to temporarily boost playback speed. + + + + + + + + ); }; diff --git a/src/components/player/modals/SubtitleModals.tsx b/src/components/player/modals/SubtitleModals.tsx index b6713d4d..914b2bdc 100644 --- a/src/components/player/modals/SubtitleModals.tsx +++ b/src/components/player/modals/SubtitleModals.tsx @@ -1,13 +1,13 @@ import React from 'react'; import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, Platform, useWindowDimensions } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; -import Animated, { - FadeIn, +import Animated, { + FadeIn, FadeOut, SlideInRight, SlideOutRight, } from 'react-native-reanimated'; -import { styles } from '../utils/playerStyles'; +import { StyleSheet } from 'react-native'; import { WyzieSubtitle, SubtitleCue } from '../utils/playerTypes'; import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -21,7 +21,7 @@ interface SubtitleModalsProps { isLoadingSubtitles: boolean; customSubtitles: SubtitleCue[]; availableSubtitles: WyzieSubtitle[]; - ksTextTracks: Array<{id: number, name: string, language?: string}>; + ksTextTracks: Array<{ id: number, name: string, language?: string }>; selectedTextTrack: number; useCustomSubtitles: boolean; // When true, KSPlayer is active (iOS MKV path). Use to gate iOS-only limitations. @@ -128,7 +128,7 @@ export const SubtitleModals: React.FC = ({ width * (isIos ? (isLandscape ? 0.6 : 0.8) : 0.85), isIos ? 420 : 400 ); - + React.useEffect(() => { if (showSubtitleModal && !isLoadingSubtitleList && availableSubtitles.length === 0) { fetchAvailableSubtitles(); @@ -183,31 +183,13 @@ export const SubtitleModals: React.FC = ({ // Main subtitle menu const renderSubtitleMenu = () => { if (!showSubtitleModal) return null; - - return ( - <> - {/* Backdrop */} - - - - {/* Side Menu */} + return ( + + + + + = ({ right: 0, bottom: 0, width: menuWidth, - backgroundColor: '#1A1A1A', - zIndex: 9999, - elevation: 20, - shadowColor: '#000', - shadowOffset: { width: -5, height: 0 }, - shadowOpacity: 0.3, - shadowRadius: 10, - borderTopLeftRadius: 20, - borderBottomLeftRadius: 20, - paddingRight: 0, + backgroundColor: '#0f0f0f', + borderLeftWidth: 1, + borderColor: 'rgba(255,255,255,0.1)', }} > - {/* Header */} - - - Subtitles - - - {useCustomSubtitles ? 'Addon in use' : 'Built‑in in use'} - + + + + Subtitles + + + {useCustomSubtitles ? 'Addon in use' : 'Built‑in in use'} + + - - - {/* Segmented Tabs */} @@ -288,7 +241,7 @@ export const SubtitleModals: React.FC = ({ ))} - = ({ )} {activeTab === 'addon' && ( - - - + - Addon Subtitles - - - {useCustomSubtitles && ( + + Addon Subtitles + + + {useCustomSubtitles && ( + { + disableCustomSubtitles(); + setSelectedOnlineSubtitleId(null); + }} + activeOpacity={0.7} + > + + + Disable + + + )} { - disableCustomSubtitles(); - setSelectedOnlineSubtitleId(null); - }} - activeOpacity={0.7} + onPress={() => fetchAvailableSubtitles()} + disabled={isLoadingSubtitleList} > - + {isLoadingSubtitleList ? ( + + ) : ( + + )} - Disable + {isLoadingSubtitleList ? 'Searching' : 'Refresh'} - )} + + + + {(availableSubtitles.length === 0) && !isLoadingSubtitleList ? ( fetchAvailableSubtitles()} - disabled={isLoadingSubtitleList} + activeOpacity={0.7} > - {isLoadingSubtitleList ? ( - - ) : ( - - )} + - {isLoadingSubtitleList ? 'Searching' : 'Refresh'} + Tap to fetch from addons - - - - {(availableSubtitles.length === 0) && !isLoadingSubtitleList ? ( - fetchAvailableSubtitles()} - activeOpacity={0.7} - > - - - Tap to fetch from addons - - - ) : isLoadingSubtitleList ? ( - - - - Searching... - - - ) : ( - - {availableSubtitles.map((sub) => { - const isSelected = useCustomSubtitles && selectedOnlineSubtitleId === sub.id; - return ( - { - handleLoadWyzieSubtitle(sub); - }} - activeOpacity={0.7} - disabled={isLoadingSubtitles} - > - - - - {sub.display} - - {(() => { - const filename = getFileNameFromUrl(sub.url); - if (!filename) return null; - return ( - - {filename} - - ); - })()} - - {formatLanguage(sub.language)}{sub.source ? ` · ${sub.source}` : ''} - + + + Searching... + + + ) : ( + + {availableSubtitles.map((sub) => { + const isSelected = useCustomSubtitles && selectedOnlineSubtitleId === sub.id; + return ( + { + handleLoadWyzieSubtitle(sub); + }} + activeOpacity={0.7} + disabled={isLoadingSubtitles} + > + + + + {sub.display} + + {(() => { + const filename = getFileNameFromUrl(sub.url); + if (!filename) return null; + return ( + + {filename} + + ); + })()} + + {formatLanguage(sub.language)}{sub.source ? ` · ${sub.source}` : ''} + + + {(isLoadingSubtitles && loadingSubtitleId === sub.id) ? ( + + ) : isSelected ? ( + + ) : ( + + )} - {(isLoadingSubtitles && loadingSubtitleId === sub.id) ? ( - - ) : isSelected ? ( - - ) : ( - - )} - - - ); - })} - - )} - + + ); + })} + + )} + )} {activeTab === 'appearance' && ( @@ -902,7 +855,7 @@ export const SubtitleModals: React.FC = ({ - + ); };