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