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:
|
||||
# sxyprn → żywy on-demand resolver (martwy stored URL ignorujemy),
|
||||
# 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:
|
||||
# youporn z podpisanym (wygasającym) posterem → on-demand resolver.
|
||||
# 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:
|
||||
if _is_rotting_thumb(thumb) or _is_flaky_thumb(thumb):
|
||||
# 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).
|
||||
prev = thumb_fallback.get(sid)
|
||||
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:
|
||||
thumb_by_scene[sid] = (thumb, page_url)
|
||||
thumb_by_scene[sid] = (thumb, page_url, origin)
|
||||
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).
|
||||
for sid, val in thumb_fallback.items():
|
||||
thumb_by_scene.setdefault(sid, val)
|
||||
|
|
@ -911,11 +911,12 @@ def _build_scenes_out_batch(
|
|||
t_url = _wrap_image_proxy(t_url, ref)
|
||||
if a_url and _needs_proxy(a_url):
|
||||
a_url = _wrap_image_proxy(a_url, ref)
|
||||
# id/origin/page_url wymagane przez schemat ale nieużywane przez kafelek
|
||||
# (SceneDetail re-fetchuje pełne źródła), dummy sentinel.
|
||||
# `origin` niesiemy PRAWDZIWY (źródła wybranej miniatury): kafelek pokazuje
|
||||
# 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(
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
|
@ -992,6 +993,7 @@ def _build_scenes_out_batch(
|
|||
external_refs=refs_by_scene.get(scene.id, []),
|
||||
playback_sources=pb_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,
|
||||
backfill=scene.backfill,
|
||||
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,
|
||||
playback_sources=playback_out,
|
||||
vertical=verticals.vertical_for_origins(p.origin for p in playback_rows),
|
||||
max_quality_p=scene.max_quality_p,
|
||||
created_at=scene.created_at,
|
||||
backfill=scene.backfill,
|
||||
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
|
||||
# wyglądało na martwe (zgłoszenia 195ae42e / e9de414c, 2026-08-14).
|
||||
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
|
||||
# "NEW" na karcie scen w PerformerScenesScreen / StudioScenesScreen, gdy
|
||||
# `created_at > last_seen_at` (favorite) → badge.
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@ export type 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',
|
||||
date: 'August 2026',
|
||||
|
|
|
|||
|
|
@ -63,6 +63,22 @@ interface Props {
|
|||
* Gdy po zdjęciu zostaje mniej niż 12 znaków, ODDAJEMY tytuł nietknięty — lepsze
|
||||
* 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 {
|
||||
const t = title.trim();
|
||||
const s = (studio || '').trim();
|
||||
|
|
@ -111,6 +127,9 @@ function SceneTileBase({ scene, secondLine = 'studio', seenSince, onLongPress }:
|
|||
: `${Math.floor(dur / 60)}m`
|
||||
: null;
|
||||
|
||||
const qLabel = qualityLabel(scene.max_quality_p);
|
||||
const srcLabel = sourceLabel(scene.playback_sources.find((x) => x.origin)?.origin);
|
||||
|
||||
const meta = (() => {
|
||||
if (secondLine === 'none') return 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>
|
||||
</View>
|
||||
) : 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
|
||||
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. */}
|
||||
|
|
@ -184,11 +208,20 @@ function SceneTileBase({ scene, secondLine = 'studio', seenSince, onLongPress }:
|
|||
? titleWithoutStudio(scene.title, scene.studio?.name)
|
||||
: scene.title}
|
||||
</Text>
|
||||
{meta || srcLabel ? (
|
||||
<View style={styles.metaRow}>
|
||||
{meta ? (
|
||||
<Text style={styles.meta} numberOfLines={1}>
|
||||
<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}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
|
@ -255,6 +288,21 @@ const styles = StyleSheet.create({
|
|||
justifyContent: 'center',
|
||||
},
|
||||
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: {
|
||||
position: 'absolute',
|
||||
bottom: 6,
|
||||
|
|
@ -298,7 +346,6 @@ const styles = StyleSheet.create({
|
|||
color: theme.muted,
|
||||
fontSize: 10,
|
||||
fontFamily: fonts.mono,
|
||||
marginTop: 3,
|
||||
letterSpacing: 0.5,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -177,6 +177,9 @@ export interface SceneOut {
|
|||
* katalogu. Przekazuj dalej przy przejściu do tagu/studia/performera, inaczej
|
||||
* backend odetnie wertykał i lista wyjdzie pusta albo z obcymi scenami. */
|
||||
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
|
||||
// `created_at > favoriteSeenSince` (param przekazany z FavoritesScreen).
|
||||
created_at?: string | null;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue