import React, { useState, useEffect, useCallback } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, FlatList, ActivityIndicator, SafeAreaView, StatusBar, Platform, Dimensions, RefreshControl, ScrollView, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; import FastImage from '@d11/react-native-fast-image'; import { MalApiService } from '../services/mal/MalApi'; import { MalAnimeNode, MalListStatus } from '../types/mal'; import { useTheme } from '../contexts/ThemeContext'; import { useTranslation } from 'react-i18next'; import { logger } from '../utils/logger'; import { MalEditModal } from '../components/mal/MalEditModal'; import { MalSync } from '../services/mal/MalSync'; const { width } = Dimensions.get('window'); const ITEM_WIDTH = width * 0.35; const ITEM_HEIGHT = ITEM_WIDTH * 1.5; const MalLibraryScreen: React.FC = () => { const { t } = useTranslation(); const navigation = useNavigation(); const { currentTheme } = useTheme(); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); const [groupedList, setGroupedList] = useState>({ watching: [], completed: [], on_hold: [], dropped: [], plan_to_watch: [], }); const [selectedAnime, setSelectedAnime] = useState(null); const [isEditModalVisible, setIsEditModalVisible] = useState(false); const fetchMalList = useCallback(async () => { try { setIsLoading(true); let allItems: MalAnimeNode[] = []; let offset = 0; let hasMore = true; while (hasMore && offset < 1000) { const response = await MalApiService.getUserList(undefined, offset, 100); if (response.data && response.data.length > 0) { allItems = [...allItems, ...response.data]; offset += response.data.length; hasMore = !!response.paging.next; } else { hasMore = false; } } const grouped: Record = { watching: [], completed: [], on_hold: [], dropped: [], plan_to_watch: [], }; allItems.forEach(item => { const status = item.list_status.status; if (grouped[status]) { grouped[status].push(item); } }); setGroupedList(grouped); } catch (error) { logger.error('[MalLibrary] Failed to fetch list', error); } finally { setIsLoading(false); setIsRefreshing(false); } }, []); useEffect(() => { fetchMalList(); }, [fetchMalList]); const handleRefresh = () => { setIsRefreshing(true); fetchMalList(); }; const handleItemPress = async (item: MalAnimeNode) => { // Requirement 8: Resolve correct Cinemata / TMDB / IMDb ID const malId = item.node.id; // Use MalSync API to get external IDs const { imdbId } = await MalSync.getIdsFromMalId(malId); if (imdbId) { navigation.navigate('Metadata', { id: imdbId, type: item.node.media_type === 'movie' ? 'movie' : 'series' }); } else { // Fallback: Navigate to Search with the title if ID mapping is missing logger.warn(`[MalLibrary] Could not resolve IMDb ID for MAL:${malId}. Falling back to Search.`); navigation.navigate('Search', { query: item.node.title }); } }; const renderAnimeItem = ({ item }: { item: MalAnimeNode }) => ( handleItemPress(item)} activeOpacity={0.7} > {item.list_status.num_episodes_watched} / {item.node.num_episodes || '?'} {item.node.title} {item.list_status.score > 0 && ( {item.list_status.score} )} {/* Requirement 5: Manual update button */} { setSelectedAnime(item); setIsEditModalVisible(true); }} > ); const renderSection = (status: MalListStatus, title: string, icon: string) => { const data = groupedList[status]; if (data.length === 0) return null; return ( {title} ({data.length}) item.node.id.toString()} horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.carouselContent} snapToInterval={ITEM_WIDTH + 12} decelerationRate="fast" /> ); }; return ( navigation.goBack()} style={styles.backButton}> MyAnimeList {/* Requirement 6: Manual Sync Button */} {isLoading ? ( ) : ( )} {!isLoading || isRefreshing ? ( } contentContainerStyle={{ paddingBottom: 40 }} > {renderSection('watching', 'Watching', 'play-circle-outline')} {renderSection('plan_to_watch', 'Plan to Watch', 'bookmark-outline')} {renderSection('completed', 'Completed', 'check-circle-outline')} {renderSection('on_hold', 'On Hold', 'pause-circle-outline')} {renderSection('dropped', 'Dropped', 'highlight-off')} ) : ( )} {selectedAnime && ( { setIsEditModalVisible(false); setSelectedAnime(null); }} onUpdateSuccess={fetchMalList} /> )} ); }; const styles = StyleSheet.create({ container: { flex: 1 }, header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 12, justifyContent: 'space-between' }, backButton: { padding: 4 }, headerTitle: { fontSize: 20, fontWeight: '700', flex: 1, marginLeft: 16 }, syncButton: { padding: 4 }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' }, sectionContainer: { marginVertical: 12 }, sectionHeader: { paddingHorizontal: 16, marginBottom: 8 }, sectionTitle: { fontSize: 18, fontWeight: '700' }, carouselContent: { paddingHorizontal: 10 }, animeItem: { width: ITEM_WIDTH, marginHorizontal: 6, marginBottom: 10, }, poster: { width: ITEM_WIDTH, height: ITEM_HEIGHT, borderRadius: 8, backgroundColor: '#333', }, badgeContainer: { position: 'absolute', top: 6, left: 6, }, episodeBadge: { paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, }, episodeText: { color: 'white', fontSize: 10, fontWeight: '700', }, animeTitle: { fontSize: 12, fontWeight: '600', marginTop: 6, lineHeight: 16, }, scoreRow: { flexDirection: 'row', alignItems: 'center', marginTop: 2, }, scoreText: { fontSize: 11, marginLeft: 4, }, editButton: { position: 'absolute', top: 6, right: 6, backgroundColor: 'rgba(0,0,0,0.6)', padding: 6, borderRadius: 15, } }); export default MalLibraryScreen;