Поставьте зависимости:
npm install expo-haptics react-native-gesture-handler react-native-reanimated react-native-workletspnpm add expo-haptics react-native-gesture-handler react-native-reanimated react-native-workletsyarn add expo-haptics react-native-gesture-handler react-native-reanimated react-native-workletsbun add expo-haptics react-native-gesture-handler react-native-reanimated react-native-workletsСкопируйте эти файлы в src/shared/components/molecules/Pagination/:
Проверьте алиас путей. Импорты в коде выше записаны так, как их пишет astra-rn add: @/shared/components, @/shared/utils и @/shared/hooks. Нужен алиас "@/*": ["./src/*"] в tsconfig.json — или поправьте импорты под свои пути.
import { useRef, useState } from "react";
import { StyleSheet } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { StatusBar } from "expo-status-bar";
import { Pagination } from "@/shared/components/molecules/Pagination";
import { Showcase, useAutoplay, useCustomize } from "~/showcase";
const AUTOPLAY_SEQUENCE: Array<[number, number]> = [
[1, 380],
[2, 280],
[3, 640],
[2, 630],
[1, 250],
[0, 200],
];
const CUSTOMIZE = [
{ key: "total", label: "Количество", type: "slider", min: 2, max: 8, step: 1, default: 4 },
{ key: "dotSize", label: "Размер точки", type: "slider", min: 4, max: 16, step: 1, unit: "px", default: 12 },
{ key: "radius", label: "Скругление", type: "slider", min: 0, max: 8, step: 0.5, unit: "px", default: 4 },
{ key: "gap", label: "Отступ точек", type: "slider", min: 0, max: 16, step: 1, unit: "px", default: 8 },
{ key: "activeColor", label: "Активный цвет", type: "color", default: "#f4f4f5" },
{ key: "inactiveColor", label: "Неактивный цвет", type: "color", default: "#3f3f46" },
{ key: "background", label: "Фон", type: "color", default: "#171717" },
{ key: "tempo", label: "Темп автопоказа", type: "slider", min: 0.5, max: 3, step: 0.1, default: 1 },
] as const;
function buildSequence(total: number): Array<[number, number]> {
if (total === 4) return AUTOPLAY_SEQUENCE;
const forward = Array.from({ length: total - 1 }, (_, i): [number, number] => [i + 1, 320]);
const back = Array.from({ length: total - 1 }, (_, i): [number, number] => [total - 2 - i, 260]);
return [...forward, ...back];
}
export default function App() {
const [activeIndex, setActiveIndex] = useState<number>(0);
const custom = useCustomize(CUSTOMIZE);
const live = useRef({ total: custom.total, tempo: custom.tempo });
live.current = { total: custom.total, tempo: custom.tempo };
useAutoplay(async ({ wait }) => {
for (const [index, delay] of buildSequence(live.current.total)) {
await wait(delay / live.current.tempo);
setActiveIndex(Math.min(index, live.current.total - 1));
}
await wait(300 / live.current.tempo);
});
return (
<Showcase>
<GestureHandlerRootView style={styles.container}>
<StatusBar style="inverted" />
<Pagination
activeIndex={Math.min(activeIndex, custom.total - 1)}
onIndexChange={setActiveIndex}
dotSize={custom.dotSize}
inactiveColor={custom.inactiveColor}
activeColor={custom.activeColor}
currentColor={custom.activeColor}
borderRadius={custom.radius}
dotContainer={custom.gap}
containerStyle={{
backgroundColor: custom.background,
padding: 12,
borderRadius: 12,
}}
totalItems={custom.total}
/>
</GestureHandlerRootView>
</Showcase>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#0a0a0a",
},
header: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
paddingTop: 70,
paddingBottom: 32,
},
title: {
fontSize: 28,
color: "#fff",
letterSpacing: 2,
},
subtitle: {
fontSize: 12,
color: "#aaa",
},
headerRight: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: "#1a1a1a",
justifyContent: "center",
alignItems: "center",
},
card: {
width: "100%",
height: 340,
borderRadius: 24,
overflow: "hidden",
},
cardImage: {
width: 300,
height: "100%",
resizeMode: "cover",
},
cardGradient: {
...StyleSheet.absoluteFillObject,
},
cardContent: {
position: "absolute",
bottom: 0,
left: 0,
right: 0,
padding: 20,
gap: 8,
},
albumName: {
fontSize: 16,
color: "#fff",
letterSpacing: 1,
},
artistRow: {
flexDirection: "row",
alignItems: "center",
gap: 6,
},
artistText: {
fontSize: 13,
color: "rgba(255,255,255,0.7)",
},
dot: {
width: 3,
height: 3,
borderRadius: 1.5,
backgroundColor: "rgba(255,255,255,0.4)",
},
yearText: {
fontSize: 13,
color: "rgba(255,255,255,0.5)",
},
footer: {
paddingHorizontal: 24,
marginTop: 32,
gap: 20,
},
nowPlaying: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
backgroundColor: "#141414",
padding: 12,
borderRadius: 16,
},
nowPlayingLeft: {
flexDirection: "row",
alignItems: "center",
gap: 12,
flex: 1,
},
nowPlayingImage: {
width: 48,
height: 48,
borderRadius: 10,
},
nowPlayingInfo: {
flex: 1,
gap: 2,
},
nowPlayingTitle: {
fontSize: 14,
fontWeight: "600",
color: "#fff",
},
nowPlayingArtist: {
fontSize: 12,
color: "#666",
},
nowPlayingControls: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: "#fff",
justifyContent: "center",
alignItems: "center",
},
dots: {
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
gap: 6,
},
dotIndicator: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: "#333",
},
dotIndicatorActive: {
width: 20,
backgroundColor: "#fff",
},
});activeIndexТип number
totalItemsТип number
inactiveColor?Тип string
activeColor?Тип string
currentColor?Тип string
containerStyle?Тип StyleProp<ViewStyle>
dotSize?Тип number
borderRadius?Тип number
dotContainer?Тип number
onIndexChange?Тип (index: number) => void