Fixed Android Safe View Areas (#294)

* Fixed Android safe view areas

* fix: xmark positioning

* fix: redundant safeareaprovider

* fix: roll back redundant changes

* fix: linter

---------

Co-authored-by: Lei Nelissen <lei@codified.nl>
This commit is contained in:
Kris
2025-08-04 15:03:40 -07:00
committed by GitHub
parent 63481d0240
commit c4838b3b9e
11 changed files with 92 additions and 76 deletions

View File

@@ -49,7 +49,7 @@
"download-track": "Download Track", "download-track": "Download Track",
"download-album": "Download Album", "download-album": "Download Album",
"download-playlist": "Download Playlist", "download-playlist": "Download Playlist",
"no-downloads": "You have not yet downloaded any tracks", "no-downloads": "You have not downloaded any tracks yet",
"delete-track": "Delete Track", "delete-track": "Delete Track",
"delete-all-tracks": "Delete All Tracks", "delete-all-tracks": "Delete All Tracks",
"confirm-delete-all-tracks": "Are you sure you want to delete all currently downloaded tracks?", "confirm-delete-all-tracks": "Are you sure you want to delete all currently downloaded tracks?",

View File

@@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { StyleSheet } from 'react-native'; import { StatusBar, StyleSheet } from 'react-native';
import { createStackNavigator } from '@react-navigation/stack'; import { createStackNavigator } from '@react-navigation/stack';
import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { t } from '@/localisation'; import { t } from '@/localisation';
import useDefaultStyles, { ColoredBlurView } from '@/components/Colors'; import useDefaultStyles, { ColoredBlurView, useUserOrSystemScheme } from '@/components/Colors';
import { StackParams } from '@/screens/types'; import { StackParams } from '@/screens/types';
import NowPlaying from './overlays/NowPlaying'; import NowPlaying from './overlays/NowPlaying';
@@ -14,14 +14,18 @@ import Playlists from './stacks/Playlists';
import Playlist from './stacks/Playlist'; import Playlist from './stacks/Playlist';
import Artists from './stacks/Artists'; import Artists from './stacks/Artists';
import Artist from './stacks/Artist'; import Artist from './stacks/Artist';
import { SafeAreaProvider } from 'react-native-safe-area-context';
const Stack = createStackNavigator<StackParams>(); const Stack = createStackNavigator<StackParams>();
function MusicStack() { function MusicStack() {
const defaultStyles = useDefaultStyles(); const defaultStyles = useDefaultStyles();
const scheme = useUserOrSystemScheme();
return ( return (
<SafeAreaProvider>
<GestureHandlerRootView style={{ flex: 1 }}> <GestureHandlerRootView style={{ flex: 1 }}>
<StatusBar backgroundColor="transparent" barStyle={scheme === 'dark' ? 'light-content' : 'dark-content'} />
<Stack.Navigator initialRouteName="RecentAlbums" screenOptions={{ <Stack.Navigator initialRouteName="RecentAlbums" screenOptions={{
headerTintColor: defaultStyles.themeColor.color, headerTintColor: defaultStyles.themeColor.color,
headerTitleStyle: defaultStyles.stackHeader, headerTitleStyle: defaultStyles.stackHeader,
@@ -39,6 +43,7 @@ function MusicStack() {
</Stack.Navigator> </Stack.Navigator>
<NowPlaying /> <NowPlaying />
</GestureHandlerRootView> </GestureHandlerRootView>
</SafeAreaProvider>
); );
} }

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { useGetImage } from '@/utility/JellyfinApi/lib'; import { useGetImage } from '@/utility/JellyfinApi/lib';
import { Text, SafeAreaView, StyleSheet } from 'react-native'; import { Text, StyleSheet, View } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { useAppDispatch, useTypedSelector } from '@/store'; import { useAppDispatch, useTypedSelector } from '@/store';
import { fetchRecentAlbums } from '@/store/music/actions'; import { fetchRecentAlbums } from '@/store/music/actions';
@@ -18,6 +18,7 @@ import styled from 'styled-components/native';
import { ShadowWrapper } from '@/components/Shadow'; import { ShadowWrapper } from '@/components/Shadow';
import { NavigationProp } from '@/screens/types'; import { NavigationProp } from '@/screens/types';
import { SafeFlatList } from '@/components/SafeNavigatorView'; import { SafeFlatList } from '@/components/SafeNavigatorView';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
columnWrapper: { columnWrapper: {
@@ -78,8 +79,14 @@ const RecentAlbums: React.FC = () => {
// Retrieve data on mount // Retrieve data on mount
useEffect(() => { retrieveData(); }, [retrieveData]); useEffect(() => { retrieveData(); }, [retrieveData]);
const insets = useSafeAreaInsets();
return ( return (
<SafeAreaView> <View
style={{
paddingTop: insets.top,
paddingBottom: 1 * insets.bottom,
}}>
<SafeFlatList <SafeFlatList
data={recentAlbums as string[]} data={recentAlbums as string[]}
refreshing={isLoading} refreshing={isLoading}
@@ -100,7 +107,7 @@ const RecentAlbums: React.FC = () => {
</TouchableHandler> </TouchableHandler>
)} )}
/> />
</SafeAreaView> </View>
); );
}; };

View File

@@ -8,6 +8,7 @@ import Album from '@/screens/Music/stacks/Album';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import NowPlaying from '@/screens/Music/overlays/NowPlaying'; import NowPlaying from '@/screens/Music/overlays/NowPlaying';
import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';
const Stack = createStackNavigator<StackParams>(); const Stack = createStackNavigator<StackParams>();
@@ -16,6 +17,7 @@ function SearchStack() {
const [isInitialRoute, setIsInitialRoute] = useState(true); const [isInitialRoute, setIsInitialRoute] = useState(true);
return ( return (
<SafeAreaProvider>
<GestureHandlerRootView style={{ flex: 1 }}> <GestureHandlerRootView style={{ flex: 1 }}>
<Stack.Navigator initialRouteName="Search" <Stack.Navigator initialRouteName="Search"
screenOptions={{ screenOptions={{
@@ -37,6 +39,7 @@ function SearchStack() {
</Stack.Navigator> </Stack.Navigator>
<NowPlaying offset={isInitialRoute ? 64 : 0} /> <NowPlaying offset={isInitialRoute ? 64 : 0} />
</GestureHandlerRootView> </GestureHandlerRootView>
</SafeAreaProvider>
); );
} }

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useEffect, useCallback, useMemo } from 'react';
import Input from '@/components/Input'; import Input from '@/components/Input';
import { ActivityIndicator, Animated, KeyboardAvoidingView, Platform, SafeAreaView, View } from 'react-native'; import { ActivityIndicator, Animated, KeyboardAvoidingView, Platform, View } from 'react-native';
import styled from 'styled-components/native'; import styled from 'styled-components/native';
import { useAppDispatch, useTypedSelector } from '@/store'; import { useAppDispatch, useTypedSelector } from '@/store';
import Fuse, { IFuseOptions } from 'fuse.js'; import Fuse, { IFuseOptions } from 'fuse.js';
@@ -21,6 +21,7 @@ import { ShadowWrapper } from '@/components/Shadow';
import { NavigationProp } from '@/screens/types'; import { NavigationProp } from '@/screens/types';
import { useNavigationOffsets } from '@/components/SafeNavigatorView'; import { useNavigationOffsets } from '@/components/SafeNavigatorView';
import BaseAlbumImage from '@/screens/Music/stacks/components/AlbumImage'; import BaseAlbumImage from '@/screens/Music/stacks/components/AlbumImage';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
// import MicrophoneIcon from '@/assets/icons/microphone.svg'; // import MicrophoneIcon from '@/assets/icons/microphone.svg';
// import AlbumIcon from '@/assets/icons/collection.svg'; // import AlbumIcon from '@/assets/icons/collection.svg';
// import TrackIcon from '@/assets/icons/note.svg'; // import TrackIcon from '@/assets/icons/note.svg';
@@ -31,7 +32,8 @@ import BaseAlbumImage from '@/screens/Music/stacks/components/AlbumImage';
const KEYBOARD_OFFSET = Platform.select({ const KEYBOARD_OFFSET = Platform.select({
ios: 0, ios: 0,
android: 72, // Android 15+ has edge-to-edge support, changing the keyboard offset to 0
android: Number.parseInt(Platform.Version as string) >= 35 ? 0 : 72,
}); });
const SEARCH_INPUT_HEIGHT = 62; const SEARCH_INPUT_HEIGHT = 62;
@@ -266,8 +268,10 @@ export default function Search() {
...jellyfinResults, ...jellyfinResults,
]), [fuseResults, jellyfinResults]); ]), [fuseResults, jellyfinResults]);
const insets = useSafeAreaInsets();
return ( return (
<SafeAreaView style={{ flex: 1, marginBottom: offsets.bottom }}> <View style={{ flex: 1, paddingTop: insets.top, marginBottom: offsets.bottom }}>
<KeyboardAvoidingView behavior="height" style={{ flex: 1 }} keyboardVerticalOffset={KEYBOARD_OFFSET}> <KeyboardAvoidingView behavior="height" style={{ flex: 1 }} keyboardVerticalOffset={KEYBOARD_OFFSET}>
<FlatList <FlatList
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
@@ -323,6 +327,6 @@ export default function Search() {
) : null} ) : null}
{SearchInput} {SearchInput}
</KeyboardAvoidingView> </KeyboardAvoidingView>
</SafeAreaView> </View>
); );
} }

View File

@@ -2,7 +2,6 @@ import { Paragraph } from '@/components/Typography';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { Switch } from 'react-native-gesture-handler'; import { Switch } from 'react-native-gesture-handler';
import { t } from '@/localisation'; import { t } from '@/localisation';
import { SafeScrollView } from '@/components/SafeNavigatorView';
import { useAppDispatch, useTypedSelector } from '@/store'; import { useAppDispatch, useTypedSelector } from '@/store';
import { setEnablePlaybackReporting } from '@/store/settings/actions'; import { setEnablePlaybackReporting } from '@/store/settings/actions';
import Container from '../components/Container'; import Container from '../components/Container';
@@ -17,7 +16,6 @@ export default function PlaybackReporting() {
}, [isEnabled, dispatch]); }, [isEnabled, dispatch]);
return ( return (
<SafeScrollView>
<Container> <Container>
<Paragraph>{t('playback-reporting-description')}</Paragraph> <Paragraph>{t('playback-reporting-description')}</Paragraph>
<SwitchContainer> <SwitchContainer>
@@ -25,6 +23,5 @@ export default function PlaybackReporting() {
<Switch value={isEnabled} onValueChange={toggleSwitch} /> <Switch value={isEnabled} onValueChange={toggleSwitch} />
</SwitchContainer> </SwitchContainer>
</Container> </Container>
</SafeScrollView>
); );
} }

