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[] = [ 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', id: '2026-09-07g',
date: 'September 2026', date: 'September 2026',

View file

@ -28,6 +28,7 @@ import { ErrorBoundary } from '../ErrorBoundary';
import type { RootStackParamList } from '../navigation'; import type { RootStackParamList } from '../navigation';
import { theme } from '../theme'; import { theme } from '../theme';
import type { SceneOut } from '../types'; import type { SceneOut } from '../types';
import { IconButton, Toolbar } from '../ui';
type Tab = 'scenes' | 'movies'; type Tab = 'scenes' | 'movies';
@ -105,15 +106,8 @@ export function PerformerScenesScreen() {
}, },
}); });
// Top tagi/kategorie performera (chipsy w headerze, bug-report 1a4bf258, zastąpiły // Chipsy top-tagow performera (bug-report 1a4bf258) zniknely 2026-09-07 razem z
// dev-only przycisk Re-scrape). Backend agreguje scene_tags po scenach z żywym // wierszem akcji: tagi sa w filtrze, a naglowek to jeden wiersz ikon jak na Scenes.
// 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,
});
const onHide = () => { const onHide = () => {
Alert.alert( Alert.alert(
@ -293,68 +287,32 @@ export function PerformerScenesScreen() {
onRefresh={scenesQuery.refetch} onRefresh={scenesQuery.refetch}
ListHeaderComponent={ ListHeaderComponent={
<View> <View>
<View style={styles.header}> {/* Jeden wiersz (2026-09-07): licznik scen po lewej, po prawej ikony
<Text style={styles.subtitle}> „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 {scenesQuery.data
? `${scenesTotal}${scenesSuffix} ${scenesTotal === 1 ? 'scene' : 'scenes'}` ? `${scenesTotal}${scenesSuffix} ${scenesTotal === 1 ? 'scene' : 'scenes'}`
: ' '} : ' '}
</Text> </Text>
</View> {refreshMutation.isPending ? (
{/* Bug-report 2026-05-17 (f3f019d0): "elementy obsługowe zajmują za <ActivityIndicator color={theme.accentText} style={{ width: 44 }} />
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; <IconButton
Search zostaje, pod nim chipsy top-tagów performera (tap → TagScenes). */} icon="magnify-scan"
<View style={styles.actionRow}> accessibilityLabel="Search more scenes"
<Pressable onPress={() => refreshMutation.mutate()}
style={[ />
styles.actionBtn, )}
refreshMutation.isPending && styles.refreshBtnLoading, <IconButton
]} icon="filter-variant"
onPress={() => refreshMutation.mutate()} accessibilityLabel="Filters"
disabled={refreshMutation.isPending} active={filterActiveCount > 0}
> badge={filterActiveCount}
{refreshMutation.isPending ? (
<ActivityIndicator color={theme.accent} size="small" />
) : (
<Text style={styles.actionBtnTextPrimary}>↻ Search more scenes</Text>
)}
</Pressable>
<Pressable
style={[styles.actionBtn, filterActiveCount > 0 && styles.actionBtnActive]}
onPress={() => setFilterOpen(true)} onPress={() => setFilterOpen(true)}
> />
<Text style={styles.actionBtnTextPrimary}> </Toolbar>
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>
)}
</View> </View>
} }
ListEmptyComponent={ 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 // 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. // które go używają) żeby uniknąć ref-before-decl errors w lint.
const initialHostRef = React.useRef<string | null>(null); 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(() => { React.useEffect(() => {
webFirstLoadDoneRef.current = false;
try { try {
initialHostRef.current = new URL(url).hostname; initialHostRef.current = new URL(url).hostname;
} catch { } catch {
@ -1989,6 +1996,11 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
// WebView blokuje cross-origin nav (popunder ad opens new tab/page poza embeddem). // WebView blokuje cross-origin nav (popunder ad opens new tab/page poza embeddem).
// Pierwsze ładowanie (`url`) ma `navigationType=undefined` lub `other`, przepuszczamy. // Pierwsze ładowanie (`url`) ma `navigationType=undefined` lub `other`, przepuszczamy.
// Cokolwiek innego (klik, JS redirect) z innego origin → block i log. // 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). // initialHostRef już declared wyżej (przed useEffect który robi nav.replace).
const onShouldStartLoad = React.useCallback( const onShouldStartLoad = React.useCallback(
(req: { url: string; navigationType?: string; isTopFrame?: boolean }) => { (req: { url: string; navigationType?: string; isTopFrame?: boolean }) => {
@ -2008,12 +2020,18 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
} }
return true; 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, // Top-frame nav: pozwól na same-eTLD+1 (paradisehill.cc → en.paradisehill.cc,
// pervl3.io → pervl5.io); blokuj cross-domain nav (adware popundery). // pervl3.io → pervl5.io); blokuj cross-domain nav (adware popundery).
try { try {
const target = new URL(req.url).hostname.toLowerCase(); const target = new URL(req.url).hostname.toLowerCase();
const initial = initialHostRef.current?.toLowerCase(); const initial = initialHostRef.current?.toLowerCase();
if (initial && target === initial) return true; 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) { if (initial) {
const tParts = target.split('.'); const tParts = target.split('.');
const iParts = initial.split('.'); const iParts = initial.split('.');
@ -2074,6 +2092,9 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
injectedJavaScriptBeforeContentLoaded={INJECTED_JS} injectedJavaScriptBeforeContentLoaded={INJECTED_JS}
onMessage={onMessage} onMessage={onMessage}
onShouldStartLoadWithRequest={onShouldStartLoad} onShouldStartLoadWithRequest={onShouldStartLoad}
onLoadEnd={() => {
webFirstLoadDoneRef.current = true;
}}
onHttpError={onHttpError} onHttpError={onHttpError}
setSupportMultipleWindows={false} setSupportMultipleWindows={false}
allowsInlineMediaPlayback allowsInlineMediaPlayback

View file

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