Add stubs for filters in search
This commit is contained in:
3
src/assets/icons/collection.svg
Normal file
3
src/assets/icons/collection.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="8" height="10" viewBox="0 0 8 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.37245 10H6.29172C6.67206 10 6.96987 9.88698 7.18515 9.66093C7.40044 9.43488 7.50808 9.09581 7.50808 8.64371V3.84285C7.50808 3.39074 7.39416 3.05257 7.16632 2.82831C6.93847 2.60406 6.59491 2.49193 6.13564 2.49193H1.37245C0.916756 2.49193 0.574092 2.60406 0.344454 2.82831C0.114818 3.05257 0 3.39074 0 3.84285V8.64371C0 9.09581 0.114818 9.43488 0.344454 9.66093C0.574092 9.88698 0.916756 10 1.37245 10ZM0.963407 1.83531H6.54467C6.50521 1.61644 6.42806 1.44959 6.31324 1.33477C6.19842 1.21995 6.02081 1.16254 5.78041 1.16254H1.72767C1.48727 1.16254 1.30966 1.21995 1.19484 1.33477C1.08002 1.44959 1.00288 1.61644 0.963407 1.83531ZM1.72228 0.613563H5.7858C5.77144 0.409042 5.70596 0.255651 5.58934 0.153391C5.47273 0.0511304 5.3032 0 5.08074 0H2.42734C2.20488 0 2.03535 0.0511304 1.91874 0.153391C1.80212 0.255651 1.73663 0.409042 1.72228 0.613563Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 962 B |
3
src/assets/icons/microphone.svg
Normal file
3
src/assets/icons/microphone.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.03813 10C5.17148 10 5.28504 9.95416 5.3788 9.86248C5.47256 9.77081 5.51944 9.65621 5.51944 9.51869V5.90574L6.96337 4.56808C7.54261 4.63475 8.08226 4.42014 8.58233 3.92424L5.85698 1.19265C5.61112 1.43851 5.43506 1.7 5.32879 1.97712C5.22253 2.25424 5.18606 2.5324 5.2194 2.81161L0.662578 7.70597C0.570901 7.81015 0.518811 7.92891 0.506309 8.06226C0.493814 8.19561 0.543822 8.31854 0.656331 8.43105L0.050007 9.23115C0.0166691 9.27699 0 9.33012 0 9.39055C0 9.45097 0.0250033 9.50827 0.0750098 9.56245L0.218772 9.70621C0.268783 9.75205 0.324001 9.77706 0.384425 9.78122C0.444848 9.78539 0.500061 9.76664 0.550065 9.72497L1.35016 9.11864C1.45852 9.23115 1.58041 9.28116 1.71584 9.26866C1.85127 9.25616 1.969 9.20198 2.06901 9.10614L4.55057 6.80585V9.51869C4.55057 9.65621 4.59745 9.77081 4.69122 9.86248C4.78498 9.95416 4.90061 10 5.03813 10ZM1.25641 8.14351L5.52569 3.6242C5.5632 3.68671 5.60591 3.74713 5.65383 3.80547C5.70175 3.86381 5.7528 3.92007 5.80698 3.97425C5.85698 4.02842 5.91115 4.07946 5.9695 4.12739C6.02784 4.17531 6.0841 4.21802 6.13827 4.25552L1.6377 8.52482L1.25641 8.14351ZM6.39455 0.648827L9.11989 3.38042C9.40326 3.10122 9.59495 2.79909 9.69497 2.47405C9.79498 2.14901 9.80227 1.82397 9.71684 1.49893C9.63142 1.17389 9.4491 0.875935 9.1699 0.60507C8.89487 0.33004 8.59691 0.14877 8.27604 0.0612579C7.95516 -0.0262539 7.6322 -0.0200025 7.30716 0.0800122C6.98212 0.180018 6.67792 0.369624 6.39455 0.648827Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
3
src/assets/icons/note.svg
Normal file
3
src/assets/icons/note.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="7" height="10" viewBox="0 0 7 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.11802 2.2789V0.406546C6.11802 0.268775 6.07001 0.161274 5.974 0.084044C5.87798 0.00681376 5.76317 -0.0171898 5.62958 0.0120333L3.04962 0.575614C2.70312 0.650758 2.52987 0.83236 2.52987 1.12042V6.63102C2.55909 6.8648 2.46725 7.00048 2.25434 7.03805L1.47158 7.20087C0.97062 7.30941 0.600115 7.48996 0.360063 7.74253C0.120021 7.9951 0 8.31133 0 8.69123C0 9.0753 0.13359 9.38945 0.400769 9.63367C0.667949 9.87789 1.01027 10 1.42774 10C1.66988 10 1.93393 9.93425 2.2199 9.80275C2.50586 9.67124 2.75113 9.45938 2.95569 9.16715C3.16025 8.87492 3.26253 8.48667 3.26253 8.00241V3.46868C3.26253 3.34344 3.27819 3.26204 3.3095 3.22446C3.3408 3.18689 3.41282 3.15767 3.52554 3.13679L5.83623 2.6233C5.9239 2.60661 5.99278 2.56695 6.04288 2.50433C6.09298 2.44171 6.11802 2.36657 6.11802 2.2789Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 898 B |
55
src/screens/Search/components/SelectableFilter.tsx
Normal file
55
src/screens/Search/components/SelectableFilter.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import useDefaultStyles from 'components/Colors';
|
||||
import { Text } from 'components/Typography';
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
import React from 'react';
|
||||
import { SvgProps } from 'react-native-svg';
|
||||
import styled, { css } from 'styled-components/native';
|
||||
|
||||
const Container = styled.TouchableOpacity<{ active?: boolean }>`
|
||||
border-radius: 80px;
|
||||
padding: 6px 12px;
|
||||
margin-right: 2px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Label = styled(Text)<{ active?: boolean }>`
|
||||
margin-left: 6px;
|
||||
opacity: 0.5;
|
||||
|
||||
${(props) => props.active && css`
|
||||
opacity: 1;
|
||||
font-weight: 500;
|
||||
color: ${THEME_COLOR};
|
||||
`}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
icon: React.FC<SvgProps>;
|
||||
text: string;
|
||||
active: boolean;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
function SelectableFilter({
|
||||
icon: Icon,
|
||||
text,
|
||||
active,
|
||||
onPress,
|
||||
}: Props) {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
|
||||
return (
|
||||
<Container
|
||||
style={[defaultStyles.filter, active ? defaultStyles.activeBackground : undefined]}
|
||||
active={active}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Icon width={14} height={14} fill={active ? THEME_COLOR : defaultStyles.textHalfOpacity.color} />
|
||||
<Label active={active}>{text}</Label>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectableFilter;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import Input from 'components/Input';
|
||||
import { ActivityIndicator, SafeAreaView, TextInput, View } from 'react-native';
|
||||
import { ActivityIndicator, SafeAreaView, View } from 'react-native';
|
||||
import styled from 'styled-components/native';
|
||||
import { useTypedSelector } from 'store';
|
||||
import Fuse from 'fuse.js';
|
||||
@@ -9,7 +9,6 @@ import { FlatList } from 'react-native-gesture-handler';
|
||||
import TouchableHandler from 'components/TouchableHandler';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useGetImage } from 'utility/JellyfinApi';
|
||||
import { MusicNavigationProp } from '../types';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { t } from '@localisation';
|
||||
import useDefaultStyles from 'components/Colors';
|
||||
@@ -17,23 +16,33 @@ import { searchAndFetchAlbums } from 'store/music/actions';
|
||||
import { debounce } from 'lodash';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Text } from 'components/Typography';
|
||||
import { MusicNavigationProp } from 'screens/Music/types';
|
||||
import DownloadIcon from 'components/DownloadIcon';
|
||||
import ChevronRight from 'assets/icons/chevron-right.svg';
|
||||
import SearchIcon from 'assets/icons/magnifying-glass.svg';
|
||||
|
||||
import { ShadowWrapper } from 'components/Shadow';
|
||||
// import MicrophoneIcon from 'assets/icons/microphone.svg';
|
||||
// import AlbumIcon from 'assets/icons/collection.svg';
|
||||
// import TrackIcon from 'assets/icons/note.svg';
|
||||
// import PlaylistIcon from 'assets/icons/note-list.svg';
|
||||
// import StreamIcon from 'assets/icons/cloud.svg';
|
||||
// import LocalIcon from 'assets/icons/internal-drive.svg';
|
||||
// import SelectableFilter from './components/SelectableFilter';
|
||||
|
||||
const Container = styled.View`
|
||||
padding: 0 32px;
|
||||
position: relative;
|
||||
padding: 4px 32px 0 32px;
|
||||
margin-bottom: 0px;
|
||||
padding-bottom: 0px;
|
||||
border-top-width: 1px;
|
||||
`;
|
||||
|
||||
const FullSizeContainer = styled(Container)`
|
||||
const FullSizeContainer = styled.View`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const Loading = styled.View`
|
||||
position: absolute;
|
||||
right: 32px;
|
||||
right: 12px;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
@@ -61,10 +70,18 @@ const SearchResult = styled.View`
|
||||
height: 54px;
|
||||
`;
|
||||
|
||||
const fuseOptions = {
|
||||
const SearchIndicator = styled(SearchIcon)`
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 26px;
|
||||
`;
|
||||
|
||||
|
||||
const fuseOptions: Fuse.IFuseOptions<Album> = {
|
||||
keys: ['Name', 'AlbumArtist', 'AlbumArtists', 'Artists'],
|
||||
threshold: 0.1,
|
||||
includeScore: true,
|
||||
fieldNormWeight: 1,
|
||||
};
|
||||
|
||||
type AudioResult = {
|
||||
@@ -86,17 +103,14 @@ type CombinedResults = (AudioResult | AlbumResult)[];
|
||||
export default function Search() {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
|
||||
// Prepare state
|
||||
// Prepare state for fuse and albums
|
||||
const [fuseIsReady, setFuseReady] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const [fuseResults, setFuseResults] = useState<CombinedResults>([]);
|
||||
const [jellyfinResults, setJellyfinResults] = useState<CombinedResults>([]);
|
||||
|
||||
const albums = useTypedSelector(state => state.music.albums.entities);
|
||||
|
||||
const fuse = useRef<Fuse<Album>>();
|
||||
const searchElement = useRef<TextInput>(null);
|
||||
|
||||
// Prepare helpers
|
||||
const navigation = useNavigation<MusicNavigationProp>();
|
||||
@@ -197,40 +211,62 @@ export default function Search() {
|
||||
retrieveResults();
|
||||
}, [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
|
||||
const selectAlbum = useCallback((id: string) =>
|
||||
navigation.navigate('Album', { id, album: albums[id] as Album }), [navigation, albums]
|
||||
);
|
||||
|
||||
const HeaderComponent = React.useMemo(() => (
|
||||
<Container>
|
||||
<Input
|
||||
ref={searchElement}
|
||||
value={searchTerm}
|
||||
onChangeText={setSearchTerm}
|
||||
style={defaultStyles.input}
|
||||
placeholder={t('search') + '...'}
|
||||
/>
|
||||
<SearchIcon width={14} height={14} fill={defaultStyles.textHalfOpacity.color} style={{ position: 'absolute', left: 48, top: 26}} />
|
||||
{isLoading && <Loading><ActivityIndicator /></Loading>}
|
||||
</Container>
|
||||
<View>
|
||||
<Container style={defaultStyles.border}>
|
||||
<View>
|
||||
<Input
|
||||
value={searchTerm}
|
||||
onChangeText={setSearchTerm}
|
||||
style={[defaultStyles.input, { marginBottom: 12 }]}
|
||||
placeholder={t('search') + '...'}
|
||||
/>
|
||||
<SearchIndicator width={14} height={14} fill={defaultStyles.textHalfOpacity.color} />
|
||||
{isLoading && <Loading><ActivityIndicator /></Loading>}
|
||||
</View>
|
||||
</Container>
|
||||
{/* <ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||
<View style={{ paddingHorizontal: 32, paddingBottom: 12, flex: 1, flexDirection: 'row' }}>
|
||||
<SelectableFilter
|
||||
text="Artists"
|
||||
icon={MicrophoneIcon}
|
||||
active
|
||||
/>
|
||||
<SelectableFilter
|
||||
text="Albums"
|
||||
icon={AlbumIcon}
|
||||
active={false}
|
||||
/>
|
||||
<SelectableFilter
|
||||
text="Tracks"
|
||||
icon={TrackIcon}
|
||||
active={false}
|
||||
/>
|
||||
<SelectableFilter
|
||||
text="Playlist"
|
||||
icon={PlaylistIcon}
|
||||
active={false}
|
||||
/>
|
||||
<SelectableFilter
|
||||
text="Streaming"
|
||||
icon={StreamIcon}
|
||||
active={false}
|
||||
/>
|
||||
<SelectableFilter
|
||||
text="Local Playback"
|
||||
icon={LocalIcon}
|
||||
active={false}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView> */}
|
||||
</View>
|
||||
), [searchTerm, setSearchTerm, defaultStyles, isLoading]);
|
||||
|
||||
// const FooterComponent = React.useMemo(() => (
|
||||
// <FullSizeContainer>
|
||||
// {(searchTerm.length && !jellyfinResults.length && !fuseResults.length && !isLoading)
|
||||
// ? <Text style={{ textAlign: 'center', opacity: 0.5 }}>{t('no-results')}</Text>
|
||||
// : null}
|
||||
// </FullSizeContainer>
|
||||
// ), [searchTerm, jellyfinResults, fuseResults, isLoading]);
|
||||
|
||||
// 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 (!fuseIsReady) {
|
||||
@@ -240,7 +276,7 @@ export default function Search() {
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<FlatList
|
||||
style={{ flex: 1 }}
|
||||
style={{ flex: 2 }}
|
||||
data={[...jellyfinResults, ...fuseResults]}
|
||||
renderItem={({ item: { id, type, album: trackAlbum, name: trackName } }: { item: AlbumResult | AudioResult }) => {
|
||||
const album = albums[trackAlbum || id];
|
||||
@@ -254,7 +290,9 @@ export default function Search() {
|
||||
return (
|
||||
<TouchableHandler<string> id={album.Id} onPress={selectAlbum}>
|
||||
<SearchResult>
|
||||
<AlbumImage source={{ uri: getImage(album.Id) }} />
|
||||
<ShadowWrapper>
|
||||
<AlbumImage source={{ uri: getImage(album.Id) }} style={defaultStyles.imageBackground} />
|
||||
</ShadowWrapper>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text numberOfLines={1}>
|
||||
{trackName || album.Name}
|
||||
@@ -277,8 +315,6 @@ export default function Search() {
|
||||
);
|
||||
}}
|
||||
keyExtractor={(item) => item.id}
|
||||
ListHeaderComponent={HeaderComponent}
|
||||
// ListFooterComponent={FooterComponent}
|
||||
extraData={[searchTerm, albums]}
|
||||
/>
|
||||
{(searchTerm.length && !jellyfinResults.length && !fuseResults.length && !isLoading) ? (
|
||||
@@ -286,6 +322,7 @@ export default function Search() {
|
||||
<Text style={{ textAlign: 'center', opacity: 0.5, fontSize: 18 }}>{t('no-results')}</Text>
|
||||
</FullSizeContainer>
|
||||
) : null}
|
||||
{HeaderComponent}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user