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 },
+});