diff --git a/app/api/favorites.py b/app/api/favorites.py index cbbebd5..e592ed9 100644 --- a/app/api/favorites.py +++ b/app/api/favorites.py @@ -200,11 +200,19 @@ def _new_movie_counts(session: Session, device_id: str, *, kind: str) -> dict: def _merged_new_counts(session: Session, device_id: str, *, kind: str) -> dict: - """Sceny + filmy w jednym liczniku "+N" (badge nie rozróżnia typu treści).""" - counts = _new_counts(session, device_id, kind=kind) - for gid, n in _new_movie_counts(session, device_id, kind=kind).items(): - counts[gid] = counts.get(gid, 0) + n - return counts + """gid -> (nowe sceny, nowe filmy). Apka od 2026-10 pokazuje je osobno (zgłoszenie + 5a724cc8), starsze wersje dostają sumę w `new_count`.""" + scenes = _new_counts(session, device_id, kind=kind) + movies = _new_movie_counts(session, device_id, kind=kind) + return {gid: (scenes.get(gid, 0), movies.get(gid, 0)) for gid in scenes.keys() | movies.keys()} + + +_NO_NEW = (0, 0) + + +def _favorites_order(new: dict, gid, name: str) -> tuple: + """Najpierw ulubione z nowościami, w obu grupach alfabetycznie (zgłoszenie 5a724cc8).""" + return (sum(new.get(gid, _NO_NEW)) == 0, (name or "").lower()) # Cache liczników "+N" w pamięci procesu API (uvicorn chodzi jednym procesem). Liczenie @@ -234,7 +242,7 @@ def _store_counts(key: tuple[str, str], started: float, counts: dict) -> None: # mark_seen w trakcie liczenia: wynik policzono ze starym last_seen_at for gid, at in _counts_seen.get(key, {}).items(): if at >= started: - counts[gid] = 0 + counts[gid] = _NO_NEW _counts_seen.pop(key, None) _counts_cache[key] = (time.monotonic(), counts) stale = [k for k, (at, _) in _counts_cache.items() if time.monotonic() - at > _COUNTS_MAX_AGE_SEC] @@ -292,7 +300,7 @@ def _mark_counts_seen(device_id: str, kind: str, gid: uuid.UUID) -> None: _counts_seen.setdefault(key, {})[gid] = time.monotonic() hit = _counts_cache.get(key) if hit is not None and hit[1].get(gid): - hit[1][gid] = 0 + hit[1][gid] = _NO_NEW class FavoriteOut(BaseModel): @@ -300,7 +308,9 @@ class FavoriteOut(BaseModel): canonical_name: str slug: str | None scene_count: int - new_count: int # sceny od last_seen_at + new_count: int # sceny + filmy od last_seen_at (dla starszych wersji apki) + new_scene_count: int = 0 + new_movie_count: int = 0 last_seen_at: datetime created_at: datetime @@ -334,19 +344,22 @@ def list_favorites( # playback). Wcześniej grouped count z EXISTS playback per-request. Migracja 0019. scene_counts: dict = {perf.id: perf.scene_count for _, perf in rows} new_counts = _cached_new_counts(device_id, kind="performer") if counts else {} + rows = sorted(rows, key=lambda r: _favorites_order(new_counts, r[1].id, r[1].canonical_name)) items: list[FavoriteOut] = [] new_total = 0 for fav, perf in rows: - nc = new_counts.get(perf.id, 0) - new_total += nc + ns, nm = new_counts.get(perf.id, _NO_NEW) + new_total += ns + nm items.append( FavoriteOut( performer_id=perf.id, canonical_name=perf.canonical_name, slug=perf.slug, scene_count=scene_counts.get(perf.id, 0), - new_count=nc, + new_count=ns + nm, + new_scene_count=ns, + new_movie_count=nm, last_seen_at=fav.last_seen_at, created_at=fav.created_at, ) @@ -427,6 +440,8 @@ class FavoriteStudioOut(BaseModel): #: pokazywała wtedy samo „0" i wyglądała na pustą, mimo że „+N" liczyło już filmy. movie_count: int = 0 new_count: int + new_scene_count: int = 0 + new_movie_count: int = 0 last_seen_at: datetime created_at: datetime @@ -499,12 +514,13 @@ def list_favorite_studios( ).all() } new_counts = _cached_new_counts(device_id, kind="studio") + rows = sorted(rows, key=lambda r: _favorites_order(new_counts, r[1].id, r[1].name)) items: list[FavoriteStudioOut] = [] new_total = 0 for fav, st in rows: - nc = new_counts.get(st.id, 0) - new_total += nc + ns, nm = new_counts.get(st.id, _NO_NEW) + new_total += ns + nm items.append( FavoriteStudioOut( studio_id=st.id, @@ -513,7 +529,9 @@ def list_favorite_studios( network=st.network, scene_count=scene_counts.get(st.id, 0), movie_count=movie_counts.get(st.id, 0), - new_count=nc, + new_count=ns + nm, + new_scene_count=ns, + new_movie_count=nm, last_seen_at=fav.last_seen_at, created_at=fav.created_at, ) diff --git a/mobile/src/screens/FavoritesScreen.tsx b/mobile/src/screens/FavoritesScreen.tsx index fc68faf..477ea09 100644 --- a/mobile/src/screens/FavoritesScreen.tsx +++ b/mobile/src/screens/FavoritesScreen.tsx @@ -3,6 +3,7 @@ // (zeruje licznik). Klucz: przekazujemy `seenSince` (last_seen_at SPRZED markSeen) // żeby PerformerScenesScreen / StudioScenesScreen mógł pokazać NEW badge na świeżych // scenach. +import { MaterialCommunityIcons } from '@expo/vector-icons'; import { useNavigation } from '@react-navigation/native'; import { formatCount } from '../lib/formatCount'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; @@ -85,7 +86,7 @@ export function FavoritesScreen() { ? { ...prev, items: prev.items.map((f) => - f.performer_id === id ? { ...f, new_count: 0 } : f, + f.performer_id === id ? { ...f, new_count: 0, new_scene_count: 0, new_movie_count: 0 } : f, ), new_total: Math.max( 0, @@ -103,7 +104,7 @@ export function FavoritesScreen() { ? { ...prev, items: prev.items.map((f) => - f.studio_id === id ? { ...f, new_count: 0 } : f, + f.studio_id === id ? { ...f, new_count: 0, new_scene_count: 0, new_movie_count: 0 } : f, ), new_total: Math.max( 0, @@ -337,6 +338,35 @@ function initials(name: string): string { return (first + last).toUpperCase() || '?'; } +// „+N" osobno dla scen i filmow (zgloszenie 5a724cc8). Starszy backend nie podaje +// rozbicia, wtedy jeden znaczek jak dawniej. Kolejnosc listy (nowosci na gorze) robi backend. +function NewBadges({ fav }: { fav: FavoriteOut | FavoriteStudioOut }) { + if (fav.new_count <= 0) return ›; + if (fav.new_scene_count == null || fav.new_movie_count == null) { + return ( + + +{fav.new_count} + + ); + } + return ( + + {fav.new_scene_count > 0 ? ( + + + +{fav.new_scene_count} + + ) : null} + {fav.new_movie_count > 0 ? ( + + + +{fav.new_movie_count} + + ) : null} + + ); +} + function PerformerRow({ fav, onPress, @@ -364,13 +394,7 @@ function PerformerRow({ {formatCount(fav.scene_count)} {fav.scene_count === 1 ? 'scene' : 'scenes'} - {fav.new_count > 0 ? ( - - +{fav.new_count} - - ) : ( - › - )} + ); } @@ -456,13 +480,7 @@ function StudioRow({ {fav.network ? ` · ${fav.network}` : ''} - {fav.new_count > 0 ? ( - - +{fav.new_count} - - ) : ( - › - )} + ); } @@ -521,7 +539,11 @@ const styles = StyleSheet.create({ rowTitle: { color: theme.fg, fontWeight: '700', fontSize: 16, marginBottom: 4 }, rowMuted: { color: theme.muted, fontSize: 13 }, + newBadges: { alignItems: 'flex-end', gap: 4 }, newBadge: { + flexDirection: 'row', + alignItems: 'center', + gap: 3, backgroundColor: theme.accent, paddingHorizontal: 10, paddingVertical: 4, diff --git a/mobile/src/types.ts b/mobile/src/types.ts index 4727c13..eb5efd9 100644 --- a/mobile/src/types.ts +++ b/mobile/src/types.ts @@ -344,6 +344,9 @@ export interface FavoriteOut { slug: string | null; scene_count: number; new_count: number; + /** Rozbicie new_count na sceny i filmy (backend od 2026-10). Brak = starszy backend. */ + new_scene_count?: number; + new_movie_count?: number; last_seen_at: string; created_at: string; } @@ -369,6 +372,8 @@ export interface FavoriteStudioOut { * setki filmów), a lista pokazywała wtedy samo „0 scenes". */ movie_count?: number; new_count: number; + new_scene_count?: number; + new_movie_count?: number; last_seen_at: string; created_at: string; } @@ -486,7 +491,9 @@ export type ClipEventKind = | 'unlike' | 'not_interested' | 'autoswipe' - | 'open'; + | 'open' + /** Odtwarzacz nie dal rady (np. 403 po skasowaniu klipu) albo stoi bez obrazu. */ + | 'error'; export interface ClipEventIn { clip_id: string;