feat(mobile): gorne belki w jednym wierszu, pasek w Edge chowa sie przy przewijaniu
- SegmentTabs w ui.tsx: zakladki w wierszu narzedzi obok ikon - Ulubione, performerka, studio, Performers i Tags: jeden wiersz pod naglowkiem zamiast trzech-czterech (etykieta z licznikiem, zakladki, podpowiedz, sortowanie) - gwiazdka na ekranie performerki i studia bierze /favorites?counts=false - Edge: kolejny klip chowa pasek, cofniecie pokazuje; podpis, przyciski i postep jada razem z paskiem; FAB zgloszen ukryty, lezal na "Not interested" - changelog 2026-09-15c/d Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
37e6b7f03c
commit
7ed878ce16
12 changed files with 316 additions and 598 deletions
|
|
@ -405,8 +405,9 @@ export class GoonClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async listFavorites(): Promise<FavoriteListOut> {
|
/** `counts: false` = bez liczników +N (tylko stan gwiazdki, serwer odpowiada od razu). */
|
||||||
return this.request('/favorites');
|
async listFavorites(opts: { counts?: boolean } = {}): Promise<FavoriteListOut> {
|
||||||
|
return this.request(opts.counts === false ? '/favorites?counts=false' : '/favorites');
|
||||||
}
|
}
|
||||||
|
|
||||||
async addFavorite(performerId: string): Promise<FavoriteAddOut> {
|
async addFavorite(performerId: string): Promise<FavoriteAddOut> {
|
||||||
|
|
@ -428,8 +429,8 @@ export class GoonClient {
|
||||||
await this.request(`/favorites/${performerId}/seen`, { method: 'POST' });
|
await this.request(`/favorites/${performerId}/seen`, { method: 'POST' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async listFavoriteStudios(): Promise<FavoriteStudioListOut> {
|
async listFavoriteStudios(opts: { counts?: boolean } = {}): Promise<FavoriteStudioListOut> {
|
||||||
return this.request('/favorites/studios');
|
return this.request(opts.counts === false ? '/favorites/studios?counts=false' : '/favorites/studios');
|
||||||
}
|
}
|
||||||
|
|
||||||
async addFavoriteStudio(studioId: string): Promise<FavoriteStudioAddOut> {
|
async addFavoriteStudio(studioId: string): Promise<FavoriteStudioAddOut> {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,23 @@ export type ChangelogEntry = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CHANGELOG: ChangelogEntry[] = [
|
export const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
id: '2026-09-15d',
|
||||||
|
date: 'September 2026',
|
||||||
|
items: [
|
||||||
|
'Edge: the bottom bar slides away when you swipe to the next clip and comes back when you swipe back, so it no longer covers the caption, tags and progress line.',
|
||||||
|
'Edge: the bug report button is hidden there, it was sitting on top of the not-interested button. Reports still work from every other tab.',
|
||||||
|
],
|
||||||
|
minRuntime: '1.4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2026-09-15c',
|
||||||
|
date: 'September 2026',
|
||||||
|
items: [
|
||||||
|
'Favorites, performer and studio pages, Performers and Tags: tabs, sorting and buttons now share one row under the title instead of stacking three or four lines above the list.',
|
||||||
|
'Favorites and similar scenes load much faster, and opening a performer or studio no longer waits for the new-scene counters.',
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: '2026-09-15b',
|
id: '2026-09-15b',
|
||||||
date: 'September 2026',
|
date: 'September 2026',
|
||||||
|
|
|
||||||
|
|
@ -106,19 +106,27 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
|
||||||
}, [unseen, client]);
|
}, [unseen, client]);
|
||||||
// Re-render na zmianę current route, żeby FAB pojawiał/znikał per-screen.
|
// Re-render na zmianę current route, żeby FAB pojawiał/znikał per-screen.
|
||||||
const [currentRoute, setCurrentRoute] = useState<string | null>(null);
|
const [currentRoute, setCurrentRoute] = useState<string | null>(null);
|
||||||
|
const [currentTab, setCurrentTab] = useState<string | null>(null);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!navRef) return;
|
if (!navRef) return;
|
||||||
const unsub = navRef.addListener('state', () => {
|
const unsub = navRef.addListener('state', () => {
|
||||||
try {
|
try {
|
||||||
const r = navRef.isReady() ? navRef.getCurrentRoute() : null;
|
const r = navRef.isReady() ? navRef.getCurrentRoute() : null;
|
||||||
setCurrentRoute(r?.name ?? null);
|
setCurrentRoute(r?.name ?? null);
|
||||||
|
const tab = (r?.params as { tab?: unknown } | undefined)?.tab;
|
||||||
|
setCurrentTab(typeof tab === 'string' ? tab : null);
|
||||||
} catch {
|
} catch {
|
||||||
setCurrentRoute(null);
|
setCurrentRoute(null);
|
||||||
|
setCurrentTab(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return unsub;
|
return unsub;
|
||||||
}, [navRef]);
|
}, [navRef]);
|
||||||
const hidden = currentRoute !== null && FAB_HIDDEN_SCREENS.has(currentRoute);
|
// Edge to wideo na caly ekran z kolumna przyciskow po prawej, a FAB lezal dokladnie na
|
||||||
|
// „Not interested" (2026-09-15). Zgloszenie da sie wyslac z kazdej innej zakladki.
|
||||||
|
const hidden =
|
||||||
|
currentRoute !== null &&
|
||||||
|
(FAB_HIDDEN_SCREENS.has(currentRoute) || (currentRoute === 'Scenes' && currentTab === 'Edge'));
|
||||||
// Na ekranie-gospodarzu (trasa 'Scenes') na dole wisi pasek zakladek, FAB idzie
|
// Na ekranie-gospodarzu (trasa 'Scenes') na dole wisi pasek zakladek, FAB idzie
|
||||||
// nad niego; na pozostalych ekranach zostaje przy krawedzi.
|
// nad niego; na pozostalych ekranach zostaje przy krawedzi.
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,11 @@
|
||||||
* double-tap → serce (ulubione) + wybuch serca + haptyka,
|
* double-tap → serce (ulubione) + wybuch serca + haptyka,
|
||||||
* przytrzymanie → 2x do puszczenia,
|
* przytrzymanie → 2x do puszczenia,
|
||||||
* przesuniecie w lewo → arkusz „nie interesuje" (pion zostaje dla pagera).
|
* przesuniecie w lewo → arkusz „nie interesuje" (pion zostaje dla pagera).
|
||||||
|
*
|
||||||
|
* Dolny pasek zakladek wisi nad feedem. Podpis, kolumna przyciskow i pasek postepu jada
|
||||||
|
* razem z nim (ta sama wartosc Animated `barHidden`): gdy pasek jest widoczny, stoja nad
|
||||||
|
* nim, gdy sie schowa, schodza na dol ekranu. Wczesniej pasek zaslanial podpis, tagi i
|
||||||
|
* postep (uwaga Jana 2026-09-15).
|
||||||
*/
|
*/
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
|
|
@ -19,7 +24,9 @@ import { VideoView } from 'expo-video';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Animated, Pressable, StyleSheet, Text, View } from 'react-native';
|
import { Animated, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
|
import { BOTTOM_BAR_HEIGHT, barHidden } from '../../lib/bottomBar';
|
||||||
import { fonts, theme } from '../../theme';
|
import { fonts, theme } from '../../theme';
|
||||||
import type { ClipFeedItem } from '../../types';
|
import type { ClipFeedItem } from '../../types';
|
||||||
|
|
||||||
|
|
@ -40,6 +47,15 @@ function ClipCellBase({ item, active, player, muted, liked, onToggleMute, onLike
|
||||||
const [progress, setProgress] = React.useState(0);
|
const [progress, setProgress] = React.useState(0);
|
||||||
const [ready, setReady] = React.useState(false);
|
const [ready, setReady] = React.useState(false);
|
||||||
const heart = React.useRef(new Animated.Value(0)).current;
|
const heart = React.useRef(new Animated.Value(0)).current;
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
const lift = React.useMemo(
|
||||||
|
() =>
|
||||||
|
barHidden.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: [-(BOTTOM_BAR_HEIGHT + insets.bottom), -insets.bottom],
|
||||||
|
}),
|
||||||
|
[insets.bottom],
|
||||||
|
);
|
||||||
|
|
||||||
// Postep i gotowosc tylko dla aktywnej strony z playerem.
|
// Postep i gotowosc tylko dla aktywnej strony z playerem.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -158,6 +174,7 @@ function ClipCellBase({ item, active, player, muted, liked, onToggleMute, onLike
|
||||||
<MaterialCommunityIcons name="heart" size={120} color="#fff" />
|
<MaterialCommunityIcons name="heart" size={120} color="#fff" />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
|
<Animated.View style={[StyleSheet.absoluteFill, { transform: [{ translateY: lift }] }]} pointerEvents="box-none">
|
||||||
{/* Kolumna przyciskow */}
|
{/* Kolumna przyciskow */}
|
||||||
<View style={styles.rail} pointerEvents="box-none">
|
<View style={styles.rail} pointerEvents="box-none">
|
||||||
<Pressable
|
<Pressable
|
||||||
|
|
@ -213,6 +230,7 @@ function ClipCellBase({ item, active, player, muted, liked, onToggleMute, onLike
|
||||||
<View style={styles.progressTrack} pointerEvents="none">
|
<View style={styles.progressTrack} pointerEvents="none">
|
||||||
<View style={[styles.progressFill, { width: `${Math.round(progress * 100)}%` }]} />
|
<View style={[styles.progressFill, { width: `${Math.round(progress * 100)}%` }]} />
|
||||||
</View>
|
</View>
|
||||||
|
</Animated.View>
|
||||||
</View>
|
</View>
|
||||||
</GestureDetector>
|
</GestureDetector>
|
||||||
);
|
);
|
||||||
|
|
@ -231,7 +249,8 @@ const styles = StyleSheet.create({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
rail: { position: 'absolute', right: 8, bottom: 96, alignItems: 'center' },
|
// Nad dolnym paskiem pilnuje tego `lift`, wiec kolumna stoi na wysokosci podpisu.
|
||||||
|
rail: { position: 'absolute', right: 8, bottom: 20, alignItems: 'center' },
|
||||||
railBtn: {
|
railBtn: {
|
||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import { SceneTile, sceneGridProps } from '../components/SceneTile';
|
||||||
import { usePreferences } from '../PreferencesContext';
|
import { usePreferences } from '../PreferencesContext';
|
||||||
import type { RootStackParamList } from '../navigation';
|
import type { RootStackParamList } from '../navigation';
|
||||||
import { theme } from '../theme';
|
import { theme } from '../theme';
|
||||||
|
import { SegmentTabs, Toolbar } from '../ui';
|
||||||
import type { FavoriteMovieOut, FavoriteOut, FavoriteStudioOut } from '../types';
|
import type { FavoriteMovieOut, FavoriteOut, FavoriteStudioOut } from '../types';
|
||||||
|
|
||||||
type Tab = 'scenes' | 'performers' | 'studios' | 'movies';
|
type Tab = 'scenes' | 'performers' | 'studios' | 'movies';
|
||||||
|
|
@ -115,12 +116,6 @@ export function FavoritesScreen() {
|
||||||
};
|
};
|
||||||
|
|
||||||
// performers/studios mają new_total badge; scenes/movies nie (single entity).
|
// performers/studios mają new_total badge; scenes/movies nie (single entity).
|
||||||
const activeData =
|
|
||||||
tab === 'performers'
|
|
||||||
? performersQuery.data
|
|
||||||
: tab === 'studios'
|
|
||||||
? studiosQuery.data
|
|
||||||
: null;
|
|
||||||
const activeQuery =
|
const activeQuery =
|
||||||
tab === 'performers'
|
tab === 'performers'
|
||||||
? performersQuery
|
? performersQuery
|
||||||
|
|
@ -129,7 +124,6 @@ export function FavoritesScreen() {
|
||||||
: tab === 'movies'
|
: tab === 'movies'
|
||||||
? moviesQuery
|
? moviesQuery
|
||||||
: scenesQuery;
|
: scenesQuery;
|
||||||
const headerCount = activeData?.total ?? (activeQuery.data as { total?: number } | undefined)?.total ?? 0;
|
|
||||||
const isLoading = activeQuery.isLoading;
|
const isLoading = activeQuery.isLoading;
|
||||||
const isRefetching = activeQuery.isRefetching;
|
const isRefetching = activeQuery.isRefetching;
|
||||||
const error = activeQuery.error;
|
const error = activeQuery.error;
|
||||||
|
|
@ -137,47 +131,21 @@ export function FavoritesScreen() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.headerRow}>
|
{/* Jeden wiersz zakładek (2026-09-15). Wcześniej pod natywnym nagłówkiem
|
||||||
<Text style={styles.headerLabel}>Favorites</Text>
|
"Favorites" stała druga etykieta FAVORITES z licznikiem, wiersz zakładek i
|
||||||
{activeData && activeData.new_total > 0 ? (
|
podpowiedź, czyli cztery linie przed listą. Podpowiedź zeszła pod listę. */}
|
||||||
<View style={styles.totalBadge}>
|
<Toolbar>
|
||||||
<Text style={styles.totalBadgeText}>+{activeData.new_total} new</Text>
|
<SegmentTabs
|
||||||
</View>
|
tabs={[
|
||||||
) : (
|
{ key: 'scenes', label: 'Scenes' },
|
||||||
<Text style={styles.headerCount}>{headerCount}</Text>
|
{ key: 'performers', label: 'Performers', badge: performersQuery.data?.new_total ?? 0 },
|
||||||
)}
|
{ key: 'studios', label: 'Studios', badge: studiosQuery.data?.new_total ?? 0 },
|
||||||
</View>
|
{ key: 'movies', label: 'Movies' },
|
||||||
|
]}
|
||||||
<View style={styles.tabRow}>
|
value={tab}
|
||||||
<TabButton
|
onChange={setTab}
|
||||||
label="Scenes"
|
|
||||||
active={tab === 'scenes'}
|
|
||||||
newCount={0}
|
|
||||||
onPress={() => setTab('scenes')}
|
|
||||||
/>
|
/>
|
||||||
<TabButton
|
</Toolbar>
|
||||||
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 }} />}
|
{isLoading && <ActivityIndicator color={theme.fg} style={{ marginTop: 24 }} />}
|
||||||
{error instanceof Error && <Text style={styles.error}>{error.message}</Text>}
|
{error instanceof Error && <Text style={styles.error}>{error.message}</Text>}
|
||||||
|
|
@ -221,6 +189,7 @@ export function FavoritesScreen() {
|
||||||
</Text>
|
</Text>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
ListFooterComponent={scenesQuery.data?.items.length ? <Text style={styles.hint}>long-press to remove</Text> : null}
|
||||||
contentContainerStyle={{ paddingBottom: 24 }}
|
contentContainerStyle={{ paddingBottom: 24 }}
|
||||||
/>
|
/>
|
||||||
) : tab === 'movies' ? (
|
) : tab === 'movies' ? (
|
||||||
|
|
@ -260,6 +229,7 @@ export function FavoritesScreen() {
|
||||||
</Text>
|
</Text>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
ListFooterComponent={moviesQuery.data?.items.length ? <Text style={styles.hint}>long-press to remove</Text> : null}
|
||||||
contentContainerStyle={{ paddingBottom: 24 }}
|
contentContainerStyle={{ paddingBottom: 24 }}
|
||||||
/>
|
/>
|
||||||
) : tab === 'performers' ? (
|
) : tab === 'performers' ? (
|
||||||
|
|
@ -306,6 +276,7 @@ export function FavoritesScreen() {
|
||||||
</Text>
|
</Text>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
ListFooterComponent={performersQuery.data?.items.length ? <Text style={styles.hint}>tap to open (clears the new badge) · long-press to remove</Text> : null}
|
||||||
contentContainerStyle={{ paddingBottom: 24 }}
|
contentContainerStyle={{ paddingBottom: 24 }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -351,6 +322,7 @@ export function FavoritesScreen() {
|
||||||
</Text>
|
</Text>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
ListFooterComponent={studiosQuery.data?.items.length ? <Text style={styles.hint}>tap to open (clears the new badge) · long-press to remove</Text> : null}
|
||||||
contentContainerStyle={{ paddingBottom: 24 }}
|
contentContainerStyle={{ paddingBottom: 24 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -358,29 +330,6 @@ export function FavoritesScreen() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
function initials(name: string): string {
|
||||||
const parts = name.trim().split(/\s+/);
|
const parts = name.trim().split(/\s+/);
|
||||||
const first = parts[0]?.[0] ?? '';
|
const first = parts[0]?.[0] ?? '';
|
||||||
|
|
@ -520,63 +469,7 @@ function StudioRow({
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: 16, paddingTop: 12 },
|
container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: 16, paddingTop: 12 },
|
||||||
headerRow: {
|
hint: { color: theme.mutedDim, fontSize: 11, textAlign: 'center', marginTop: 4, marginBottom: 8 },
|
||||||
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 },
|
gridRow: { gap: 10, marginBottom: 14 },
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,6 @@ import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Alert,
|
Alert,
|
||||||
FlatList,
|
FlatList,
|
||||||
Pressable,
|
|
||||||
ScrollView,
|
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
View,
|
View,
|
||||||
|
|
@ -28,7 +26,7 @@ import { ErrorBoundary } from '../ErrorBoundary';
|
||||||
import type { RootStackParamList } from '../navigation';
|
import type { RootStackParamList } from '../navigation';
|
||||||
import { theme } from '../theme';
|
import { theme } from '../theme';
|
||||||
import type { SceneOut } from '../types';
|
import type { SceneOut } from '../types';
|
||||||
import { HeaderIconButton, IconButton, Toolbar } from '../ui';
|
import { HeaderIconButton, IconButton, SegmentTabs, Toolbar } from '../ui';
|
||||||
|
|
||||||
type Tab = 'scenes' | 'movies';
|
type Tab = 'scenes' | 'movies';
|
||||||
|
|
||||||
|
|
@ -50,9 +48,12 @@ export function PerformerScenesScreen() {
|
||||||
|
|
||||||
const [tab, setTab] = React.useState<Tab>('scenes');
|
const [tab, setTab] = React.useState<Tab>('scenes');
|
||||||
|
|
||||||
|
// Sam stan gwiazdki, bez liczników +N: pełne /favorites liczy nowe sceny wszystkich
|
||||||
|
// ulubionych (sekundy przy setkach), a ten ekran potrzebuje tylko listy id.
|
||||||
|
// Klucz pod ['favorites'], więc każde invalidateQueries(['favorites']) odświeża też to.
|
||||||
const favoritesQuery = useQuery({
|
const favoritesQuery = useQuery({
|
||||||
queryKey: ['favorites'],
|
queryKey: ['favorites', 'ids'],
|
||||||
queryFn: () => client.listFavorites(),
|
queryFn: () => client.listFavorites({ counts: false }),
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
});
|
});
|
||||||
const isFavorite = !!favoritesQuery.data?.items.find((f) => f.performer_id === id);
|
const isFavorite = !!favoritesQuery.data?.items.find((f) => f.performer_id === id);
|
||||||
|
|
@ -240,21 +241,40 @@ export function PerformerScenesScreen() {
|
||||||
setTab(next);
|
setTab(next);
|
||||||
}, [id, name, tab]);
|
}, [id, name, tab]);
|
||||||
|
|
||||||
|
// Jeden wiersz (2026-09-15): zakładki z licznikami po lewej, ikony sceny po prawej.
|
||||||
|
// Wcześniej zakładki, pod nimi wiersz "200+ scenes" z ikonami, czyli trzy linie
|
||||||
|
// razem z nagłówkiem, a licznik stał dwa razy.
|
||||||
const tabRow = (
|
const tabRow = (
|
||||||
<View style={styles.tabRow}>
|
<Toolbar>
|
||||||
<TabButton
|
<SegmentTabs
|
||||||
label="Scenes"
|
tabs={[
|
||||||
count={scenesTotal}
|
{ key: 'scenes', label: 'Scenes', count: scenesQuery.data ? `${scenesTotal}${scenesSuffix}` : undefined },
|
||||||
active={tab === 'scenes'}
|
{ key: 'movies', label: 'Movies', count: moviesTotal },
|
||||||
onPress={() => handleTabSwitch('scenes')}
|
]}
|
||||||
|
value={tab}
|
||||||
|
onChange={handleTabSwitch}
|
||||||
/>
|
/>
|
||||||
<TabButton
|
{tab === 'scenes' ? (
|
||||||
label="Movies"
|
<>
|
||||||
count={moviesTotal}
|
{refreshMutation.isPending ? (
|
||||||
active={tab === 'movies'}
|
<ActivityIndicator color={theme.accentText} style={{ width: 44 }} />
|
||||||
onPress={() => handleTabSwitch('movies')}
|
) : (
|
||||||
|
<IconButton
|
||||||
|
icon="magnify-scan"
|
||||||
|
accessibilityLabel="Search more scenes"
|
||||||
|
onPress={() => refreshMutation.mutate()}
|
||||||
/>
|
/>
|
||||||
</View>
|
)}
|
||||||
|
<IconButton
|
||||||
|
icon="filter-variant"
|
||||||
|
accessibilityLabel="Filters"
|
||||||
|
active={filterActiveCount > 0}
|
||||||
|
badge={filterActiveCount}
|
||||||
|
onPress={() => setFilterOpen(true)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</Toolbar>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -286,36 +306,6 @@ export function PerformerScenesScreen() {
|
||||||
if (filter.sort === 'random') setSeed(Math.random().toString(36).slice(2) + Date.now().toString(36));
|
if (filter.sort === 'random') setSeed(Math.random().toString(36).slice(2) + Date.now().toString(36));
|
||||||
else scenesQuery.refetch();
|
else scenesQuery.refetch();
|
||||||
}}
|
}}
|
||||||
ListHeaderComponent={
|
|
||||||
<View>
|
|
||||||
{/* Jeden wiersz (2026-09-07): licznik scen po lewej, po prawej ikony
|
|
||||||
„szukaj wiecej" i filtr z licznikiem. Chipsy tagow pod spodem
|
|
||||||
zniknely, tagi sa w filtrze. */}
|
|
||||||
<Toolbar>
|
|
||||||
<Text style={[styles.subtitle, { flex: 1 }]} numberOfLines={1}>
|
|
||||||
{scenesQuery.data
|
|
||||||
? `${scenesTotal}${scenesSuffix} ${scenesTotal === 1 ? 'scene' : 'scenes'}`
|
|
||||||
: ' '}
|
|
||||||
</Text>
|
|
||||||
{refreshMutation.isPending ? (
|
|
||||||
<ActivityIndicator color={theme.accentText} style={{ width: 44 }} />
|
|
||||||
) : (
|
|
||||||
<IconButton
|
|
||||||
icon="magnify-scan"
|
|
||||||
accessibilityLabel="Search more scenes"
|
|
||||||
onPress={() => refreshMutation.mutate()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
icon="filter-variant"
|
|
||||||
accessibilityLabel="Filters"
|
|
||||||
active={filterActiveCount > 0}
|
|
||||||
badge={filterActiveCount}
|
|
||||||
onPress={() => setFilterOpen(true)}
|
|
||||||
/>
|
|
||||||
</Toolbar>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
!scenesQuery.isLoading ? <Text style={styles.muted}>no scenes</Text> : null
|
!scenesQuery.isLoading ? <Text style={styles.muted}>no scenes</Text> : null
|
||||||
}
|
}
|
||||||
|
|
@ -363,15 +353,6 @@ export function PerformerScenesScreen() {
|
||||||
}}
|
}}
|
||||||
refreshing={moviesQuery.isRefetching}
|
refreshing={moviesQuery.isRefetching}
|
||||||
onRefresh={moviesQuery.refetch}
|
onRefresh={moviesQuery.refetch}
|
||||||
ListHeaderComponent={
|
|
||||||
<View style={styles.header}>
|
|
||||||
<Text style={styles.subtitle}>
|
|
||||||
{moviesQuery.data
|
|
||||||
? `${moviesTotal} ${moviesTotal === 1 ? 'film' : 'films'}`
|
|
||||||
: ' '}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
!moviesQuery.isLoading ? (
|
!moviesQuery.isLoading ? (
|
||||||
<Text style={styles.muted}>no movies for this performer</Text>
|
<Text style={styles.muted}>no movies for this performer</Text>
|
||||||
|
|
@ -399,31 +380,6 @@ export function PerformerScenesScreen() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabButton({
|
|
||||||
label,
|
|
||||||
count,
|
|
||||||
active,
|
|
||||||
onPress,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
count: number;
|
|
||||||
active: boolean;
|
|
||||||
onPress: () => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Pressable style={[styles.tabBtn, active && styles.tabBtnActive]} onPress={onPress}>
|
|
||||||
<Text style={[styles.tabBtnText, active && styles.tabBtnTextActive]}>
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
{count > 0 ? (
|
|
||||||
<View style={styles.tabCount}>
|
|
||||||
<Text style={styles.tabCountText}>{count}</Text>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 },
|
gridRow: { gap: 10, marginBottom: 14 },
|
||||||
|
|
@ -475,27 +431,4 @@ const styles = StyleSheet.create({
|
||||||
tagChipActive: { backgroundColor: theme.accent, borderColor: theme.accentText },
|
tagChipActive: { backgroundColor: theme.accent, borderColor: theme.accentText },
|
||||||
tagChipText: { color: theme.muted, fontSize: 12, fontWeight: '600' },
|
tagChipText: { color: theme.muted, fontSize: 12, fontWeight: '600' },
|
||||||
tagChipTextActive: { color: theme.fg, fontWeight: '700' },
|
tagChipTextActive: { color: theme.fg, fontWeight: '700' },
|
||||||
tabRow: { flexDirection: 'row', gap: 8, marginBottom: 8 },
|
|
||||||
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 },
|
|
||||||
tabCount: {
|
|
||||||
backgroundColor: theme.border,
|
|
||||||
paddingHorizontal: 6,
|
|
||||||
paddingVertical: 1,
|
|
||||||
borderRadius: 4,
|
|
||||||
},
|
|
||||||
tabCountText: { color: theme.muted, fontWeight: '700', fontSize: 11 },
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,12 @@ import {
|
||||||
Pressable,
|
Pressable,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
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';
|
||||||
|
import { SearchInput, SegmentTabs, Toolbar } from '../ui';
|
||||||
import type { PerformerCount } from '../types';
|
import type { PerformerCount } from '../types';
|
||||||
|
|
||||||
type Order = 'scene_count' | 'name';
|
type Order = 'scene_count' | 'name';
|
||||||
|
|
@ -29,7 +29,6 @@ export function PerformersScreen() {
|
||||||
const [q, setQ] = useState('');
|
const [q, setQ] = useState('');
|
||||||
const [debouncedQ, setDebouncedQ] = useState('');
|
const [debouncedQ, setDebouncedQ] = useState('');
|
||||||
const [order, setOrder] = useState<Order>('scene_count');
|
const [order, setOrder] = useState<Order>('scene_count');
|
||||||
const [searchFocused, setSearchFocused] = useState(false);
|
|
||||||
const route = useRoute<RouteProp<RootStackParamList, 'Performers'>>();
|
const route = useRoute<RouteProp<RootStackParamList, 'Performers'>>();
|
||||||
const vertical = route.params?.vertical;
|
const vertical = route.params?.vertical;
|
||||||
|
|
||||||
|
|
@ -44,41 +43,23 @@ export function PerformersScreen() {
|
||||||
client.listPerformers({ q: debouncedQ || undefined, order, per_page: 200, vertical }),
|
client.listPerformers({ q: debouncedQ || undefined, order, per_page: 200, vertical }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const total = data?.items.length ?? 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.headerRow}>
|
{/* Jeden wiersz (2026-09-15): pole i przełącznik sortowania obok siebie. Wcześniej
|
||||||
<Text style={styles.headerLabel}>Performers</Text>
|
pod nagłówkiem była druga etykieta z licznikiem, podpowiedź, pole i osobny
|
||||||
<Text style={styles.headerCount}>{total}</Text>
|
wiersz Top/A-Z, czyli cztery linie przed listą. */}
|
||||||
</View>
|
<Toolbar>
|
||||||
<Text style={styles.hint}>tap a row → all their scenes</Text>
|
<SearchInput value={q} onChangeText={setQ} placeholder="search performer…" />
|
||||||
|
<SegmentTabs
|
||||||
<View style={styles.toolbar}>
|
style={styles.order}
|
||||||
<TextInput
|
tabs={[
|
||||||
style={[styles.search, searchFocused && styles.searchFocused]}
|
{ key: 'scene_count', label: 'Top' },
|
||||||
value={q}
|
{ key: 'name', label: 'A-Z' },
|
||||||
onChangeText={setQ}
|
]}
|
||||||
onFocus={() => setSearchFocused(true)}
|
value={order}
|
||||||
onBlur={() => setSearchFocused(false)}
|
onChange={setOrder}
|
||||||
placeholder="search performer…"
|
|
||||||
placeholderTextColor={theme.mutedDim}
|
|
||||||
autoCapitalize="none"
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</Toolbar>
|
||||||
|
|
||||||
<View style={styles.segment}>
|
|
||||||
<SegButton
|
|
||||||
active={order === 'scene_count'}
|
|
||||||
onPress={() => setOrder('scene_count')}
|
|
||||||
label="Top"
|
|
||||||
/>
|
|
||||||
<SegButton
|
|
||||||
active={order === 'name'}
|
|
||||||
onPress={() => setOrder('name')}
|
|
||||||
label="A-Z"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{isLoading && <ActivityIndicator color={theme.fg} style={{ marginTop: 24 }} />}
|
{isLoading && <ActivityIndicator color={theme.fg} style={{ marginTop: 24 }} />}
|
||||||
{error instanceof Error && <Text style={styles.error}>{error.message}</Text>}
|
{error instanceof Error && <Text style={styles.error}>{error.message}</Text>}
|
||||||
|
|
@ -109,27 +90,6 @@ export function PerformersScreen() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SegButton({
|
|
||||||
active,
|
|
||||||
onPress,
|
|
||||||
label,
|
|
||||||
}: {
|
|
||||||
active: boolean;
|
|
||||||
onPress: () => void;
|
|
||||||
label: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Pressable
|
|
||||||
onPress={onPress}
|
|
||||||
style={[styles.segButton, active && styles.segButtonActive]}
|
|
||||||
>
|
|
||||||
<Text style={[styles.segButtonText, active && styles.segButtonTextActive]}>
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initials(name: string): string {
|
function initials(name: string): string {
|
||||||
const parts = name.trim().split(/\s+/);
|
const parts = name.trim().split(/\s+/);
|
||||||
if (parts.length === 0) return '?';
|
if (parts.length === 0) return '?';
|
||||||
|
|
@ -176,64 +136,7 @@ function PerformerRow({
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: 16, paddingTop: 12 },
|
container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: 16, paddingTop: 12 },
|
||||||
|
|
||||||
headerRow: {
|
order: { flex: 0, width: 124 },
|
||||||
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' },
|
|
||||||
hint: { color: theme.mutedDim, fontSize: 11, marginBottom: 12 },
|
|
||||||
|
|
||||||
toolbar: { flexDirection: 'row', gap: 12, marginBottom: 10 },
|
|
||||||
search: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: theme.card,
|
|
||||||
borderColor: theme.borderStrong,
|
|
||||||
borderWidth: 1.5,
|
|
||||||
borderRadius: 12,
|
|
||||||
color: theme.fg,
|
|
||||||
padding: 12,
|
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
searchFocused: { borderColor: theme.borderFocus },
|
|
||||||
|
|
||||||
segment: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
backgroundColor: theme.bgElevated,
|
|
||||||
borderColor: theme.border,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: 4,
|
|
||||||
marginBottom: 14,
|
|
||||||
alignSelf: 'flex-start',
|
|
||||||
},
|
|
||||||
segButton: {
|
|
||||||
paddingHorizontal: 14,
|
|
||||||
paddingVertical: 6,
|
|
||||||
borderRadius: 8,
|
|
||||||
},
|
|
||||||
segButtonActive: {
|
|
||||||
backgroundColor: theme.accent,
|
|
||||||
shadowColor: theme.accent,
|
|
||||||
shadowOffset: { width: 0, height: 0 },
|
|
||||||
shadowOpacity: 0.4,
|
|
||||||
shadowRadius: 6,
|
|
||||||
elevation: 2,
|
|
||||||
},
|
|
||||||
segButtonText: {
|
|
||||||
color: theme.muted,
|
|
||||||
fontWeight: '700',
|
|
||||||
fontSize: 13,
|
|
||||||
},
|
|
||||||
segButtonTextActive: { color: theme.fg },
|
|
||||||
|
|
||||||
row: {
|
row: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@ export function StudioMoviesScreen() {
|
||||||
const { slug, name, studioId } = route.params;
|
const { slug, name, studioId } = route.params;
|
||||||
|
|
||||||
const favoritesQuery = useQuery({
|
const favoritesQuery = useQuery({
|
||||||
queryKey: ['favorites-studios'],
|
queryKey: ['favorites-studios', 'ids'],
|
||||||
queryFn: () => client.listFavoriteStudios(),
|
queryFn: () => client.listFavoriteStudios({ counts: false }),
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
});
|
});
|
||||||
const isFavorite = !!favoritesQuery.data?.items.find((f) => f.studio_id === studioId);
|
const isFavorite = !!favoritesQuery.data?.items.find((f) => f.studio_id === studioId);
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Alert,
|
Alert,
|
||||||
FlatList,
|
FlatList,
|
||||||
Pressable,
|
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
View,
|
View,
|
||||||
|
|
@ -30,7 +29,7 @@ import { useClient } from '../ClientContext';
|
||||||
import { usePreferences } from '../PreferencesContext';
|
import { usePreferences } from '../PreferencesContext';
|
||||||
import type { RootStackParamList } from '../navigation';
|
import type { RootStackParamList } from '../navigation';
|
||||||
import { theme } from '../theme';
|
import { theme } from '../theme';
|
||||||
import { HeaderIconButton } from '../ui';
|
import { HeaderIconButton, SegmentTabs, Toolbar } from '../ui';
|
||||||
import type { SceneOut } from '../types';
|
import type { SceneOut } from '../types';
|
||||||
|
|
||||||
type Tab = 'scenes' | 'movies';
|
type Tab = 'scenes' | 'movies';
|
||||||
|
|
@ -51,9 +50,11 @@ export function StudioScenesScreen() {
|
||||||
const { id: slug, name, seenSince, studioId, vertical } = route.params;
|
const { id: slug, name, seenSince, studioId, vertical } = route.params;
|
||||||
const [tab, setTab] = React.useState<Tab>('scenes');
|
const [tab, setTab] = React.useState<Tab>('scenes');
|
||||||
|
|
||||||
|
// Sam stan gwiazdki (bez liczników +N, patrz PerformerScenesScreen). Klucz pod
|
||||||
|
// ['favorites-studios'], więc invalidate tamtego klucza odświeża też ten.
|
||||||
const favoritesQuery = useQuery({
|
const favoritesQuery = useQuery({
|
||||||
queryKey: ['favorites-studios'],
|
queryKey: ['favorites-studios', 'ids'],
|
||||||
queryFn: () => client.listFavoriteStudios(),
|
queryFn: () => client.listFavoriteStudios({ counts: false }),
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
});
|
});
|
||||||
const isFavorite = !!favoritesQuery.data?.items.find((f) => f.studio_id === studioId);
|
const isFavorite = !!favoritesQuery.data?.items.find((f) => f.studio_id === studioId);
|
||||||
|
|
@ -158,21 +159,18 @@ export function StudioScenesScreen() {
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{/* Pasek zakładek ZAWSZE widoczny, też podczas ładowania: gdyby siedział w
|
{/* Pasek zakładek ZAWSZE widoczny, też podczas ładowania: gdyby siedział w
|
||||||
ListHeaderComponent, pierwsze wejście na Movies dałoby pusty ekran bez
|
ListHeaderComponent, pierwsze wejście na Movies dałoby pusty ekran bez
|
||||||
drogi powrotnej (ta sama pułapka co w PerformerScenesScreen). */}
|
drogi powrotnej (ta sama pułapka co w PerformerScenesScreen). Liczniki są
|
||||||
<View style={styles.tabRow}>
|
w zakładkach, osobny wiersz "N scenes" pod nimi zniknął (2026-09-15). */}
|
||||||
<TabButton
|
<Toolbar>
|
||||||
label="Scenes"
|
<SegmentTabs
|
||||||
count={data?.total ?? 0}
|
tabs={[
|
||||||
active={tab === 'scenes'}
|
{ key: 'scenes', label: 'Scenes', count: data ? `${data.total}${data.total_capped ? '+' : ''}` : undefined },
|
||||||
onPress={() => setTab('scenes')}
|
{ key: 'movies', label: 'Movies', count: moviesTotal },
|
||||||
|
]}
|
||||||
|
value={tab}
|
||||||
|
onChange={setTab}
|
||||||
/>
|
/>
|
||||||
<TabButton
|
</Toolbar>
|
||||||
label="Movies"
|
|
||||||
count={moviesTotal}
|
|
||||||
active={tab === 'movies'}
|
|
||||||
onPress={() => setTab('movies')}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{tab === 'scenes' ? (
|
{tab === 'scenes' ? (
|
||||||
<>
|
<>
|
||||||
|
|
@ -190,13 +188,6 @@ export function StudioScenesScreen() {
|
||||||
)}
|
)}
|
||||||
refreshing={isRefetching}
|
refreshing={isRefetching}
|
||||||
onRefresh={refetch}
|
onRefresh={refetch}
|
||||||
ListHeaderComponent={
|
|
||||||
<View style={styles.header}>
|
|
||||||
<Text style={styles.subtitle}>
|
|
||||||
{data ? `${data.total}${data.total_capped ? '+' : ''} ${data.total === 1 ? 'scene' : 'scenes'}` : ' '}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
ListEmptyComponent={!isLoading ? <Text style={styles.muted}>no scenes</Text> : null}
|
ListEmptyComponent={!isLoading ? <Text style={styles.muted}>no scenes</Text> : null}
|
||||||
contentContainerStyle={{ paddingBottom: 24 }}
|
contentContainerStyle={{ paddingBottom: 24 }}
|
||||||
/>
|
/>
|
||||||
|
|
@ -226,15 +217,6 @@ export function StudioScenesScreen() {
|
||||||
)}
|
)}
|
||||||
refreshing={moviesQuery.isRefetching}
|
refreshing={moviesQuery.isRefetching}
|
||||||
onRefresh={moviesQuery.refetch}
|
onRefresh={moviesQuery.refetch}
|
||||||
ListHeaderComponent={
|
|
||||||
<View style={styles.header}>
|
|
||||||
<Text style={styles.subtitle}>
|
|
||||||
{moviesQuery.data
|
|
||||||
? `${moviesTotal} ${moviesTotal === 1 ? 'film' : 'films'}`
|
|
||||||
: ' '}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
!moviesQuery.isLoading ? (
|
!moviesQuery.isLoading ? (
|
||||||
<Text style={styles.muted}>no movies for this studio</Text>
|
<Text style={styles.muted}>no movies for this studio</Text>
|
||||||
|
|
@ -249,61 +231,9 @@ export function StudioScenesScreen() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabButton({
|
|
||||||
label,
|
|
||||||
count,
|
|
||||||
active,
|
|
||||||
onPress,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
count: number;
|
|
||||||
active: boolean;
|
|
||||||
onPress: () => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Pressable style={[styles.tabBtn, active && styles.tabBtnActive]} onPress={onPress}>
|
|
||||||
<Text style={[styles.tabBtnText, active && styles.tabBtnTextActive]}>{label}</Text>
|
|
||||||
{count > 0 ? (
|
|
||||||
<View style={styles.tabCount}>
|
|
||||||
<Text style={styles.tabCountText}>{count}</Text>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 },
|
gridRow: { gap: 10, marginBottom: 14 },
|
||||||
header: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 8,
|
|
||||||
paddingHorizontal: 4,
|
|
||||||
},
|
|
||||||
subtitle: { color: theme.muted },
|
|
||||||
tabRow: { flexDirection: 'row', gap: 8, marginBottom: 10, paddingHorizontal: 4 },
|
|
||||||
tabBtn: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 6,
|
|
||||||
paddingVertical: 6,
|
|
||||||
paddingHorizontal: 14,
|
|
||||||
borderRadius: 999,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: theme.border,
|
|
||||||
},
|
|
||||||
tabBtnActive: { borderColor: theme.accentText },
|
|
||||||
tabBtnText: { color: theme.muted, fontSize: 13 },
|
|
||||||
tabBtnTextActive: { color: theme.accentText, fontWeight: '700' },
|
|
||||||
tabCount: {
|
|
||||||
backgroundColor: theme.border,
|
|
||||||
borderRadius: 999,
|
|
||||||
paddingHorizontal: 6,
|
|
||||||
paddingVertical: 1,
|
|
||||||
},
|
|
||||||
tabCountText: { color: theme.muted, fontSize: 11 },
|
|
||||||
muted: { color: theme.muted, textAlign: 'center', marginTop: 24 },
|
muted: { color: theme.muted, textAlign: 'center', marginTop: 24 },
|
||||||
error: { color: theme.bad, padding: 12 },
|
error: { color: theme.bad, padding: 12 },
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import { ActivityIndicator, AppState, Modal, Pressable, ScrollView, StyleSheet,
|
||||||
import { useClient } from '../ClientContext';
|
import { useClient } from '../ClientContext';
|
||||||
import { ClipCell } from '../components/swipe/ClipCell';
|
import { ClipCell } from '../components/swipe/ClipCell';
|
||||||
import { NATIVE_RUNTIME, isNewerVersion } from '../lib/appVersion';
|
import { NATIVE_RUNTIME, isNewerVersion } from '../lib/appVersion';
|
||||||
|
import { setBarHidden } from '../lib/bottomBar';
|
||||||
import { usePersistedState } from '../lib/persistedState';
|
import { usePersistedState } from '../lib/persistedState';
|
||||||
import { SwipeTelemetry, leaveKind, newSessionId } from '../lib/swipeTelemetry';
|
import { SwipeTelemetry, leaveKind, newSessionId } from '../lib/swipeTelemetry';
|
||||||
import { fonts, theme } from '../theme';
|
import { fonts, theme } from '../theme';
|
||||||
|
|
@ -99,6 +100,21 @@ export function SwipeScreen() {
|
||||||
}
|
}
|
||||||
}, [index, items.length, feed.hasNextPage, feed.isFetchingNextPage, feed]);
|
}, [index, items.length, feed.hasNextPage, feed.isFetchingNextPage, feed]);
|
||||||
|
|
||||||
|
// ---- dolny pasek ----------------------------------------------------------------------
|
||||||
|
// Jak na listach: przewijanie do przodu chowa pasek, cofniecie o klip go przywraca, na
|
||||||
|
// pierwszym klipie jest zawsze. Zeby wrocic do innych zakladek, wystarczy cofnac sie
|
||||||
|
// o jeden klip. Po wyjsciu z Edge pasek musi byc widoczny.
|
||||||
|
React.useEffect(() => () => setBarHidden(false), []);
|
||||||
|
const onPageSelected = React.useCallback(
|
||||||
|
(e: { nativeEvent: { position: number } }) => {
|
||||||
|
const next = e.nativeEvent.position;
|
||||||
|
if (next === 0 || next < index) setBarHidden(false);
|
||||||
|
else if (next > index) setBarHidden(true);
|
||||||
|
setIndex(next);
|
||||||
|
},
|
||||||
|
[index],
|
||||||
|
);
|
||||||
|
|
||||||
// ---- telemetria ----------------------------------------------------------------------
|
// ---- telemetria ----------------------------------------------------------------------
|
||||||
const telemetry = React.useRef<SwipeTelemetry | null>(null);
|
const telemetry = React.useRef<SwipeTelemetry | null>(null);
|
||||||
if (!telemetry.current) telemetry.current = new SwipeTelemetry(client, newSessionId());
|
if (!telemetry.current) telemetry.current = new SwipeTelemetry(client, newSessionId());
|
||||||
|
|
@ -261,6 +277,7 @@ export function SwipeScreen() {
|
||||||
telemetry.current = new SwipeTelemetry(client, newSessionId());
|
telemetry.current = new SwipeTelemetry(client, newSessionId());
|
||||||
telemetry.current.start();
|
telemetry.current.start();
|
||||||
prevIndexRef.current = null;
|
prevIndexRef.current = null;
|
||||||
|
setBarHidden(false);
|
||||||
setIndex(0);
|
setIndex(0);
|
||||||
setSeedKey((k) => k + 1);
|
setSeedKey((k) => k + 1);
|
||||||
}, [client]);
|
}, [client]);
|
||||||
|
|
@ -317,7 +334,7 @@ export function SwipeScreen() {
|
||||||
initialPage={0}
|
initialPage={0}
|
||||||
offscreenPageLimit={1}
|
offscreenPageLimit={1}
|
||||||
overdrag
|
overdrag
|
||||||
onPageSelected={(e: { nativeEvent: { position: number } }) => setIndex(e.nativeEvent.position)}
|
onPageSelected={onPageSelected}
|
||||||
>
|
>
|
||||||
{items.map((it, i) => (
|
{items.map((it, i) => (
|
||||||
<View key={it.clip.id} style={styles.page} collapsable={false}>
|
<View key={it.clip.id} style={styles.page} collapsable={false}>
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,12 @@ import {
|
||||||
Pressable,
|
Pressable,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
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';
|
||||||
|
import { SearchInput, SegmentTabs, Toolbar } from '../ui';
|
||||||
import type { TagCount } from '../types';
|
import type { TagCount } from '../types';
|
||||||
|
|
||||||
type Order = 'popular' | 'name';
|
type Order = 'popular' | 'name';
|
||||||
|
|
@ -31,7 +31,6 @@ export function TagsScreen() {
|
||||||
const [q, setQ] = useState('');
|
const [q, setQ] = useState('');
|
||||||
const [debouncedQ, setDebouncedQ] = useState('');
|
const [debouncedQ, setDebouncedQ] = useState('');
|
||||||
const [order, setOrder] = useState<Order>('popular');
|
const [order, setOrder] = useState<Order>('popular');
|
||||||
const [searchFocused, setSearchFocused] = useState(false);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const t = setTimeout(() => setDebouncedQ(q), 350);
|
const t = setTimeout(() => setDebouncedQ(q), 350);
|
||||||
|
|
@ -53,41 +52,23 @@ export function TagsScreen() {
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const total = data?.items.length ?? 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.headerRow}>
|
{/* Jeden wiersz (2026-09-15): pole i przełącznik sortowania obok siebie. Wcześniej
|
||||||
<Text style={styles.headerLabel}>Tags</Text>
|
pod nagłówkiem była druga etykieta z licznikiem, podpowiedź, pole i osobny
|
||||||
<Text style={styles.headerCount}>{total}</Text>
|
wiersz Top/A-Z, czyli cztery linie przed listą. */}
|
||||||
</View>
|
<Toolbar>
|
||||||
<Text style={styles.hint}>tap a chip → filter scenes by tag</Text>
|
<SearchInput value={q} onChangeText={setQ} placeholder="search tag…" />
|
||||||
|
<SegmentTabs
|
||||||
<View style={styles.toolbar}>
|
style={styles.order}
|
||||||
<TextInput
|
tabs={[
|
||||||
style={[styles.search, searchFocused && styles.searchFocused]}
|
{ key: 'popular', label: 'Top' },
|
||||||
value={q}
|
{ key: 'name', label: 'A-Z' },
|
||||||
onChangeText={setQ}
|
]}
|
||||||
onFocus={() => setSearchFocused(true)}
|
value={order}
|
||||||
onBlur={() => setSearchFocused(false)}
|
onChange={setOrder}
|
||||||
placeholder="search tag…"
|
|
||||||
placeholderTextColor={theme.mutedDim}
|
|
||||||
autoCapitalize="none"
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</Toolbar>
|
||||||
|
|
||||||
<View style={styles.segment}>
|
|
||||||
<SegButton
|
|
||||||
active={order === 'popular'}
|
|
||||||
onPress={() => setOrder('popular')}
|
|
||||||
label="Top"
|
|
||||||
/>
|
|
||||||
<SegButton
|
|
||||||
active={order === 'name'}
|
|
||||||
onPress={() => setOrder('name')}
|
|
||||||
label="A-Z"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{isLoading && <ActivityIndicator color={theme.fg} style={{ marginTop: 24 }} />}
|
{isLoading && <ActivityIndicator color={theme.fg} style={{ marginTop: 24 }} />}
|
||||||
{error instanceof Error && <Text style={styles.error}>{error.message}</Text>}
|
{error instanceof Error && <Text style={styles.error}>{error.message}</Text>}
|
||||||
|
|
@ -114,27 +95,6 @@ export function TagsScreen() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SegButton({
|
|
||||||
active,
|
|
||||||
onPress,
|
|
||||||
label,
|
|
||||||
}: {
|
|
||||||
active: boolean;
|
|
||||||
onPress: () => void;
|
|
||||||
label: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Pressable
|
|
||||||
onPress={onPress}
|
|
||||||
style={[styles.segButton, active && styles.segButtonActive]}
|
|
||||||
>
|
|
||||||
<Text style={[styles.segButtonText, active && styles.segButtonTextActive]}>
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TagChip({ tag, onPress }: { tag: TagCount; onPress: () => void }) {
|
function TagChip({ tag, onPress }: { tag: TagCount; onPress: () => void }) {
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
|
|
@ -154,56 +114,7 @@ function TagChip({ tag, onPress }: { tag: TagCount; onPress: () => void }) {
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: 16, paddingTop: 12 },
|
container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: 16, paddingTop: 12 },
|
||||||
|
|
||||||
headerRow: {
|
order: { flex: 0, width: 124 },
|
||||||
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' },
|
|
||||||
hint: { color: theme.mutedDim, fontSize: 11, marginBottom: 12 },
|
|
||||||
|
|
||||||
toolbar: { flexDirection: 'row', gap: 12, marginBottom: 10 },
|
|
||||||
search: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: theme.card,
|
|
||||||
borderColor: theme.borderStrong,
|
|
||||||
borderWidth: 1.5,
|
|
||||||
borderRadius: 12,
|
|
||||||
color: theme.fg,
|
|
||||||
padding: 12,
|
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
searchFocused: { borderColor: theme.borderFocus },
|
|
||||||
|
|
||||||
segment: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
backgroundColor: theme.bgElevated,
|
|
||||||
borderColor: theme.border,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: 4,
|
|
||||||
marginBottom: 14,
|
|
||||||
alignSelf: 'flex-start',
|
|
||||||
},
|
|
||||||
segButton: { paddingHorizontal: 14, paddingVertical: 6, borderRadius: 8 },
|
|
||||||
segButtonActive: {
|
|
||||||
backgroundColor: theme.accent,
|
|
||||||
shadowColor: theme.accent,
|
|
||||||
shadowOffset: { width: 0, height: 0 },
|
|
||||||
shadowOpacity: 0.4,
|
|
||||||
shadowRadius: 6,
|
|
||||||
elevation: 2,
|
|
||||||
},
|
|
||||||
segButtonText: { color: theme.muted, fontWeight: '700', fontSize: 13 },
|
|
||||||
segButtonTextActive: { color: theme.fg },
|
|
||||||
|
|
||||||
gridRow: { gap: 10, marginBottom: 10 },
|
gridRow: { gap: 10, marginBottom: 10 },
|
||||||
chip: {
|
chip: {
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,66 @@ export function Toolbar({ children }: { children: React.ReactNode }) {
|
||||||
return <View style={styles.toolbar}>{children}</View>;
|
return <View style={styles.toolbar}>{children}</View>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SegmentTab<T extends string> {
|
||||||
|
key: T;
|
||||||
|
label: string;
|
||||||
|
/** Liczba po etykiecie (np. 200+), pomijana gdy pusta albo 0. */
|
||||||
|
count?: number | string;
|
||||||
|
/** Nowe pozycje: czerwony licznik w rogu, jak na IconButton. */
|
||||||
|
badge?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zakladki ekranu w jednym wierszu o wysokosci pola wyszukiwania (2026-09-15). Wczesniej
|
||||||
|
* ekrany podrzedne mialy pod naglowkiem osobny wiersz zakladek, pod nim wiersz z
|
||||||
|
* licznikiem i ikonami, a ulubione jeszcze etykiete i podpowiedz, czyli trzy albo cztery
|
||||||
|
* linie zanim zaczynala sie lista. Teraz zakladki siedza w Toolbarze obok ikon.
|
||||||
|
*/
|
||||||
|
export function SegmentTabs<T extends string>({
|
||||||
|
tabs,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
tabs: SegmentTab<T>[];
|
||||||
|
value: T;
|
||||||
|
onChange: (key: T) => void;
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<View style={[styles.segWrap, style]} accessibilityRole="tablist">
|
||||||
|
{tabs.map((t) => {
|
||||||
|
const on = t.key === value;
|
||||||
|
const showCount = t.count !== undefined && t.count !== 0 && t.count !== '';
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={t.key}
|
||||||
|
onPress={() => onChange(t.key)}
|
||||||
|
accessibilityRole="tab"
|
||||||
|
accessibilityState={{ selected: on }}
|
||||||
|
accessibilityLabel={showCount ? `${t.label}, ${t.count}` : t.label}
|
||||||
|
style={[styles.seg, on && styles.segActive]}
|
||||||
|
>
|
||||||
|
<Text numberOfLines={1} style={[styles.segText, on && styles.segTextActive]}>
|
||||||
|
{t.label}
|
||||||
|
</Text>
|
||||||
|
{showCount ? (
|
||||||
|
<Text numberOfLines={1} style={styles.segCount}>
|
||||||
|
{t.count}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
{t.badge && t.badge > 0 ? (
|
||||||
|
<View style={styles.badge}>
|
||||||
|
<Text style={styles.badgeText}>+{t.badge}</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
toolbar: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 12 },
|
toolbar: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 12 },
|
||||||
headerIcon: { paddingHorizontal: 6, paddingVertical: 4 },
|
headerIcon: { paddingHorizontal: 6, paddingVertical: 4 },
|
||||||
|
|
@ -237,4 +297,30 @@ const styles = StyleSheet.create({
|
||||||
pillCompact: { height: 36, paddingHorizontal: 14 },
|
pillCompact: { height: 36, paddingHorizontal: 14 },
|
||||||
pillActive: { borderColor: theme.accentText, backgroundColor: `${theme.accent}1A` },
|
pillActive: { borderColor: theme.accentText, backgroundColor: `${theme.accent}1A` },
|
||||||
pillText: { fontFamily: fonts.medium, fontSize: 14 },
|
pillText: { fontFamily: fonts.medium, fontSize: 14 },
|
||||||
|
segWrap: {
|
||||||
|
flex: 1,
|
||||||
|
height: CONTROL_H,
|
||||||
|
flexDirection: 'row',
|
||||||
|
padding: 3,
|
||||||
|
gap: 3,
|
||||||
|
backgroundColor: theme.card,
|
||||||
|
borderColor: theme.borderStrong,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: RADIUS,
|
||||||
|
},
|
||||||
|
seg: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 5,
|
||||||
|
paddingHorizontal: 4,
|
||||||
|
borderRadius: RADIUS - 3,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
},
|
||||||
|
segActive: { borderColor: theme.accentText, backgroundColor: `${theme.accent}1A` },
|
||||||
|
segText: { fontFamily: fonts.medium, fontSize: 14, color: theme.muted, flexShrink: 1 },
|
||||||
|
segTextActive: { color: theme.accentText },
|
||||||
|
segCount: { fontFamily: fonts.body, fontSize: 12, color: theme.mutedDim },
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue