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;