fix(player): przekierowanie z pierwszego adresu przechodzi (myvidplay -> playmogo), rodzina dooda jako jeden host; jeden wiersz ikon na ekranie strony i performera

- WebView: przed pierwszym onLoadEnd kazda nawigacja top-frame to lancuch redirectow
  serwera, nie popunder; po zaladowaniu przeskoki w obrebie rodziny DoodStream tez OK
- SiteScenes: wyszukiwarka w obrebie strony + ikona tagow z licznikiem + X
- PerformerScenes: licznik scen + ikony szukaj-wiecej/filtr w jednym wierszu, chipsy
  top-tagow wyciete (tagi sa w filtrze), zbedne zapytanie performer-tags usuniete
- changelog 2026-09-07h, OTA 49cda698 na 1.3
This commit is contained in:
goon-foss 2026-09-07 15:13:43 +02:00
parent e13463cba4
commit b0350baff0
4 changed files with 73 additions and 79 deletions

View file

@ -29,6 +29,14 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
{
id: '2026-09-07h',
date: 'September 2026',
items: [
'Movies on myvidplay and doply play again. Those hosts redirect to playmogo, and the player was treating that redirect as a pop-up and blocking it, so you got a blank screen. The human check on playmogo still needs your tap.',
'Site and performer pages use the same single row of icons as the main sections: search plus tag filter on a site, "search more" plus filter next to the scene count on a performer. The tag chips row on performer pages is gone, tags live in the filter.',
],
},
{
id: '2026-09-07g',
date: 'September 2026',

View file

@ -28,6 +28,7 @@ import { ErrorBoundary } from '../ErrorBoundary';
import type { RootStackParamList } from '../navigation';
import { theme } from '../theme';
import type { SceneOut } from '../types';
import { IconButton, Toolbar } from '../ui';
type Tab = 'scenes' | 'movies';
@ -105,15 +106,8 @@ export function PerformerScenesScreen() {
},
});
// Top tagi/kategorie performera (chipsy w headerze, bug-report 1a4bf258, zastąpiły
// dev-only przycisk Re-scrape). Backend agreguje scene_tags po scenach z żywym
// playbackiem. Tap → TagScenes. Rescrape (bulk enrich miniaturek/tagów) został
// przeniesiony do flow per-scene (SceneDetail), na liście performera był devowy szum.
const tagsQuery = useQuery({
queryKey: ['performer-tags', id],
queryFn: () => client.performerTags(id, 18),
staleTime: 5 * 60_000,
});
// Chipsy top-tagow performera (bug-report 1a4bf258) zniknely 2026-09-07 razem z
// wierszem akcji: tagi sa w filtrze, a naglowek to jeden wiersz ikon jak na Scenes.
const onHide = () => {
Alert.alert(
@ -293,68 +287,32 @@ export function PerformerScenesScreen() {
onRefresh={scenesQuery.refetch}
ListHeaderComponent={
<View>
<View style={styles.header}>
<Text style={styles.subtitle}>
{/* Jeden wiersz (2026-09-07): licznik scen po lewej, po prawej ikony
„szukaj wiecej" i filtr z licznikiem. Chipsy tagow pod spodem
zniknely, tagi sa w filtrze. */}
<Toolbar>
<Text style={[styles.subtitle, { flex: 1 }]} numberOfLines={1}>
{scenesQuery.data
? `${scenesTotal}${scenesSuffix} ${scenesTotal === 1 ? 'scene' : 'scenes'}`
: ' '}
</Text>
</View>
{/* Bug-report 2026-05-17 (f3f019d0): "elementy obsługowe zajmują za
dużo ekranu" + 1a4bf258: "Re-scrape mógłby zniknąć, za to tagi/
kategorie by mogły". Re-scrape (dev-only bulk enrich) usunięty;
Search zostaje, pod nim chipsy top-tagów performera (tap → TagScenes). */}
<View style={styles.actionRow}>
<Pressable
style={[
styles.actionBtn,
refreshMutation.isPending && styles.refreshBtnLoading,
]}
onPress={() => refreshMutation.mutate()}
disabled={refreshMutation.isPending}
>
{refreshMutation.isPending ? (
<ActivityIndicator color={theme.accent} size="small" />
<ActivityIndicator color={theme.accentText} style={{ width: 44 }} />
) : (
<Text style={styles.actionBtnTextPrimary}>↻ Search more scenes</Text>
<IconButton
icon="magnify-scan"
accessibilityLabel="Search more scenes"
onPress={() => refreshMutation.mutate()}
/>
)}
</Pressable>
<Pressable
style={[styles.actionBtn, filterActiveCount > 0 && styles.actionBtnActive]}
<IconButton
icon="filter-variant"
accessibilityLabel="Filters"
active={filterActiveCount > 0}
badge={filterActiveCount}
onPress={() => setFilterOpen(true)}
>
<Text style={styles.actionBtnTextPrimary}>
Filter{filterActiveCount > 0 ? ` ${filterActiveCount}` : ''}
</Text>
</Pressable>
</View>
{tab === 'scenes' && !!tagsQuery.data?.items.length && (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.tagRow}
>
{tagsQuery.data.items.map((t) => {
const active = selectedTag?.slug === t.slug;
return (
<Pressable
key={t.id}
style={[styles.tagChip, active && styles.tagChipActive]}
onPress={() =>
setSelectedTag(active ? null : { slug: t.slug, name: t.name })
}
>
<Text
style={[styles.tagChipText, active && styles.tagChipTextActive]}
numberOfLines={1}
>
{active ? `${t.name} ✕` : t.name}
</Text>
</Pressable>
);
})}
</ScrollView>
)}
/>
</Toolbar>
</View>
}
ListEmptyComponent={

View file

@ -1922,7 +1922,14 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
// używany do allow-listy redirectów same-eTLD+1. DECLARED HERE (przed useEffectami
// które go używają) żeby uniknąć ref-before-decl errors w lint.
const initialHostRef = React.useRef<string | null>(null);
// Czy WebView skonczyl ladowac JAKAKOLWIEK strone. Do tego momentu kazda nawigacja
// top-frame to lancuch przekierowan serwera z pierwszego adresu (myvidplay.com 301 ->
// playmogo.com), nie popunder: popunder potrzebuje zaladowanej strony z JS. Blokada
// cross-domain przed pierwszym zaladowaniem zostawiala pusty ekran na kazdym
// hosterze, ktory zmienil domene (zgloszenie 1278eb49, freeomovie/myvidplay).
const webFirstLoadDoneRef = React.useRef(false);
React.useEffect(() => {
webFirstLoadDoneRef.current = false;
try {
initialHostRef.current = new URL(url).hostname;
} catch {
@ -1989,6 +1996,11 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
// WebView blokuje cross-origin nav (popunder ad opens new tab/page poza embeddem).
// Pierwsze ładowanie (`url`) ma `navigationType=undefined` lub `other`, przepuszczamy.
// Cokolwiek innego (klik, JS redirect) z innego origin → block i log.
// Ta sama lista co w doodstream.ts DOOD_HOSTS, tu jako regex po hoscie (bez importu
// modulu ladowanego dynamicznie w resolverze).
const isDoodHost = (h: string) =>
/vik[i1]ngfile\.|playmogo\.|dood|d0000d|dooood|do0od|do7go|ds2play|doply\.|myvidplay\./i.test(h);
// initialHostRef już declared wyżej (przed useEffect który robi nav.replace).
const onShouldStartLoad = React.useCallback(
(req: { url: string; navigationType?: string; isTopFrame?: boolean }) => {
@ -2008,12 +2020,18 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
}
return true;
}
// Przed pierwszym zaladowaniem strony to jeszcze przekierowania serwera z
// pierwszego adresu (patrz webFirstLoadDoneRef), przepusc.
if (!webFirstLoadDoneRef.current) return true;
// Top-frame nav: pozwól na same-eTLD+1 (paradisehill.cc → en.paradisehill.cc,
// pervl3.io → pervl5.io); blokuj cross-domain nav (adware popundery).
try {
const target = new URL(req.url).hostname.toLowerCase();
const initial = initialHostRef.current?.toLowerCase();
if (initial && target === initial) return true;
// Rodzina DoodStreama (myvidplay/doply -> playmogo) to jeden hoster pod kilkoma
// domenami; przeskok miedzy nimi po zaladowaniu (np. po Turnstile) tez jest legalny.
if (initial && isDoodHost(target) && isDoodHost(initial)) return true;
if (initial) {
const tParts = target.split('.');
const iParts = initial.split('.');
@ -2074,6 +2092,9 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
injectedJavaScriptBeforeContentLoaded={INJECTED_JS}
onMessage={onMessage}
onShouldStartLoadWithRequest={onShouldStartLoad}
onLoadEnd={() => {
webFirstLoadDoneRef.current = true;
}}
onHttpError={onHttpError}
setSupportMultipleWindows={false}
allowsInlineMediaPlayback

View file

@ -29,6 +29,7 @@ import { usePreferences } from '../PreferencesContext';
import type { RootStackParamList } from '../navigation';
import { theme } from '../theme';
import type { SceneOut, TagOut } from '../types';
import { IconButton, SearchInput, Toolbar } from '../ui';
export function SiteScenesScreen() {
const client = useClient();
@ -46,6 +47,13 @@ export function SiteScenesScreen() {
// stronach Sites". Multi-select AND (passed as comma-CSV do `tags` query).
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [filterOpen, setFilterOpen] = useState(false);
// Szukanie w obrebie strony (2026-09-07): ten sam wiersz narzedzi co na Scenes.
const [q, setQ] = useState('');
const [debouncedQ, setDebouncedQ] = useState('');
React.useEffect(() => {
const t = setTimeout(() => setDebouncedQ(q.trim()), 350);
return () => clearTimeout(t);
}, [q]);
const PER_PAGE = 50;
const {
@ -58,10 +66,11 @@ export function SiteScenesScreen() {
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['site-scenes', origin, selectedTags.sort().join(',')],
queryKey: ['site-scenes', origin, selectedTags.sort().join(','), debouncedQ],
queryFn: ({ pageParam = 1 }) =>
client.listScenes({
origin,
q: debouncedQ || undefined,
tags: selectedTags.length > 0 ? selectedTags : undefined,
sort: 'release_date',
// 0 = nie nakładaj progu długości. User wszedł JAWNIE w tę pozycję, więc
@ -87,21 +96,19 @@ export function SiteScenesScreen() {
return (
<View style={styles.container}>
<View style={styles.toolbar}>
<Pressable
style={[styles.filterBtn, selectedTags.length > 0 && styles.filterBtnActive]}
<Toolbar>
<SearchInput value={q} onChangeText={setQ} placeholder={`search ${name}…`} />
<IconButton
icon="tag-multiple-outline"
accessibilityLabel="Filter by tags"
active={selectedTags.length > 0}
badge={selectedTags.length}
onPress={() => setFilterOpen(true)}
>
<Text style={styles.filterBtnText}>
Tags{selectedTags.length > 0 ? ` ${selectedTags.length}` : ''}
</Text>
</Pressable>
/>
{selectedTags.length > 0 ? (
<Pressable style={styles.clearBtn} onPress={() => setSelectedTags([])}>
<Text style={styles.clearBtnText}>Clear</Text>
</Pressable>
<IconButton icon="close" accessibilityLabel="Clear tags" onPress={() => setSelectedTags([])} />
) : null}
</View>
</Toolbar>
<TagPickerModal
visible={filterOpen}