View File

@@ -98,7 +98,7 @@ export default function Routes() {
}} id="MAIN"> }} id="MAIN">
<Stack.Screen name="Screens" component={Screens} /> <Stack.Screen name="Screens" component={Screens} />
<Stack.Screen name="SetJellyfinServer" component={SetJellyfinServer} /> <Stack.Screen name="SetJellyfinServer" component={SetJellyfinServer} />
<Stack.Screen name="TrackPopupMenu" component={TrackPopupMenu} options={{ presentation: 'formSheet' }} /> <Stack.Screen name="TrackPopupMenu" component={TrackPopupMenu} options={{ presentation: 'formSheet', sheetCornerRadius: 10, sheetAllowedDetents: [0.85, 1.0]}} />
<Stack.Screen name="ErrorReporting" component={ErrorReportingPopup} /> <Stack.Screen name="ErrorReporting" component={ErrorReportingPopup} />
<Stack.Screen name="Player" component={Player} /> <Stack.Screen name="Player" component={Player} />
<Stack.Screen name="Lyrics" component={Lyrics} /> <Stack.Screen name="Lyrics" component={Lyrics} />

View File

@@ -1,11 +1,11 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import XmarkIcon from '@/assets/icons/xmark.svg'; import XmarkIcon from '@/assets/icons/xmark.svg';
import { TouchableOpacity } from 'react-native';
import styled from 'styled-components/native'; import styled from 'styled-components/native';
const Container = styled.View` const Container = styled.TouchableOpacity`
padding: 6px 12px; padding: 12px 0px;
z-index: 2;
`; `;
function BackButton() { function BackButton() {
@@ -16,10 +16,8 @@ function BackButton() {
}, [navigation]); }, [navigation]);
return ( return (
<Container> <Container onPress={handlePress}>
<TouchableOpacity onPress={handlePress}>
<XmarkIcon /> <XmarkIcon />
</TouchableOpacity>
</Container> </Container>
); );
} }

View File

@@ -23,9 +23,11 @@ export default function Player() {
return ( return (
<GestureHandlerRootView style={{ flex: 1 }}> <GestureHandlerRootView style={{ flex: 1 }}>
<ColoredBlurView> <ColoredBlurView>
{Platform.OS === 'android' && (<BackButton />)}
<Queue header={( <Queue header={(
<> <>
{Platform.OS === 'android' && (
<BackButton />
)}
<NowPlaying /> <NowPlaying />
<ConnectionNotice /> <ConnectionNotice />
<StreamStatus /> <StreamStatus />

View File

@@ -75,7 +75,7 @@ function TrackPopupMenu() {
}, [trackId, dispatch, closeModal]); }, [trackId, dispatch, closeModal]);
return ( return (
<ColoredBlurView> <ColoredBlurView style={{flex: 1}}>
<Container> <Container>
<Artwork src={getImage(track)} /> <Artwork src={getImage(track)} />
<Header>{track?.Name}</Header> <Header>{track?.Name}</Header>