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;