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:
parent
e13463cba4
commit
b0350baff0
4 changed files with 73 additions and 79 deletions
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
{/* 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 ? (
|
{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>
|
<IconButton
|
||||||
<Pressable
|
icon="filter-variant"
|
||||||
style={[styles.actionBtn, filterActiveCount > 0 && styles.actionBtnActive]}
|
accessibilityLabel="Filters"
|
||||||
|
active={filterActiveCount > 0}
|
||||||
|
badge={filterActiveCount}
|
||||||
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={
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue