fix(mobile): show Refresh thumbnail when the hero image actually fails to load

The button keyed on thumbnail_url presence, but a URL can be present yet broken (hqfap
404 → blank hero, no button — report ef0c6a5a). Tie it to the hero Image load state
(onLoad ok / onError broken / no url none) and show Refresh only when the image is
broken or missing. Reconciles 26c114ed (hidden for good previews) with ef0c6a5a.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jtrzupek 2026-06-14 14:18:44 +02:00
parent 81d617efc2
commit b66dd99eba

View file

@ -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 (
<ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
<Hero data={data} />
<Hero data={data} onThumbState={setThumbState} />
<View style={styles.body}>
{/* 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 (
<Pressable
style={styles.refreshThumbBtn}
onPress={() => refreshThumbMutation.mutate()}
disabled={refreshThumbMutation.isPending}
>
<Text style={styles.refreshThumbText}>
{refreshThumbMutation.isPending ? 'Refreshing thumbnail…' : '↻ Refresh thumbnail'}
</Text>
</Pressable>
);
})()}
{/* 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' ? (
<Pressable
style={styles.refreshThumbBtn}
onPress={() => refreshThumbMutation.mutate()}
disabled={refreshThumbMutation.isPending}
>
<Text style={styles.refreshThumbText}>
{refreshThumbMutation.isPending ? 'Refreshing thumbnail…' : '↻ Refresh thumbnail'}
</Text>
</Pressable>
) : null}
{(data.code || data.director) && (
<View style={styles.metaRow}>
@ -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')}
/>
<LinearGradient
colors={['transparent', 'rgba(8,9,15,0.55)', theme.bg]}