diff --git a/src/components/player/modals/AudioTrackModal.tsx b/src/components/player/modals/AudioTrackModal.tsx index eb304334..07e61a07 100644 --- a/src/components/player/modals/AudioTrackModal.tsx +++ b/src/components/player/modals/AudioTrackModal.tsx @@ -1,18 +1,19 @@ import React from 'react'; -import { View, Text, TouchableOpacity, ScrollView, StyleSheet, Platform, useWindowDimensions } from 'react-native'; +import { View, Text, TouchableOpacity, ScrollView, useWindowDimensions, StyleSheet, Platform } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import Animated, { FadeIn, FadeOut, - SlideInRight, - SlideOutRight, + SlideInDown, + SlideOutDown, } from 'react-native-reanimated'; -import { getTrackDisplayName } from '../utils/playerUtils'; +import { getTrackDisplayName, DEBUG_MODE } from '../utils/playerUtils'; +import { logger } from '../../../utils/logger'; 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; } @@ -24,89 +25,99 @@ export const AudioTrackModal: React.FC = ({ selectedAudioTrack, selectAudioTrack, }) => { - const { width } = useWindowDimensions(); - const MENU_WIDTH = Math.min(width * 0.85, 400); + const { width, height } = useWindowDimensions(); + + // Size constants matching SubtitleModal aesthetics + const menuWidth = Math.min(width * 0.9, 420); + const menuMaxHeight = height * 0.9; const handleClose = () => setShowAudioModal(false); if (!showAudioModal) return null; return ( - - - + + {/* Backdrop matching SubtitleModal */} + + - - - - Audio Tracks - - - - + - - {ksAudioTracks.map((track) => { - const isSelected = selectedAudioTrack === track.id; - - 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 - - )} + {/* Header with shared aesthetics */} + + Audio Tracks - - + + + + {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 && } + + ); + })} + + {ksAudioTracks.length === 0 && ( + + + No audio tracks available + + )} + + + + ); }; diff --git a/src/components/player/modals/EpisodeStreamsModal.tsx b/src/components/player/modals/EpisodeStreamsModal.tsx index 747f611b..935d9765 100644 --- a/src/components/player/modals/EpisodeStreamsModal.tsx +++ b/src/components/player/modals/EpisodeStreamsModal.tsx @@ -31,34 +31,22 @@ const QualityBadge = ({ quality }: { quality: string | null }) => { color = '#F59E0B'; label = '4K'; } else if (qualityNum >= 1080) { - color = '#EF4444'; - label = 'FHD'; + color = '#3B82F6'; + label = '1080p'; } else if (qualityNum >= 720) { color = '#10B981'; - label = 'HD'; + label = '720p'; } return ( - - - {label} - + + {label} ); }; @@ -114,7 +102,6 @@ export const EpisodeStreamsModal: React.FC = ({ 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) { setAvailableStreams(prev => ({ @@ -124,29 +111,20 @@ export const EpisodeStreamsModal: React.FC = ({ addonName: addonName || addonId } })); - logger.log(`[EpisodeStreamsModal] Added ${streams.length} streams from ${addonName || addonId}`); - } 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) { - logger.warn(`[EpisodeStreamsModal] Timeout: No providers responded`); - setHasErrors(prev => [...prev, 'Timeout: No providers responded']); setIsLoading(false); } }, 8000); } catch (error) { - logger.error('[EpisodeStreamsModal] Error fetching streams:', error); - setHasErrors(prev => [...prev, `Failed to fetch streams: ${error}`]); setIsLoading(false); } }; @@ -162,9 +140,18 @@ export const EpisodeStreamsModal: React.FC = ({ const sortedProviders = Object.entries(availableStreams); return ( - - - + + {/* Backdrop */} + + = ({ borderColor: 'rgba(255,255,255,0.1)', }} > - - - - - {episode?.name || 'Select Stream'} + {/* Header */} + + + + + {episode?.name || 'Sources'} {episode && ( - - S{episode.season_number}E{episode.episode_number} + + S{episode.season_number} • E{episode.episode_number} )} @@ -198,176 +190,82 @@ export const EpisodeStreamsModal: React.FC = ({ - {isLoading && ( - - - - Finding available streams... - + {isLoading && sortedProviders.length === 0 && ( + + + Finding sources... )} - {!isLoading && sortedProviders.length > 0 && ( - sortedProviders.map(([providerId, providerData]) => ( - - - {providerData.addonName} ({providerData.streams.length}) - - - - {providerData.streams.map((stream, index) => { - const quality = getQualityFromTitle(stream.title) || stream.quality; - - return ( - onSelectStream(stream)} - activeOpacity={0.7} - > - - - - - {stream.title || stream.name || `Stream ${index + 1}`} - - {quality && } - - - {(stream.size || stream.lang) && ( - - {stream.size && ( - - - - {(stream.size / (1024 * 1024 * 1024)).toFixed(1)} GB - - - )} - {stream.lang && ( - - - - {stream.lang.toUpperCase()} - - - )} - - )} - - - - - - - - ); - })} - - - )) - )} - - {!isLoading && sortedProviders.length === 0 && hasErrors.length === 0 && ( - - - - No sources available - + {sortedProviders.map(([providerId, providerData]) => ( + - Try searching for different content + {providerData.addonName} + + + {providerData.streams.map((stream, index) => { + const quality = getQualityFromTitle(stream.title) || stream.quality; + + return ( + { + onSelectStream(stream); + onClose(); + }} + activeOpacity={0.7} + > + + + + + {stream.name || 'Unknown Source'} + + + + {stream.title && ( + + {stream.title} + + )} + + + + ); + })} + + + ))} + + {!isLoading && sortedProviders.length === 0 && ( + + + No sources found )} - {!isLoading && hasErrors.length > 0 && ( - - - - - Errors occurred - - - {hasErrors.map((error, index) => ( - - {error} - - ))} - + {hasErrors.length > 0 && ( + + Sources might be limited due to provider errors. + )} diff --git a/src/components/player/modals/EpisodesModal.tsx b/src/components/player/modals/EpisodesModal.tsx index 2fd8b580..5bb8b6a2 100644 --- a/src/components/player/modals/EpisodesModal.tsx +++ b/src/components/player/modals/EpisodesModal.tsx @@ -43,7 +43,7 @@ export const EpisodesModal: React.FC = ({ text: '#FFFFFF', textMuted: 'rgba(255,255,255,0.6)', mediumEmphasis: 'rgba(255,255,255,0.7)', - primary: '#3B82F6', + primary: 'rgba(255,255,255,0.9)', white: '#FFFFFF', elevation2: 'rgba(255,255,255,0.05)' } @@ -55,22 +55,12 @@ export const EpisodesModal: React.FC = ({ if (showEpisodesModal && metadata?.id) { setIsLoadingProgress(true); try { - 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); + const progress = await storageService.getShowProgress(metadata.id); + setEpisodeProgress(progress || {}); // Trakt sync logic preserved - const traktService = TraktService.getInstance(); - if (await traktService.isAuthenticated()) { - // Optional: background sync logic + if (await TraktService.isAuthenticated()) { + // Optional: background sync logic } } catch (err) { logger.error('Failed to fetch episode progress', err); @@ -94,7 +84,7 @@ export const EpisodesModal: React.FC = ({ const currentSeasonEpisodes = groupedEpisodes[selectedSeason] || []; return ( - + setShowEpisodesModal(false)}> @@ -113,13 +103,18 @@ export const EpisodesModal: React.FC = ({ borderColor: 'rgba(255,255,255,0.1)', }} > - - + + Episodes - {seasons.map((season) => ( + {[...seasons] + .sort((a, b) => { + if (a === 0) return 1; + if (b === 0) return -1; + return a - b; + }).map((season) => ( setSelectedSeason(season)} @@ -136,14 +131,14 @@ export const EpisodesModal: React.FC = ({ color: selectedSeason === season ? 'black' : 'white', fontWeight: selectedSeason === season ? '700' : '500' }}> - Season {season} + {season === 0 ? 'Specials' : `Season ${season}`} ))} - + {isLoadingProgress ? ( ) : ( diff --git a/src/components/player/modals/SourcesModal.tsx b/src/components/player/modals/SourcesModal.tsx index d40896d0..b126a5eb 100644 --- a/src/components/player/modals/SourcesModal.tsx +++ b/src/components/player/modals/SourcesModal.tsx @@ -22,41 +22,29 @@ const QualityBadge = ({ quality }: { quality: string | null }) => { if (!quality) return null; const qualityNum = parseInt(quality); - let color = '#8B5CF6'; // Default purple + let color = '#8B5CF6'; let label = `${quality}p`; if (qualityNum >= 2160) { - color = '#F59E0B'; // Gold for 4K + color = '#F59E0B'; label = '4K'; } else if (qualityNum >= 1080) { - color = '#EF4444'; // Red for 1080p - label = 'FHD'; + color = '#3B82F6'; + label = '1080p'; } else if (qualityNum >= 720) { - color = '#10B981'; // Green for 720p - label = 'HD'; + color = '#10B981'; + label = '720p'; } return ( - - - {label} - + + {label} ); }; @@ -97,9 +85,18 @@ export const SourcesModal: React.FC = ({ }; return ( - - - + + {/* Backdrop */} + + = ({ borderColor: 'rgba(255,255,255,0.1)', }} > - - - Change Source - + {/* Header */} + + + Change Source + {isChangingSource && ( - + Switching source... @@ -149,14 +149,15 @@ export const SourcesModal: React.FC = ({ {sortedProviders.length > 0 ? ( sortedProviders.map(([providerId, providerData]) => ( - + {providerData.addonName} ({providerData.streams.length}) @@ -170,12 +171,12 @@ export const SourcesModal: React.FC = ({ handleStreamSelect(stream)} activeOpacity={0.7} @@ -183,60 +184,46 @@ export const SourcesModal: React.FC = ({ > - + + }} numberOfLines={1}> {stream.title || stream.name || `Stream ${index + 1}`} - {quality && } + {(stream.size || stream.lang) && ( - + {stream.size && ( - - - - {(stream.size / (1024 * 1024 * 1024)).toFixed(1)} GB - - + + {(stream.size / (1024 * 1024 * 1024)).toFixed(1)} GB + )} {stream.lang && ( - - - - {stream.lang.toUpperCase()} - - + + {stream.lang.toUpperCase()} + )} )} - + {isSelected ? ( - + ) : ( - + )} @@ -247,28 +234,10 @@ export const SourcesModal: React.FC = ({ )) ) : ( - - - - No sources available - - - Try searching for different content + + + + No sources found )} @@ -278,4 +247,4 @@ export const SourcesModal: React.FC = ({ ); }; -export default SourcesModal; \ No newline at end of file +export default SourcesModal; diff --git a/src/components/player/modals/SpeedModal.tsx b/src/components/player/modals/SpeedModal.tsx index 2bb7bcc6..4e158034 100644 --- a/src/components/player/modals/SpeedModal.tsx +++ b/src/components/player/modals/SpeedModal.tsx @@ -1,11 +1,13 @@ import React from 'react'; -import { View, Text, TouchableOpacity, Platform, useWindowDimensions, ScrollView, StyleSheet } from 'react-native'; +import { View, Text, TouchableOpacity, useWindowDimensions, StyleSheet } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import Animated, { FadeIn, FadeOut, - SlideInRight, - SlideOutRight, + SlideInDown, + SlideOutDown, + useAnimatedStyle, + withTiming, } from 'react-native-reanimated'; interface SpeedModalProps { @@ -19,7 +21,31 @@ interface SpeedModalProps { setHoldToSpeedValue: (speed: number) => void; } -export const SpeedModal: React.FC = ({ +const MorphingButton = ({ label, isSelected, onPress, isSmall = false }: any) => { + const animatedStyle = useAnimatedStyle(() => { + return { + // Linear transition from 40 (Pill) to 10 (Rectangle) + borderRadius: withTiming(isSelected ? 10 : 40, { duration: 250 }), + backgroundColor: withTiming(isSelected ? (isSmall ? 'rgba(255,255,255,0.2)' : 'white') : 'rgba(255,255,255,0.06)', { duration: 50 }), + }; + }); + + return ( + + + + {label} + + + + ); +}; + +const SpeedModal: React.FC = ({ showSpeedModal, setShowSpeedModal, currentSpeed, @@ -30,219 +56,84 @@ export const SpeedModal: React.FC = ({ setHoldToSpeedValue, }) => { 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]; - - const handleClose = () => { - setShowSpeedModal(false); - }; - - const handleSpeedSelect = (speed: number) => { - setPlaybackSpeed(speed); - }; - - const handleHoldSpeedSelect = (speed: number) => { - setHoldToSpeedValue(speed); - }; + const speedPresets = [0.5, 1.0, 1.25, 1.5, 2.0, 2.5]; + const holdSpeedOptions = [1.0, 2.0, 3.0]; if (!showSpeedModal) return null; return ( - - - + + setShowSpeedModal(false)} + > + - - - - Playback Speed - - - - - {/* Current Speed Display */} - + - - - + + Playback Speed + + + {/* Speed Selection Row */} + + {speedPresets.map((speed) => ( + setPlaybackSpeed(speed)} + /> + ))} + + + + + {/* On Hold Section */} + + setHoldToSpeedEnabled(!holdToSpeedEnabled)} + style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: holdToSpeedEnabled ? 15 : 0 }} + > + On Hold + - 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 - - - ); - })} - - + + {holdSpeedOptions.map((speed) => ( + setHoldToSpeedValue(speed)} + /> + ))} + )} - - {/* 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 914b2bdc..b1491d02 100644 --- a/src/components/player/modals/SubtitleModals.tsx +++ b/src/components/player/modals/SubtitleModals.tsx @@ -1,16 +1,16 @@ import React from 'react'; -import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, Platform, useWindowDimensions } from 'react-native'; +import { View, Text, TouchableOpacity, ScrollView, Platform, useWindowDimensions, StyleSheet } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import Animated, { FadeIn, FadeOut, - SlideInRight, - SlideOutRight, + SlideInDown, + SlideOutDown, + useAnimatedStyle, + withTiming, } from 'react-native-reanimated'; -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'; interface SubtitleModalsProps { showSubtitleModal: boolean; @@ -21,10 +21,9 @@ 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. isKsPlayerActive?: boolean; subtitleSize: number; subtitleBackground: boolean; @@ -35,7 +34,6 @@ interface SubtitleModalsProps { increaseSubtitleSize: () => void; decreaseSubtitleSize: () => void; toggleSubtitleBackground: () => void; - // Customization props subtitleTextColor: string; setSubtitleTextColor: (c: string) => void; subtitleBgOpacity: number; @@ -60,810 +58,408 @@ interface SubtitleModalsProps { setSubtitleOffsetSec: (n: number) => void; } -// Dynamic sizing handled inside component with useWindowDimensions +const MorphingTab = ({ label, isSelected, onPress }: any) => { + const animatedStyle = useAnimatedStyle(() => ({ + borderRadius: withTiming(isSelected ? 10 : 40, { duration: 250 }), + backgroundColor: withTiming(isSelected ? 'white' : 'rgba(255,255,255,0.06)', { duration: 250 }), + })); + + return ( + + + + {label} + + + + ); +}; export const SubtitleModals: React.FC = ({ - showSubtitleModal, - setShowSubtitleModal, - showSubtitleLanguageModal, - setShowSubtitleLanguageModal, - isLoadingSubtitleList, - isLoadingSubtitles, - customSubtitles, - availableSubtitles, - ksTextTracks, - selectedTextTrack, - useCustomSubtitles, - isKsPlayerActive, - subtitleSize, - subtitleBackground, - fetchAvailableSubtitles, - loadWyzieSubtitle, - selectTextTrack, - disableCustomSubtitles, - increaseSubtitleSize, - decreaseSubtitleSize, - toggleSubtitleBackground, - subtitleTextColor, - setSubtitleTextColor, - subtitleBgOpacity, - setSubtitleBgOpacity, - subtitleTextShadow, - setSubtitleTextShadow, - subtitleOutline, - setSubtitleOutline, - subtitleOutlineColor, - setSubtitleOutlineColor, - subtitleOutlineWidth, - setSubtitleOutlineWidth, - subtitleAlign, - setSubtitleAlign, - subtitleBottomOffset, - setSubtitleBottomOffset, - subtitleLetterSpacing, - setSubtitleLetterSpacing, - subtitleLineHeightMultiplier, - setSubtitleLineHeightMultiplier, - subtitleOffsetSec, - setSubtitleOffsetSec, + showSubtitleModal, setShowSubtitleModal, isLoadingSubtitleList, isLoadingSubtitles, + availableSubtitles, ksTextTracks, selectedTextTrack, useCustomSubtitles, + subtitleSize, subtitleBackground, fetchAvailableSubtitles, + loadWyzieSubtitle, selectTextTrack, increaseSubtitleSize, + decreaseSubtitleSize, toggleSubtitleBackground, subtitleTextColor, setSubtitleTextColor, + subtitleBgOpacity, setSubtitleBgOpacity, subtitleTextShadow, setSubtitleTextShadow, + subtitleOutline, setSubtitleOutline, subtitleOutlineColor, setSubtitleOutlineColor, + subtitleOutlineWidth, setSubtitleOutlineWidth, subtitleAlign, setSubtitleAlign, + subtitleBottomOffset, setSubtitleBottomOffset, subtitleLetterSpacing, setSubtitleLetterSpacing, + subtitleLineHeightMultiplier, setSubtitleLineHeightMultiplier, subtitleOffsetSec, setSubtitleOffsetSec, }) => { - const insets = useSafeAreaInsets(); const { width, height } = useWindowDimensions(); const isIos = Platform.OS === 'ios'; const isLandscape = width > height; - // Track which specific addon subtitle is currently loaded const [selectedOnlineSubtitleId, setSelectedOnlineSubtitleId] = React.useState(null); - // Track which addon subtitle is currently loading to show spinner per-item - const [loadingSubtitleId, setLoadingSubtitleId] = React.useState(null); - // Active tab for better organization - const [activeTab, setActiveTab] = React.useState<'built-in' | 'addon' | 'appearance'>(useCustomSubtitles ? 'addon' : 'built-in'); - // Responsive tuning + const [activeTab, setActiveTab] = React.useState<'built-in' | 'addon' | 'appearance'>('built-in'); + const isCompact = width < 360 || height < 640; const sectionPad = isCompact ? 12 : 16; const chipPadH = isCompact ? 8 : 12; const chipPadV = isCompact ? 6 : 8; const controlBtn = { size: isCompact ? 28 : 32, radius: isCompact ? 14 : 16 }; const previewHeight = isCompact ? 90 : (isIos && isLandscape ? 100 : 120); - const menuWidth = Math.min( - width * (isIos ? (isLandscape ? 0.6 : 0.8) : 0.85), - isIos ? 420 : 400 - ); + + const menuWidth = Math.min(width * 0.9, 420); + const menuMaxHeight = height * 0.95; React.useEffect(() => { - if (showSubtitleModal && !isLoadingSubtitleList && availableSubtitles.length === 0) { - fetchAvailableSubtitles(); - } + if (showSubtitleModal && !isLoadingSubtitleList && availableSubtitles.length === 0) fetchAvailableSubtitles(); }, [showSubtitleModal]); - // Reset selected addon subtitle when switching to built-in tracks - React.useEffect(() => { - if (!useCustomSubtitles) { - setSelectedOnlineSubtitleId(null); - } - }, [useCustomSubtitles]); + const handleClose = () => setShowSubtitleModal(false); - // Clear loading state when subtitles have finished loading - React.useEffect(() => { - if (!isLoadingSubtitles) { - setLoadingSubtitleId(null); - } - }, [isLoadingSubtitles]); + if (!showSubtitleModal) return null; - // Keep tab in sync with current usage - React.useEffect(() => { - setActiveTab(useCustomSubtitles ? 'addon' : 'built-in'); - }, [useCustomSubtitles]); - - const handleClose = () => { - setShowSubtitleModal(false); - }; - - const handleLoadWyzieSubtitle = (subtitle: WyzieSubtitle) => { - setSelectedOnlineSubtitleId(subtitle.id); - setLoadingSubtitleId(subtitle.id); - loadWyzieSubtitle(subtitle); - }; - - const getFileNameFromUrl = (url?: string): string | null => { - if (!url || typeof url !== 'string') return null; - try { - // Prefer URL parsing to safely strip query/hash - const u = new URL(url); - const raw = u.pathname.split('/').pop() || ''; - const decoded = decodeURIComponent(raw); - return decoded || null; - } catch { - // Fallback for non-standard URLs - const path = url.split('?')[0].split('#')[0]; - const raw = path.split('/').pop() || ''; - try { return decodeURIComponent(raw) || null; } catch { return raw || null; } - } - }; - - // Main subtitle menu - const renderSubtitleMenu = () => { - if (!showSubtitleModal) return null; - - return ( - - - - + return ( + + {/* Backdrop */} + + + + {/* Centered Modal Container */} + - - - - Subtitles - - - {useCustomSubtitles ? 'Addon in use' : 'Built‑in in use'} - - - - + {/* Header */} + + Subtitles - {/* Segmented Tabs */} - - {([ - { key: 'built-in', label: 'Built‑in' }, - { key: 'addon', label: 'Addons' }, - { key: 'appearance', label: 'Appearance' }, - ] as const).map(tab => ( - setActiveTab(tab.key)} - style={{ - paddingHorizontal: chipPadH, - paddingVertical: chipPadV, - borderRadius: 16, - backgroundColor: activeTab === tab.key ? 'rgba(255,255,255,0.15)' : 'rgba(255,255,255,0.06)', - borderWidth: 1, - borderColor: activeTab === tab.key ? 'rgba(255,255,255,0.3)' : 'rgba(255,255,255,0.1)' - }} - > - {tab.label} - - ))} + {/* Tab Bar */} + + setActiveTab('built-in')} /> + setActiveTab('addon')} /> + setActiveTab('appearance')} /> - - {activeTab === 'built-in' && ( - - - Built-in Subtitles - - - {/* Built-in subtitles now enabled for KSPlayer */} - {isKsPlayerActive && ( - - - - - - Built-in subtitles enabled for KSPlayer - - - KSPlayer built-in subtitle rendering is now available. You can select from embedded subtitle tracks below. - - - - - )} - - {/* Disable Subtitles Button */} - { - selectTextTrack(-1); - setSelectedOnlineSubtitleId(null); - }} - activeOpacity={0.7} - > - - - Disable All Subtitles - - {selectedTextTrack === -1 && ( - - )} - - - - {/* Always show built-in subtitles */} - {ksTextTracks.length > 0 && ( - - {ksTextTracks.map((track) => { - const isSelected = selectedTextTrack === track.id && !useCustomSubtitles; - return ( - { - selectTextTrack(track.id); - setSelectedOnlineSubtitleId(null); - }} - activeOpacity={0.7} - > - - - {getTrackDisplayName(track)} - - {isSelected && ( - - )} - - - ); - })} - - )} - - )} - - {activeTab === 'addon' && ( - - - - Addon Subtitles - - - {useCustomSubtitles && ( - { - disableCustomSubtitles(); - setSelectedOnlineSubtitleId(null); - }} - activeOpacity={0.7} - > - - - Disable - - - )} - fetchAvailableSubtitles()} - disabled={isLoadingSubtitleList} - > - {isLoadingSubtitleList ? ( - - ) : ( - - )} - - {isLoadingSubtitleList ? 'Searching' : 'Refresh'} - - - - - - {(availableSubtitles.length === 0) && !isLoadingSubtitleList ? ( + + + {activeTab === 'built-in' && ( + fetchAvailableSubtitles()} - activeOpacity={0.7} + onPress={() => { selectTextTrack(-1); setSelectedOnlineSubtitleId(null); }} + style={{ padding: 10, borderRadius: 12, backgroundColor: selectedTextTrack === -1 ? 'white' : 'rgba(242, 184, 181)' }} > - - - Tap to fetch from addons - + None - ) : 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}` : ''} - - - {(isLoadingSubtitles && loadingSubtitleId === sub.id) ? ( - - ) : isSelected ? ( - - ) : ( - - )} - - - ); - })} - - )} - - )} + {ksTextTracks.map((track) => ( + { selectTextTrack(track.id); setSelectedOnlineSubtitleId(null); }} + style={{ padding: 10, borderRadius: 12, backgroundColor: selectedTextTrack === track.id ? 'white' : 'rgba(255,255,255,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }} + > + {getTrackDisplayName(track)} + {selectedTextTrack === track.id && } + + ))} + + )} - {activeTab === 'appearance' && ( - - {/* Live Preview */} - - - - Preview - - - - - + {availableSubtitles.length === 0 ? ( + + + Search Online Subtitles + + ) : ( + availableSubtitles.map((sub) => ( + { setSelectedOnlineSubtitleId(sub.id); loadWyzieSubtitle(sub); }} + style={{ padding: 5,paddingLeft: 8, paddingRight: 10, borderRadius: 12, backgroundColor: selectedOnlineSubtitleId === sub.id ? 'white' : 'rgba(255,255,255,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', textAlignVertical: 'center' }} + > + + {sub.display} + {formatLanguage(sub.language)} + + {selectedOnlineSubtitleId === sub.id && } + + )) + )} + + )} + + {activeTab === 'appearance' && ( + + {/* Live Preview */} + + + + Preview + + + + - The quick brown fox jumps over the lazy dog. - - - - - - - {/* Quick Presets */} - - - - Quick Presets - - - { - setSubtitleTextColor('#FFFFFF'); - setSubtitleBgOpacity(0.7); - setSubtitleTextShadow(true); - setSubtitleOutline(true); - setSubtitleOutlineColor('#000000'); - setSubtitleOutlineWidth(4); - setSubtitleAlign('center'); - setSubtitleBottomOffset(10); - setSubtitleLetterSpacing(0); - setSubtitleLineHeightMultiplier(1.2); - }} - style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }} - > - Default - - { - setSubtitleTextColor('#FFD700'); - setSubtitleOutline(true); - setSubtitleOutlineColor('#000000'); - setSubtitleOutlineWidth(4); - setSubtitleBgOpacity(0.3); - setSubtitleTextShadow(false); - }} - style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,215,0,0.12)', borderWidth: 1, borderColor: 'rgba(255,215,0,0.35)' }} - > - Yellow - - { - setSubtitleTextColor('#FFFFFF'); - setSubtitleOutline(true); - setSubtitleOutlineColor('#000000'); - setSubtitleOutlineWidth(3); - setSubtitleBgOpacity(0.0); - setSubtitleTextShadow(false); - setSubtitleLetterSpacing(0.5); - }} - style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(34,197,94,0.12)', borderWidth: 1, borderColor: 'rgba(34,197,94,0.35)' }} - > - High Contrast - - { - setSubtitleTextColor('#FFFFFF'); - setSubtitleBgOpacity(0.6); - setSubtitleTextShadow(true); - setSubtitleOutline(true); - setSubtitleAlign('center'); - setSubtitleLineHeightMultiplier(1.3); - }} - style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(59,130,246,0.12)', borderWidth: 1, borderColor: 'rgba(59,130,246,0.35)' }} - > - Large - - - - - {/* Core controls */} - - - - Core - - {/* Font Size */} - - - - Font Size - - - - - - - {subtitleSize} - - - - - - - {/* Background toggle */} - - - - Show Background - - - - - - - - {/* Advanced controls */} - - - - Advanced - - - {/* Text Color */} - - - - Text Color - - - {['#FFFFFF', '#FFD700', '#00E5FF', '#FF5C5C', '#00FF88', '#9b59b6', '#f97316'].map(c => ( - setSubtitleTextColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleTextColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} /> - ))} - - - - {/* Align */} - - Align - - {([ - { key: 'left', icon: 'format-align-left' }, - { key: 'center', icon: 'format-align-center' }, - { key: 'right', icon: 'format-align-right' }, - ] as const).map(a => ( - setSubtitleAlign(a.key)} - style={{ paddingHorizontal: isCompact ? 8 : 10, paddingVertical: isCompact ? 4 : 6, borderRadius: 8, backgroundColor: subtitleAlign === a.key ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }} - > - - - ))} - - - - {/* Bottom Offset */} - - Bottom Offset - - setSubtitleBottomOffset(Math.max(0, subtitleBottomOffset - 5))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - - - - {subtitleBottomOffset} - - setSubtitleBottomOffset(subtitleBottomOffset + 5)} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - - - - - - {/* Background Opacity */} - - Background Opacity - - setSubtitleBgOpacity(Math.max(0, +(subtitleBgOpacity - 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - - - - {subtitleBgOpacity.toFixed(1)} - - setSubtitleBgOpacity(Math.min(1, +(subtitleBgOpacity + 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - - - - - - {/* Shadow */} - - Text Shadow - setSubtitleTextShadow(!subtitleTextShadow)} style={{ paddingHorizontal: 10, paddingVertical: 8, borderRadius: 10, backgroundColor: subtitleTextShadow ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)', alignItems: 'center' }}> - {subtitleTextShadow ? 'On' : 'Off'} - - - {/* Outline color & width */} - - Outline Color - - {['#000000', '#FFFFFF', '#00E5FF', '#FF5C5C'].map(c => ( - setSubtitleOutlineColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleOutlineColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} /> - ))} - - - - Outline Width - - setSubtitleOutlineWidth(Math.max(0, subtitleOutlineWidth - 1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - - - - {subtitleOutlineWidth} - - setSubtitleOutlineWidth(subtitleOutlineWidth + 1)} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - - - - - - {/* Spacing (two columns) */} - - - Letter Spacing - - setSubtitleLetterSpacing(Math.max(0, +(subtitleLetterSpacing - 0.5).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - - - - {subtitleLetterSpacing.toFixed(1)} + + The quick brown fox jumps over the lazy dog. + - setSubtitleLetterSpacing(+(subtitleLetterSpacing + 0.5).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - - - - - - Line Height - - setSubtitleLineHeightMultiplier(Math.max(1, +(subtitleLineHeightMultiplier - 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - - - - {subtitleLineHeightMultiplier.toFixed(1)} - - setSubtitleLineHeightMultiplier(+(subtitleLineHeightMultiplier + 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - - - {/* Timing Offset */} - + {/* Quick Presets */} + + + + Quick Presets + + + { + setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.7); setSubtitleTextShadow(true); + setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4); + setSubtitleAlign('center'); setSubtitleBottomOffset(10); setSubtitleLetterSpacing(0); + setSubtitleLineHeightMultiplier(1.2); + }} + style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }} + > + Default + + { + setSubtitleTextColor('#FFD700'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4); setSubtitleBgOpacity(0.3); setSubtitleTextShadow(false); + }} + style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,215,0,0.12)', borderWidth: 1, borderColor: 'rgba(255,215,0,0.35)' }} + > + Yellow + + { + setSubtitleTextColor('#FFFFFF'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(3); setSubtitleBgOpacity(0.0); setSubtitleTextShadow(false); setSubtitleLetterSpacing(0.5); + }} + style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(34,197,94,0.12)', borderWidth: 1, borderColor: 'rgba(34,197,94,0.35)' }} + > + High Contrast + + { + setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.6); setSubtitleTextShadow(true); setSubtitleOutline(true); setSubtitleAlign('center'); setSubtitleLineHeightMultiplier(1.3); + }} + style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(59,130,246,0.12)', borderWidth: 1, borderColor: 'rgba(59,130,246,0.35)' }} + > + Large + + + + + {/* Core controls */} + + + + Core + - Timing Offset (s) + + + Font Size + + + + + + + {subtitleSize} + + + + + + + + + + Show Background + + + + + + + + {/* Advanced controls */} + + + + Advanced + + + + + Text Color + + + {['#FFFFFF', '#FFD700', '#00E5FF', '#FF5C5C', '#00FF88', '#9b59b6', '#f97316'].map(c => ( + setSubtitleTextColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleTextColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} /> + ))} + + + + Align + + {([ { key: 'left', icon: 'format-align-left' }, { key: 'center', icon: 'format-align-center' }, { key: 'right', icon: 'format-align-right' } ] as const).map(a => ( + setSubtitleAlign(a.key)} style={{ paddingHorizontal: isCompact ? 8 : 10, paddingVertical: isCompact ? 4 : 6, borderRadius: 8, backgroundColor: subtitleAlign === a.key ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}> + + + ))} + + + + Bottom Offset - setSubtitleOffsetSec(+(subtitleOffsetSec - 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + setSubtitleBottomOffset(Math.max(0, subtitleBottomOffset - 5))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + + + + {subtitleBottomOffset} + + setSubtitleBottomOffset(subtitleBottomOffset + 5)} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + + + + + + Background Opacity + + setSubtitleBgOpacity(Math.max(0, +(subtitleBgOpacity - 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - - {subtitleOffsetSec.toFixed(1)} + + {subtitleBgOpacity.toFixed(1)} - setSubtitleOffsetSec(+(subtitleOffsetSec + 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + setSubtitleBgOpacity(Math.min(1, +(subtitleBgOpacity + 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - Nudge subtitles earlier (-) or later (+) to sync if needed. - - - {/* Reset to defaults */} - - { - setSubtitleTextColor('#FFFFFF'); - setSubtitleBgOpacity(0.7); - setSubtitleTextShadow(true); - setSubtitleOutline(true); - setSubtitleOutlineColor('#000000'); - setSubtitleOutlineWidth(4); - setSubtitleAlign('center'); - setSubtitleBottomOffset(10); - setSubtitleLetterSpacing(0); - setSubtitleLineHeightMultiplier(1.2); - setSubtitleOffsetSec(0); - }} - style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 8, backgroundColor: 'rgba(255,255,255,0.1)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }} - > - Reset to defaults - + + Text Shadow + setSubtitleTextShadow(!subtitleTextShadow)} style={{ paddingHorizontal: 10, paddingVertical: 8, borderRadius: 10, backgroundColor: subtitleTextShadow ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)', alignItems: 'center' }}> + {subtitleTextShadow ? 'On' : 'Off'} + + + + Outline Color + + {['#000000', '#FFFFFF', '#00E5FF', '#FF5C5C'].map(c => ( + setSubtitleOutlineColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleOutlineColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} /> + ))} + + + + Outline Width + + setSubtitleOutlineWidth(Math.max(0, subtitleOutlineWidth - 1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + + + + {subtitleOutlineWidth} + + setSubtitleOutlineWidth(subtitleOutlineWidth + 1)} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + + + + + + + Letter Spacing + + setSubtitleLetterSpacing(Math.max(0, +(subtitleLetterSpacing - 0.5).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + + + + {subtitleLetterSpacing.toFixed(1)} + + setSubtitleLetterSpacing(+(subtitleLetterSpacing + 0.5).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + + + + + + Line Height + + setSubtitleLineHeightMultiplier(Math.max(1, +(subtitleLineHeightMultiplier - 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + + + + {subtitleLineHeightMultiplier.toFixed(1)} + + setSubtitleLineHeightMultiplier(+(subtitleLineHeightMultiplier + 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + + + + + + + + Timing Offset (s) + + setSubtitleOffsetSec(+(subtitleOffsetSec - 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + + + + {subtitleOffsetSec.toFixed(1)} + + setSubtitleOffsetSec(+(subtitleOffsetSec + 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + + + + + Nudge subtitles earlier (-) or later (+) to sync if needed. + + + { + setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.7); setSubtitleTextShadow(true); + setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4); + setSubtitleAlign('center'); setSubtitleBottomOffset(10); setSubtitleLetterSpacing(0); + setSubtitleLineHeightMultiplier(1.2); setSubtitleOffsetSec(0); + }} + style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 8, backgroundColor: 'rgba(255,255,255,0.1)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }} + > + Reset to defaults + + - - )} - + )} + - ); - }; - - return ( - <> - {renderSubtitleMenu()} - + ); }; -export default SubtitleModals; \ No newline at end of file +export default SubtitleModals;