import { DefaultTheme, NavigationContainer, useNavigationContainerRef, } from '@react-navigation/native'; import { BugReportFAB } from './components/BugReportFAB'; import { TopTabsBar } from './components/TopTabsBar'; import { GoonMark } from './components/GoonWordmark'; import { GoonClient } from './api'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import React from 'react'; import { Pressable, Text, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { AppLockSettingsScreen } from './screens/AppLockSettingsScreen'; import { BlacklistScreen } from './screens/BlacklistScreen'; import { DiagnosticBrowserScreen } from './screens/DiagnosticBrowserScreen'; import { DonateScreen } from './screens/DonateScreen'; import { FavoritesScreen } from './screens/FavoritesScreen'; import { MovieDetailScreen } from './screens/MovieDetailScreen'; import { MoviesScreen } from './screens/MoviesScreen'; import { PerformerScenesScreen } from './screens/PerformerScenesScreen'; import { PerformersScreen } from './screens/PerformersScreen'; import { PlayerScreen } from './screens/PlayerScreen'; import { ScenesScreen } from './screens/ScenesScreen'; import { SceneDetailScreen } from './screens/SceneDetailScreen'; import { SiteScenesScreen } from './screens/SiteScenesScreen'; import { SitesScreen } from './screens/SitesScreen'; import { StudioMoviesScreen } from './screens/StudioMoviesScreen'; import { StudioScenesScreen } from './screens/StudioScenesScreen'; import { TagScenesScreen } from './screens/TagScenesScreen'; import { TagsScreen } from './screens/TagsScreen'; import { fonts, theme } from './theme'; export type RootStackParamList = { // `tab` odkłada HomeScreen (setParams) wyłącznie po to, żeby BugReportFAB wiedział, // na której zakładce user był: zakładki to stan lokalny, nie osobne trasy. Scenes: { tab?: string } | undefined; // UWAGA: Movies/Sites/JAV/Anime NIE sa zarejestrowane w navigatorze od 2026-08-18 — // zakladki to stan lokalny HomeScreen (trasa 'Scenes'). Klucze zostaja, bo ekrany // typuja useNavigation<...,'Movies'> itd.; nawigowanie do nich rzuciloby bledem. Movies: undefined; Sites: undefined; // JAV: osobna sekcja, ScenesScreen z paramem jav → backend ?jav=true. JAV: { jav: true } | undefined; // Anime: hentai i 3D, ScenesScreen z paramem vertical → backend ?vertical=animation. // Osobna sekcja z tego samego powodu co JAV: inny namespace tytułów, inne słownictwo // tagów (85% tagów animacji nie występuje nigdzie indziej), zero dedupu z resztą. Anime: { vertical: 'animation' } | undefined; // `origin`: raw playback_source.origin (np. 'tube:hqpornercom'). Idzie do // listScenes({origin}), backend robi substring match. `name`: display name // do title bara (np. 'hqporner.com'). SiteScenes: { origin: string; name: string }; MovieDetail: { id: string }; // autoplay: wejście z ▶ na kafelku (quick-play), SceneDetail sam odpala najlepsze źródło. SceneDetail: { id: string; autoplay?: boolean }; Performers: { vertical?: string } | undefined; // `seenSince` (ISO timestamp): jeśli ten performer jest favorited, FavoritesScreen // przekazuje `last_seen_at` SPRZED markFavoriteSeen. Sceny z `created_at > seenSince` // dostają NEW badge. PerformerScenes: { id: string; name: string; seenSince?: string; vertical?: string | null }; // StudioScenes: `id` to studio.slug (do filter scen `studio_slugs`), // `studioId` to UUID (do favorite/blacklist API). Backend filtruje scen po slug, // ale favorites + blacklist używają UUID. StudioScenes: { id: string; name: string; studioId: string; seenSince?: string; vertical?: string | null }; // StudioMovies: `slug` do filter movies `studio_slugs`, `studioId` (UUID) do favorite. StudioMovies: { slug: string; name: string; studioId: string }; Favorites: undefined; // `vertical` przynoszony z zakladki (Anime/JAV). Bez niego lista tagow pokazywala // slownictwo GLOWNEGO feedu, wiec z Anime kazdy tag prowadzil donikad. Tags: { vertical?: string } | undefined; TagScenes: { slug: string; name: string; vertical?: string | null }; AppLockSettings: undefined; Blacklist: undefined; Donate: undefined; // In-app incognito browser do diagnostyki hostera (long-press na linku playbacku). DiagnosticBrowser: { url: string }; Player: { url: string; sceneId: string; // 'tube:' źródła, telemetria odtwarzania zasilająca ranking źródeł. // Opcjonalne; brak → telemetria pomijana (canonical/non-tube). origin?: string; // Post page URL dla IP-bound tubów (sxyprn/eporner/fpoxxx), re-resolve on error. resolvePageUrl?: string; // 'movie' = MovieDetail wywołał Player z movieId zamiast sceneId. Backend // ma /movies/{id}/progress oddzielnie od /scenes/{id}/progress (2026-05-28). // Default 'scene' dla back-compat z istniejącymi nav callami. entityKind?: 'scene' | 'movie'; // playback_source.id, pozwala Playerowi oznaczyć źródło jako dead bez powrotu // do SceneDetail (gdy CDN zwróci 404/410 przy odtwarzaniu, sxyprn/fpo „404 // którego apka nie potrafi zidentyfikować”, bug a78cc3b6). Undefined dla // legacy/non-tube nav callów → przycisk „Mark broken” się nie pokazuje. playbackId?: string; durationSec?: number | null; refererHost?: string; title?: string; mode?: 'video' | 'webview'; // Lista jakości tego samego wideo (z resolve). Pozwala przełączyć jakość BEZ // wychodzenia z playera (user-request 89845707), player robi replace() i wraca // na zapamiętaną pozycję. Undefined = brak wyboru (jedno źródło). qualityLinks?: { label: string; url: string; headers?: Record }[]; // Headers do bezpośredniego fetchu CDN URL'a (Referer + User-Agent). Mobile // próbuje direct PIERWSZY, bandwidth idzie device→CDN, nie przez VPS proxy. // Backend zwraca te headers w StreamLink.headers przy resolve. headers?: Record; // Proxy URL fallback gdy direct fails (CDN IP-bound, np. fpo.xxx z kt_remote_ips). // Native player na error → replace na ten URL (proxy re-fetchuje z VPS IP). fallbackProxyUrl?: string; // Gdy native VideoView dostanie błąd (typowe dla IP-bound CDN URL'i z luluvids/ // iceyfile/tnmr, extracted z VPS IP, mobile fetch dostaje 403), apka przełącza // się automatycznie na WebView z tym URL'em, tam JS embed page sam pobiera // m3u8 we własnym kontekście (Cookies + JA3 + IP set ok). fallbackEmbedUrl?: string; }; }; const Stack = createNativeStackNavigator(); type TopTab = 'Scenes' | 'Movies' | 'Sites' | 'JAV' | 'Anime'; /** * Jeden ekran-gospodarz dla pięciu zakładek. Zakładka to STAN LOKALNY, nie trasa. * * Poprzednio każda zakładka była osobnym ekranem native stacka, a przełączenie robiło * `navigation.replace(t)` — wymieniało cały ekran RAZEM z nagłówkiem, więc pasek zakładek * montował się od nowa ze wskaźnikiem już na docelowej pozycji i nie mógł się animować * (user widział teleport, zgłoszenie 2026-08-18). Pasek musi przeżywać przełączenie, * żeby wskaźnik przejeżdżał, stąd jeden ekran-gospodarz. * * `key={tab}` na treści wymusza czysty remount przy przełączeniu — to samo, co robiło * `replace` (reset wyszukiwania/scrolla). Bez key przełączenie Scenes→JAV reużyłoby * instancję ScenesScreen i zostawiło stary tekst wyszukiwania w nowej sekcji. */ function HomeScreen({ navigation, onLogout, }: { navigation: { setParams: (p: { tab?: string }) => void; navigate: (r: 'Donate' | 'AppLockSettings') => void }; onLogout: () => void; }) { const insets = useSafeAreaInsets(); const tabs: TopTab[] = ['Scenes', 'Movies', 'Sites', 'JAV', 'Anime']; const [tab, setTab] = React.useState('Scenes'); // BugReportFAB czyta zakładkę z route.params.tab (nazwa trasy to zawsze 'Scenes'). React.useEffect(() => { navigation.setParams({ tab }); }, [tab, navigation]); return ( {/* Własny nagłówek zamiast natywnego (headerShown:false): natywny należy do ekranu, a ten pasek musi przeżywać przełączanie zakładek. Ikony po prawej są teraz jednakowe na każdej zakładce (wcześniej ♥ i ⚙ miał tylko Scenes). */} navigation.navigate('Donate')} hitSlop={12}> ♥ navigation.navigate('AppLockSettings')} hitSlop={12}> ⚙ 🚪 {tab === 'Movies' ? ( ) : tab === 'Sites' ? ( ) : tab === 'JAV' ? ( ) : tab === 'Anime' ? ( ) : ( )} ); } const navTheme = { ...DefaultTheme, dark: true, colors: { ...DefaultTheme.colors, background: theme.bg, card: theme.card, text: theme.fg, border: theme.border, primary: theme.accent, notification: theme.bad, }, }; interface AppNavigatorProps { onLogout: () => void; client: GoonClient | null; appVersion: string; } export function AppNavigator({ onLogout, client, appVersion }: AppNavigatorProps) { // Ref przypisany do NavigationContainer pozwala BugReportFAB (sibling // Stack.Navigator) wyciągnąć current route bez bycia w screen context, // useNavigationState wymagałby NavigationContext którego siblings nie mają. const navRef = useNavigationContainerRef(); return ( {({ navigation }) => } {() => } ); }