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 {
constructor(private baseUrl: string, private apiKey: string) {
constructor(readonly baseUrl: string, private apiKey: string) {
this.baseUrl = baseUrl.replace(/\/+$/, '');
}

View file

@ -29,6 +29,14 @@ export type 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',
date: 'September 2026',

View file

@ -13,6 +13,10 @@
* (theme.ts). Pierscien to `accentText` z miekka poswiata (druga, szersza obwodka o
* 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
* 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. */
const RING_R = 20;
const RING_TOP = 8;
const RING_TOP = 6;
const ICON_SIZE = 22;
/** O ile poswiata wystaje poza pierscien, z kazdej strony. */
const GLOW_PAD = 3;
/** 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. */
export const BOTTOM_BAR_GAP = 10;
export const BOTTOM_BAR_GAP = 0;
export function BottomTabsBar<T extends string>({ tabs, icons, current, onSelect }: Props<T>) {
const insets = useSafeAreaInsets();
@ -164,7 +168,9 @@ const styles = StyleSheet.create({
},
pill: {
height: BOTTOM_BAR_HEIGHT,
borderRadius: BOTTOM_BAR_HEIGHT / 2,
borderTopLeftRadius: 26,
borderTopRightRadius: 26,
borderBottomWidth: 0,
backgroundColor: theme.card,
borderWidth: 1,
borderColor: theme.border,

View file

@ -5,11 +5,10 @@ import {
} from '@react-navigation/native';
import { BugReportFAB } from './components/BugReportFAB';
import { BottomTabsBar } from './components/BottomTabsBar';
import { GoonMark } from './components/GoonWordmark';
import { GoonClient } from './api';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
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 { AppLockSettingsScreen } from './screens/AppLockSettingsScreen';
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
* instancję ScenesScreen i zostawiło stary tekst wyszukiwania w nowej sekcji.
*/
function HomeScreen({
navigation,
onLogout,
}: {
navigation: { setParams: (p: { tab?: string }) => void; navigate: (r: 'Donate' | 'AppLockSettings') => void };
onLogout: () => void;
}) {
function HomeScreen({ navigation }: { navigation: { setParams: (p: { tab?: string }) => void } }) {
const insets = useSafeAreaInsets();
const tabs: TopTab[] = ['Scenes', 'Movies', 'Sites', 'JAV', 'Anime'];
const [tab, setTab] = React.useState<TopTab>('Scenes');
@ -148,47 +141,10 @@ function HomeScreen({
}, [tab, navigation]);
return (
<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). */}
<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, paddingTop: insets.top, backgroundColor: theme.bg }}>
{/* Gornej belki nie ma (2026-09-07): nazwe sekcji mowi pasek na dole, kolo zebate
siedzi w wierszu narzedzi, darowizna i zmiana backendu w ustawieniach. Zostaje
tylko odstep na pasek statusu. */}
<View style={{ flex: 1 }} key={tab}>
{tab === 'Movies' ? (
<MoviesScreen />
@ -254,7 +210,7 @@ export function AppNavigator({ onLogout, client, appVersion }: AppNavigatorProps
}}
>
<Stack.Screen name="Scenes" options={{ headerShown: false }}>
{({ navigation }) => <HomeScreen navigation={navigation} onLogout={onLogout} />}
{({ navigation }) => <HomeScreen navigation={navigation} />}
</Stack.Screen>
<Stack.Screen
name="SiteScenes"
@ -288,9 +244,9 @@ export function AppNavigator({ onLogout, client, appVersion }: AppNavigatorProps
/>
<Stack.Screen
name="AppLockSettings"
options={{ title: 'App lock' }}
options={{ title: 'Settings' }}
>
{() => <AppLockSettingsScreen />}
{() => <AppLockSettingsScreen onLogout={onLogout} />}
</Stack.Screen>
<Stack.Screen
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';
export function AppLockSettingsScreen() {
export function AppLockSettingsScreen({ onLogout }: { onLogout?: () => void }) {
const [settings, setSettings] = useState<AppLockSettings | null>(null);
const [bioAvailable, setBioAvailable] = useState(false);
const [stage, setStage] = useState<Stage>('menu');
@ -65,6 +65,9 @@ export function AppLockSettingsScreen() {
const { gridColumns, setGridColumns, defaultQuality, setDefaultQuality, autoRotate, setAutoRotate } = usePreferences();
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
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 [deviceId, setDeviceId] = useState('');
const [importCode, setImportCode] = useState('');
@ -223,6 +226,37 @@ export function AppLockSettingsScreen() {
return (
<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}>
<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 type { RootStackParamList } from '../navigation';
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;
// 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}
returnKeyType="search"
/>
<PillButton
label={`Filter${!isDefaultFilters(filters) ? ' 1' : ''}`}
<IconButton
icon="filter-variant"
accessibilityLabel="Filters"
active={!isDefaultFilters(filters)}
badge={isDefaultFilters(filters) ? 0 : 1}
onPress={() => setFiltersOpen(true)}
/>
<SettingsButton />
</Toolbar>
<MovieFiltersSheet
visible={filtersOpen}

View file

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

View file

@ -21,7 +21,7 @@ import {
import { useClient } from '../ClientContext';
import type { RootStackParamList } from '../navigation';
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';
type Order = 'popular' | 'recent';
@ -95,6 +95,7 @@ export function SitesScreen() {
<View style={styles.container}>
<Toolbar>
<SearchInput value={q} onChangeText={setQ} placeholder="search site…" />
<SettingsButton />
</Toolbar>
<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).
*
@ -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. */
export function Toolbar({ children }: { children: React.ReactNode }) {
return <View style={styles.toolbar}>{children}</View>;
@ -99,6 +153,29 @@ export function Toolbar({ children }: { children: React.ReactNode }) {
const styles = StyleSheet.create({
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: {
flex: 1,
height: CONTROL_H,