feat(mobile): bez gornej belki, ikona filtra, darowizna i backend w ustawieniach

- pasek dolny nizszy (62) i przyciety na krawedzi ekranu, zaokraglone tylko gorne rogi
- IconButton (filter-variant z licznikiem) zamiast pigulki Filter, SettingsButton w
  wierszu narzedzi Scenes/Movies/Sites
- ustawienia: sekcje Support the project i Backend (host + wylogowanie), tytul Settings
- changelog 2026-09-07c, OTA 5c8895d6 na 1.3
This commit is contained in:
goon-foss 2026-09-07 11:37:12 +02:00
parent dce738b8ab
commit 90724af8ef
9 changed files with 162 additions and 66 deletions

View file

@ -41,7 +41,7 @@ export class ApiError extends Error {
} }
export class GoonClient { export class GoonClient {
constructor(private baseUrl: string, private apiKey: string) { constructor(readonly baseUrl: string, private apiKey: string) {
this.baseUrl = baseUrl.replace(/\/+$/, ''); this.baseUrl = baseUrl.replace(/\/+$/, '');
} }

View file

@ -29,6 +29,14 @@ export type ChangelogEntry = {
}; };
export const CHANGELOG: ChangelogEntry[] = [ export const CHANGELOG: ChangelogEntry[] = [
{
id: '2026-09-07c',
date: 'September 2026',
items: [
'The top bar is gone: the bottom bar names the section, the gear next to the search box opens Settings, and Donate plus switching backend now live inside Settings.',
'Filter is an icon with a counter of active filters. The bottom bar is lower and grows out of the screen edge.',
],
},
{ {
id: '2026-09-07b', id: '2026-09-07b',
date: 'September 2026', date: 'September 2026',

View file

@ -13,6 +13,10 @@
* (theme.ts). Pierscien to `accentText` z miekka poswiata (druga, szersza obwodka o * (theme.ts). Pierscien to `accentText` z miekka poswiata (druga, szersza obwodka o
* niskiej przezroczystosci), jeden kolor na wszystkich zakladkach. * niskiej przezroczystosci), jeden kolor na wszystkich zakladkach.
* *
* Ksztalt (uwaga Jana 2026-09-07): pigulka nie jest zamknieta polokregiem od dolu,
* tylko przycieta na krawedzi ekranu, jakby z niej wyrastala. Zaokraglone sa tylko
* gorne rogi, dolne siedza na insecie systemowego paska.
*
* Pozycje ikon mierzymy `onLayout` (flex:1 daje rowne komorki, ale szerokosc paska * Pozycje ikon mierzymy `onLayout` (flex:1 daje rowne komorki, ale szerokosc paska
* zalezy od ekranu). Do pierwszego pomiaru pierscienia nie rysujemy. * zalezy od ekranu). Do pierwszego pomiaru pierscienia nie rysujemy.
*/ */
@ -35,14 +39,14 @@ interface Props<T extends string> {
/** Promien pierscienia: kolo 40 px wokol ikony 22 px. */ /** Promien pierscienia: kolo 40 px wokol ikony 22 px. */
const RING_R = 20; const RING_R = 20;
const RING_TOP = 8; const RING_TOP = 6;
const ICON_SIZE = 22; const ICON_SIZE = 22;
/** O ile poswiata wystaje poza pierscien, z kazdej strony. */ /** O ile poswiata wystaje poza pierscien, z kazdej strony. */
const GLOW_PAD = 3; const GLOW_PAD = 3;
/** Wysokosc samej pigulki, bez marginesu i bez inset systemowego paska. */ /** Wysokosc samej pigulki, bez marginesu i bez inset systemowego paska. */
export const BOTTOM_BAR_HEIGHT = 74; export const BOTTOM_BAR_HEIGHT = 62;
/** Odstep pigulki od dolu (nad inset). Eksport, zeby FAB zglaszania mogl wisiec wyzej. */ /** Odstep pigulki od dolu (nad inset). Eksport, zeby FAB zglaszania mogl wisiec wyzej. */
export const BOTTOM_BAR_GAP = 10; export const BOTTOM_BAR_GAP = 0;
export function BottomTabsBar<T extends string>({ tabs, icons, current, onSelect }: Props<T>) { export function BottomTabsBar<T extends string>({ tabs, icons, current, onSelect }: Props<T>) {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
@ -164,7 +168,9 @@ const styles = StyleSheet.create({
}, },
pill: { pill: {
height: BOTTOM_BAR_HEIGHT, height: BOTTOM_BAR_HEIGHT,
borderRadius: BOTTOM_BAR_HEIGHT / 2, borderTopLeftRadius: 26,
borderTopRightRadius: 26,
borderBottomWidth: 0,
backgroundColor: theme.card, backgroundColor: theme.card,
borderWidth: 1, borderWidth: 1,
borderColor: theme.border, borderColor: theme.border,

View file

@ -5,11 +5,10 @@ import {
} from '@react-navigation/native'; } from '@react-navigation/native';
import { BugReportFAB } from './components/BugReportFAB'; import { BugReportFAB } from './components/BugReportFAB';
import { BottomTabsBar } from './components/BottomTabsBar'; import { BottomTabsBar } from './components/BottomTabsBar';
import { GoonMark } from './components/GoonWordmark';
import { GoonClient } from './api'; import { GoonClient } from './api';
import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { createNativeStackNavigator } from '@react-navigation/native-stack';
import React from 'react'; import React from 'react';
import { Pressable, Text, View } from 'react-native'; import { View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { AppLockSettingsScreen } from './screens/AppLockSettingsScreen'; import { AppLockSettingsScreen } from './screens/AppLockSettingsScreen';
import { BlacklistScreen } from './screens/BlacklistScreen'; import { BlacklistScreen } from './screens/BlacklistScreen';
@ -131,13 +130,7 @@ type TopTab = 'Scenes' | 'Movies' | 'Sites' | 'JAV' | 'Anime';
* `replace` (reset wyszukiwania/scrolla). Bez key przełączenie Scenes→JAV reużyłoby * `replace` (reset wyszukiwania/scrolla). Bez key przełączenie Scenes→JAV reużyłoby
* instancję ScenesScreen i zostawiło stary tekst wyszukiwania w nowej sekcji. * instancję ScenesScreen i zostawiło stary tekst wyszukiwania w nowej sekcji.
*/ */
function HomeScreen({ function HomeScreen({ navigation }: { navigation: { setParams: (p: { tab?: string }) => void } }) {
navigation,
onLogout,
}: {
navigation: { setParams: (p: { tab?: string }) => void; navigate: (r: 'Donate' | 'AppLockSettings') => void };
onLogout: () => void;
}) {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const tabs: TopTab[] = ['Scenes', 'Movies', 'Sites', 'JAV', 'Anime']; const tabs: TopTab[] = ['Scenes', 'Movies', 'Sites', 'JAV', 'Anime'];
const [tab, setTab] = React.useState<TopTab>('Scenes'); const [tab, setTab] = React.useState<TopTab>('Scenes');
@ -148,47 +141,10 @@ function HomeScreen({
}, [tab, navigation]); }, [tab, navigation]);
return ( return (
<View style={{ flex: 1 }}> <View style={{ flex: 1, paddingTop: insets.top, backgroundColor: theme.bg }}>
{/* Własny nagłówek zamiast natywnego (headerShown:false): natywny należy do {/* Gornej belki nie ma (2026-09-07): nazwe sekcji mowi pasek na dole, kolo zebate
ekranu, a ten pasek musi przeżywać przełączanie zakładek. Ikony po prawej są siedzi w wierszu narzedzi, darowizna i zmiana backendu w ustawieniach. Zostaje
teraz jednakowe na każdej zakładce (wcześniej ♥ i ⚙ miał tylko Scenes). */} tylko odstep na pasek statusu. */}
<View
style={{
backgroundColor: theme.card,
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 }} />
<Text
style={{
flex: 1,
color: theme.fg,
fontFamily: fonts.display,
fontSize: 17,
letterSpacing: 0.2,
}}
>
{tab}
</Text>
<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}> <View style={{ flex: 1 }} key={tab}>
{tab === 'Movies' ? ( {tab === 'Movies' ? (
<MoviesScreen /> <MoviesScreen />
@ -254,7 +210,7 @@ export function AppNavigator({ onLogout, client, appVersion }: AppNavigatorProps
}} }}
> >
<Stack.Screen name="Scenes" options={{ headerShown: false }}> <Stack.Screen name="Scenes" options={{ headerShown: false }}>
{({ navigation }) => <HomeScreen navigation={navigation} onLogout={onLogout} />} {({ navigation }) => <HomeScreen navigation={navigation} />}
</Stack.Screen> </Stack.Screen>
<Stack.Screen <Stack.Screen
name="SiteScenes" name="SiteScenes"
@ -288,9 +244,9 @@ export function AppNavigator({ onLogout, client, appVersion }: AppNavigatorProps
/> />
<Stack.Screen <Stack.Screen
name="AppLockSettings" name="AppLockSettings"
options={{ title: 'App lock' }} options={{ title: 'Settings' }}
> >
{() => <AppLockSettingsScreen />} {() => <AppLockSettingsScreen onLogout={onLogout} />}
</Stack.Screen> </Stack.Screen>
<Stack.Screen <Stack.Screen
name="Blacklist" name="Blacklist"

View file

@ -56,7 +56,7 @@ const QUALITY_OPTIONS: { label: string; value: DefaultQuality }[] = [
type Stage = 'menu' | 'enter-current' | 'set-new' | 'confirm-new' | 'disable-confirm'; type Stage = 'menu' | 'enter-current' | 'set-new' | 'confirm-new' | 'disable-confirm';
export function AppLockSettingsScreen() { export function AppLockSettingsScreen({ onLogout }: { onLogout?: () => void }) {
const [settings, setSettings] = useState<AppLockSettings | null>(null); const [settings, setSettings] = useState<AppLockSettings | null>(null);
const [bioAvailable, setBioAvailable] = useState(false); const [bioAvailable, setBioAvailable] = useState(false);
const [stage, setStage] = useState<Stage>('menu'); const [stage, setStage] = useState<Stage>('menu');
@ -65,6 +65,9 @@ export function AppLockSettingsScreen() {
const { gridColumns, setGridColumns, defaultQuality, setDefaultQuality, autoRotate, setAutoRotate } = usePreferences(); const { gridColumns, setGridColumns, defaultQuality, setDefaultQuality, autoRotate, setAutoRotate } = usePreferences();
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>(); const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const client = useClient(); const client = useClient();
// Sam host, bez schematu: user ma zobaczyc, czy siedzi na instancji publicznej,
// czy na wlasnym serwerze.
const backendHost = client ? client.baseUrl.replace(/^https?:\/\//, '') : null;
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [deviceId, setDeviceId] = useState(''); const [deviceId, setDeviceId] = useState('');
const [importCode, setImportCode] = useState(''); const [importCode, setImportCode] = useState('');
@ -223,6 +226,37 @@ export function AppLockSettingsScreen() {
return ( return (
<ScrollView style={styles.root} contentContainerStyle={{ paddingBottom: 40 }}> <ScrollView style={styles.root} contentContainerStyle={{ paddingBottom: 40 }}>
{/* Darowizna i backend byly w gornej belce; belka znikla 2026-09-07. */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Support the project</Text>
<Text style={styles.hint}>
Goon is free, open-source and ad-free. Donations cover the server and the API keys.
</Text>
<Pressable style={styles.actionBtn} onPress={() => navigation.navigate('Donate')}>
<Text style={styles.actionText}>♥ Donate</Text>
</Pressable>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Backend</Text>
<Text style={styles.hint}>
{backendHost ? `Connected to ${backendHost}` : 'Not connected'}
</Text>
{onLogout ? (
<Pressable
style={styles.actionBtn}
onPress={() =>
Alert.alert('Switch backend', 'Sign out and pick the public instance or your own server?', [
{ text: 'Cancel', style: 'cancel' },
{ text: 'Sign out', style: 'destructive', onPress: onLogout },
])
}
>
<Text style={styles.actionText}>Switch backend / sign out</Text>
</Pressable>
) : null}
</View>
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>App lock</Text> <Text style={styles.sectionTitle}>App lock</Text>

View file

@ -25,7 +25,7 @@ import { MoviePosterCard } from '../components/MoviePosterCard';
import { getLastSeen, markSeen } from '../lib/seenStore'; import { getLastSeen, markSeen } from '../lib/seenStore';
import type { RootStackParamList } from '../navigation'; import type { RootStackParamList } from '../navigation';
import { theme } from '../theme'; import { theme } from '../theme';
import { PillButton, SCREEN_PAD_H, SCREEN_PAD_TOP, SearchInput, Toolbar } from '../ui'; import { IconButton, SCREEN_PAD_H, SCREEN_PAD_TOP, SearchInput, SettingsButton, Toolbar } from '../ui';
const PER_PAGE = 30; const PER_PAGE = 30;
// 3 kolumny, plakaty 2:3 mieszczą się dobrze i widać 50% więcej filmów na ekran // 3 kolumny, plakaty 2:3 mieszczą się dobrze i widać 50% więcej filmów na ekran
@ -122,11 +122,14 @@ export function MoviesScreen() {
autoCorrect={false} autoCorrect={false}
returnKeyType="search" returnKeyType="search"
/> />
<PillButton <IconButton
label={`Filter${!isDefaultFilters(filters) ? ' 1' : ''}`} icon="filter-variant"
accessibilityLabel="Filters"
active={!isDefaultFilters(filters)} active={!isDefaultFilters(filters)}
badge={isDefaultFilters(filters) ? 0 : 1}
onPress={() => setFiltersOpen(true)} onPress={() => setFiltersOpen(true)}
/> />
<SettingsButton />
</Toolbar> </Toolbar>
<MovieFiltersSheet <MovieFiltersSheet
visible={filtersOpen} visible={filtersOpen}

View file

@ -15,7 +15,15 @@ import {
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import { MigrationBanner } from '../components/MigrationBanner'; import { MigrationBanner } from '../components/MigrationBanner';
import { PillButton, SCREEN_PAD_H, SCREEN_PAD_TOP, SearchInput, Toolbar } from '../ui'; import {
IconButton,
PillButton,
SCREEN_PAD_H,
SCREEN_PAD_TOP,
SearchInput,
SettingsButton,
Toolbar,
} from '../ui';
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';
@ -172,11 +180,14 @@ export function ScenesScreen({
}} }}
/> />
) : null} ) : null}
<PillButton <IconButton
label={`Filter${activeCount > 0 ? ` ${activeCount}` : ''}`} icon="filter-variant"
accessibilityLabel="Filters"
active={activeCount > 0} active={activeCount > 0}
badge={activeCount}
onPress={() => setFilterOpen(true)} onPress={() => setFilterOpen(true)}
/> />
<SettingsButton />
</Toolbar> </Toolbar>
{saved.length > 0 ? ( {saved.length > 0 ? (

View file

@ -21,7 +21,7 @@ import {
import { useClient } from '../ClientContext'; import { useClient } from '../ClientContext';
import type { RootStackParamList } from '../navigation'; import type { RootStackParamList } from '../navigation';
import { theme } from '../theme'; import { theme } from '../theme';
import { PillButton, SCREEN_PAD_H, SCREEN_PAD_TOP, SearchInput, 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';
type Order = 'popular' | 'recent'; type Order = 'popular' | 'recent';
@ -95,6 +95,7 @@ export function SitesScreen() {
<View style={styles.container}> <View style={styles.container}>
<Toolbar> <Toolbar>
<SearchInput value={q} onChangeText={setQ} placeholder="search site…" /> <SearchInput value={q} onChangeText={setQ} placeholder="search site…" />
<SettingsButton />
</Toolbar> </Toolbar>
<View style={styles.segment}> <View style={styles.segment}>

View file

@ -1,3 +1,7 @@
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { RootStackParamList } from './navigation';
/** /**
* Wspólny zestaw kontrolek nagłówka ekranów-list (Scenes, Movies, Sites, JAV, Anime). * Wspólny zestaw kontrolek nagłówka ekranów-list (Scenes, Movies, Sites, JAV, Anime).
* *
@ -92,6 +96,56 @@ export function PillButton({
); );
} }
/**
* Kwadratowy przycisk z ikona zamiast pigulki z napisem. `badge` to licznik w rogu
* (np. liczba aktywnych filtrow), pokazywany tylko gdy > 0.
*/
export function IconButton({
icon,
active,
badge,
onPress,
accessibilityLabel,
}: {
icon: React.ComponentProps<typeof MaterialCommunityIcons>['name'];
active?: boolean;
badge?: number;
onPress?: () => void;
accessibilityLabel: string;
}) {
return (
<Pressable
onPress={onPress}
hitSlop={6}
accessibilityRole="button"
accessibilityLabel={accessibilityLabel}
style={[styles.iconBtn, active && styles.pillActive]}
>
<MaterialCommunityIcons name={icon} size={22} color={active ? theme.accentText : theme.fg} />
{badge && badge > 0 ? (
<View style={styles.badge}>
<Text style={styles.badgeText}>{badge}</Text>
</View>
) : null}
</Pressable>
);
}
/**
* Wejscie do ustawien. Gorna belka znikla (2026-09-07), wiec kolo zebate siedzi w
* wierszu narzedzi kazdej sekcji; darowizna i zmiana backendu sa w ustawieniach.
*/
export function SettingsButton() {
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
return (
<IconButton
icon="cog-outline"
accessibilityLabel="Settings"
onPress={() => navigation.navigate('AppLockSettings')}
/>
);
}
/** Wiersz narzędzi: pole + przyciski, jednakowy odstęp i margines na każdym ekranie. */ /** Wiersz narzędzi: pole + przyciski, jednakowy odstęp i margines na każdym ekranie. */
export function Toolbar({ children }: { children: React.ReactNode }) { export function Toolbar({ children }: { children: React.ReactNode }) {
return <View style={styles.toolbar}>{children}</View>; return <View style={styles.toolbar}>{children}</View>;
@ -99,6 +153,29 @@ export function Toolbar({ children }: { children: React.ReactNode }) {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
toolbar: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 12 }, toolbar: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 12 },
iconBtn: {
width: CONTROL_H,
height: CONTROL_H,
borderRadius: RADIUS,
backgroundColor: theme.card,
borderColor: theme.borderStrong,
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
},
badge: {
position: 'absolute',
top: -6,
right: -6,
minWidth: 18,
height: 18,
borderRadius: 9,
paddingHorizontal: 4,
backgroundColor: theme.accent,
alignItems: 'center',
justifyContent: 'center',
},
badgeText: { color: theme.fg, fontSize: 11, fontFamily: fonts.display },
search: { search: {
flex: 1, flex: 1,
height: CONTROL_H, height: CONTROL_H,