mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
added sub customize support
This commit is contained in:
parent
7d9f8fba86
commit
51550316ec
4 changed files with 394 additions and 11 deletions
|
|
@ -149,6 +149,18 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
const [customSubtitleVersion, setCustomSubtitleVersion] = useState<number>(0);
|
||||
const [subtitleSize, setSubtitleSize] = useState<number>(DEFAULT_SUBTITLE_SIZE);
|
||||
const [subtitleBackground, setSubtitleBackground] = useState<boolean>(true);
|
||||
// External subtitle customization
|
||||
const [subtitleFontFamily, setSubtitleFontFamily] = useState<string | undefined>(undefined);
|
||||
const [subtitleTextColor, setSubtitleTextColor] = useState<string>('#FFFFFF');
|
||||
const [subtitleBgOpacity, setSubtitleBgOpacity] = useState<number>(0.7);
|
||||
const [subtitleTextShadow, setSubtitleTextShadow] = useState<boolean>(true);
|
||||
const [subtitleOutline, setSubtitleOutline] = useState<boolean>(false);
|
||||
const [subtitleOutlineColor, setSubtitleOutlineColor] = useState<string>('#000000');
|
||||
const [subtitleOutlineWidth, setSubtitleOutlineWidth] = useState<number>(2);
|
||||
const [subtitleAlign, setSubtitleAlign] = useState<'center' | 'left' | 'right'>('center');
|
||||
const [subtitleBottomOffset, setSubtitleBottomOffset] = useState<number>(20);
|
||||
const [subtitleLetterSpacing, setSubtitleLetterSpacing] = useState<number>(0);
|
||||
const [subtitleLineHeightMultiplier, setSubtitleLineHeightMultiplier] = useState<number>(1.2);
|
||||
const [useCustomSubtitles, setUseCustomSubtitles] = useState<boolean>(false);
|
||||
const [isLoadingSubtitles, setIsLoadingSubtitles] = useState<boolean>(false);
|
||||
const [availableSubtitles, setAvailableSubtitles] = useState<WyzieSubtitle[]>([]);
|
||||
|
|
@ -1450,6 +1462,17 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
subtitleSize={subtitleSize}
|
||||
subtitleBackground={subtitleBackground}
|
||||
zoomScale={zoomScale}
|
||||
fontFamily={subtitleFontFamily}
|
||||
textColor={subtitleTextColor}
|
||||
backgroundOpacity={subtitleBgOpacity}
|
||||
textShadow={subtitleTextShadow}
|
||||
outline={subtitleOutline}
|
||||
outlineColor={subtitleOutlineColor}
|
||||
outlineWidth={subtitleOutlineWidth}
|
||||
align={subtitleAlign}
|
||||
bottomOffset={subtitleBottomOffset}
|
||||
letterSpacing={subtitleLetterSpacing}
|
||||
lineHeightMultiplier={subtitleLineHeightMultiplier}
|
||||
/>
|
||||
|
||||
<ResumeOverlay
|
||||
|
|
@ -1492,6 +1515,28 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
increaseSubtitleSize={increaseSubtitleSize}
|
||||
decreaseSubtitleSize={decreaseSubtitleSize}
|
||||
toggleSubtitleBackground={toggleSubtitleBackground}
|
||||
subtitleFontFamily={subtitleFontFamily}
|
||||
setSubtitleFontFamily={setSubtitleFontFamily}
|
||||
subtitleTextColor={subtitleTextColor}
|
||||
setSubtitleTextColor={setSubtitleTextColor}
|
||||
subtitleBgOpacity={subtitleBgOpacity}
|
||||
setSubtitleBgOpacity={setSubtitleBgOpacity}
|
||||
subtitleTextShadow={subtitleTextShadow}
|
||||
setSubtitleTextShadow={setSubtitleTextShadow}
|
||||
subtitleOutline={subtitleOutline}
|
||||
setSubtitleOutline={setSubtitleOutline}
|
||||
subtitleOutlineColor={subtitleOutlineColor}
|
||||
setSubtitleOutlineColor={setSubtitleOutlineColor}
|
||||
subtitleOutlineWidth={subtitleOutlineWidth}
|
||||
setSubtitleOutlineWidth={setSubtitleOutlineWidth}
|
||||
subtitleAlign={subtitleAlign}
|
||||
setSubtitleAlign={setSubtitleAlign}
|
||||
subtitleBottomOffset={subtitleBottomOffset}
|
||||
setSubtitleBottomOffset={setSubtitleBottomOffset}
|
||||
subtitleLetterSpacing={subtitleLetterSpacing}
|
||||
setSubtitleLetterSpacing={setSubtitleLetterSpacing}
|
||||
subtitleLineHeightMultiplier={subtitleLineHeightMultiplier}
|
||||
setSubtitleLineHeightMultiplier={setSubtitleLineHeightMultiplier}
|
||||
/>
|
||||
|
||||
<SourcesModal
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import ResumeOverlay from './modals/ResumeOverlay';
|
|||
import PlayerControls from './controls/PlayerControls';
|
||||
import CustomSubtitles from './subtitles/CustomSubtitles';
|
||||
import { SourcesModal } from './modals/SourcesModal';
|
||||
import axios from 'axios';
|
||||
import { stremioService } from '../../services/stremioService';
|
||||
|
||||
const VideoPlayer: React.FC = () => {
|
||||
|
|
@ -159,6 +160,18 @@ const VideoPlayer: React.FC = () => {
|
|||
const [currentSubtitle, setCurrentSubtitle] = useState<string>('');
|
||||
const [subtitleSize, setSubtitleSize] = useState<number>(DEFAULT_SUBTITLE_SIZE);
|
||||
const [subtitleBackground, setSubtitleBackground] = useState<boolean>(true);
|
||||
// External subtitle customization
|
||||
const [subtitleFontFamily, setSubtitleFontFamily] = useState<string | undefined>(undefined);
|
||||
const [subtitleTextColor, setSubtitleTextColor] = useState<string>('#FFFFFF');
|
||||
const [subtitleBgOpacity, setSubtitleBgOpacity] = useState<number>(0.7);
|
||||
const [subtitleTextShadow, setSubtitleTextShadow] = useState<boolean>(true);
|
||||
const [subtitleOutline, setSubtitleOutline] = useState<boolean>(false);
|
||||
const [subtitleOutlineColor, setSubtitleOutlineColor] = useState<string>('#000000');
|
||||
const [subtitleOutlineWidth, setSubtitleOutlineWidth] = useState<number>(2);
|
||||
const [subtitleAlign, setSubtitleAlign] = useState<'center' | 'left' | 'right'>('center');
|
||||
const [subtitleBottomOffset, setSubtitleBottomOffset] = useState<number>(20);
|
||||
const [subtitleLetterSpacing, setSubtitleLetterSpacing] = useState<number>(0);
|
||||
const [subtitleLineHeightMultiplier, setSubtitleLineHeightMultiplier] = useState<number>(1.2);
|
||||
const [useCustomSubtitles, setUseCustomSubtitles] = useState<boolean>(false);
|
||||
const [isLoadingSubtitles, setIsLoadingSubtitles] = useState<boolean>(false);
|
||||
const [availableSubtitles, setAvailableSubtitles] = useState<WyzieSubtitle[]>([]);
|
||||
|
|
@ -953,18 +966,100 @@ const VideoPlayer: React.FC = () => {
|
|||
};
|
||||
|
||||
const loadWyzieSubtitle = async (subtitle: WyzieSubtitle) => {
|
||||
logger.log(`[VideoPlayer] Subtitle click received: id=${subtitle.id}, lang=${subtitle.language}, url=${subtitle.url}`);
|
||||
setShowSubtitleLanguageModal(false);
|
||||
setIsLoadingSubtitles(true);
|
||||
try {
|
||||
const response = await fetch(subtitle.url);
|
||||
const srtContent = await response.text();
|
||||
logger.log('[VideoPlayer] Fetching subtitle SRT start');
|
||||
let srtContent = '';
|
||||
try {
|
||||
const axiosResp = await axios.get(subtitle.url, {
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Accept': 'text/plain, */*',
|
||||
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Nuvio/1.0'
|
||||
},
|
||||
responseType: 'text',
|
||||
transitional: { clarifyTimeoutError: true }
|
||||
});
|
||||
srtContent = typeof axiosResp.data === 'string' ? axiosResp.data : String(axiosResp.data || '');
|
||||
} catch (axiosErr: any) {
|
||||
logger.warn('[VideoPlayer] Axios subtitle fetch failed, falling back to fetch()', {
|
||||
message: axiosErr?.message,
|
||||
code: axiosErr?.code
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
try {
|
||||
const resp = await fetch(subtitle.url, { signal: controller.signal });
|
||||
srtContent = await resp.text();
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
logger.log(`[VideoPlayer] Fetching subtitle SRT done, size=${srtContent.length}`);
|
||||
const parsedCues = parseSRT(srtContent);
|
||||
setCustomSubtitles(parsedCues);
|
||||
setUseCustomSubtitles(true);
|
||||
logger.log(`[VideoPlayer] Parsed cues count=${parsedCues.length}`);
|
||||
|
||||
// For VLC on iOS: stop spinner early, then clear-apply and micro-seek nudge
|
||||
setIsLoadingSubtitles(false);
|
||||
logger.log('[VideoPlayer] isLoadingSubtitles -> false (early)');
|
||||
|
||||
// Clear existing state
|
||||
setUseCustomSubtitles(false);
|
||||
logger.log('[VideoPlayer] useCustomSubtitles -> false');
|
||||
setCustomSubtitles([]);
|
||||
logger.log('[VideoPlayer] customSubtitles -> []');
|
||||
setSelectedTextTrack(-1);
|
||||
logger.log('[VideoPlayer] selectedTextTrack -> -1');
|
||||
|
||||
// Apply immediately
|
||||
setCustomSubtitles(parsedCues);
|
||||
logger.log('[VideoPlayer] customSubtitles <- parsedCues');
|
||||
setUseCustomSubtitles(true);
|
||||
logger.log('[VideoPlayer] useCustomSubtitles -> true');
|
||||
setSelectedTextTrack(-1);
|
||||
logger.log('[VideoPlayer] selectedTextTrack -> -1 (disable native while using custom)');
|
||||
|
||||
// Immediately set current subtitle text
|
||||
try {
|
||||
const cueNow = parsedCues.find(cue => currentTime >= cue.start && currentTime <= cue.end);
|
||||
const textNow = cueNow ? cueNow.text : '';
|
||||
setCurrentSubtitle(textNow);
|
||||
logger.log('[VideoPlayer] currentSubtitle set immediately after apply');
|
||||
} catch (e) {
|
||||
logger.error('[VideoPlayer] Error setting immediate subtitle', e);
|
||||
}
|
||||
|
||||
// VLC micro-seek nudge
|
||||
try {
|
||||
if (vlcRef.current && duration > 0) {
|
||||
const wasPaused = paused;
|
||||
const original = currentTime;
|
||||
const forward = Math.min(original + 0.05, Math.max(duration - 0.1, 0));
|
||||
logger.log('[VideoPlayer] Performing micro-seek nudge', { original, forward });
|
||||
if (wasPaused) setPaused(false);
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// @ts-ignore - VLCPlayer seek method
|
||||
vlcRef.current?.seek(forward);
|
||||
setTimeout(() => {
|
||||
// @ts-ignore
|
||||
vlcRef.current?.seek(original);
|
||||
if (wasPaused) setPaused(true);
|
||||
logger.log('[VideoPlayer] Micro-seek nudge complete');
|
||||
}, 150);
|
||||
} catch (e) {
|
||||
logger.warn('[VideoPlayer] Micro-seek nudge failed', e);
|
||||
if (wasPaused) setPaused(true);
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
} catch(e) {
|
||||
logger.warn('[VideoPlayer] Outer micro-seek failed', e);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[VideoPlayer] Error loading Wyzie subtitle:', error);
|
||||
} finally {
|
||||
setIsLoadingSubtitles(false);
|
||||
}
|
||||
};
|
||||
|
|
@ -1275,7 +1370,7 @@ const VideoPlayer: React.FC = () => {
|
|||
<VLCPlayer
|
||||
ref={vlcRef}
|
||||
style={[styles.video, customVideoStyles, { transform: [{ scale: zoomScale }] }]}
|
||||
source={(() => {
|
||||
source={(() => {
|
||||
// FORCEFULLY use headers from route params if available - no filtering or modification
|
||||
const sourceWithHeaders = headers ? {
|
||||
uri: currentStreamUrl,
|
||||
|
|
@ -1339,6 +1434,17 @@ const VideoPlayer: React.FC = () => {
|
|||
subtitleSize={subtitleSize}
|
||||
subtitleBackground={subtitleBackground}
|
||||
zoomScale={zoomScale}
|
||||
fontFamily={subtitleFontFamily}
|
||||
textColor={subtitleTextColor}
|
||||
backgroundOpacity={subtitleBgOpacity}
|
||||
textShadow={subtitleTextShadow}
|
||||
outline={subtitleOutline}
|
||||
outlineColor={subtitleOutlineColor}
|
||||
outlineWidth={subtitleOutlineWidth}
|
||||
align={subtitleAlign}
|
||||
bottomOffset={subtitleBottomOffset}
|
||||
letterSpacing={subtitleLetterSpacing}
|
||||
lineHeightMultiplier={subtitleLineHeightMultiplier}
|
||||
/>
|
||||
|
||||
<ResumeOverlay
|
||||
|
|
@ -1381,6 +1487,28 @@ const VideoPlayer: React.FC = () => {
|
|||
increaseSubtitleSize={increaseSubtitleSize}
|
||||
decreaseSubtitleSize={decreaseSubtitleSize}
|
||||
toggleSubtitleBackground={toggleSubtitleBackground}
|
||||
subtitleFontFamily={subtitleFontFamily}
|
||||
setSubtitleFontFamily={setSubtitleFontFamily}
|
||||
subtitleTextColor={subtitleTextColor}
|
||||
setSubtitleTextColor={setSubtitleTextColor}
|
||||
subtitleBgOpacity={subtitleBgOpacity}
|
||||
setSubtitleBgOpacity={setSubtitleBgOpacity}
|
||||
subtitleTextShadow={subtitleTextShadow}
|
||||
setSubtitleTextShadow={setSubtitleTextShadow}
|
||||
subtitleOutline={subtitleOutline}
|
||||
setSubtitleOutline={setSubtitleOutline}
|
||||
subtitleOutlineColor={subtitleOutlineColor}
|
||||
setSubtitleOutlineColor={setSubtitleOutlineColor}
|
||||
subtitleOutlineWidth={subtitleOutlineWidth}
|
||||
setSubtitleOutlineWidth={setSubtitleOutlineWidth}
|
||||
subtitleAlign={subtitleAlign}
|
||||
setSubtitleAlign={setSubtitleAlign}
|
||||
subtitleBottomOffset={subtitleBottomOffset}
|
||||
setSubtitleBottomOffset={setSubtitleBottomOffset}
|
||||
subtitleLetterSpacing={subtitleLetterSpacing}
|
||||
setSubtitleLetterSpacing={setSubtitleLetterSpacing}
|
||||
subtitleLineHeightMultiplier={subtitleLineHeightMultiplier}
|
||||
setSubtitleLineHeightMultiplier={setSubtitleLineHeightMultiplier}
|
||||
/>
|
||||
|
||||
<SourcesModal
|
||||
|
|
|
|||
|
|
@ -31,6 +31,29 @@ interface SubtitleModalsProps {
|
|||
increaseSubtitleSize: () => void;
|
||||
decreaseSubtitleSize: () => void;
|
||||
toggleSubtitleBackground: () => void;
|
||||
// Customization props
|
||||
subtitleFontFamily?: string;
|
||||
setSubtitleFontFamily: (f?: string) => 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;
|
||||
}
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
|
|
@ -56,6 +79,28 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
increaseSubtitleSize,
|
||||
decreaseSubtitleSize,
|
||||
toggleSubtitleBackground,
|
||||
subtitleFontFamily,
|
||||
setSubtitleFontFamily,
|
||||
subtitleTextColor,
|
||||
setSubtitleTextColor,
|
||||
subtitleBgOpacity,
|
||||
setSubtitleBgOpacity,
|
||||
subtitleTextShadow,
|
||||
setSubtitleTextShadow,
|
||||
subtitleOutline,
|
||||
setSubtitleOutline,
|
||||
subtitleOutlineColor,
|
||||
setSubtitleOutlineColor,
|
||||
subtitleOutlineWidth,
|
||||
setSubtitleOutlineWidth,
|
||||
subtitleAlign,
|
||||
setSubtitleAlign,
|
||||
subtitleBottomOffset,
|
||||
setSubtitleBottomOffset,
|
||||
subtitleLetterSpacing,
|
||||
setSubtitleLetterSpacing,
|
||||
subtitleLineHeightMultiplier,
|
||||
setSubtitleLineHeightMultiplier,
|
||||
}) => {
|
||||
// Track which specific online subtitle is currently loaded
|
||||
const [selectedOnlineSubtitleId, setSelectedOnlineSubtitleId] = React.useState<string | null>(null);
|
||||
|
|
@ -295,6 +340,118 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
</View>
|
||||
)}
|
||||
|
||||
{/* Customization Section - Only for custom subtitles */}
|
||||
{useCustomSubtitles && (
|
||||
<View style={{ marginBottom: 30, gap: 10 }}>
|
||||
<Text style={{ color: 'rgba(255,255,255,0.7)', fontSize: 14, fontWeight: '600', marginBottom: 10, textTransform: 'uppercase' }}>Appearance</Text>
|
||||
<View style={{ backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 16, padding: 16, gap: 12 }}>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text style={{ color: 'white' }}>Text Color</Text>
|
||||
<View style={{ flexDirection: 'row', gap: 8 }}>
|
||||
{['#FFFFFF', '#FFD700', '#00E5FF', '#FF5C5C', '#00FF88'].map(c => (
|
||||
<TouchableOpacity key={c} onPress={() => setSubtitleTextColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 1, borderColor: 'rgba(255,255,255,0.3)' }} />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text style={{ color: 'white' }}>Align</Text>
|
||||
<View style={{ flexDirection: 'row', gap: 8 }}>
|
||||
{(['left','center','right'] as const).map(a => (
|
||||
<TouchableOpacity key={a} onPress={() => setSubtitleAlign(a)} style={{ paddingHorizontal: 10, paddingVertical: 6, borderRadius: 6, backgroundColor: subtitleAlign === a ? 'rgba(255,255,255,0.2)' : 'transparent', borderWidth: 1, borderColor: 'rgba(255,255,255,0.2)' }}>
|
||||
<Text style={{ color: 'white', textTransform: 'capitalize' }}>{a}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text style={{ color: 'white' }}>Bottom Offset</Text>
|
||||
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
|
||||
<TouchableOpacity onPress={() => setSubtitleBottomOffset(Math.max(0, subtitleBottomOffset - 5))} style={{ width: 30, height: 30, borderRadius: 15, backgroundColor: 'rgba(255,255,255,0.2)', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<MaterialIcons name="keyboard-arrow-down" color="#fff" size={20} />
|
||||
</TouchableOpacity>
|
||||
<Text style={{ color: 'white', width: 40, textAlign: 'center' }}>{subtitleBottomOffset}</Text>
|
||||
<TouchableOpacity onPress={() => setSubtitleBottomOffset(subtitleBottomOffset + 5)} style={{ width: 30, height: 30, borderRadius: 15, backgroundColor: 'rgba(255,255,255,0.2)', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<MaterialIcons name="keyboard-arrow-up" color="#fff" size={20} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text style={{ color: 'white' }}>Background Opacity</Text>
|
||||
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
|
||||
<TouchableOpacity onPress={() => setSubtitleBgOpacity(Math.max(0, +(subtitleBgOpacity - 0.1).toFixed(1)))} style={{ width: 30, height: 30, borderRadius: 15, backgroundColor: 'rgba(255,255,255,0.2)', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<MaterialIcons name="remove" color="#fff" size={18} />
|
||||
</TouchableOpacity>
|
||||
<Text style={{ color: 'white', width: 40, textAlign: 'center' }}>{subtitleBgOpacity.toFixed(1)}</Text>
|
||||
<TouchableOpacity onPress={() => setSubtitleBgOpacity(Math.min(1, +(subtitleBgOpacity + 0.1).toFixed(1)))} style={{ width: 30, height: 30, borderRadius: 15, backgroundColor: 'rgba(255,255,255,0.2)', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<MaterialIcons name="add" color="#fff" size={18} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text style={{ color: 'white' }}>Text Shadow</Text>
|
||||
<TouchableOpacity onPress={() => setSubtitleTextShadow(!subtitleTextShadow)} style={{ paddingHorizontal: 10, paddingVertical: 6, borderRadius: 6, backgroundColor: subtitleTextShadow ? 'rgba(255,255,255,0.2)' : 'transparent', borderWidth: 1, borderColor: 'rgba(255,255,255,0.2)' }}>
|
||||
<Text style={{ color: 'white' }}>{subtitleTextShadow ? 'On' : 'Off'}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text style={{ color: 'white' }}>Outline</Text>
|
||||
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
|
||||
<TouchableOpacity onPress={() => setSubtitleOutline(!subtitleOutline)} style={{ paddingHorizontal: 10, paddingVertical: 6, borderRadius: 6, backgroundColor: subtitleOutline ? 'rgba(255,255,255,0.2)' : 'transparent', borderWidth: 1, borderColor: 'rgba(255,255,255,0.2)' }}>
|
||||
<Text style={{ color: 'white' }}>{subtitleOutline ? 'On' : 'Off'}</Text>
|
||||
</TouchableOpacity>
|
||||
{['#000000', '#FFFFFF', '#00E5FF', '#FF5C5C'].map(c => (
|
||||
<TouchableOpacity key={c} onPress={() => setSubtitleOutlineColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 1, borderColor: 'rgba(255,255,255,0.3)' }} />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text style={{ color: 'white' }}>Outline Width</Text>
|
||||
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
|
||||
<TouchableOpacity onPress={() => setSubtitleOutlineWidth(Math.max(0, subtitleOutlineWidth - 1))} style={{ width: 30, height: 30, borderRadius: 15, backgroundColor: 'rgba(255,255,255,0.2)', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<MaterialIcons name="remove" color="#fff" size={18} />
|
||||
</TouchableOpacity>
|
||||
<Text style={{ color: 'white', width: 40, textAlign: 'center' }}>{subtitleOutlineWidth}</Text>
|
||||
<TouchableOpacity onPress={() => setSubtitleOutlineWidth(subtitleOutlineWidth + 1)} style={{ width: 30, height: 30, borderRadius: 15, backgroundColor: 'rgba(255,255,255,0.2)', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<MaterialIcons name="add" color="#fff" size={18} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text style={{ color: 'white' }}>Letter Spacing</Text>
|
||||
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
|
||||
<TouchableOpacity onPress={() => setSubtitleLetterSpacing(Math.max(0, +(subtitleLetterSpacing - 0.5).toFixed(1)))} style={{ width: 30, height: 30, borderRadius: 15, backgroundColor: 'rgba(255,255,255,0.2)', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<MaterialIcons name="remove" color="#fff" size={18} />
|
||||
</TouchableOpacity>
|
||||
<Text style={{ color: 'white', width: 40, textAlign: 'center' }}>{subtitleLetterSpacing.toFixed(1)}</Text>
|
||||
<TouchableOpacity onPress={() => setSubtitleLetterSpacing(+(subtitleLetterSpacing + 0.5).toFixed(1))} style={{ width: 30, height: 30, borderRadius: 15, backgroundColor: 'rgba(255,255,255,0.2)', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<MaterialIcons name="add" color="#fff" size={18} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text style={{ color: 'white' }}>Line Height</Text>
|
||||
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
|
||||
<TouchableOpacity onPress={() => setSubtitleLineHeightMultiplier(Math.max(1, +(subtitleLineHeightMultiplier - 0.1).toFixed(1)))} style={{ width: 30, height: 30, borderRadius: 15, backgroundColor: 'rgba(255,255,255,0.2)', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<MaterialIcons name="remove" color="#fff" size={18} />
|
||||
</TouchableOpacity>
|
||||
<Text style={{ color: 'white', width: 40, textAlign: 'center' }}>{subtitleLineHeightMultiplier.toFixed(1)}</Text>
|
||||
<TouchableOpacity onPress={() => setSubtitleLineHeightMultiplier(+(subtitleLineHeightMultiplier + 0.1).toFixed(1))} style={{ width: 30, height: 30, borderRadius: 15, backgroundColor: 'rgba(255,255,255,0.2)', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<MaterialIcons name="add" color="#fff" size={18} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Built-in Subtitles */}
|
||||
{vlcTextTracks.length > 0 && (
|
||||
<View style={{ marginBottom: 30 }}>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,18 @@ interface CustomSubtitlesProps {
|
|||
subtitleSize: number;
|
||||
subtitleBackground: boolean;
|
||||
zoomScale?: number; // current video zoom scale; defaults to 1
|
||||
// New customization props
|
||||
fontFamily?: string;
|
||||
textColor?: string;
|
||||
backgroundOpacity?: number; // 0..1
|
||||
textShadow?: boolean;
|
||||
outline?: boolean;
|
||||
outlineColor?: string;
|
||||
outlineWidth?: number; // px
|
||||
align?: 'center' | 'left' | 'right';
|
||||
bottomOffset?: number; // px from bottom
|
||||
letterSpacing?: number;
|
||||
lineHeightMultiplier?: number; // multiplies subtitleSize
|
||||
}
|
||||
|
||||
export const CustomSubtitles: React.FC<CustomSubtitlesProps> = ({
|
||||
|
|
@ -16,27 +28,68 @@ export const CustomSubtitles: React.FC<CustomSubtitlesProps> = ({
|
|||
subtitleSize,
|
||||
subtitleBackground,
|
||||
zoomScale = 1,
|
||||
fontFamily,
|
||||
textColor = '#FFFFFF',
|
||||
backgroundOpacity = 0.7,
|
||||
textShadow = true,
|
||||
outline = false,
|
||||
outlineColor = '#000000',
|
||||
outlineWidth = 2,
|
||||
align = 'center',
|
||||
bottomOffset = 20,
|
||||
letterSpacing = 0,
|
||||
lineHeightMultiplier = 1.2,
|
||||
}) => {
|
||||
if (!useCustomSubtitles || !currentSubtitle) return null;
|
||||
|
||||
const inverseScale = 1 / zoomScale;
|
||||
const bgColor = subtitleBackground ? `rgba(0, 0, 0, ${Math.min(Math.max(backgroundOpacity, 0), 1)})` : 'transparent';
|
||||
|
||||
// Outline via textShadow for multi-direction pass
|
||||
const outlineStyle = outline
|
||||
? {
|
||||
textShadowColor: outlineColor,
|
||||
textShadowOffset: { width: 0, height: 0 },
|
||||
textShadowRadius: outlineWidth,
|
||||
}
|
||||
: {};
|
||||
|
||||
const shadowStyle = textShadow
|
||||
? {
|
||||
textShadowColor: 'rgba(0, 0, 0, 0.9)',
|
||||
textShadowOffset: { width: 2, height: 2 },
|
||||
textShadowRadius: 4,
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.customSubtitleContainer}
|
||||
style={[
|
||||
styles.customSubtitleContainer,
|
||||
{ bottom: bottomOffset },
|
||||
]}
|
||||
pointerEvents="none"
|
||||
>
|
||||
<View style={[
|
||||
styles.customSubtitleWrapper,
|
||||
{
|
||||
backgroundColor: subtitleBackground ? 'rgba(0, 0, 0, 0.7)' : 'transparent',
|
||||
backgroundColor: bgColor,
|
||||
alignSelf: align === 'center' ? 'center' : align === 'left' ? 'flex-start' : 'flex-end',
|
||||
}
|
||||
]}>
|
||||
<Text style={[
|
||||
styles.customSubtitleText,
|
||||
{
|
||||
styles.customSubtitleText,
|
||||
{
|
||||
color: textColor,
|
||||
fontFamily,
|
||||
textAlign: align,
|
||||
letterSpacing,
|
||||
fontSize: subtitleSize * inverseScale,
|
||||
lineHeight: subtitleSize * lineHeightMultiplier * inverseScale,
|
||||
transform: [{ scale: inverseScale }],
|
||||
}
|
||||
},
|
||||
shadowStyle,
|
||||
outlineStyle,
|
||||
]}>
|
||||
{currentSubtitle}
|
||||
</Text>
|
||||
|
|
|
|||
Loading…
Reference in a new issue