From c8283fc5803abcd24efb71f1832e0a524e1a36f0 Mon Sep 17 00:00:00 2001 From: Lei Nelissen Date: Sun, 23 Apr 2023 23:31:35 +0200 Subject: [PATCH] feat: finish offsets on new navigation views --- src/components/SafeNavigatorView.tsx | 100 ++++++++++ src/screens/Downloads/index.tsx | 6 +- src/screens/Music/stacks/Albums.tsx | 8 +- src/screens/Music/stacks/Playlists.tsx | 9 +- src/screens/Music/stacks/RecentAlbums.tsx | 5 +- .../Music/stacks/components/TrackListView.tsx | 176 +++++++++--------- src/screens/Search/stacks/Search/index.tsx | 18 +- src/screens/Settings/components/Cache.tsx | 8 +- src/screens/Settings/components/Library.tsx | 8 +- src/screens/Settings/components/Sentry.tsx | 8 +- src/screens/Settings/index.tsx | 9 +- src/utility/SafeNavigatorView.tsx | 24 --- 12 files changed, 227 insertions(+), 152 deletions(-) create mode 100644 src/components/SafeNavigatorView.tsx delete mode 100644 src/utility/SafeNavigatorView.tsx diff --git a/src/components/SafeNavigatorView.tsx b/src/components/SafeNavigatorView.tsx new file mode 100644 index 0000000..6e918a7 --- /dev/null +++ b/src/components/SafeNavigatorView.tsx @@ -0,0 +1,100 @@ +import React, { ForwardedRef, Ref, forwardRef } from 'react'; +import { useHeaderHeight } from '@react-navigation/elements'; +import { FlatList, FlatListProps, ScrollView, ScrollViewProps, SectionList, SectionListProps } from 'react-native'; +import useCurrentTrack from '../utility/useCurrentTrack'; +import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; + +declare module 'react' { + function forwardRef( + render: (props: P, ref: React.Ref) => React.ReactElement | null + ): (props: P & React.RefAttributes) => React.ReactElement | null; +} + +/** + * A wrapper for ScrollView that takes any paddings, margins and insets into + * account that result from the bottom tabs, potential NowPlaying overlay and header. + */ +export function SafeScrollView({ + contentContainerStyle, + ...props +}: ScrollViewProps) { + const { top, bottom } = useNavigationOffsets(); + + return ( + + ); +} + +/** + * A wrapper for ScrollView that takes any paddings, margins and insets into + * account that result from the bottom tabs, potential NowPlaying overlay and header. + */ +function BareSafeSectionList({ + contentContainerStyle, + ...props +}: SectionListProps, ref: ForwardedRef>) { + const { top, bottom } = useNavigationOffsets(); + + return ( + + ); +} +export const SafeSectionList = forwardRef(BareSafeSectionList); + +/** + * A wrapper for ScrollView that takes any paddings, margins and insets into + * account that result from the bottom tabs, potential NowPlaying overlay and header. + */ +function BareSafeFlatList({ + contentContainerStyle, + ...props +}: FlatListProps, ref: ForwardedRef>) { + const { top, bottom } = useNavigationOffsets(); + + return ( + + ); +} + +export const SafeFlatList = forwardRef(BareSafeFlatList); + +/** + * A hook that returns the correct offset that should be applied to any Views + * that are wrapped in a NavigationView, in order to account for overlays, + * headers and bottom tabs. + */ +export function useNavigationOffsets({ includeOverlay = true } = {} as { includeOverlay?: boolean }) { + const headerHeight = useHeaderHeight(); + const bottomBarHeight = useBottomTabBarHeight(); + const { track } = useCurrentTrack(); + + return { + top: headerHeight, + bottom: (track && includeOverlay ? 68 : 0) + bottomBarHeight || 0, + }; +} + + diff --git a/src/screens/Downloads/index.tsx b/src/screens/Downloads/index.tsx index f899b4c..eb39df8 100644 --- a/src/screens/Downloads/index.tsx +++ b/src/screens/Downloads/index.tsx @@ -1,7 +1,6 @@ import useDefaultStyles from 'components/Colors'; import React, { useCallback, useMemo } from 'react'; import { FlatListProps, View } from 'react-native'; -import { FlatList } from 'react-native-gesture-handler'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useAppDispatch, useTypedSelector } from 'store'; import formatBytes from 'utility/formatBytes'; @@ -17,6 +16,7 @@ import { Text } from 'components/Typography'; import FastImage from 'react-native-fast-image'; import { useGetImage } from 'utility/JellyfinApi'; import { ShadowWrapper } from 'components/Shadow'; +import { SafeFlatList } from 'components/SafeNavigatorView'; const DownloadedTrack = styled.View` flex: 1 0 auto; @@ -151,10 +151,10 @@ function Downloads() { return ( {ListHeaderComponent} - diff --git a/src/screens/Music/stacks/Albums.tsx b/src/screens/Music/stacks/Albums.tsx index a83b76e..3be8623 100644 --- a/src/screens/Music/stacks/Albums.tsx +++ b/src/screens/Music/stacks/Albums.tsx @@ -17,7 +17,7 @@ import { Album } from 'store/music/types'; import { Text } from 'components/Typography'; import { ShadowWrapper } from 'components/Shadow'; import { NavigationProp } from 'screens/types'; -import { useNavigatorPadding } from 'utility/SafeNavigatorView'; +import { SafeSectionList } from 'components/SafeNavigatorView'; const HeadingHeight = 50; @@ -80,8 +80,6 @@ const GeneratedAlbumItem = React.memo(function GeneratedAlbumItem(props: Generat }); const Albums: React.FC = () => { - const navigatorPadding = useNavigatorPadding(); - // Retrieve data from store const { entities: albums } = useTypedSelector((state) => state.music.albums); const isLoading = useTypedSelector((state) => state.music.albums.isLoading); @@ -173,9 +171,7 @@ const Albums: React.FC = () => { return ( <> - { - const navigatorPadding = useNavigatorPadding(); + const offsets = useNavigationOffsets(); // Retrieve data from store const { entities, ids } = useTypedSelector((state) => state.music.playlists); @@ -96,11 +96,10 @@ const Playlists: React.FC = () => { }); return ( - + } - contentContainerStyle={navigatorPadding} data={ids} getItemLayout={getItemLayout} ref={listRef} diff --git a/src/screens/Music/stacks/RecentAlbums.tsx b/src/screens/Music/stacks/RecentAlbums.tsx index d25580b..3ba1d25 100644 --- a/src/screens/Music/stacks/RecentAlbums.tsx +++ b/src/screens/Music/stacks/RecentAlbums.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect } from 'react'; import { useGetImage } from 'utility/JellyfinApi'; -import { Text, SafeAreaView, FlatList, StyleSheet } from 'react-native'; +import { Text, SafeAreaView, StyleSheet } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { useAppDispatch, useTypedSelector } from 'store'; import { fetchRecentAlbums } from 'store/music/actions'; @@ -17,6 +17,7 @@ import Divider from 'components/Divider'; import styled from 'styled-components/native'; import { ShadowWrapper } from 'components/Shadow'; import { NavigationProp } from 'screens/types'; +import { SafeFlatList } from 'components/SafeNavigatorView'; const styles = StyleSheet.create({ columnWrapper: { @@ -71,7 +72,7 @@ const RecentAlbums: React.FC = () => { return ( - ` +const TrackContainer = styled.View<{ isPlaying: boolean, small?: boolean }>` padding: 12px 4px; flex-direction: row; border-radius: 6px; @@ -54,6 +54,10 @@ const TrackContainer = styled.View<{ isPlaying: boolean }>` margin: 0 -12px; padding: 12px 16px; `} + + ${props => props.small && css` + padding: ${Platform.select({ ios: '8px 4px', android: '4px'})}; + `} `; export interface TrackListViewProps extends PropsWithChildren<{}> { @@ -85,7 +89,7 @@ const TrackListView: React.FC = ({ children }) => { const defaultStyles = useDefaultStyles(); - const navigatorPadding = useNavigatorPadding(); + const offsets = useNavigationOffsets(); // Retrieve state const tracks = useTypedSelector((state) => state.music.tracks.entities); @@ -123,107 +127,109 @@ const TrackListView: React.FC = ({ }, [dispatch, downloadedTracks]); return ( - + } > - - - -
{title}
- {artist} - - - - - - {trackIds.map((trackId, i) => - - + + + +
{title}
+ {artist} + + + + + + {trackIds.map((trackId, i) => + - - {listNumberingStyle === 'index' - ? i + 1 - : tracks[trackId]?.IndexNumber} - - - {tracks[trackId]?.Name} + {listNumberingStyle === 'index' + ? i + 1 + : tracks[trackId]?.IndexNumber} - {itemDisplayStyle === 'playlist' && ( + - {tracks[trackId]?.Artists.join(', ')} + {tracks[trackId]?.Name} - )} - - - - {ticksToDuration(tracks[trackId]?.RunTimeTicks || 0)} - - - -
-
- )} - {t('total-duration')}{': '}{ticksToDuration(totalDuration)} - - - - + {itemDisplayStyle === 'playlist' && ( + + {tracks[trackId]?.Artists.join(', ')} + + )} +
+ + + {ticksToDuration(tracks[trackId]?.RunTimeTicks || 0)} + + + + + + )} + {t('total-duration')}{': '}{ticksToDuration(totalDuration)} + + + + + + {children} - {children} -
+ ); }; diff --git a/src/screens/Search/stacks/Search/index.tsx b/src/screens/Search/stacks/Search/index.tsx index 2723d0a..9a83ca2 100644 --- a/src/screens/Search/stacks/Search/index.tsx +++ b/src/screens/Search/stacks/Search/index.tsx @@ -20,8 +20,8 @@ import ChevronRight from 'assets/icons/chevron-right.svg'; import SearchIcon from 'assets/icons/magnifying-glass.svg'; import { ShadowWrapper } from 'components/Shadow'; import { useKeyboardHeight } from 'utility/useKeyboardHeight'; -import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { NavigationProp } from 'screens/types'; +import { useNavigationOffsets } from 'components/SafeNavigatorView'; // import MicrophoneIcon from 'assets/icons/microphone.svg'; // import AlbumIcon from 'assets/icons/collection.svg'; // import TrackIcon from 'assets/icons/note.svg'; @@ -30,6 +30,8 @@ import { NavigationProp } from 'screens/types'; // import LocalIcon from 'assets/icons/internal-drive.svg'; // import SelectableFilter from './components/SelectableFilter'; +const SEARCH_INPUT_HEIGHT = 62; + const Container = styled(View)` padding: 4px 24px 0 24px; margin-bottom: 0px; @@ -96,7 +98,7 @@ type CombinedResults = (AudioResult | AlbumResult)[]; export default function Search() { const defaultStyles = useDefaultStyles(); - const tabBarHeight = useBottomTabBarHeight(); + const offsets = useNavigationOffsets({ includeOverlay: false }); // Prepare state for fuse and albums const [fuseIsReady, setFuseReady] = useState(false); @@ -211,9 +213,9 @@ export default function Search() { navigation.navigate('Album', { id, album: albums[id] as Album }); }, [navigation, albums]); - const HeaderComponent = React.useMemo(() => ( + const SearchInput = React.useMemo(() => ( @@ -266,7 +268,7 @@ export default function Search() {
*/} - ), [searchTerm, setSearchTerm, defaultStyles, isLoading, keyboardHeight, tabBarHeight]); + ), [searchTerm, setSearchTerm, defaultStyles, isLoading, keyboardHeight, offsets]); // GUARD: We cannot search for stuff unless Fuse is loaded with results. // Therefore we delay rendering to when we are certain it's there. @@ -277,7 +279,9 @@ export default function Search() { return ( { const album = albums[trackAlbum || id]; @@ -325,7 +329,7 @@ export default function Search() { {t('no-results')} ) : null} - {HeaderComponent} + {SearchInput} ); } \ No newline at end of file diff --git a/src/screens/Settings/components/Cache.tsx b/src/screens/Settings/components/Cache.tsx index 9231d38..efce9e7 100644 --- a/src/screens/Settings/components/Cache.tsx +++ b/src/screens/Settings/components/Cache.tsx @@ -6,19 +6,17 @@ import Button from 'components/Button'; import styled from 'styled-components/native'; import { Paragraph } from 'components/Typography'; import { useAppDispatch } from 'store'; -import { useHeaderHeight } from '@react-navigation/elements'; - +import { SafeScrollView } from 'components/SafeNavigatorView'; const ClearCache = styled(Button)` margin-top: 16px; `; -const Container = styled.ScrollView` +const Container = styled(SafeScrollView)` padding: 24px; `; export default function CacheSettings() { - const headerHeight = useHeaderHeight(); const dispatch = useAppDispatch(); const handleClearCache = useCallback(() => { // Dispatch an action to reset the music subreducer state @@ -29,7 +27,7 @@ export default function CacheSettings() { }, [dispatch]); return ( - + {t('setting-cache-description')} diff --git a/src/screens/Settings/components/Library.tsx b/src/screens/Settings/components/Library.tsx index 7c58042..fdd7274 100644 --- a/src/screens/Settings/components/Library.tsx +++ b/src/screens/Settings/components/Library.tsx @@ -7,8 +7,7 @@ import { useTypedSelector } from 'store'; import { t } from '@localisation'; import Button from 'components/Button'; import { Paragraph } from 'components/Typography'; -import { useHeaderHeight } from '@react-navigation/elements'; - +import { SafeScrollView } from 'components/SafeNavigatorView'; const InputContainer = styled.View` margin: 10px 0; @@ -20,19 +19,18 @@ const Input = styled.TextInput` border-radius: 5px; `; -const Container = styled.ScrollView` +const Container = styled(SafeScrollView)` padding: 24px; `; export default function LibrarySettings() { const defaultStyles = useDefaultStyles(); - const headerHeight = useHeaderHeight(); const { jellyfin } = useTypedSelector(state => state.settings); const navigation = useNavigation(); const handleSetLibrary = useCallback(() => navigation.navigate('SetJellyfinServer'), [navigation]); return ( - + {t('jellyfin-server-url')} diff --git a/src/screens/Settings/components/Sentry.tsx b/src/screens/Settings/components/Sentry.tsx index 5d979d8..5e28fc6 100644 --- a/src/screens/Settings/components/Sentry.tsx +++ b/src/screens/Settings/components/Sentry.tsx @@ -9,8 +9,7 @@ import ChevronIcon from 'assets/icons/chevron-right.svg'; import { THEME_COLOR } from 'CONSTANTS'; import useDefaultStyles, { DefaultStylesProvider } from 'components/Colors'; import { t } from '@localisation'; -import { ScrollView } from 'react-native'; -import { useHeaderHeight } from '@react-navigation/elements'; +import { SafeScrollView } from 'components/SafeNavigatorView'; const Container = styled.View` padding: 24px; @@ -99,7 +98,6 @@ function renderContent(question: Question) { export default function Sentry() { const defaultStyles = useDefaultStyles(); - const headerHeight = useHeaderHeight(); const [isReportingEnabled, setReporting] = useState(isSentryEnabled); const [activeSections, setActiveSections] = useState([]); @@ -110,7 +108,7 @@ export default function Sentry() { }); return ( - + {t('error-reporting-description')} @@ -129,6 +127,6 @@ export default function Sentry() { onChange={setActiveSections} underlayColor={defaultStyles.activeBackground.backgroundColor} /> - + ); } \ No newline at end of file diff --git a/src/screens/Settings/index.tsx b/src/screens/Settings/index.tsx index a64ca65..11e6062 100644 --- a/src/screens/Settings/index.tsx +++ b/src/screens/Settings/index.tsx @@ -1,5 +1,5 @@ import React, { useCallback } from 'react'; -import { ScrollView, StyleSheet } from 'react-native'; +import { StyleSheet } from 'react-native'; import Library from './components/Library'; import Cache from './components/Cache'; import useDefaultStyles, { ColoredBlurView } from 'components/Colors'; @@ -10,21 +10,20 @@ import ListButton from 'components/ListButton'; import { THEME_COLOR } from 'CONSTANTS'; import Sentry from './components/Sentry'; import { SettingsNavigationProp } from './types'; -import { useHeaderHeight } from '@react-navigation/elements'; +import { SafeScrollView } from 'components/SafeNavigatorView'; export function SettingsList() { - const headerHeight = useHeaderHeight(); const navigation = useNavigation(); const handleLibraryClick = useCallback(() => { navigation.navigate('Library'); }, [navigation]); const handleCacheClick = useCallback(() => { navigation.navigate('Cache'); }, [navigation]); const handleSentryClick = useCallback(() => { navigation.navigate('Sentry'); }, [navigation]); return ( - + {t('jellyfin-library')} {t('setting-cache')} {t('error-reporting')} - + ); } diff --git a/src/utility/SafeNavigatorView.tsx b/src/utility/SafeNavigatorView.tsx deleted file mode 100644 index 026207e..0000000 --- a/src/utility/SafeNavigatorView.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { useHeaderHeight } from '@react-navigation/elements'; -import React from 'react'; -import { View, ViewProps } from 'react-native'; -import useCurrentTrack from './useCurrentTrack'; - -export function useNavigatorPadding() { - const headerHeight = useHeaderHeight(); - const { index } = useCurrentTrack(); - - return { - paddingTop: headerHeight, - paddingBottom: index !== undefined ? 68 : 0 - }; -} - -function SafeNavigatorView({ style, ...props }: ViewProps) { - const headerHeight = useHeaderHeight(); - - return ( - - ); -} - -export default SafeNavigatorView;