(1) Automatically focus on text field in search
(2) Restyle no results message
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback, Ref } from 'react';
|
||||||
import Input from 'components/Input';
|
import Input from 'components/Input';
|
||||||
import { ActivityIndicator, Text, View } from 'react-native';
|
import { ActivityIndicator, Text, TextInput, 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 from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
@@ -21,6 +21,10 @@ const Container = styled.View`
|
|||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const FullSizeContainer = styled(Container)`
|
||||||
|
flex: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
const Loading = styled.View`
|
const Loading = styled.View`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 32px;
|
right: 32px;
|
||||||
@@ -80,12 +84,14 @@ export default function Search() {
|
|||||||
// Prepare state
|
// Prepare state
|
||||||
const [fuseIsReady, setFuseReady] = useState(false);
|
const [fuseIsReady, setFuseReady] = useState(false);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const albums = useTypedSelector(state => state.music.albums.entities);
|
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 [isLoading, setLoading] = useState(false);
|
const albums = useTypedSelector(state => state.music.albums.entities);
|
||||||
|
|
||||||
const fuse = useRef<Fuse<Album>>();
|
const fuse = useRef<Fuse<Album>>();
|
||||||
|
const searchElement = useRef<TextInput>(null);
|
||||||
|
|
||||||
// Prepare helpers
|
// Prepare helpers
|
||||||
const navigation = useNavigation<NavigationProp>();
|
const navigation = useNavigation<NavigationProp>();
|
||||||
@@ -185,6 +191,13 @@ export default function Search() {
|
|||||||
retrieveResults();
|
retrieveResults();
|
||||||
}, [searchTerm, setFuseResults, setLoading, fuse, fetchJellyfinResults]);
|
}, [searchTerm, setFuseResults, setLoading, fuse, fetchJellyfinResults]);
|
||||||
|
|
||||||
|
// Automatically focus on the text input on mount
|
||||||
|
useEffect(() => {
|
||||||
|
// Give the timeout a slight delay so the component has a chance to actually
|
||||||
|
// render the text input field.
|
||||||
|
setTimeout(() => searchElement.current?.focus(), 10);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
const selectAlbum = useCallback((id: string) =>
|
const selectAlbum = useCallback((id: string) =>
|
||||||
navigation.navigate('Album', { id, album: albums[id] as Album }), [navigation, albums]
|
navigation.navigate('Album', { id, album: albums[id] as Album }), [navigation, albums]
|
||||||
@@ -192,18 +205,25 @@ export default function Search() {
|
|||||||
|
|
||||||
const HeaderComponent = React.useMemo(() => (
|
const HeaderComponent = React.useMemo(() => (
|
||||||
<Container>
|
<Container>
|
||||||
<Input value={searchTerm} onChangeText={setSearchTerm} style={defaultStyles.input} placeholder={t('search') + '...'} />
|
<Input
|
||||||
|
// @ts-expect-error Ref typing shenanigans
|
||||||
|
ref={searchElement}
|
||||||
|
value={searchTerm}
|
||||||
|
onChangeText={setSearchTerm}
|
||||||
|
style={defaultStyles.input}
|
||||||
|
placeholder={t('search') + '...'}
|
||||||
|
/>
|
||||||
{isLoading && <Loading><ActivityIndicator /></Loading>}
|
{isLoading && <Loading><ActivityIndicator /></Loading>}
|
||||||
</Container>
|
</Container>
|
||||||
), [searchTerm, setSearchTerm, defaultStyles, isLoading]);
|
), [searchTerm, setSearchTerm, defaultStyles, isLoading]);
|
||||||
|
|
||||||
const FooterComponent = React.useMemo(() => (
|
// const FooterComponent = React.useMemo(() => (
|
||||||
<Container>
|
// <FullSizeContainer>
|
||||||
{(searchTerm.length && !jellyfinResults.length && !fuseResults.length && !isLoading)
|
// {(searchTerm.length && !jellyfinResults.length && !fuseResults.length && !isLoading)
|
||||||
? <Text style={{ textAlign: 'center' }}>{t('no-results')}</Text>
|
// ? <Text style={{ textAlign: 'center', opacity: 0.5 }}>{t('no-results')}</Text>
|
||||||
: null}
|
// : null}
|
||||||
</Container>
|
// </FullSizeContainer>
|
||||||
), [searchTerm, jellyfinResults, fuseResults, isLoading]);
|
// ), [searchTerm, jellyfinResults, fuseResults, isLoading]);
|
||||||
|
|
||||||
// GUARD: We cannot search for stuff unless Fuse is loaded with results.
|
// GUARD: We cannot search for stuff unless Fuse is loaded with results.
|
||||||
// Therefore we delay rendering to when we are certain it's there.
|
// Therefore we delay rendering to when we are certain it's there.
|
||||||
@@ -243,9 +263,14 @@ export default function Search() {
|
|||||||
}}
|
}}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
ListHeaderComponent={HeaderComponent}
|
ListHeaderComponent={HeaderComponent}
|
||||||
ListFooterComponent={FooterComponent}
|
// ListFooterComponent={FooterComponent}
|
||||||
extraData={[searchTerm, albums]}
|
extraData={[searchTerm, albums]}
|
||||||
/>
|
/>
|
||||||
|
<FullSizeContainer>
|
||||||
|
{(searchTerm.length && !jellyfinResults.length && !fuseResults.length && !isLoading)
|
||||||
|
? <Text style={{ textAlign: 'center', opacity: 0.5, fontSize: 18 }}>{t('no-results')}</Text>
|
||||||
|
: null}
|
||||||
|
</FullSizeContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user