diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts
index 11ef9c5..5c63ea9 100644
--- a/mobile/src/changelog.ts
+++ b/mobile/src/changelog.ts
@@ -16,6 +16,13 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
+ {
+ id: '2026-07-05b',
+ date: 'July 2026',
+ items: [
+ 'Tap a movie\'s studio to see every other film from that studio. You can also filter the Movies list by studio (Movies → Filter → Studios).',
+ ],
+ },
{
id: '2026-07-05',
date: 'July 2026',
diff --git a/mobile/src/navigation.tsx b/mobile/src/navigation.tsx
index 17c07fc..3bcfc2a 100644
--- a/mobile/src/navigation.tsx
+++ b/mobile/src/navigation.tsx
@@ -23,6 +23,7 @@ import { ScenesScreen } from './screens/ScenesScreen';
import { SceneDetailScreen } from './screens/SceneDetailScreen';
import { SiteScenesScreen } from './screens/SiteScenesScreen';
import { SitesScreen } from './screens/SitesScreen';
+import { StudioMoviesScreen } from './screens/StudioMoviesScreen';
import { StudioScenesScreen } from './screens/StudioScenesScreen';
import { TagScenesScreen } from './screens/TagScenesScreen';
import { TagsScreen } from './screens/TagsScreen';
@@ -48,6 +49,8 @@ export type RootStackParamList = {
// `studioId` to UUID (do favorite/blacklist API). Backend filtruje scen po slug,
// ale favorites + blacklist używają UUID.
StudioScenes: { id: string; name: string; studioId: string; seenSince?: string };
+ // StudioMovies: `slug` do filter movies `studio_slugs`, `studioId` (UUID) do favorite.
+ StudioMovies: { slug: string; name: string; studioId: string };
Favorites: undefined;
Tags: undefined;
TagScenes: { slug: string; name: string };
@@ -257,6 +260,11 @@ export function AppNavigator({ onLogout, client, appVersion }: AppNavigatorProps
component={StudioScenesScreen}
options={{ title: 'Studio scenes' }}
/>
+
{data.title}
{data.release_year ?? '—'}
- {data.studio?.name ? ` · ${data.studio.name}` : ''}
+ {data.studio ? (
+
+ {' · '}
+
+ navigation.navigate('StudioMovies', {
+ slug: data.studio!.slug,
+ name: data.studio!.name,
+ studioId: data.studio!.id,
+ })
+ }
+ >
+ {data.studio.name}
+
+
+ ) : null}
{dur ? ` · ${dur}` : ''}
{data.director ? dir. {data.director} : null}
@@ -375,6 +391,7 @@ const styles = StyleSheet.create({
heroMeta: { flex: 1, gap: 4 },
title: { color: theme.fg, fontSize: 18, fontWeight: '700' },
subtitle: { color: theme.muted, fontSize: 13 },
+ studioLink: { color: theme.accent, fontWeight: '600' },
rating: { color: theme.accent, fontSize: 14, fontWeight: '700', marginTop: 4 },
section: { paddingHorizontal: 14, paddingVertical: 10 },
diff --git a/mobile/src/screens/StudioMoviesScreen.tsx b/mobile/src/screens/StudioMoviesScreen.tsx
new file mode 100644
index 0000000..af67a02
--- /dev/null
+++ b/mobile/src/screens/StudioMoviesScreen.tsx
@@ -0,0 +1,134 @@
+// Filmy wybranego studia (filter studio_slugs=). Mirror StudioScenesScreen, ale
+// grid plakatów filmów (MoviePosterCard) zamiast SceneTile. Wchodzi się tu klikając
+// studio w MovieDetail, więc dociera do KAŻDEGO studia, nie tylko top-40 z MovieFiltersSheet.
+import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
+import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
+import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import React from 'react';
+import { ActivityIndicator, FlatList, Pressable, StyleSheet, Text, View } from 'react-native';
+import { useClient } from '../ClientContext';
+import { MoviePosterCard } from '../components/MoviePosterCard';
+import type { RootStackParamList } from '../navigation';
+import { theme } from '../theme';
+
+const PER_PAGE = 30;
+// 3 kolumny — parytet z MoviesScreen (plakaty 2:3, 50% więcej filmów na ekran).
+const NUM_COLS = 3;
+
+export function StudioMoviesScreen() {
+ const client = useClient();
+ const queryClient = useQueryClient();
+ const navigation =
+ useNavigation>();
+ const route = useRoute>();
+ // `slug` to studio.slug (backend MoviesListParams.studio_slugs). `studioId` (UUID)
+ // tylko do favorite/star (favorites-studios są po UUID).
+ const { slug, name, studioId } = route.params;
+
+ const favoritesQuery = useQuery({
+ queryKey: ['favorites-studios'],
+ queryFn: () => client.listFavoriteStudios(),
+ staleTime: 30_000,
+ });
+ const isFavorite = !!favoritesQuery.data?.items.find((f) => f.studio_id === studioId);
+ const addMutation = useMutation({
+ mutationFn: () => client.addFavoriteStudio(studioId),
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['favorites-studios'] }),
+ });
+ const removeMutation = useMutation({
+ mutationFn: () => client.removeFavoriteStudio(studioId),
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['favorites-studios'] }),
+ });
+
+ React.useLayoutEffect(() => {
+ navigation.setOptions({
+ title: name,
+ headerRight: () => (
+ (isFavorite ? removeMutation.mutate() : addMutation.mutate())}
+ hitSlop={12}
+ disabled={addMutation.isPending || removeMutation.isPending}
+ >
+
+ {isFavorite ? '★' : '☆'}
+
+
+ ),
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [navigation, name, isFavorite, addMutation.isPending, removeMutation.isPending]);
+
+ const {
+ data,
+ isLoading,
+ error,
+ refetch,
+ isRefetching,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ } = useInfiniteQuery({
+ queryKey: ['studio-movies', slug],
+ queryFn: ({ pageParam = 1 }) =>
+ client.listMovies({
+ studio_slugs: [slug],
+ sort: 'release_year',
+ page: pageParam,
+ per_page: PER_PAGE,
+ }),
+ initialPageParam: 1,
+ getNextPageParam: (last) => {
+ const loaded = last.page * last.per_page;
+ return loaded < last.total ? last.page + 1 : undefined;
+ },
+ });
+
+ const items = data?.pages.flatMap((p) => p.items) ?? [];
+ const total = data?.pages[0]?.total ?? 0;
+
+ return (
+
+ {isLoading && }
+ {error instanceof Error && {error.message}}
+
+ m.id}
+ removeClippedSubviews={false}
+ renderItem={({ item }) => (
+ navigation.navigate('MovieDetail', { id: item.id })}
+ />
+ )}
+ refreshing={isRefetching}
+ onRefresh={refetch}
+ ListHeaderComponent={
+ total > 0 ? (
+
+ {total} {total === 1 ? 'film' : 'films'}
+
+ ) : null
+ }
+ ListEmptyComponent={!isLoading ? no movies : null}
+ contentContainerStyle={{ paddingBottom: 24, paddingHorizontal: 6 }}
+ columnWrapperStyle={{ gap: 8 }}
+ onEndReached={() => {
+ if (hasNextPage && !isFetchingNextPage) fetchNextPage();
+ }}
+ onEndReachedThreshold={0.3}
+ ListFooterComponent={
+ isFetchingNextPage ? : null
+ }
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: { flex: 1, backgroundColor: theme.bg, paddingTop: 8 },
+ subtitle: { color: theme.muted, fontSize: 12, paddingHorizontal: 10, paddingTop: 6, paddingBottom: 8 },
+ muted: { color: theme.muted, textAlign: 'center', marginTop: 24 },
+ error: { color: theme.bad, padding: 12 },
+});