From 17e4ecaa8aed90adc5d4829b9342e0ecc5cb172d Mon Sep 17 00:00:00 2001 From: goon-foss Date: Tue, 18 Aug 2026 11:42:32 +0200 Subject: [PATCH] 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 --- mobile/src/changelog.ts | 7 ++ mobile/src/screens/MoviesScreen.tsx | 53 +++--------- mobile/src/screens/ScenesScreen.tsx | 106 +++++++---------------- mobile/src/screens/SitesScreen.tsx | 87 ++----------------- mobile/src/ui.tsx | 127 ++++++++++++++++++++++++++++ 5 files changed, 181 insertions(+), 199 deletions(-) create mode 100644 mobile/src/ui.tsx diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts index 846c05c..2af1beb 100644 --- a/mobile/src/changelog.ts +++ b/mobile/src/changelog.ts @@ -16,6 +16,13 @@ export type 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', date: 'August 2026', diff --git a/mobile/src/screens/MoviesScreen.tsx b/mobile/src/screens/MoviesScreen.tsx index ead9430..043830c 100644 --- a/mobile/src/screens/MoviesScreen.tsx +++ b/mobile/src/screens/MoviesScreen.tsx @@ -11,7 +11,6 @@ import { Pressable, StyleSheet, Text, - TextInput, View, } from 'react-native'; import { useClient } from '../ClientContext'; @@ -25,6 +24,7 @@ import { MoviePosterCard } from '../components/MoviePosterCard'; import { getLastSeen, markSeen } from '../lib/seenStore'; import type { RootStackParamList } from '../navigation'; import { theme } from '../theme'; +import { PillButton, SCREEN_PAD_H, SCREEN_PAD_TOP, SearchInput, Toolbar } from '../ui'; const PER_PAGE = 30; // 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 ( - - + - setFiltersOpen(true)} - > - Filter{!isDefaultFilters(filters) ? '·' : ''} - - + /> + 0 ? {total} films : null } ListEmptyComponent={!isLoading ? no movies : null} - contentContainerStyle={{ paddingBottom: 24, paddingHorizontal: 6 }} + contentContainerStyle={{ paddingBottom: 24 }} columnWrapperStyle={{ gap: 8 }} onEndReached={() => { if (hasNextPage && !isFetchingNextPage) fetchNextPage(); @@ -167,40 +163,11 @@ export function MoviesScreen() { } const styles = StyleSheet.create({ - container: { flex: 1, backgroundColor: theme.bg }, - 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', - }, + container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: SCREEN_PAD_H, paddingTop: SCREEN_PAD_TOP }, filterBtnActive: { backgroundColor: theme.accentDeep, 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 }, error: { color: theme.bad, padding: 12 }, muted: { color: theme.muted, textAlign: 'center', marginTop: 24 }, diff --git a/mobile/src/screens/ScenesScreen.tsx b/mobile/src/screens/ScenesScreen.tsx index 1953963..2dc1364 100644 --- a/mobile/src/screens/ScenesScreen.tsx +++ b/mobile/src/screens/ScenesScreen.tsx @@ -10,12 +10,12 @@ import { ScrollView, StyleSheet, Text, - TextInput, View, } from 'react-native'; import { Image } from 'expo-image'; import * as Haptics from 'expo-haptics'; import { MigrationBanner } from '../components/MigrationBanner'; +import { PillButton, SCREEN_PAD_H, SCREEN_PAD_TOP, SearchInput, Toolbar } from '../ui'; import { SceneTile, sceneGridProps } from '../components/SceneTile'; import { SceneGridSkeleton } from '../components/SceneGridSkeleton'; import { Thumb } from '../components/Thumb'; @@ -151,40 +151,24 @@ export function ScenesScreen({ return ( - - + + {q.trim().length > 0 ? ( - { const v = q.trim(); if (v && !alreadySaved) addSaved.mutate(v); }} - disabled={alreadySaved} - style={[styles.toolbarButton, alreadySaved && styles.toolbarButtonActive]} - > - - {alreadySaved ? '★ Saved' : '☆ Save'} - - + /> ) : null} - 0 ? ` ${activeCount}` : ''}`} + active={activeCount > 0} onPress={() => setFilterOpen(true)} - style={[styles.toolbarButton, activeCount > 0 && styles.toolbarButtonActive]} - > - 0 && { color: theme.accentText }]}> - Filter{activeCount > 0 ? ` ${activeCount}` : ''} - - - + /> + {saved.length > 0 ? ( {hasCast ? ( - navigation.navigate('Performers', verticalKey ? { vertical: verticalKey } : undefined) } - > - Performers - + /> ) : null} - navigation.navigate('Tags', verticalKey ? { vertical: verticalKey } : undefined)} - > - Tags - + /> @@ -352,14 +336,13 @@ function FavoritesButton() { const hasNew = newTotal > 0; return ( - navigation.navigate('Favorites')} - > - - ★ {hasNew ? newTotal : ''} - - + /> ); } @@ -429,28 +412,7 @@ function SceneRow({ scene }: { scene: SceneOut }) { } const styles = StyleSheet.create({ - container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: 16, paddingTop: 16 }, - 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' }, + container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: SCREEN_PAD_H, paddingTop: SCREEN_PAD_TOP }, savedRow: { flexGrow: 0, marginBottom: 12 }, savedRowContent: { gap: 8, paddingRight: 12 }, savedChip: { @@ -466,14 +428,6 @@ const styles = StyleSheet.create({ savedChipText: { color: theme.fg, fontSize: 13 }, savedChipTextActive: { color: theme.accentText, fontWeight: '600' }, subToolbar: { flexDirection: 'row', gap: 8, marginBottom: 16 }, - subButton: { - backgroundColor: theme.card, - borderColor: theme.border, - borderWidth: 1.5, - borderRadius: 12, - paddingHorizontal: 14, - paddingVertical: 8, - }, subButtonActive: { backgroundColor: theme.accent, borderColor: theme.accentText, @@ -483,8 +437,6 @@ const styles = StyleSheet.create({ shadowRadius: 6, elevation: 3, }, - subButtonText: { color: theme.accentSecondary, fontSize: 13, fontWeight: '700' }, - subButtonTextActive: { color: theme.fg }, row: { backgroundColor: theme.card, borderColor: theme.border, diff --git a/mobile/src/screens/SitesScreen.tsx b/mobile/src/screens/SitesScreen.tsx index 17dc05b..ec7ff61 100644 --- a/mobile/src/screens/SitesScreen.tsx +++ b/mobile/src/screens/SitesScreen.tsx @@ -16,12 +16,12 @@ import { ScrollView, StyleSheet, Text, - TextInput, View, } from 'react-native'; import { useClient } from '../ClientContext'; import type { RootStackParamList } from '../navigation'; import { theme } from '../theme'; +import { PillButton, SCREEN_PAD_H, SCREEN_PAD_TOP, SearchInput, Toolbar } from '../ui'; import type { SourceOut, SourceRating } from '../types'; type Order = 'popular' | 'recent'; @@ -79,30 +79,13 @@ export function SitesScreen() { tap a tube → newest scenes · tap the ★ → rating breakdown - - setSearchFocused(true)} - onBlur={() => setSearchFocused(false)} - placeholder="search site…" - placeholderTextColor={theme.mutedDim} - autoCapitalize="none" - /> - + + + - setOrder('popular')} - label="Top" - /> - setOrder('recent')} - label="Recent" - /> + setOrder('popular')} /> + setOrder('recent')} /> {isLoading && } @@ -138,27 +121,6 @@ export function SitesScreen() { ); } -function SegButton({ - active, - onPress, - label, -}: { - active: boolean; - onPress: () => void; - label: string; -}) { - return ( - - - {label} - - - ); -} - // Lista Sites pokazuje display_name = domena (hqporner.com). User-report 18105d14: // pozbyć się suffixów com/org/itp. Strip końcowego TLD → czysta nazwa. function prettySiteName(name: string): string { @@ -303,7 +265,7 @@ function RatingModal({ source, onClose }: { source: SourceOut | null; onClose: ( } 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: { flexDirection: 'row', @@ -320,41 +282,8 @@ const styles = StyleSheet.create({ }, headerCount: { color: theme.fg, fontSize: 22, fontWeight: '800' }, 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 }, chip: { diff --git a/mobile/src/ui.tsx b/mobile/src/ui.tsx new file mode 100644 index 0000000..e22f477 --- /dev/null +++ b/mobile/src/ui.tsx @@ -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 ( + { + 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; +}) { + const textColor = active ? theme.accentText : tone === 'secondary' ? theme.accentSecondary : theme.fg; + return ( + + {label} + + ); +} + +/** Wiersz narzędzi: pole + przyciski, jednakowy odstęp i margines na każdym ekranie. */ +export function Toolbar({ children }: { children: React.ReactNode }) { + return {children}; +} + +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 }, +});