From c0a5d02ece77fb7dcfdb91d6656c3be9f75fc987 Mon Sep 17 00:00:00 2001 From: goon-foss Date: Wed, 26 Aug 2026 14:19:36 +0200 Subject: [PATCH] feat(performers): pelny filtr scen na ekranie performera Prosba 39314780: "Add similar filters to when we search for videos by pornstars". Ekran performera dostaje TEN SAM modal co glowny feed (ScenesFilterModal), nie osobny: jakosc, minimalna dlugosc, sortowanie, zrodlo/hoster, tagi, studia oraz "Also featuring" (sceny takze z inna osoba). Chipsy top-tagow zostaja OBOK, jako jednodotykowy skrot na najczestszy przypadek; ich wybor scala sie z tagami z modala. Trzy roznice wzgledem glownego feedu, wszystkie zamierzone: * domyslne sortowanie po dacie premiery (tak ten ekran dzialal od zawsze), * prog dlugosci domyslnie 0 (wejscie w konkretnego performera nie ma ukrywac scen o nieznanej dlugosci, bo NULL nie przechodzi `>= 60`; wybor "5+ min" podnosi go jawnie), * "Also featuring" zamiast "Performers": backend laczy performer_ids koniunkcja, wiec wybrani sa dokladani do biezacego performera przez AND, nie zastepuja go. Modal dostal na to prop `performersLabel`. Licznik na przycisku liczy sie wzgledem domyslnych TEGO ekranu, inaczej has_playback=true podbijaloby "Filter 1" na czysto. Backend bez zmian: /scenes juz przyjmuje wszystkie te parametry. --- mobile/src/changelog.ts | 7 ++ mobile/src/screens/PerformerScenesScreen.tsx | 68 ++++++++++++++++++-- mobile/src/screens/ScenesFilterModal.tsx | 7 +- 3 files changed, 74 insertions(+), 8 deletions(-) diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts index 2af7524..0d81ae9 100644 --- a/mobile/src/changelog.ts +++ b/mobile/src/changelog.ts @@ -29,6 +29,13 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + id: '2026-08-26b', + date: 'August 2026', + items: [ + 'A performer page now has the same Filter as the main feed: quality, minimum length, sort order, source, tags, studios, and scenes that also feature another performer. The quick tag chips are still there for a one-tap filter.', + ], + }, { id: '2026-08-26a', date: 'August 2026', diff --git a/mobile/src/screens/PerformerScenesScreen.tsx b/mobile/src/screens/PerformerScenesScreen.tsx index 7606453..540e2f0 100644 --- a/mobile/src/screens/PerformerScenesScreen.tsx +++ b/mobile/src/screens/PerformerScenesScreen.tsx @@ -21,6 +21,7 @@ import { import { useClient } from '../ClientContext'; import { MoviePosterCard } from '../components/MoviePosterCard'; import { SceneTile, sceneGridProps } from '../components/SceneTile'; +import { DEFAULT_FILTER, FilterState, ScenesFilterModal } from './ScenesFilterModal'; import { sortNewFirst } from '../lib/newScenes'; import { usePreferences } from '../PreferencesContext'; import { ErrorBoundary } from '../ErrorBoundary'; @@ -32,6 +33,11 @@ type Tab = 'scenes' | 'movies'; const MOVIE_COLS = 2; +// Ten sam modal i stan filtra co główny feed (prośba 39314780: "similar filters to +// when we search for videos by pornstars"), z jedną różnicą: domyślne sortowanie po +// dacie premiery, bo tak ten ekran sortował od zawsze i taka jest intuicja "filmografii". +const PERFORMER_DEFAULT_FILTER: FilterState = { ...DEFAULT_FILTER, sort: 'release_date' }; + export function PerformerScenesScreen() { const client = useClient(); const { gridColumns } = usePreferences(); @@ -155,23 +161,51 @@ export function PerformerScenesScreen() { // Filtr tagu IN-PLACE (bug-report 0264a3ff: wybór tagu ma filtrować sceny tej // aktorki w tym widoku, nie nawigować do osobnego ekranu). null = bez filtra. + // Chipsy zostają OBOK pełnego modala: to skrót jednodotykowy na najczęstszy + // przypadek, a modal dokłada resztę (jakość, długość, studio, sort, obsada). const [selectedTag, setSelectedTag] = React.useState<{ slug: string; name: string } | null>(null); + const [filter, setFilter] = React.useState(PERFORMER_DEFAULT_FILTER); + const [filterOpen, setFilterOpen] = React.useState(false); + // Licznik liczony WZGLĘDEM domyślnych tego ekranu, nie jak w głównym feedzie, gdzie + // domyślne has_playback=true podbija licznik i "Filter 1" świeci na czysto. + const filterActiveCount = + filter.tagSlugs.length + + filter.studioSlugs.length + + filter.performerIds.length + + (filter.origin.trim() ? 1 : 0) + + (filter.minDurationSec > 0 ? 1 : 0) + + (filter.minQualityP > 0 ? 1 : 0) + + (filter.sort !== PERFORMER_DEFAULT_FILTER.sort ? 1 : 0) + + (!filter.hasPlayback ? 1 : 0); + const scenesQuery = useQuery({ - queryKey: ['performer-scenes', id, selectedTag?.slug ?? null, vertical ?? null], + queryKey: ['performer-scenes', id, selectedTag?.slug ?? null, vertical ?? null, filter], queryFn: () => client.listScenes({ - performer_ids: [id], + // Wybrani w modalu to KONIUNKCJA z bieżącym performerem ("sceny także z..."), + // backend łączy performer_ids przez AND. Dubel id jest niegroźny, ale filtrujemy. + performer_ids: [id, ...filter.performerIds.filter((p) => p !== id)], vertical: vertical ?? undefined, // 0 = nie nakładaj progu długości. User wszedł JAWNIE w tę pozycję, więc // ukrywanie scen o nieznanej długości (w SQL NULL nie przechodzi `>= 60`) // wyglądało jak pusty albo zatrzymany serwis. Patrz komentarz w api.ts. - min_duration_sec: 0, - tags: selectedTag ? [selectedTag.slug] : undefined, - sort: 'release_date', + // Wybór w filtrze ("5+ min" itd.) podnosi próg jawnie. + min_duration_sec: filter.minDurationSec > 0 ? filter.minDurationSec : 0, + tags: (() => { + const merged = new Set(filter.tagSlugs); + if (selectedTag) merged.add(selectedTag.slug); + return merged.size ? [...merged] : undefined; + })(), + studio_slugs: filter.studioSlugs.length ? filter.studioSlugs : undefined, + origin: filter.origin.trim() || undefined, + min_quality_p: filter.minQualityP > 0 ? filter.minQualityP : undefined, + include_stubs: filter.includeStubs || undefined, + sort: filter.sort, per_page: 200, - // Sceny TPDB/StashDB bez tube-linków nie są usable, zawsze filtrujemy. - has_playback: true, + // Domyślnie ukrywamy stuby TPDB/StashDB bez tube-linków; wyłączenie w modalu + // pokazuje pełną filmografię, z pozycjami, których (jeszcze) nie da się odtworzyć. + has_playback: filter.hasPlayback || undefined, }), }); @@ -285,6 +319,14 @@ export function PerformerScenesScreen() { ↻ Search more scenes )} + 0 && styles.actionBtnActive]} + onPress={() => setFilterOpen(true)} + > + + Filter{filterActiveCount > 0 ? ` ${filterActiveCount}` : ''} + + {tab === 'scenes' && !!tagsQuery.data?.items.length && ( )} + { + setFilter(next); + setFilterOpen(false); + }} + onClose={() => setFilterOpen(false)} + /> ); } @@ -441,6 +494,7 @@ const styles = StyleSheet.create({ paddingVertical: 6, alignItems: 'center', }, + actionBtnActive: { borderColor: theme.accent }, actionBtnText: { color: theme.muted, fontWeight: '600', fontSize: 12 }, actionBtnTextPrimary: { color: theme.accentText, fontWeight: '700', fontSize: 12 }, tagRow: { diff --git a/mobile/src/screens/ScenesFilterModal.tsx b/mobile/src/screens/ScenesFilterModal.tsx index 15aac46..ac0dfef 100644 --- a/mobile/src/screens/ScenesFilterModal.tsx +++ b/mobile/src/screens/ScenesFilterModal.tsx @@ -86,6 +86,7 @@ export function ScenesFilterModal({ onApply, onClose, vertical, + performersLabel, }: { visible: boolean; initial: FilterState; @@ -94,6 +95,10 @@ export function ScenesFilterModal({ /** Wertykał ekranu ('animation'/'jav'); filtruje listę tagów do jego słownika. * Bez tego sekcja animacji dostawała zachodnie tagi, których tam nie ma czym wypełnić. */ vertical?: string; + /** Tytuł sekcji performerów. Ekran performera podaje "Also featuring", bo tam wybór + * znaczy "sceny TAKŻE z tą osobą" (backend łączy performer_ids koniunkcją), a nie + * wybór, czyje sceny oglądamy. */ + performersLabel?: string; }) { const client = useClient(); const [draft, setDraft] = useState(initial); @@ -362,7 +367,7 @@ export function ScenesFilterModal({ )} -
+