- 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>
326 lines
9.8 KiB
TypeScript
326 lines
9.8 KiB
TypeScript
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
import { useNavigation } from '@react-navigation/native';
|
|
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|
import type { RootStackParamList } from './navigation';
|
|
/**
|
|
* Wspólny zestaw kontrolek nagłówka ekranów-list (Scenes, Movies, Sites, JAV, Anime).
|
|
*
|
|
* Po co: każdy ekran miał WŁASNĄ kopię stylów `search`/`toolbar`/przycisku filtra,
|
|
* definiowaną niezależnie, więc rozjechały się na wszystkim naraz (zgłoszenie 2026-08-18,
|
|
* cztery zrzuty obok siebie): róg pola 12 vs 10, ramka 1,5 vs 1, font 16 vs 14, odstępy
|
|
* 12 vs 8, dwa promienie pigułek (8 i 12), a etykiety miały `fontWeight:'700'` BEZ
|
|
* `fontFamily` — RN nie syntezuje pogrubienia dla fontu custom, więc renderowały się
|
|
* chudym Regularem (stąd wrażenie „innych czcionek"). Jeden moduł = jeden wygląd.
|
|
*
|
|
* Zasady: jedna wysokość kontrolek w wierszu (pole = przycisk filtra), jeden promień,
|
|
* jedna ramka, waga przez `fontFamily` (nigdy przez `fontWeight` na custom foncie),
|
|
* stan aktywny robi kolor (accentText + delikatny tint), nie kształt.
|
|
*/
|
|
import React from 'react';
|
|
import {
|
|
Pressable,
|
|
StyleProp,
|
|
StyleSheet,
|
|
Text,
|
|
TextInput,
|
|
TextInputProps,
|
|
View,
|
|
ViewStyle,
|
|
} from 'react-native';
|
|
|
|
import { fonts, theme } from './theme';
|
|
|
|
/** Poziomy padding treści list i wysokość kontrolek w wierszu narzędzi. Wspólne, żeby
|
|
* pola i przyciski na każdej zakładce zaczynały się i kończyły w tej samej linii. */
|
|
export const SCREEN_PAD_H = 16;
|
|
export const SCREEN_PAD_TOP = 14;
|
|
export const CONTROL_H = 44;
|
|
const RADIUS = 12;
|
|
|
|
/** Pole wyszukiwania. Ramka focus zarządzana wewnątrz, żeby każdy ekran nie powtarzał
|
|
* tego samego `searchFocused`. Wypełnia szerokość (flex:1) w wierszu z przyciskiem. */
|
|
export function SearchInput(props: TextInputProps) {
|
|
const [focused, setFocused] = React.useState(false);
|
|
return (
|
|
<TextInput
|
|
placeholderTextColor={theme.muted}
|
|
autoCapitalize="none"
|
|
{...props}
|
|
onFocus={(e) => {
|
|
setFocused(true);
|
|
props.onFocus?.(e);
|
|
}}
|
|
onBlur={(e) => {
|
|
setFocused(false);
|
|
props.onBlur?.(e);
|
|
}}
|
|
style={[styles.search, focused && styles.searchFocused, props.style]}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/** Pigułka-przycisk (Filter, Performers, Tags, ★, Top/Recent). `compact` = niższy wariant
|
|
* dla wiersza akcji pod polem. `active` przełącza kolor, nie kształt. */
|
|
export function PillButton({
|
|
label,
|
|
active,
|
|
onPress,
|
|
onLongPress,
|
|
compact,
|
|
tone = 'accent',
|
|
style,
|
|
}: {
|
|
label: string;
|
|
active?: boolean;
|
|
onPress?: () => void;
|
|
onLongPress?: () => void;
|
|
compact?: boolean;
|
|
/** 'accent' = czerwień (filtr/aktywny), 'secondary' = amber (Performers/Tags). */
|
|
tone?: 'accent' | 'secondary';
|
|
style?: StyleProp<ViewStyle>;
|
|
}) {
|
|
const textColor = active ? theme.accentText : tone === 'secondary' ? theme.accentSecondary : theme.fg;
|
|
return (
|
|
<Pressable
|
|
onPress={onPress}
|
|
onLongPress={onLongPress}
|
|
style={[
|
|
styles.pill,
|
|
compact ? styles.pillCompact : styles.pillTall,
|
|
active && styles.pillActive,
|
|
style,
|
|
]}
|
|
>
|
|
<Text style={[styles.pillText, { color: textColor }]}>{label}</Text>
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Kwadratowy przycisk z ikona zamiast pigulki z napisem. `badge` to licznik w rogu
|
|
* (np. liczba aktywnych filtrow), pokazywany tylko gdy > 0.
|
|
*/
|
|
export function IconButton({
|
|
icon,
|
|
active,
|
|
badge,
|
|
onPress,
|
|
accessibilityLabel,
|
|
}: {
|
|
icon: React.ComponentProps<typeof MaterialCommunityIcons>['name'];
|
|
active?: boolean;
|
|
badge?: number;
|
|
onPress?: () => void;
|
|
accessibilityLabel: string;
|
|
}) {
|
|
return (
|
|
<Pressable
|
|
onPress={onPress}
|
|
hitSlop={6}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={accessibilityLabel}
|
|
style={[styles.iconBtn, active && styles.pillActive]}
|
|
>
|
|
<MaterialCommunityIcons name={icon} size={22} color={active ? theme.accentText : theme.fg} />
|
|
{badge && badge > 0 ? (
|
|
<View style={styles.badge}>
|
|
<Text style={styles.badgeText}>{badge}</Text>
|
|
</View>
|
|
) : null}
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Wejscie do ustawien. Gorna belka znikla (2026-09-07), wiec kolo zebate siedzi w
|
|
* wierszu narzedzi kazdej sekcji; darowizna i zmiana backendu sa w ustawieniach.
|
|
*/
|
|
export function SettingsButton() {
|
|
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
|
return (
|
|
<IconButton
|
|
icon="cog-outline"
|
|
accessibilityLabel="Settings"
|
|
onPress={() => navigation.navigate('AppLockSettings')}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Ikona w prawym rogu natywnego naglowka (ulubione, ukryj). Jeden rozmiar i kolor
|
|
* na wszystkich ekranach zamiast znakow ★ i 🚫 w roznych rozmiarach.
|
|
*/
|
|
export function HeaderIconButton({
|
|
icon,
|
|
active,
|
|
muted,
|
|
onPress,
|
|
disabled,
|
|
accessibilityLabel,
|
|
}: {
|
|
icon: React.ComponentProps<typeof MaterialCommunityIcons>['name'];
|
|
active?: boolean;
|
|
/** Drugorzedna akcja (np. ukryj): szary zamiast koloru tekstu. */
|
|
muted?: boolean;
|
|
onPress?: () => void;
|
|
disabled?: boolean;
|
|
accessibilityLabel: string;
|
|
}) {
|
|
const color = active ? theme.accentText : muted ? theme.mutedDim : theme.muted;
|
|
return (
|
|
<Pressable
|
|
onPress={onPress}
|
|
disabled={disabled}
|
|
hitSlop={12}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={accessibilityLabel}
|
|
style={({ pressed }) => [styles.headerIcon, pressed && { opacity: 0.6 }]}
|
|
>
|
|
<MaterialCommunityIcons name={icon} size={24} color={color} />
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
/** Wiersz narzędzi: pole + przyciski, jednakowy odstęp i margines na każdym ekranie. */
|
|
export function Toolbar({ children }: { children: React.ReactNode }) {
|
|
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({
|
|
toolbar: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 12 },
|
|
headerIcon: { paddingHorizontal: 6, paddingVertical: 4 },
|
|
iconBtn: {
|
|
width: CONTROL_H,
|
|
height: CONTROL_H,
|
|
borderRadius: RADIUS,
|
|
backgroundColor: theme.card,
|
|
borderColor: theme.borderStrong,
|
|
borderWidth: 1,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
badge: {
|
|
position: 'absolute',
|
|
top: -6,
|
|
right: -6,
|
|
minWidth: 18,
|
|
height: 18,
|
|
borderRadius: 9,
|
|
paddingHorizontal: 4,
|
|
backgroundColor: theme.accent,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
badgeText: { color: theme.fg, fontSize: 11, fontFamily: fonts.display },
|
|
search: {
|
|
flex: 1,
|
|
height: CONTROL_H,
|
|
backgroundColor: theme.card,
|
|
borderColor: theme.borderStrong,
|
|
borderWidth: 1,
|
|
borderRadius: RADIUS,
|
|
color: theme.fg,
|
|
paddingHorizontal: 14,
|
|
fontFamily: fonts.body,
|
|
fontSize: 15,
|
|
},
|
|
searchFocused: { borderColor: theme.borderFocus },
|
|
pill: {
|
|
backgroundColor: theme.card,
|
|
borderColor: theme.border,
|
|
borderWidth: 1,
|
|
borderRadius: RADIUS,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
pillTall: { height: CONTROL_H, paddingHorizontal: 16 },
|
|
pillCompact: { height: 36, paddingHorizontal: 14 },
|
|
pillActive: { borderColor: theme.accentText, backgroundColor: `${theme.accent}1A` },
|
|
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 },
|
|
});
|