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.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()}`);

View file

@ -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',

View file

@ -156,6 +156,7 @@ export function PerformerScenesScreen() {
const [selectedTag, setSelectedTag] = React.useState<{ slug: string; name: string } | null>(null);
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);
// 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() {
<SceneTile scene={item} seenSince={seenSince} secondLine="studio" />
)}
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={
<View>
{/* 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: '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

View file

@ -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<FlatList<SceneOut>>(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 }) => <SceneTile scene={item} />}
ListHeaderComponent={!debouncedQ && activeCount === 0 ? <ContinueWatchingRail /> : 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<NativeStackNavigationProp<RootStackParamList, 'Scenes'>>();

View file

@ -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;
}