Pomiar WCAG na realnych parach z apki (2026-08-13):
accent #B23A48 jako tekst: 3,21 na bg | 2,96 na bgElevated | 2,75 na card
border #3A3128 jako granica: 1,48 na bg | 1,27 na card
Czyli kolor MARKI byl ponizej nawet progu 3:1 dla elementow interfejsu,
a nie tylko ponizej 4,5 dla tekstu. Dotyczylo to aktywnych zakladek,
gwiazdek ulubionych, linkow i chipow. Ramki przy 1,27 nie byly granica,
tylko cieniem.
NIE zmieniam koloru marki. Podniesienie samego `accent` do progu tekstu
wymagaloby zejscia w roz (#CE6975), co likwiduje tozsamosc, ktorej broni
naglowek theme.ts. Zamiast tego rozdzielam ROLE tego samego koloru:
accent zostaje do WYPELNIEN — jasny tekst na nim ma 5,03, przechodzi
accentText #C75260 dla tekstu i obwodek — 4,30 / 3,96 / 3,68
62 podmiany w 24 plikach (34 color, 26 borderColor, 2 thumbColor).
38 wypelnien nietknietych. Sprawdzone skryptem, ze zaden zmieniony tekst
nie stoi na tle akcentu.
borderStrong #7B7065 dla granic NIOSACYCH ZNACZENIE (pola tekstowe, 12
miejsc); dekoracyjne wloski zostaja na `border`. Wartosc dobrana pod obie
powierzchnie, bo pola leza i na bg, i na card: #6B6157 dawal na karcie
2,66, a #73695E dokladnie 2,999, czyli wciaz ponizej progu.
654 lines
20 KiB
TypeScript
654 lines
20 KiB
TypeScript
// Lista ulubionych performerów + studiów + badge "ile nowych scen". Dwie zakładki
|
||
// na górze (Performers / Studios). Tap → odpowiedni screen scen + auto mark-seen
|
||
// (zeruje licznik). Klucz: przekazujemy `seenSince` (last_seen_at SPRZED markSeen)
|
||
// żeby PerformerScenesScreen / StudioScenesScreen mógł pokazać NEW badge na świeżych
|
||
// scenach.
|
||
import { useNavigation } from '@react-navigation/native';
|
||
import { formatCount } from '../lib/formatCount';
|
||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||
import React from 'react';
|
||
import {
|
||
ActivityIndicator,
|
||
Alert,
|
||
FlatList,
|
||
Pressable,
|
||
StyleSheet,
|
||
Text,
|
||
View,
|
||
} from 'react-native';
|
||
import { Image } from 'expo-image';
|
||
import { useClient } from '../ClientContext';
|
||
import { SceneTile, sceneGridProps } from '../components/SceneTile';
|
||
import { usePreferences } from '../PreferencesContext';
|
||
import type { RootStackParamList } from '../navigation';
|
||
import { theme } from '../theme';
|
||
import type { FavoriteMovieOut, FavoriteOut, FavoriteStudioOut } from '../types';
|
||
|
||
type Tab = 'scenes' | 'performers' | 'studios' | 'movies';
|
||
|
||
export function FavoritesScreen() {
|
||
const client = useClient();
|
||
const { gridColumns } = usePreferences();
|
||
const queryClient = useQueryClient();
|
||
const nav = useNavigation<NativeStackNavigationProp<RootStackParamList, 'Favorites'>>();
|
||
const [tab, setTab] = React.useState<Tab>('scenes');
|
||
|
||
const performersQuery = useQuery({
|
||
queryKey: ['favorites'],
|
||
queryFn: () => client.listFavorites(),
|
||
});
|
||
|
||
const studiosQuery = useQuery({
|
||
queryKey: ['favorites-studios'],
|
||
queryFn: () => client.listFavoriteStudios(),
|
||
});
|
||
|
||
const moviesQuery = useQuery({
|
||
queryKey: ['favorites-movies'],
|
||
queryFn: () => client.listFavoriteMovies(),
|
||
});
|
||
|
||
const scenesQuery = useQuery({
|
||
queryKey: ['favorites-scenes'],
|
||
queryFn: () => client.listSceneFavorites(),
|
||
});
|
||
|
||
const removeScene = useMutation({
|
||
mutationFn: (id: string) => client.removeSceneFavorite(id),
|
||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['favorites-scenes'] }),
|
||
onError: (e) => Alert.alert('Failed', e instanceof Error ? e.message : String(e)),
|
||
});
|
||
|
||
const removeMovie = useMutation({
|
||
mutationFn: (id: string) => client.removeFavoriteMovie(id),
|
||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['favorites-movies'] }),
|
||
onError: (e) => Alert.alert('Failed', e instanceof Error ? e.message : String(e)),
|
||
});
|
||
|
||
const removePerformer = useMutation({
|
||
mutationFn: (id: string) => client.removeFavorite(id),
|
||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['favorites'] }),
|
||
onError: (e) => Alert.alert('Failed', e instanceof Error ? e.message : String(e)),
|
||
});
|
||
|
||
const removeStudio = useMutation({
|
||
mutationFn: (id: string) => client.removeFavoriteStudio(id),
|
||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['favorites-studios'] }),
|
||
onError: (e) => Alert.alert('Failed', e instanceof Error ? e.message : String(e)),
|
||
});
|
||
|
||
const markPerformerSeen = (id: string) => {
|
||
queryClient.setQueryData(['favorites'], (prev: typeof performersQuery.data) =>
|
||
prev
|
||
? {
|
||
...prev,
|
||
items: prev.items.map((f) =>
|
||
f.performer_id === id ? { ...f, new_count: 0 } : f,
|
||
),
|
||
new_total: Math.max(
|
||
0,
|
||
prev.new_total - (prev.items.find((f) => f.performer_id === id)?.new_count ?? 0),
|
||
),
|
||
}
|
||
: prev,
|
||
);
|
||
client.markFavoriteSeen(id).catch(() => {});
|
||
};
|
||
|
||
const markStudioSeen = (id: string) => {
|
||
queryClient.setQueryData(['favorites-studios'], (prev: typeof studiosQuery.data) =>
|
||
prev
|
||
? {
|
||
...prev,
|
||
items: prev.items.map((f) =>
|
||
f.studio_id === id ? { ...f, new_count: 0 } : f,
|
||
),
|
||
new_total: Math.max(
|
||
0,
|
||
prev.new_total - (prev.items.find((f) => f.studio_id === id)?.new_count ?? 0),
|
||
),
|
||
}
|
||
: prev,
|
||
);
|
||
client.markFavoriteStudioSeen(id).catch(() => {});
|
||
};
|
||
|
||
// performers/studios mają new_total badge; scenes/movies nie (single entity).
|
||
const activeData =
|
||
tab === 'performers'
|
||
? performersQuery.data
|
||
: tab === 'studios'
|
||
? studiosQuery.data
|
||
: null;
|
||
const activeQuery =
|
||
tab === 'performers'
|
||
? performersQuery
|
||
: tab === 'studios'
|
||
? studiosQuery
|
||
: tab === 'movies'
|
||
? moviesQuery
|
||
: scenesQuery;
|
||
const headerCount = activeData?.total ?? (activeQuery.data as { total?: number } | undefined)?.total ?? 0;
|
||
const isLoading = activeQuery.isLoading;
|
||
const isRefetching = activeQuery.isRefetching;
|
||
const error = activeQuery.error;
|
||
const refetch = activeQuery.refetch;
|
||
|
||
return (
|
||
<View style={styles.container}>
|
||
<View style={styles.headerRow}>
|
||
<Text style={styles.headerLabel}>Favorites</Text>
|
||
{activeData && activeData.new_total > 0 ? (
|
||
<View style={styles.totalBadge}>
|
||
<Text style={styles.totalBadgeText}>+{activeData.new_total} new</Text>
|
||
</View>
|
||
) : (
|
||
<Text style={styles.headerCount}>{headerCount}</Text>
|
||
)}
|
||
</View>
|
||
|
||
<View style={styles.tabRow}>
|
||
<TabButton
|
||
label="Scenes"
|
||
active={tab === 'scenes'}
|
||
newCount={0}
|
||
onPress={() => setTab('scenes')}
|
||
/>
|
||
<TabButton
|
||
label="Performers"
|
||
active={tab === 'performers'}
|
||
newCount={performersQuery.data?.new_total ?? 0}
|
||
onPress={() => setTab('performers')}
|
||
/>
|
||
<TabButton
|
||
label="Studios"
|
||
active={tab === 'studios'}
|
||
newCount={studiosQuery.data?.new_total ?? 0}
|
||
onPress={() => setTab('studios')}
|
||
/>
|
||
<TabButton
|
||
label="Movies"
|
||
active={tab === 'movies'}
|
||
newCount={0}
|
||
onPress={() => setTab('movies')}
|
||
/>
|
||
</View>
|
||
|
||
<Text style={styles.hint}>
|
||
tap to open scenes (resets the new badge) · long-press to remove
|
||
</Text>
|
||
|
||
{isLoading && <ActivityIndicator color={theme.fg} style={{ marginTop: 24 }} />}
|
||
{error instanceof Error && <Text style={styles.error}>{error.message}</Text>}
|
||
|
||
{tab === 'scenes' ? (
|
||
<FlatList
|
||
{...sceneGridProps(gridColumns)}
|
||
// key per tab + per liczba kolumn: bez remountu RN rzuca "Changing numColumns
|
||
// on the fly is not supported" (GOON-11). Zmiana kolumn też wymaga remountu.
|
||
key={`fav-scenes-${gridColumns}`}
|
||
data={scenesQuery.data?.items ?? []}
|
||
keyExtractor={(s) => s.id}
|
||
// patrz ScenesScreen: removeClippedSubviews blankuje miniaturki po scrollu.
|
||
removeClippedSubviews={false}
|
||
renderItem={({ item }) => (
|
||
<SceneTile
|
||
scene={item}
|
||
secondLine="studio"
|
||
onLongPress={() =>
|
||
Alert.alert(
|
||
'Remove favorite',
|
||
`Remove "${item.title}" from favorites?`,
|
||
[
|
||
{ text: 'Cancel', style: 'cancel' },
|
||
{
|
||
text: 'Remove',
|
||
style: 'destructive',
|
||
onPress: () => removeScene.mutate(item.id),
|
||
},
|
||
],
|
||
)
|
||
}
|
||
/>
|
||
)}
|
||
refreshing={isRefetching}
|
||
onRefresh={refetch}
|
||
ListEmptyComponent={
|
||
!isLoading ? (
|
||
<Text style={styles.emptyText}>
|
||
No scene favorites yet. Tap the heart on a scene to save it here.
|
||
</Text>
|
||
) : null
|
||
}
|
||
contentContainerStyle={{ paddingBottom: 24 }}
|
||
/>
|
||
) : tab === 'movies' ? (
|
||
<FlatList
|
||
key="fav-list"
|
||
data={moviesQuery.data?.items ?? []}
|
||
keyExtractor={(f) => f.movie_id}
|
||
renderItem={({ item }) => (
|
||
<MovieRow
|
||
fav={item}
|
||
onPress={() => {
|
||
client.markFavoriteMovieSeen(item.movie_id).catch(() => {});
|
||
nav.navigate('MovieDetail', { id: item.movie_id });
|
||
}}
|
||
onLongPress={() =>
|
||
Alert.alert(
|
||
'Remove favorite',
|
||
`Remove "${item.title}" from favorites?`,
|
||
[
|
||
{ text: 'Cancel', style: 'cancel' },
|
||
{
|
||
text: 'Remove',
|
||
style: 'destructive',
|
||
onPress: () => removeMovie.mutate(item.movie_id),
|
||
},
|
||
],
|
||
)
|
||
}
|
||
/>
|
||
)}
|
||
refreshing={isRefetching}
|
||
onRefresh={refetch}
|
||
ListEmptyComponent={
|
||
!isLoading ? (
|
||
<Text style={styles.emptyText}>
|
||
No movie favorites yet. Tap the star on a movie to follow.
|
||
</Text>
|
||
) : null
|
||
}
|
||
contentContainerStyle={{ paddingBottom: 24 }}
|
||
/>
|
||
) : tab === 'performers' ? (
|
||
<FlatList
|
||
key="fav-list"
|
||
data={performersQuery.data?.items ?? []}
|
||
keyExtractor={(f) => f.performer_id}
|
||
renderItem={({ item }) => (
|
||
<PerformerRow
|
||
fav={item}
|
||
onPress={() => {
|
||
// Capture last_seen_at PRZED markSeen, żeby PerformerScenes mógł
|
||
// porównać scene.created_at > seenSince i pokazać NEW badge.
|
||
const seenSince = item.last_seen_at;
|
||
markPerformerSeen(item.performer_id);
|
||
nav.navigate('PerformerScenes', {
|
||
id: item.performer_id,
|
||
name: item.canonical_name,
|
||
seenSince,
|
||
});
|
||
}}
|
||
onLongPress={() =>
|
||
Alert.alert(
|
||
'Remove favorite',
|
||
`Stop following ${item.canonical_name}?`,
|
||
[
|
||
{ text: 'Cancel', style: 'cancel' },
|
||
{
|
||
text: 'Remove',
|
||
style: 'destructive',
|
||
onPress: () => removePerformer.mutate(item.performer_id),
|
||
},
|
||
],
|
||
)
|
||
}
|
||
/>
|
||
)}
|
||
refreshing={isRefetching}
|
||
onRefresh={refetch}
|
||
ListEmptyComponent={
|
||
!isLoading ? (
|
||
<Text style={styles.emptyText}>
|
||
No performer favorites yet. Tap the star on a performer to follow.
|
||
</Text>
|
||
) : null
|
||
}
|
||
contentContainerStyle={{ paddingBottom: 24 }}
|
||
/>
|
||
) : (
|
||
<FlatList
|
||
key="fav-list"
|
||
data={studiosQuery.data?.items ?? []}
|
||
keyExtractor={(f) => f.studio_id}
|
||
renderItem={({ item }) => (
|
||
<StudioRow
|
||
fav={item}
|
||
onPress={() => {
|
||
const seenSince = item.last_seen_at;
|
||
markStudioSeen(item.studio_id);
|
||
nav.navigate('StudioScenes', {
|
||
id: item.slug,
|
||
studioId: item.studio_id,
|
||
name: item.name,
|
||
seenSince,
|
||
});
|
||
}}
|
||
onLongPress={() =>
|
||
Alert.alert(
|
||
'Remove favorite',
|
||
`Stop following ${item.name}?`,
|
||
[
|
||
{ text: 'Cancel', style: 'cancel' },
|
||
{
|
||
text: 'Remove',
|
||
style: 'destructive',
|
||
onPress: () => removeStudio.mutate(item.studio_id),
|
||
},
|
||
],
|
||
)
|
||
}
|
||
/>
|
||
)}
|
||
refreshing={isRefetching}
|
||
onRefresh={refetch}
|
||
ListEmptyComponent={
|
||
!isLoading ? (
|
||
<Text style={styles.emptyText}>
|
||
No studio favorites yet. Tap the star on a studio to follow.
|
||
</Text>
|
||
) : null
|
||
}
|
||
contentContainerStyle={{ paddingBottom: 24 }}
|
||
/>
|
||
)}
|
||
</View>
|
||
);
|
||
}
|
||
|
||
function TabButton({
|
||
label,
|
||
active,
|
||
newCount,
|
||
onPress,
|
||
}: {
|
||
label: string;
|
||
active: boolean;
|
||
newCount: number;
|
||
onPress: () => void;
|
||
}) {
|
||
return (
|
||
<Pressable style={[styles.tabBtn, active && styles.tabBtnActive]} onPress={onPress}>
|
||
<Text style={[styles.tabBtnText, active && styles.tabBtnTextActive]}>{label}</Text>
|
||
{newCount > 0 ? (
|
||
<View style={styles.tabBadge}>
|
||
<Text style={styles.tabBadgeText}>+{newCount}</Text>
|
||
</View>
|
||
) : null}
|
||
</Pressable>
|
||
);
|
||
}
|
||
|
||
function initials(name: string): string {
|
||
const parts = name.trim().split(/\s+/);
|
||
const first = parts[0]?.[0] ?? '';
|
||
const last = parts.length > 1 ? parts[parts.length - 1][0] ?? '' : '';
|
||
return (first + last).toUpperCase() || '?';
|
||
}
|
||
|
||
function PerformerRow({
|
||
fav,
|
||
onPress,
|
||
onLongPress,
|
||
}: {
|
||
fav: FavoriteOut;
|
||
onPress: () => void;
|
||
onLongPress: () => void;
|
||
}) {
|
||
return (
|
||
<Pressable
|
||
style={({ pressed }) => [styles.row, pressed && styles.rowPressed]}
|
||
onPress={onPress}
|
||
onLongPress={onLongPress}
|
||
delayLongPress={400}
|
||
>
|
||
<View style={styles.avatar}>
|
||
<Text style={styles.avatarText}>{initials(fav.canonical_name)}</Text>
|
||
</View>
|
||
<View style={styles.rowContent}>
|
||
<Text style={styles.rowTitle} numberOfLines={1}>
|
||
{fav.canonical_name}
|
||
</Text>
|
||
<Text style={styles.rowMuted}>
|
||
{formatCount(fav.scene_count)} {fav.scene_count === 1 ? 'scene' : 'scenes'}
|
||
</Text>
|
||
</View>
|
||
{fav.new_count > 0 ? (
|
||
<View style={styles.newBadge}>
|
||
<Text style={styles.newBadgeText}>+{fav.new_count}</Text>
|
||
</View>
|
||
) : (
|
||
<Text style={styles.chevron}>›</Text>
|
||
)}
|
||
</Pressable>
|
||
);
|
||
}
|
||
|
||
function MovieRow({
|
||
fav,
|
||
onPress,
|
||
onLongPress,
|
||
}: {
|
||
fav: FavoriteMovieOut;
|
||
onPress: () => void;
|
||
onLongPress: () => void;
|
||
}) {
|
||
return (
|
||
<Pressable
|
||
style={({ pressed }) => [styles.row, pressed && styles.rowPressed]}
|
||
onPress={onPress}
|
||
onLongPress={onLongPress}
|
||
delayLongPress={400}
|
||
>
|
||
{fav.poster_url ? (
|
||
<Image source={{ uri: fav.poster_url }} style={styles.poster} contentFit="cover" />
|
||
) : (
|
||
<View style={[styles.poster, styles.posterPlaceholder]}>
|
||
<Text style={styles.avatarText}>{initials(fav.title)}</Text>
|
||
</View>
|
||
)}
|
||
<View style={styles.rowContent}>
|
||
<Text style={styles.rowTitle} numberOfLines={1}>
|
||
{fav.title}
|
||
</Text>
|
||
<Text style={styles.rowMuted}>
|
||
{fav.release_year ?? '—'}
|
||
{fav.studio_name ? ` · ${fav.studio_name}` : ''}
|
||
</Text>
|
||
</View>
|
||
<Text style={styles.chevron}>›</Text>
|
||
</Pressable>
|
||
);
|
||
}
|
||
|
||
// Studio bywa WYŁĄCZNIE filmowe (Jonathan Jordan XXX 0 scen / 654 filmy, Kink Clips
|
||
// 0/616), a wiersz pokazywał wtedy „0 scenes", czyli wyglądał na pusty, mimo że badge
|
||
// „+N" liczył filmy od 2026-07-28. Pokazujemy oba, a gdy jednego nie ma, pomijamy go,
|
||
// żeby nie robić z tego „0 scenes · 654 movies".
|
||
function studioCounts(fav: FavoriteStudioOut): string {
|
||
const parts: string[] = [];
|
||
const movies = fav.movie_count ?? 0;
|
||
if (fav.scene_count > 0 || movies === 0) {
|
||
parts.push(`${formatCount(fav.scene_count)} ${fav.scene_count === 1 ? 'scene' : 'scenes'}`);
|
||
}
|
||
if (movies > 0) {
|
||
parts.push(`${formatCount(movies)} ${movies === 1 ? 'movie' : 'movies'}`);
|
||
}
|
||
return parts.join(' · ');
|
||
}
|
||
|
||
function StudioRow({
|
||
fav,
|
||
onPress,
|
||
onLongPress,
|
||
}: {
|
||
fav: FavoriteStudioOut;
|
||
onPress: () => void;
|
||
onLongPress: () => void;
|
||
}) {
|
||
return (
|
||
<Pressable
|
||
style={({ pressed }) => [styles.row, pressed && styles.rowPressed]}
|
||
onPress={onPress}
|
||
onLongPress={onLongPress}
|
||
delayLongPress={400}
|
||
>
|
||
<View style={styles.avatar}>
|
||
<Text style={styles.avatarText}>{initials(fav.name)}</Text>
|
||
</View>
|
||
<View style={styles.rowContent}>
|
||
<Text style={styles.rowTitle} numberOfLines={1}>
|
||
{fav.name}
|
||
</Text>
|
||
<Text style={styles.rowMuted}>
|
||
{studioCounts(fav)}
|
||
{fav.network ? ` · ${fav.network}` : ''}
|
||
</Text>
|
||
</View>
|
||
{fav.new_count > 0 ? (
|
||
<View style={styles.newBadge}>
|
||
<Text style={styles.newBadgeText}>+{fav.new_count}</Text>
|
||
</View>
|
||
) : (
|
||
<Text style={styles.chevron}>›</Text>
|
||
)}
|
||
</Pressable>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: 16, paddingTop: 12 },
|
||
headerRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
paddingBottom: 4,
|
||
},
|
||
headerLabel: {
|
||
color: theme.muted,
|
||
fontSize: 12,
|
||
textTransform: 'uppercase',
|
||
letterSpacing: 1.2,
|
||
fontWeight: '700',
|
||
},
|
||
headerCount: { color: theme.fg, fontSize: 22, fontWeight: '800' },
|
||
totalBadge: {
|
||
backgroundColor: theme.accent,
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 4,
|
||
borderRadius: 10,
|
||
shadowColor: theme.accent,
|
||
shadowOffset: { width: 0, height: 0 },
|
||
shadowOpacity: 0.5,
|
||
shadowRadius: 6,
|
||
elevation: 3,
|
||
},
|
||
totalBadgeText: {
|
||
color: theme.fg,
|
||
fontWeight: '800',
|
||
fontSize: 13,
|
||
letterSpacing: 0.3,
|
||
},
|
||
|
||
tabRow: { flexDirection: 'row', gap: 8, marginTop: 10, marginBottom: 4 },
|
||
tabBtn: {
|
||
flex: 1,
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: 6,
|
||
paddingVertical: 8,
|
||
borderRadius: 8,
|
||
backgroundColor: theme.card,
|
||
borderColor: theme.border,
|
||
borderWidth: 1,
|
||
},
|
||
tabBtnActive: { borderColor: theme.accentText, backgroundColor: theme.bgElevated },
|
||
tabBtnText: { color: theme.muted, fontWeight: '600', fontSize: 14 },
|
||
tabBtnTextActive: { color: theme.fg },
|
||
tabBadge: {
|
||
backgroundColor: theme.accent,
|
||
paddingHorizontal: 6,
|
||
paddingVertical: 1,
|
||
borderRadius: 4,
|
||
},
|
||
tabBadgeText: { color: theme.bg, fontWeight: '800', fontSize: 11 },
|
||
|
||
hint: { color: theme.mutedDim, fontSize: 11, marginTop: 8, marginBottom: 12 },
|
||
|
||
gridRow: { gap: 10, marginBottom: 14 },
|
||
|
||
row: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 12,
|
||
backgroundColor: theme.card,
|
||
borderColor: theme.border,
|
||
borderWidth: 1,
|
||
borderRadius: 14,
|
||
paddingHorizontal: 14,
|
||
paddingVertical: 12,
|
||
marginBottom: 10,
|
||
shadowColor: '#000',
|
||
shadowOffset: { width: 0, height: 2 },
|
||
shadowOpacity: 0.2,
|
||
shadowRadius: 4,
|
||
elevation: 3,
|
||
},
|
||
rowPressed: { backgroundColor: theme.bgElevated, borderColor: theme.borderFocus },
|
||
|
||
avatar: {
|
||
width: 40,
|
||
height: 40,
|
||
borderRadius: 20,
|
||
backgroundColor: theme.bgElevated,
|
||
borderColor: theme.border,
|
||
borderWidth: 1,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
avatarText: {
|
||
color: theme.accentGlow,
|
||
fontWeight: '800',
|
||
fontSize: 13,
|
||
letterSpacing: 0.5,
|
||
},
|
||
poster: {
|
||
width: 40,
|
||
height: 56,
|
||
borderRadius: 6,
|
||
backgroundColor: theme.bgElevated,
|
||
borderColor: theme.border,
|
||
borderWidth: 1,
|
||
},
|
||
posterPlaceholder: { alignItems: 'center', justifyContent: 'center' },
|
||
rowContent: { flex: 1 },
|
||
rowTitle: { color: theme.fg, fontWeight: '700', fontSize: 16, marginBottom: 4 },
|
||
rowMuted: { color: theme.muted, fontSize: 13 },
|
||
|
||
newBadge: {
|
||
backgroundColor: theme.accent,
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 4,
|
||
borderRadius: 10,
|
||
shadowColor: theme.accent,
|
||
shadowOffset: { width: 0, height: 0 },
|
||
shadowOpacity: 0.5,
|
||
shadowRadius: 6,
|
||
elevation: 3,
|
||
},
|
||
newBadgeText: { color: theme.fg, fontWeight: '800', fontSize: 13 },
|
||
chevron: { color: theme.mutedDim, fontSize: 22, fontWeight: '300' },
|
||
|
||
emptyText: {
|
||
color: theme.muted,
|
||
textAlign: 'center',
|
||
marginTop: 48,
|
||
fontSize: 15,
|
||
paddingHorizontal: 24,
|
||
lineHeight: 22,
|
||
},
|
||
error: { color: theme.bad, padding: 16 },
|
||
});
|