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:
parent
81d617efc2
commit
b66dd99eba
1 changed files with 38 additions and 23 deletions
|
|
@ -134,6 +134,14 @@ export function SceneDetailScreen() {
|
||||||
onError: (e) => Alert.alert('Thumbnail', e instanceof Error ? e.message : String(e)),
|
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.
|
// 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ą
|
// 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.
|
// dedicated tag_extract patterny. SceneDetail wywołuje 1 fetch → upsert do DB.
|
||||||
|
|
@ -235,19 +243,15 @@ export function SceneDetailScreen() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
|
<ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
|
||||||
<Hero data={data} />
|
<Hero data={data} onThumbState={setThumbState} />
|
||||||
|
|
||||||
<View style={styles.body}>
|
<View style={styles.body}>
|
||||||
{/* Refresh tylko gdy BRAK dobrej miniatury (brak w ogóle lub rotting sxyprn/
|
{/* Refresh pokazujemy gdy wyświetlana miniatura faktycznie się NIE ładuje
|
||||||
trafficdeposit) — feedback 26c114ed: na scenach z dobrą miniaturą przycisk
|
(brak URL lub obraz padł — onError), nie tylko po obecności URL. Łączy
|
||||||
był zbędnym szumem. Dla zepsutych/stałych nadal się pojawia (cel d3376a71). */}
|
feedback 26c114ed (nie na każdej scenie z dobrą miniaturą) i ef0c6a5a
|
||||||
{(() => {
|
(thumbnail_url bywa obecny, ale obraz 404 → user widzi puste pole). */}
|
||||||
const hasTube = data.playback_sources.some((s) => s.origin?.startsWith('tube:'));
|
{data.playback_sources.some((s) => s.origin?.startsWith('tube:')) &&
|
||||||
const hasGoodThumb = data.playback_sources.some(
|
thumbState !== 'ok' ? (
|
||||||
(s) => s.thumbnail_url && !/trafficdeposit|sxyprn|\/proxy\/sxyprn-thumb/i.test(s.thumbnail_url),
|
|
||||||
);
|
|
||||||
if (!hasTube || hasGoodThumb) return null;
|
|
||||||
return (
|
|
||||||
<Pressable
|
<Pressable
|
||||||
style={styles.refreshThumbBtn}
|
style={styles.refreshThumbBtn}
|
||||||
onPress={() => refreshThumbMutation.mutate()}
|
onPress={() => refreshThumbMutation.mutate()}
|
||||||
|
|
@ -257,8 +261,7 @@ export function SceneDetailScreen() {
|
||||||
{refreshThumbMutation.isPending ? 'Refreshing thumbnail…' : '↻ Refresh thumbnail'}
|
{refreshThumbMutation.isPending ? 'Refreshing thumbnail…' : '↻ Refresh thumbnail'}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
) : null}
|
||||||
})()}
|
|
||||||
|
|
||||||
{(data.code || data.director) && (
|
{(data.code || data.director) && (
|
||||||
<View style={styles.metaRow}>
|
<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 { width } = useWindowDimensions();
|
||||||
const heroHeight = Math.round(width * (9 / 16));
|
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 staticUrl = data.playback_sources.find((s) => s.thumbnail_url)?.thumbnail_url;
|
||||||
const url = animatedUrl ?? staticUrl;
|
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 minutes = data.duration_sec ? Math.floor(data.duration_sec / 60) : null;
|
||||||
const meta = [
|
const meta = [
|
||||||
data.release_date ?? null,
|
data.release_date ?? null,
|
||||||
|
|
@ -415,6 +428,8 @@ function Hero({ data }: { data: SceneOut }) {
|
||||||
style={StyleSheet.absoluteFill}
|
style={StyleSheet.absoluteFill}
|
||||||
contentFit="cover"
|
contentFit="cover"
|
||||||
transition={200}
|
transition={200}
|
||||||
|
onLoad={() => onThumbState?.('ok')}
|
||||||
|
onError={() => onThumbState?.('broken')}
|
||||||
/>
|
/>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={['transparent', 'rgba(8,9,15,0.55)', theme.bg]}
|
colors={['transparent', 'rgba(8,9,15,0.55)', theme.bg]}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue