/** * Tutorial przy pierwszym odpaleniu — paged carousel po stronach, funkcjach, * longpressach i gestach playera. Pokazuje się raz (flaga goon.onboarding_seen_v1 * w SecureStore); replay z ustawień przez onboardingBus. Montowany raz nad * nawigacją (App.tsx), niezależny od ekranów — slajdy opisują UI ilustracyjnie, * bez spotlightowania realnych elementów (odporne na zmiany layoutu). * * Treść trzymać ZGODNĄ z realnymi gestami/akcjami (patrz SceneActionsContext, * PlayerScreen gesty, SceneDetail performer long-press, navigation TopTabs). */ import React from 'react'; import { Modal, NativeScrollEvent, NativeSyntheticEvent, Pressable, ScrollView, StyleSheet, Text, View, } from 'react-native'; import { NEWEST_CHANGELOG_ID } from '../changelog'; import { onReplay } from '../lib/onboardingBus'; import { getOnboardingSeen, setLastSeenChangelog, setOnboardingSeen } from '../storage'; import { theme } from '../theme'; type Slide = { icon: string; title: string; lines: string[] }; const SLIDES: Slide[] = [ { icon: '👋', title: 'Welcome to Goon', lines: [ 'Scenes and movies pulled together from 30+ sites — browse and play in one place.', 'Quick tour, about 30 seconds. Swipe or tap Next.', ], }, { icon: '🗂️', title: 'Three tabs up top', lines: [ 'Scenes — individual clips.', 'Movies — full-length, often multi-part.', 'Sites — browse by source, each rated 0–5★.', ], }, { icon: '🔎', title: 'Find what you want', lines: [ 'Search, then Filter by length, tags, studio, performer or sort.', 'Tap ☆ Save next to the search box to reuse a query as a chip.', 'From Scenes you can jump to Performers, Tags and Favorites.', ], }, { icon: '👆', title: 'Long-press for more', lines: [ 'Hold a scene tile → Hide it, or mark it as a duplicate.', 'Hold a performer on a scene → remove a wrong tag.', 'Hold a play link → open in browser (diagnostics) or mark it broken.', ], }, { icon: '▶️', title: 'Player gestures', lines: [ 'Tap once → show or hide the controls.', 'Double-tap left / right → skip back / forward 15s.', 'Swipe across the video → scrub to any point.', 'It starts muted — tap the speaker for sound.', ], }, { icon: '⭐', title: 'Make it yours', lines: [ 'Heart scenes, movies and performers — they collect in Favorites.', 'Settings ⚙ → Hidden content hides tags, performers or studios. Grid columns and a PIN lock live there too.', 'Hit a problem? The ? button sends a report — and you can read replies.', ], }, { icon: '✅', title: "You're set", lines: [ 'On Sites, the ★ rating tells you what’s fresh and what actually plays — tap the stars for the breakdown.', 'Replay this tour anytime: Settings ⚙ → Replay tutorial.', ], }, ]; export function OnboardingModal() { const [visible, setVisible] = React.useState(false); const [index, setIndex] = React.useState(0); const scrollRef = React.useRef(null); // Szerokość STRONY = realna szerokość viewportu ScrollView (mierzona onLayout), // NIE szerokość ekranu — karta ma marginesy + padding, więc ekran ≠ strona. // Użycie szerokości ekranu rozjeżdżało paging vs scrollTo vs index (ostatni // slajd pokazywał "Next" zamiast "Start browsing"). const [pageW, setPageW] = React.useState(0); // Pierwsze odpalenie: pokaż jeśli jeszcze nie widziany. React.useEffect(() => { let cancelled = false; getOnboardingSeen().then((seen) => { if (!cancelled && !seen) setVisible(true); }); return () => { cancelled = true; }; }, []); // Replay z ustawień. React.useEffect( () => onReplay(() => { setIndex(0); setVisible(true); // reset pozycji scrolla po otwarciu requestAnimationFrame(() => scrollRef.current?.scrollTo({ x: 0, animated: false })); }), [], ); const finish = React.useCallback(() => { setVisible(false); setOnboardingSeen(true).catch(() => {}); // Nowy user nie powinien po tutorialu dostać od razu "What's new" — oznacz // changelog jako widziany do najnowszego wpisu. if (NEWEST_CHANGELOG_ID) setLastSeenChangelog(NEWEST_CHANGELOG_ID).catch(() => {}); }, []); const goTo = React.useCallback( (i: number) => { const clamped = Math.max(0, Math.min(SLIDES.length - 1, i)); setIndex(clamped); if (pageW > 0) scrollRef.current?.scrollTo({ x: clamped * pageW, animated: true }); }, [pageW], ); const onScroll = (e: NativeSyntheticEvent) => { if (pageW <= 0) return; const i = Math.round(e.nativeEvent.contentOffset.x / pageW); if (i !== index) setIndex(i); }; if (!visible) return null; const isLast = index === SLIDES.length - 1; return ( GOON {isLast ? '' : 'Skip'} setPageW(e.nativeEvent.layout.width)} style={styles.pager} > {SLIDES.map((s, i) => ( 0 && { width: pageW }]}> {s.icon} {s.title} {s.lines.map((ln, j) => ( • {ln} ))} ))} {SLIDES.map((_, i) => ( ))} goTo(index - 1)} disabled={index === 0} style={[styles.btnGhost, index === 0 && styles.btnHidden]} > Back (isLast ? finish() : goTo(index + 1))} style={styles.btnPrimary}> {isLast ? 'Start browsing' : 'Next'} ); } const styles = StyleSheet.create({ backdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.82)', alignItems: 'center', justifyContent: 'center', padding: 24, }, card: { width: '100%', backgroundColor: theme.bgElevated, borderRadius: 18, borderColor: theme.border, borderWidth: 1, paddingVertical: 18, paddingHorizontal: 24, }, topRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6, }, brand: { color: theme.accent, fontWeight: '900', letterSpacing: 2, fontSize: 13 }, skip: { color: theme.muted, fontSize: 14, fontWeight: '600', minWidth: 36, textAlign: 'right' }, pager: { flexGrow: 0 }, slide: { paddingTop: 14, paddingBottom: 8, minHeight: 250 }, icon: { fontSize: 44, marginBottom: 12 }, title: { color: theme.fg, fontSize: 22, fontWeight: '800', marginBottom: 16 }, lines: { gap: 12 }, lineRow: { flexDirection: 'row', gap: 10 }, bullet: { color: theme.accent, fontSize: 16, lineHeight: 22 }, lineText: { color: theme.fg, fontSize: 15, lineHeight: 22, flex: 1 }, dots: { flexDirection: 'row', justifyContent: 'center', gap: 6, marginVertical: 16 }, dot: { width: 7, height: 7, borderRadius: 4, backgroundColor: theme.border }, dotActive: { backgroundColor: theme.accent, width: 18 }, btnRow: { flexDirection: 'row', alignItems: 'center', gap: 12 }, btnGhost: { paddingVertical: 12, paddingHorizontal: 18, borderRadius: 12 }, btnHidden: { opacity: 0 }, btnGhostText: { color: theme.muted, fontSize: 15, fontWeight: '700' }, btnPrimary: { flex: 1, backgroundColor: theme.accent, borderRadius: 12, paddingVertical: 13, alignItems: 'center', }, btnPrimaryText: { color: theme.fg, fontSize: 15, fontWeight: '800' }, });