fix(ui): jeden wspolny zestaw kontrolek naglowka zamiast kopii per ekran
Zgloszenie 2026-08-18 (cztery zrzuty obok siebie): "rozne odstepy miedzy zakladkami, kazda zakladka wyglada inaczej, rozne wielkosci text fields, rozne obramowki, chyba nawet inne czcionki". Przyczyna strukturalna: kazdy ekran-lista mial WLASNA kopie stylow search/toolbar/ przycisku filtra. Zmierzony dryf: rog pola 12 vs 10, ramka 1,5 vs 1, font 16 vs 14, odstep toolbara 12 vs 8, dwa promienie pigulek (8 i 12), padding kontenera 16/16 vs 16/12 vs 0. "Inne czcionki" = etykiety mialy fontWeight:'700' BEZ fontFamily, a RN nie syntezuje pogrubienia dla fontu custom -> chudy Regular zamiast bold. Nowy `src/ui.tsx`: SearchInput, PillButton, Toolbar + wspolne stale SCREEN_PAD_*. Jedna wysokosc kontrolek w wierszu, jeden promien, jedna ramka, waga przez fontFamily (nie fontWeight), stan aktywny robi kolor. Scenes/Movies/Sites przepiete na to, ich lokalne duplikaty stylow usuniete (w tym osierocony SegButton w Sites). JAV i Anime reuzywaja ScenesScreen, wiec dostaja to samo za darmo. Zweryfikowane na emulatorze: cztery zakladki jeden przy drugim maja identyczne pole, przycisk, pigulki i marginesy. 403 na liscie to build debug spoza allowlisty, nie UI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
6d338219ea
commit
17e4ecaa8a
5 changed files with 181 additions and 199 deletions
|
|
@ -16,6 +16,13 @@ export type ChangelogEntry = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CHANGELOG: ChangelogEntry[] = [
|
export const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
id: '2026-08-18e',
|
||||||
|
date: 'August 2026',
|
||||||
|
items: [
|
||||||
|
'Consistent look across the Scenes, Movies, Sites, JAV and Anime tabs. The search box, filter button and the little pills were each styled slightly differently per tab (different corners, borders, font sizes, spacing); they now all come from one shared set, so the top of every tab lines up and matches.',
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: '2026-08-18d',
|
id: '2026-08-18d',
|
||||||
date: 'August 2026',
|
date: 'August 2026',
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import {
|
||||||
Pressable,
|
Pressable,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useClient } from '../ClientContext';
|
import { useClient } from '../ClientContext';
|
||||||
|
|
@ -25,6 +24,7 @@ import { MoviePosterCard } from '../components/MoviePosterCard';
|
||||||
import { getLastSeen, markSeen } from '../lib/seenStore';
|
import { getLastSeen, markSeen } from '../lib/seenStore';
|
||||||
import type { RootStackParamList } from '../navigation';
|
import type { RootStackParamList } from '../navigation';
|
||||||
import { theme } from '../theme';
|
import { theme } from '../theme';
|
||||||
|
import { PillButton, SCREEN_PAD_H, SCREEN_PAD_TOP, SearchInput, Toolbar } from '../ui';
|
||||||
|
|
||||||
const PER_PAGE = 30;
|
const PER_PAGE = 30;
|
||||||
// 3 kolumny, plakaty 2:3 mieszczą się dobrze i widać 50% więcej filmów na ekran
|
// 3 kolumny, plakaty 2:3 mieszczą się dobrze i widać 50% więcej filmów na ekran
|
||||||
|
|
@ -104,24 +104,20 @@ export function MoviesScreen() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.toolbar}>
|
<Toolbar>
|
||||||
<TextInput
|
<SearchInput
|
||||||
style={[styles.search, { flex: 1 }]}
|
|
||||||
placeholder="search movies"
|
placeholder="search movies"
|
||||||
placeholderTextColor={theme.muted}
|
|
||||||
value={q}
|
value={q}
|
||||||
onChangeText={setQ}
|
onChangeText={setQ}
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
autoCapitalize="none"
|
|
||||||
returnKeyType="search"
|
returnKeyType="search"
|
||||||
/>
|
/>
|
||||||
<Pressable
|
<PillButton
|
||||||
style={[styles.filterBtn, !isDefaultFilters(filters) && styles.filterBtnActive]}
|
label={`Filter${!isDefaultFilters(filters) ? ' 1' : ''}`}
|
||||||
|
active={!isDefaultFilters(filters)}
|
||||||
onPress={() => setFiltersOpen(true)}
|
onPress={() => setFiltersOpen(true)}
|
||||||
>
|
/>
|
||||||
<Text style={styles.filterBtnText}>Filter{!isDefaultFilters(filters) ? '·' : ''}</Text>
|
</Toolbar>
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
<MovieFiltersSheet
|
<MovieFiltersSheet
|
||||||
visible={filtersOpen}
|
visible={filtersOpen}
|
||||||
value={filters}
|
value={filters}
|
||||||
|
|
@ -152,7 +148,7 @@ export function MoviesScreen() {
|
||||||
total > 0 ? <Text style={styles.subtitle}>{total} films</Text> : null
|
total > 0 ? <Text style={styles.subtitle}>{total} films</Text> : null
|
||||||
}
|
}
|
||||||
ListEmptyComponent={!isLoading ? <Text style={styles.muted}>no movies</Text> : null}
|
ListEmptyComponent={!isLoading ? <Text style={styles.muted}>no movies</Text> : null}
|
||||||
contentContainerStyle={{ paddingBottom: 24, paddingHorizontal: 6 }}
|
contentContainerStyle={{ paddingBottom: 24 }}
|
||||||
columnWrapperStyle={{ gap: 8 }}
|
columnWrapperStyle={{ gap: 8 }}
|
||||||
onEndReached={() => {
|
onEndReached={() => {
|
||||||
if (hasNextPage && !isFetchingNextPage) fetchNextPage();
|
if (hasNextPage && !isFetchingNextPage) fetchNextPage();
|
||||||
|
|
@ -167,40 +163,11 @@ export function MoviesScreen() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: theme.bg },
|
container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: SCREEN_PAD_H, paddingTop: SCREEN_PAD_TOP },
|
||||||
toolbar: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 8,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingTop: 8,
|
|
||||||
paddingBottom: 4,
|
|
||||||
},
|
|
||||||
filterBtn: {
|
|
||||||
height: 40,
|
|
||||||
paddingHorizontal: 14,
|
|
||||||
borderRadius: 10,
|
|
||||||
backgroundColor: theme.card,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: theme.border,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
filterBtnActive: {
|
filterBtnActive: {
|
||||||
backgroundColor: theme.accentDeep,
|
backgroundColor: theme.accentDeep,
|
||||||
borderColor: theme.accentText,
|
borderColor: theme.accentText,
|
||||||
},
|
},
|
||||||
filterBtnText: { color: theme.fg, fontSize: 13, fontWeight: '600' },
|
|
||||||
search: {
|
|
||||||
backgroundColor: theme.card,
|
|
||||||
borderColor: theme.borderStrong,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 10,
|
|
||||||
color: theme.fg,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingVertical: 8,
|
|
||||||
fontSize: 14,
|
|
||||||
},
|
|
||||||
subtitle: { color: theme.muted, fontSize: 12, paddingHorizontal: 4, paddingTop: 6, paddingBottom: 8 },
|
subtitle: { color: theme.muted, fontSize: 12, paddingHorizontal: 4, paddingTop: 6, paddingBottom: 8 },
|
||||||
error: { color: theme.bad, padding: 12 },
|
error: { color: theme.bad, padding: 12 },
|
||||||
muted: { color: theme.muted, textAlign: 'center', marginTop: 24 },
|
muted: { color: theme.muted, textAlign: 'center', marginTop: 24 },
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@ import {
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
|
||||||
View,
|
View,
|
||||||
} 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 { MigrationBanner } from '../components/MigrationBanner';
|
import { MigrationBanner } from '../components/MigrationBanner';
|
||||||
|
import { PillButton, SCREEN_PAD_H, SCREEN_PAD_TOP, SearchInput, Toolbar } from '../ui';
|
||||||
import { SceneTile, sceneGridProps } from '../components/SceneTile';
|
import { SceneTile, sceneGridProps } from '../components/SceneTile';
|
||||||
import { SceneGridSkeleton } from '../components/SceneGridSkeleton';
|
import { SceneGridSkeleton } from '../components/SceneGridSkeleton';
|
||||||
import { Thumb } from '../components/Thumb';
|
import { Thumb } from '../components/Thumb';
|
||||||
|
|
@ -151,40 +151,24 @@ export function ScenesScreen({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.toolbar}>
|
<Toolbar>
|
||||||
<TextInput
|
<SearchInput value={q} onChangeText={setQ} placeholder="search title…" />
|
||||||
style={styles.search}
|
|
||||||
value={q}
|
|
||||||
onChangeText={setQ}
|
|
||||||
placeholder="search title…"
|
|
||||||
placeholderTextColor={theme.muted}
|
|
||||||
autoCapitalize="none"
|
|
||||||
/>
|
|
||||||
{q.trim().length > 0 ? (
|
{q.trim().length > 0 ? (
|
||||||
<Pressable
|
<PillButton
|
||||||
|
label={alreadySaved ? '★ Saved' : '☆ Save'}
|
||||||
|
active={alreadySaved}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
const v = q.trim();
|
const v = q.trim();
|
||||||
if (v && !alreadySaved) addSaved.mutate(v);
|
if (v && !alreadySaved) addSaved.mutate(v);
|
||||||
}}
|
}}
|
||||||
disabled={alreadySaved}
|
/>
|
||||||
style={[styles.toolbarButton, alreadySaved && styles.toolbarButtonActive]}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[styles.toolbarButtonText, alreadySaved && { color: theme.accentText }]}
|
|
||||||
>
|
|
||||||
{alreadySaved ? '★ Saved' : '☆ Save'}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
) : null}
|
) : null}
|
||||||
<Pressable
|
<PillButton
|
||||||
|
label={`Filter${activeCount > 0 ? ` ${activeCount}` : ''}`}
|
||||||
|
active={activeCount > 0}
|
||||||
onPress={() => setFilterOpen(true)}
|
onPress={() => setFilterOpen(true)}
|
||||||
style={[styles.toolbarButton, activeCount > 0 && styles.toolbarButtonActive]}
|
/>
|
||||||
>
|
</Toolbar>
|
||||||
<Text style={[styles.toolbarButtonText, activeCount > 0 && { color: theme.accentText }]}>
|
|
||||||
Filter{activeCount > 0 ? ` ${activeCount}` : ''}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{saved.length > 0 ? (
|
{saved.length > 0 ? (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
|
@ -225,21 +209,21 @@ export function ScenesScreen({
|
||||||
|
|
||||||
<View style={styles.subToolbar}>
|
<View style={styles.subToolbar}>
|
||||||
{hasCast ? (
|
{hasCast ? (
|
||||||
<Pressable
|
<PillButton
|
||||||
style={styles.subButton}
|
label="Performers"
|
||||||
|
tone="secondary"
|
||||||
|
compact
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.navigate('Performers', verticalKey ? { vertical: verticalKey } : undefined)
|
navigation.navigate('Performers', verticalKey ? { vertical: verticalKey } : undefined)
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
<Text style={styles.subButtonText}>Performers</Text>
|
|
||||||
</Pressable>
|
|
||||||
) : null}
|
) : null}
|
||||||
<Pressable
|
<PillButton
|
||||||
style={styles.subButton}
|
label="Tags"
|
||||||
|
tone="secondary"
|
||||||
|
compact
|
||||||
onPress={() => navigation.navigate('Tags', verticalKey ? { vertical: verticalKey } : undefined)}
|
onPress={() => navigation.navigate('Tags', verticalKey ? { vertical: verticalKey } : undefined)}
|
||||||
>
|
/>
|
||||||
<Text style={styles.subButtonText}>Tags</Text>
|
|
||||||
</Pressable>
|
|
||||||
<FavoritesButton />
|
<FavoritesButton />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -352,14 +336,13 @@ function FavoritesButton() {
|
||||||
const hasNew = newTotal > 0;
|
const hasNew = newTotal > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<PillButton
|
||||||
style={[styles.subButton, hasNew && styles.subButtonActive]}
|
label={`★ ${hasNew ? newTotal : ''}`.trim()}
|
||||||
|
tone="secondary"
|
||||||
|
compact
|
||||||
|
active={hasNew}
|
||||||
onPress={() => navigation.navigate('Favorites')}
|
onPress={() => navigation.navigate('Favorites')}
|
||||||
>
|
/>
|
||||||
<Text style={[styles.subButtonText, hasNew && styles.subButtonTextActive]}>
|
|
||||||
★ {hasNew ? newTotal : ''}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -429,28 +412,7 @@ function SceneRow({ scene }: { scene: SceneOut }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: 16, paddingTop: 16 },
|
container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: SCREEN_PAD_H, paddingTop: SCREEN_PAD_TOP },
|
||||||
toolbar: { flexDirection: 'row', gap: 12, marginBottom: 12 },
|
|
||||||
search: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: theme.card,
|
|
||||||
borderColor: theme.borderStrong,
|
|
||||||
borderWidth: 1.5,
|
|
||||||
borderRadius: 12,
|
|
||||||
color: theme.fg,
|
|
||||||
padding: 12,
|
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
toolbarButton: {
|
|
||||||
backgroundColor: theme.card,
|
|
||||||
borderColor: theme.border,
|
|
||||||
borderWidth: 1.5,
|
|
||||||
borderRadius: 12,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
toolbarButtonActive: { borderColor: theme.accentText, backgroundColor: `${theme.accent}1A` },
|
|
||||||
toolbarButtonText: { color: theme.fg, fontWeight: '700' },
|
|
||||||
savedRow: { flexGrow: 0, marginBottom: 12 },
|
savedRow: { flexGrow: 0, marginBottom: 12 },
|
||||||
savedRowContent: { gap: 8, paddingRight: 12 },
|
savedRowContent: { gap: 8, paddingRight: 12 },
|
||||||
savedChip: {
|
savedChip: {
|
||||||
|
|
@ -466,14 +428,6 @@ const styles = StyleSheet.create({
|
||||||
savedChipText: { color: theme.fg, fontSize: 13 },
|
savedChipText: { color: theme.fg, fontSize: 13 },
|
||||||
savedChipTextActive: { color: theme.accentText, fontWeight: '600' },
|
savedChipTextActive: { color: theme.accentText, fontWeight: '600' },
|
||||||
subToolbar: { flexDirection: 'row', gap: 8, marginBottom: 16 },
|
subToolbar: { flexDirection: 'row', gap: 8, marginBottom: 16 },
|
||||||
subButton: {
|
|
||||||
backgroundColor: theme.card,
|
|
||||||
borderColor: theme.border,
|
|
||||||
borderWidth: 1.5,
|
|
||||||
borderRadius: 12,
|
|
||||||
paddingHorizontal: 14,
|
|
||||||
paddingVertical: 8,
|
|
||||||
},
|
|
||||||
subButtonActive: {
|
subButtonActive: {
|
||||||
backgroundColor: theme.accent,
|
backgroundColor: theme.accent,
|
||||||
borderColor: theme.accentText,
|
borderColor: theme.accentText,
|
||||||
|
|
@ -483,8 +437,6 @@ const styles = StyleSheet.create({
|
||||||
shadowRadius: 6,
|
shadowRadius: 6,
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
},
|
},
|
||||||
subButtonText: { color: theme.accentSecondary, fontSize: 13, fontWeight: '700' },
|
|
||||||
subButtonTextActive: { color: theme.fg },
|
|
||||||
row: {
|
row: {
|
||||||
backgroundColor: theme.card,
|
backgroundColor: theme.card,
|
||||||
borderColor: theme.border,
|
borderColor: theme.border,
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,12 @@ import {
|
||||||
ScrollView,
|
ScrollView,
|
||||||
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 { PillButton, SCREEN_PAD_H, SCREEN_PAD_TOP, SearchInput, Toolbar } from '../ui';
|
||||||
import type { SourceOut, SourceRating } from '../types';
|
import type { SourceOut, SourceRating } from '../types';
|
||||||
|
|
||||||
type Order = 'popular' | 'recent';
|
type Order = 'popular' | 'recent';
|
||||||
|
|
@ -79,30 +79,13 @@ export function SitesScreen() {
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.hint}>tap a tube → newest scenes · tap the ★ → rating breakdown</Text>
|
<Text style={styles.hint}>tap a tube → newest scenes · tap the ★ → rating breakdown</Text>
|
||||||
|
|
||||||
<View style={styles.toolbar}>
|
<Toolbar>
|
||||||
<TextInput
|
<SearchInput value={q} onChangeText={setQ} placeholder="search site…" />
|
||||||
style={[styles.search, searchFocused && styles.searchFocused]}
|
</Toolbar>
|
||||||
value={q}
|
|
||||||
onChangeText={setQ}
|
|
||||||
onFocus={() => setSearchFocused(true)}
|
|
||||||
onBlur={() => setSearchFocused(false)}
|
|
||||||
placeholder="search site…"
|
|
||||||
placeholderTextColor={theme.mutedDim}
|
|
||||||
autoCapitalize="none"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.segment}>
|
<View style={styles.segment}>
|
||||||
<SegButton
|
<PillButton label="Top" compact active={order === 'popular'} onPress={() => setOrder('popular')} />
|
||||||
active={order === 'popular'}
|
<PillButton label="Recent" compact active={order === 'recent'} onPress={() => setOrder('recent')} />
|
||||||
onPress={() => setOrder('popular')}
|
|
||||||
label="Top"
|
|
||||||
/>
|
|
||||||
<SegButton
|
|
||||||
active={order === 'recent'}
|
|
||||||
onPress={() => setOrder('recent')}
|
|
||||||
label="Recent"
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{isLoading && <ActivityIndicator color={theme.fg} style={{ marginTop: 24 }} />}
|
{isLoading && <ActivityIndicator color={theme.fg} style={{ marginTop: 24 }} />}
|
||||||
|
|
@ -138,27 +121,6 @@ export function SitesScreen() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lista Sites pokazuje display_name = domena (hqporner.com). User-report 18105d14:
|
// Lista Sites pokazuje display_name = domena (hqporner.com). User-report 18105d14:
|
||||||
// pozbyć się suffixów com/org/itp. Strip końcowego TLD → czysta nazwa.
|
// pozbyć się suffixów com/org/itp. Strip końcowego TLD → czysta nazwa.
|
||||||
function prettySiteName(name: string): string {
|
function prettySiteName(name: string): string {
|
||||||
|
|
@ -303,7 +265,7 @@ function RatingModal({ source, onClose }: { source: SourceOut | null; onClose: (
|
||||||
}
|
}
|
||||||
|
|
||||||
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: SCREEN_PAD_H, paddingTop: SCREEN_PAD_TOP },
|
||||||
|
|
||||||
headerRow: {
|
headerRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
@ -320,41 +282,8 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
headerCount: { color: theme.fg, fontSize: 22, fontWeight: '800' },
|
headerCount: { color: theme.fg, fontSize: 22, fontWeight: '800' },
|
||||||
hint: { color: theme.mutedDim, fontSize: 11, marginBottom: 12 },
|
hint: { color: theme.mutedDim, fontSize: 11, marginBottom: 12 },
|
||||||
|
segment: { flexDirection: 'row', gap: 8, 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: {
|
||||||
|
|
|
||||||
127
mobile/src/ui.tsx
Normal file
127
mobile/src/ui.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
toolbar: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 12 },
|
||||||
|
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 },
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue