Compare commits
7 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3164016752 | ||
|
|
e305dee777 | ||
|
|
23acda3167 | ||
|
|
6e975ffe26 | ||
|
|
ca2e95e6f4 | ||
|
|
f895428e3d | ||
|
|
698456c205 |
4 changed files with 410 additions and 21 deletions
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
|
@ -1,2 +1,3 @@
|
|||
{
|
||||
"java.compile.nullAnalysis.mode": "automatic"
|
||||
}
|
||||
|
|
@ -726,7 +726,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
|
||||
DefaultRenderersFactory renderersFactory =
|
||||
new DefaultRenderersFactory(getContext())
|
||||
.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER)
|
||||
.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF)
|
||||
.setEnableDecoderFallback(true)
|
||||
.forceEnableMediaCodecAsynchronousQueueing();
|
||||
|
||||
|
|
|
|||
|
|
@ -90,7 +90,9 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|||
onAirPlayPress,
|
||||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
const deviceWidth = Dimensions.get('window').width;
|
||||
|
||||
/* Responsive Spacing - Merged with tablet support */
|
||||
const screenWidth = Dimensions.get('window').width;
|
||||
const BREAKPOINTS = { phone: 0, tablet: 768, largeTablet: 1024, tv: 1440 } as const;
|
||||
const getDeviceType = (w: number) => {
|
||||
if (w >= BREAKPOINTS.tv) return 'tv';
|
||||
|
|
@ -98,14 +100,161 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|||
if (w >= BREAKPOINTS.tablet) return 'tablet';
|
||||
return 'phone';
|
||||
};
|
||||
const deviceType = getDeviceType(deviceWidth);
|
||||
const deviceType = getDeviceType(screenWidth);
|
||||
const isTablet = deviceType === 'tablet';
|
||||
const isLargeTablet = deviceType === 'largeTablet';
|
||||
const isTV = deviceType === 'tv';
|
||||
|
||||
// Responsive button sizing - combines percentage-based with breakpoint scaling
|
||||
const baseButtonSpacing = screenWidth * 0.15;
|
||||
const buttonSpacing = isTV ? baseButtonSpacing * 1.2 : isLargeTablet ? baseButtonSpacing * 1.1 : isTablet ? baseButtonSpacing : baseButtonSpacing * 0.9;
|
||||
|
||||
const basePlayButtonSize = screenWidth * 0.12;
|
||||
const playButtonSize = isTV ? basePlayButtonSize * 1.2 : isLargeTablet ? basePlayButtonSize * 1.1 : isTablet ? basePlayButtonSize : basePlayButtonSize * 0.9;
|
||||
const playIconSize = playButtonSize * 0.6;
|
||||
|
||||
const baseSeekButtonSize = screenWidth * 0.11;
|
||||
const seekButtonSize = isTV ? baseSeekButtonSize * 1.2 : isLargeTablet ? baseSeekButtonSize * 1.1 : isTablet ? baseSeekButtonSize : baseSeekButtonSize * 0.9;
|
||||
const seekIconSize = seekButtonSize * 0.75;
|
||||
const seekNumberSize = seekButtonSize * 0.25;
|
||||
const arcBorderWidth = seekButtonSize * 0.05;
|
||||
|
||||
// Icon sizes for other controls
|
||||
const closeIconSize = isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 24;
|
||||
const skipIconSize = isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 24;
|
||||
const playIconSize = isTV ? 56 : isLargeTablet ? 48 : isTablet ? 44 : 40;
|
||||
|
||||
/* Animations - State & Refs */
|
||||
const [showBackwardSign, setShowBackwardSign] = React.useState(false);
|
||||
const [showForwardSign, setShowForwardSign] = React.useState(false);
|
||||
|
||||
/* Separate Animations for Each Button */
|
||||
const backwardPressAnim = React.useRef(new Animated.Value(0)).current;
|
||||
const backwardSlideAnim = React.useRef(new Animated.Value(0)).current;
|
||||
const backwardScaleAnim = React.useRef(new Animated.Value(1)).current;
|
||||
const backwardArcOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
const backwardArcRotation = React.useRef(new Animated.Value(0)).current;
|
||||
|
||||
const forwardPressAnim = React.useRef(new Animated.Value(0)).current;
|
||||
const forwardSlideAnim = React.useRef(new Animated.Value(0)).current;
|
||||
const forwardScaleAnim = React.useRef(new Animated.Value(1)).current;
|
||||
const forwardArcOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
const forwardArcRotation = React.useRef(new Animated.Value(0)).current;
|
||||
|
||||
const playPressAnim = React.useRef(new Animated.Value(0)).current;
|
||||
const playIconScale = React.useRef(new Animated.Value(1)).current;
|
||||
const playIconOpacity = React.useRef(new Animated.Value(1)).current;
|
||||
|
||||
/* Handle Seek with Animation */
|
||||
const handleSeekWithAnimation = (seconds: number) => {
|
||||
const isForward = seconds > 0;
|
||||
|
||||
if (isForward) {
|
||||
setShowForwardSign(true);
|
||||
} else {
|
||||
setShowBackwardSign(true);
|
||||
}
|
||||
|
||||
const pressAnim = isForward ? forwardPressAnim : backwardPressAnim;
|
||||
const slideAnim = isForward ? forwardSlideAnim : backwardSlideAnim;
|
||||
const scaleAnim = isForward ? forwardScaleAnim : backwardScaleAnim;
|
||||
const arcOpacity = isForward ? forwardArcOpacity : backwardArcOpacity;
|
||||
const arcRotation = isForward ? forwardArcRotation : backwardArcRotation;
|
||||
|
||||
Animated.parallel([
|
||||
// Button press effect (circle flash)
|
||||
Animated.sequence([
|
||||
Animated.timing(pressAnim, {
|
||||
toValue: 1,
|
||||
duration: 100,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(pressAnim, {
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]),
|
||||
// Number slide out
|
||||
Animated.sequence([
|
||||
Animated.timing(slideAnim, {
|
||||
toValue: isForward ? (seekButtonSize * 0.75) : -(seekButtonSize * 0.75),
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(slideAnim, {
|
||||
toValue: 0,
|
||||
duration: 120,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]),
|
||||
// Button scale pulse
|
||||
Animated.sequence([
|
||||
Animated.timing(scaleAnim, {
|
||||
toValue: 1.15,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(scaleAnim, {
|
||||
toValue: 1,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]),
|
||||
// Arc sweep animation
|
||||
Animated.parallel([
|
||||
Animated.timing(arcOpacity, {
|
||||
toValue: 1,
|
||||
duration: 50,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(arcRotation, {
|
||||
toValue: 1,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]),
|
||||
]).start(() => {
|
||||
if (isForward) {
|
||||
setShowForwardSign(false);
|
||||
} else {
|
||||
setShowBackwardSign(false);
|
||||
}
|
||||
arcOpacity.setValue(0);
|
||||
arcRotation.setValue(0);
|
||||
});
|
||||
|
||||
skip(seconds);
|
||||
};
|
||||
|
||||
/* Handle Play/Pause with Animation */
|
||||
const handlePlayPauseWithAnimation = () => {
|
||||
Animated.sequence([
|
||||
Animated.timing(playPressAnim, {
|
||||
toValue: 1,
|
||||
duration: 100,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(playPressAnim, {
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
|
||||
Animated.sequence([
|
||||
Animated.timing(playIconScale, {
|
||||
toValue: 0.85,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(playIconScale, {
|
||||
toValue: 1,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
|
||||
togglePlayback();
|
||||
};
|
||||
return (
|
||||
<Animated.View
|
||||
style={[StyleSheet.absoluteFill, { opacity: fadeAnim, zIndex: 20 }]}
|
||||
|
|
@ -170,21 +319,185 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|||
</View>
|
||||
</LinearGradient>
|
||||
|
||||
{/* Center Controls (Play/Pause, Skip) */}
|
||||
<View style={styles.controls}>
|
||||
<TouchableOpacity onPress={() => skip(-10)} style={styles.skipButton}>
|
||||
<Ionicons name="play-back" size={skipIconSize} color="white" />
|
||||
<Text style={styles.skipText}>10</Text>
|
||||
{/* Center Controls - CloudStream Style with Responsive Sizing */}
|
||||
<View style={[styles.controls, {
|
||||
transform: [{ translateY: -(playButtonSize / 2) }]
|
||||
}]}>
|
||||
|
||||
{/* Backward Seek Button (-10s) */}
|
||||
<TouchableOpacity
|
||||
onPress={() => handleSeekWithAnimation(-10)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Animated.View style={[
|
||||
styles.seekButtonContainer,
|
||||
{
|
||||
width: seekButtonSize,
|
||||
height: seekButtonSize,
|
||||
transform: [{ scale: backwardScaleAnim }]
|
||||
}
|
||||
]}>
|
||||
<Ionicons
|
||||
name="reload-outline"
|
||||
size={seekIconSize}
|
||||
color="white"
|
||||
style={{ transform: [{ scaleX: -1 }] }}
|
||||
/>
|
||||
<Animated.View style={[
|
||||
styles.buttonCircle,
|
||||
{
|
||||
opacity: backwardPressAnim,
|
||||
width: seekButtonSize * 0.6, // 60% of seek button
|
||||
height: seekButtonSize * 0.6,
|
||||
borderRadius: (seekButtonSize * 0.6) / 2,
|
||||
}
|
||||
]} />
|
||||
<View style={[styles.seekNumberContainer, {
|
||||
width: seekButtonSize,
|
||||
height: seekButtonSize,
|
||||
}]}>
|
||||
<Animated.Text style={[
|
||||
styles.seekNumber,
|
||||
{
|
||||
fontSize: seekNumberSize,
|
||||
marginLeft: 7, // Opposite offset for flipped icon
|
||||
transform: [{ translateX: backwardSlideAnim }]
|
||||
}
|
||||
]}>
|
||||
{showBackwardSign ? '-10' : '10'}
|
||||
</Animated.Text>
|
||||
</View>
|
||||
</Animated.View>
|
||||
<Animated.View style={[
|
||||
styles.arcContainer,
|
||||
{
|
||||
width: seekButtonSize,
|
||||
height: seekButtonSize,
|
||||
opacity: backwardArcOpacity,
|
||||
transform: [{
|
||||
rotate: backwardArcRotation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['90deg', '-90deg']
|
||||
})
|
||||
}]
|
||||
}
|
||||
]}>
|
||||
<View style={[
|
||||
styles.arcLeft,
|
||||
{
|
||||
width: seekButtonSize,
|
||||
height: seekButtonSize,
|
||||
borderRadius: seekButtonSize / 2,
|
||||
borderWidth: arcBorderWidth,
|
||||
}
|
||||
]} />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={togglePlayback} style={styles.playButton}>
|
||||
<Ionicons name={paused ? "play" : "pause"} size={playIconSize} color="white" />
|
||||
|
||||
{/* Play/Pause Button */}
|
||||
<TouchableOpacity
|
||||
onPress={handlePlayPauseWithAnimation}
|
||||
activeOpacity={0.7}
|
||||
style={{ marginHorizontal: buttonSpacing }}
|
||||
>
|
||||
<View style={[styles.playButtonCircle, { width: playButtonSize, height: playButtonSize }]}>
|
||||
<Animated.View style={[
|
||||
styles.playPressCircle,
|
||||
{
|
||||
opacity: playPressAnim,
|
||||
width: playButtonSize * 0.85, // ← 85% of button size
|
||||
height: playButtonSize * 0.85,
|
||||
borderRadius: (playButtonSize * 0.85) / 2, // ← Half of width/height for circle
|
||||
}
|
||||
]} />
|
||||
<Animated.View style={{
|
||||
transform: [{ scale: playIconScale }],
|
||||
opacity: playIconOpacity
|
||||
}}>
|
||||
<Ionicons
|
||||
name={paused ? "play" : "pause"}
|
||||
size={playIconSize}
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => skip(10)} style={styles.skipButton}>
|
||||
<Ionicons name="play-forward" size={skipIconSize} color="white" />
|
||||
<Text style={styles.skipText}>10</Text>
|
||||
|
||||
{/* Forward Seek Button (+10s) */}
|
||||
<TouchableOpacity
|
||||
onPress={() => handleSeekWithAnimation(10)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Animated.View style={[
|
||||
styles.seekButtonContainer,
|
||||
{
|
||||
width: seekButtonSize,
|
||||
height: seekButtonSize,
|
||||
transform: [{ scale: forwardScaleAnim }]
|
||||
}
|
||||
]}>
|
||||
<Ionicons
|
||||
name="reload-outline"
|
||||
size={seekIconSize}
|
||||
color="white"
|
||||
/>
|
||||
<Animated.View style={[
|
||||
styles.buttonCircle,
|
||||
{
|
||||
opacity: forwardPressAnim,
|
||||
width: seekButtonSize * 0.6, // 60% of seek button
|
||||
height: seekButtonSize * 0.6,
|
||||
borderRadius: (seekButtonSize * 0.6) / 2,
|
||||
}
|
||||
]} />
|
||||
<View style={[styles.seekNumberContainer, {
|
||||
width: seekButtonSize,
|
||||
height: seekButtonSize,
|
||||
}]}>
|
||||
<Animated.Text style={[
|
||||
styles.seekNumber,
|
||||
{
|
||||
fontSize: seekNumberSize,
|
||||
transform: [{ translateX: forwardSlideAnim }]
|
||||
}
|
||||
]}>
|
||||
{showForwardSign ? '+10' : '10'}
|
||||
</Animated.Text>
|
||||
</View>
|
||||
<Animated.View style={[
|
||||
styles.arcContainer,
|
||||
{
|
||||
width: seekButtonSize,
|
||||
height: seekButtonSize,
|
||||
},
|
||||
{
|
||||
opacity: forwardArcOpacity,
|
||||
transform: [{
|
||||
rotate: forwardArcRotation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['-90deg', '90deg']
|
||||
})
|
||||
}]
|
||||
}
|
||||
]}>
|
||||
<View style={[
|
||||
styles.arcRight,
|
||||
{
|
||||
width: seekButtonSize,
|
||||
height: seekButtonSize,
|
||||
borderRadius: seekButtonSize / 2,
|
||||
borderWidth: arcBorderWidth,
|
||||
}
|
||||
]} />
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{/* Bottom Gradient */}
|
||||
<LinearGradient
|
||||
colors={['transparent', 'rgba(0,0,0,0.7)']}
|
||||
|
|
|
|||
|
|
@ -131,32 +131,107 @@ export const styles = StyleSheet.create({
|
|||
closeButton: {
|
||||
padding: 8,
|
||||
},
|
||||
|
||||
|
||||
/* CloudStream Style - Center Controls */
|
||||
controls: {
|
||||
position: 'absolute',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: controlsGap,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: '50%',
|
||||
transform: [{ translateY: controlsTranslateY }],
|
||||
transform: [{ translateY: -50 }],
|
||||
paddingHorizontal: 20,
|
||||
zIndex: 1000,
|
||||
},
|
||||
playButton: {
|
||||
justifyContent: 'center',
|
||||
|
||||
/* CloudStream Style - Seek Buttons */
|
||||
seekButtonContainer: {
|
||||
alignItems: 'center',
|
||||
padding: 10,
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
},
|
||||
skipButton: {
|
||||
buttonCircle: {
|
||||
position: 'absolute',
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 25,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.3)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
seekNumberContainer: {
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 50,
|
||||
height: 50,
|
||||
},
|
||||
seekNumber: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 24,
|
||||
fontWeight: '500',
|
||||
opacity: 1,
|
||||
textAlign: 'center',
|
||||
marginLeft: -7, // Adjusted for better centering with icon
|
||||
},
|
||||
|
||||
/* CloudStream Style - Play Button */
|
||||
playButton: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
playButtonCircle: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
},
|
||||
skipText: {
|
||||
color: 'white',
|
||||
fontSize: skipTextFont,
|
||||
marginTop: 2,
|
||||
},
|
||||
playIcon: {
|
||||
color: '#FFFFFF',
|
||||
opacity: 1,
|
||||
},
|
||||
|
||||
/* CloudStream Style - Arc Animations */
|
||||
arcContainer: {
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
arcLeft: {
|
||||
borderWidth: 4,
|
||||
borderColor: 'rgba(255, 255, 255, 0.9)',
|
||||
borderTopColor: 'transparent',
|
||||
borderRightColor: 'transparent',
|
||||
borderBottomColor: 'transparent',
|
||||
position: 'absolute',
|
||||
},
|
||||
arcRight: {
|
||||
borderWidth: 4,
|
||||
borderColor: 'rgba(255, 255, 255, 0.9)',
|
||||
borderTopColor: 'transparent',
|
||||
borderLeftColor: 'transparent',
|
||||
borderBottomColor: 'transparent',
|
||||
position: 'absolute',
|
||||
},
|
||||
playPressCircle: {
|
||||
position: 'absolute',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.3)',
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
bottomControls: {
|
||||
gap: 12,
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue