import React from 'react'; import { View, Text, TouchableOpacity, ScrollView, Platform, useWindowDimensions, StyleSheet } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import Animated, { FadeIn, FadeOut, SlideInDown, SlideOutDown, useAnimatedStyle, withTiming, } from 'react-native-reanimated'; import { useTranslation } from 'react-i18next'; import { WyzieSubtitle, SubtitleCue } from '../utils/playerTypes'; import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils'; 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; isKsPlayerActive?: boolean; // Whether ExoPlayer is being used (limits subtitle styling options) useExoPlayer?: boolean; subtitleSize: number; subtitleBackground: boolean; fetchAvailableSubtitles: () => void; loadWyzieSubtitle: (subtitle: WyzieSubtitle) => void; selectTextTrack: (trackId: number) => void; disableCustomSubtitles: () => void; setSubtitlesAutoSelect?: (autoSelect: boolean) => void; increaseSubtitleSize: () => void; decreaseSubtitleSize: () => void; toggleSubtitleBackground: () => void; 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; selectedExternalSubtitleId?: string | null; // ID of currently selected external/addon subtitle onOpenSyncModal?: () => void; // Callback to open the visual sync modal } 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, isLoadingSubtitleList, isLoadingSubtitles, availableSubtitles, ksTextTracks, selectedTextTrack, useCustomSubtitles, subtitleSize, subtitleBackground, fetchAvailableSubtitles, loadWyzieSubtitle, selectTextTrack, increaseSubtitleSize, decreaseSubtitleSize, toggleSubtitleBackground, subtitleTextColor, setSubtitleTextColor, useExoPlayer = false, subtitleBgOpacity, setSubtitleBgOpacity, subtitleTextShadow, setSubtitleTextShadow, subtitleOutline, setSubtitleOutline, subtitleOutlineColor, setSubtitleOutlineColor, subtitleOutlineWidth, setSubtitleOutlineWidth, subtitleAlign, setSubtitleAlign, subtitleBottomOffset, setSubtitleBottomOffset, subtitleLetterSpacing, setSubtitleLetterSpacing, subtitleLineHeightMultiplier, setSubtitleLineHeightMultiplier, subtitleOffsetSec, setSubtitleOffsetSec, setSubtitlesAutoSelect, selectedExternalSubtitleId, onOpenSyncModal, }) => { const { t } = useTranslation(); const { width, height } = useWindowDimensions(); const isIos = Platform.OS === 'ios'; const isLandscape = width > height; // Use prop value if provided (for auto-selected subtitles), otherwise use local state const [localSelectedId, setLocalSelectedId] = React.useState(null); const selectedOnlineSubtitleId = selectedExternalSubtitleId ?? localSelectedId; const setSelectedOnlineSubtitleId = setLocalSelectedId; const [activeTab, setActiveTab] = React.useState<'built-in' | 'addon' | 'appearance'>('built-in'); const isCompact = width < 360 || height < 640; // Internal subtitle is active when a built-in track is selected AND not using custom/addon subtitles const isUsingInternalSubtitle = selectedTextTrack >= 0 && !useCustomSubtitles; // ExoPlayer internal subtitles have limited styling support const isExoPlayerInternal = useExoPlayer && isUsingInternalSubtitle; 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; React.useEffect(() => { if (showSubtitleModal && !isLoadingSubtitleList && availableSubtitles.length === 0) { fetchAvailableSubtitles(); } }, [showSubtitleModal]); const handleClose = () => setShowSubtitleModal(false); if (!showSubtitleModal) return null; return ( {/* Backdrop */} {/* Centered Modal Container */} {/* Header */} {t('player_ui.subtitles')} {/* Tab Bar */} setActiveTab('built-in')} /> setActiveTab('addon')} /> setActiveTab('appearance')} /> {activeTab === 'built-in' && ( { selectTextTrack(-1); setSelectedOnlineSubtitleId(null); // Disable auto-select for future playback sessions setSubtitlesAutoSelect?.(false); }} style={{ padding: 10, borderRadius: 12, backgroundColor: selectedTextTrack === -1 ? 'white' : 'rgba(242, 184, 181)' }} > {t('player_ui.none')} {ksTextTracks.map((track) => ( { selectTextTrack(track.id); setSelectedOnlineSubtitleId(null); // Enable auto-select for future playback sessions when user selects a subtitle setSubtitlesAutoSelect?.(true); }} 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 === 'addon' && ( {availableSubtitles.length === 0 ? ( {t('player_ui.search_online_subtitles')} ) : ( availableSubtitles.map((sub) => ( { setSelectedOnlineSubtitleId(sub.id); loadWyzieSubtitle(sub); // Enable auto-select for future playback sessions when user selects a subtitle setSubtitlesAutoSelect?.(true); }} 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' }} > {sub.display} {formatLanguage(sub.language)} {selectedOnlineSubtitleId === sub.id && } )) )} )} {activeTab === 'appearance' && ( {/* Live Preview */} {t('player_ui.preview')} The quick brown fox jumps over the lazy dog. {/* Quick Presets - only for CustomSubtitles overlay */} {!isUsingInternalSubtitle && ( {t('player_ui.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)' }} > {t('player_ui.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)' }} > {t('player_ui.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)' }} > {t('player_ui.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)' }} > {t('player_ui.large')} )} {/* Core controls */} {t('player_ui.core')} {t('player_ui.font_size')} {subtitleSize} {/* Background is only for CustomSubtitles (external/addon). Built-in subtitles force background OFF. */} {!isUsingInternalSubtitle && ( {t('player_ui.show_background')} )} {/* Advanced controls */} {isUsingInternalSubtitle ? t('player_ui.position') : t('player_ui.advanced')} {/* Text Color - supported for MPV built-in, and for CustomSubtitles */} {t('player_ui.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 - only supported for CustomSubtitles overlay */} {!isUsingInternalSubtitle && ( {t('player_ui.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)' }}> ))} )} {t('player_ui.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 (CustomSubtitles only) */} {!isUsingInternalSubtitle && ( {t('player_ui.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' }}> )} {!isUsingInternalSubtitle && ( {t('player_ui.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 ? t('player_ui.on') : t('player_ui.off')} )} {/* Outline controls (now supported for ExoPlayer internal via native patch) */} {isUsingInternalSubtitle ? ( // KSPlayer built-in subtitles: only expose an Outline on/off toggle (no width control). {t('player_ui.outline')} setSubtitleOutline(!subtitleOutline)} style={{ paddingHorizontal: 10, paddingVertical: 8, borderRadius: 10, backgroundColor: subtitleOutline ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)', alignItems: 'center' }} > {subtitleOutline ? t('player_ui.on') : t('player_ui.off')} ) : ( <> {t('player_ui.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)' }} /> ))} {t('player_ui.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' }}> )} {!isUsingInternalSubtitle && ( {t('player_ui.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' }}> {t('player_ui.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 - Not supported on ExoPlayer internal subtitles */} {!isExoPlayerInternal && ( {t('player_ui.timing_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' }}> {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' }}> {/* Visual Sync Button */} {onOpenSyncModal && ( { setShowSubtitleModal(false); setTimeout(() => onOpenSyncModal(), 100); }} style={{ marginTop: 12, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', padding: 12, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)', }} > {t('player_ui.visual_sync')} )} {t('player_ui.timing_hint')} )} { 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)' }} > {t('player_ui.reset_defaults')} )} ); }; export default SubtitleModals;