fix: search container height when keyboard is open
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import Input from '@/components/Input';
|
import Input from '@/components/Input';
|
||||||
import { ActivityIndicator, Animated, SafeAreaView, View } from 'react-native';
|
import { ActivityIndicator, Animated, KeyboardAvoidingView, SafeAreaView, 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';
|
||||||
@@ -18,7 +18,6 @@ import DownloadIcon from '@/components/DownloadIcon';
|
|||||||
import ChevronRight from '@/assets/icons/chevron-right.svg';
|
import ChevronRight from '@/assets/icons/chevron-right.svg';
|
||||||
import SearchIcon from '@/assets/icons/magnifying-glass.svg';
|
import SearchIcon from '@/assets/icons/magnifying-glass.svg';
|
||||||
import { ShadowWrapper } from '@/components/Shadow';
|
import { ShadowWrapper } from '@/components/Shadow';
|
||||||
import { useKeyboardHeight } from '@/utility/useKeyboardHeight';
|
|
||||||
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';
|
||||||
@@ -101,17 +100,14 @@ export default function Search() {
|
|||||||
const offsets = useNavigationOffsets({ includeOverlay: false });
|
const offsets = useNavigationOffsets({ includeOverlay: false });
|
||||||
|
|
||||||
// Prepare state for fuse and albums
|
// Prepare state for fuse and albums
|
||||||
const [fuseIsReady, setFuseReady] = useState(false);
|
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [isLoading, setLoading] = useState(false);
|
const [isLoading, setLoading] = useState(false);
|
||||||
const [fuseResults, setFuseResults] = useState<CombinedResults>([]);
|
const [fuseResults, setFuseResults] = useState<CombinedResults>([]);
|
||||||
const [jellyfinResults, setJellyfinResults] = useState<CombinedResults>([]);
|
const [jellyfinResults, setJellyfinResults] = useState<CombinedResults>([]);
|
||||||
const albums = useTypedSelector(state => state.music.albums.entities);
|
const albums = useTypedSelector(state => state.music.albums.entities);
|
||||||
const fuse = useRef<Fuse<Album>>();
|
|
||||||
|
|
||||||
// Prepare helpers
|
// Prepare helpers
|
||||||
const navigation = useNavigation<NavigationProp>();
|
const navigation = useNavigation<NavigationProp>();
|
||||||
const keyboardHeight = useKeyboardHeight();
|
|
||||||
const getImage = useGetImage();
|
const getImage = useGetImage();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@@ -122,10 +118,9 @@ export default function Search() {
|
|||||||
* more intelligently by removing and adding the changed albums, but this is
|
* more intelligently by removing and adding the changed albums, but this is
|
||||||
* an open todo.
|
* an open todo.
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
const fuse = useMemo(() => (
|
||||||
fuse.current = new Fuse(Object.values(albums) as Album[], fuseOptions);
|
new Fuse(Object.values(albums) as Album[], fuseOptions)
|
||||||
setFuseReady(true);
|
), [albums]);
|
||||||
}, [albums, setFuseReady]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function retrieves search results from Jellyfin. It is a seperate
|
* This function retrieves search results from Jellyfin. It is a seperate
|
||||||
@@ -175,14 +170,8 @@ export default function Search() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const retrieveResults = async () => {
|
const retrieveResults = async () => {
|
||||||
// GUARD: In some extraordinary cases, Fuse might not be presented since
|
|
||||||
// it is assigned via refs. In this case, we can't handle any searching.
|
|
||||||
if (!fuse.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// First set the immediate results from fuse
|
// First set the immediate results from fuse
|
||||||
const fuseResults = fuse.current.search(searchTerm);
|
const fuseResults = fuse.search(searchTerm);
|
||||||
const albums: AlbumResult[] = fuseResults
|
const albums: AlbumResult[] = fuseResults
|
||||||
.map(({ item }) => ({
|
.map(({ item }) => ({
|
||||||
id: item.Id,
|
id: item.Id,
|
||||||
@@ -214,10 +203,7 @@ export default function Search() {
|
|||||||
}, [navigation, albums]);
|
}, [navigation, albums]);
|
||||||
|
|
||||||
const SearchInput = React.useMemo(() => (
|
const SearchInput = React.useMemo(() => (
|
||||||
<Animated.View style={[
|
<Animated.View>
|
||||||
{ position: 'absolute', bottom: offsets.bottom, right: 0, left: 0 },
|
|
||||||
{ transform: [{ translateY: keyboardHeight }] },
|
|
||||||
]}>
|
|
||||||
<ColoredBlurView>
|
<ColoredBlurView>
|
||||||
<Container style={[ defaultStyles.border ]}>
|
<Container style={[ defaultStyles.border ]}>
|
||||||
<View>
|
<View>
|
||||||
@@ -269,69 +255,70 @@ export default function Search() {
|
|||||||
</ScrollView> */}
|
</ScrollView> */}
|
||||||
</ColoredBlurView>
|
</ColoredBlurView>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
), [searchTerm, setSearchTerm, defaultStyles, isLoading, keyboardHeight, offsets]);
|
), [searchTerm, setSearchTerm, defaultStyles, isLoading]);
|
||||||
|
|
||||||
// GUARD: We cannot search for stuff unless Fuse is loaded with results.
|
const searchResults = useMemo(() => ([
|
||||||
// Therefore we delay rendering to when we are certain it's there.
|
...fuseResults,
|
||||||
if (!fuseIsReady) {
|
...jellyfinResults,
|
||||||
return null;
|
]), [fuseResults, jellyfinResults]);
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1 }}>
|
<SafeAreaView style={{ flex: 1, marginBottom: offsets.bottom }}>
|
||||||
<FlatList
|
<KeyboardAvoidingView behavior="height" style={{ flex: 1 }}>
|
||||||
keyboardShouldPersistTaps="handled"
|
<FlatList
|
||||||
style={{ flex: 2, }}
|
keyboardShouldPersistTaps="handled"
|
||||||
contentContainerStyle={{ paddingTop: offsets.top, paddingBottom: offsets.bottom + SEARCH_INPUT_HEIGHT }}
|
style={{ flex: 2, }}
|
||||||
scrollIndicatorInsets={{ top: offsets.top / 2, bottom: offsets.bottom / 2 + 10 + SEARCH_INPUT_HEIGHT }}
|
contentContainerStyle={{ paddingTop: offsets.top, paddingBottom: SEARCH_INPUT_HEIGHT }}
|
||||||
data={[...jellyfinResults, ...fuseResults]}
|
scrollIndicatorInsets={{ top: offsets.top / 2, bottom: offsets.bottom / 2 + 10 + SEARCH_INPUT_HEIGHT }}
|
||||||
renderItem={({ item: { id, type, album: trackAlbum, name: trackName } }: { item: AlbumResult | AudioResult }) => {
|
data={searchResults}
|
||||||
const album = albums[trackAlbum || id];
|
renderItem={({ item: { id, type, album: trackAlbum, name: trackName } }: { item: AlbumResult | AudioResult }) => {
|
||||||
|
const album = albums[trackAlbum || id];
|
||||||
|
|
||||||
// GUARD: If the album cannot be found in the store, we
|
// GUARD: If the album cannot be found in the store, we
|
||||||
// cannot display it.
|
// cannot display it.
|
||||||
if (!album) {
|
if (!album) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableHandler<string> id={album.Id} onPress={selectAlbum} testID={`search-result-${album.Id}`}>
|
<TouchableHandler<string> id={album.Id} onPress={selectAlbum} testID={`search-result-${album.Id}`}>
|
||||||
<SearchResult>
|
<SearchResult>
|
||||||
<ShadowWrapper>
|
<ShadowWrapper>
|
||||||
<AlbumImage source={{ uri: getImage(album) }} style={defaultStyles.imageBackground} />
|
<AlbumImage source={{ uri: getImage(album) }} style={defaultStyles.imageBackground} />
|
||||||
</ShadowWrapper>
|
</ShadowWrapper>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<Text numberOfLines={1}>
|
<Text numberOfLines={1}>
|
||||||
{trackName || album.Name}
|
{trackName || album.Name}
|
||||||
</Text>
|
</Text>
|
||||||
{(album.AlbumArtist || album.Name) && (
|
{(album.AlbumArtist || album.Name) && (
|
||||||
<HalfOpacity style={defaultStyles.text} numberOfLines={1}>
|
<HalfOpacity style={defaultStyles.text} numberOfLines={1}>
|
||||||
{type === 'AlbumArtist'
|
{type === 'AlbumArtist'
|
||||||
? `${t('album')} • ${album.AlbumArtist}`
|
? `${t('album')} • ${album.AlbumArtist}`
|
||||||
: `${t('track')} • ${album.AlbumArtist} — ${album.Name}`
|
: `${t('track')} • ${album.AlbumArtist} — ${album.Name}`
|
||||||
}
|
}
|
||||||
</HalfOpacity>
|
</HalfOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={{ marginLeft: 16 }}>
|
<View style={{ marginLeft: 16 }}>
|
||||||
<DownloadIcon trackId={id} />
|
<DownloadIcon trackId={id} />
|
||||||
</View>
|
</View>
|
||||||
<View style={{ marginLeft: 16 }}>
|
<View style={{ marginLeft: 16 }}>
|
||||||
<ChevronRight width={14} height={14} fill={defaultStyles.textQuarterOpacity.color} />
|
<ChevronRight width={14} height={14} fill={defaultStyles.textQuarterOpacity.color} />
|
||||||
</View>
|
</View>
|
||||||
</SearchResult>
|
</SearchResult>
|
||||||
</TouchableHandler>
|
</TouchableHandler>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
extraData={[searchTerm, albums]}
|
extraData={[searchTerm, albums]}
|
||||||
/>
|
/>
|
||||||
{(searchTerm.length && !jellyfinResults.length && !fuseResults.length && !isLoading) ? (
|
{(searchTerm.length && !jellyfinResults.length && !fuseResults.length && !isLoading) ? (
|
||||||
<FullSizeContainer>
|
<FullSizeContainer>
|
||||||
<Text style={{ textAlign: 'center', opacity: 0.5, fontSize: 18 }}>{t('no-results')}</Text>
|
<Text style={{ textAlign: 'center', opacity: 0.5, fontSize: 18 }}>{t('no-results')}</Text>
|
||||||
</FullSizeContainer>
|
</FullSizeContainer>
|
||||||
) : null}
|
) : null}
|
||||||
{SearchInput}
|
{SearchInput}
|
||||||
|
</KeyboardAvoidingView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user