ci: typecheck on push

This commit is contained in:
ImSavsis 2026-07-19 06:05:12 +05:00
parent 3c407248a7
commit 144a2cd227
17 changed files with 2345 additions and 1 deletions

View file

@ -0,0 +1,148 @@
import { useEffect, useRef, useState } from 'react'
import { Animated, Image, Pressable, StyleSheet, Text, View } from 'react-native'
import Ionicons from '@expo/vector-icons/Ionicons'
import type { Release } from '../lib/types'
import { imageUrl } from '../lib/api'
import { colors, radius, spacing } from '../lib/theme'
import { isInWishlist, onWishlistChange, toggleWishlist } from '../lib/wishlist'
interface AnimeCardProps {
release: Release
onPress?: () => void
}
export default function AnimeCard({ release, onPress }: AnimeCardProps) {
const [saved, setSaved] = useState(false)
const scale = useRef(new Animated.Value(1)).current
useEffect(() => {
let mounted = true
isInWishlist(release.id).then((v) => mounted && setSaved(v))
const unsubscribe = onWishlistChange(() => {
isInWishlist(release.id).then((v) => mounted && setSaved(v))
})
return () => {
mounted = false
unsubscribe()
}
}, [release.id])
function pressIn() {
Animated.spring(scale, { toValue: 0.96, useNativeDriver: true, speed: 50 }).start()
}
function pressOut() {
Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 50 }).start()
}
async function handleToggleWishlist() {
const nowSaved = await toggleWishlist(release)
setSaved(nowSaved)
}
const poster = imageUrl(release.poster?.optimized?.preview || release.poster?.preview)
return (
<Pressable onPress={onPress} onPressIn={pressIn} onPressOut={pressOut}>
<Animated.View style={[styles.container, { transform: [{ scale }] }]}>
<View style={styles.posterWrap}>
{poster && <Image source={{ uri: poster }} style={styles.poster} resizeMode="cover" />}
<View style={styles.topLeftCol}>
{release.is_ongoing && (
<View style={styles.badgeAccent}>
<Text style={styles.badgeText}>ONGOING</Text>
</View>
)}
{release.episodes_total != null && (
<View style={styles.badgeMuted}>
<Text style={styles.badgeText}>{release.episodes_total} эп.</Text>
</View>
)}
</View>
<Pressable hitSlop={10} onPress={handleToggleWishlist} style={styles.starButton}>
<Ionicons
name={saved ? 'star' : 'star-outline'}
size={15}
color={saved ? colors.accent : colors.text}
/>
</Pressable>
</View>
<Text numberOfLines={2} style={styles.title}>
{release.name.main}
</Text>
<Text numberOfLines={1} style={styles.meta}>
{release.season?.description} {release.year} · {release.type?.description}
</Text>
</Animated.View>
</Pressable>
)
}
const styles = StyleSheet.create({
container: {
width: '100%',
},
posterWrap: {
width: '100%',
aspectRatio: 2 / 3,
borderRadius: radius.md,
overflow: 'hidden',
backgroundColor: colors.surfaceRaised,
},
poster: {
width: '100%',
height: '100%',
},
topLeftCol: {
position: 'absolute',
top: spacing(1.5),
left: spacing(1.5),
gap: spacing(1),
},
badgeAccent: {
backgroundColor: colors.accent,
paddingHorizontal: spacing(1.5),
paddingVertical: spacing(0.5),
borderRadius: radius.sm,
alignSelf: 'flex-start',
},
badgeMuted: {
backgroundColor: colors.overlay,
paddingHorizontal: spacing(1.5),
paddingVertical: spacing(0.5),
borderRadius: radius.sm,
alignSelf: 'flex-start',
},
badgeText: {
color: colors.text,
fontSize: 9,
fontWeight: '700',
textTransform: 'uppercase',
},
starButton: {
position: 'absolute',
top: spacing(1.5),
right: spacing(1.5),
width: 26,
height: 26,
borderRadius: radius.full,
backgroundColor: colors.overlay,
alignItems: 'center',
justifyContent: 'center',
},
title: {
marginTop: spacing(2),
color: colors.text,
fontSize: 13,
fontWeight: '600',
lineHeight: 16,
},
meta: {
marginTop: spacing(1),
color: colors.textFaint,
fontSize: 11,
},
})

