From 068a65dd8cc588b8fd848fd3802449180c8d4fd6 Mon Sep 17 00:00:00 2001 From: goon-foss Date: Tue, 18 Aug 2026 14:36:56 +0200 Subject: [PATCH] feat(ui): przebudowa ekranu szczegolow sceny wg makiety (kierunek A) Hero dostaje badge w rogach: jakosc po lewej, dlugosc po prawej, ten sam jezyk co na kafelku, wiec przejscie z siatki do szczegolow nie zmienia zasad czytania. Sekcja STUDIO zwinieta do linii metadanych (studio - serwis) pod tytulem. Zajmowala caly blok na jeden chip. WAZNE: studio ZOSTAJE klikalne i dalej prowadzi do swoich scen, zwiniecie wygladu nie moglo zabrac funkcji. Zweryfikowane na emulatorze: tap w "Gang AV" otwiera ekran studia z 200 scenami. Sekcja SOURCES usunieta. Pokazywala wewnetrzna nazwe konektora ("TUBE-SCRAPER"), ktora uzytkownikowi nic nie mowila. Zastapiona nazwa serwisu wyliczana z originu (PORNDOE, FULLPORNER) w tej samej linii co studio. Naglowki sekcji dostaly kreske ciagnaca sie do prawej krawedzi: cztery sekcje mialy cztery rozne odstepy i ekran nie mial rytmu. Opis (Description) zostaje, wbrew makiecie, ktora go zgubila. Zweryfikowane na emulatorze na prawdziwych danych (build debugowy podpisany kluczem release): badge dlugosci, linia "Gang AV / PORNDOE", kreski przy naglowkach, zero wystapien "TUBE-SCRAPER". Co-Authored-By: Claude Opus 5 --- mobile/src/changelog.ts | 9 ++ mobile/src/screens/SceneDetailScreen.tsx | 160 ++++++++++++++--------- 2 files changed, 109 insertions(+), 60 deletions(-) diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts index e836368..38b791c 100644 --- a/mobile/src/changelog.ts +++ b/mobile/src/changelog.ts @@ -16,6 +16,15 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + id: '2026-08-18h', + date: 'August 2026', + items: [ + 'Cleaner scene page. Quality and length now sit in the corners of the artwork, and the studio, site and length share one line under it instead of taking a whole section each. The studio is still tappable.', + 'Dropped the "Sources" box that showed an internal name like TUBE-SCRAPER; the site the scene comes from is now named properly next to the studio.', + 'Section headings got a divider line, so the page has a steadier rhythm as you scroll.', + ], + }, { id: '2026-08-18g', date: 'August 2026', diff --git a/mobile/src/screens/SceneDetailScreen.tsx b/mobile/src/screens/SceneDetailScreen.tsx index cb9d27e..66750cb 100644 --- a/mobile/src/screens/SceneDetailScreen.tsx +++ b/mobile/src/screens/SceneDetailScreen.tsx @@ -26,7 +26,7 @@ import { resolveSxyprnPage } from '../lib/sxyprnResolver'; import { resolveEpornerPage } from '../lib/epornerResolver'; import { resolveFpoxxxPage } from '../lib/fpoxxxResolver'; import type { RootStackParamList } from '../navigation'; -import { theme } from '../theme'; +import { fonts, theme } from '../theme'; import type { PlaybackSource, SceneOut, StreamLink } from '../types'; import { PlaybackQualityModal, pickByDefaultQuality } from './PlaybackQualityModal'; import { usePreferences } from '../PreferencesContext'; @@ -243,6 +243,16 @@ export function SceneDetailScreen() { if (error instanceof Error) return {error.message}; if (!data) return null; + // Nazwa serwisu z originu ('tube:sxyprncom' → 'sxyprn'). To zastępuje sekcję SOURCES, + // która pokazywała wewnętrzną nazwę konektora ("TUBE-SCRAPER") i nic użytkownikowi + // nie mówiła (decyzja 2026-08-18). + const sourceLabel = (() => { + const origin = data.playback_sources.find((x) => x.origin)?.origin; + if (!origin) return null; + const raw = origin.replace(/^tube:/, '').trim(); + return raw ? raw.replace(/(com|net|org|xxx|tv|to)$/i, '') || raw : null; + })(); + return ( @@ -265,6 +275,30 @@ export function SceneDetailScreen() { ) : null} + {/* Studio · serwis · długość w jednej linii, zamiast osobnej sekcji STUDIO na + jeden chip. Studio ZOSTAJE klikalne, bo to wejście do jego scen (zwinięcie + wyglądu nie może zabrać funkcji). */} + + {data.studio ? ( + + nav.push('StudioScenes', { + id: data.studio!.slug, + studioId: data.studio!.id, + name: data.studio!.name, + vertical: data.vertical, + }) + } + > + + {data.studio.name} + {data.studio.network ? ` · ${data.studio.network}` : ''} + + + ) : null} + {sourceLabel ? {sourceLabel} : null} + + {(data.code || data.director) && ( {data.code ? code · {data.code} : null} @@ -296,31 +330,6 @@ export function SceneDetailScreen() { )} - {data.studio && ( -
- - - nav.push('StudioScenes', { - id: data.studio!.slug, - studioId: data.studio!.id, - name: data.studio!.name, - // Bez tego backend odcina wertykał i studio animacyjne wychodzi - // puste, co wyglądało jakby kliknięcie nic nie robiło (195ae42e). - vertical: data.vertical, - }) - } - > - - {data.studio.name} - {data.studio.network ? ` · ${data.studio.network}` : ''} - - - -
- )} - {data.tags.length > 0 && (
@@ -340,29 +349,6 @@ export function SceneDetailScreen() {
)} - {data.external_refs.length > 0 && ( -
- - {(() => { - // Sceny popularne mogą mieć dziesiątki refów na ten sam tube (różne URL, - // np. /search/.../ + /tag/... + /performer/..., wszystkie dla tej samej sceny). - // Deduplikujemy po source name, pokazujemy count jeśli > 1. - const counts = new Map(); - for (const r of data.external_refs) { - counts.set(r.source, (counts.get(r.source) ?? 0) + 1); - } - return Array.from(counts.entries()).map(([source, count]) => ( - - - {source}{count > 1 ? ` ×${count}` : ''} - - - )); - })()} - -
- )} - {data.playback_sources.length > 0 && (
{data.playback_sources.map((p, i) => ( @@ -467,14 +453,22 @@ function Hero({ if (!url) onThumbState?.('none'); }, [url, onThumbState]); - const minutes = data.duration_sec ? Math.floor(data.duration_sec / 60) : null; - const meta = [ - data.release_date ?? null, - data.studio?.name ?? null, - minutes !== null ? `${minutes} min` : null, - ] - .filter(Boolean) - .join(' · '); + // Data zostaje na hero; studio/serwis/długość przeniesione do klikalnej linii w body + // (makieta 2026-08-18), bo studio MUSI dalej prowadzić do swoich scen. + const meta = data.release_date ?? ''; + const dur = data.duration_sec ?? 0; + const durLabel = + dur > 0 + ? dur >= 3600 + ? `${Math.floor(dur / 3600)}h${String(Math.floor((dur % 3600) / 60)).padStart(2, '0')}` + : `${Math.floor(dur / 60)}m` + : null; + const qLabel = + data.max_quality_p && data.max_quality_p >= 720 + ? data.max_quality_p >= 2160 + ? '4K' + : `${data.max_quality_p}p` + : null; if (!url) { return ( @@ -503,6 +497,18 @@ function Hero({ style={StyleSheet.absoluteFill} pointerEvents="none" /> + {/* Rogi jak na kafelku: jakość po lewej, długość po prawej. Ten sam język, + więc przejście z siatki do szczegółów nie zmienia zasad czytania. */} + {qLabel ? ( + + {qLabel} + + ) : null} + {durLabel ? ( + + {durLabel} + + ) : null} {data.title} @@ -513,10 +519,15 @@ function Hero({ ); } +/** Nagłówek + cienka kreska ciągnąca się do prawej krawędzi. Daje ekranowi rytm, + * którego brakowało, gdy cztery sekcje miały cztery różne odstępy (makieta 2026-08-18). */ function Section({ title, children }: { title: string; children: React.ReactNode }) { return ( - {title} + + {title} + + {children} ); @@ -947,6 +958,34 @@ const styles = StyleSheet.create({ backgroundColor: theme.card, overflow: 'hidden', }, + heroBadgeLeft: { + position: 'absolute', + left: 12, + bottom: 12, + backgroundColor: 'rgba(0,0,0,0.78)', + paddingHorizontal: 7, + paddingVertical: 3, + borderRadius: 5, + }, + heroBadgeRight: { + position: 'absolute', + right: 12, + bottom: 12, + backgroundColor: 'rgba(0,0,0,0.78)', + paddingHorizontal: 7, + paddingVertical: 3, + borderRadius: 5, + }, + heroBadgeText: { color: theme.fg, fontSize: 11, fontFamily: fonts.mono, letterSpacing: 0.3 }, + factRow: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', gap: 10, marginTop: 10 }, + factLink: { color: theme.accentText, fontSize: 13, fontFamily: fonts.medium }, + factDim: { + color: theme.mutedDim, + fontSize: 11, + fontFamily: fonts.mono, + textTransform: 'uppercase', + letterSpacing: 0.5, + }, heroOverlay: { position: 'absolute', left: 16, @@ -980,12 +1019,13 @@ const styles = StyleSheet.create({ metaRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 14, marginTop: 8 }, metaDim: { color: theme.mutedDim, fontSize: 12, fontWeight: '500' }, - section: { marginTop: 20 }, + section: { marginTop: 22 }, + sectionHead: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 10 }, + sectionRule: { flex: 1, height: 1, backgroundColor: theme.border }, sectionTitle: { color: theme.mutedDim, fontSize: 11, textTransform: 'uppercase', - marginBottom: 10, letterSpacing: 1.2, fontWeight: '700', },