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:
parent
385aeb3ddb
commit
320d928927
10 changed files with 136 additions and 51 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue