mattermost-mobile/app/screens/gallery/renderers/video/video_controls/icons/seek.tsx
2025-07-22 14:00:57 +08:00

147 lines
4.8 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useImperativeHandle, forwardRef} from 'react';
import {StyleSheet, View} from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
Easing,
} from 'react-native-reanimated';
import Svg, {G, Path, Text as SvgText} from 'react-native-svg';
type Props = {
size?: number;
color?: string;
seekSeconds: 0 | 10 | 30;
type: 'fastForward' | 'rewind';
}
export interface SeekIconRef {
triggerSpin: () => void;
}
const styles = StyleSheet.create({
absolute: {
position: 'absolute',
},
});
const SeekIcon = forwardRef<SeekIconRef, Props>(({
size = 38,
color = 'white',
seekSeconds,
type,
}, ref) => {
const rotation = useSharedValue(0);
useImperativeHandle(ref, () => ({
triggerSpin: () => {
rotation.value = 0; // Reset immediately
// Spin in the correct direction based on type
const targetRotation = type === 'fastForward' ? 360 : -360;
rotation.value = withTiming(targetRotation, {
duration: 600,
easing: Easing.out(Easing.cubic),
// eslint-disable-next-line max-nested-callbacks
}, () => {
rotation.value = 0; // Reset after animation
});
},
}));
if (!seekSeconds) {
return null;
}
const strokeWidth = size * 0.09;
const center = size / 2;
const radius = center - (strokeWidth / 2);
const gapDegrees = 35;
// Calculate start and end angles based on type
const [startAngle, endAngle, baseRotation] = type === 'fastForward' ? [
((360 - (gapDegrees / 2)) * (Math.PI / 180)),
((gapDegrees / 2) * (Math.PI / 180)),
45,
] : [
((gapDegrees / 2) * (Math.PI / 180)),
((360 - (gapDegrees / 2)) * (Math.PI / 180)),
-45,
];
const startX = center + (radius * Math.cos(startAngle - (Math.PI / 2)));
const startY = center + (radius * Math.sin(startAngle - (Math.PI / 2)));
const endX = center + (radius * Math.cos(endAngle - (Math.PI / 2)));
const endY = center + (radius * Math.sin(endAngle - (Math.PI / 2)));
const tangentDegrees = (startAngle * 180) / Math.PI;
const arrowLength = size * 0.25;
const arcPath = type === 'fastForward'? `M ${startX} ${startY} A ${radius} ${radius} 0 1 0 ${endX} ${endY}`: `M ${endX} ${endY} A ${radius} ${radius} 0 1 0 ${startX} ${startY}`;
const arrowRotation = type === 'fastForward' ? tangentDegrees : tangentDegrees + 180;
const arrowPointX = startX;
const arrowPointY = startY;
// Animated style for the spinning arc container only
const spinStyle = useAnimatedStyle(() => ({
transform: [{rotate: `${rotation.value}deg`}],
}));
return (
<View style={{width: size, height: size}}>
{/* Spinning arc and arrow */}
<Animated.View style={[styles.absolute, spinStyle]}>
<Svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
>
<G transform={`rotate(${baseRotation}, ${center}, ${center})`}>
<Path
d={arcPath}
stroke={color}
strokeWidth={strokeWidth}
fill='none'
strokeLinecap='round'
/>
<G transform={`rotate(${arrowRotation}, ${arrowPointX}, ${arrowPointY})`}>
<Path
d={`M ${arrowPointX + (arrowLength - 5)} ${arrowPointY} l -${arrowLength} -${arrowLength / 2} l 0 ${arrowLength} Z`}
strokeWidth={strokeWidth * 3}
fill={color}
/>
</G>
</G>
</Svg>
</Animated.View>
{/* Static text - no animation at all */}
<View style={styles.absolute}>
<Svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
>
<SvgText
x={center}
y={center + (size * 0.03)}
fontSize={size * 0.45}
fontWeight='bold'
fill={color}
textAnchor='middle'
alignmentBaseline='middle'
>
{seekSeconds}
</SvgText>
</Svg>
</View>
</View>
);
});
SeekIcon.displayName = 'SeekIcon';
export default SeekIcon;