From 4afebacad851e434885e009c6b8864b11bdd0073 Mon Sep 17 00:00:00 2001 From: jtrzupek Date: Sun, 21 Jun 2026 23:21:22 +0200 Subject: [PATCH] =?UTF-8?q?feat(mobile):=20movies=20=E2=80=94=20performer?= =?UTF-8?q?=20filter=20+=203-column=20grid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two Movies-list reports. (1) 1044cd34 'do movies have a metadata base for performers/categories/studio/year': yes — 90% have year, 92% studio, 81% performers, 93% tags, and the filter already covered studio/genre/year. Added the missing dimension: a performer search-and-select in MovieFiltersSheet (backend listMovies + api.ts already accepted performer_ids; only the UI was missing). (2) 0200956f 'use the space better': Movies grid goes 2 -> 3 columns (poster card is flex:1, scales fine) so ~50% more films per screen. Co-Authored-By: Claude Opus 4.8 (1M context) --- mobile/src/changelog.ts | 8 ++ mobile/src/components/MovieFiltersSheet.tsx | 84 ++++++++++++++++++++- mobile/src/screens/MoviesScreen.tsx | 5 +- 3 files changed, 95 insertions(+), 2 deletions(-) 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