mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +00:00
Added proper subtitle outline support
This commit is contained in:
parent
2a118a17d4
commit
65845c6e10
2 changed files with 107 additions and 27 deletions
|
|
@ -679,7 +679,24 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
{/* Outline */}
|
{/* Outline */}
|
||||||
<View style={{ flex: 1, gap: 8 }}>
|
<View style={{ flex: 1, gap: 8 }}>
|
||||||
<Text style={{ color: 'white', fontWeight: '600' }}>Outline</Text>
|
<Text style={{ color: 'white', fontWeight: '600' }}>Outline</Text>
|
||||||
<TouchableOpacity onPress={() => 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' }}>
|
<TouchableOpacity onPress={() => {
|
||||||
|
const next = !subtitleOutline;
|
||||||
|
setSubtitleOutline(next);
|
||||||
|
if (next) {
|
||||||
|
// Apply sensible defaults when enabling outline unless user already set larger values
|
||||||
|
if (subtitleSize < 24) {
|
||||||
|
// increase by calling increase handler enough times or provide a direct setter via size controls
|
||||||
|
// We only have +/- handlers here, so set via stepping until >= 24
|
||||||
|
const steps = Math.ceil((24 - subtitleSize) / 1); // size is integer steps
|
||||||
|
for (let i = 0; i < steps; i++) {
|
||||||
|
increaseSubtitleSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (subtitleBottomOffset < 40) {
|
||||||
|
setSubtitleBottomOffset(40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}} 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' }}>
|
||||||
<Text style={{ color: '#fff', fontWeight: '700' }}>{subtitleOutline ? 'On' : 'Off'}</Text>
|
<Text style={{ color: '#fff', fontWeight: '700' }}>{subtitleOutline ? 'On' : 'Off'}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text } from 'react-native';
|
import { View, Text } from 'react-native';
|
||||||
|
import Svg, { Text as SvgText, TSpan } from 'react-native-svg';
|
||||||
import { styles } from '../utils/playerStyles';
|
import { styles } from '../utils/playerStyles';
|
||||||
|
|
||||||
interface CustomSubtitlesProps {
|
interface CustomSubtitlesProps {
|
||||||
|
|
@ -45,16 +46,10 @@ export const CustomSubtitles: React.FC<CustomSubtitlesProps> = ({
|
||||||
const inverseScale = 1 / zoomScale;
|
const inverseScale = 1 / zoomScale;
|
||||||
const bgColor = subtitleBackground ? `rgba(0, 0, 0, ${Math.min(Math.max(backgroundOpacity, 0), 1)})` : 'transparent';
|
const bgColor = subtitleBackground ? `rgba(0, 0, 0, ${Math.min(Math.max(backgroundOpacity, 0), 1)})` : 'transparent';
|
||||||
|
|
||||||
// Outline via textShadow for multi-direction pass
|
// When using crisp outline, prefer SVG text with real stroke instead of blur shadow
|
||||||
const outlineStyle = outline
|
const useCrispSvgOutline = outline === true;
|
||||||
? {
|
|
||||||
textShadowColor: outlineColor,
|
|
||||||
textShadowOffset: { width: 0, height: 0 },
|
|
||||||
textShadowRadius: outlineWidth,
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const shadowStyle = textShadow
|
const shadowStyle = (textShadow && !useCrispSvgOutline)
|
||||||
? {
|
? {
|
||||||
textShadowColor: 'rgba(0, 0, 0, 0.9)',
|
textShadowColor: 'rgba(0, 0, 0, 0.9)',
|
||||||
textShadowOffset: { width: 2, height: 2 },
|
textShadowOffset: { width: 2, height: 2 },
|
||||||
|
|
@ -62,6 +57,12 @@ export const CustomSubtitles: React.FC<CustomSubtitlesProps> = ({
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
|
// Prepare content lines
|
||||||
|
const lines = String(currentSubtitle).split(/\r?\n/);
|
||||||
|
const displayFontSize = subtitleSize * inverseScale;
|
||||||
|
const displayLineHeight = subtitleSize * lineHeightMultiplier * inverseScale;
|
||||||
|
const svgHeight = Math.max(displayFontSize, lines.length * displayLineHeight);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
|
@ -74,25 +75,87 @@ export const CustomSubtitles: React.FC<CustomSubtitlesProps> = ({
|
||||||
styles.customSubtitleWrapper,
|
styles.customSubtitleWrapper,
|
||||||
{
|
{
|
||||||
backgroundColor: bgColor,
|
backgroundColor: bgColor,
|
||||||
alignSelf: align === 'center' ? 'center' : align === 'left' ? 'flex-start' : 'flex-end',
|
position: 'relative',
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
}
|
}
|
||||||
]}>
|
]}>
|
||||||
<Text style={[
|
{useCrispSvgOutline ? (
|
||||||
styles.customSubtitleText,
|
// Crisp outline using react-native-svg (stroke under, fill on top)
|
||||||
{
|
<Svg
|
||||||
color: textColor,
|
width={'100%'}
|
||||||
fontFamily,
|
height={svgHeight}
|
||||||
textAlign: align,
|
viewBox={`0 0 1000 ${svgHeight}`}
|
||||||
letterSpacing,
|
preserveAspectRatio="xMidYMid meet"
|
||||||
fontSize: subtitleSize * inverseScale,
|
style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}
|
||||||
lineHeight: subtitleSize * lineHeightMultiplier * inverseScale,
|
>
|
||||||
transform: [{ scale: inverseScale }],
|
{(() => {
|
||||||
},
|
const anchor = align === 'center' ? 'middle' : align === 'left' ? 'start' : 'end';
|
||||||
shadowStyle,
|
const x = 500; // always compute against center of 0..1000; anchor handles alignment
|
||||||
outlineStyle,
|
const baseFontSize = displayFontSize;
|
||||||
]}>
|
const lineHeightPx = displayLineHeight;
|
||||||
{currentSubtitle}
|
const strokeWidth = Math.max(0.5, outlineWidth);
|
||||||
</Text>
|
return (
|
||||||
|
<>
|
||||||
|
{/* Stroke layer */}
|
||||||
|
<SvgText
|
||||||
|
x={x}
|
||||||
|
y={baseFontSize}
|
||||||
|
textAnchor={anchor}
|
||||||
|
fill="none"
|
||||||
|
stroke={outlineColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeMiterlimit={2}
|
||||||
|
fontFamily={fontFamily}
|
||||||
|
fontSize={baseFontSize}
|
||||||
|
letterSpacing={letterSpacing}
|
||||||
|
>
|
||||||
|
{lines.map((line, idx) => (
|
||||||
|
<TSpan key={idx} x={x} dy={idx === 0 ? 0 : lineHeightPx}>
|
||||||
|
{line}
|
||||||
|
</TSpan>
|
||||||
|
))}
|
||||||
|
</SvgText>
|
||||||
|
{/* Fill layer */}
|
||||||
|
<SvgText
|
||||||
|
x={x}
|
||||||
|
y={baseFontSize}
|
||||||
|
textAnchor={anchor}
|
||||||
|
fill={textColor}
|
||||||
|
fontFamily={fontFamily}
|
||||||
|
fontSize={baseFontSize}
|
||||||
|
letterSpacing={letterSpacing}
|
||||||
|
>
|
||||||
|
{lines.map((line, idx) => (
|
||||||
|
<TSpan key={idx} x={x} dy={idx === 0 ? 0 : lineHeightPx}>
|
||||||
|
{line}
|
||||||
|
</TSpan>
|
||||||
|
))}
|
||||||
|
</SvgText>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</Svg>
|
||||||
|
) : (
|
||||||
|
// No outline: use RN Text with (optional) shadow
|
||||||
|
<Text style={[
|
||||||
|
styles.customSubtitleText,
|
||||||
|
{
|
||||||
|
color: textColor,
|
||||||
|
fontFamily,
|
||||||
|
textAlign: align,
|
||||||
|
letterSpacing,
|
||||||
|
fontSize: subtitleSize * inverseScale,
|
||||||
|
lineHeight: subtitleSize * lineHeightMultiplier * inverseScale,
|
||||||
|
transform: [{ scale: inverseScale }],
|
||||||
|
},
|
||||||
|
shadowStyle,
|
||||||
|
]}>
|
||||||
|
{currentSubtitle}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue