diff --git a/mobile/src/components/SceneGridSkeleton.tsx b/mobile/src/components/SceneGridSkeleton.tsx new file mode 100644 index 0000000..f2a5079 --- /dev/null +++ b/mobile/src/components/SceneGridSkeleton.tsx @@ -0,0 +1,81 @@ +/** + * SceneGridSkeleton — placeholder grid pokazywany podczas ŁADOWANIA listy scen, + * zamiast spinnera. Rysuje się natychmiast (zero danych), więc ekran "ożywa" w ms + * nawet gdy backend liczy kilka sekund — percepcyjna szybkość jak w dużych apkach + * (YT/Netflix pokazują szkielety/placeholdery zanim dane dojdą). + * + * Layout 1:1 z SceneTile (2-col, thumb 16:9 + linia tytułu + linia meta), żeby + * przejście skeleton→content nie "skakało". Subtelny pulse (Animated opacity loop), + * bez zewnętrznych zależności. + */ +import React from 'react'; +import { Animated, StyleSheet, View } from 'react-native'; + +import { theme } from '../theme'; + +function usepulse() { + const v = React.useRef(new Animated.Value(0.4)).current; + React.useEffect(() => { + const loop = Animated.loop( + Animated.sequence([ + Animated.timing(v, { toValue: 0.85, duration: 700, useNativeDriver: true }), + Animated.timing(v, { toValue: 0.4, duration: 700, useNativeDriver: true }), + ]), + ); + loop.start(); + return () => loop.stop(); + }, [v]); + return v; +} + +function SkeletonTile({ opacity }: { opacity: Animated.Value }) { + return ( + + + + + + ); +} + +/** `count` kafelków (parzysta liczba dla 2-col siatki). */ +export function SceneGridSkeleton({ count = 8 }: { count?: number }) { + const opacity = usepulse(); + const rows = Math.ceil(count / 2); + return ( + + {Array.from({ length: rows }).map((_, r) => ( + + + + + ))} + + ); +} + +const styles = StyleSheet.create({ + grid: { marginTop: 2 }, + row: { flexDirection: 'row', gap: 10, marginBottom: 14 }, + tile: { flex: 1 }, + thumb: { + width: '100%', + aspectRatio: 16 / 9, + borderRadius: 6, + backgroundColor: theme.bgElevated, + }, + lineTitle: { + height: 12, + borderRadius: 4, + backgroundColor: theme.bgElevated, + marginTop: 8, + width: '85%', + }, + lineMeta: { + height: 9, + borderRadius: 4, + backgroundColor: theme.bgElevated, + marginTop: 6, + width: '55%', + }, +}); diff --git a/mobile/src/screens/ScenesScreen.tsx b/mobile/src/screens/ScenesScreen.tsx index 0c332a3..19984d3 100644 --- a/mobile/src/screens/ScenesScreen.tsx +++ b/mobile/src/screens/ScenesScreen.tsx @@ -15,6 +15,7 @@ import { import { Image } from 'expo-image'; import * as Haptics from 'expo-haptics'; import { SceneTile } from '../components/SceneTile'; +import { SceneGridSkeleton } from '../components/SceneGridSkeleton'; import { Thumb } from '../components/Thumb'; import { useClient } from '../ClientContext'; import { theme } from '../theme'; @@ -119,9 +120,11 @@ export function ScenesScreen() { - {isLoading && } {error instanceof Error && {error.message}} + {isLoading ? ( + + ) : ( s.id} @@ -145,6 +148,7 @@ export function ScenesScreen() { ListEmptyComponent={!isLoading ? no scenes : null} contentContainerStyle={{ paddingBottom: 24 }} /> + )} - {isLoading && } {error instanceof Error && {error.message}} + {isLoading ? ( + + ) : ( s.id} @@ -62,6 +64,7 @@ export function TagScenesScreen() { ListEmptyComponent={!isLoading ? no scenes : null} contentContainerStyle={{ paddingBottom: 24 }} /> + )} ); }