feat(mobile): pasek chowa sie przy przewijaniu w dol (hystereza po drodze), FAB jedzie z nim, podpowiedz Sites do pierwszego przypiecia

- useAutoHideOnScroll: licznik drogi w jednym kierunku (48 px w dol chowa, 24 w gore
  pokazuje), strefy zawsze-widoczny przy gorze i dole listy, overscroll ignorowany
- HomeScreen useFocusEffect pokazuje pasek po powrocie z ekranu szczegolow
- changelog 2026-09-07e/f, OTA 40931479 i 8604dadb na 1.3
This commit is contained in:
goon-foss 2026-09-07 11:53:31 +02:00
parent d9fbc08218
commit 8c75b12066
6 changed files with 60 additions and 14 deletions

View file

@ -29,6 +29,21 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
{
id: '2026-09-07f',
date: 'September 2026',
items: [
'The bottom bar is back on screen whenever you return from a scene, movie or performer, even if it was tucked away before you left.',
],
},
{
id: '2026-09-07e',
date: 'September 2026',
items: [
'The bottom bar slides away while you scroll down and comes back as soon as you scroll up, so the grid gets the whole screen.',
'Sites: the pinning hint disappears once you have pinned something.',
],
},
{
id: '2026-09-07d',
date: 'September 2026',

View file

@ -17,6 +17,7 @@ import React, { useCallback, useState } from 'react';
import {
ActivityIndicator,
Alert,
Animated,
Image,
KeyboardAvoidingView,
Modal,
@ -35,7 +36,7 @@ import { captureScreen } from 'react-native-view-shot';
import { GoonClient } from '../api';
import { theme } from '../theme';
import { BOTTOM_BAR_GAP, BOTTOM_BAR_HEIGHT } from '../lib/bottomBar';
import { BOTTOM_BAR_GAP, BOTTOM_BAR_HEIGHT, barHidden } from '../lib/bottomBar';
import { formatLastPlayback } from '../lib/lastPlayback';
import { getSxyprnDiag } from '../lib/sxyprnResolver';
@ -123,6 +124,9 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
const insets = useSafeAreaInsets();
const liftAboveTabs = currentRoute === null || currentRoute === 'Scenes';
const fabBottom = 24 + (liftAboveTabs ? insets.bottom + BOTTOM_BAR_GAP + BOTTOM_BAR_HEIGHT : 0);
// Gdy pasek chowa sie na scroll, FAB zjezdza o tyle samo (ta sama wartosc Animated).
const fabShift = liftAboveTabs ? insets.bottom + BOTTOM_BAR_GAP + BOTTOM_BAR_HEIGHT + 6 : 0;
const fabTranslate = barHidden.interpolate({ inputRange: [0, 1], outputRange: [0, fabShift] });
const onPress = useCallback(async () => {
let captured: string | null = null;
@ -244,14 +248,18 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
return (
<>
{!hidden ? (
<TouchableOpacity style={[styles.fab, { bottom: fabBottom }]} onPress={onPress} activeOpacity={0.7}>
<Text style={styles.fabIcon}>?</Text>
{unseen > 0 ? (
<View style={styles.fabDot}>
<Text style={styles.fabDotText}>{unseen > 9 ? '9+' : unseen}</Text>
</View>
) : null}
</TouchableOpacity>
<Animated.View
style={[styles.fabWrap, { bottom: fabBottom, transform: [{ translateY: fabTranslate }] }]}
>
<TouchableOpacity style={styles.fab} onPress={onPress} activeOpacity={0.7}>
<Text style={styles.fabIcon}>?</Text>
{unseen > 0 ? (
<View style={styles.fabDot}>
<Text style={styles.fabDotText}>{unseen > 9 ? '9+' : unseen}</Text>
</View>
) : null}
</TouchableOpacity>
</Animated.View>
) : null}
<Modal
@ -381,10 +389,12 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
}
const styles = StyleSheet.create({
fab: {
fabWrap: {
position: 'absolute',
right: 16,
bottom: 24,
},
fab: {
width: 44,
height: 44,
borderRadius: 22,

View file

@ -1,8 +1,10 @@
import {
DefaultTheme,
NavigationContainer,
useFocusEffect,
useNavigationContainerRef,
} from '@react-navigation/native';
import { setBarHidden } from './lib/bottomBar';
import { BugReportFAB } from './components/BugReportFAB';
import { BottomTabsBar } from './components/BottomTabsBar';
import { GoonClient } from './api';
@ -140,6 +142,14 @@ function HomeScreen({ navigation }: { navigation: { setParams: (p: { tab?: strin
navigation.setParams({ tab });
}, [tab, navigation]);
// Powrot z ekranu sceny/filmu/performera: pasek ma byc widoczny, nawet jesli lista
// zostala przewinieta i pasek schowal sie przed wyjsciem (test 2026-09-07).
useFocusEffect(
React.useCallback(() => {
setBarHidden(false);
}, []),
);
return (
<View style={{ flex: 1, paddingTop: insets.top, backgroundColor: theme.bg }}>
{/* Gornej belki nie ma (2026-09-07): nazwe sekcji mowi pasek na dole, kolo zebate

View file

@ -20,7 +20,7 @@ import {
MovieFilters,
MovieFiltersSheet,
} from '../components/MovieFiltersSheet';
import { useBottomBarInset, useScrollTopListener } from '../lib/bottomBar';
import { useAutoHideOnScroll, useBottomBarInset, useScrollTopListener } from '../lib/bottomBar';
import { usePersistedState } from '../lib/persistedState';
import { MoviePosterCard } from '../components/MoviePosterCard';
import type { MovieOut } from '../types';
@ -51,6 +51,7 @@ export function MoviesScreen() {
React.useCallback(() => listRef.current?.scrollToOffset({ offset: 0, animated: true }), []),
);
const bottomInset = useBottomBarInset();
const onListScroll = useAutoHideOnScroll();
// seenSince: ISO timestamp ostatniego "kompletnie obejrzanej listy". Movies
// z created_at > seenSince mają NEW badge. Ładujemy z SecureStore raz
// przy mount; aktualizujemy gdy app idzie do tła (user widzi NEW przez
@ -152,6 +153,8 @@ export function MoviesScreen() {
ref={listRef}
data={items}
numColumns={NUM_COLS}
onScroll={onListScroll}
scrollEventThrottle={16}
keyExtractor={(m) => m.id}
// Android removeClippedSubviews=true blankuje plakaty po scrollu (expo-image
// nie re-renderuje odpiętych), ten sam bug co "znikające miniaturki" scen.

View file

@ -23,7 +23,7 @@ import {
SettingsButton,
Toolbar,
} from '../ui';
import { useBottomBarInset, useScrollTopListener } from '../lib/bottomBar';
import { useAutoHideOnScroll, useBottomBarInset, useScrollTopListener } from '../lib/bottomBar';
import { SceneTile, sceneGridProps } from '../components/SceneTile';
import { SceneGridSkeleton } from '../components/SceneGridSkeleton';
import { Thumb } from '../components/Thumb';
@ -75,6 +75,7 @@ export function ScenesScreen({
React.useCallback(() => listRef.current?.scrollToOffset({ offset: 0, animated: true }), []),
);
const bottomInset = useBottomBarInset();
const onListScroll = useAutoHideOnScroll();
React.useEffect(() => {
const t = setTimeout(() => setDebouncedQ(q), 350);
@ -273,6 +274,8 @@ export function ScenesScreen({
ref={listRef}
{...sceneGridProps(gridColumns)}
data={items}
onScroll={onListScroll}
scrollEventThrottle={16}
keyExtractor={(s) => s.id}
// removeClippedSubviews + okno renderowania (windowSize itd.) idą z sceneGridProps
// patrz SceneTile. Perf bug-report 5b7ca1e1 (jank/obciążenie przy długim scrollu).

View file

@ -23,7 +23,7 @@ import type { RootStackParamList } from '../navigation';
import { theme } from '../theme';
import { PillButton, SCREEN_PAD_H, SCREEN_PAD_TOP, SearchInput, SettingsButton, Toolbar } from '../ui';
import type { SourceOut, SourceRating } from '../types';
import { useBottomBarInset, useScrollTopListener } from '../lib/bottomBar';
import { useAutoHideOnScroll, useBottomBarInset, useScrollTopListener } from '../lib/bottomBar';
type Order = 'popular' | 'recent';
@ -42,6 +42,7 @@ export function SitesScreen() {
React.useCallback(() => listRef.current?.scrollToOffset({ offset: 0, animated: true }), []),
);
const bottomInset = useBottomBarInset();
const onListScroll = useAutoHideOnScroll();
React.useEffect(() => {
const t = setTimeout(() => setDebouncedQ(q), 250);
@ -108,7 +109,9 @@ export function SitesScreen() {
<PillButton label="Top" compact active={order === 'popular'} onPress={() => setOrder('popular')} />
<PillButton label="Recent" compact active={order === 'recent'} onPress={() => setOrder('recent')} />
</View>
<Text style={styles.hint}>long-press a site to pin it to the top</Text>
{favQuery.data && pinned.size === 0 ? (
<Text style={styles.hint}>long-press a site to pin it to the top</Text>
) : null}
{isLoading && <ActivityIndicator color={theme.fg} style={{ marginTop: 24 }} />}
{error instanceof Error && <Text style={styles.error}>{error.message}</Text>}
@ -117,6 +120,8 @@ export function SitesScreen() {
ref={listRef}
data={items}
keyExtractor={(s) => s.origin}
onScroll={onListScroll}
scrollEventThrottle={16}
numColumns={2}
columnWrapperStyle={styles.gridRow}
renderItem={({ item }) => (