/** * @license * SPDX-License-Identifier: Apache-2.0 */ import { useState, useMemo, useEffect } from 'react'; import { LayoutGroup } from 'motion/react'; import { BrowserRouter, Routes, Route, useNavigate, useSearchParams, useParams, useLocation } from 'react-router-dom'; import Header from './components/Header'; import BrowseView from './components/BrowseView'; import DetailView from './components/DetailView'; import CastView from './components/CastView'; import CastDetailView from './components/CastDetailView'; import AddMediaView from './components/AddMediaView'; import ImporterView from './components/ImporterView'; import SettingsView from './components/SettingsView'; import { MOCK_MEDIA, DETAIL_MEDIA } from './data'; import { Media, Staff, MediaCategory, UserSettings } from './types'; import { fetchAllMedia, fetchMediaById, fetchCastById, convertApiCastToStaff, fetchSettings, updateSettings } from './api'; function AppContent() { const navigate = useNavigate(); const location = useLocation(); const [searchParams, setSearchParams] = useSearchParams(); const [activeCategory, setActiveCategory] = useState( (searchParams.get('category') as MediaCategory) || 'Anime' ); const [selectedMedia, setSelectedMedia] = useState(null); const [selectedPerson, setSelectedPerson] = useState(null); const [searchQuery, setSearchQuery] = useState(searchParams.get('search') || ''); const [enabledCategories, setEnabledCategories] = useState(['Anime', 'Movies', 'TV Series', 'Music', 'Books', 'Consoles', 'Games', 'Adult']); const [settings, setSettings] = useState(null); const [customMedia, setCustomMedia] = useState([]); const [adultMedia, setAdultMedia] = useState([]); // Load media from API on component mount (only when not on cast routes) const [apiMedia, setApiMedia] = useState([]); useEffect(() => { const loadSettingsFromApi = async () => { try { const loadedSettings = await fetchSettings(); if (loadedSettings) { setSettings(loadedSettings); setEnabledCategories(loadedSettings.enabledCategories); } } catch (error) { console.error('Failed to load settings from API:', error); } }; loadSettingsFromApi(); }, []); const reloadSettings = async () => { try { const loadedSettings = await fetchSettings(); if (loadedSettings) { setSettings(loadedSettings); setEnabledCategories(loadedSettings.enabledCategories); } } catch (error) { console.error('Failed to reload settings from API:', error); } }; useEffect(() => { const loadMediaFromApi = async () => { try { const media = await fetchAllMedia(); setApiMedia(media); } catch (error) { console.error('Failed to load media from API:', error); } }; // Only load media if not on cast routes if (!location.pathname.startsWith('/cast')) { loadMediaFromApi(); } }, [location.pathname]); const toggleCategory = async (category: MediaCategory) => { setEnabledCategories(prev => { const isEnabling = !prev.includes(category); const newList = isEnabling ? [...prev, category] : prev.filter(c => c !== category); // If we disable the current active category, switch to another enabled one if (!isEnabling && activeCategory === category) { const nextCategory = newList.find(c => c !== category) || 'Anime'; setActiveCategory(nextCategory as MediaCategory); } // Save to API const baseSettings = settings || { enabledCategories: prev, itemsPerPage: 20, defaultView: 'grid', showAdultContent: false, autoPlayTrailers: false, language: 'en', theme: 'system', }; const updatedSettings: UserSettings = { ...baseSettings, enabledCategories: newList, }; updateSettings(updatedSettings).then(saved => { if (saved) { setSettings(saved); } }); return newList; }); }; const handleCategoryChange = (category: MediaCategory) => { setActiveCategory(category); setSearchParams({ category }); navigate('/'); window.scrollTo({ top: 0, behavior: 'smooth' }); }; const handleAddMediaView = () => { navigate('/add'); window.scrollTo({ top: 0, behavior: 'smooth' }); }; const handleImporterView = () => { navigate('/import'); window.scrollTo({ top: 0, behavior: 'smooth' }); }; const allMedia = useMemo(() => { // Use API data if available, otherwise fall back to mock data let list: Media[] = []; if (apiMedia.length > 0) { // API has data, use it list = [...apiMedia]; } else { // API is empty, use mock data as fallback list = [...MOCK_MEDIA]; } // Add custom media and detail media list = [...list, ...customMedia]; if (!list.find(m => m.id === DETAIL_MEDIA.id)) { list.push(DETAIL_MEDIA); } // Filter by active category AND ensure it's enabled return list.filter(m => m.category === activeCategory && enabledCategories.includes(m.category)); }, [activeCategory, enabledCategories, customMedia, apiMedia]); const handleAddMedia = async () => { // Reload all media from API to get the newly added item try { const media = await fetchAllMedia(); setApiMedia(media); } catch (error) { console.error('Failed to reload media from API:', error); } }; const allStaff = useMemo(() => { const staff: Staff[] = []; // Use API data if available, otherwise fall back to mock data let baseList: Media[] = []; if (apiMedia.length > 0) { // API has data, use it baseList = [...apiMedia]; } else { // API is empty, use mock data as fallback baseList = [...MOCK_MEDIA]; } // Add custom media and detail media baseList = [...baseList, ...customMedia]; if (!baseList.find(m => m.id === DETAIL_MEDIA.id)) { baseList.push(DETAIL_MEDIA); } const enabledMedia = baseList.filter(m => enabledCategories.includes(m.category)); enabledMedia.forEach(media => { media.staff?.forEach(s => { staff.push({ ...s, mediaId: media.id, mediaTitle: media.title }); }); }); return staff; }, [enabledCategories, customMedia, apiMedia]); const filteredMedia = useMemo(() => { if (!searchQuery.trim()) return allMedia; const query = searchQuery.toLowerCase(); return allMedia.filter(media => media.title.toLowerCase().includes(query) || media.year.toLowerCase().includes(query) || media.genres?.some(g => g.toLowerCase().includes(query)) || media.studios?.some(s => s.toLowerCase().includes(query)) ); }, [allMedia, searchQuery]); const handleMediaClick = async (media: Media) => { // For adult media, try to fetch detailed data by ID if (media.category === 'Adult') { try { const detailedMedia = await fetchMediaById(parseInt(media.id)); if (detailedMedia) { setSelectedMedia(detailedMedia); } else { // Fallback to original media if detailed fetch fails setSelectedMedia(media); } } catch (error) { console.error('Failed to fetch detailed media:', error); setSelectedMedia(media); } } else { // For non-adult media, use the original media setSelectedMedia(media); } navigate(`/media/${media.id}`); window.scrollTo({ top: 0, behavior: 'smooth' }); }; const handleBack = () => { navigate('/'); window.scrollTo({ top: 0, behavior: 'smooth' }); }; const handleCastClick = () => { navigate('/cast'); window.scrollTo({ top: 0, behavior: 'smooth' }); }; const handlePersonClick = (person: Staff) => { // Enrich person with some mock data for the detail page const enrichedPerson: Staff = { ...person, bio: `${person.name} is a renowned ${person.role} with a career spanning over a decade. Known for their versatility and emotional depth, they have become a staple in the industry, particularly for their work in ${person.mediaTitle || 'major productions'}.`, birthDate: 'October 14, 1985', birthPlace: 'Tokyo, Japan', occupations: ['Voice Actor', 'Singer', 'Narrator'] }; setSelectedPerson(enrichedPerson); navigate(`/cast/${person.id}`); window.scrollTo({ top: 0, behavior: 'smooth' }); }; const handleSearch = (query: string) => { setSearchQuery(query); const params = new URLSearchParams(searchParams); if (query) { params.set('search', query); } else { params.delete('search'); } setSearchParams(params); navigate('/'); }; return (
} /> } /> } /> } /> } /> } /> } />
{/* Footer */}
); } // Helper component for media detail route function MediaDetailRoute({ selectedMedia, setSelectedMedia, allMedia, onPersonClick }: any) { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); useEffect(() => { const loadMedia = async () => { if (id) { // First check if media is in allMedia const media = allMedia.find(m => m.id === id); if (media) { setSelectedMedia(media); } else { // If not found, fetch from API try { const fetchedMedia = await fetchMediaById(id); if (fetchedMedia) { setSelectedMedia(fetchedMedia); } else { navigate('/'); } } catch (error) { console.error('Failed to fetch media:', error); navigate('/'); } } } }; loadMedia(); }, [id, allMedia]); if (!selectedMedia) return null; return ( ); } // Helper component for cast detail route function CastDetailRoute({ selectedPerson, setSelectedPerson }: any) { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); useEffect(() => { const loadCast = async () => { if (id) { try { const castData = await fetchCastById(id); if (castData) { const person = convertApiCastToStaff(castData); setSelectedPerson(person); } else { navigate('/cast'); } } catch (error) { console.error('Failed to load cast:', error); navigate('/cast'); } } }; loadCast(); }, [id]); if (!selectedPerson) return null; return ( ); } export default function App() { return ( ); }