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 <noreply@anthropic.com>
This commit is contained in:
parent
bbc2059728
commit
068a65dd8c
2 changed files with 109 additions and 60 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 <Text style={styles.error}>{error.message}</Text>;
|
||||
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 (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
|
||||
<Hero data={data} onThumbState={setThumbState} />
|
||||
|
|
@ -265,6 +275,30 @@ export function SceneDetailScreen() {
|
|||
</Pressable>
|
||||
) : 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). */}
|
||||
<View style={styles.factRow}>
|
||||
{data.studio ? (
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
nav.push('StudioScenes', {
|
||||
id: data.studio!.slug,
|
||||
studioId: data.studio!.id,
|
||||
name: data.studio!.name,
|
||||
vertical: data.vertical,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Text style={styles.factLink} numberOfLines={1}>
|
||||
{data.studio.name}
|
||||
{data.studio.network ? ` · ${data.studio.network}` : ''}
|
||||
</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
{sourceLabel ? <Text style={styles.factDim}>{sourceLabel}</Text> : null}
|
||||
</View>
|
||||
|
||||
{(data.code || data.director) && (
|
||||
<View style={styles.metaRow}>
|
||||
{data.code ? <Text style={styles.metaDim}>code · {data.code}</Text> : null}
|
||||
|
|
@ -296,31 +330,6 @@ export function SceneDetailScreen() {
|
|||
</Section>
|
||||
)}
|
||||
|
||||
{data.studio && (
|
||||
<Section title="Studio">
|
||||
<View style={styles.pillRow}>
|
||||
<Pressable
|
||||
style={styles.pillPrimary}
|
||||
onPress={() =>
|
||||
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,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Text style={styles.pillPrimaryText}>
|
||||
{data.studio.name}
|
||||
{data.studio.network ? ` · ${data.studio.network}` : ''}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{data.tags.length > 0 && (
|
||||
<Section title="Tags">
|
||||
<View style={styles.pillRow}>
|
||||
|
|
@ -340,29 +349,6 @@ export function SceneDetailScreen() {
|
|||
</Section>
|
||||
)}
|
||||
|
||||
{data.external_refs.length > 0 && (
|
||||
<Section title="Sources">
|
||||
<View style={styles.pillRow}>
|
||||
{(() => {
|
||||
// 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<string, number>();
|
||||
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]) => (
|
||||
<View key={source} style={styles.pillSource}>
|
||||
<Text style={styles.pillSourceText}>
|
||||
{source}{count > 1 ? ` ×${count}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
));
|
||||
})()}
|
||||
</View>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{data.playback_sources.length > 0 && (
|
||||
<Section title="Watch">
|
||||
{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 ? (
|
||||
<View style={styles.heroBadgeLeft}>
|
||||
<Text style={styles.heroBadgeText}>{qLabel}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{durLabel ? (
|
||||
<View style={styles.heroBadgeRight}>
|
||||
<Text style={styles.heroBadgeText}>{durLabel}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
<View style={styles.heroOverlay}>
|
||||
<Text style={styles.heroTitle} numberOfLines={3}>
|
||||
{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 (
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>{title}</Text>
|
||||
<View style={styles.sectionHead}>
|
||||
<Text style={styles.sectionTitle}>{title}</Text>
|
||||
<View style={styles.sectionRule} />
|
||||
</View>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
|
|
@ -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',
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue