210 lines
6 KiB
TypeScript
210 lines
6 KiB
TypeScript
|
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|||
|
|
import {
|
|||
|
|
FlatList,
|
|||
|
|
Pressable,
|
|||
|
|
RefreshControl,
|
|||
|
|
ScrollView,
|
|||
|
|
StyleSheet,
|
|||
|
|
Text,
|
|||
|
|
View,
|
|||
|
|
useWindowDimensions,
|
|||
|
|
} from 'react-native'
|
|||
|
|
import { useRouter } from 'expo-router'
|
|||
|
|
import Ionicons from '@expo/vector-icons/Ionicons'
|
|||
|
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|||
|
|
import type { CollectionType, Release } from '../../src/lib/types'
|
|||
|
|
import { api } from '../../src/lib/api'
|
|||
|
|
import { useAuth } from '../../src/lib/AuthContext'
|
|||
|
|
import { colors, spacing } from '../../src/lib/theme'
|
|||
|
|
import AnimeCard from '../../src/components/AnimeCard'
|
|||
|
|
import Loader from '../../src/components/Loader'
|
|||
|
|
import Chip from '../../src/components/Chip'
|
|||
|
|
|
|||
|
|
type TabKey = CollectionType | 'FAVORITES'
|
|||
|
|
|
|||
|
|
const TABS: Array<{ key: TabKey; label: string }> = [
|
|||
|
|
{ key: 'WATCHING', label: 'Смотрю' },
|
|||
|
|
{ key: 'PLANNED', label: 'Запланировано' },
|
|||
|
|
{ key: 'WATCHED', label: 'Просмотрено' },
|
|||
|
|
{ key: 'POSTPONED', label: 'Отложено' },
|
|||
|
|
{ key: 'ABANDONED', label: 'Брошено' },
|
|||
|
|
{ key: 'FAVORITES', label: 'Избранное' },
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
export default function MyListsScreen() {
|
|||
|
|
const router = useRouter()
|
|||
|
|
const insets = useSafeAreaInsets()
|
|||
|
|
const { width } = useWindowDimensions()
|
|||
|
|
const { user, loading: authLoading } = useAuth()
|
|||
|
|
const [tab, setTab] = useState<TabKey>('WATCHING')
|
|||
|
|
const [releases, setReleases] = useState<Release[] | null>(null)
|
|||
|
|
const [refreshing, setRefreshing] = useState(false)
|
|||
|
|
|
|||
|
|
const load = useCallback((currentTab: TabKey) => {
|
|||
|
|
const req =
|
|||
|
|
currentTab === 'FAVORITES' ? api.favoritesList(1, 48) : api.collectionList(currentTab, 1, 48)
|
|||
|
|
return req.then((res) => setReleases(res.data)).catch(() => setReleases([]))
|
|||
|
|
}, [])
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!user) return
|
|||
|
|
setReleases(null)
|
|||
|
|
load(tab)
|
|||
|
|
}, [tab, user, load])
|
|||
|
|
|
|||
|
|
async function onRefresh() {
|
|||
|
|
setRefreshing(true)
|
|||
|
|
await load(tab)
|
|||
|
|
setRefreshing(false)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const numColumns = Math.max(3, Math.floor((width - spacing(4) * 2) / 120))
|
|||
|
|
|
|||
|
|
const paddedReleases = useMemo(() => {
|
|||
|
|
if (!releases) return []
|
|||
|
|
const remainder = releases.length % numColumns
|
|||
|
|
if (remainder === 0) return releases
|
|||
|
|
return [...releases, ...Array(numColumns - remainder).fill(null)]
|
|||
|
|
}, [releases, numColumns])
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<View style={styles.container}>
|
|||
|
|
<View style={[styles.header, { paddingTop: insets.top + spacing(2) }]}>
|
|||
|
|
<Pressable onPress={() => router.back()} hitSlop={10} style={styles.backButton}>
|
|||
|
|
<Ionicons name="chevron-back" size={24} color={colors.text} />
|
|||
|
|
</Pressable>
|
|||
|
|
<Text style={styles.headerTitle}>Мои списки</Text>
|
|||
|
|
</View>
|
|||
|
|
|
|||
|
|
{authLoading && <Loader />}
|
|||
|
|
|
|||
|
|
{!authLoading && !user && (
|
|||
|
|
<View style={styles.empty}>
|
|||
|
|
<Text style={styles.emptyText}>Войдите, чтобы видеть свои списки</Text>
|
|||
|
|
<Pressable style={styles.loginButton} onPress={() => router.push('/login')}>
|
|||
|
|
<Text style={styles.loginButtonText}>Войти</Text>
|
|||
|
|
</Pressable>
|
|||
|
|
</View>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{!authLoading && user && (
|
|||
|
|
<>
|
|||
|
|
<ScrollView
|
|||
|
|
horizontal
|
|||
|
|
showsHorizontalScrollIndicator={false}
|
|||
|
|
contentContainerStyle={styles.tabs}
|
|||
|
|
>
|
|||
|
|
{TABS.map((t) => (
|
|||
|
|
<Pressable key={t.key} onPress={() => setTab(t.key)}>
|
|||
|
|
<Chip accent={tab === t.key}>{t.label}</Chip>
|
|||
|
|
</Pressable>
|
|||
|
|
))}
|
|||
|
|
</ScrollView>
|
|||
|
|
|
|||
|
|
{!releases && <Loader />}
|
|||
|
|
|
|||
|
|
{releases && releases.length === 0 && (
|
|||
|
|
<View style={styles.empty}>
|
|||
|
|
<Text style={styles.emptyText}>Список пуст</Text>
|
|||
|
|
</View>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{releases && releases.length > 0 && (
|
|||
|
|
<FlatList
|
|||
|
|
key={`grid-${numColumns}`}
|
|||
|
|
data={paddedReleases}
|
|||
|
|
numColumns={numColumns}
|
|||
|
|
keyExtractor={(item, index) => (item ? String(item.id) : `empty-${index}`)}
|
|||
|
|
columnWrapperStyle={styles.row}
|
|||
|
|
contentContainerStyle={[styles.grid, { paddingBottom: insets.bottom + spacing(6) }]}
|
|||
|
|
refreshControl={
|
|||
|
|
<RefreshControl
|
|||
|
|
refreshing={refreshing}
|
|||
|
|
onRefresh={onRefresh}
|
|||
|
|
tintColor={colors.accent}
|
|||
|
|
colors={[colors.accent]}
|
|||
|
|
/>
|
|||
|
|
}
|
|||
|
|
renderItem={({ item }) =>
|
|||
|
|
item ? (
|
|||
|
|
<View style={styles.gridItem}>
|
|||
|
|
<AnimeCard release={item} onPress={() => router.push(`/title/${item.alias || item.id}`)} />
|
|||
|
|
</View>
|
|||
|
|
) : (
|
|||
|
|
<View style={styles.gridItem} />
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</View>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const styles = StyleSheet.create({
|
|||
|
|
container: {
|
|||
|
|
flex: 1,
|
|||
|
|
backgroundColor: colors.background,
|
|||
|
|
},
|
|||
|
|
header: {
|
|||
|
|
flexDirection: 'row',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
gap: spacing(3),
|
|||
|
|
paddingHorizontal: spacing(4),
|
|||
|
|
paddingBottom: spacing(3),
|
|||
|
|
},
|
|||
|
|
backButton: {
|
|||
|
|
width: 36,
|
|||
|
|
height: 36,
|
|||
|
|
borderRadius: 18,
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
backgroundColor: colors.surfaceRaised,
|
|||
|
|
},
|
|||
|
|
headerTitle: {
|
|||
|
|
color: colors.text,
|
|||
|
|
fontSize: 19,
|
|||
|
|
fontWeight: '700',
|
|||
|
|
},
|
|||
|
|
tabs: {
|
|||
|
|
gap: spacing(2),
|
|||
|
|
paddingHorizontal: spacing(4),
|
|||
|
|
paddingBottom: spacing(3),
|
|||
|
|
},
|
|||
|
|
empty: {
|
|||
|
|
flex: 1,
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
paddingHorizontal: spacing(8),
|
|||
|
|
gap: spacing(4),
|
|||
|
|
},
|
|||
|
|
emptyText: {
|
|||
|
|
color: colors.textFaint,
|
|||
|
|
fontSize: 14,
|
|||
|
|
textAlign: 'center',
|
|||
|
|
lineHeight: 20,
|
|||
|
|
},
|
|||
|
|
loginButton: {
|
|||
|
|
backgroundColor: colors.accent,
|
|||
|
|
paddingHorizontal: spacing(6),
|
|||
|
|
paddingVertical: spacing(3),
|
|||
|
|
borderRadius: 999,
|
|||
|
|
},
|
|||
|
|
loginButtonText: {
|
|||
|
|
color: colors.text,
|
|||
|
|
fontSize: 14,
|
|||
|
|
fontWeight: '700',
|
|||
|
|
},
|
|||
|
|
grid: {
|
|||
|
|
padding: spacing(4),
|
|||
|
|
gap: spacing(3),
|
|||
|
|
},
|
|||
|
|
row: {
|
|||
|
|
gap: spacing(3),
|
|||
|
|
},
|
|||
|
|
gridItem: {
|
|||
|
|
flex: 1,
|
|||
|
|
},
|
|||
|
|
})
|