style(mobile): SceneTile shared component, 2-col grid w 5 ekranach scen
Jan feedback po pierwszym overhaulu: layout 2-col tile pasuje, ale aktywnie tylko na ScenesScreen - reszta ekranow scen (SiteScenes, PerformerScenes, StudioScenes, TagScenes) dalej w full-width row layoucie. Wyciagniety SceneTile do mobile/src/components/SceneTile.tsx ze wsparciem: - secondLine: 'studio' | 'performers' | 'date' | 'none' - per-ekran dobor metadanej (Studio na SiteScenes/Performer, performers na Studio, etc) - seenSince: ISO timestamp - pokazuje NEW badge gdy scene.created_at > seen (uzywane na Performer/Studio screens dla NEW od ostatniego markFavoriteSeen) - onLongPress: opcjonalny custom handler (default = animated preview) Refaktor 5 ekranow: - ScenesScreen: usuwa lokalna kopie SceneTile, import shared - SiteScenesScreen: SceneRow -> SceneTile (numColumns=2, secondLine='studio') - PerformerScenesScreen: FavoriteSceneRow -> SceneTile (numColumns=2) - StudioScenesScreen: FavoriteSceneRow -> SceneTile (numColumns=2, performers) - TagScenesScreen: lokalna SceneRow -> SceneTile FavoriteSceneRow component zostaje (legacy import w PerformerScenes - nie ruszamy bo moze byc uzyty w innym kontekscie). gridRow style scaffold (gap+ marginBottom) dodany w kazdym StyleSheet osobno bo te ekrany maja rozne paddingHorizontal w container. OTA: 9eea7ac6-df72-460e-9660-22bf6c39c3ac live, runtime 1.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aa647dcf97
commit
177a45eee7
6 changed files with 239 additions and 144 deletions
216
mobile/src/components/SceneTile.tsx
Normal file
216
mobile/src/components/SceneTile.tsx
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
/**
|
||||||
|
* SceneTile — 2-col 16:9 grid item używany w listach scen
|
||||||
|
* (Scenes, SiteScenes, PerformerScenes, StudioScenes, TagScenes).
|
||||||
|
*
|
||||||
|
* Per impeccable.style/slop + Jan feedback "większe miniaturki, mniej tekstu":
|
||||||
|
* - Thumb wypełnia szerokość kolumny (aspect 16:9)
|
||||||
|
* - Title 1 linijka, weight 600, letter-spacing -0.2
|
||||||
|
* - Pod tytułem: 1 linia uppercase micro-meta (studio | performers | tag — wybór per ekran)
|
||||||
|
* - Overlay na thumb: fav (top-left), duration (bottom-right), NEW (top-right gdy seenSince),
|
||||||
|
* ✓watched (top-right gdy finished)
|
||||||
|
* - Long-press → animated preview (gdy playback_source ma animated_thumbnail_url)
|
||||||
|
*
|
||||||
|
* Zostawia Pressable do parenta dla custom onLongPress (np. delete-from-favorites) —
|
||||||
|
* default onLongPress robi preview (jak w ScenesScreen).
|
||||||
|
*
|
||||||
|
* Used inline w 2-column FlatList:
|
||||||
|
* <FlatList
|
||||||
|
* numColumns={2}
|
||||||
|
* columnWrapperStyle={{ gap: 10, marginBottom: 14 }}
|
||||||
|
* renderItem={({ item }) => <SceneTile scene={item} secondLine="studio" />}
|
||||||
|
* />
|
||||||
|
*/
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
|
import * as Haptics from 'expo-haptics';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||||
|
|
||||||
|
import type { RootStackParamList } from '../navigation';
|
||||||
|
import { theme } from '../theme';
|
||||||
|
import type { SceneOut } from '../types';
|
||||||
|
import { Thumb } from './Thumb';
|
||||||
|
|
||||||
|
export type SecondLine = 'studio' | 'performers' | 'date' | 'none';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
scene: SceneOut;
|
||||||
|
secondLine?: SecondLine;
|
||||||
|
/**
|
||||||
|
* Pokazuj NEW badge gdy scene.created_at > seenSince. Używane na ekranach
|
||||||
|
* Performer/Studio scenes — gdy user owner'ował fav, last_seen_at z favorite
|
||||||
|
* jest porównywany ze sceną.
|
||||||
|
*/
|
||||||
|
seenSince?: string;
|
||||||
|
/**
|
||||||
|
* Custom long-press handler (np. removal from favorites). Default = animated
|
||||||
|
* preview gdy thumb ma animated wariant.
|
||||||
|
*/
|
||||||
|
onLongPress?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SceneTile({ scene, secondLine = 'studio', seenSince, onLongPress }: Props) {
|
||||||
|
const navigation =
|
||||||
|
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||||
|
const [isPreviewing, setIsPreviewing] = useState(false);
|
||||||
|
|
||||||
|
const animatedUrl = scene.playback_sources.find((s) => s.animated_thumbnail_url)
|
||||||
|
?.animated_thumbnail_url;
|
||||||
|
const staticUrl = scene.playback_sources.find((s) => s.thumbnail_url)?.thumbnail_url;
|
||||||
|
const displayUrl = isPreviewing && animatedUrl ? animatedUrl : staticUrl ?? animatedUrl;
|
||||||
|
|
||||||
|
const startPreview = () => {
|
||||||
|
if (onLongPress) {
|
||||||
|
onLongPress();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!animatedUrl) return;
|
||||||
|
setIsPreviewing(true);
|
||||||
|
Haptics.selectionAsync().catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const dim = scene.finished === true;
|
||||||
|
const isNew = !!(seenSince && scene.created_at && scene.created_at > seenSince);
|
||||||
|
const dur = scene.duration_sec;
|
||||||
|
const durLabel =
|
||||||
|
dur && 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 meta = (() => {
|
||||||
|
if (secondLine === 'none') return null;
|
||||||
|
if (secondLine === 'studio') return scene.studio?.name || null;
|
||||||
|
if (secondLine === 'performers') {
|
||||||
|
if (scene.performers.length === 0) return null;
|
||||||
|
const names = scene.performers.slice(0, 2).map((p) => p.canonical_name).join(', ');
|
||||||
|
return scene.performers.length > 2 ? `${names} +${scene.performers.length - 2}` : names;
|
||||||
|
}
|
||||||
|
if (secondLine === 'date') return scene.release_date;
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
style={styles.tile}
|
||||||
|
onPress={() => navigation.navigate('SceneDetail', { id: scene.id })}
|
||||||
|
onLongPress={startPreview}
|
||||||
|
onPressOut={() => setIsPreviewing(false)}
|
||||||
|
delayLongPress={180}
|
||||||
|
>
|
||||||
|
<View style={[styles.thumbWrap, dim && styles.thumbDim]}>
|
||||||
|
<Thumb url={displayUrl} style={styles.thumb} />
|
||||||
|
{scene.is_favorite ? (
|
||||||
|
<View style={styles.favBadge}>
|
||||||
|
<Text style={styles.favBadgeText}>★</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
{isNew ? (
|
||||||
|
<View style={styles.newBadge}>
|
||||||
|
<Text style={styles.newBadgeText}>NEW</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
{dim ? (
|
||||||
|
<View style={styles.watchedBadge}>
|
||||||
|
<Text style={styles.watchedText}>✓</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
{durLabel ? (
|
||||||
|
<View style={styles.durBadge}>
|
||||||
|
<Text style={styles.durText}>{durLabel}</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
<Text style={[styles.title, dim && styles.titleDim]} numberOfLines={1}>
|
||||||
|
{scene.title}
|
||||||
|
</Text>
|
||||||
|
{meta ? (
|
||||||
|
<Text style={styles.meta} numberOfLines={1}>
|
||||||
|
{meta}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
tile: { flex: 1 },
|
||||||
|
thumbWrap: {
|
||||||
|
width: '100%',
|
||||||
|
aspectRatio: 16 / 9,
|
||||||
|
borderRadius: 6,
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
backgroundColor: theme.bgElevated,
|
||||||
|
},
|
||||||
|
thumb: { width: '100%', height: '100%' },
|
||||||
|
thumbDim: { opacity: 0.45 },
|
||||||
|
favBadge: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 6,
|
||||||
|
left: 6,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||||
|
paddingHorizontal: 5,
|
||||||
|
paddingVertical: 1,
|
||||||
|
borderRadius: 6,
|
||||||
|
},
|
||||||
|
favBadgeText: { color: theme.accent, fontSize: 11, fontWeight: '700' },
|
||||||
|
newBadge: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 6,
|
||||||
|
right: 6,
|
||||||
|
backgroundColor: theme.accent,
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
newBadgeText: { color: theme.fg, fontSize: 9, fontWeight: '800', letterSpacing: 0.6 },
|
||||||
|
watchedBadge: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 6,
|
||||||
|
right: 6,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.78)',
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
borderRadius: 999,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
watchedText: { color: theme.fg, fontSize: 11, fontWeight: '700' },
|
||||||
|
durBadge: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 6,
|
||||||
|
right: 6,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.78)',
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
durText: {
|
||||||
|
color: theme.fg,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: '600',
|
||||||
|
fontVariant: ['tabular-nums'],
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
color: theme.fg,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginTop: 8,
|
||||||
|
letterSpacing: -0.2,
|
||||||
|
},
|
||||||
|
titleDim: { color: theme.muted },
|
||||||
|
meta: {
|
||||||
|
color: theme.muted,
|
||||||
|
fontSize: 11,
|
||||||
|
marginTop: 2,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sceneTileGridProps = {
|
||||||
|
numColumns: 2 as const,
|
||||||
|
columnWrapperStyle: { gap: 10, marginBottom: 14 },
|
||||||
|
};
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
import { useClient } from '../ClientContext';
|
import { useClient } from '../ClientContext';
|
||||||
import { FavoriteSceneRow } from '../components/FavoriteSceneRow';
|
import { FavoriteSceneRow } from '../components/FavoriteSceneRow';
|
||||||
import { MoviePosterCard } from '../components/MoviePosterCard';
|
import { MoviePosterCard } from '../components/MoviePosterCard';
|
||||||
|
import { SceneTile } from '../components/SceneTile';
|
||||||
import { ErrorBoundary } from '../ErrorBoundary';
|
import { ErrorBoundary } from '../ErrorBoundary';
|
||||||
import type { RootStackParamList } from '../navigation';
|
import type { RootStackParamList } from '../navigation';
|
||||||
import { theme } from '../theme';
|
import { theme } from '../theme';
|
||||||
|
|
@ -270,9 +271,11 @@ export function PerformerScenesScreen() {
|
||||||
key="scenes-list"
|
key="scenes-list"
|
||||||
data={sortedScenes}
|
data={sortedScenes}
|
||||||
keyExtractor={(s) => s.id}
|
keyExtractor={(s) => s.id}
|
||||||
|
numColumns={2}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<FavoriteSceneRow scene={item} seenSince={seenSince} secondLine="studio" />
|
<SceneTile scene={item} seenSince={seenSince} secondLine="studio" />
|
||||||
)}
|
)}
|
||||||
|
columnWrapperStyle={styles.gridRow}
|
||||||
refreshing={scenesQuery.isRefetching}
|
refreshing={scenesQuery.isRefetching}
|
||||||
onRefresh={scenesQuery.refetch}
|
onRefresh={scenesQuery.refetch}
|
||||||
ListHeaderComponent={
|
ListHeaderComponent={
|
||||||
|
|
@ -417,6 +420,7 @@ function TabButton({
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: 12, paddingTop: 8 },
|
container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: 12, paddingTop: 8 },
|
||||||
|
gridRow: { gap: 10, marginBottom: 14 },
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
|
import { SceneTile } from '../components/SceneTile';
|
||||||
import { Thumb } from '../components/Thumb';
|
import { Thumb } from '../components/Thumb';
|
||||||
import { useClient } from '../ClientContext';
|
import { useClient } from '../ClientContext';
|
||||||
import { theme } from '../theme';
|
import { theme } from '../theme';
|
||||||
|
|
@ -223,80 +224,6 @@ function FavoritesButton() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* SceneTile — 2-col 16:9 grid item, minimal text (video portal).
|
|
||||||
*
|
|
||||||
* Per impeccable.style/slop + Jan feedback "większe miniaturki, mniej tekstu":
|
|
||||||
* - Thumb wypełnia szerokość kolumny (16:9 aspect ratio)
|
|
||||||
* - Title 1 linijka, system font, weight 600
|
|
||||||
* - Overlay: studio (mono small) + duration → tylko na thumb, NIE pod
|
|
||||||
* - Wyrzucone: performers, release_date, sources count, "✓ watched" string
|
|
||||||
* (zastąpione check badge na thumb)
|
|
||||||
* - Long-press → animated preview (zachowane)
|
|
||||||
*/
|
|
||||||
function SceneTile({ scene }: { scene: SceneOut }) {
|
|
||||||
const navigation =
|
|
||||||
useNavigation<NativeStackNavigationProp<RootStackParamList, 'Scenes'>>();
|
|
||||||
const [isPreviewing, setIsPreviewing] = useState(false);
|
|
||||||
|
|
||||||
const animatedUrl = scene.playback_sources.find((s) => s.animated_thumbnail_url)
|
|
||||||
?.animated_thumbnail_url;
|
|
||||||
const staticUrl = scene.playback_sources.find((s) => s.thumbnail_url)?.thumbnail_url;
|
|
||||||
const displayUrl = isPreviewing && animatedUrl ? animatedUrl : staticUrl ?? animatedUrl;
|
|
||||||
|
|
||||||
const startPreview = () => {
|
|
||||||
if (!animatedUrl) return;
|
|
||||||
setIsPreviewing(true);
|
|
||||||
Haptics.selectionAsync().catch(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const dim = scene.finished === true;
|
|
||||||
const dur = scene.duration_sec;
|
|
||||||
const durLabel =
|
|
||||||
dur && dur > 0
|
|
||||||
? dur >= 3600
|
|
||||||
? `${Math.floor(dur / 3600)}h${String(Math.floor((dur % 3600) / 60)).padStart(2, '0')}`
|
|
||||||
: `${Math.floor(dur / 60)}m`
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pressable
|
|
||||||
style={styles.tile}
|
|
||||||
onPress={() => navigation.navigate('SceneDetail', { id: scene.id })}
|
|
||||||
onLongPress={startPreview}
|
|
||||||
onPressOut={() => setIsPreviewing(false)}
|
|
||||||
delayLongPress={180}
|
|
||||||
>
|
|
||||||
<View style={[styles.tileThumbWrap, dim && styles.tileThumbDim]}>
|
|
||||||
<Thumb url={displayUrl} style={styles.tileThumb} />
|
|
||||||
{scene.is_favorite ? (
|
|
||||||
<View style={styles.tileFavBadge}>
|
|
||||||
<Text style={styles.tileFavBadgeText}>★</Text>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
{durLabel ? (
|
|
||||||
<View style={styles.tileDurBadge}>
|
|
||||||
<Text style={styles.tileDurText}>{durLabel}</Text>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
{dim ? (
|
|
||||||
<View style={styles.tileWatchedBadge}>
|
|
||||||
<Text style={styles.tileWatchedText}>✓</Text>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
<Text style={[styles.tileTitle, dim && styles.tileTitleDim]} numberOfLines={1}>
|
|
||||||
{scene.title}
|
|
||||||
</Text>
|
|
||||||
{scene.studio?.name ? (
|
|
||||||
<Text style={styles.tileStudio} numberOfLines={1}>
|
|
||||||
{scene.studio.name}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SceneRow({ scene }: { scene: SceneOut }) {
|
function SceneRow({ scene }: { scene: SceneOut }) {
|
||||||
const navigation =
|
const navigation =
|
||||||
useNavigation<NativeStackNavigationProp<RootStackParamList, 'Scenes'>>();
|
useNavigation<NativeStackNavigationProp<RootStackParamList, 'Scenes'>>();
|
||||||
|
|
@ -506,71 +433,7 @@ const styles = StyleSheet.create({
|
||||||
lineHeight: 15,
|
lineHeight: 15,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 2-col grid (SceneTile) — wprowadzone 2026-05-29 (UI overhaul, Jan feedback
|
// 2-col grid wrapper — SceneTile sam ma styling tile/thumb/etc, tu tylko
|
||||||
// "większe miniaturki, mniej tekstu"). Wcześniej był full-width SceneRow z 6
|
// odstęp między tilami w wierszu i pod wierszem.
|
||||||
// liniami tekstu — sloppy density dla video portalu.
|
|
||||||
gridRow: { gap: 10, marginBottom: 14 },
|
gridRow: { gap: 10, marginBottom: 14 },
|
||||||
tile: { flex: 1 },
|
|
||||||
tileThumbWrap: {
|
|
||||||
width: '100%',
|
|
||||||
aspectRatio: 16 / 9,
|
|
||||||
borderRadius: 6,
|
|
||||||
overflow: 'hidden',
|
|
||||||
position: 'relative',
|
|
||||||
backgroundColor: theme.bgElevated,
|
|
||||||
},
|
|
||||||
tileThumb: { width: '100%', height: '100%' },
|
|
||||||
tileThumbDim: { opacity: 0.45 },
|
|
||||||
tileFavBadge: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 6,
|
|
||||||
left: 6,
|
|
||||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
||||||
paddingHorizontal: 5,
|
|
||||||
paddingVertical: 1,
|
|
||||||
borderRadius: 6,
|
|
||||||
},
|
|
||||||
tileFavBadgeText: { color: theme.accent, fontSize: 11, fontWeight: '700' },
|
|
||||||
tileDurBadge: {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 6,
|
|
||||||
right: 6,
|
|
||||||
backgroundColor: 'rgba(0,0,0,0.78)',
|
|
||||||
paddingHorizontal: 6,
|
|
||||||
paddingVertical: 2,
|
|
||||||
borderRadius: 4,
|
|
||||||
},
|
|
||||||
tileDurText: {
|
|
||||||
color: theme.fg,
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: '600',
|
|
||||||
fontVariant: ['tabular-nums'],
|
|
||||||
},
|
|
||||||
tileWatchedBadge: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 6,
|
|
||||||
right: 6,
|
|
||||||
backgroundColor: 'rgba(0,0,0,0.78)',
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
borderRadius: 999,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
tileWatchedText: { color: theme.fg, fontSize: 11, fontWeight: '700' },
|
|
||||||
tileTitle: {
|
|
||||||
color: theme.fg,
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '600',
|
|
||||||
marginTop: 8,
|
|
||||||
letterSpacing: -0.2,
|
|
||||||
},
|
|
||||||
tileTitleDim: { color: theme.muted },
|
|
||||||
tileStudio: {
|
|
||||||
color: theme.muted,
|
|
||||||
fontSize: 11,
|
|
||||||
marginTop: 2,
|
|
||||||
letterSpacing: 0.3,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import {
|
||||||
TextInput,
|
TextInput,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { SceneTile } from '../components/SceneTile';
|
||||||
import { Thumb } from '../components/Thumb';
|
import { Thumb } from '../components/Thumb';
|
||||||
import { useClient } from '../ClientContext';
|
import { useClient } from '../ClientContext';
|
||||||
import type { RootStackParamList } from '../navigation';
|
import type { RootStackParamList } from '../navigation';
|
||||||
|
|
@ -107,7 +108,9 @@ export function SiteScenesScreen() {
|
||||||
<FlatList
|
<FlatList
|
||||||
data={items}
|
data={items}
|
||||||
keyExtractor={(s) => s.id}
|
keyExtractor={(s) => s.id}
|
||||||
renderItem={({ item }) => <SceneRow scene={item} />}
|
numColumns={2}
|
||||||
|
renderItem={({ item }) => <SceneTile scene={item} secondLine="studio" />}
|
||||||
|
columnWrapperStyle={styles.gridRow}
|
||||||
refreshing={isRefetching}
|
refreshing={isRefetching}
|
||||||
onRefresh={refetch}
|
onRefresh={refetch}
|
||||||
onEndReached={() => {
|
onEndReached={() => {
|
||||||
|
|
@ -353,6 +356,7 @@ const styles = StyleSheet.create({
|
||||||
muted: { color: theme.muted, textAlign: 'center', marginTop: 24, fontSize: 14 },
|
muted: { color: theme.muted, textAlign: 'center', marginTop: 24, fontSize: 14 },
|
||||||
error: { color: theme.bad, padding: 12 },
|
error: { color: theme.bad, padding: 12 },
|
||||||
toolbar: { flexDirection: 'row', gap: 8, marginBottom: 8, alignItems: 'center' },
|
toolbar: { flexDirection: 'row', gap: 8, marginBottom: 8, alignItems: 'center' },
|
||||||
|
gridRow: { gap: 10, marginBottom: 14 },
|
||||||
filterBtn: {
|
filterBtn: {
|
||||||
paddingHorizontal: 14,
|
paddingHorizontal: 14,
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { FavoriteSceneRow } from '../components/FavoriteSceneRow';
|
import { FavoriteSceneRow } from '../components/FavoriteSceneRow';
|
||||||
|
import { SceneTile } from '../components/SceneTile';
|
||||||
import { useClient } from '../ClientContext';
|
import { useClient } from '../ClientContext';
|
||||||
import type { RootStackParamList } from '../navigation';
|
import type { RootStackParamList } from '../navigation';
|
||||||
import { theme } from '../theme';
|
import { theme } from '../theme';
|
||||||
|
|
@ -136,9 +137,11 @@ export function StudioScenesScreen() {
|
||||||
<FlatList
|
<FlatList
|
||||||
data={sortedItems}
|
data={sortedItems}
|
||||||
keyExtractor={(s) => s.id}
|
keyExtractor={(s) => s.id}
|
||||||
|
numColumns={2}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<FavoriteSceneRow scene={item} seenSince={seenSince} secondLine="performers" />
|
<SceneTile scene={item} seenSince={seenSince} secondLine="performers" />
|
||||||
)}
|
)}
|
||||||
|
columnWrapperStyle={styles.gridRow}
|
||||||
refreshing={isRefetching}
|
refreshing={isRefetching}
|
||||||
onRefresh={refetch}
|
onRefresh={refetch}
|
||||||
ListHeaderComponent={
|
ListHeaderComponent={
|
||||||
|
|
@ -157,6 +160,7 @@ export function StudioScenesScreen() {
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: 12, paddingTop: 8 },
|
container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: 12, paddingTop: 8 },
|
||||||
|
gridRow: { gap: 10, marginBottom: 14 },
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
Text,
|
Text,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { SceneTile } from '../components/SceneTile';
|
||||||
import { useClient } from '../ClientContext';
|
import { useClient } from '../ClientContext';
|
||||||
import type { RootStackParamList } from '../navigation';
|
import type { RootStackParamList } from '../navigation';
|
||||||
import { theme } from '../theme';
|
import { theme } from '../theme';
|
||||||
|
|
@ -46,7 +47,9 @@ export function TagScenesScreen() {
|
||||||
<FlatList
|
<FlatList
|
||||||
data={data?.items ?? []}
|
data={data?.items ?? []}
|
||||||
keyExtractor={(s) => s.id}
|
keyExtractor={(s) => s.id}
|
||||||
renderItem={({ item }) => <SceneRow scene={item} />}
|
numColumns={2}
|
||||||
|
renderItem={({ item }) => <SceneTile scene={item} secondLine="studio" />}
|
||||||
|
columnWrapperStyle={styles.gridRow}
|
||||||
refreshing={isRefetching}
|
refreshing={isRefetching}
|
||||||
onRefresh={refetch}
|
onRefresh={refetch}
|
||||||
ListHeaderComponent={
|
ListHeaderComponent={
|
||||||
|
|
@ -99,6 +102,7 @@ function SceneRow({ scene }: { scene: SceneOut }) {
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: 12, paddingTop: 8 },
|
container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: 12, paddingTop: 8 },
|
||||||
|
gridRow: { gap: 10, marginBottom: 14 },
|
||||||
subtitle: { color: theme.muted, marginBottom: 8, paddingHorizontal: 4 },
|
subtitle: { color: theme.muted, marginBottom: 8, paddingHorizontal: 4 },
|
||||||
row: {
|
row: {
|
||||||
backgroundColor: theme.card,
|
backgroundColor: theme.card,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue