fix(player): hide ad-heavy WebView behind opaque cover until stream scraped

User bug: opening a WebView-fallback scene (yespornvip etc.) shows the host's
ad-heavy page while INJECTED_JS auto-plays + scrapes the stream url in the
background. User sees ads instead of a loading state.

Render an opaque cover (theme.bg + spinner "Loading video…") over the WebView
while !extractedUrl. The WebView is still laid out and painted underneath, so
media keeps playing (autoplay via mediaPlaybackRequiresUserAction=false) and the
performance-scan picks up the CDN url — but the user only ever sees a loading
screen, then the native player. Applies to every WebView-fallback host.

Safety: if no stream is scraped within 15s (host needs a real tap to start),
reveal the WebView so the user can interact manually — no worse than before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jtrzupek 2026-05-31 21:47:35 +02:00
parent bb4a17ae79
commit 00ea8c3fd4

View file

@ -988,6 +988,17 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
const [resolveError, setResolveError] = React.useState<string | null>(null); const [resolveError, setResolveError] = React.useState<string | null>(null);
const [skipResolve, setSkipResolve] = React.useState(false); const [skipResolve, setSkipResolve] = React.useState(false);
const [resolveAttempted, setResolveAttempted] = React.useState(false); const [resolveAttempted, setResolveAttempted] = React.useState(false);
// Anti-ads: trzymamy nieprzezroczysty cover NA WebView dopóki INJECTED_JS nie
// wyciągnie URL — user nie widzi ad-heavy strony hostera (bug yespornvip "otwiera
// reklamy" 2026-05-31). WebView gra pod spodem (jest malowany, media leci, scrape
// łapie CDN). Jeśli po REVEAL_AFTER_MS autoplay nie zaskoczył (host wymaga gestu) —
// odsłaniamy WebView żeby user mógł ręcznie tapnąć play (graceful fallback).
const [revealEmbed, setRevealEmbed] = React.useState(false);
React.useEffect(() => {
if (extractedUrl) return;
const t = setTimeout(() => setRevealEmbed(true), 15000);
return () => clearTimeout(t);
}, [extractedUrl]);
// Stage 0.8: mobile-side hoster resolvery. Mobile IP usera unika Cloudflare // Stage 0.8: mobile-side hoster resolvery. Mobile IP usera unika Cloudflare
// Turnstile / CAPTCHA gate który blokuje Hetzner VPS — embed page renderuje // Turnstile / CAPTCHA gate który blokuje Hetzner VPS — embed page renderuje
@ -1206,6 +1217,12 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
</View> </View>
)} )}
/> />
{!extractedUrl && !revealEmbed && (
<View style={styles.coverOverlay}>
<ActivityIndicator color={theme.fg} size="large" />
<Text style={styles.overlayText}>Loading video</Text>
</View>
)}
{extractedUrl && ( {extractedUrl && (
<Pressable <Pressable
style={styles.extractBanner} style={styles.extractBanner}
@ -1241,6 +1258,20 @@ const styles = StyleSheet.create({
backgroundColor: 'rgba(0,0,0,0.6)', backgroundColor: 'rgba(0,0,0,0.6)',
paddingHorizontal: 32, paddingHorizontal: 32,
}, },
// Nieprzezroczysty cover na WebView — ukrywa ad-heavy stronę hostera podczas gdy
// INJECTED_JS auto-play + scrape działają pod spodem. Solidne tło (nie alpha) =
// zero podglądu reklam.
coverOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: theme.bg,
paddingHorizontal: 32,
},
overlayText: { color: theme.fg, marginTop: 12, fontSize: 13, textAlign: 'center' }, overlayText: { color: theme.fg, marginTop: 12, fontSize: 13, textAlign: 'center' },
errorTitle: { color: theme.bad, fontSize: 18, fontWeight: '700', marginBottom: 8 }, errorTitle: { color: theme.bad, fontSize: 18, fontWeight: '700', marginBottom: 8 },
errorBody: { color: theme.fg, fontSize: 14, marginBottom: 16, textAlign: 'center' }, errorBody: { color: theme.fg, fontSize: 14, marginBottom: 16, textAlign: 'center' },