fix(ui): pasek zakladek naprawde plynie - zakladki jako stan, nie trasy
Zgloszenie: "zamiast slicznych animowanych przesuwajacych sie elementow mam
statyczna kropke". Dwie niezalezne przyczyny, obie konieczne do naprawy:
1. Animacja NIE MIALA FIZYCZNEJ MOZLIWOSCI sie wykonac. Kazda zakladka byla
osobnym ekranem native stacka, a przelaczenie robilo navigation.replace(t),
czyli wymienialo caly ekran razem z naglowkiem. LiquidTabs montowal sie od
nowa z kropla juz na docelowej pozycji (pierwsze ustawienie celowo bez
animacji). Fix: jeden HomeScreen, zakladka to stan lokalny, pasek przezywa
przelaczenie. key={tab} na tresci zachowuje dotychczasowy reset stanu.
2. Pasek byl NIEWIDZIALNY: wypelnienie theme.card na naglowku o tle theme.card.
Z calego "roztapiania" zostawala sama akcentowa kropka. Fix: naglowek na
theme.bg.
Do tego drobne: easing Easing.out(cubic) 260ms (wczesniej bez znaczenia, bo kod
nigdy nie dzialal); BugReportFAB czyta zakladke z params.tab (nazwa trasy to
teraz zawsze 'Scenes'); ikony naglowka ujednolicone na wszystkich zakladkach;
App.tsx: blad ladowania fontow przestaje wieszac boot na wiecznym spinnerze
(fail-open na system font) - obnazone przez rozjazd JS/natywki w dev buildzie.
Trasy Movies/Sites/JAV/Anime wyrejestrowane (nic poza paskiem do nich nie
nawigowalo); typy w RootStackParamList zostaja dla useNavigation.
Zweryfikowane nagraniem ekranu emulatora klatka po klatce: kropla plynie
Scenes->Sites i JAV->Anime z wedrujacym wglebieniem, pasek widoczny, tresc
i przycisk Performers/Tags zgodne z zakladka.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f48fbbd4ad
commit
0949091f78
6 changed files with 120 additions and 137 deletions
|
|
@ -113,12 +113,16 @@ function applyDefaultFont() {
|
|||
}
|
||||
|
||||
export default function App() {
|
||||
const [fontsLoaded] = useFonts({
|
||||
const [fontsLoaded, fontsError] = useFonts({
|
||||
'GeneralSans-Regular': require('./assets/fonts/GeneralSans-Regular.ttf'),
|
||||
'GeneralSans-Medium': require('./assets/fonts/GeneralSans-Medium.ttf'),
|
||||
'GeneralSans-Semibold': require('./assets/fonts/GeneralSans-Semibold.ttf'),
|
||||
'GeistMono-Regular': require('./assets/fonts/GeistMono-Regular.ttf'),
|
||||
});
|
||||
// Fail-open: błąd ładowania fontów NIE MOŻE wieszać bootu. `fontsLoaded` zostaje
|
||||
// false na zawsze przy odrzuconym loadAsync (np. rozjazd JS/natywki w dev build,
|
||||
// 2026-08-18: 'AppDirectories not found' → wieczny spinner). System font < brak apki.
|
||||
const fontsSettled = fontsLoaded || fontsError != null;
|
||||
if (fontsLoaded) applyDefaultFont();
|
||||
|
||||
const [hydrated, setHydrated] = useState(false);
|
||||
|
|
@ -323,7 +327,7 @@ export default function App() {
|
|||
return () => sub.remove();
|
||||
}, []);
|
||||
|
||||
if (!fontsLoaded || !hydrated || !lockReady) {
|
||||
if (!fontsSettled || !hydrated || !lockReady) {
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: theme.bg, justifyContent: 'center' }}>
|
||||
<ActivityIndicator color={theme.fg} />
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@ export type ChangelogEntry = {
|
|||
};
|
||||
|
||||
export const CHANGELOG: ChangelogEntry[] = [
|
||||
{
|
||||
id: '2026-08-18',
|
||||
date: 'August 2026',
|
||||
items: [
|
||||
'The liquid tab bar now actually flows. The melt animation existed but could never run: switching tabs rebuilt the whole header, so the blob just teleported. The bar also drew in the same colour as the header behind it, which reduced the whole effect to a floating dot. Both fixed, the blob now slides and the bar melts around it.',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2026-08-14e',
|
||||
date: 'August 2026',
|
||||
|
|
|
|||
|
|
@ -160,6 +160,10 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
|
|||
const route = navRef.isReady() ? navRef.getCurrentRoute() : null;
|
||||
routeName = route?.name ?? null;
|
||||
const params = (route?.params ?? {}) as Record<string, unknown>;
|
||||
// Zakładki (Scenes/Movies/Sites/JAV/Anime) to JEDNA trasa ze stanem lokalnym
|
||||
// (HomeScreen), więc nazwa trasy nie mówi, na której zakładce user był. HomeScreen
|
||||
// odkłada ją w params.tab — bez tego triage zgłoszeń straciłby rozróżnienie.
|
||||
if (typeof params.tab === 'string' && params.tab) routeName = params.tab;
|
||||
rawSceneId =
|
||||
(params['sceneId'] as string | undefined) ??
|
||||
(params['id'] as string | undefined);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
* realna szerokość zależy od telefonu. Do pierwszego pomiaru rysujemy sam pasek.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Animated, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import { Animated, Easing, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import Svg, { Path } from 'react-native-svg';
|
||||
|
||||
import { liquidTabPath, plainBar } from '../lib/liquidTabPath';
|
||||
|
|
@ -65,7 +65,11 @@ export function LiquidTabs<T extends string>({ tabs, current, onSelect }: Props<
|
|||
}
|
||||
const a = Animated.timing(anim, {
|
||||
toValue: targetCx,
|
||||
duration: 200,
|
||||
// Szybki start i miękkie dojście: kropla „dopływa" zamiast stawać jak wagon.
|
||||
// Do 2026-08-18 ten kod nigdy nie działał (pasek montował się od nowa przy każdym
|
||||
// przełączeniu, patrz HomeScreen w navigation.tsx), więc easing nie miał znaczenia.
|
||||
duration: 260,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
useNativeDriver: false,
|
||||
});
|
||||
a.start();
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { GoonClient } from './api';
|
|||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import React from 'react';
|
||||
import { Pressable, Text, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { AppLockSettingsScreen } from './screens/AppLockSettingsScreen';
|
||||
import { BlacklistScreen } from './screens/BlacklistScreen';
|
||||
import { DiagnosticBrowserScreen } from './screens/DiagnosticBrowserScreen';
|
||||
|
|
@ -31,7 +32,12 @@ import { TagsScreen } from './screens/TagsScreen';
|
|||
import { fonts, theme } from './theme';
|
||||
|
||||
export type RootStackParamList = {
|
||||
Scenes: undefined;
|
||||
// `tab` odkłada HomeScreen (setParams) wyłącznie po to, żeby BugReportFAB wiedział,
|
||||
// na której zakładce user był: zakładki to stan lokalny, nie osobne trasy.
|
||||
Scenes: { tab?: string } | undefined;
|
||||
// UWAGA: Movies/Sites/JAV/Anime NIE sa zarejestrowane w navigatorze od 2026-08-18 —
|
||||
// zakladki to stan lokalny HomeScreen (trasa 'Scenes'). Klucze zostaja, bo ekrany
|
||||
// typuja useNavigation<...,'Movies'> itd.; nawigowanie do nich rzuciloby bledem.
|
||||
Movies: undefined;
|
||||
Sites: undefined;
|
||||
// JAV: osobna sekcja, ScenesScreen z paramem jav → backend ?jav=true.
|
||||
|
|
@ -112,23 +118,85 @@ const Stack = createNativeStackNavigator<RootStackParamList>();
|
|||
|
||||
type TopTab = 'Scenes' | 'Movies' | 'Sites' | 'JAV' | 'Anime';
|
||||
|
||||
function TopTabs({
|
||||
current,
|
||||
onNavigate,
|
||||
/**
|
||||
* Jeden ekran-gospodarz dla pięciu zakładek. Zakładka to STAN LOKALNY, nie trasa.
|
||||
*
|
||||
* Poprzednio każda zakładka była osobnym ekranem native stacka, a przełączenie robiło
|
||||
* `navigation.replace(t)` — czyli wymieniało cały ekran RAZEM z nagłówkiem. LiquidTabs
|
||||
* montował się od nowa z kroplą już na docelowej pozycji (pierwsze ustawienie jest
|
||||
* celowo bez animacji), więc animacja „przelewania" nie miała fizycznej możliwości się
|
||||
* wykonać: user widział statyczną, teleportującą się kropkę (zgłoszenie 2026-08-18).
|
||||
* Pasek musi przeżywać przełączenie, żeby się animować, stąd jeden ekran.
|
||||
*
|
||||
* `key={tab}` na treści wymusza czysty remount przy przełączeniu — to samo, co robiło
|
||||
* `replace` (reset wyszukiwania/scrolla). Bez key przełączenie Scenes→JAV reużyłoby
|
||||
* instancję ScenesScreen i zostawiło stary tekst wyszukiwania w nowej sekcji.
|
||||
*/
|
||||
function HomeScreen({
|
||||
navigation,
|
||||
onLogout,
|
||||
}: {
|
||||
current: TopTab;
|
||||
onNavigate: (tab: TopTab) => void;
|
||||
navigation: { setParams: (p: { tab?: string }) => void; navigate: (r: 'Donate' | 'AppLockSettings') => void };
|
||||
onLogout: () => void;
|
||||
}) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const tabs: TopTab[] = ['Scenes', 'Movies', 'Sites', 'JAV', 'Anime'];
|
||||
const [tab, setTab] = React.useState<TopTab>('Scenes');
|
||||
|
||||
// BugReportFAB czyta zakładkę z route.params.tab (nazwa trasy to zawsze 'Scenes').
|
||||
React.useEffect(() => {
|
||||
navigation.setParams({ tab });
|
||||
}, [tab, navigation]);
|
||||
|
||||
return (
|
||||
// Logo + separator zostają statyczne, przewija się (i „roztapia") tylko pasek.
|
||||
// `flexShrink` trzyma całość w swojej połowie nagłówka: bez tego pasek rozpycha
|
||||
// się na całą szerokość i nachodzi na ikony po prawej.
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8, paddingLeft: 10, flexShrink: 1 }}>
|
||||
<GoonMark size={28} />
|
||||
<View style={{ width: 1, height: 16, backgroundColor: theme.border }} />
|
||||
<View style={{ flex: 1, minWidth: 170 }}>
|
||||
<LiquidTabs tabs={tabs} current={current} onSelect={onNavigate} />
|
||||
<View style={{ flex: 1 }}>
|
||||
{/* Własny nagłówek zamiast natywnego (headerShown:false): natywny należy do
|
||||
ekranu, a ten pasek musi przeżywać przełączanie zakładek. Ikony po prawej są
|
||||
teraz jednakowe na każdej zakładce (wcześniej ♥ i ⚙ miał tylko Scenes). */}
|
||||
{/* Tło nagłówka MUSI różnić się od wypełnienia paska (theme.card), inaczej
|
||||
roztopiony kształt jest niewidzialny i z całego efektu zostaje sama kropka —
|
||||
to była druga połowa zgłoszenia „statyczna kropka" (2026-08-18). */}
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: theme.bg,
|
||||
paddingTop: insets.top,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 14,
|
||||
paddingBottom: 2,
|
||||
}}
|
||||
>
|
||||
<GoonMark size={28} />
|
||||
<View style={{ width: 1, height: 16, backgroundColor: theme.border }} />
|
||||
<View style={{ flex: 1, minWidth: 170 }}>
|
||||
<LiquidTabs tabs={tabs} current={tab} onSelect={setTab} />
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', gap: 14, alignItems: 'center', marginLeft: 6 }}>
|
||||
<Pressable onPress={() => navigation.navigate('Donate')} hitSlop={12}>
|
||||
<Text style={{ color: theme.accentText, fontSize: 18 }}>♥</Text>
|
||||
</Pressable>
|
||||
<Pressable onPress={() => navigation.navigate('AppLockSettings')} hitSlop={12}>
|
||||
<Text style={{ color: theme.muted, fontSize: 18 }}>⚙</Text>
|
||||
</Pressable>
|
||||
<Pressable onPress={onLogout} hitSlop={12}>
|
||||
<Text style={{ fontSize: 17 }}>🚪</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ flex: 1 }} key={tab}>
|
||||
{tab === 'Movies' ? (
|
||||
<MoviesScreen />
|
||||
) : tab === 'Sites' ? (
|
||||
<SitesScreen />
|
||||
) : tab === 'JAV' ? (
|
||||
<ScenesScreen jav />
|
||||
) : tab === 'Anime' ? (
|
||||
<ScenesScreen vertical="animation" />
|
||||
) : (
|
||||
<ScenesScreen />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
|
@ -170,121 +238,9 @@ export function AppNavigator({ onLogout, client, appVersion }: AppNavigatorProps
|
|||
contentStyle: { backgroundColor: theme.bg },
|
||||
}}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="Scenes"
|
||||
component={ScenesScreen}
|
||||
options={({ navigation }) => ({
|
||||
title: '',
|
||||
headerLeft: () => (
|
||||
<TopTabs
|
||||
current="Scenes"
|
||||
onNavigate={(t) => navigation.replace(t)}
|
||||
/>
|
||||
),
|
||||
headerRight: () => (
|
||||
<View style={{ flexDirection: 'row', gap: 14, alignItems: 'center' }}>
|
||||
<Pressable onPress={() => navigation.navigate('Donate')} hitSlop={12}>
|
||||
<Text style={{ color: theme.accentText, fontSize: 18 }}>♥</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => navigation.navigate('AppLockSettings')}
|
||||
hitSlop={12}
|
||||
>
|
||||
<Text style={{ color: theme.muted, fontSize: 18 }}>⚙</Text>
|
||||
</Pressable>
|
||||
<Pressable onPress={onLogout} hitSlop={12}>
|
||||
<Text style={{ fontSize: 17 }}>🚪</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
),
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Movies"
|
||||
component={MoviesScreen}
|
||||
options={({ navigation }) => ({
|
||||
title: '',
|
||||
headerBackVisible: false,
|
||||
headerLeft: () => (
|
||||
<TopTabs
|
||||
current="Movies"
|
||||
onNavigate={(t) => navigation.replace(t)}
|
||||
/>
|
||||
),
|
||||
headerRight: () => (
|
||||
<View style={{ flexDirection: 'row', gap: 14, alignItems: 'center' }}>
|
||||
<Pressable onPress={onLogout} hitSlop={12}>
|
||||
<Text style={{ fontSize: 17 }}>🚪</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
),
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Sites"
|
||||
component={SitesScreen}
|
||||
options={({ navigation }) => ({
|
||||
title: '',
|
||||
headerBackVisible: false,
|
||||
headerLeft: () => (
|
||||
<TopTabs
|
||||
current="Sites"
|
||||
onNavigate={(t) => navigation.replace(t)}
|
||||
/>
|
||||
),
|
||||
headerRight: () => (
|
||||
<View style={{ flexDirection: 'row', gap: 14, alignItems: 'center' }}>
|
||||
<Pressable onPress={onLogout} hitSlop={12}>
|
||||
<Text style={{ fontSize: 17 }}>🚪</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
),
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="JAV"
|
||||
component={ScenesScreen}
|
||||
initialParams={{ jav: true }}
|
||||
options={({ navigation }) => ({
|
||||
title: '',
|
||||
headerBackVisible: false,
|
||||
headerLeft: () => (
|
||||
<TopTabs
|
||||
current="JAV"
|
||||
onNavigate={(t) => navigation.replace(t)}
|
||||
/>
|
||||
),
|
||||
headerRight: () => (
|
||||
<View style={{ flexDirection: 'row', gap: 14, alignItems: 'center' }}>
|
||||
<Pressable onPress={onLogout} hitSlop={12}>
|
||||
<Text style={{ fontSize: 17 }}>🚪</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
),
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Anime"
|
||||
component={ScenesScreen}
|
||||
initialParams={{ vertical: 'animation' }}
|
||||
options={({ navigation }) => ({
|
||||
title: '',
|
||||
headerBackVisible: false,
|
||||
headerLeft: () => (
|
||||
<TopTabs
|
||||
current="Anime"
|
||||
onNavigate={(t) => navigation.replace(t)}
|
||||
/>
|
||||
),
|
||||
headerRight: () => (
|
||||
<View style={{ flexDirection: 'row', gap: 14, alignItems: 'center' }}>
|
||||
<Pressable onPress={onLogout} hitSlop={12}>
|
||||
<Text style={{ fontSize: 17 }}>🚪</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
),
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen name="Scenes" options={{ headerShown: false }}>
|
||||
{({ navigation }) => <HomeScreen navigation={navigation} onLogout={onLogout} />}
|
||||
</Stack.Screen>
|
||||
<Stack.Screen
|
||||
name="SiteScenes"
|
||||
component={SiteScenesScreen}
|
||||
|
|
|
|||
|
|
@ -26,16 +26,24 @@ import type { RootStackParamList } from '../navigation';
|
|||
import type { SceneOut } from '../types';
|
||||
import { DEFAULT_FILTER, FilterState, ScenesFilterModal } from './ScenesFilterModal';
|
||||
|
||||
export function ScenesScreen() {
|
||||
export function ScenesScreen({
|
||||
jav: javProp,
|
||||
vertical: verticalProp,
|
||||
}: {
|
||||
/** Nadpisuje param trasy. HomeScreen renderuje ten ekran BEZ nawigacji (zakładki to
|
||||
* stan lokalny, nie osobne trasy), więc wertykał przychodzi propsem, nie paramem. */
|
||||
jav?: boolean;
|
||||
vertical?: string;
|
||||
} = {}) {
|
||||
const client = useClient();
|
||||
const { gridColumns } = usePreferences();
|
||||
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList, 'Scenes'>>();
|
||||
// Zakładki wertykałów reużywają ten ekran: JAV przez stary param `jav`, Anime przez
|
||||
// Zakładki wertykałów reużywają ten ekran: JAV przez `jav`, Anime przez
|
||||
// `vertical='animation'`. Brak obu → główny feed, z którego backend odcina wertykały.
|
||||
const route = useRoute();
|
||||
const params = route.params as { jav?: boolean; vertical?: string } | undefined;
|
||||
const jav = params?.jav === true;
|
||||
const vertical = params?.vertical;
|
||||
const jav = javProp ?? (params?.jav === true);
|
||||
const vertical = verticalProp ?? params?.vertical;
|
||||
// Wspólna nazwa wertykału do zapytań o tagi (stary `jav` też ma swój słownik).
|
||||
const verticalKey = vertical ?? (jav ? 'jav' : undefined);
|
||||
// Animacja NIE MA obsady i to nie brak danych, tylko wlasciwosc tresci: dla postaci
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue