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:
parent
d9fbc08218
commit
8c75b12066
6 changed files with 60 additions and 14 deletions
|
|
@ -29,6 +29,21 @@ export type ChangelogEntry = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CHANGELOG: 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',
|
id: '2026-09-07d',
|
||||||
date: 'September 2026',
|
date: 'September 2026',
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import React, { useCallback, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Alert,
|
Alert,
|
||||||
|
Animated,
|
||||||
Image,
|
Image,
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
Modal,
|
Modal,
|
||||||
|
|
@ -35,7 +36,7 @@ import { captureScreen } from 'react-native-view-shot';
|
||||||
|
|
||||||
import { GoonClient } from '../api';
|
import { GoonClient } from '../api';
|
||||||
import { theme } from '../theme';
|
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 { formatLastPlayback } from '../lib/lastPlayback';
|
||||||
import { getSxyprnDiag } from '../lib/sxyprnResolver';
|
import { getSxyprnDiag } from '../lib/sxyprnResolver';
|
||||||
|
|
||||||
|
|
@ -123,6 +124,9 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const liftAboveTabs = currentRoute === null || currentRoute === 'Scenes';
|
const liftAboveTabs = currentRoute === null || currentRoute === 'Scenes';
|
||||||
const fabBottom = 24 + (liftAboveTabs ? insets.bottom + BOTTOM_BAR_GAP + BOTTOM_BAR_HEIGHT : 0);
|
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 () => {
|
const onPress = useCallback(async () => {
|
||||||
let captured: string | null = null;
|
let captured: string | null = null;
|
||||||
|
|
@ -244,14 +248,18 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!hidden ? (
|
{!hidden ? (
|
||||||
<TouchableOpacity style={[styles.fab, { bottom: fabBottom }]} onPress={onPress} activeOpacity={0.7}>
|
<Animated.View
|
||||||
<Text style={styles.fabIcon}>?</Text>
|
style={[styles.fabWrap, { bottom: fabBottom, transform: [{ translateY: fabTranslate }] }]}
|
||||||
{unseen > 0 ? (
|
>
|
||||||
<View style={styles.fabDot}>
|
<TouchableOpacity style={styles.fab} onPress={onPress} activeOpacity={0.7}>
|
||||||
<Text style={styles.fabDotText}>{unseen > 9 ? '9+' : unseen}</Text>
|
<Text style={styles.fabIcon}>?</Text>
|
||||||
</View>
|
{unseen > 0 ? (
|
||||||
) : null}
|
<View style={styles.fabDot}>
|
||||||
</TouchableOpacity>
|
<Text style={styles.fabDotText}>{unseen > 9 ? '9+' : unseen}</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Animated.View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|
@ -381,10 +389,12 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
fab: {
|
fabWrap: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
right: 16,
|
right: 16,
|
||||||
bottom: 24,
|
bottom: 24,
|
||||||
|
},
|
||||||
|
fab: {
|
||||||
width: 44,
|
width: 44,
|
||||||
height: 44,
|
height: 44,
|
||||||
borderRadius: 22,
|
borderRadius: 22,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import {
|
import {
|
||||||
DefaultTheme,
|
DefaultTheme,
|
||||||
NavigationContainer,
|
NavigationContainer,
|
||||||
|
useFocusEffect,
|
||||||
useNavigationContainerRef,
|
useNavigationContainerRef,
|
||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native';
|
||||||
|
import { setBarHidden } from './lib/bottomBar';
|
||||||
import { BugReportFAB } from './components/BugReportFAB';
|
import { BugReportFAB } from './components/BugReportFAB';
|
||||||
import { BottomTabsBar } from './components/BottomTabsBar';
|
import { BottomTabsBar } from './components/BottomTabsBar';
|
||||||
import { GoonClient } from './api';
|
import { GoonClient } from './api';
|
||||||
|
|
@ -140,6 +142,14 @@ function HomeScreen({ navigation }: { navigation: { setParams: (p: { tab?: strin
|
||||||
navigation.setParams({ tab });
|
navigation.setParams({ tab });
|
||||||
}, [tab, navigation]);
|
}, [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 (
|
return (
|
||||||
<View style={{ flex: 1, paddingTop: insets.top, backgroundColor: theme.bg }}>
|
<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
|
{/* Gornej belki nie ma (2026-09-07): nazwe sekcji mowi pasek na dole, kolo zebate
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import {
|
||||||
MovieFilters,
|
MovieFilters,
|
||||||
MovieFiltersSheet,
|
MovieFiltersSheet,
|
||||||
} from '../components/MovieFiltersSheet';
|
} from '../components/MovieFiltersSheet';
|
||||||
import { useBottomBarInset, useScrollTopListener } from '../lib/bottomBar';
|
import { useAutoHideOnScroll, useBottomBarInset, useScrollTopListener } from '../lib/bottomBar';
|
||||||
import { usePersistedState } from '../lib/persistedState';
|
import { usePersistedState } from '../lib/persistedState';
|
||||||
import { MoviePosterCard } from '../components/MoviePosterCard';
|
import { MoviePosterCard } from '../components/MoviePosterCard';
|
||||||
import type { MovieOut } from '../types';
|
import type { MovieOut } from '../types';
|
||||||
|
|
@ -51,6 +51,7 @@ export function MoviesScreen() {
|
||||||
React.useCallback(() => listRef.current?.scrollToOffset({ offset: 0, animated: true }), []),
|
React.useCallback(() => listRef.current?.scrollToOffset({ offset: 0, animated: true }), []),
|
||||||
);
|
);
|
||||||
const bottomInset = useBottomBarInset();
|
const bottomInset = useBottomBarInset();
|
||||||
|
const onListScroll = useAutoHideOnScroll();
|
||||||
// seenSince: ISO timestamp ostatniego "kompletnie obejrzanej listy". Movies
|
// seenSince: ISO timestamp ostatniego "kompletnie obejrzanej listy". Movies
|
||||||
// z created_at > seenSince mają NEW badge. Ładujemy z SecureStore raz
|
// z created_at > seenSince mają NEW badge. Ładujemy z SecureStore raz
|
||||||
// przy mount; aktualizujemy gdy app idzie do tła (user widzi NEW przez
|
// przy mount; aktualizujemy gdy app idzie do tła (user widzi NEW przez
|
||||||
|
|
@ -152,6 +153,8 @@ export function MoviesScreen() {
|
||||||
ref={listRef}
|
ref={listRef}
|
||||||
data={items}
|
data={items}
|
||||||
numColumns={NUM_COLS}
|
numColumns={NUM_COLS}
|
||||||
|
onScroll={onListScroll}
|
||||||
|
scrollEventThrottle={16}
|
||||||
keyExtractor={(m) => m.id}
|
keyExtractor={(m) => m.id}
|
||||||
// Android removeClippedSubviews=true blankuje plakaty po scrollu (expo-image
|
// Android removeClippedSubviews=true blankuje plakaty po scrollu (expo-image
|
||||||
// nie re-renderuje odpiętych), ten sam bug co "znikające miniaturki" scen.
|
// nie re-renderuje odpiętych), ten sam bug co "znikające miniaturki" scen.
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import {
|
||||||
SettingsButton,
|
SettingsButton,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
} from '../ui';
|
} from '../ui';
|
||||||
import { useBottomBarInset, useScrollTopListener } from '../lib/bottomBar';
|
import { useAutoHideOnScroll, useBottomBarInset, useScrollTopListener } from '../lib/bottomBar';
|
||||||
import { SceneTile, sceneGridProps } from '../components/SceneTile';
|
import { SceneTile, sceneGridProps } from '../components/SceneTile';
|
||||||
import { SceneGridSkeleton } from '../components/SceneGridSkeleton';
|
import { SceneGridSkeleton } from '../components/SceneGridSkeleton';
|
||||||
import { Thumb } from '../components/Thumb';
|
import { Thumb } from '../components/Thumb';
|
||||||
|
|
@ -75,6 +75,7 @@ export function ScenesScreen({
|
||||||
React.useCallback(() => listRef.current?.scrollToOffset({ offset: 0, animated: true }), []),
|
React.useCallback(() => listRef.current?.scrollToOffset({ offset: 0, animated: true }), []),
|
||||||
);
|
);
|
||||||
const bottomInset = useBottomBarInset();
|
const bottomInset = useBottomBarInset();
|
||||||
|
const onListScroll = useAutoHideOnScroll();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const t = setTimeout(() => setDebouncedQ(q), 350);
|
const t = setTimeout(() => setDebouncedQ(q), 350);
|
||||||
|
|
@ -273,6 +274,8 @@ export function ScenesScreen({
|
||||||
ref={listRef}
|
ref={listRef}
|
||||||
{...sceneGridProps(gridColumns)}
|
{...sceneGridProps(gridColumns)}
|
||||||
data={items}
|
data={items}
|
||||||
|
onScroll={onListScroll}
|
||||||
|
scrollEventThrottle={16}
|
||||||
keyExtractor={(s) => s.id}
|
keyExtractor={(s) => s.id}
|
||||||
// removeClippedSubviews + okno renderowania (windowSize itd.) idą z sceneGridProps
|
// removeClippedSubviews + okno renderowania (windowSize itd.) idą z sceneGridProps
|
||||||
// patrz SceneTile. Perf bug-report 5b7ca1e1 (jank/obciążenie przy długim scrollu).
|
// patrz SceneTile. Perf bug-report 5b7ca1e1 (jank/obciążenie przy długim scrollu).
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import type { RootStackParamList } from '../navigation';
|
||||||
import { theme } from '../theme';
|
import { theme } from '../theme';
|
||||||
import { PillButton, SCREEN_PAD_H, SCREEN_PAD_TOP, SearchInput, SettingsButton, Toolbar } from '../ui';
|
import { PillButton, SCREEN_PAD_H, SCREEN_PAD_TOP, SearchInput, SettingsButton, Toolbar } from '../ui';
|
||||||
import type { SourceOut, SourceRating } from '../types';
|
import type { SourceOut, SourceRating } from '../types';
|
||||||
import { useBottomBarInset, useScrollTopListener } from '../lib/bottomBar';
|
import { useAutoHideOnScroll, useBottomBarInset, useScrollTopListener } from '../lib/bottomBar';
|
||||||
|
|
||||||
type Order = 'popular' | 'recent';
|
type Order = 'popular' | 'recent';
|
||||||
|
|
||||||
|
|
@ -42,6 +42,7 @@ export function SitesScreen() {
|
||||||
React.useCallback(() => listRef.current?.scrollToOffset({ offset: 0, animated: true }), []),
|
React.useCallback(() => listRef.current?.scrollToOffset({ offset: 0, animated: true }), []),
|
||||||
);
|
);
|
||||||
const bottomInset = useBottomBarInset();
|
const bottomInset = useBottomBarInset();
|
||||||
|
const onListScroll = useAutoHideOnScroll();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const t = setTimeout(() => setDebouncedQ(q), 250);
|
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="Top" compact active={order === 'popular'} onPress={() => setOrder('popular')} />
|
||||||
<PillButton label="Recent" compact active={order === 'recent'} onPress={() => setOrder('recent')} />
|
<PillButton label="Recent" compact active={order === 'recent'} onPress={() => setOrder('recent')} />
|
||||||
</View>
|
</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 }} />}
|
{isLoading && <ActivityIndicator color={theme.fg} style={{ marginTop: 24 }} />}
|
||||||
{error instanceof Error && <Text style={styles.error}>{error.message}</Text>}
|
{error instanceof Error && <Text style={styles.error}>{error.message}</Text>}
|
||||||
|
|
@ -117,6 +120,8 @@ export function SitesScreen() {
|
||||||
ref={listRef}
|
ref={listRef}
|
||||||
data={items}
|
data={items}
|
||||||
keyExtractor={(s) => s.origin}
|
keyExtractor={(s) => s.origin}
|
||||||
|
onScroll={onListScroll}
|
||||||
|
scrollEventThrottle={16}
|
||||||
numColumns={2}
|
numColumns={2}
|
||||||
columnWrapperStyle={styles.gridRow}
|
columnWrapperStyle={styles.gridRow}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue