From 320d92892752cb3e35a4d7229e29217a48dc9d98 Mon Sep 17 00:00:00 2001 From: goon-foss Date: Tue, 8 Sep 2026 10:09:34 +0200 Subject: [PATCH] feat(mobile): naglowki ekranow podrzednych jak sekcje glowne, ikony ulubione/ukryj, pasek dolny wyzszy z pierscieniem centrowanym przez layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- mobile/src/changelog.ts | 14 ++++++ mobile/src/components/BottomTabsBar.tsx | 45 +++++++++++++++++--- mobile/src/lib/bottomBar.ts | 4 +- mobile/src/navigation.tsx | 10 +++-- mobile/src/screens/MovieDetailScreen.tsx | 13 +++--- mobile/src/screens/PerformerScenesScreen.tsx | 20 ++++----- mobile/src/screens/SceneDetailScreen.tsx | 13 +++--- mobile/src/screens/StudioMoviesScreen.tsx | 13 +++--- mobile/src/screens/StudioScenesScreen.tsx | 19 ++++----- mobile/src/ui.tsx | 36 ++++++++++++++++ 10 files changed, 136 insertions(+), 51 deletions(-) diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts index 978cde7..0729993 100644 --- a/mobile/src/changelog.ts +++ b/mobile/src/changelog.ts @@ -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', diff --git a/mobile/src/components/BottomTabsBar.tsx b/mobile/src/components/BottomTabsBar.tsx index 2ad29f3..1c3e156 100644 --- a/mobile/src/components/BottomTabsBar.tsx +++ b/mobile/src/components/BottomTabsBar.tsx @@ -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 { /** 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({ 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({ 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({ 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({ tabs, icons, current, onSelect ]} > - {ring ? ( + {ring && animating ? ( <> @@ -185,6 +193,12 @@ export function BottomTabsBar({ tabs, icons, current, onSelect }} > + {active && !animating ? ( + <> + + + + ) : null} diff --git a/mobile/src/screens/MovieDetailScreen.tsx b/mobile/src/screens/MovieDetailScreen.tsx index 68c0a44..fe0d966 100644 --- a/mobile/src/screens/MovieDetailScreen.tsx +++ b/mobile/src/screens/MovieDetailScreen.tsx @@ -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: () => ( - favMutation.mutate()} - hitSlop={12} disabled={favMutation.isPending} - > - - {isFav ? '★' : '☆'} - - + /> ), }); }, [navigation, data?.title, isFav, favMutation]); diff --git a/mobile/src/screens/PerformerScenesScreen.tsx b/mobile/src/screens/PerformerScenesScreen.tsx index a6c6906..ff8368a 100644 --- a/mobile/src/screens/PerformerScenesScreen.tsx +++ b/mobile/src/screens/PerformerScenesScreen.tsx @@ -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: () => ( - - - 🚫 - - + + (isFavorite ? removeMutation.mutate() : addMutation.mutate())} - hitSlop={12} disabled={addMutation.isPending || removeMutation.isPending} - > - - {isFavorite ? '★' : '☆'} - - + /> ), }); diff --git a/mobile/src/screens/SceneDetailScreen.tsx b/mobile/src/screens/SceneDetailScreen.tsx index 66750cb..d44624a 100644 --- a/mobile/src/screens/SceneDetailScreen.tsx +++ b/mobile/src/screens/SceneDetailScreen.tsx @@ -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: () => ( - favMutation.mutate()} hitSlop={12} disabled={favMutation.isPending}> - - {isFav ? '★' : '☆'} - - + favMutation.mutate()} + disabled={favMutation.isPending} + /> ), }); }, [nav, isFav, favMutation]); diff --git a/mobile/src/screens/StudioMoviesScreen.tsx b/mobile/src/screens/StudioMoviesScreen.tsx index 68bb6a2..4dc18f5 100644 --- a/mobile/src/screens/StudioMoviesScreen.tsx +++ b/mobile/src/screens/StudioMoviesScreen.tsx @@ -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: () => ( - (isFavorite ? removeMutation.mutate() : addMutation.mutate())} - hitSlop={12} disabled={addMutation.isPending || removeMutation.isPending} - > - - {isFavorite ? '★' : '☆'} - - + /> ), }); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/mobile/src/screens/StudioScenesScreen.tsx b/mobile/src/screens/StudioScenesScreen.tsx index 92e545d..465cd0e 100644 --- a/mobile/src/screens/StudioScenesScreen.tsx +++ b/mobile/src/screens/StudioScenesScreen.tsx @@ -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: () => ( - - - 🚫 - - + + (isFavorite ? removeMutation.mutate() : addMutation.mutate())} - hitSlop={12} disabled={addMutation.isPending || removeMutation.isPending} - > - - {isFavorite ? '★' : '☆'} - - + /> ), }); diff --git a/mobile/src/ui.tsx b/mobile/src/ui.tsx index 481519f..f942af0 100644 --- a/mobile/src/ui.tsx +++ b/mobile/src/ui.tsx @@ -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['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 ( + [styles.headerIcon, pressed && { opacity: 0.6 }]} + > + + + ); +} + /** Wiersz narzędzi: pole + przyciski, jednakowy odstęp i margines na każdym ekranie. */ export function Toolbar({ children }: { children: React.ReactNode }) { return {children}; @@ -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,