40
src/components/Chip.tsx Normal file
View file

@ -0,0 +1,40 @@
import { ReactNode } from 'react'
import { StyleSheet, Text, View } from 'react-native'
import { colors, radius, spacing } from '../lib/theme'
interface ChipProps {
children: ReactNode
accent?: boolean
}
export default function Chip({ children, accent }: ChipProps) {
return (
<View style={[styles.chip, accent && styles.chipAccent]}>
<Text style={[styles.text, accent && styles.textAccent]}>{children}</Text>
</View>
)
}
const styles = StyleSheet.create({
chip: {
backgroundColor: colors.surfaceRaised,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.border,
paddingHorizontal: spacing(3),
paddingVertical: spacing(1.5),
borderRadius: radius.full,
alignSelf: 'flex-start',
},
chipAccent: {
backgroundColor: colors.accent,
borderColor: colors.accent,
},
text: {
color: colors.textDim,
fontSize: 12,
fontWeight: '600',
},
textAccent: {
color: colors.text,
},
})

29
src/components/Loader.tsx Normal file
View file

@ -0,0 +1,29 @@
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'
import { colors, spacing } from '../lib/theme'
interface LoaderProps {
label?: string
}
export default function Loader({ label }: LoaderProps) {
return (
<View style={styles.container}>
<ActivityIndicator size="large" color={colors.accent} />
{label && <Text style={styles.label}>{label}</Text>}
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing(24),
gap: spacing(3),
},
label: {
color: colors.textDim,
fontSize: 13,
},
})

59
src/components/Row.tsx Normal file
View file

@ -0,0 +1,59 @@
import { FlatList, StyleSheet, Text, View } from 'react-native'
import { useRouter } from 'expo-router'
import type { Release } from '../lib/types'
import { colors, spacing } from '../lib/theme'
import AnimeCard from './AnimeCard'
interface RowProps {
title: string
releases: Release[]
}
const CARD_WIDTH = 132
export default function Row({ title, releases }: RowProps) {
const router = useRouter()
if (!releases.length) return null
return (
<View style={styles.section}>
<Text style={styles.heading}>{title}</Text>
<FlatList
horizontal
data={releases}
keyExtractor={(item) => String(item.id)}
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.list}
renderItem={({ item }) => (
<View style={styles.item}>
<AnimeCard
release={item}
onPress={() => router.push(`/title/${item.alias || item.id}`)}
/>
</View>
)}
/>
</View>
)
}
const styles = StyleSheet.create({
section: {
marginTop: spacing(4),
},
heading: {
color: colors.text,
fontSize: 17,
fontWeight: '700',
marginBottom: spacing(2),
paddingHorizontal: spacing(4),
},
list: {
paddingHorizontal: spacing(4),
gap: spacing(3),
},
item: {
width: CARD_WIDTH,
},
})

28
src/lib/playerPrefs.ts Normal file
View file

@ -0,0 +1,28 @@
import AsyncStorage from '@react-native-async-storage/async-storage'
const KEY = 'anisav:playerPrefs'
export interface PlayerPrefs {
autoSkipOpening: boolean
autoSkipEnding: boolean
autoPlayNext: boolean
}
const DEFAULTS: PlayerPrefs = {
autoSkipOpening: false,
autoSkipEnding: false,
autoPlayNext: true,
}
export async function loadPlayerPrefs(): Promise<PlayerPrefs> {
try {
const raw = await AsyncStorage.getItem(KEY)
return raw ? { ...DEFAULTS, ...JSON.parse(raw) } : DEFAULTS
} catch {
return DEFAULTS
}
}
export async function savePlayerPrefs(prefs: PlayerPrefs): Promise<void> {
await AsyncStorage.setItem(KEY, JSON.stringify(prefs))
}