feat(ui): badge jakosci i nazwa serwisu na kafelku sceny
Kierunek A z makiet (miniaturki niosa ekran, chrome cichy) + jeden element z C: jakosc i zrodlo widoczne wprost na kafelku. Backend musial to najpierw WYSTAWIC, bo lista jedzie w trybie `light`, ktory okrawa playback_sources do samej miniatury: - `SceneOut.max_quality_p` (kolumna z migracji 0029, dotad uzywana tylko jako filtr), - slim wpis niesie PRAWDZIWY `origin` zrodla wybranej miniatury zamiast "". Kafelek: badge jakosci w LEWYM dolnym rogu (czas zostaje w prawym, ten sam ciezar wizualny), a w linii meta studio + przygaszona nazwa serwisu. Badge pokazujemy TYLKO gdy jakosc jest znana (6,5% scen), bo ma byc faktem, nie zgadywaniem. Zweryfikowane na emulatorze na PRAWDZIWYCH danych: zeby to bylo mozliwe, zbudowalem build debugowy podpisany kluczem release (przechodzi kontrole X-App-Signature, a jako debug nie ma FLAG_SECURE, wiec da sie zrobic zrzut). Widac "4K" i "1280p" w rogu oraz "ONLY FANS SXYPRN" w linii meta. Tymczasowa zmiana podpisu w build.gradle cofnieta. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
576058497a
commit
bbc2059728
5 changed files with 76 additions and 13 deletions
|
|
@ -877,11 +877,11 @@ def _build_scenes_out_batch(
|
||||||
if sxy:
|
if sxy:
|
||||||
# sxyprn → żywy on-demand resolver (martwy stored URL ignorujemy),
|
# sxyprn → żywy on-demand resolver (martwy stored URL ignorujemy),
|
||||||
# tier fallback: użyty tylko gdy scena nie ma stabilniejszej miniatury.
|
# tier fallback: użyty tylko gdy scena nie ma stabilniejszej miniatury.
|
||||||
thumb_fallback.setdefault(sid, (sxy, page_url))
|
thumb_fallback.setdefault(sid, (sxy, page_url, origin))
|
||||||
elif yp:
|
elif yp:
|
||||||
# youporn z podpisanym (wygasającym) posterem → on-demand resolver.
|
# youporn z podpisanym (wygasającym) posterem → on-demand resolver.
|
||||||
# Też tier fallback: statyczna miniatura z innego źródła jest tańsza.
|
# Też tier fallback: statyczna miniatura z innego źródła jest tańsza.
|
||||||
thumb_fallback.setdefault(sid, (yp, page_url))
|
thumb_fallback.setdefault(sid, (yp, page_url, origin))
|
||||||
elif thumb:
|
elif thumb:
|
||||||
if _is_rotting_thumb(thumb) or _is_flaky_thumb(thumb):
|
if _is_rotting_thumb(thumb) or _is_flaky_thumb(thumb):
|
||||||
# Martwe ORAZ chwiejne lądują w tierze fallback: użyte tylko wtedy,
|
# Martwe ORAZ chwiejne lądują w tierze fallback: użyte tylko wtedy,
|
||||||
|
|
@ -891,11 +891,11 @@ def _build_scenes_out_batch(
|
||||||
# hdporn dostawała gorszy z dwóch (pomiar: 38 z 60 takich scen).
|
# hdporn dostawała gorszy z dwóch (pomiar: 38 z 60 takich scen).
|
||||||
prev = thumb_fallback.get(sid)
|
prev = thumb_fallback.get(sid)
|
||||||
if prev is None or _flaky_rank(thumb) < _flaky_rank(prev[0]):
|
if prev is None or _flaky_rank(thumb) < _flaky_rank(prev[0]):
|
||||||
thumb_fallback[sid] = (thumb, page_url)
|
thumb_fallback[sid] = (thumb, page_url, origin)
|
||||||
elif sid not in thumb_by_scene:
|
elif sid not in thumb_by_scene:
|
||||||
thumb_by_scene[sid] = (thumb, page_url)
|
thumb_by_scene[sid] = (thumb, page_url, origin)
|
||||||
if sid not in anim_by_scene and anim:
|
if sid not in anim_by_scene and anim:
|
||||||
anim_by_scene[sid] = (anim, page_url)
|
anim_by_scene[sid] = (anim, page_url, origin)
|
||||||
# Uzupełnij scenami które mają TYLKO rotting thumbnail (sxyprn-only).
|
# Uzupełnij scenami które mają TYLKO rotting thumbnail (sxyprn-only).
|
||||||
for sid, val in thumb_fallback.items():
|
for sid, val in thumb_fallback.items():
|
||||||
thumb_by_scene.setdefault(sid, val)
|
thumb_by_scene.setdefault(sid, val)
|
||||||
|
|
@ -911,11 +911,12 @@ def _build_scenes_out_batch(
|
||||||
t_url = _wrap_image_proxy(t_url, ref)
|
t_url = _wrap_image_proxy(t_url, ref)
|
||||||
if a_url and _needs_proxy(a_url):
|
if a_url and _needs_proxy(a_url):
|
||||||
a_url = _wrap_image_proxy(a_url, ref)
|
a_url = _wrap_image_proxy(a_url, ref)
|
||||||
# id/origin/page_url wymagane przez schemat ale nieużywane przez kafelek
|
# `origin` niesiemy PRAWDZIWY (źródła wybranej miniatury): kafelek pokazuje
|
||||||
# (SceneDetail re-fetchuje pełne źródła), dummy sentinel.
|
# nazwę serwisu (element z makiety C, 2026-08-18). `id`/`page_url` zostają
|
||||||
|
# sentinelem, bo SceneDetail i tak re-fetchuje pełne źródła.
|
||||||
pb_by_scene[sid].append(
|
pb_by_scene[sid].append(
|
||||||
PlaybackSourceOut(
|
PlaybackSourceOut(
|
||||||
id=uuid.UUID(int=0), origin="", page_url="",
|
id=uuid.UUID(int=0), origin=(t or a)[2] or "", page_url="",
|
||||||
thumbnail_url=t_url, animated_thumbnail_url=a_url,
|
thumbnail_url=t_url, animated_thumbnail_url=a_url,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -992,6 +993,7 @@ def _build_scenes_out_batch(
|
||||||
external_refs=refs_by_scene.get(scene.id, []),
|
external_refs=refs_by_scene.get(scene.id, []),
|
||||||
playback_sources=pb_by_scene.get(scene.id, []),
|
playback_sources=pb_by_scene.get(scene.id, []),
|
||||||
vertical=verticals.vertical_for_origins(origins_by_scene.get(scene.id, ())),
|
vertical=verticals.vertical_for_origins(origins_by_scene.get(scene.id, ())),
|
||||||
|
max_quality_p=scene.max_quality_p,
|
||||||
created_at=scene.created_at,
|
created_at=scene.created_at,
|
||||||
backfill=scene.backfill,
|
backfill=scene.backfill,
|
||||||
last_played_at=progress.last_played_at if progress else None,
|
last_played_at=progress.last_played_at if progress else None,
|
||||||
|
|
@ -1141,6 +1143,7 @@ def _build_scene_out(session: Session, scene: Scene, *, device_id: str = LEGACY_
|
||||||
external_refs=refs_out,
|
external_refs=refs_out,
|
||||||
playback_sources=playback_out,
|
playback_sources=playback_out,
|
||||||
vertical=verticals.vertical_for_origins(p.origin for p in playback_rows),
|
vertical=verticals.vertical_for_origins(p.origin for p in playback_rows),
|
||||||
|
max_quality_p=scene.max_quality_p,
|
||||||
created_at=scene.created_at,
|
created_at=scene.created_at,
|
||||||
backfill=scene.backfill,
|
backfill=scene.backfill,
|
||||||
last_played_at=progress.last_played_at if progress else None,
|
last_played_at=progress.last_played_at if progress else None,
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,9 @@ class SceneOut(BaseModel):
|
||||||
# wertykały, więc tag z anime pokazywał sceny spoza anime, a studio animacyjne
|
# wertykały, więc tag z anime pokazywał sceny spoza anime, a studio animacyjne
|
||||||
# wyglądało na martwe (zgłoszenia 195ae42e / e9de414c, 2026-08-14).
|
# wyglądało na martwe (zgłoszenia 195ae42e / e9de414c, 2026-08-14).
|
||||||
vertical: str | None = None
|
vertical: str | None = None
|
||||||
|
# Najwyższa ZNANA jakość źródeł (720/1080/2160), NULL gdy żadne jej nie podało.
|
||||||
|
# Kafelek pokazuje ją jako badge; patrz migracja 0029_scenes_max_quality.
|
||||||
|
max_quality_p: int | None = None
|
||||||
# Kiedy scena trafiła do bazy (ingest). Używane przez mobile do oznaczenia
|
# Kiedy scena trafiła do bazy (ingest). Używane przez mobile do oznaczenia
|
||||||
# "NEW" na karcie scen w PerformerScenesScreen / StudioScenesScreen, gdy
|
# "NEW" na karcie scen w PerformerScenesScreen / StudioScenesScreen, gdy
|
||||||
# `created_at > last_seen_at` (favorite) → badge.
|
# `created_at > last_seen_at` (favorite) → badge.
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,13 @@ export type ChangelogEntry = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CHANGELOG: ChangelogEntry[] = [
|
export const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
id: '2026-08-18g',
|
||||||
|
date: 'August 2026',
|
||||||
|
items: [
|
||||||
|
'Scene tiles now show the quality (4K, 1080p) in the corner when we actually know it, and the site the scene comes from next to the studio name. No more opening a scene just to find out where it is hosted.',
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: '2026-08-18f',
|
id: '2026-08-18f',
|
||||||
date: 'August 2026',
|
date: 'August 2026',
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,22 @@ interface Props {
|
||||||
* Gdy po zdjęciu zostaje mniej niż 12 znaków, ODDAJEMY tytuł nietknięty — lepsze
|
* Gdy po zdjęciu zostaje mniej niż 12 znaków, ODDAJEMY tytuł nietknięty — lepsze
|
||||||
* powtórzenie niż ogryzek w rodzaju „Scene 1".
|
* powtórzenie niż ogryzek w rodzaju „Scene 1".
|
||||||
*/
|
*/
|
||||||
|
/** 'tube:hqpornercom' → 'hqporner'. Kafelek pokazuje serwis, z którego jest miniatura,
|
||||||
|
* żeby było widać skąd scena pochodzi bez wchodzenia w szczegóły (element z makiety C). */
|
||||||
|
function sourceLabel(origin?: string | null): string | null {
|
||||||
|
if (!origin) return null;
|
||||||
|
const raw = origin.replace(/^tube:/, '').trim();
|
||||||
|
if (!raw) return null;
|
||||||
|
return raw.replace(/(com|net|org|xxx|tv|to)$/i, '') || raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 2160 → '4K', reszta → '1080p'. Pokazujemy TYLKO gdy jakość jest znana (6,5% scen),
|
||||||
|
* bo badge ma być faktem, nie zgadywaniem. */
|
||||||
|
function qualityLabel(p?: number | null): string | null {
|
||||||
|
if (!p || p < 720) return null;
|
||||||
|
return p >= 2160 ? '4K' : `${p}p`;
|
||||||
|
}
|
||||||
|
|
||||||
function titleWithoutStudio(title: string, studio?: string | null): string {
|
function titleWithoutStudio(title: string, studio?: string | null): string {
|
||||||
const t = title.trim();
|
const t = title.trim();
|
||||||
const s = (studio || '').trim();
|
const s = (studio || '').trim();
|
||||||
|
|
@ -111,6 +127,9 @@ function SceneTileBase({ scene, secondLine = 'studio', seenSince, onLongPress }:
|
||||||
: `${Math.floor(dur / 60)}m`
|
: `${Math.floor(dur / 60)}m`
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const qLabel = qualityLabel(scene.max_quality_p);
|
||||||
|
const srcLabel = sourceLabel(scene.playback_sources.find((x) => x.origin)?.origin);
|
||||||
|
|
||||||
const meta = (() => {
|
const meta = (() => {
|
||||||
if (secondLine === 'none') return null;
|
if (secondLine === 'none') return null;
|
||||||
if (secondLine === 'studio') return scene.studio?.name || null;
|
if (secondLine === 'studio') return scene.studio?.name || null;
|
||||||
|
|
@ -164,6 +183,11 @@ function SceneTileBase({ scene, secondLine = 'studio', seenSince, onLongPress }:
|
||||||
<Text style={styles.durText}>{durLabel}</Text>
|
<Text style={styles.durText}>{durLabel}</Text>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
{qLabel ? (
|
||||||
|
<View style={styles.qualBadge}>
|
||||||
|
<Text style={styles.qualText}>{qLabel}</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
{/* Quick-play (user-request): tap ▶ = od razu graj najlepsze źródło (bez wchodzenia
|
{/* Quick-play (user-request): tap ▶ = od razu graj najlepsze źródło (bez wchodzenia
|
||||||
na SceneDetail). Autoplay resolvuje top-source na mount; z ustawioną domyślną
|
na SceneDetail). Autoplay resolvuje top-source na mount; z ustawioną domyślną
|
||||||
jakością to 1 tap → wideo. Tap poza ▶ = szczegóły. Ukryte w trybie wyboru dup. */}
|
jakością to 1 tap → wideo. Tap poza ▶ = szczegóły. Ukryte w trybie wyboru dup. */}
|
||||||
|
|
@ -184,10 +208,19 @@ function SceneTileBase({ scene, secondLine = 'studio', seenSince, onLongPress }:
|
||||||
? titleWithoutStudio(scene.title, scene.studio?.name)
|
? titleWithoutStudio(scene.title, scene.studio?.name)
|
||||||
: scene.title}
|
: scene.title}
|
||||||
</Text>
|
</Text>
|
||||||
{meta ? (
|
{meta || srcLabel ? (
|
||||||
<Text style={styles.meta} numberOfLines={1}>
|
<View style={styles.metaRow}>
|
||||||
{meta}
|
{meta ? (
|
||||||
</Text>
|
<Text style={[styles.meta, styles.metaMain]} numberOfLines={1}>
|
||||||
|
{meta}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
{srcLabel ? (
|
||||||
|
<Text style={[styles.meta, styles.metaSource]} numberOfLines={1}>
|
||||||
|
{srcLabel}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
|
|
@ -255,6 +288,21 @@ const styles = StyleSheet.create({
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
watchedText: { color: theme.fg, fontSize: 11, fontWeight: '700' },
|
watchedText: { color: theme.fg, fontSize: 11, fontWeight: '700' },
|
||||||
|
// Jakość w LEWYM dolnym rogu (czas jest w prawym), ten sam ciężar wizualny, żeby
|
||||||
|
// miniaturka dalej niosła kafelek. Kierunek A: chrome ma być cichy.
|
||||||
|
qualBadge: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 6,
|
||||||
|
left: 6,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.78)',
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
qualText: { color: theme.fg, fontSize: 10, fontFamily: fonts.mono, letterSpacing: 0.3 },
|
||||||
|
metaRow: { flexDirection: 'row', alignItems: 'center', gap: 6, marginTop: 3 },
|
||||||
|
metaMain: { flexShrink: 1 },
|
||||||
|
metaSource: { color: theme.mutedDim },
|
||||||
durBadge: {
|
durBadge: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 6,
|
bottom: 6,
|
||||||
|
|
@ -298,7 +346,6 @@ const styles = StyleSheet.create({
|
||||||
color: theme.muted,
|
color: theme.muted,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontFamily: fonts.mono,
|
fontFamily: fonts.mono,
|
||||||
marginTop: 3,
|
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,9 @@ export interface SceneOut {
|
||||||
* katalogu. Przekazuj dalej przy przejściu do tagu/studia/performera, inaczej
|
* katalogu. Przekazuj dalej przy przejściu do tagu/studia/performera, inaczej
|
||||||
* backend odetnie wertykał i lista wyjdzie pusta albo z obcymi scenami. */
|
* backend odetnie wertykał i lista wyjdzie pusta albo z obcymi scenami. */
|
||||||
vertical?: string | null;
|
vertical?: string | null;
|
||||||
|
/** Najwyższa ZNANA jakość źródeł (720/1080/2160). Brak = żadne źródło jej nie podało
|
||||||
|
* (93,5% katalogu), wtedy kafelek nie pokazuje badge. */
|
||||||
|
max_quality_p?: number | null;
|
||||||
// Kiedy scena trafiła do bazy (ingest). Używane do oznaczenia "NEW", gdy
|
// Kiedy scena trafiła do bazy (ingest). Używane do oznaczenia "NEW", gdy
|
||||||
// `created_at > favoriteSeenSince` (param przekazany z FavoritesScreen).
|
// `created_at > favoriteSeenSince` (param przekazany z FavoritesScreen).
|
||||||
created_at?: string | null;
|
created_at?: string | null;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue