diff --git a/src/screens/Music/stacks/RecentAlbums.tsx b/src/screens/Music/stacks/RecentAlbums.tsx index 117f8f7..cb965f3 100644 --- a/src/screens/Music/stacks/RecentAlbums.tsx +++ b/src/screens/Music/stacks/RecentAlbums.tsx @@ -45,7 +45,7 @@ const RecentAlbums: React.FC = () => { console.log(recentAlbums.map((d) => albums[d]?.DateCreated)); // Retrieve data on mount - useEffect(() => { retrieveData(); }, []); + useEffect(() => { retrieveData(); }, [retrieveData]); return ( diff --git a/src/screens/Music/stacks/Search.tsx b/src/screens/Music/stacks/Search.tsx index d03f776..8a25b59 100644 --- a/src/screens/Music/stacks/Search.tsx +++ b/src/screens/Music/stacks/Search.tsx @@ -1,38 +1,124 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import Input from 'components/Input'; -import { Text } from 'react-native'; +import { Text, View } from 'react-native'; import styled from 'styled-components/native'; import { useTypedSelector } from 'store'; import Fuse from 'fuse.js'; +import { Album } from 'store/music/types'; +import { FlatList } from 'react-native-gesture-handler'; +import TouchableHandler from 'components/TouchableHandler'; +import { useNavigation } from '@react-navigation/native'; +import { useGetImage } from 'utility/JellyfinApi'; +import { NavigationProp } from '../types'; +import FastImage from 'react-native-fast-image'; const Container = styled.View` padding: 0 20px; `; +const AlbumImage = styled(FastImage)` + border-radius: 4px; + width: 25px; + height: 25px; + background-color: #fefefe; + margin-right: 10px; +`; + +const HalfOpacity = styled.Text` + opacity: 0.5; + margin-top: 2px; + font-size: 12px; +`; + +const SearchResult = styled.View` + flex-direction: row; + align-items: center; + border-bottom-color: #ddd; + border-bottom-width: 1px; + margin-left: 15px; + padding-right: 15px; + height: 50px; +`; + +const fuseOptions = { + keys: ['Name', 'AlbumArtist', 'AlbumArtists', 'Artists'], + threshold: 0.1, + includeScore: true, +}; + export default function Search() { + // Prepare state const [searchTerm, setSearchTerm] = useState(''); const albums = useTypedSelector(state => state.music.albums.entities); - const [results, setResults] = useState([]); - let fuse: Fuse<{}, {}>; + const [results, setResults] = useState[]>([]); + let fuse = useRef>(); + // Prepare helpers + const navigation = useNavigation(); + const getImage = useGetImage(); + + /** + * Since it is impractical to have a global fuse variable, we need to + * instantiate it for thsi function. With this effect, we generate a new + * Fuse instance every time the albums change. This can of course be done + * more intelligently by removing and adding the changed albums, but this is + * an open todo. + */ useEffect(() => { - fuse = new Fuse(Object.values(albums), { - keys: ['Name', 'AlbumArtist', 'AlbumArtists', 'Artists'], - threshold: 0.1, - includeScore: true, - }); + fuse.current = new Fuse(Object.values(albums) as Album[], fuseOptions); }, [albums]); + /** + * Whenever the search term changes, we gather results from Fuse and assign + * them to state + */ useEffect(() => { - setResults(fuse.search(searchTerm)); + // 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; + } + + setResults(fuse.current.search(searchTerm)); }, [searchTerm, setResults, fuse]); - return ( + // Handlers + const selectAlbum = useCallback((id: string) => + navigation.navigate('Album', { id, album: albums[id] as Album }), [navigation, albums] + ); + + const HeaderComponent = React.useMemo(() => ( - {results.map((result) => ( - {result.item.Name} - {result.item.AlbumArtist} - ))} + {(searchTerm.length && !results.length) ? No results... : null} + ), [searchTerm, results, setSearchTerm]); + + // GUARD: We cannot search for stuff unless Fuse is loaded with results. + // Therefore we delay rendering to when we are certain it's there. + if (!fuse.current) { + return null; + } + + return ( + ( + + + + + + {album.Name} - {album.AlbumArtist} + + Album + + + + )} + keyExtractor={(item) => item.refIndex.toString()} + ListHeaderComponent={HeaderComponent} + extraData={searchTerm} + /> ); } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index b3389a6..4507940 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ /* Basic Options */ "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - "lib": ["es6"], /* Specify library files to be included in the compilation. */ + "lib": ["esnext"], /* Specify library files to be included in the compilation. */ "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ "jsx": "react-native", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */