feat(mobile): sortowanie losowe, najdluzsze i najkrotsze w filtrze scen; seed losowy per lista, nowy przy pull-to-refresh

- changelog 2026-09-14b, OTA 6770483f na 1.3

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
goon-foss 2026-09-14 15:19:58 +02:00
parent bd8af5f44c
commit 656604e868
6 changed files with 41 additions and 5 deletions

View file

@ -157,6 +157,7 @@ export class GoonClient {
if (params.vertical) qs.set('vertical', params.vertical); if (params.vertical) qs.set('vertical', params.vertical);
if (params.origin) qs.set('origin', params.origin); if (params.origin) qs.set('origin', params.origin);
if (params.sort) qs.set('sort', params.sort); if (params.sort) qs.set('sort', params.sort);
if (params.seed) qs.set('seed', params.seed);
qs.set('page', String(params.page ?? 1)); qs.set('page', String(params.page ?? 1));
qs.set('per_page', String(params.per_page ?? 50)); qs.set('per_page', String(params.per_page ?? 50));
return this.request(`/scenes?${qs.toString()}`); return this.request(`/scenes?${qs.toString()}`);

View file

@ -29,6 +29,13 @@ export type ChangelogEntry = {
}; };
export const CHANGELOG: ChangelogEntry[] = [ export const CHANGELOG: ChangelogEntry[] = [
{
id: '2026-09-14b',
date: 'September 2026',
items: [
'Three new sort orders in the scene filter: random, longest and shortest. Random keeps its order while you scroll and reshuffles when you pull to refresh. Works on performer pages too.',
],
},
{ {
id: '2026-09-14a', id: '2026-09-14a',
date: 'September 2026', date: 'September 2026',

View file

@ -156,6 +156,7 @@ export function PerformerScenesScreen() {
const [selectedTag, setSelectedTag] = React.useState<{ slug: string; name: string } | null>(null); const [selectedTag, setSelectedTag] = React.useState<{ slug: string; name: string } | null>(null);
const [filter, setFilter] = React.useState<FilterState>(PERFORMER_DEFAULT_FILTER); const [filter, setFilter] = React.useState<FilterState>(PERFORMER_DEFAULT_FILTER);
const [seed, setSeed] = React.useState(() => Math.random().toString(36).slice(2) + Date.now().toString(36));
const [filterOpen, setFilterOpen] = React.useState(false); const [filterOpen, setFilterOpen] = React.useState(false);
// Licznik liczony WZGLĘDEM domyślnych tego ekranu, nie jak w głównym feedzie, gdzie // 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. // domyślne has_playback=true podbija licznik i "Filter 1" świeci na czysto.
@ -170,7 +171,7 @@ export function PerformerScenesScreen() {
(!filter.hasPlayback ? 1 : 0); (!filter.hasPlayback ? 1 : 0);
const scenesQuery = useQuery({ const scenesQuery = useQuery({
queryKey: ['performer-scenes', id, selectedTag?.slug ?? null, vertical ?? null, filter], queryKey: ['performer-scenes', id, selectedTag?.slug ?? null, vertical ?? null, filter, filter.sort === 'random' ? seed : null],
queryFn: () => queryFn: () =>
client.listScenes({ client.listScenes({
// Wybrani w modalu to KONIUNKCJA z bieżącym performerem ("sceny także z..."), // Wybrani w modalu to KONIUNKCJA z bieżącym performerem ("sceny także z..."),
@ -192,6 +193,7 @@ export function PerformerScenesScreen() {
min_quality_p: filter.minQualityP > 0 ? filter.minQualityP : undefined, min_quality_p: filter.minQualityP > 0 ? filter.minQualityP : undefined,
include_stubs: filter.includeStubs || undefined, include_stubs: filter.includeStubs || undefined,
sort: filter.sort, sort: filter.sort,
seed: filter.sort === 'random' ? seed : undefined,
per_page: 200, per_page: 200,
// Domyślnie ukrywamy stuby TPDB/StashDB bez tube-linków; wyłączenie w modalu // 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ć. // pokazuje pełną filmografię, z pozycjami, których (jeszcze) nie da się odtworzyć.
@ -280,7 +282,10 @@ export function PerformerScenesScreen() {
<SceneTile scene={item} seenSince={seenSince} secondLine="studio" /> <SceneTile scene={item} seenSince={seenSince} secondLine="studio" />
)} )}
refreshing={scenesQuery.isRefetching} refreshing={scenesQuery.isRefetching}
onRefresh={scenesQuery.refetch} onRefresh={() => {
if (filter.sort === 'random') setSeed(Math.random().toString(36).slice(2) + Date.now().toString(36));
else scenesQuery.refetch();
}}
ListHeaderComponent={ ListHeaderComponent={
<View> <View>
{/* Jeden wiersz (2026-09-07): licznik scen po lewej, po prawej ikony {/* Jeden wiersz (2026-09-07): licznik scen po lewej, po prawej ikony

View file

@ -48,6 +48,9 @@ const SORT_OPTIONS: { value: ScenesSort; label: string }[] = [
{ value: 'release_date', label: 'release date' }, { value: 'release_date', label: 'release date' },
{ value: 'title', label: 'title' }, { value: 'title', label: 'title' },
{ value: 'studio', label: 'studio' }, { value: 'studio', label: 'studio' },
{ value: 'random', label: 'random' },
{ value: 'duration_desc', label: 'longest' },
{ value: 'duration_asc', label: 'shortest' },
]; ];
// Filtr długości, user-report mobilism: "short clips nowadays are mostly ads in // Filtr długości, user-report mobilism: "short clips nowadays are mostly ads in

View file

@ -70,6 +70,9 @@ export function ScenesScreen({
); );
const [filterOpen, setFilterOpen] = useState(false); const [filterOpen, setFilterOpen] = useState(false);
const [searchFocused, setSearchFocused] = useState(false); const [searchFocused, setSearchFocused] = useState(false);
// Seed losowej kolejnosci: staly w obrebie listy (paginacja spójna), nowy przy
// pull-to-refresh, zeby odswiezenie dalo nowe rozdanie.
const [seed, setSeed] = useState(() => newSeed());
const listRef = React.useRef<FlatList<SceneOut>>(null); const listRef = React.useRef<FlatList<SceneOut>>(null);
useScrollTopListener( useScrollTopListener(
React.useCallback(() => listRef.current?.scrollToOffset({ offset: 0, animated: true }), []), React.useCallback(() => listRef.current?.scrollToOffset({ offset: 0, animated: true }), []),
@ -94,7 +97,7 @@ export function ScenesScreen({
isFetchingNextPage, isFetchingNextPage,
} = useInfiniteQuery({ } = useInfiniteQuery({
enabled: filterHydrated, enabled: filterHydrated,
queryKey: ['scenes', debouncedQ, filter, jav, vertical], queryKey: ['scenes', debouncedQ, filter, jav, vertical, filter.sort === 'random' ? seed : null],
queryFn: ({ pageParam = 1 }) => queryFn: ({ pageParam = 1 }) =>
client.listScenes({ client.listScenes({
q: debouncedQ || undefined, q: debouncedQ || undefined,
@ -103,6 +106,7 @@ export function ScenesScreen({
performer_ids: filter.performerIds.length ? filter.performerIds : undefined, performer_ids: filter.performerIds.length ? filter.performerIds : undefined,
has_playback: filter.hasPlayback || undefined, has_playback: filter.hasPlayback || undefined,
sort: filter.sort, sort: filter.sort,
seed: filter.sort === 'random' ? seed : undefined,
include_stubs: filter.includeStubs || undefined, include_stubs: filter.includeStubs || undefined,
origin: filter.origin.trim() || undefined, origin: filter.origin.trim() || undefined,
jav: jav || undefined, jav: jav || undefined,
@ -286,7 +290,10 @@ export function ScenesScreen({
renderItem={({ item }) => <SceneTile scene={item} />} renderItem={({ item }) => <SceneTile scene={item} />}
ListHeaderComponent={!debouncedQ && activeCount === 0 ? <ContinueWatchingRail /> : null} ListHeaderComponent={!debouncedQ && activeCount === 0 ? <ContinueWatchingRail /> : null}
refreshing={isRefetching} refreshing={isRefetching}
onRefresh={refetch} onRefresh={() => {
if (filter.sort === 'random') setSeed(newSeed());
else refetch();
}}
onEndReached={() => { onEndReached={() => {
if (hasNextPage && !isFetchingNextPage) fetchNextPage(); if (hasNextPage && !isFetchingNextPage) fetchNextPage();
}} }}
@ -317,6 +324,10 @@ export function ScenesScreen({
); );
} }
function newSeed(): string {
return Math.random().toString(36).slice(2) + Date.now().toString(36);
}
function ContinueWatchingRail() { function ContinueWatchingRail() {
const client = useClient(); const client = useClient();
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList, 'Scenes'>>(); const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList, 'Scenes'>>();

View file

@ -139,7 +139,14 @@ export interface SourceListOut {
total: number; total: number;
} }
export type ScenesSort = 'created_at' | 'release_date' | 'title' | 'studio'; export type ScenesSort =
| 'created_at'
| 'release_date'
| 'title'
| 'studio'
| 'random'
| 'duration_desc'
| 'duration_asc';
export interface ScenesListParams { export interface ScenesListParams {
q?: string; q?: string;
@ -157,6 +164,8 @@ export interface ScenesListParams {
/** Wertykał do pokazania: 'animation' | 'jav'. Brak → główny feed (wertykały odcięte). */ /** Wertykał do pokazania: 'animation' | 'jav'. Brak → główny feed (wertykały odcięte). */
vertical?: string; vertical?: string;
sort?: ScenesSort; sort?: ScenesSort;
/** Dla sort=random: stały w obrębie sesji listy, nowy przy pull-to-refresh. */
seed?: string;
page?: number; page?: number;
per_page?: number; per_page?: number;
} }