import React, { useState, useEffect, useCallback, useRef } from 'react'; import { View, StyleSheet, ActivityIndicator, FlatList, Pressable, Dimensions } from 'react-native'; import { ThemedView } from '@/components/ThemedView'; import { ThemedText } from '@/components/ThemedText'; import { api } from '@/services/api'; import { SearchResult } from '@/services/api'; import { PlayRecord } from '@/services/storage'; export type RowItem = (SearchResult | PlayRecord) & { id: string; source: string; title: string; poster: string; progress?: number; lastPlayed?: number; episodeIndex?: number; sourceName?: string; totalEpisodes?: number; year?: string; rate?: string; }; import VideoCard from '@/components/VideoCard.tv'; import { PlayRecordManager } from '@/services/storage'; import { useFocusEffect, useRouter } from 'expo-router'; import { useColorScheme } from 'react-native'; import { Search, Settings } from 'lucide-react-native'; import { SettingsModal } from '@/components/SettingsModal'; // --- 类别定义 --- interface Category { title: string; type?: 'movie' | 'tv' | 'record'; tag?: string; } const initialCategories: Category[] = [ { title: '最近播放', type: 'record' }, { title: '热门剧集', type: 'tv', tag: '热门' }, { title: '综艺', type: 'tv', tag: '综艺' }, { title: '热门电影', type: 'movie', tag: '热门' }, { title: '豆瓣 Top250', type: 'movie', tag: 'top250' }, { title: '儿童', type: 'movie', tag: '少儿' }, { title: '美剧', type: 'tv', tag: '美剧' }, { title: '韩剧', type: 'tv', tag: '韩剧' }, { title: '日剧', type: 'tv', tag: '日剧' }, { title: '日漫', type: 'tv', tag: '日本动画' }, ]; const NUM_COLUMNS = 5; const { width } = Dimensions.get('window'); const ITEM_WIDTH = width / NUM_COLUMNS - 24; export default function HomeScreen() { const router = useRouter(); const colorScheme = useColorScheme(); const [categories, setCategories] = useState(initialCategories); const [selectedCategory, setSelectedCategory] = useState(categories[0]); const [contentData, setContentData] = useState([]); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [error, setError] = useState(null); const [isSettingsVisible, setSettingsVisible] = useState(false); const [pageStart, setPageStart] = useState(0); const [hasMore, setHasMore] = useState(true); const flatListRef = useRef(null); // --- 数据获取逻辑 --- const fetchPlayRecords = async () => { const records = await PlayRecordManager.getAll(); return Object.entries(records) .map(([key, record]) => { const [source, id] = key.split('+'); return { id, source, title: record.title, poster: record.cover, progress: record.play_time / record.total_time, lastPlayed: record.save_time, episodeIndex: record.index, sourceName: record.source_name, totalEpisodes: record.total_episodes, } as RowItem; }) .filter(record => record.progress !== undefined && record.progress > 0 && record.progress < 1) .sort((a, b) => (b.lastPlayed || 0) - (a.lastPlayed || 0)); }; const fetchData = async (category: Category, start: number, preloadedRecords?: RowItem[]) => { if (category.type === 'record') { const records = preloadedRecords ?? (await fetchPlayRecords()); if (records.length === 0 && categories.some(c => c.type === 'record')) { // 如果没有播放记录,则移除"最近播放"分类并选择第一个真实分类 const newCategories = categories.filter(c => c.type !== 'record'); setCategories(newCategories); if (newCategories.length > 0) { handleCategorySelect(newCategories[0]); } } else { setContentData(records); setHasMore(false); } setLoading(false); return; } if (!category.type || !category.tag) return; setLoadingMore(start > 0); setError(null); try { const result = await api.getDoubanData(category.type, category.tag, 20, start); if (result.list.length === 0) { setHasMore(false); } else { const newItems = result.list.map(item => ({ ...item, id: item.title, // 临时ID source: 'douban', })) as RowItem[]; setContentData(prev => (start === 0 ? newItems : [...prev, ...newItems])); setPageStart(prev => prev + result.list.length); setHasMore(true); } } catch (err: any) { if (err.message === 'API_URL_NOT_SET') { setError('请点击右上角设置按钮,配置您的 API 地址'); } else { setError('加载失败,请重试'); } } finally { setLoading(false); setLoadingMore(false); } }; // --- Effects --- useFocusEffect( useCallback(() => { const manageRecordCategory = async () => { const records = await fetchPlayRecords(); const hasRecords = records.length > 0; setCategories(currentCategories => { const recordCategoryExists = currentCategories.some(c => c.type === 'record'); if (hasRecords && !recordCategoryExists) { // Add 'Recent Plays' if records exist and the tab doesn't return [initialCategories[0], ...currentCategories]; } return currentCategories; }); // If 'Recent Plays' is selected, always refresh its data. // This will also handle removing the tab if records have disappeared. if (selectedCategory.type === 'record') { loadInitialData(records); } }; manageRecordCategory(); }, [selectedCategory]) ); useEffect(() => { loadInitialData(); }, [selectedCategory]); const loadInitialData = (records?: RowItem[]) => { setLoading(true); setContentData([]); setPageStart(0); setHasMore(true); flatListRef.current?.scrollToOffset({ animated: false, offset: 0 }); fetchData(selectedCategory, 0, records); }; const loadMoreData = () => { if (loading || loadingMore || !hasMore || selectedCategory.type === 'record') return; fetchData(selectedCategory, pageStart); }; const handleCategorySelect = (category: Category) => { setSelectedCategory(category); }; // --- 渲染组件 --- const renderCategory = ({ item }: { item: Category }) => { const isSelected = selectedCategory.title === item.title; return ( [ styles.categoryButton, isSelected && styles.categoryButtonSelected, focused && styles.categoryButtonFocused, ]} onPress={() => handleCategorySelect(item)} > {item.title} ); }; const renderContentItem = ({ item }: { item: RowItem }) => ( ); const renderFooter = () => { if (!loadingMore) return null; return ; }; return ( {/* 顶部导航 */} 首页 [styles.searchButton, focused && styles.searchButtonFocused]} onPress={() => router.push({ pathname: '/search' })} > [styles.searchButton, focused && styles.searchButtonFocused]} onPress={() => setSettingsVisible(true)} > {/* 分类选择器 */} item.title} horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.categoryListContent} /> {/* 内容网格 */} {loading ? ( ) : error ? ( {error} ) : ( `${item.source}-${item.id}-${index}`} numColumns={NUM_COLUMNS} contentContainerStyle={styles.listContent} onEndReached={loadMoreData} onEndReachedThreshold={0.5} ListFooterComponent={renderFooter} ListEmptyComponent={ 该分类下暂无内容 } /> )} setSettingsVisible(false)} onSave={() => { setSettingsVisible(false); loadInitialData(); }} /> ); } const styles = StyleSheet.create({ container: { flex: 1, paddingTop: 40, }, centerContainer: { flex: 1, paddingTop: 20, justifyContent: 'center', alignItems: 'center', }, // Header headerContainer: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 24, marginBottom: 10, }, headerTitle: { fontSize: 32, fontWeight: 'bold', paddingTop: 16, }, rightHeaderButtons: { flexDirection: 'row', alignItems: 'center', }, searchButton: { padding: 10, borderRadius: 30, marginLeft: 10, }, searchButtonFocused: { backgroundColor: '#007AFF', transform: [{ scale: 1.1 }], }, // Category Selector categoryContainer: { paddingBottom: 10, }, categoryListContent: { paddingHorizontal: 16, }, categoryButton: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 8, marginHorizontal: 5, }, categoryButtonSelected: { backgroundColor: '#007AFF', // A bright blue for selected state }, categoryButtonFocused: { backgroundColor: '#0056b3', // A darker blue for focused state elevation: 5, }, categoryText: { fontSize: 16, fontWeight: '500', }, categoryTextSelected: { color: '#FFFFFF', }, // Content Grid listContent: { paddingHorizontal: 16, paddingBottom: 20, }, itemContainer: { margin: 8, width: ITEM_WIDTH, alignItems: 'center', }, });