diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts index c3048c8..adb1224 100644 --- a/mobile/src/changelog.ts +++ b/mobile/src/changelog.ts @@ -16,6 +16,14 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + id: '2026-06-21b', + date: 'June 2026', + items: [ + 'Filter movies by performer (Movies → Filter → Performers).', + 'Movies grid fits more per screen (3 columns).', + ], + }, { id: '2026-06-21', date: 'June 2026', diff --git a/mobile/src/components/MovieFiltersSheet.tsx b/mobile/src/components/MovieFiltersSheet.tsx index 9b79acd..9b8ef64 100644 --- a/mobile/src/components/MovieFiltersSheet.tsx +++ b/mobile/src/components/MovieFiltersSheet.tsx @@ -34,6 +34,7 @@ export interface MovieFilters { has_playback: boolean; tag_slugs: string[]; studio_slugs: string[]; + performer_ids: string[]; } export const DEFAULT_MOVIE_FILTERS: MovieFilters = { @@ -43,6 +44,7 @@ export const DEFAULT_MOVIE_FILTERS: MovieFilters = { has_playback: false, tag_slugs: [], studio_slugs: [], + performer_ids: [], }; const SORT_OPTIONS: { value: MoviesSort; label: string }[] = [ @@ -126,6 +128,35 @@ export function MovieFiltersSheet({ visible, value, onChange, onClose }: Props) })); }; + // Performers — long-tail, więc search-by-q (nie top-N jak studia/genres). `pnames` + // zapamiętuje id→imię z wyników, żeby zaznaczone chipy pokazywały nazwę. + const [perfQ, setPerfQ] = useState(''); + const [pnames, setPnames] = useState>({}); + const performersQuery = useQuery({ + queryKey: ['movie-perf-search', perfQ], + queryFn: () => client.listPerformers({ q: perfQ, order: 'scene_count', per_page: 30 }), + enabled: visible && perfQ.trim().length > 0, + }); + React.useEffect(() => { + const items = performersQuery.data?.items; + if (!items?.length) return; + setPnames((m) => { + const next = { ...m }; + for (const p of items) next[p.id] = p.canonical_name; + return next; + }); + }, [performersQuery.data]); + + const togglePerformer = (id: string, name: string) => { + setPnames((m) => ({ ...m, [id]: name })); + setDraft((d) => ({ + ...d, + performer_ids: d.performer_ids.includes(id) + ? d.performer_ids.filter((x) => x !== id) + : [...d.performer_ids, id], + })); + }; + const apply = () => { const yf = parseInt(yearFromText, 10); const yt = parseInt(yearToText, 10); @@ -263,6 +294,56 @@ export function MovieFiltersSheet({ visible, value, onChange, onClose }: Props) no studios yet )} + + Performers {draft.performer_ids.length > 0 ? `(${draft.performer_ids.length})` : ''} + + {draft.performer_ids.length > 0 ? ( + + {draft.performer_ids.map((id) => ( + togglePerformer(id, pnames[id] || '')} + > + + {pnames[id] || 'performer'} ✕ + + + ))} + + ) : null} + + {perfQ.trim().length > 0 ? ( + performersQuery.isLoading ? ( + + ) : ( + + {(performersQuery.data?.items ?? []) + .filter((p) => !draft.performer_ids.includes(p.id)) + .map((p) => ( + togglePerformer(p.id, p.canonical_name)} + > + + {p.canonical_name} + {p.scene_count > 0 ? ` · ${p.scene_count}` : ''} + + + ))} + + ) + ) : null} + Only with playback