From 656604e8688645fb03db3134e17d85d5fe96aa64 Mon Sep 17 00:00:00 2001 From: goon-foss Date: Mon, 14 Sep 2026 15:19:58 +0200 Subject: [PATCH] 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 --- mobile/src/api.ts | 1 + mobile/src/changelog.ts | 7 +++++++ mobile/src/screens/PerformerScenesScreen.tsx | 9 +++++++-- mobile/src/screens/ScenesFilterModal.tsx | 3 +++ mobile/src/screens/ScenesScreen.tsx | 15 +++++++++++++-- mobile/src/types.ts | 11 ++++++++++- 6 files changed, 41 insertions(+), 5 deletions(-) diff --git a/mobile/src/api.ts b/mobile/src/api.ts index 95a221f..4ec6e09 100644 --- a/mobile/src/api.ts +++ b/mobile/src/api.ts @@ -157,6 +157,7 @@ export class GoonClient { if (params.vertical) qs.set('vertical', params.vertical); if (params.origin) qs.set('origin', params.origin); 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('per_page', String(params.per_page ?? 50)); return this.request(`/scenes?${qs.toString()}`); diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts index 67605bb..1a1c603 100644 --- a/mobile/src/changelog.ts +++ b/mobile/src/changelog.ts @@ -29,6 +29,13 @@ export type 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', date: 'September 2026', diff --git a/mobile/src/screens/PerformerScenesScreen.tsx b/mobile/src/screens/PerformerScenesScreen.tsx index ff8368a..964115c 100644 --- a/mobile/src/screens/PerformerScenesScreen.tsx +++ b/mobile/src/screens/PerformerScenesScreen.tsx @@ -156,6 +156,7 @@ export function PerformerScenesScreen() { const [selectedTag, setSelectedTag] = React.useState<{ slug: string; name: string } | null>(null); const [filter, setFilter] = React.useState(PERFORMER_DEFAULT_FILTER); + const [seed, setSeed] = React.useState(() => Math.random().toString(36).slice(2) + Date.now().toString(36)); 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. @@ -170,7 +171,7 @@ export function PerformerScenesScreen() { (!filter.hasPlayback ? 1 : 0); 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: () => client.listScenes({ // 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, include_stubs: filter.includeStubs || undefined, sort: filter.sort, + seed: filter.sort === 'random' ? seed : undefined, per_page: 200, // 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ć. @@ -280,7 +282,10 @@ export function PerformerScenesScreen() { )} 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={ {/* Jeden wiersz (2026-09-07): licznik scen po lewej, po prawej ikony diff --git a/mobile/src/screens/ScenesFilterModal.tsx b/mobile/src/screens/ScenesFilterModal.tsx index 44ee81d..cc733fc 100644 --- a/mobile/src/screens/ScenesFilterModal.tsx +++ b/mobile/src/screens/ScenesFilterModal.tsx @@ -48,6 +48,9 @@ const SORT_OPTIONS: { value: ScenesSort; label: string }[] = [ { value: 'release_date', label: 'release date' }, { value: 'title', label: 'title' }, { 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 diff --git a/mobile/src/screens/ScenesScreen.tsx b/mobile/src/screens/ScenesScreen.tsx index 03c2c42..becc202 100644 --- a/mobile/src/screens/ScenesScreen.tsx +++ b/mobile/src/screens/ScenesScreen.tsx @@ -70,6 +70,9 @@ export function ScenesScreen({ ); const [filterOpen, setFilterOpen] = 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>(null); useScrollTopListener( React.useCallback(() => listRef.current?.scrollToOffset({ offset: 0, animated: true }), []), @@ -94,7 +97,7 @@ export function ScenesScreen({ isFetchingNextPage, } = useInfiniteQuery({ enabled: filterHydrated, - queryKey: ['scenes', debouncedQ, filter, jav, vertical], + queryKey: ['scenes', debouncedQ, filter, jav, vertical, filter.sort === 'random' ? seed : null], queryFn: ({ pageParam = 1 }) => client.listScenes({ q: debouncedQ || undefined, @@ -103,6 +106,7 @@ export function ScenesScreen({ performer_ids: filter.performerIds.length ? filter.performerIds : undefined, has_playback: filter.hasPlayback || undefined, sort: filter.sort, + seed: filter.sort === 'random' ? seed : undefined, include_stubs: filter.includeStubs || undefined, origin: filter.origin.trim() || undefined, jav: jav || undefined, @@ -286,7 +290,10 @@ export function ScenesScreen({ renderItem={({ item }) => } ListHeaderComponent={!debouncedQ && activeCount === 0 ? : null} refreshing={isRefetching} - onRefresh={refetch} + onRefresh={() => { + if (filter.sort === 'random') setSeed(newSeed()); + else refetch(); + }} onEndReached={() => { 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() { const client = useClient(); const navigation = useNavigation>(); diff --git a/mobile/src/types.ts b/mobile/src/types.ts index a9dacd7..39aade6 100644 --- a/mobile/src/types.ts +++ b/mobile/src/types.ts @@ -139,7 +139,14 @@ export interface SourceListOut { 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 { q?: string; @@ -157,6 +164,8 @@ export interface ScenesListParams { /** Wertykał do pokazania: 'animation' | 'jav'. Brak → główny feed (wertykały odcięte). */ vertical?: string; sort?: ScenesSort; + /** Dla sort=random: stały w obrębie sesji listy, nowy przy pull-to-refresh. */ + seed?: string; page?: number; per_page?: number; }