import React from 'react'; import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, Platform, useWindowDimensions } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import Animated, { FadeIn, FadeOut, 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; setShowSubtitleModal: (show: boolean) => void; showSubtitleLanguageModal: boolean; setShowSubtitleLanguageModal: (show: boolean) => void; isLoadingSubtitleList: boolean; isLoadingSubtitles: boolean; customSubtitles: SubtitleCue[]; availableSubtitles: WyzieSubtitle[]; 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; fetchAvailableSubtitles: () => void; loadWyzieSubtitle: (subtitle: WyzieSubtitle) => void; selectTextTrack: (trackId: number) => void; disableCustomSubtitles: () => void; increaseSubtitleSize: () => void; decreaseSubtitleSize: () => void; toggleSubtitleBackground: () => void; // Customization props subtitleTextColor: string; setSubtitleTextColor: (c: string) => void; subtitleBgOpacity: number; setSubtitleBgOpacity: (o: number) => void; subtitleTextShadow: boolean; setSubtitleTextShadow: (b: boolean) => void; subtitleOutline: boolean; setSubtitleOutline: (b: boolean) => void; subtitleOutlineColor: string; setSubtitleOutlineColor: (c: string) => void; subtitleOutlineWidth: number; setSubtitleOutlineWidth: (n: number) => void; subtitleAlign: 'center' | 'left' | 'right'; setSubtitleAlign: (a: 'center' | 'left' | 'right') => void; subtitleBottomOffset: number; setSubtitleBottomOffset: (n: number) => void; subtitleLetterSpacing: number; setSubtitleLetterSpacing: (n: number) => void; subtitleLineHeightMultiplier: number; setSubtitleLineHeightMultiplier: (n: number) => void; subtitleOffsetSec: number; setSubtitleOffsetSec: (n: number) => void; } // Dynamic sizing handled inside component with useWindowDimensions 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, }) => { 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 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 ); React.useEffect(() => { 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]); // Clear loading state when subtitles have finished loading React.useEffect(() => { if (!isLoadingSubtitles) { setLoadingSubtitleId(null); } }, [isLoadingSubtitles]); // 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 ( <> {/* Backdrop */} {/* Side Menu */} {/* Header */} Subtitles {useCustomSubtitles ? 'Addon in use' : 'Built‑in in use'} {/* 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' && ( 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 ? ( 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}` : ''} {(isLoadingSubtitles && loadingSubtitleId === sub.id) ? ( ) : isSelected ? ( ) : ( )} ); })} )} )} {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;