242 lines
7.1 KiB
TypeScript
242 lines
7.1 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { View, Text, FlatList, StyleSheet, TouchableOpacity, Image, ActivityIndicator } from 'react-native';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { Audio } from 'expo-av';
|
|
import { Theme } from '../../constants/Theme';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
|
|
const API_URL = process.env.EXPO_PUBLIC_API_URL || 'http://216.158.230.94:8001/api/v1';
|
|
|
|
interface PodcastEpisode {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
audio_url: string;
|
|
published_at: string;
|
|
duration: string;
|
|
image_url: string;
|
|
}
|
|
|
|
interface PodcastFeed {
|
|
title: string;
|
|
description: string;
|
|
image_url: string;
|
|
episodes: PodcastEpisode[];
|
|
}
|
|
|
|
const fetchPodcasts = async (): Promise<PodcastFeed> => {
|
|
const response = await fetch(`${API_URL}/podcast/`);
|
|
if (!response.ok) {
|
|
throw new Error('Network response was not ok');
|
|
}
|
|
return response.json();
|
|
};
|
|
|
|
export default function PodcastsScreen() {
|
|
const { data, isLoading, error } = useQuery({
|
|
queryKey: ['podcasts'],
|
|
queryFn: fetchPodcasts,
|
|
});
|
|
|
|
const [sound, setSound] = useState<Audio.Sound | null>(null);
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
const [currentEpisodeId, setCurrentEpisodeId] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (sound) {
|
|
sound.unloadAsync();
|
|
}
|
|
};
|
|
}, [sound]);
|
|
|
|
const playPodcast = async (audioUrl: string, episodeId: string) => {
|
|
try {
|
|
if (sound) {
|
|
await sound.unloadAsync();
|
|
}
|
|
|
|
const { sound: newSound } = await Audio.Sound.createAsync(
|
|
{ uri: audioUrl },
|
|
{ shouldPlay: true }
|
|
);
|
|
|
|
setSound(newSound);
|
|
setCurrentEpisodeId(episodeId);
|
|
setIsPlaying(true);
|
|
|
|
newSound.setOnPlaybackStatusUpdate((status) => {
|
|
if (status.isLoaded) {
|
|
setIsPlaying(status.isPlaying);
|
|
if (status.didJustFinish) {
|
|
setIsPlaying(false);
|
|
setCurrentEpisodeId(null);
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Error playing sound:', error);
|
|
}
|
|
};
|
|
|
|
const pausePodcast = async () => {
|
|
if (sound) {
|
|
await sound.pauseAsync();
|
|
setIsPlaying(false);
|
|
}
|
|
};
|
|
|
|
const resumePodcast = async () => {
|
|
if (sound) {
|
|
await sound.playAsync();
|
|
setIsPlaying(true);
|
|
}
|
|
};
|
|
|
|
const handlePlayPause = (audioUrl: string, episodeId: string) => {
|
|
if (currentEpisodeId === episodeId) {
|
|
if (isPlaying) {
|
|
pausePodcast();
|
|
} else {
|
|
resumePodcast();
|
|
}
|
|
} else {
|
|
playPodcast(audioUrl, episodeId);
|
|
}
|
|
};
|
|
|
|
const renderItem = ({ item }: { item: PodcastEpisode }) => (
|
|
<View style={styles.card}>
|
|
<View style={styles.cardContent}>
|
|
<Image source={{ uri: item.image_url || data?.image_url }} style={styles.thumbnail} />
|
|
<View style={styles.textContainer}>
|
|
<Text style={styles.episodeTitle} numberOfLines={2}>{item.title}</Text>
|
|
<Text style={styles.episodeDate}>{new Date(item.published_at).toLocaleDateString()}</Text>
|
|
<Text style={styles.episodeDuration}>{item.duration}</Text>
|
|
</View>
|
|
<TouchableOpacity
|
|
style={styles.playButton}
|
|
onPress={() => handlePlayPause(item.audio_url, item.id)}
|
|
>
|
|
<Ionicons
|
|
name={currentEpisodeId === item.id && isPlaying ? "pause-circle" : "play-circle"}
|
|
size={48}
|
|
color={Theme.colors.primary}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<View style={styles.center}>
|
|
<ActivityIndicator size="large" color={Theme.colors.primary} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<View style={styles.center}>
|
|
<Text style={styles.errorText}>Failed to load podcasts</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<FlatList
|
|
data={data?.episodes}
|
|
renderItem={renderItem}
|
|
keyExtractor={(item) => item.id}
|
|
contentContainerStyle={styles.listContent}
|
|
ListHeaderComponent={
|
|
<View style={styles.header}>
|
|
<Text style={styles.headerTitle}>Podcasts</Text>
|
|
<Text style={styles.headerSubtitle}>Listen to the latest episodes</Text>
|
|
</View>
|
|
}
|
|
/>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: Theme.colors.background,
|
|
},
|
|
center: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
listContent: {
|
|
padding: Theme.spacing.m,
|
|
},
|
|
header: {
|
|
marginBottom: Theme.spacing.l,
|
|
},
|
|
headerTitle: {
|
|
fontSize: Theme.typography.fontSize.xxl,
|
|
fontFamily: Theme.typography.fontFamily.heading,
|
|
color: Theme.colors.text,
|
|
fontWeight: 'bold',
|
|
},
|
|
headerSubtitle: {
|
|
fontSize: Theme.typography.fontSize.m,
|
|
fontFamily: Theme.typography.fontFamily.body,
|
|
color: Theme.colors.text,
|
|
opacity: 0.7,
|
|
},
|
|
card: {
|
|
backgroundColor: Theme.colors.surface,
|
|
borderRadius: Theme.borderRadius.m,
|
|
padding: Theme.spacing.m,
|
|
marginBottom: Theme.spacing.m,
|
|
...Theme.shadows.card,
|
|
},
|
|
cardContent: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
},
|
|
thumbnail: {
|
|
width: 60,
|
|
height: 60,
|
|
borderRadius: Theme.borderRadius.s,
|
|
marginRight: Theme.spacing.m,
|
|
backgroundColor: Theme.colors.secondary,
|
|
},
|
|
textContainer: {
|
|
flex: 1,
|
|
marginRight: Theme.spacing.s,
|
|
},
|
|
episodeTitle: {
|
|
fontSize: Theme.typography.fontSize.m,
|
|
fontFamily: Theme.typography.fontFamily.heading,
|
|
color: Theme.colors.text,
|
|
fontWeight: '600',
|
|
marginBottom: 4,
|
|
},
|
|
episodeDate: {
|
|
fontSize: Theme.typography.fontSize.xs,
|
|
fontFamily: Theme.typography.fontFamily.body,
|
|
color: Theme.colors.text,
|
|
opacity: 0.6,
|
|
},
|
|
episodeDuration: {
|
|
fontSize: Theme.typography.fontSize.xs,
|
|
fontFamily: Theme.typography.fontFamily.body,
|
|
color: Theme.colors.text,
|
|
opacity: 0.6,
|
|
},
|
|
playButton: {
|
|
padding: 4,
|
|
},
|
|
errorText: {
|
|
color: Theme.colors.error || 'red',
|
|
fontSize: Theme.typography.fontSize.m,
|
|
},
|
|
});
|