import React, { useEffect, useState, useRef } from 'react'; import { View, Text, StyleSheet, Image, ScrollView, ActivityIndicator } from 'react-native'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { ThemedView } from '@/components/ThemedView'; import { ThemedText } from '@/components/ThemedText'; import { api, SearchResult } from '@/services/api'; import { getResolutionFromM3U8 } from '@/services/m3u8'; import { DetailButton } from '@/components/DetailButton'; export default function DetailScreen() { const { source, q } = useLocalSearchParams(); const router = useRouter(); const [searchResults, setSearchResults] = useState<(SearchResult & { resolution?: string | null })[]>([]); const [detail, setDetail] = useState<(SearchResult & { resolution?: string | null }) | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [allSourcesLoaded, setAllSourcesLoaded] = useState(false); const controllerRef = useRef(null); useEffect(() => { if (controllerRef.current) { controllerRef.current.abort(); } controllerRef.current = new AbortController(); const signal = controllerRef.current.signal; if (typeof q === 'string') { const fetchDetailData = async () => { setLoading(true); setSearchResults([]); setDetail(null); setError(null); setAllSourcesLoaded(false); try { const resources = await api.getResources(signal); if (!resources || resources.length === 0) { setError('没有可用的播放源'); setLoading(false); return; } let foundFirstResult = false; // Prioritize source from params if available if (typeof source === 'string') { const index = resources.findIndex(r => r.key === source); if (index > 0) { resources.unshift(resources.splice(index, 1)[0]); } } for (const resource of resources) { try { const { results } = await api.searchVideo(q, resource.key, signal); if (results && results.length > 0) { const searchResult = results[0]; let resolution; try { if (searchResult.episodes && searchResult.episodes.length > 0) { resolution = await getResolutionFromM3U8(searchResult.episodes[0], signal); } } catch (e) { if ((e as Error).name !== 'AbortError') { console.error(`Failed to get resolution for ${resource.name}`, e); } } const resultWithResolution = { ...searchResult, resolution }; setSearchResults(prev => [...prev, resultWithResolution]); if (!foundFirstResult) { setDetail(resultWithResolution); foundFirstResult = true; setLoading(false); } } } catch (e) { if ((e as Error).name !== 'AbortError') { console.error(`Error searching in resource ${resource.name}:`, e); } } } if (!foundFirstResult) { setError('未找到播放源'); setLoading(false); } } catch (e) { if ((e as Error).name !== 'AbortError') { setError(e instanceof Error ? e.message : '获取资源列表失败'); setLoading(false); } } finally { setAllSourcesLoaded(true); } }; fetchDetailData(); } return () => { controllerRef.current?.abort(); }; }, [q, source]); const handlePlay = (episodeName: string, episodeIndex: number) => { if (!detail) return; controllerRef.current?.abort(); // Cancel any ongoing fetches router.push({ pathname: '/play', params: { source: detail.source, id: detail.id.toString(), episodeUrl: episodeName, // The "episode" is actually the URL episodeIndex: episodeIndex.toString(), title: detail.title, poster: detail.poster, }, }); }; if (loading) { return ( ); } if (error) { return ( {error} ); } if (!detail) { return ( 未找到详情信息 ); } return ( {detail.title} {detail.year} {detail.type_name} {detail.desc} 选择播放源 共 {searchResults.length} 个 {!allSourcesLoaded && } {searchResults.map((item, index) => ( setDetail(item)} hasTVPreferredFocus={index === 0} style={[styles.sourceButton, detail?.source === item.source && styles.sourceButtonSelected]} > {item.source_name} {item.episodes.length > 1 && ( {item.episodes.length > 99 ? '99+' : `${item.episodes.length}`} )} {item.resolution && ( {item.resolution} )} ))} 播放列表 {detail.episodes.map((episode, index) => ( handlePlay(episode, index)}> {`第 ${index + 1} 集`} ))} ); } const styles = StyleSheet.create({ container: { flex: 1 }, centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, topContainer: { flexDirection: 'row', padding: 20, }, poster: { width: 200, height: 300, borderRadius: 8, }, infoContainer: { flex: 1, marginLeft: 20, justifyContent: 'flex-start', }, title: { fontSize: 28, fontWeight: 'bold', marginBottom: 10, paddingTop: 20, }, metaContainer: { flexDirection: 'row', marginBottom: 10, }, metaText: { color: '#aaa', marginRight: 10, fontSize: 14, }, descriptionScrollView: { height: 150, // Constrain height to make it scrollable }, description: { fontSize: 14, color: '#ccc', lineHeight: 22, }, bottomContainer: { paddingHorizontal: 20, }, sourcesContainer: { marginTop: 20, }, sourcesTitleContainer: { flexDirection: 'row', alignItems: 'center', marginBottom: 10, }, sourcesTitle: { fontSize: 20, fontWeight: 'bold', }, sourceList: { flexDirection: 'row', flexWrap: 'wrap', }, sourceButton: { backgroundColor: '#333', paddingHorizontal: 15, paddingVertical: 10, borderRadius: 8, margin: 5, flexDirection: 'row', alignItems: 'center', borderWidth: 2, borderColor: 'transparent', }, sourceButtonSelected: { backgroundColor: '#007bff', }, sourceButtonText: { color: 'white', fontSize: 16, }, badge: { backgroundColor: 'red', borderRadius: 10, paddingHorizontal: 6, paddingVertical: 2, marginLeft: 8, }, badgeText: { color: 'white', fontSize: 12, fontWeight: 'bold', }, episodesContainer: { marginTop: 20, }, episodesTitle: { fontSize: 20, fontWeight: 'bold', marginBottom: 10, }, episodeList: { flexDirection: 'row', flexWrap: 'wrap', }, episodeButton: { backgroundColor: '#333', paddingHorizontal: 20, paddingVertical: 10, borderRadius: 8, margin: 5, borderWidth: 2, borderColor: 'transparent', }, episodeButtonText: { color: 'white', }, });