diff --git a/src/components/player/modals/SubtitleModals.tsx b/src/components/player/modals/SubtitleModals.tsx index da5f5b5..b6713d4 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, Platform, useWindowDimensions, StyleSheet } from 'react-native'; +import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, Platform, useWindowDimensions } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; -import Animated, { - FadeIn, +import Animated, { + FadeIn, FadeOut, - SlideInDown, - SlideOutDown, - useAnimatedStyle, - withTiming, + SlideInRight, + SlideOutRight, } from 'react-native-reanimated'; +import { styles } from '../utils/playerStyles'; import { WyzieSubtitle, SubtitleCue } from '../utils/playerTypes'; import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; interface SubtitleModalsProps { showSubtitleModal: boolean; @@ -24,6 +24,7 @@ interface SubtitleModalsProps { 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; @@ -34,6 +35,7 @@ interface SubtitleModalsProps { increaseSubtitleSize: () => void; decreaseSubtitleSize: () => void; toggleSubtitleBackground: () => void; + // Customization props subtitleTextColor: string; setSubtitleTextColor: (c: string) => void; subtitleBgOpacity: number; @@ -58,407 +60,857 @@ interface SubtitleModalsProps { setSubtitleOffsetSec: (n: number) => void; } -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} - - - - ); -}; +// Dynamic sizing handled inside component with useWindowDimensions export const SubtitleModals: React.FC = ({ - 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, + 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, }) => { + 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); - const [activeTab, setActiveTab] = React.useState<'built-in' | 'addon' | 'appearance'>('built-in'); - + // 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 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 * 0.9, 420); - const menuMaxHeight = height * 0.95; - + const menuWidth = Math.min( + width * (isIos ? (isLandscape ? 0.6 : 0.8) : 0.85), + isIos ? 420 : 400 + ); + React.useEffect(() => { - if (showSubtitleModal && !isLoadingSubtitleList && availableSubtitles.length === 0) fetchAvailableSubtitles(); + if (showSubtitleModal && !isLoadingSubtitleList && availableSubtitles.length === 0) { + fetchAvailableSubtitles(); + } }, [showSubtitleModal]); - const handleClose = () => setShowSubtitleModal(false); + // Reset selected addon subtitle when switching to built-in tracks + React.useEffect(() => { + if (!useCustomSubtitles) { + setSelectedOnlineSubtitleId(null); + } + }, [useCustomSubtitles]); - if (!showSubtitleModal) return null; + // Clear loading state when subtitles have finished loading + React.useEffect(() => { + if (!isLoadingSubtitles) { + setLoadingSubtitleId(null); + } + }, [isLoadingSubtitles]); - return ( - - {/* Backdrop */} - - - + // Keep tab in sync with current usage + React.useEffect(() => { + setActiveTab(useCustomSubtitles ? 'addon' : 'built-in'); + }, [useCustomSubtitles]); - {/* Centered Modal Container */} - - { + 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 ( + <> + {/* Backdrop */} + + + + + {/* Side Menu */} + {/* Header */} - - Subtitles + + + Subtitles + + + {useCustomSubtitles ? 'Addon in use' : 'Built‑in in use'} + + + + + + - {/* Tab Bar */} - - setActiveTab('built-in')} /> - setActiveTab('addon')} /> - setActiveTab('appearance')} /> + {/* 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} + + ))} - - - {activeTab === 'built-in' && ( - - { selectTextTrack(-1); setSelectedOnlineSubtitleId(null); }} - style={{ padding: 10, borderRadius: 12, backgroundColor: selectedTextTrack === -1 ? 'white' : 'rgba(242, 184, 181)' }} - > - None - - {ksTextTracks.map((track) => ( + + {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 && ( { 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' }} + style={{ + backgroundColor: 'rgba(239, 68, 68, 0.15)', + borderRadius: 12, + paddingHorizontal: chipPadH, + paddingVertical: chipPadV-2, + flexDirection: 'row', + alignItems: 'center', + }} + onPress={() => { + disableCustomSubtitles(); + setSelectedOnlineSubtitleId(null); + }} + activeOpacity={0.7} > - {getTrackDisplayName(track)} - {selectedTextTrack === track.id && } + + + Disable + - ))} + )} + fetchAvailableSubtitles()} + disabled={isLoadingSubtitleList} + > + {isLoadingSubtitleList ? ( + + ) : ( + + )} + + {isLoadingSubtitleList ? 'Searching' : 'Refresh'} + + - )} + - {activeTab === 'addon' && ( + {(availableSubtitles.length === 0) && !isLoadingSubtitleList ? ( + fetchAvailableSubtitles()} + activeOpacity={0.7} + > + + + Tap to fetch from addons + + + ) : isLoadingSubtitleList ? ( + + + + Searching... + + + ) : ( - {availableSubtitles.length === 0 ? ( - - - Search Online Subtitles - - ) : ( - availableSubtitles.map((sub) => ( + {availableSubtitles.map((sub) => { + const isSelected = useCustomSubtitles && selectedOnlineSubtitleId === sub.id; + return ( { setSelectedOnlineSubtitleId(sub.id); loadWyzieSubtitle(sub); }} - style={{ padding: 6, borderRadius: 12, backgroundColor: selectedOnlineSubtitleId === sub.id ? 'white' : 'rgba(255,255,255,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }} - > - - {sub.display} - {formatLanguage(sub.language)} - - - )) - )} - - )} - - {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={{ + backgroundColor: isSelected ? 'rgba(34, 197, 94, 0.15)' : 'rgba(255, 255, 255, 0.05)', + borderRadius: 16, + padding: sectionPad, + borderWidth: 1, + borderColor: isSelected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(255, 255, 255, 0.1)', }} - 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); + handleLoadWyzieSubtitle(sub); }} - style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,215,0,0.12)', borderWidth: 1, borderColor: 'rgba(255,215,0,0.35)' }} + activeOpacity={0.7} + disabled={isLoadingSubtitles} > - 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 - - - - - - - {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 - - 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' }}> - - - - {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' }}> - - - - - - 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)} + + + + {sub.display} + + {(() => { + const filename = getFileNameFromUrl(sub.url); + if (!filename) return null; + return ( + + {filename} + + ); + })()} + + {formatLanguage(sub.language)}{sub.source ? ` · ${sub.source}` : ''} + - 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' }}> - - + {(isLoadingSubtitles && loadingSubtitleId === sub.id) ? ( + + ) : isSelected ? ( + + ) : ( + + )} - - - 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 - - + ); + })} )} + )} + + {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)} + + 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 */} + + + 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. + + + {/* 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 + + + + + )} + - - + + ); + }; + + return ( + <> + {renderSubtitleMenu()} + ); }; -export default SubtitleModals; +export default SubtitleModals; \ No newline at end of file