diff --git a/mobile/src/screens/SceneDetailScreen.tsx b/mobile/src/screens/SceneDetailScreen.tsx index 620b8d8..960b8fe 100644 --- a/mobile/src/screens/SceneDetailScreen.tsx +++ b/mobile/src/screens/SceneDetailScreen.tsx @@ -134,6 +134,14 @@ export function SceneDetailScreen() { onError: (e) => Alert.alert('Thumbnail', e instanceof Error ? e.message : String(e)), }); + // Stan ładowania hero-miniatury: 'ok' (załadowała się — domyślnie, brak migotania + // przycisku), 'none' (brak URL), 'broken' (onError). Steruje widocznością przycisku + // Refresh. Reset przy zmianie sceny. + const [thumbState, setThumbState] = React.useState<'none' | 'ok' | 'broken'>('ok'); + React.useEffect(() => { + setThumbState('ok'); + }, [id]); + // Auto-enrich tags: search-only scrapery nie pobierają tagów z detail page. // Pornhat ma `js-ajax-tag` data-setup JSON; xhamster/xvideos/youporn/inne mają // dedicated tag_extract patterny. SceneDetail wywołuje 1 fetch → upsert do DB. @@ -235,30 +243,25 @@ export function SceneDetailScreen() { return ( - + - {/* Refresh tylko gdy BRAK dobrej miniatury (brak w ogóle lub rotting sxyprn/ - trafficdeposit) — feedback 26c114ed: na scenach z dobrą miniaturą przycisk - był zbędnym szumem. Dla zepsutych/stałych nadal się pojawia (cel d3376a71). */} - {(() => { - const hasTube = data.playback_sources.some((s) => s.origin?.startsWith('tube:')); - const hasGoodThumb = data.playback_sources.some( - (s) => s.thumbnail_url && !/trafficdeposit|sxyprn|\/proxy\/sxyprn-thumb/i.test(s.thumbnail_url), - ); - if (!hasTube || hasGoodThumb) return null; - return ( - refreshThumbMutation.mutate()} - disabled={refreshThumbMutation.isPending} - > - - {refreshThumbMutation.isPending ? 'Refreshing thumbnail…' : '↻ Refresh thumbnail'} - - - ); - })()} + {/* Refresh pokazujemy gdy wyświetlana miniatura faktycznie się NIE ładuje + (brak URL lub obraz padł — onError), nie tylko po obecności URL. Łączy + feedback 26c114ed (nie na każdej scenie z dobrą miniaturą) i ef0c6a5a + (thumbnail_url bywa obecny, ale obraz 404 → user widzi puste pole). */} + {data.playback_sources.some((s) => s.origin?.startsWith('tube:')) && + thumbState !== 'ok' ? ( + refreshThumbMutation.mutate()} + disabled={refreshThumbMutation.isPending} + > + + {refreshThumbMutation.isPending ? 'Refreshing thumbnail…' : '↻ Refresh thumbnail'} + + + ) : null} {(data.code || data.director) && ( @@ -379,7 +382,13 @@ export function SceneDetailScreen() { ); } -function Hero({ data }: { data: SceneOut }) { +function Hero({ + data, + onThumbState, +}: { + data: SceneOut; + onThumbState?: (s: 'none' | 'ok' | 'broken') => void; +}) { const { width } = useWindowDimensions(); const heroHeight = Math.round(width * (9 / 16)); @@ -388,6 +397,10 @@ function Hero({ data }: { data: SceneOut }) { const staticUrl = data.playback_sources.find((s) => s.thumbnail_url)?.thumbnail_url; const url = animatedUrl ?? staticUrl; + React.useEffect(() => { + if (!url) onThumbState?.('none'); + }, [url, onThumbState]); + const minutes = data.duration_sec ? Math.floor(data.duration_sec / 60) : null; const meta = [ data.release_date ?? null, @@ -415,6 +428,8 @@ function Hero({ data }: { data: SceneOut }) { style={StyleSheet.absoluteFill} contentFit="cover" transition={200} + onLoad={() => onThumbState?.('ok')} + onError={() => onThumbState?.('broken')} />