import { DefaultTheme, NavigationContainer, useNavigationContainerRef, } from '@react-navigation/native'; import { BugReportFAB } from './components/BugReportFAB'; 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 { 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 { StudioScenesScreen } from './screens/StudioScenesScreen'; import { TagScenesScreen } from './screens/TagScenesScreen'; import { TagsScreen } from './screens/TagsScreen'; import { fonts, theme } from './theme'; export type RootStackParamList = { Scenes: undefined; Movies: undefined; Sites: 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: 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 }; // 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 }; Favorites: undefined; Tags: undefined; TagScenes: { slug: string; name: string }; 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'; // 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'; function TopTabs({ current, onNavigate, }: { current: TopTab; onNavigate: (tab: TopTab) => void; }) { const tabs: TopTab[] = ['Scenes', 'Movies', 'Sites']; return ( {/* Logo (mark) — spójne z ekranem logowania; węższe od wordmarku, więc Scenes/Movies/Sites + akcje po prawej mieszczą się bez nachodzenia na wąskich telefonach. */} {tabs.map((t) => { const active = t === current; return ( (active ? null : onNavigate(t))} hitSlop={12}> {t} ); })} ); } 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 ( ({ title: '', headerLeft: () => ( navigation.replace(t)} /> ), headerRight: () => ( navigation.navigate('Donate')} hitSlop={12}> navigation.navigate('AppLockSettings')} hitSlop={12} > 🚪 ), })} /> ({ title: '', headerBackVisible: false, headerLeft: () => ( navigation.replace(t)} /> ), headerRight: () => ( 🚪 ), })} /> ({ title: '', headerBackVisible: false, headerLeft: () => ( navigation.replace(t)} /> ), headerRight: () => ( 🚪 ), })} /> {() => } ); }