fix(mobile): stop Scenes feed repeating on scroll + mute audio during 2x hold
Some checks are pending
Backend tests / test (push) Waiting to run
Some checks are pending
Backend tests / test (push) Waiting to run
- Scenes feed repeated the same videos on scroll (report 304c9258): OFFSET pagination + continuous ingest means new scenes shift the offset, so later pages re-include earlier rows. Backend now appends Scene.id as a unique final tiebreaker to every sort (deterministic order), and the mobile Scenes list dedups by scene id when concatenating pages so a drifted overlap can never show a duplicate. - Player: holding to fast-forward at 2x now mutes the sped-up (chipmunk) audio for the duration of the hold and restores the user's real mute preference on release (report 35bbf428). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
28f56d646c
commit
fa697726e9
4 changed files with 37 additions and 5 deletions
|
|
@ -387,13 +387,19 @@ def list_scenes(
|
|||
total_capped = False
|
||||
total: int | None = _default_scene_count(session) if _is_pure_default else None
|
||||
|
||||
# Sort: zawsze tie-break po created_at desc dla determinizmu paginacji.
|
||||
# Sort: KAŻDY tryb kończy się `Scene.id.desc()` — unikalny PK jako ostateczny
|
||||
# tie-break. Bez tego przy OFFSET-paginacji sceny z IDENTYCZNYM created_at (masowy
|
||||
# ingest = ten sam timestamp co do sekundy) i release_date mają NIEustalony porządek,
|
||||
# więc kolejne strony nakładają się na siebie → feed "powtarza się" przy scrollu
|
||||
# (report 304c9258). created_at desc dalej pierwszy, więc index-walk zostaje.
|
||||
if sort == "release_date":
|
||||
ordered = base.order_by(
|
||||
Scene.release_date.desc().nullslast(), Scene.created_at.desc()
|
||||
Scene.release_date.desc().nullslast(), Scene.created_at.desc(), Scene.id.desc()
|
||||
)
|
||||
elif sort == "title":
|
||||
ordered = base.order_by(Scene.title_normalized.asc(), Scene.created_at.desc())
|
||||
ordered = base.order_by(
|
||||
Scene.title_normalized.asc(), Scene.created_at.desc(), Scene.id.desc()
|
||||
)
|
||||
elif sort == "studio":
|
||||
# Sceny bez studio na końcu; w obrębie studio — najświeższe pierwsze.
|
||||
ordered = (
|
||||
|
|
@ -402,11 +408,12 @@ def list_scenes(
|
|||
Studio.name_normalized.asc().nullslast(),
|
||||
Scene.release_date.desc().nullslast(),
|
||||
Scene.created_at.desc(),
|
||||
Scene.id.desc(),
|
||||
)
|
||||
)
|
||||
else: # created_at
|
||||
ordered = base.order_by(
|
||||
Scene.created_at.desc(), Scene.release_date.desc().nullslast()
|
||||
Scene.created_at.desc(), Scene.release_date.desc().nullslast(), Scene.id.desc()
|
||||
)
|
||||
|
||||
# Fetch per_page+1 — obecność (per_page+1)-szego wiersza = jest kolejna strona.
|
||||
|
|
|
|||
|
|
@ -16,6 +16,15 @@ export type ChangelogEntry = {
|
|||
};
|
||||
|
||||
export const CHANGELOG: ChangelogEntry[] = [
|
||||
{
|
||||
id: '2026-07-07',
|
||||
date: 'July 2026',
|
||||
items: [
|
||||
'The Scenes feed no longer repeats the same videos as you scroll down.',
|
||||
'Hold to fast-forward (2x) now mutes the sped-up audio until you let go.',
|
||||
'hqfap is back: the site returned with working video, so its scenes are being re-added.',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2026-07-05b',
|
||||
date: 'July 2026',
|
||||
|
|
|
|||
|
|
@ -549,6 +549,8 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
|
|||
[player, showSeekHint],
|
||||
);
|
||||
|
||||
// Stan wyciszenia SPRZED 2× (żeby przywrócić dokładnie preferencję usera po puszczeniu).
|
||||
const preSpeedMutedRef = React.useRef(true);
|
||||
// Long-press → 2× speed dopóki trzymasz. minDuration 220ms żeby nie konfliktowało
|
||||
// z double-tap (drugi tap trwa krócej). onTouchesUp finalizuje gest.
|
||||
const longPress = React.useMemo(
|
||||
|
|
@ -558,6 +560,10 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
|
|||
.maxDistance(30)
|
||||
.onStart(() => {
|
||||
try {
|
||||
// Wycisz na czas 2× — przyspieszony dźwięk (chipmunk) jest nieprzyjemny
|
||||
// (report 35bbf428). Zapamiętujemy stan usera i przywracamy po puszczeniu.
|
||||
preSpeedMutedRef.current = player.muted;
|
||||
player.muted = true;
|
||||
player.playbackRate = 2.0;
|
||||
} catch {
|
||||
// ignore
|
||||
|
|
@ -575,6 +581,7 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
|
|||
.onEnd(() => {
|
||||
try {
|
||||
player.playbackRate = 1.0;
|
||||
player.muted = preSpeedMutedRef.current;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
|
@ -585,6 +592,7 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
|
|||
if (player.playbackRate !== 1.0) {
|
||||
player.playbackRate = 1.0;
|
||||
}
|
||||
player.muted = preSpeedMutedRef.current;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,15 @@ export function ScenesScreen() {
|
|||
return more ? lastPage.page + 1 : undefined;
|
||||
},
|
||||
});
|
||||
const items = data?.pages.flatMap((p) => p.items) ?? [];
|
||||
// Dedup po id przy sklejaniu stron. OFFSET-paginacja + ciągły ingest = kolejne strony
|
||||
// potrafią nałożyć się na siebie (nowe sceny zsuwają offset w dół), przez co ta sama
|
||||
// scena wracała w feedzie przy scrollu (report 304c9258). Zostawiamy pierwsze
|
||||
// wystąpienie (najświeższe). Backend ma teraz też deterministyczny tie-break (Scene.id).
|
||||
const items = React.useMemo(() => {
|
||||
const flat = data?.pages.flatMap((p) => p.items) ?? [];
|
||||
const seen = new Set<string>();
|
||||
return flat.filter((s) => (seen.has(s.id) ? false : (seen.add(s.id), true)));
|
||||
}, [data?.pages]);
|
||||
const total = data?.pages[0]?.total ?? 0;
|
||||
// total bywa bounded ("1000+") dla list filtrowanych (q/tag) — patrz backend _COUNT_CAP.
|
||||
const totalLabel = `${total}${data?.pages[0]?.total_capped ? '+' : ''}`;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue