fix: safe area and UI polish pass
- Enable dark navigation headers in Tab navigator (fixes status bar overlap on all 5 tab screens automatically) - Use useSafeAreaInsets for AlbumDetail/ArtistDetail back buttons - Use useSafeAreaInsets for NowPlaying header and bottom padding - Remove redundant paddingTop from Home heading and Login content - Cleaner tab bar (height 60, better label spacing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
98394bab75
commit
b25e162806
6 files changed
+37
-17
No files matched your search
+20
-5
@@ -50,17 +50,32 @@ function HomeTabs() {
|
|||||||
return (
|
return (
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
screenOptions={({ route }) => ({
|
screenOptions={({ route }) => ({
|
||||||
headerShown: false,
|
headerStyle: {
|
||||||
tabBarStyle: { backgroundColor: '#1e1e1e', borderTopColor: '#2a2a2a' },
|
backgroundColor: '#121212',
|
||||||
|
elevation: 0,
|
||||||
|
shadowOpacity: 0,
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
},
|
||||||
|
headerTintColor: '#fff',
|
||||||
|
headerTitleStyle: { fontWeight: '800', fontSize: 20 },
|
||||||
|
tabBarStyle: {
|
||||||
|
backgroundColor: '#1a1a1a',
|
||||||
|
borderTopColor: '#2a2a2a',
|
||||||
|
borderTopWidth: 1,
|
||||||
|
height: 60,
|
||||||
|
paddingBottom: 8,
|
||||||
|
paddingTop: 4,
|
||||||
|
},
|
||||||
tabBarActiveTintColor: '#1db954',
|
tabBarActiveTintColor: '#1db954',
|
||||||
tabBarInactiveTintColor: '#888',
|
tabBarInactiveTintColor: '#666',
|
||||||
|
tabBarLabelStyle: { fontSize: 10, fontWeight: '600', marginTop: 2 },
|
||||||
tabBarIcon: ({ color, size }) => {
|
tabBarIcon: ({ color, size }) => {
|
||||||
const icons = { Home: 'home', Artists: 'person', Albums: 'disc', Search: 'search', Settings: 'settings-outline' };
|
const icons = { Home: 'home', Artists: 'person', Albums: 'disc', Search: 'search', Settings: 'settings-outline' };
|
||||||
return <Ionicons name={icons[route.name] || 'ellipse'} size={size} color={color} />;
|
return <Ionicons name={icons[route.name] || 'ellipse'} size={22} color={color} />;
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Tab.Screen name="Home" component={HomeScreen} options={{ title: 'Start' }} />
|
<Tab.Screen name="Home" component={HomeScreen} options={{ title: 'Start', headerTitle: 'NovaSonic' }} />
|
||||||
<Tab.Screen name="Artists" component={ArtistsScreen} options={{ title: 'Künstler' }} />
|
<Tab.Screen name="Artists" component={ArtistsScreen} options={{ title: 'Künstler' }} />
|
||||||
<Tab.Screen name="Albums" component={AlbumsScreen} options={{ title: 'Alben' }} />
|
<Tab.Screen name="Albums" component={AlbumsScreen} options={{ title: 'Alben' }} />
|
||||||
<Tab.Screen name="Search" component={SearchScreen} options={{ title: 'Suche' }} />
|
<Tab.Screen name="Search" component={SearchScreen} options={{ title: 'Suche' }} />
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
StyleSheet, ActivityIndicator,
|
StyleSheet, ActivityIndicator,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { api, ensureArtistSortMap, dispAlbumName, dispArtistById, coverUrl } from '../api';
|
import { api, ensureArtistSortMap, dispAlbumName, dispArtistById, coverUrl } from '../api';
|
||||||
import { playQueue, skipToIndex } from '../audio/player';
|
import { playQueue, skipToIndex } from '../audio/player';
|
||||||
@@ -12,6 +13,7 @@ import SongItem from '../components/SongItem';
|
|||||||
|
|
||||||
export default function AlbumDetailScreen() {
|
export default function AlbumDetailScreen() {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
const { album } = useRoute().params;
|
const { album } = useRoute().params;
|
||||||
const { cfg } = useStore();
|
const { cfg } = useStore();
|
||||||
const [songs, setSongs] = useState([]);
|
const [songs, setSongs] = useState([]);
|
||||||
@@ -49,7 +51,7 @@ export default function AlbumDetailScreen() {
|
|||||||
ListHeaderComponent={() => (
|
ListHeaderComponent={() => (
|
||||||
<View>
|
<View>
|
||||||
{/* Back button */}
|
{/* Back button */}
|
||||||
<TouchableOpacity style={styles.backBtn} onPress={() => navigation.goBack()}>
|
<TouchableOpacity style={[styles.backBtn, { top: insets.top + 8 }]} onPress={() => navigation.goBack()}>
|
||||||
<Ionicons name="chevron-back" size={24} color="#fff" />
|
<Ionicons name="chevron-back" size={24} color="#fff" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
@@ -111,10 +113,10 @@ export default function AlbumDetailScreen() {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: '#121212' },
|
container: { flex: 1, backgroundColor: '#121212' },
|
||||||
backBtn: {
|
backBtn: {
|
||||||
position: 'absolute', top: 44, left: 12, zIndex: 10,
|
position: 'absolute', left: 12, zIndex: 10,
|
||||||
backgroundColor: 'rgba(0,0,0,0.5)', borderRadius: 20, padding: 6,
|
backgroundColor: 'rgba(0,0,0,0.5)', borderRadius: 20, padding: 6,
|
||||||
},
|
},
|
||||||
artWrap: { alignItems: 'center', paddingTop: 60, paddingBottom: 16 },
|
artWrap: { alignItems: 'center', paddingTop: 72, paddingBottom: 16 },
|
||||||
art: {
|
art: {
|
||||||
width: 220, height: 220, borderRadius: 8,
|
width: 220, height: 220, borderRadius: 8,
|
||||||
shadowColor: '#000', shadowOpacity: 0.5, shadowRadius: 16,
|
shadowColor: '#000', shadowOpacity: 0.5, shadowRadius: 16,
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import {
|
|||||||
|
|
||||||
const SCREEN_WIDTH = Dimensions.get('window').width;
|
const SCREEN_WIDTH = Dimensions.get('window').width;
|
||||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { api, apiCached, dispArtistById, dispAlbumName, coverUrl, getArtistImage, cacheArtistSortNames } from '../api';
|
import { api, apiCached, dispArtistById, dispAlbumName, coverUrl, getArtistImage, cacheArtistSortNames } from '../api';
|
||||||
import AlbumCard from '../components/AlbumCard';
|
import AlbumCard from '../components/AlbumCard';
|
||||||
|
|
||||||
export default function ArtistDetailScreen() {
|
export default function ArtistDetailScreen() {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
const { artist } = useRoute().params;
|
const { artist } = useRoute().params;
|
||||||
const [albums, setAlbums] = useState([]);
|
const [albums, setAlbums] = useState([]);
|
||||||
const [img, setImg] = useState(null);
|
const [img, setImg] = useState(null);
|
||||||
@@ -43,7 +45,7 @@ export default function ArtistDetailScreen() {
|
|||||||
<Text style={{ fontSize: 64 }}>🎤</Text>
|
<Text style={{ fontSize: 64 }}>🎤</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<TouchableOpacity style={styles.backBtn} onPress={() => navigation.goBack()}>
|
<TouchableOpacity style={[styles.backBtn, { top: insets.top + 8 }]} onPress={() => navigation.goBack()}>
|
||||||
<Ionicons name="chevron-back" size={24} color="#fff" />
|
<Ionicons name="chevron-back" size={24} color="#fff" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={styles.heroOverlay}>
|
<View style={styles.heroOverlay}>
|
||||||
@@ -91,7 +93,7 @@ const styles = StyleSheet.create({
|
|||||||
heroName: { color: '#fff', fontSize: 24, fontWeight: '800', textShadowColor: '#000', textShadowRadius: 8 },
|
heroName: { color: '#fff', fontSize: 24, fontWeight: '800', textShadowColor: '#000', textShadowRadius: 8 },
|
||||||
heroSub: { color: '#ddd', fontSize: 13, marginTop: 2 },
|
heroSub: { color: '#ddd', fontSize: 13, marginTop: 2 },
|
||||||
backBtn: {
|
backBtn: {
|
||||||
position: 'absolute', top: 44, left: 12,
|
position: 'absolute', left: 12,
|
||||||
backgroundColor: 'rgba(0,0,0,0.5)', borderRadius: 20,
|
backgroundColor: 'rgba(0,0,0,0.5)', borderRadius: 20,
|
||||||
padding: 6,
|
padding: 6,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -92,8 +92,7 @@ export default function HomeScreen() {
|
|||||||
style={styles.container}
|
style={styles.container}
|
||||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); load(true); }} tintColor="#1db954" />}
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); load(true); }} tintColor="#1db954" />}
|
||||||
>
|
>
|
||||||
<Text style={styles.heading}>Willkommen zurück 👋</Text>
|
<AlbumRow title="Zuletzt gehört" albums={recent} onAlbumPress={goAlbum} />
|
||||||
<AlbumRow title="Zuletzt gehört" albums={recent} onAlbumPress={goAlbum} />
|
|
||||||
<AlbumRow title="Meistgespielt" albums={frequent} onAlbumPress={goAlbum} />
|
<AlbumRow title="Meistgespielt" albums={frequent} onAlbumPress={goAlbum} />
|
||||||
<AlbumRow title="Neu hinzugefügt" albums={newest} onAlbumPress={goAlbum} />
|
<AlbumRow title="Neu hinzugefügt" albums={newest} onAlbumPress={goAlbum} />
|
||||||
<View style={{ height: 16 }} />
|
<View style={{ height: 16 }} />
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export default function LoginScreen() {
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: '#121212' },
|
container: { flex: 1, backgroundColor: '#121212' },
|
||||||
content: { padding: 24, paddingTop: 60 },
|
content: { padding: 24, paddingTop: 24 },
|
||||||
logo: { fontSize: 32, textAlign: 'center', marginBottom: 8 },
|
logo: { fontSize: 32, textAlign: 'center', marginBottom: 8 },
|
||||||
subtitle: { color: '#aaa', textAlign: 'center', marginBottom: 32, fontSize: 14 },
|
subtitle: { color: '#aaa', textAlign: 'center', marginBottom: 32, fontSize: 14 },
|
||||||
label: { color: '#ccc', fontSize: 13, marginBottom: 6, marginTop: 16 },
|
label: { color: '#ccc', fontSize: 13, marginBottom: 6, marginTop: 16 },
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
View, Text, Image, TouchableOpacity,
|
View, Text, Image, TouchableOpacity,
|
||||||
StyleSheet, Dimensions, PanResponder,
|
StyleSheet, Dimensions,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { usePlaybackState, useProgress, State } from 'react-native-track-player';
|
import { usePlaybackState, useProgress, State } from 'react-native-track-player';
|
||||||
import Slider from '@react-native-community/slider';
|
import Slider from '@react-native-community/slider';
|
||||||
@@ -15,6 +16,7 @@ const { width } = Dimensions.get('window');
|
|||||||
|
|
||||||
export default function NowPlayingScreen() {
|
export default function NowPlayingScreen() {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
const { currentTrack, repeat, shuffle, setShuffle } = useStore();
|
const { currentTrack, repeat, shuffle, setShuffle } = useStore();
|
||||||
const playbackState = usePlaybackState();
|
const playbackState = usePlaybackState();
|
||||||
const { position, duration } = useProgress(500);
|
const { position, duration } = useProgress(500);
|
||||||
@@ -38,7 +40,7 @@ export default function NowPlayingScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={[styles.container, { paddingTop: insets.top, paddingBottom: insets.bottom + 16 }]}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.headerBtn}>
|
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.headerBtn}>
|
||||||
@@ -119,13 +121,13 @@ export default function NowPlayingScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: '#121212', paddingBottom: 40 },
|
container: { flex: 1, backgroundColor: '#121212' },
|
||||||
empty: { flex: 1, backgroundColor: '#121212', alignItems: 'center', justifyContent: 'center' },
|
empty: { flex: 1, backgroundColor: '#121212', alignItems: 'center', justifyContent: 'center' },
|
||||||
emptyText: { color: '#aaa', fontSize: 16 },
|
emptyText: { color: '#aaa', fontSize: 16 },
|
||||||
back: { color: '#1db954', marginTop: 12, fontSize: 14 },
|
back: { color: '#1db954', marginTop: 12, fontSize: 14 },
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between',
|
flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between',
|
||||||
paddingTop: 52, paddingHorizontal: 16, paddingBottom: 12,
|
paddingTop: 12, paddingHorizontal: 16, paddingBottom: 12,
|
||||||
},
|
},
|
||||||
headerBtn: { width: 40, alignItems: 'flex-start' },
|
headerBtn: { width: 40, alignItems: 'flex-start' },
|
||||||
headerTitle: { color: '#fff', fontSize: 14, fontWeight: '700', letterSpacing: 0.5 },
|
headerTitle: { color: '#fff', fontSize: 14, fontWeight: '700', letterSpacing: 0.5 },
|
||||||
|
|||||||
Reference in new issue
Block a user