diff --git a/frontend/src/components/FloatingSpeedSelector.tsx b/frontend/src/components/FloatingSpeedSelector.tsx new file mode 100644 index 00000000..de454c6d --- /dev/null +++ b/frontend/src/components/FloatingSpeedSelector.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { + View, + Text, + TouchableOpacity, + Modal, + StyleSheet, + Pressable, +} from 'react-native'; + +const SPEEDS = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]; + +interface FloatingSpeedSelectorProps { + visible: boolean; + currentSpeed: number; + onSelect: (speed: number) => void; + onClose: () => void; +} + +const FloatingSpeedSelector: React.FC = ({ + visible, + currentSpeed, + onSelect, + onClose, +}) => { + return ( + + + + Playback Speed + {SPEEDS.map(speed => ( + { + onSelect(speed); + onClose(); + }}> + + {Number.isInteger(speed) ? `${speed}x` : `${speed}x`} + + + ))} + + + + ); +}; + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.5)', + justifyContent: 'center', + alignItems: 'center', + }, + menu: { + backgroundColor: '#1E293B', + borderRadius: 16, + paddingVertical: 8, + paddingHorizontal: 4, + minWidth: 180, + elevation: 8, + shadowColor: '#000', + shadowOffset: {width: 0, height: 4}, + shadowOpacity: 0.3, + shadowRadius: 8, + }, + menuTitle: { + color: '#94A3B8', + fontSize: 12, + fontWeight: '600', + textAlign: 'center', + paddingVertical: 8, + letterSpacing: 1, + textTransform: 'uppercase', + }, + speedItem: { + paddingVertical: 12, + paddingHorizontal: 24, + borderRadius: 10, + marginHorizontal: 8, + marginVertical: 2, + }, + selectedItem: { + backgroundColor: '#3B82F6', + }, + speedText: { + color: '#F1F5F9', + fontSize: 16, + fontWeight: '600', + textAlign: 'center', + }, + selectedText: { + color: '#ffffff', + fontWeight: '800', + }, +}); + +export default FloatingSpeedSelector; diff --git a/frontend/src/screens/PodcastPlayer.tsx b/frontend/src/screens/PodcastPlayer.tsx index 93eeed24..6dce8b25 100644 --- a/frontend/src/screens/PodcastPlayer.tsx +++ b/frontend/src/screens/PodcastPlayer.tsx @@ -16,13 +16,15 @@ import {AntDesign, Ionicons} from '@expo/vector-icons'; import AudioWaveform from '../components/AudioWaveform'; import {useUploadPodcast} from '../hooks/useUploadPodcast'; import Loader from '../components/Loader'; +import FloatingSpeedSelector from '../components/FloatingSpeedSelector'; -const PLAYBACK_SPEEDS = [1, 1.25, 1.5, 2]; +const PLAYBACK_SPEEDS = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]; const PodcastPlayer = ({navigation, route}: PodcastPlayerScreenProps) => { const {uploadImage, loading, error: imageError} = useUploadImage(); const {uploadAudio, loading: audioLoading, error} = useUploadAudio(); const [isPlaying, setIsPlaying] = useState(false); + const [speedMenuVisible, setSpeedMenuVisible] = useState(false); const [position, setPosition] = useState(0); const [speed, setSpeed] = useState(1); @@ -75,7 +77,7 @@ const PodcastPlayer = ({navigation, route}: PodcastPlayerScreenProps) => { // Handle transitions const handlePlay = async () => { - if (!player) return; + // If the track has fully finished, restart from the beginning. // Otherwise resume from the current paused position. if (duration > 0 && !isNaN(duration) && position >= duration - 0.5){ @@ -89,28 +91,29 @@ const PodcastPlayer = ({navigation, route}: PodcastPlayerScreenProps) => { const handlePause = async () => { console.log('Pause called'); - if (!player) return; + player.pause(); setUiState('paused'); setIsPlaying(false); }; - const handleCycleSpeed = () => { - if (!player) return; + const handleSpeedSelect = (newSpeed: number) => { + - const currentIndex = PLAYBACK_SPEEDS.indexOf(speed); - const nextSpeed = + + // replaced by floating selector PLAYBACK_SPEEDS[(currentIndex + 1) % PLAYBACK_SPEEDS.length]; - player.setPlaybackRate(nextSpeed, 'high'); - setSpeed(nextSpeed); + if (!player) return; + player.setPlaybackRate(newSpeed, 'high'); + setSpeed(newSpeed); }; const SKIP_TIME = 5; // seconds const handleForward = async () => { - if (!player) return; + let next = position + SKIP_TIME; @@ -123,7 +126,7 @@ const PodcastPlayer = ({navigation, route}: PodcastPlayerScreenProps) => { }; const handleBackward = async () => { - if (!player) return; + let next = position - SKIP_TIME; @@ -284,7 +287,7 @@ const PodcastPlayer = ({navigation, route}: PodcastPlayerScreenProps) => { }; useEffect(() => { - if (!player) return; + const interval = setInterval(() => { const status = player.currentStatus; @@ -459,7 +462,7 @@ const PodcastPlayer = ({navigation, route}: PodcastPlayerScreenProps) => { borderWidth={1} borderColor="$borderColor" pressStyle={{scale: 0.94, backgroundColor: '$backgroundPress'}} - onPress={handleCycleSpeed}> + onPress={() => setSpeedMenuVisible(true)}> {formatPlaybackSpeed(speed)} diff --git a/frontend/src/screens/article/ArticleScreen.tsx b/frontend/src/screens/article/ArticleScreen.tsx index db5bc6f8..ff3b0fc0 100644 --- a/frontend/src/screens/article/ArticleScreen.tsx +++ b/frontend/src/screens/article/ArticleScreen.tsx @@ -59,6 +59,7 @@ const ArticleScreen = ({navigation, route}: ArticleScreenProp) => { const [fontScale, setFontScale] = useState(1); const [isPlaying, setIsPlaying] = useState(false); const [isPaused, setIsPaused] = useState(false); + const [speedMenuVisible, setSpeedMenuVisible] = useState(false); const [speechRate, setSpeechRate] = useState(0.5); const [playerVisible, setPlayerVisible] = useState(false); const chunkIndexRef = useRef(0); @@ -524,11 +525,11 @@ const ArticleScreen = ({navigation, route}: ArticleScreenProp) => { 1.5: '1.5x', }; - const handleSpeedChange = () => { - const currentIndex = SPEED_OPTIONS.indexOf(speechRate); - const nextRate = SPEED_OPTIONS[(currentIndex + 1) % SPEED_OPTIONS.length]; - setSpeechRate(nextRate); - Tts.setDefaultRate(nextRate); + const handleSpeedChange = (newRate: number) => { + + + setSpeechRate(newRate); + Tts.setDefaultRate(newRate); // Restart current position with new speed if currently playing if (isPlaying && !isPaused) { Tts.removeAllListeners('tts-finish'); @@ -1095,13 +1096,19 @@ const ArticleScreen = ({navigation, route}: ArticleScreenProp) => { {/* Floating TTS Media Player */} - {playerVisible && ( + {playerVisible {playerVisible && ({playerVisible && ( ( + { handleSpeedChange(rate); setSpeedMenuVisible(false); }} + onClose={() => setSpeedMenuVisible(false)} + /> {/* Speed button */} + onPress={() => setSpeedMenuVisible(true)}> {SPEED_LABELS[speechRate]}