feat(mobile): naglowki ekranow podrzednych jak sekcje glowne, ikony ulubione/ukryj, pasek dolny wyzszy z pierscieniem centrowanym przez layout

- screenOptions: tlo ekranu, bez cienia, tytul 17 display, tint accentText
- HeaderIconButton (star/star-outline, eye-off-outline) na 5 ekranach zamiast ★/🚫
- BottomTabsBar: 76 dp, pierscien statyczny w iconBox w spoczynku, kapsula tylko w
  trakcie przejscia, srodki zaokraglone do piksela
- changelog 2026-09-08b/c, OTA 1cbb9181 i 18ba425a na 1.3
This commit is contained in:
goon-foss 2026-09-08 10:09:34 +02:00
parent 385aeb3ddb
commit 320d928927
10 changed files with 136 additions and 51 deletions

View file

@ -29,6 +29,20 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
{
id: '2026-09-08c',
date: 'September 2026',
items: ['Bottom bar: a little more room under the labels.'],
},
{
id: '2026-09-08b',
date: 'September 2026',
items: [
'Scene, movie, performer, studio and site pages got the same look as the main sections: no lighter bar on top, same title style, and proper icons for favorite and hide instead of text symbols.',
'Bottom bar is a bit taller so the labels no longer sit on the edge, and the ring around the active icon is centred exactly.',
'Movie lists load about four times faster and the Sites tab opens instantly.',
],
},
{
id: '2026-09-08a',
date: 'September 2026',

View file

@ -23,6 +23,12 @@
* paddingu. Przy otwartej klawiaturze (adjustResize wypychal pasek nad klawiature)
* pasek znika w calosci; stukniecie w aktywna zakladke przewija liste na gore.
*
* Wyrownanie pierscienia (uwaga Jana 2026-09-08): w spoczynku pierscien NIE jest
* pozycjonowany absolutnie po zmierzonych x, tylko siedzi wewnatrz iconBox aktywnej
* zakladki i centruje go flexbox, wiec nie ma ulamkow piksela ani przesuniecia o
* obramowanie pigulki. Absolutna, animowana kapsula pokazuje sie wylacznie na czas
* przejscia miedzy zakladkami.
*
* Pozycje ikon mierzymy `onLayout` (flex:1 daje rowne komorki, ale szerokosc paska
* zalezy od ekranu). Do pierwszego pomiaru pierscienia nie rysujemy.
*/
@ -61,7 +67,7 @@ interface Props<T extends string> {
/** Promien pierscienia: kolo 40 px wokol ikony 22 px. */
const RING_R = 20;
const RING_TOP = 6;
const RING_TOP = 8;
const ICON_SIZE = 22;
/** O ile poswiata wystaje poza pierscien, z kazdej strony. */
const GLOW_PAD = 3;
@ -89,6 +95,7 @@ export function BottomTabsBar<T extends string>({ tabs, icons, current, onSelect
// Skad i dokad jedzie pierscien. `from` to poprzednia aktywna, do pierwszego
// przelaczenia from == to i pierscien stoi.
const [trip, setTrip] = React.useState({ from: activeIndex, to: activeIndex });
const [animating, setAnimating] = React.useState(false);
const lead = React.useRef(new Animated.Value(1)).current;
const trail = React.useRef(new Animated.Value(1)).current;
@ -103,6 +110,7 @@ export function BottomTabsBar<T extends string>({ tabs, icons, current, onSelect
if (trip.from === trip.to) return;
lead.setValue(0);
trail.setValue(0);
setAnimating(true);
Animated.parallel([
Animated.timing(lead, {
toValue: 1,
@ -117,12 +125,12 @@ export function BottomTabsBar<T extends string>({ tabs, icons, current, onSelect
easing: Easing.out(Easing.cubic),
useNativeDriver: false,
}),
]).start();
]).start(() => setAnimating(false));
}, [trip, lead, trail]);
const onTabLayout = (i: number) => (e: LayoutChangeEvent) => {
const { x, width } = e.nativeEvent.layout;
const cx = x + width / 2;
const cx = Math.round(x + width / 2);
setCenters((p) => (p[i] === cx ? p : { ...p, [i]: cx }));
};
@ -157,7 +165,7 @@ export function BottomTabsBar<T extends string>({ tabs, icons, current, onSelect
]}
>
<View style={styles.pill}>
{ring ? (
{ring && animating ? (
<>
<Animated.View style={[styles.glow, { left: ring.glowLeft, width: ring.glowWidth }]} />
<Animated.View style={[styles.ring, { left: ring.left, width: ring.width }]} />
@ -185,6 +193,12 @@ export function BottomTabsBar<T extends string>({ tabs, icons, current, onSelect
}}
>
<View style={styles.iconBox}>
{active && !animating ? (
<>
<View style={styles.glowStatic} />
<View style={styles.ringStatic} />
</>
) : null}
<MaterialCommunityIcons
name={icons[t]}
size={ICON_SIZE}
@ -236,13 +250,34 @@ const styles = StyleSheet.create({
justifyContent: 'center',
},
label: {
marginTop: 2,
marginTop: 4,
color: theme.muted,
fontSize: 11,
fontFamily: fonts.medium,
letterSpacing: 0.3,
},
labelActive: { color: theme.accentText, fontFamily: fonts.display },
ringStatic: {
position: 'absolute',
left: 0,
top: 0,
width: RING_R * 2,
height: RING_R * 2,
borderRadius: RING_R,
borderWidth: 1.5,
borderColor: theme.accentText,
},
glowStatic: {
position: 'absolute',
left: -GLOW_PAD,
top: -GLOW_PAD,
width: RING_R * 2 + GLOW_PAD * 2,
height: RING_R * 2 + GLOW_PAD * 2,
borderRadius: RING_R + GLOW_PAD,
borderWidth: 4,
borderColor: theme.accentText,
opacity: 0.16,
},
ring: {
position: 'absolute',
top: RING_TOP,

View file

@ -18,7 +18,9 @@ import { Animated, Easing, NativeScrollEvent, NativeSyntheticEvent } from 'react
import { useSafeAreaInsets } from 'react-native-safe-area-context';
/** Wysokosc samej pigulki, bez inset systemowego paska. */
export const BOTTOM_BAR_HEIGHT = 62;
// 8 (gora) + 40 (ikona) + 4 + ~14 (podpis) = 66, reszta to oddech pod podpisem. Przy 70
// podpis konczyl sie 3 dp nad krawedzia (pomiar na emulatorze 2026-09-08).
export const BOTTOM_BAR_HEIGHT = 76;
/** Odstep pigulki od dolu (nad inset). Eksport, zeby FAB zglaszania mogl wisiec wyzej. */
export const BOTTOM_BAR_GAP = 0;

View file

@ -213,9 +213,13 @@ export function AppNavigator({ onLogout, client, appVersion }: AppNavigatorProps
<View style={{ flex: 1 }}>
<Stack.Navigator
screenOptions={{
headerStyle: { backgroundColor: theme.card },
headerTitleStyle: { color: theme.fg, fontFamily: fonts.display },
headerTintColor: theme.accent,
// Ekrany podrzedne maja wygladac jak sekcje glowne (2026-09-08): tlo ekranu
// zamiast jasniejszej belki, bez cienia, tytul tym samym krojem i rozmiarem
// co naglowki sekcji, strzalka i ikony po prawej w accentText.
headerStyle: { backgroundColor: theme.bg },
headerShadowVisible: false,
headerTitleStyle: { color: theme.fg, fontFamily: fonts.display, fontSize: 17 },
headerTintColor: theme.accentText,
contentStyle: { backgroundColor: theme.bg },
}}
>

View file

@ -17,6 +17,7 @@ import {
import { useClient } from '../ClientContext';
import type { RootStackParamList } from '../navigation';
import { theme } from '../theme';
import { HeaderIconButton } from '../ui';
import type { PlaybackSource, StreamLink } from '../types';
import { resolveVoePage } from '../lib/voeResolver';
import { PlaybackQualityModal } from './PlaybackQualityModal';
@ -47,15 +48,13 @@ export function MovieDetailScreen() {
navigation.setOptions({
title: data?.title ?? 'Movie',
headerRight: () => (
<Pressable
<HeaderIconButton
icon={isFav ? 'star' : 'star-outline'}
active={isFav}
accessibilityLabel={isFav ? 'Remove from favorites' : 'Add to favorites'}
onPress={() => favMutation.mutate()}
hitSlop={12}
disabled={favMutation.isPending}
>
<Text style={{ color: isFav ? theme.accent : theme.muted, fontSize: 22 }}>
{isFav ? '★' : '☆'}
</Text>
</Pressable>
/>
),
});
}, [navigation, data?.title, isFav, favMutation]);

View file

@ -28,7 +28,7 @@ import { ErrorBoundary } from '../ErrorBoundary';
import type { RootStackParamList } from '../navigation';
import { theme } from '../theme';
import type { SceneOut } from '../types';
import { IconButton, Toolbar } from '../ui';
import { HeaderIconButton, IconButton, Toolbar } from '../ui';
type Tab = 'scenes' | 'movies';
@ -134,19 +134,15 @@ export function PerformerScenesScreen() {
navigation.setOptions({
title: name,
headerRight: () => (
<View style={{ flexDirection: 'row', gap: 16, alignItems: 'center' }}>
<Pressable onPress={onHide} hitSlop={10}>
<Text style={{ color: theme.mutedDim, fontSize: 18 }}>🚫</Text>
</Pressable>
<Pressable
<View style={{ flexDirection: 'row', gap: 4, alignItems: 'center' }}>
<HeaderIconButton icon="eye-off-outline" muted accessibilityLabel="Hide" onPress={onHide} />
<HeaderIconButton
icon={isFavorite ? 'star' : 'star-outline'}
active={isFavorite}
accessibilityLabel={isFavorite ? 'Remove from favorites' : 'Add to favorites'}
onPress={() => (isFavorite ? removeMutation.mutate() : addMutation.mutate())}
hitSlop={12}
disabled={addMutation.isPending || removeMutation.isPending}
>
<Text style={{ color: isFavorite ? theme.accent : theme.muted, fontSize: 22 }}>
{isFavorite ? '★' : '☆'}
</Text>
</Pressable>
/>
</View>
),
});

View file

@ -27,6 +27,7 @@ import { resolveEpornerPage } from '../lib/epornerResolver';
import { resolveFpoxxxPage } from '../lib/fpoxxxResolver';
import type { RootStackParamList } from '../navigation';
import { fonts, theme } from '../theme';
import { HeaderIconButton } from '../ui';
import type { PlaybackSource, SceneOut, StreamLink } from '../types';
import { PlaybackQualityModal, pickByDefaultQuality } from './PlaybackQualityModal';
import { usePreferences } from '../PreferencesContext';
@ -230,11 +231,13 @@ export function SceneDetailScreen() {
React.useLayoutEffect(() => {
nav.setOptions({
headerRight: () => (
<Pressable onPress={() => favMutation.mutate()} hitSlop={12} disabled={favMutation.isPending}>
<Text style={{ color: isFav ? theme.accent : theme.muted, fontSize: 22 }}>
{isFav ? '★' : '☆'}
</Text>
</Pressable>
<HeaderIconButton
icon={isFav ? 'star' : 'star-outline'}
active={isFav}
accessibilityLabel={isFav ? 'Remove from favorites' : 'Add to favorites'}
onPress={() => favMutation.mutate()}
disabled={favMutation.isPending}
/>
),
});
}, [nav, isFav, favMutation]);

View file

@ -10,6 +10,7 @@ import { useClient } from '../ClientContext';
import { MoviePosterCard } from '../components/MoviePosterCard';
import type { RootStackParamList } from '../navigation';
import { theme } from '../theme';
import { HeaderIconButton } from '../ui';
const PER_PAGE = 30;
// 3 kolumny, parytet z MoviesScreen (plakaty 2:3, 50% więcej filmów na ekran).
@ -44,15 +45,13 @@ export function StudioMoviesScreen() {
navigation.setOptions({
title: name,
headerRight: () => (
<Pressable
<HeaderIconButton
icon={isFavorite ? 'star' : 'star-outline'}
active={isFavorite}
accessibilityLabel={isFavorite ? 'Remove from favorites' : 'Add to favorites'}
onPress={() => (isFavorite ? removeMutation.mutate() : addMutation.mutate())}
hitSlop={12}
disabled={addMutation.isPending || removeMutation.isPending}
>
<Text style={{ color: isFavorite ? theme.accent : theme.muted, fontSize: 22 }}>
{isFavorite ? '★' : '☆'}
</Text>
</Pressable>
/>
),
});
// eslint-disable-next-line react-hooks/exhaustive-deps

View file

@ -30,6 +30,7 @@ import { useClient } from '../ClientContext';
import { usePreferences } from '../PreferencesContext';
import type { RootStackParamList } from '../navigation';
import { theme } from '../theme';
import { HeaderIconButton } from '../ui';
import type { SceneOut } from '../types';
type Tab = 'scenes' | 'movies';
@ -100,19 +101,15 @@ export function StudioScenesScreen() {
navigation.setOptions({
title: name,
headerRight: () => (
<View style={{ flexDirection: 'row', gap: 16, alignItems: 'center' }}>
<Pressable onPress={onHide} hitSlop={10}>
<Text style={{ color: theme.mutedDim, fontSize: 18 }}>🚫</Text>
</Pressable>
<Pressable
<View style={{ flexDirection: 'row', gap: 4, alignItems: 'center' }}>
<HeaderIconButton icon="eye-off-outline" muted accessibilityLabel="Hide" onPress={onHide} />
<HeaderIconButton
icon={isFavorite ? 'star' : 'star-outline'}
active={isFavorite}
accessibilityLabel={isFavorite ? 'Remove from favorites' : 'Add to favorites'}
onPress={() => (isFavorite ? removeMutation.mutate() : addMutation.mutate())}
hitSlop={12}
disabled={addMutation.isPending || removeMutation.isPending}
>
<Text style={{ color: isFavorite ? theme.accent : theme.muted, fontSize: 22 }}>
{isFavorite ? '★' : '☆'}
</Text>
</Pressable>
/>
</View>
),
});

View file

@ -146,6 +146,41 @@ export function SettingsButton() {
);
}
/**
* 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>;
@ -153,6 +188,7 @@ export function Toolbar({ children }: { children: React.ReactNode }) {
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,