Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions frontend/src/components/FloatingSpeedSelector.tsx
Original file line number Diff line number Diff line change
@@ -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<FloatingSpeedSelectorProps> = ({
visible,
currentSpeed,
onSelect,
onClose,
}) => {
return (
<Modal
transparent
visible={visible}
animationType="fade"
onRequestClose={onClose}>
<Pressable style={styles.overlay} onPress={onClose}>
<View style={styles.menu}>
<Text style={styles.menuTitle}>Playback Speed</Text>
{SPEEDS.map(speed => (
<TouchableOpacity
key={speed}
style={[
styles.speedItem,
currentSpeed === speed && styles.selectedItem,
]}
onPress={() => {
onSelect(speed);
onClose();
}}>
<Text
style={[
styles.speedText,
currentSpeed === speed && styles.selectedText,
]}>
{Number.isInteger(speed) ? `${speed}x` : `${speed}x`}
</Text>
</TouchableOpacity>
))}
</View>
</Pressable>
</Modal>
);
};

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;
29 changes: 16 additions & 13 deletions frontend/src/screens/PodcastPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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){
Expand All @@ -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;

Expand All @@ -123,7 +126,7 @@ const PodcastPlayer = ({navigation, route}: PodcastPlayerScreenProps) => {
};

const handleBackward = async () => {
if (!player) return;


let next = position - SKIP_TIME;

Expand Down Expand Up @@ -284,7 +287,7 @@ const PodcastPlayer = ({navigation, route}: PodcastPlayerScreenProps) => {
};

useEffect(() => {
if (!player) return;


const interval = setInterval(() => {
const status = player.currentStatus;
Expand Down Expand Up @@ -459,7 +462,7 @@ const PodcastPlayer = ({navigation, route}: PodcastPlayerScreenProps) => {
borderWidth={1}
borderColor="$borderColor"
pressStyle={{scale: 0.94, backgroundColor: '$backgroundPress'}}
onPress={handleCycleSpeed}>
onPress={() => setSpeedMenuVisible(true)}>
<Text color="$color" fontSize={14} fontWeight="800">
{formatPlaybackSpeed(speed)}
</Text>
Expand Down
21 changes: 14 additions & 7 deletions frontend/src/screens/article/ArticleScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -1095,13 +1096,19 @@ const ArticleScreen = ({navigation, route}: ArticleScreenProp) => {
</View>
</View>
{/* Floating TTS Media Player */}
{playerVisible && (
{playerVisible {playerVisible && ({playerVisible && ( (
<FloatingSpeedSelector
visible={speedMenuVisible}
currentSpeed={speechRate}
onSelect={(rate) => { handleSpeedChange(rate); setSpeedMenuVisible(false); }}
onClose={() => setSpeedMenuVisible(false)}
/>
<View style={styles.ttsPlayerContainer}>
<View style={styles.ttsPlayerInner}>
{/* Speed button */}
<TouchableOpacity
style={styles.ttsSpeedButton}
onPress={handleSpeedChange}>
onPress={() => setSpeedMenuVisible(true)}>
<Text style={styles.ttsSpeedText}>
{SPEED_LABELS[speechRate]}
</Text>
Expand Down